| import 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 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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) { |
| |
| |
| |
| 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.`, |
| }, |
| } |
| } |
|
|
| |
| |
| |
| 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'] } |
|
|
| |
| |
| |
| 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 }, |
| ) |
|
|
| |
| |
| |
| 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 { |
| |
| |
| 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> |
| } |
|
|