forked from ultraworkers/claw-code
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathheadersHelper.ts
More file actions
138 lines (129 loc) · 4.61 KB
/
Copy pathheadersHelper.ts
File metadata and controls
138 lines (129 loc) · 4.61 KB
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
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,
}
}