the claude code sourcemaps leaked march 31
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 215 lines 7.9 kB view raw
1import reject from 'lodash-es/reject.js' 2import { z } from 'zod/v4' 3import { performMCPOAuthFlow } from '../../services/mcp/auth.js' 4import { 5 clearMcpAuthCache, 6 reconnectMcpServerImpl, 7} from '../../services/mcp/client.js' 8import { 9 buildMcpToolName, 10 getMcpPrefix, 11} from '../../services/mcp/mcpStringUtils.js' 12import type { 13 McpHTTPServerConfig, 14 McpSSEServerConfig, 15 ScopedMcpServerConfig, 16} from '../../services/mcp/types.js' 17import type { Tool } from '../../Tool.js' 18import { errorMessage } from '../../utils/errors.js' 19import { lazySchema } from '../../utils/lazySchema.js' 20import { logMCPDebug, logMCPError } from '../../utils/log.js' 21import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js' 22 23const inputSchema = lazySchema(() => z.object({})) 24type InputSchema = ReturnType<typeof inputSchema> 25 26export type McpAuthOutput = { 27 status: 'auth_url' | 'unsupported' | 'error' 28 message: string 29 authUrl?: string 30} 31 32function getConfigUrl(config: ScopedMcpServerConfig): string | undefined { 33 if ('url' in config) return config.url 34 return undefined 35} 36 37/** 38 * Creates a pseudo-tool for an MCP server that is installed but not 39 * authenticated. Surfaced in place of the server's real tools so the model 40 * knows the server exists and can start the OAuth flow on the user's behalf. 41 * 42 * When called, starts performMCPOAuthFlow with skipBrowserOpen and returns 43 * the authorization URL. The OAuth callback completes in the background; 44 * once it fires, reconnectMcpServerImpl runs and the server's real tools 45 * are swapped into appState.mcp.tools via the existing prefix-based 46 * replacement (useManageMCPConnections.updateServer wipes anything matching 47 * mcp__<server>__*, so this pseudo-tool is removed automatically). 48 */ 49export function createMcpAuthTool( 50 serverName: string, 51 config: ScopedMcpServerConfig, 52): Tool<InputSchema, McpAuthOutput> { 53 const url = getConfigUrl(config) 54 const transport = config.type ?? 'stdio' 55 const location = url ? `${transport} at ${url}` : transport 56 57 const description = 58 `The \`${serverName}\` MCP server (${location}) is installed but requires authentication. ` + 59 `Call this tool to start the OAuth flow — you'll receive an authorization URL to share with the user. ` + 60 `Once the user completes authorization in their browser, the server's real tools will become available automatically.` 61 62 return { 63 name: buildMcpToolName(serverName, 'authenticate'), 64 isMcp: true, 65 mcpInfo: { serverName, toolName: 'authenticate' }, 66 isEnabled: () => true, 67 isConcurrencySafe: () => false, 68 isReadOnly: () => false, 69 toAutoClassifierInput: () => serverName, 70 userFacingName: () => `${serverName} - authenticate (MCP)`, 71 maxResultSizeChars: 10_000, 72 renderToolUseMessage: () => `Authenticate ${serverName} MCP server`, 73 async description() { 74 return description 75 }, 76 async prompt() { 77 return description 78 }, 79 get inputSchema(): InputSchema { 80 return inputSchema() 81 }, 82 async checkPermissions(input): Promise<PermissionDecision> { 83 return { behavior: 'allow', updatedInput: input } 84 }, 85 async call(_input, context) { 86 // claude.ai connectors use a separate auth flow (handleClaudeAIAuth in 87 // MCPRemoteServerMenu) that we don't invoke programmatically here — 88 // just point the user at /mcp. 89 if (config.type === 'claudeai-proxy') { 90 return { 91 data: { 92 status: 'unsupported' as const, 93 message: `This is a claude.ai MCP connector. Ask the user to run /mcp and select "${serverName}" to authenticate.`, 94 }, 95 } 96 } 97 98 // performMCPOAuthFlow only accepts sse/http. needs-auth state is only 99 // set on HTTP 401 (UnauthorizedError) so other transports shouldn't 100 // reach here, but be defensive. 101 if (config.type !== 'sse' && config.type !== 'http') { 102 return { 103 data: { 104 status: 'unsupported' as const, 105 message: `Server "${serverName}" uses ${transport} transport which does not support OAuth from this tool. Ask the user to run /mcp and authenticate manually.`, 106 }, 107 } 108 } 109 110 const sseOrHttpConfig = config as ( 111 | McpSSEServerConfig 112 | McpHTTPServerConfig 113 ) & { scope: ScopedMcpServerConfig['scope'] } 114 115 // Mirror cli/print.ts mcp_authenticate: start the flow, capture the 116 // URL via onAuthorizationUrl, return it immediately. The flow's 117 // Promise resolves later when the browser callback fires. 118 let resolveAuthUrl: ((url: string) => void) | undefined 119 const authUrlPromise = new Promise<string>(resolve => { 120 resolveAuthUrl = resolve 121 }) 122 123 const controller = new AbortController() 124 const { setAppState } = context 125 126 const oauthPromise = performMCPOAuthFlow( 127 serverName, 128 sseOrHttpConfig, 129 u => resolveAuthUrl?.(u), 130 controller.signal, 131 { skipBrowserOpen: true }, 132 ) 133 134 // Background continuation: once OAuth completes, reconnect and swap 135 // the real tools into appState. Prefix-based replacement removes this 136 // pseudo-tool since it shares the mcp__<server>__ prefix. 137 void oauthPromise 138 .then(async () => { 139 clearMcpAuthCache() 140 const result = await reconnectMcpServerImpl(serverName, config) 141 const prefix = getMcpPrefix(serverName) 142 setAppState(prev => ({ 143 ...prev, 144 mcp: { 145 ...prev.mcp, 146 clients: prev.mcp.clients.map(c => 147 c.name === serverName ? result.client : c, 148 ), 149 tools: [ 150 ...reject(prev.mcp.tools, t => t.name?.startsWith(prefix)), 151 ...result.tools, 152 ], 153 commands: [ 154 ...reject(prev.mcp.commands, c => c.name?.startsWith(prefix)), 155 ...result.commands, 156 ], 157 resources: result.resources 158 ? { ...prev.mcp.resources, [serverName]: result.resources } 159 : prev.mcp.resources, 160 }, 161 })) 162 logMCPDebug( 163 serverName, 164 `OAuth complete, reconnected with ${result.tools.length} tool(s)`, 165 ) 166 }) 167 .catch(err => { 168 logMCPError( 169 serverName, 170 `OAuth flow failed after tool-triggered start: ${errorMessage(err)}`, 171 ) 172 }) 173 174 try { 175 // Race: get the URL, or the flow completes without needing one 176 // (e.g. XAA with cached IdP token — silent auth). 177 const authUrl = await Promise.race([ 178 authUrlPromise, 179 oauthPromise.then(() => null as string | null), 180 ]) 181 182 if (authUrl) { 183 return { 184 data: { 185 status: 'auth_url' as const, 186 authUrl, 187 message: `Ask the user to open this URL in their browser to authorize the ${serverName} MCP server:\n\n${authUrl}\n\nOnce they complete the flow, the server's tools will become available automatically.`, 188 }, 189 } 190 } 191 192 return { 193 data: { 194 status: 'auth_url' as const, 195 message: `Authentication completed silently for ${serverName}. The server's tools should now be available.`, 196 }, 197 } 198 } catch (err) { 199 return { 200 data: { 201 status: 'error' as const, 202 message: `Failed to start OAuth flow for ${serverName}: ${errorMessage(err)}. Ask the user to run /mcp and authenticate manually.`, 203 }, 204 } 205 } 206 }, 207 mapToolResultToToolResultBlockParam(data, toolUseID) { 208 return { 209 tool_use_id: toolUseID, 210 type: 'tool_result', 211 content: data.message, 212 } 213 }, 214 } satisfies Tool<InputSchema, McpAuthOutput> 215}