Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
7.9 kB · 215 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216import reject from 'lodash-es/reject.js'import { z } from 'zod/v4'import { performMCPOAuthFlow } from '../../services/mcp/auth.js'import { clearMcpAuthCache, reconnectMcpServerImpl,} from '../../services/mcp/client.js'import { buildMcpToolName, getMcpPrefix,} from '../../services/mcp/mcpStringUtils.js'import type { McpHTTPServerConfig, McpSSEServerConfig, ScopedMcpServerConfig,} from '../../services/mcp/types.js'import type { Tool } from '../../Tool.js'import { errorMessage } from '../../utils/errors.js'import { lazySchema } from '../../utils/lazySchema.js'import { logMCPDebug, logMCPError } from '../../utils/log.js'import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
const inputSchema = lazySchema(() => z.object({}))type InputSchema = ReturnType<typeof inputSchema>
export type McpAuthOutput = { status: 'auth_url' | 'unsupported' | 'error' message: string authUrl?: string}
function getConfigUrl(config: ScopedMcpServerConfig): string | undefined { if ('url' in config) return config.url return undefined}
/** * Creates a pseudo-tool for an MCP server that is installed but not * authenticated. Surfaced in place of the server's real tools so the model * knows the server exists and can start the OAuth flow on the user's behalf. * * When called, starts performMCPOAuthFlow with skipBrowserOpen and returns * the authorization URL. The OAuth callback completes in the background; * once it fires, reconnectMcpServerImpl runs and the server's real tools * are swapped into appState.mcp.tools via the existing prefix-based * replacement (useManageMCPConnections.updateServer wipes anything matching * mcp__<server>__*, so this pseudo-tool is removed automatically). */export function createMcpAuthTool( serverName: string, config: ScopedMcpServerConfig,): Tool<InputSchema, McpAuthOutput> { const url = getConfigUrl(config) const transport = config.type ?? 'stdio' const location = url ? `${transport} at ${url}` : transport
const description = `The \`${serverName}\` MCP server (${location}) is installed but requires authentication. ` + `Call this tool to start the OAuth flow — you'll receive an authorization URL to share with the user. ` + `Once the user completes authorization in their browser, the server's real tools will become available automatically.`
return { name: buildMcpToolName(serverName, 'authenticate'), isMcp: true, mcpInfo: { serverName, toolName: 'authenticate' }, isEnabled: () => true, isConcurrencySafe: () => false, isReadOnly: () => false, toAutoClassifierInput: () => serverName, userFacingName: () => `${serverName} - authenticate (MCP)`, maxResultSizeChars: 10_000, renderToolUseMessage: () => `Authenticate ${serverName} MCP server`, async description() { return description }, async prompt() { return description }, get inputSchema(): InputSchema { return inputSchema() }, async checkPermissions(input): Promise<PermissionDecision> { return { behavior: 'allow', updatedInput: input } }, async call(_input, context) { // claude.ai connectors use a separate auth flow (handleClaudeAIAuth in // MCPRemoteServerMenu) that we don't invoke programmatically here — // just point the user at /mcp. if (config.type === 'claudeai-proxy') { return { data: { status: 'unsupported' as const, message: `This is a claude.ai MCP connector. Ask the user to run /mcp and select "${serverName}" to authenticate.`, }, } }
// performMCPOAuthFlow only accepts sse/http. needs-auth state is only // set on HTTP 401 (UnauthorizedError) so other transports shouldn't // reach here, but be defensive. if (config.type !== 'sse' && config.type !== 'http') { return { data: { status: 'unsupported' as const, message: `Server "${serverName}" uses ${transport} transport which does not support OAuth from this tool. Ask the user to run /mcp and authenticate manually.`, }, } }
const sseOrHttpConfig = config as ( | McpSSEServerConfig | McpHTTPServerConfig ) & { scope: ScopedMcpServerConfig['scope'] }
// Mirror cli/print.ts mcp_authenticate: start the flow, capture the // URL via onAuthorizationUrl, return it immediately. The flow's // Promise resolves later when the browser callback fires. let resolveAuthUrl: ((url: string) => void) | undefined const authUrlPromise = new Promise<string>(resolve => { resolveAuthUrl = resolve })
const controller = new AbortController() const { setAppState } = context
const oauthPromise = performMCPOAuthFlow( serverName, sseOrHttpConfig, u => resolveAuthUrl?.(u), controller.signal, { skipBrowserOpen: true }, )
// Background continuation: once OAuth completes, reconnect and swap // the real tools into appState. Prefix-based replacement removes this // pseudo-tool since it shares the mcp__<server>__ prefix. void oauthPromise .then(async () => { clearMcpAuthCache() const result = await reconnectMcpServerImpl(serverName, config) const prefix = getMcpPrefix(serverName) setAppState(prev => ({ ...prev, mcp: { ...prev.mcp, clients: prev.mcp.clients.map(c => c.name === serverName ? result.client : c, ), tools: [ ...reject(prev.mcp.tools, t => t.name?.startsWith(prefix)), ...result.tools, ], commands: [ ...reject(prev.mcp.commands, c => c.name?.startsWith(prefix)), ...result.commands, ], resources: result.resources ? { ...prev.mcp.resources, [serverName]: result.resources } : prev.mcp.resources, }, })) logMCPDebug( serverName, `OAuth complete, reconnected with ${result.tools.length} tool(s)`, ) }) .catch(err => { logMCPError( serverName, `OAuth flow failed after tool-triggered start: ${errorMessage(err)}`, ) })
try { // Race: get the URL, or the flow completes without needing one // (e.g. XAA with cached IdP token — silent auth). const authUrl = await Promise.race([ authUrlPromise, oauthPromise.then(() => null as string | null), ])
if (authUrl) { return { data: { status: 'auth_url' as const, authUrl, 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.`, }, } }
return { data: { status: 'auth_url' as const, message: `Authentication completed silently for ${serverName}. The server's tools should now be available.`, }, } } catch (err) { return { data: { status: 'error' as const, message: `Failed to start OAuth flow for ${serverName}: ${errorMessage(err)}. Ask the user to run /mcp and authenticate manually.`, }, } } }, mapToolResultToToolResultBlockParam(data, toolUseID) { return { tool_use_id: toolUseID, type: 'tool_result', content: data.message, } }, } satisfies Tool<InputSchema, McpAuthOutput>}