File size: 4,718 Bytes
064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | 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<Record<string, string> | 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<string, string>
} 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<Record<string, string>> {
const staticHeaders = config.headers || {}
const dynamicHeaders =
(await getMcpHeadersFromHelper(serverName, config)) || {}
// Dynamic headers override static headers if both are present
return {
...staticHeaders,
...dynamicHeaders,
}
}
|