import { getIsNonInteractiveSession } from '../../bootstrap/state.js' import { checkHasTrustDialogAccepted } from '../../utils/config.js' import { logAntError } from '../../utils/debug.js' import { errorMessage } from '../../utils/errors.js' import { execFileNoThrowWithCwd } from '../../utils/execFileNoThrow.js' import { logError, logMCPDebug, logMCPError } from '../../utils/log.js' import { jsonParse } from '../../utils/slowOperations.js' import { logEvent } from '../analytics/index.js' import type { McpHTTPServerConfig, McpSSEServerConfig, McpWebSocketServerConfig, ScopedMcpServerConfig, } from './types.js' /** * Check if the MCP server config comes from project settings (projectSettings or localSettings) * This is important for security checks */ function isMcpServerFromProjectOrLocalSettings( config: ScopedMcpServerConfig, ): boolean { return config.scope === 'project' || config.scope === 'local' } /** * Get dynamic headers for an MCP server using the headersHelper script * @param serverName The name of the MCP server * @param config The MCP server configuration * @returns Headers object or null if not configured or failed */ export async function getMcpHeadersFromHelper( serverName: string, config: McpSSEServerConfig | McpHTTPServerConfig | McpWebSocketServerConfig, ): Promise | null> { if (!config.headersHelper) { return null } // Security check for project/local settings // Skip trust check in non-interactive mode (e.g., CI/CD, automation) if ( 'scope' in config && isMcpServerFromProjectOrLocalSettings(config as ScopedMcpServerConfig) && !getIsNonInteractiveSession() ) { // Check if trust has been established for this project const hasTrust = checkHasTrustDialogAccepted() if (!hasTrust) { const error = new Error( `Security: headersHelper for MCP server '${serverName}' executed before workspace trust is confirmed. If you see this message, post in ${MACRO.FEEDBACK_CHANNEL}.`, ) logAntError('MCP headersHelper invoked before trust check', error) logEvent('tengu_mcp_headersHelper_missing_trust', {}) return null } } try { logMCPDebug(serverName, 'Executing headersHelper to get dynamic headers') const execResult = await execFileNoThrowWithCwd(config.headersHelper, [], { shell: true, timeout: 10000, // Pass server context so one helper script can serve multiple MCP servers // (git credential-helper style). See deshaw/anthropic-issues#28. env: { ...process.env, CLAUDE_CODE_MCP_SERVER_NAME: serverName, CLAUDE_CODE_MCP_SERVER_URL: config.url, }, }) if (execResult.code !== 0 || !execResult.stdout) { throw new Error( `headersHelper for MCP server '${serverName}' did not return a valid value`, ) } const result = execResult.stdout.trim() const headers = jsonParse(result) if ( typeof headers !== 'object' || headers === null || Array.isArray(headers) ) { throw new Error( `headersHelper for MCP server '${serverName}' must return a JSON object with string key-value pairs`, ) } // Validate all values are strings for (const [key, value] of Object.entries(headers)) { if (typeof value !== 'string') { throw new Error( `headersHelper for MCP server '${serverName}' returned non-string value for key "${key}": ${typeof value}`, ) } } logMCPDebug( serverName, `Successfully retrieved ${Object.keys(headers).length} headers from headersHelper`, ) return headers as Record } catch (error) { logMCPError( serverName, `Error getting headers from headersHelper: ${errorMessage(error)}`, ) logError( new Error( `Error getting MCP headers from headersHelper for server '${serverName}': ${errorMessage(error)}`, ), ) // Return null instead of throwing to avoid blocking the connection return null } } /** * Get combined headers for an MCP server (static + dynamic) * @param serverName The name of the MCP server * @param config The MCP server configuration * @returns Combined headers object */ export async function getMcpServerHeaders( serverName: string, config: McpSSEServerConfig | McpHTTPServerConfig | McpWebSocketServerConfig, ): Promise> { const staticHeaders = config.headers || {} const dynamicHeaders = (await getMcpHeadersFromHelper(serverName, config)) || {} // Dynamic headers override static headers if both are present return { ...staticHeaders, ...dynamicHeaders, } }