chore: initialize recovered claude workspace
This commit is contained in:
215
src/tools/McpAuthTool/McpAuthTool.ts
Normal file
215
src/tools/McpAuthTool/McpAuthTool.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
}
|
||||
Reference in New Issue
Block a user