init claude-code
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user