@mcp-z/client 1.0.3 â 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/auth/capability-discovery.js +6 -4
- package/dist/cjs/auth/capability-discovery.js.map +1 -1
- package/dist/cjs/auth/rfc9728-discovery.d.cts +3 -0
- package/dist/cjs/auth/rfc9728-discovery.d.ts +3 -0
- package/dist/cjs/auth/rfc9728-discovery.js +140 -63
- package/dist/cjs/auth/rfc9728-discovery.js.map +1 -1
- package/dist/cjs/connection/connect-client.d.cts +9 -0
- package/dist/cjs/connection/connect-client.d.ts +9 -0
- package/dist/cjs/connection/connect-client.js +26 -9
- package/dist/cjs/connection/connect-client.js.map +1 -1
- package/dist/cjs/lib/url-utils.d.cts +2 -0
- package/dist/cjs/lib/url-utils.d.ts +2 -0
- package/dist/cjs/lib/url-utils.js +33 -0
- package/dist/cjs/lib/url-utils.js.map +1 -0
- package/dist/esm/auth/capability-discovery.js +6 -4
- package/dist/esm/auth/capability-discovery.js.map +1 -1
- package/dist/esm/auth/rfc9728-discovery.d.ts +3 -0
- package/dist/esm/auth/rfc9728-discovery.js +39 -7
- package/dist/esm/auth/rfc9728-discovery.js.map +1 -1
- package/dist/esm/connection/connect-client.d.ts +9 -0
- package/dist/esm/connection/connect-client.js +21 -5
- package/dist/esm/connection/connect-client.js.map +1 -1
- package/dist/esm/lib/url-utils.d.ts +2 -0
- package/dist/esm/lib/url-utils.js +14 -0
- package/dist/esm/lib/url-utils.js.map +1 -0
- package/package.json +11 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/connection/connect-client.ts"],"sourcesContent":["/**\n * connect-mcp-client.ts\n *\n * Helper to connect MCP SDK clients to servers with intelligent transport inference.\n * Automatically detects transport type from URL protocol or type field.\n */\n\nimport '../monkey-patches.ts';\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport getPort from 'get-port';\nimport { probeAuthCapabilities } from '../auth/index.ts';\nimport { DcrAuthenticator, type DcrAuthenticatorOptions } from '../dcr/index.ts';\nimport type { ServerProcess } from '../spawn/spawn-server.ts';\nimport type { ServersConfig } from '../spawn/spawn-servers.ts';\n\n/**\n * Minimal interface for connecting to servers.\n * Only needs config and servers map for connection logic.\n */\ninterface RegistryLike {\n config: ServersConfig;\n servers: Map<string, ServerProcess>;\n}\n\nimport type { McpServerEntry, TransportType } from '../types.ts';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { ExistingProcessTransport } from './existing-process-transport.ts';\nimport { waitForHttpReady } from './wait-for-http-ready.ts';\n\n/**\n * Wrap promise with timeout - throws if promise takes too long\n * Clears timeout when promise completes to prevent hanging event loop\n * @param promise - Promise to wrap\n * @param ms - Timeout in milliseconds\n * @param operation - Description of operation for error message\n * @returns Promise result or timeout error\n */\nasync function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {\n let timeoutId: NodeJS.Timeout;\n\n return Promise.race([\n promise.finally(() => clearTimeout(timeoutId)),\n new Promise<T>((_, reject) => {\n timeoutId = setTimeout(() => reject(new Error(`Timeout after ${ms}ms: ${operation}`)), ms);\n }),\n ]);\n}\n\n/**\n * Extract base URL from MCP server URL\n * @param mcpUrl - Full MCP endpoint URL (e.g., https://example.com/mcp)\n * @returns Base URL (e.g., https://example.com)\n */\nfunction extractBaseUrl(mcpUrl: string): string {\n const url = new URL(mcpUrl);\n return `${url.protocol}//${url.host}`;\n}\n\n/**\n * Infer transport type from server configuration with validation.\n *\n * Priority:\n * 1. Explicit type field (if present)\n * 2. URL protocol (if URL present): http://, https://\n * 3. Default to 'stdio' (if neither present)\n *\n * @param config - Server configuration\n * @returns Transport type\n * @throws Error if configuration is invalid or has conflicts\n */\nfunction inferTransportType(config: McpServerEntry): TransportType {\n // Priority 1: Explicit type field\n if (config.type) {\n // Validate consistency with URL if both present\n if (config.url) {\n const url = new URL(config.url);\n const protocol = url.protocol;\n\n if ((protocol === 'http:' || protocol === 'https:') && config.type !== 'http' && config.type !== 'sse-ide') {\n throw new Error(`Conflicting transport: URL protocol '${protocol}' requires type 'http', but got '${config.type}'`);\n }\n }\n\n // Return normalized type\n if (config.type === 'http' || config.type === 'sse-ide') return 'http';\n if (config.type === 'stdio') return 'stdio';\n\n throw new Error(`Unsupported transport type: ${config.type}`);\n }\n\n // Priority 2: Infer from URL protocol\n if (config.url) {\n const url = new URL(config.url);\n const protocol = url.protocol;\n\n if (protocol === 'http:' || protocol === 'https:') {\n return 'http';\n }\n throw new Error(`Unsupported URL protocol: ${protocol}`);\n }\n\n // Priority 3: Default to stdio\n return 'stdio';\n}\n\n/**\n * Connect MCP SDK client to server with full readiness handling.\n * @internal - Use registry.connect() instead\n *\n * **Completely handles readiness**: transport availability + MCP protocol handshake.\n *\n * Transport is intelligently inferred and handled:\n * - **Stdio servers**: Direct MCP connect (fast for spawned processes)\n * - **HTTP servers**: Transport polling (/mcp endpoint) + MCP connect\n * - **Registry result**: Handles both spawned and external servers\n *\n * Returns only when server is fully MCP-ready (initialize handshake complete).\n *\n * @param registryOrConfig - Result from createServerRegistry() or servers config object\n * @param serverName - Server name from servers config\n * @returns Connected MCP SDK Client (guaranteed ready)\n *\n * @example\n * // Using registry (recommended)\n * const registry = createServerRegistry({ echo: { command: 'node', args: ['server.ts'] } });\n * const client = await registry.connect('echo');\n * // Server is fully ready - transport available + MCP handshake complete\n *\n * @example\n * // HTTP server readiness (waits for /mcp polling + MCP handshake)\n * const registry = createServerRegistry(\n * { http: { type: 'http', url: 'http://localhost:3000/mcp', start: {...} } },\n * { dialects: ['start'] }\n * );\n * const client = await registry.connect('http');\n * // 1. Waits for HTTP server to respond on /mcp\n * // 2. Performs MCP initialize handshake\n * // 3. Returns ready client\n */\nexport async function connectMcpClient(\n registryOrConfig: RegistryLike | ServersConfig,\n serverName: string,\n options?: {\n dcrAuthenticator?: Partial<DcrAuthenticatorOptions>;\n logger?: Logger;\n }\n): Promise<Client> {\n // Detect whether we have a RegistryLike instance or just config\n const isRegistry = 'servers' in registryOrConfig && registryOrConfig.servers instanceof Map;\n const serversConfig = isRegistry ? (registryOrConfig as RegistryLike).config : registryOrConfig;\n const registry = isRegistry ? (registryOrConfig as RegistryLike) : undefined;\n const logger = options?.logger ?? defaultLogger;\n\n const serverConfig = serversConfig[serverName];\n\n if (!serverConfig) {\n const available = Object.keys(serversConfig).join(', ');\n throw new Error(`Server '${serverName}' not found in config. Available servers: ${available || 'none'}`);\n }\n\n // Infer transport type with validation\n const transportType = inferTransportType(serverConfig);\n\n // Create MCP client\n const client = new Client({ name: 'mcp-cli-client', version: '1.0.0' }, { capabilities: {} });\n\n // Connect based on inferred transport\n if (transportType === 'stdio') {\n // Check if we have a spawned process in the registry\n const serverHandle = registry?.servers.get(serverName);\n\n if (serverHandle) {\n // Reuse the already-spawned process\n const transport = new ExistingProcessTransport(serverHandle.process);\n await client.connect(transport);\n } else {\n // No registry or server not in registry - spawn new process directly\n // This is the standard fallback when process management is not used\n if (!serverConfig.command) {\n throw new Error(`Server '${serverName}' has stdio transport but missing 'command' field`);\n }\n\n const transport = new StdioClientTransport({\n command: serverConfig.command,\n args: serverConfig.args || [],\n env: serverConfig.env || {},\n });\n\n // client.connect() performs initialize handshake - when it resolves, server is ready\n await client.connect(transport);\n }\n } else if (transportType === 'http') {\n if (!('url' in serverConfig) || !serverConfig.url) {\n throw new Error(`Server '${serverName}' has http transport but missing 'url' field`);\n }\n\n // Check if this is a freshly spawned HTTP server (from registry)\n // that might not be ready yet - transport readiness check needed\n const isSpawnedHttp = registry?.servers.has(serverName);\n\n if (isSpawnedHttp) {\n logger.debug(`[connectMcpClient] waiting for HTTP server '${serverName}' at ${serverConfig.url}`);\n await waitForHttpReady(serverConfig.url);\n logger.debug(`[connectMcpClient] HTTP server '${serverName}' ready`);\n }\n\n const url = new URL(serverConfig.url);\n\n // Check for DCR support and handle authentication automatically\n const baseUrl = extractBaseUrl(serverConfig.url);\n const capabilities = await withTimeout(probeAuthCapabilities(baseUrl), 5000, 'DCR capability discovery');\n\n let authToken: string | undefined;\n\n if (capabilities.supportsDcr) {\n logger.debug(`đ Server '${serverName}' supports DCR authentication`);\n\n // Get available port and create the exact redirect URI to use\n const port = await getPort();\n const redirectUri = `http://localhost:${port}/callback`;\n\n // Handle authentication using DcrAuthenticator with fully resolved redirectUri\n const authenticator = new DcrAuthenticator({\n headless: false,\n redirectUri,\n logger,\n ...options?.dcrAuthenticator,\n });\n\n // Ensure we have valid tokens (performs DCR + OAuth if needed)\n const tokens = await authenticator.ensureAuthenticated(baseUrl, capabilities);\n authToken = tokens.accessToken;\n\n logger.debug(`â
Authentication complete for '${serverName}'`);\n } else {\n logger.debug(`âšī¸ Server '${serverName}' does not support DCR - connecting without authentication`);\n }\n\n try {\n // Try modern Streamable HTTP first (protocol version 2025-03-26)\n // Merge static headers from config with DCR auth headers (DCR Authorization takes precedence)\n const staticHeaders = serverConfig.headers || {};\n const dcrHeaders = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n const mergedHeaders = { ...staticHeaders, ...dcrHeaders };\n\n const transportOptions =\n Object.keys(mergedHeaders).length > 0\n ? {\n requestInit: {\n headers: mergedHeaders,\n },\n }\n : undefined;\n\n const transport = new StreamableHTTPClientTransport(url, transportOptions);\n // Type assertion: SDK transport has sessionId: string | undefined but Transport expects string\n // This is safe at runtime - the undefined is valid per MCP spec\n await withTimeout(client.connect(transport as unknown as Transport), 30000, 'StreamableHTTP connection');\n } catch (error) {\n // Fall back to SSE transport (MCP protocol version 2024-11-05)\n // SSE is a standard MCP transport used by many servers (e.g., FastMCP ecosystem)\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Fast-fail: Don't try SSE if connection was refused (server not running)\n // Check error.cause.code for ECONNREFUSED (fetch errors wrap the actual error in cause)\n const cause = error instanceof Error ? (error as Error & { cause?: { code?: string } }).cause : undefined;\n const isConnectionRefused = cause?.code === 'ECONNREFUSED' || errorMessage.includes('Connection refused');\n\n if (isConnectionRefused) {\n // Clean up client resources before throwing\n await client.close().catch(() => {});\n throw new Error(`Server not running at ${url}`);\n }\n\n // Check for known errors that indicate SSE fallback is needed\n const shouldFallback =\n errorMessage.includes('Missing session ID') || // FastMCP specific\n errorMessage.includes('404') || // Server doesn't have streamable HTTP endpoint\n errorMessage.includes('405'); // Method not allowed\n\n if (shouldFallback) {\n logger.warn(`Streamable HTTP failed (${errorMessage}), falling back to SSE transport`);\n } else {\n logger.warn('Streamable HTTP connection failed, trying SSE transport as fallback');\n }\n\n // Create new client for SSE transport (required per SDK pattern)\n const sseClient = new Client({ name: 'mcp-cli-client', version: '1.0.0' }, { capabilities: {} });\n\n // SSE transport with merged headers (static + DCR auth)\n // Reuse the same header merging logic as Streamable HTTP\n const staticHeaders = serverConfig.headers || {};\n const dcrHeaders = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n const mergedHeaders = { ...staticHeaders, ...dcrHeaders };\n\n const sseTransportOptions =\n Object.keys(mergedHeaders).length > 0\n ? {\n requestInit: {\n headers: mergedHeaders,\n },\n }\n : undefined;\n\n const sseTransport = new SSEClientTransport(url, sseTransportOptions);\n\n try {\n await withTimeout(sseClient.connect(sseTransport), 30000, 'SSE connection');\n // Return SSE client instead of original\n return sseClient;\n } catch (sseError) {\n // SSE connection failed - clean up both clients before throwing\n await Promise.all([client.close().catch(() => {}), sseClient.close().catch(() => {})]);\n throw sseError;\n }\n }\n }\n\n return client; // Guaranteed ready when returned\n}\n"],"names":["connectMcpClient","withTimeout","promise","ms","operation","timeoutId","Promise","race","finally","clearTimeout","_","reject","setTimeout","Error","extractBaseUrl","mcpUrl","url","URL","protocol","host","inferTransportType","config","type","registryOrConfig","serverName","options","isRegistry","serversConfig","registry","logger","serverConfig","available","transportType","client","serverHandle","transport","isSpawnedHttp","baseUrl","capabilities","authToken","port","redirectUri","authenticator","tokens","staticHeaders","dcrHeaders","mergedHeaders","transportOptions","error","errorMessage","cause","isConnectionRefused","shouldFallback","sseClient","sseTransportOptions","sseTransport","sseError","servers","Map","undefined","defaultLogger","Object","keys","join","Client","name","version","get","ExistingProcessTransport","process","connect","command","StdioClientTransport","args","env","has","debug","waitForHttpReady","probeAuthCapabilities","supportsDcr","getPort","DcrAuthenticator","headless","dcrAuthenticator","ensureAuthenticated","accessToken","headers","Authorization","length","requestInit","StreamableHTTPClientTransport","message","String","code","includes","close","catch","warn","SSEClientTransport","all"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/connection/connect-client.ts"],"sourcesContent":["/**\n * connect-mcp-client.ts\n *\n * Helper to connect MCP SDK clients to servers with intelligent transport inference.\n * Automatically detects transport type from URL protocol or type field.\n */\n\nimport '../monkey-patches.ts';\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport getPort from 'get-port';\nimport { probeAuthCapabilities } from '../auth/index.ts';\nimport { DcrAuthenticator, type DcrAuthenticatorOptions } from '../dcr/index.ts';\nimport type { ServerProcess } from '../spawn/spawn-server.ts';\nimport type { ServersConfig } from '../spawn/spawn-servers.ts';\n\n/**\n * Minimal interface for connecting to servers.\n * Only needs config and servers map for connection logic.\n */\ninterface RegistryLike {\n config: ServersConfig;\n servers: Map<string, ServerProcess>;\n}\n\nimport type { McpServerEntry, TransportType } from '../types.ts';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { ExistingProcessTransport } from './existing-process-transport.ts';\nimport { waitForHttpReady } from './wait-for-http-ready.ts';\n\n/**\n * Wrap promise with timeout - throws if promise takes too long\n * Clears timeout when promise completes to prevent hanging event loop\n * @param promise - Promise to wrap\n * @param ms - Timeout in milliseconds\n * @param operation - Description of operation for error message\n * @returns Promise result or timeout error\n */\nasync function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {\n let timeoutId: NodeJS.Timeout;\n\n return Promise.race([\n promise.finally(() => clearTimeout(timeoutId)),\n new Promise<T>((_, reject) => {\n timeoutId = setTimeout(() => reject(new Error(`Timeout after ${ms}ms: ${operation}`)), ms);\n }),\n ]);\n}\n\n/**\n * Extract the \"server base\" by removing a trailing `/mcp` path segment if present.\n * Examples:\n * - https://example.com/mcp -> https://example.com\n * - https://example.com/sheets/mcp -> https://example.com/sheets\n * - https://example.com/sheets/mcp/ -> https://example.com/sheets\n * - https://example.com/sheets -> https://example.com/sheets\n */\nexport function extractBaseUrl(mcpUrl: string): string {\n const url = new URL(mcpUrl);\n\n // Ignore query/hash for base URL purposes\n url.search = '';\n url.hash = '';\n\n // Normalize path segments (removes empty segments from leading/trailing slashes)\n const segments = url.pathname.split('/').filter(Boolean);\n\n // If last segment is exactly \"mcp\", drop it\n if (segments[segments.length - 1] === 'mcp') {\n segments.pop();\n }\n\n // Rebuild pathname; empty means root\n url.pathname = segments.length ? `/${segments.join('/')}` : '';\n\n // Return without trailing slash (except root origin)\n const out = url.origin + url.pathname;\n return out === url.origin ? out : out.replace(/\\/+$/, '');\n}\n\n/**\n * Infer transport type from server configuration with validation.\n *\n * Priority:\n * 1. Explicit type field (if present)\n * 2. URL protocol (if URL present): http://, https://\n * 3. Default to 'stdio' (if neither present)\n *\n * @param config - Server configuration\n * @returns Transport type\n * @throws Error if configuration is invalid or has conflicts\n */\nfunction inferTransportType(config: McpServerEntry): TransportType {\n // Priority 1: Explicit type field\n if (config.type) {\n // Validate consistency with URL if both present\n if (config.url) {\n const url = new URL(config.url);\n const protocol = url.protocol;\n\n if ((protocol === 'http:' || protocol === 'https:') && config.type !== 'http' && config.type !== 'sse-ide') {\n throw new Error(`Conflicting transport: URL protocol '${protocol}' requires type 'http', but got '${config.type}'`);\n }\n }\n\n // Return normalized type\n if (config.type === 'http' || config.type === 'sse-ide') return 'http';\n if (config.type === 'stdio') return 'stdio';\n\n throw new Error(`Unsupported transport type: ${config.type}`);\n }\n\n // Priority 2: Infer from URL protocol\n if (config.url) {\n const url = new URL(config.url);\n const protocol = url.protocol;\n\n if (protocol === 'http:' || protocol === 'https:') {\n return 'http';\n }\n throw new Error(`Unsupported URL protocol: ${protocol}`);\n }\n\n // Priority 3: Default to stdio\n return 'stdio';\n}\n\n/**\n * Connect MCP SDK client to server with full readiness handling.\n * @internal - Use registry.connect() instead\n *\n * **Completely handles readiness**: transport availability + MCP protocol handshake.\n *\n * Transport is intelligently inferred and handled:\n * - **Stdio servers**: Direct MCP connect (fast for spawned processes)\n * - **HTTP servers**: Transport polling (/mcp endpoint) + MCP connect\n * - **Registry result**: Handles both spawned and external servers\n *\n * Returns only when server is fully MCP-ready (initialize handshake complete).\n *\n * @param registryOrConfig - Result from createServerRegistry() or servers config object\n * @param serverName - Server name from servers config\n * @returns Connected MCP SDK Client (guaranteed ready)\n *\n * @example\n * // Using registry (recommended)\n * const registry = createServerRegistry({ echo: { command: 'node', args: ['server.ts'] } });\n * const client = await registry.connect('echo');\n * // Server is fully ready - transport available + MCP handshake complete\n *\n * @example\n * // HTTP server readiness (waits for /mcp polling + MCP handshake)\n * const registry = createServerRegistry(\n * { http: { type: 'http', url: 'http://localhost:3000/mcp', start: {...} } },\n * { dialects: ['start'] }\n * );\n * const client = await registry.connect('http');\n * // 1. Waits for HTTP server to respond on /mcp\n * // 2. Performs MCP initialize handshake\n * // 3. Returns ready client\n */\nexport async function connectMcpClient(\n registryOrConfig: RegistryLike | ServersConfig,\n serverName: string,\n options?: {\n dcrAuthenticator?: Partial<DcrAuthenticatorOptions>;\n logger?: Logger;\n }\n): Promise<Client> {\n // Detect whether we have a RegistryLike instance or just config\n const isRegistry = 'servers' in registryOrConfig && registryOrConfig.servers instanceof Map;\n const serversConfig = isRegistry ? (registryOrConfig as RegistryLike).config : registryOrConfig;\n const registry = isRegistry ? (registryOrConfig as RegistryLike) : undefined;\n const logger = options?.logger ?? defaultLogger;\n\n const serverConfig = serversConfig[serverName];\n\n if (!serverConfig) {\n const available = Object.keys(serversConfig).join(', ');\n throw new Error(`Server '${serverName}' not found in config. Available servers: ${available || 'none'}`);\n }\n\n // Infer transport type with validation\n const transportType = inferTransportType(serverConfig);\n\n // Create MCP client\n const client = new Client({ name: 'mcp-cli-client', version: '1.0.0' }, { capabilities: {} });\n\n // Connect based on inferred transport\n if (transportType === 'stdio') {\n // Check if we have a spawned process in the registry\n const serverHandle = registry?.servers.get(serverName);\n\n if (serverHandle) {\n // Reuse the already-spawned process\n const transport = new ExistingProcessTransport(serverHandle.process);\n await client.connect(transport);\n } else {\n // No registry or server not in registry - spawn new process directly\n // This is the standard fallback when process management is not used\n if (!serverConfig.command) {\n throw new Error(`Server '${serverName}' has stdio transport but missing 'command' field`);\n }\n\n const transport = new StdioClientTransport({\n command: serverConfig.command,\n args: serverConfig.args || [],\n env: serverConfig.env || {},\n });\n\n // client.connect() performs initialize handshake - when it resolves, server is ready\n await client.connect(transport);\n }\n } else if (transportType === 'http') {\n if (!('url' in serverConfig) || !serverConfig.url) {\n throw new Error(`Server '${serverName}' has http transport but missing 'url' field`);\n }\n\n // Check if this is a freshly spawned HTTP server (from registry)\n // that might not be ready yet - transport readiness check needed\n const isSpawnedHttp = registry?.servers.has(serverName);\n\n if (isSpawnedHttp) {\n logger.debug(`[connectMcpClient] waiting for HTTP server '${serverName}' at ${serverConfig.url}`);\n await waitForHttpReady(serverConfig.url);\n logger.debug(`[connectMcpClient] HTTP server '${serverName}' ready`);\n }\n\n const url = new URL(serverConfig.url);\n\n // Check for DCR support and handle authentication automatically\n const baseUrl = extractBaseUrl(serverConfig.url);\n const capabilities = await withTimeout(probeAuthCapabilities(baseUrl), 5000, 'DCR capability discovery');\n\n let authToken: string | undefined;\n\n if (capabilities.supportsDcr) {\n logger.debug(`đ Server '${serverName}' supports DCR authentication`);\n\n // Get available port and create the exact redirect URI to use\n const port = await getPort();\n const redirectUri = `http://localhost:${port}/callback`;\n\n // Handle authentication using DcrAuthenticator with fully resolved redirectUri\n const authenticator = new DcrAuthenticator({\n headless: false,\n redirectUri,\n logger,\n ...options?.dcrAuthenticator,\n });\n\n // Ensure we have valid tokens (performs DCR + OAuth if needed)\n const tokens = await authenticator.ensureAuthenticated(baseUrl, capabilities);\n authToken = tokens.accessToken;\n\n logger.debug(`â
Authentication complete for '${serverName}'`);\n } else {\n logger.debug(`âšī¸ Server '${serverName}' does not support DCR - connecting without authentication`);\n }\n\n try {\n // Try modern Streamable HTTP first (protocol version 2025-03-26)\n // Merge static headers from config with DCR auth headers (DCR Authorization takes precedence)\n const staticHeaders = serverConfig.headers || {};\n const dcrHeaders = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n const mergedHeaders = { ...staticHeaders, ...dcrHeaders };\n\n const transportOptions =\n Object.keys(mergedHeaders).length > 0\n ? {\n requestInit: {\n headers: mergedHeaders,\n },\n }\n : undefined;\n\n const transport = new StreamableHTTPClientTransport(url, transportOptions);\n // Type assertion: SDK transport has sessionId: string | undefined but Transport expects string\n // This is safe at runtime - the undefined is valid per MCP spec\n await withTimeout(client.connect(transport as unknown as Transport), 30000, 'StreamableHTTP connection');\n } catch (error) {\n // Fall back to SSE transport (MCP protocol version 2024-11-05)\n // SSE is a standard MCP transport used by many servers (e.g., FastMCP ecosystem)\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Fast-fail: Don't try SSE if connection was refused (server not running)\n // Check error.cause.code for ECONNREFUSED (fetch errors wrap the actual error in cause)\n const cause = error instanceof Error ? (error as Error & { cause?: { code?: string } }).cause : undefined;\n const isConnectionRefused = cause?.code === 'ECONNREFUSED' || errorMessage.includes('Connection refused');\n\n if (isConnectionRefused) {\n // Clean up client resources before throwing\n await client.close().catch(() => {});\n throw new Error(`Server not running at ${url}`);\n }\n\n // Check for known errors that indicate SSE fallback is needed\n const shouldFallback =\n errorMessage.includes('Missing session ID') || // FastMCP specific\n errorMessage.includes('404') || // Server doesn't have streamable HTTP endpoint\n errorMessage.includes('405'); // Method not allowed\n\n if (shouldFallback) {\n logger.warn(`Streamable HTTP failed (${errorMessage}), falling back to SSE transport`);\n } else {\n logger.warn('Streamable HTTP connection failed, trying SSE transport as fallback');\n }\n\n // Create new client for SSE transport (required per SDK pattern)\n const sseClient = new Client({ name: 'mcp-cli-client', version: '1.0.0' }, { capabilities: {} });\n\n // SSE transport with merged headers (static + DCR auth)\n // Reuse the same header merging logic as Streamable HTTP\n const staticHeaders = serverConfig.headers || {};\n const dcrHeaders = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n const mergedHeaders = { ...staticHeaders, ...dcrHeaders };\n\n const sseTransportOptions =\n Object.keys(mergedHeaders).length > 0\n ? {\n requestInit: {\n headers: mergedHeaders,\n },\n }\n : undefined;\n\n const sseTransport = new SSEClientTransport(url, sseTransportOptions);\n\n try {\n await withTimeout(sseClient.connect(sseTransport), 30000, 'SSE connection');\n // Return SSE client instead of original\n return sseClient;\n } catch (sseError) {\n // SSE connection failed - clean up both clients before throwing\n await Promise.all([client.close().catch(() => {}), sseClient.close().catch(() => {})]);\n throw sseError;\n }\n }\n }\n\n return client; // Guaranteed ready when returned\n}\n"],"names":["connectMcpClient","extractBaseUrl","withTimeout","promise","ms","operation","timeoutId","Promise","race","finally","clearTimeout","_","reject","setTimeout","Error","mcpUrl","url","URL","search","hash","segments","pathname","split","filter","Boolean","length","pop","join","out","origin","replace","inferTransportType","config","type","protocol","registryOrConfig","serverName","options","isRegistry","serversConfig","registry","logger","serverConfig","available","transportType","client","serverHandle","transport","isSpawnedHttp","baseUrl","capabilities","authToken","port","redirectUri","authenticator","tokens","staticHeaders","dcrHeaders","mergedHeaders","transportOptions","error","errorMessage","cause","isConnectionRefused","shouldFallback","sseClient","sseTransportOptions","sseTransport","sseError","servers","Map","undefined","defaultLogger","Object","keys","Client","name","version","get","ExistingProcessTransport","process","connect","command","StdioClientTransport","args","env","has","debug","waitForHttpReady","probeAuthCapabilities","supportsDcr","getPort","DcrAuthenticator","headless","dcrAuthenticator","ensureAuthenticated","accessToken","headers","Authorization","requestInit","StreamableHTTPClientTransport","message","String","code","includes","close","catch","warn","SSEClientTransport","all"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
function _export(target, all) {
|
|
6
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
_export(exports, {
|
|
12
|
+
get joinWellKnown () {
|
|
13
|
+
return joinWellKnown;
|
|
14
|
+
},
|
|
15
|
+
get normalizeUrl () {
|
|
16
|
+
return normalizeUrl;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
function normalizeUrl(input) {
|
|
20
|
+
try {
|
|
21
|
+
var url = new URL(input);
|
|
22
|
+
url.search = '';
|
|
23
|
+
url.hash = '';
|
|
24
|
+
url.pathname = url.pathname.replace(/\/+$/, '');
|
|
25
|
+
return url.origin + url.pathname;
|
|
26
|
+
} catch (unused) {
|
|
27
|
+
return input.replace(/\/+$/, '');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function joinWellKnown(baseUrl, suffix) {
|
|
31
|
+
return "".concat(normalizeUrl(baseUrl)).concat(suffix);
|
|
32
|
+
}
|
|
33
|
+
/* CJS INTEROP */ if (exports.__esModule && exports.default) { try { Object.defineProperty(exports.default, '__esModule', { value: true }); for (var key in exports) { exports.default[key] = exports[key]; } } catch (_) {}; module.exports = exports.default; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/lib/url-utils.ts"],"sourcesContent":["export function normalizeUrl(input: string): string {\n try {\n const url = new URL(input);\n url.search = '';\n url.hash = '';\n url.pathname = url.pathname.replace(/\\/+$/, '');\n return url.origin + url.pathname;\n } catch {\n return input.replace(/\\/+$/, '');\n }\n}\n\nexport function joinWellKnown(baseUrl: string, suffix: string): string {\n return `${normalizeUrl(baseUrl)}${suffix}`;\n}\n"],"names":["joinWellKnown","normalizeUrl","input","url","URL","search","hash","pathname","replace","origin","baseUrl","suffix"],"mappings":";;;;;;;;;;;QAYgBA;eAAAA;;QAZAC;eAAAA;;;AAAT,SAASA,aAAaC,KAAa;IACxC,IAAI;QACF,IAAMC,MAAM,IAAIC,IAAIF;QACpBC,IAAIE,MAAM,GAAG;QACbF,IAAIG,IAAI,GAAG;QACXH,IAAII,QAAQ,GAAGJ,IAAII,QAAQ,CAACC,OAAO,CAAC,QAAQ;QAC5C,OAAOL,IAAIM,MAAM,GAAGN,IAAII,QAAQ;IAClC,EAAE,eAAM;QACN,OAAOL,MAAMM,OAAO,CAAC,QAAQ;IAC/B;AACF;AAEO,SAASR,cAAcU,OAAe,EAAEC,MAAc;IAC3D,OAAO,AAAC,GAA0BA,OAAxBV,aAAaS,UAAkB,OAAPC;AACpC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OAuth Server Capability Discovery
|
|
3
3
|
* Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata
|
|
4
|
-
*/ import {
|
|
4
|
+
*/ import { normalizeUrl } from '../lib/url-utils.js';
|
|
5
|
+
import { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.js';
|
|
5
6
|
/**
|
|
6
7
|
* Extract origin (protocol + host) from a URL
|
|
7
8
|
* @param url - Full URL that may include a path
|
|
@@ -65,9 +66,10 @@ async function resolveCapabilitiesFromAuthorizationServer(authServerUrl, scopes)
|
|
|
65
66
|
}
|
|
66
67
|
export async function probeAuthCapabilities(baseUrl) {
|
|
67
68
|
try {
|
|
69
|
+
const normalizedBaseUrl = normalizeUrl(baseUrl);
|
|
68
70
|
// Strategy 1: Try RFC 9728 Protected Resource Metadata discovery
|
|
69
71
|
// This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp â todoist.com)
|
|
70
|
-
const resourceMetadata = await discoverProtectedResourceMetadata(
|
|
72
|
+
const resourceMetadata = await discoverProtectedResourceMetadata(normalizedBaseUrl);
|
|
71
73
|
if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {
|
|
72
74
|
// Found protected resource metadata with authorization servers
|
|
73
75
|
// Discover the authorization server's metadata (RFC 8414)
|
|
@@ -88,14 +90,14 @@ export async function probeAuthCapabilities(baseUrl) {
|
|
|
88
90
|
if (issuerCapabilities) return issuerCapabilities;
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
|
-
const issuer = await discoverAuthorizationServerIssuer(
|
|
93
|
+
const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);
|
|
92
94
|
if (issuer) {
|
|
93
95
|
const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer);
|
|
94
96
|
if (issuerCapabilities) return issuerCapabilities;
|
|
95
97
|
}
|
|
96
98
|
// Strategy 2: Fall back to direct RFC 8414 discovery at resource origin
|
|
97
99
|
// This handles same-domain OAuth (traditional setup)
|
|
98
|
-
const origin = getOrigin(
|
|
100
|
+
const origin = getOrigin(normalizedBaseUrl);
|
|
99
101
|
const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin);
|
|
100
102
|
if (originCapabilities) return originCapabilities;
|
|
101
103
|
// No OAuth metadata found
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // â 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // â 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 â RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[]): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr };\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes?: string[]): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl);\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp â todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/capability-discovery.ts"],"sourcesContent":["/**\n * OAuth Server Capability Discovery\n * Probes RFC 9728 (Protected Resource) and RFC 8414 (Authorization Server) metadata\n */\n\nimport { normalizeUrl } from '../lib/url-utils.ts';\nimport { discoverAuthorizationServerIssuer, discoverAuthorizationServerMetadata, discoverProtectedResourceMetadata } from './rfc9728-discovery.ts';\nimport type { AuthCapabilities, AuthorizationServerMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // â 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // â 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Probe OAuth server capabilities using RFC 9728 â RFC 8414 discovery chain\n * Returns capabilities including DCR support detection\n *\n * Discovery Strategy:\n * 1. Try RFC 9728 Protected Resource Metadata (supports cross-domain OAuth)\n * 2. If found, use first authorization_server to discover RFC 8414 Authorization Server Metadata\n * 3. Fall back to direct RFC 8414 discovery at resource origin\n *\n * @param baseUrl - Base URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns AuthCapabilities object with discovered endpoints and features\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const caps = await probeAuthCapabilities('https://ai.todoist.net/mcp');\n * if (caps.supportsDcr) {\n * console.log('Registration endpoint:', caps.registrationEndpoint);\n * }\n */\nfunction buildCapabilities(metadata: AuthorizationServerMetadata, scopes?: string[]): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr };\n\n if (metadata.registration_endpoint) {\n capabilities.registrationEndpoint = metadata.registration_endpoint;\n }\n if (metadata.authorization_endpoint) {\n capabilities.authorizationEndpoint = metadata.authorization_endpoint;\n }\n if (metadata.token_endpoint) capabilities.tokenEndpoint = metadata.token_endpoint;\n if (metadata.introspection_endpoint) {\n capabilities.introspectionEndpoint = metadata.introspection_endpoint;\n }\n\n if (scopes && scopes.length > 0) {\n capabilities.scopes = scopes;\n } else if (metadata.scopes_supported) {\n capabilities.scopes = metadata.scopes_supported;\n }\n\n return capabilities;\n}\n\nasync function resolveCapabilitiesFromAuthorizationServer(authServerUrl: string, scopes?: string[]): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl);\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n const normalizedBaseUrl = normalizeUrl(baseUrl);\n // Strategy 1: Try RFC 9728 Protected Resource Metadata discovery\n // This handles cross-domain OAuth (e.g., Todoist: ai.todoist.net/mcp â todoist.com)\n const resourceMetadata = await discoverProtectedResourceMetadata(normalizedBaseUrl);\n\n if (resourceMetadata && resourceMetadata.authorization_servers.length > 0) {\n // Found protected resource metadata with authorization servers\n // Discover the authorization server's metadata (RFC 8414)\n const authServerUrl = resourceMetadata.authorization_servers[0];\n if (!authServerUrl) {\n // Array has length > 0 but first element is undefined/null - skip this path\n return { supportsDcr: false };\n }\n const capabilities = await resolveCapabilitiesFromAuthorizationServer(authServerUrl, resourceMetadata.scopes_supported);\n if (capabilities) {\n return capabilities;\n }\n\n const issuer = await discoverAuthorizationServerIssuer(baseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, resourceMetadata.scopes_supported);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer);\n if (issuerCapabilities) return issuerCapabilities;\n }\n\n // Strategy 2: Fall back to direct RFC 8414 discovery at resource origin\n // This handles same-domain OAuth (traditional setup)\n const origin = getOrigin(normalizedBaseUrl);\n const originCapabilities = await resolveCapabilitiesFromAuthorizationServer(origin);\n if (originCapabilities) return originCapabilities;\n\n // No OAuth metadata found\n return { supportsDcr: false };\n } catch (_error) {\n // Network error, invalid JSON, or other fetch failure\n // Gracefully degrade - assume no DCR support\n return { supportsDcr: false };\n }\n}\n"],"names":["normalizeUrl","discoverAuthorizationServerIssuer","discoverAuthorizationServerMetadata","discoverProtectedResourceMetadata","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","supportsDcr","registration_endpoint","capabilities","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","probeAuthCapabilities","baseUrl","normalizedBaseUrl","resourceMetadata","authorization_servers","issuer","issuerCapabilities","originCapabilities","_error"],"mappings":"AAAA;;;CAGC,GAED,SAASA,YAAY,QAAQ,sBAAsB;AACnD,SAASC,iCAAiC,EAAEC,mCAAmC,EAAEC,iCAAiC,QAAQ,yBAAyB;AAGnJ;;;;;;;;CAQC,GACD,SAASC,UAAUC,GAAW;IAC5B,IAAI;QACF,OAAO,IAAIC,IAAID,KAAKE,MAAM;IAC5B,EAAE,OAAM;QACN,sDAAsD;QACtD,OAAOF;IACT;AACF;AAEA;;;;;;;;;;;;;;;;;;CAkBC,GACD,SAASG,kBAAkBC,QAAqC,EAAEC,MAAiB;IACjF,MAAMC,cAAc,CAAC,CAACF,SAASG,qBAAqB;IACpD,MAAMC,eAAiC;QAAEF;IAAY;IAErD,IAAIF,SAASG,qBAAqB,EAAE;QAClCC,aAAaC,oBAAoB,GAAGL,SAASG,qBAAqB;IACpE;IACA,IAAIH,SAASM,sBAAsB,EAAE;QACnCF,aAAaG,qBAAqB,GAAGP,SAASM,sBAAsB;IACtE;IACA,IAAIN,SAASQ,cAAc,EAAEJ,aAAaK,aAAa,GAAGT,SAASQ,cAAc;IACjF,IAAIR,SAASU,sBAAsB,EAAE;QACnCN,aAAaO,qBAAqB,GAAGX,SAASU,sBAAsB;IACtE;IAEA,IAAIT,UAAUA,OAAOW,MAAM,GAAG,GAAG;QAC/BR,aAAaH,MAAM,GAAGA;IACxB,OAAO,IAAID,SAASa,gBAAgB,EAAE;QACpCT,aAAaH,MAAM,GAAGD,SAASa,gBAAgB;IACjD;IAEA,OAAOT;AACT;AAEA,eAAeU,2CAA2CC,aAAqB,EAAEd,MAAiB;IAChG,MAAMD,WAAW,MAAMP,oCAAoCsB;IAC3D,IAAI,CAACf,UAAU,OAAO;IACtB,OAAOD,kBAAkBC,UAAUC;AACrC;AAEA,OAAO,eAAee,sBAAsBC,OAAe;IACzD,IAAI;QACF,MAAMC,oBAAoB3B,aAAa0B;QACvC,iEAAiE;QACjE,oFAAoF;QACpF,MAAME,mBAAmB,MAAMzB,kCAAkCwB;QAEjE,IAAIC,oBAAoBA,iBAAiBC,qBAAqB,CAACR,MAAM,GAAG,GAAG;YACzE,+DAA+D;YAC/D,0DAA0D;YAC1D,MAAMG,gBAAgBI,iBAAiBC,qBAAqB,CAAC,EAAE;YAC/D,IAAI,CAACL,eAAe;gBAClB,4EAA4E;gBAC5E,OAAO;oBAAEb,aAAa;gBAAM;YAC9B;YACA,MAAME,eAAe,MAAMU,2CAA2CC,eAAeI,iBAAiBN,gBAAgB;YACtH,IAAIT,cAAc;gBAChB,OAAOA;YACT;YAEA,MAAMiB,SAAS,MAAM7B,kCAAkCyB;YACvD,IAAII,QAAQ;gBACV,MAAMC,qBAAqB,MAAMR,2CAA2CO,QAAQF,iBAAiBN,gBAAgB;gBACrH,IAAIS,oBAAoB,OAAOA;YACjC;QACF;QAEA,MAAMD,SAAS,MAAM7B,kCAAkC0B;QACvD,IAAIG,QAAQ;YACV,MAAMC,qBAAqB,MAAMR,2CAA2CO;YAC5E,IAAIC,oBAAoB,OAAOA;QACjC;QAEA,wEAAwE;QACxE,qDAAqD;QACrD,MAAMxB,SAASH,UAAUuB;QACzB,MAAMK,qBAAqB,MAAMT,2CAA2ChB;QAC5E,IAAIyB,oBAAoB,OAAOA;QAE/B,0BAA0B;QAC1B,OAAO;YAAErB,aAAa;QAAM;IAC9B,EAAE,OAAOsB,QAAQ;QACf,sDAAsD;QACtD,6CAA6C;QAC7C,OAAO;YAAEtB,aAAa;QAAM;IAC9B;AACF"}
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Probes .well-known/oauth-protected-resource endpoint
|
|
4
4
|
*/
|
|
5
5
|
import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Normalize a resource URL by stripping query/hash and trailing slashes.
|
|
8
|
+
*/
|
|
6
9
|
/**
|
|
7
10
|
* Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
8
11
|
* Probes .well-known/oauth-protected-resource endpoint
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* RFC 9728 Protected Resource Metadata Discovery
|
|
3
3
|
* Probes .well-known/oauth-protected-resource endpoint
|
|
4
|
-
*/
|
|
4
|
+
*/ import { joinWellKnown, normalizeUrl } from '../lib/url-utils.js';
|
|
5
|
+
/**
|
|
5
6
|
* Extract origin (protocol + host) from a URL
|
|
6
7
|
* @param url - Full URL that may include a path
|
|
7
8
|
* @returns Origin (e.g., "https://example.com") or original string if invalid URL
|
|
@@ -31,6 +32,8 @@
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
35
|
+
* Normalize a resource URL by stripping query/hash and trailing slashes.
|
|
36
|
+
*/ /**
|
|
34
37
|
* Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
35
38
|
* Probes .well-known/oauth-protected-resource endpoint
|
|
36
39
|
*
|
|
@@ -47,10 +50,27 @@
|
|
|
47
50
|
* // Returns: { resource: "https://ai.todoist.net/mcp", authorization_servers: ["https://todoist.com"] }
|
|
48
51
|
*/ export async function discoverProtectedResourceMetadata(resourceUrl) {
|
|
49
52
|
try {
|
|
50
|
-
const
|
|
53
|
+
const normalizedResourceUrl = normalizeUrl(resourceUrl);
|
|
54
|
+
const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl);
|
|
51
55
|
if (headerMetadata) return headerMetadata;
|
|
52
|
-
|
|
53
|
-
const
|
|
56
|
+
// Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)
|
|
57
|
+
const localWellKnownUrl = joinWellKnown(normalizedResourceUrl, '/.well-known/oauth-protected-resource');
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(localWellKnownUrl, {
|
|
60
|
+
method: 'GET',
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: 'application/json',
|
|
63
|
+
Connection: 'close'
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
if (response.ok) {
|
|
67
|
+
return await response.json();
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Continue to origin-based discovery
|
|
71
|
+
}
|
|
72
|
+
const origin = getOrigin(normalizedResourceUrl);
|
|
73
|
+
const path = getPath(normalizedResourceUrl);
|
|
54
74
|
// Strategy 1: Try root location (REQUIRED by RFC 9728)
|
|
55
75
|
const rootUrl = `${origin}/.well-known/oauth-protected-resource`;
|
|
56
76
|
try {
|
|
@@ -64,7 +84,7 @@
|
|
|
64
84
|
if (response.ok) {
|
|
65
85
|
const metadata = await response.json();
|
|
66
86
|
// Check if the discovered resource matches what we're looking for
|
|
67
|
-
if (metadata.resource ===
|
|
87
|
+
if (metadata.resource === normalizedResourceUrl) {
|
|
68
88
|
return metadata;
|
|
69
89
|
}
|
|
70
90
|
// If there's no path component, return root metadata
|
|
@@ -74,7 +94,7 @@
|
|
|
74
94
|
}
|
|
75
95
|
// If requested URL starts with metadata.resource, the root metadata applies to sub-paths
|
|
76
96
|
// (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)
|
|
77
|
-
if (
|
|
97
|
+
if (normalizedResourceUrl.startsWith(metadata.resource)) {
|
|
78
98
|
// Still try sub-path location to see if there's more specific metadata
|
|
79
99
|
// But save root metadata as fallback
|
|
80
100
|
const rootMetadata = metadata;
|
|
@@ -181,7 +201,19 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl) {
|
|
|
181
201
|
* // Returns: { issuer: "https://todoist.com", authorization_endpoint: "...", ... }
|
|
182
202
|
*/ export async function discoverAuthorizationServerMetadata(authServerUrl) {
|
|
183
203
|
try {
|
|
184
|
-
const
|
|
204
|
+
const normalizedAuthServerUrl = normalizeUrl(authServerUrl);
|
|
205
|
+
const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');
|
|
206
|
+
const localResponse = await fetch(localWellKnownUrl, {
|
|
207
|
+
method: 'GET',
|
|
208
|
+
headers: {
|
|
209
|
+
Accept: 'application/json',
|
|
210
|
+
Connection: 'close'
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
if (localResponse.ok) {
|
|
214
|
+
return await localResponse.json();
|
|
215
|
+
}
|
|
216
|
+
const origin = getOrigin(normalizedAuthServerUrl);
|
|
185
217
|
const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;
|
|
186
218
|
const response = await fetch(wellKnownUrl, {
|
|
187
219
|
method: 'GET',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/rfc9728-discovery.ts"],"sourcesContent":["/**\n * RFC 9728 Protected Resource Metadata Discovery\n * Probes .well-known/oauth-protected-resource endpoint\n */\n\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // â 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // â 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Extract path from a URL (without origin)\n * @param url - Full URL\n * @returns Path component (e.g., \"/mcp\", \"/api/v1/mcp\") or empty string if no path\n */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n // pathname includes leading slash, e.g., \"/mcp\"\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Probes .well-known/oauth-protected-resource endpoint\n *\n * Discovery Strategy:\n * 1. Try origin root: {origin}/.well-known/oauth-protected-resource\n * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}\n *\n * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns ProtectedResourceMetadata if discovered, null otherwise\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');\n * // Returns: { resource: \"https://ai.todoist.net/mcp\", authorization_servers: [\"https://todoist.com\"] }\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(resourceUrl);\n if (headerMetadata) return headerMetadata;\n\n const origin = getOrigin(resourceUrl);\n const path = getPath(resourceUrl);\n\n // Strategy 1: Try root location (REQUIRED by RFC 9728)\n const rootUrl = `${origin}/.well-known/oauth-protected-resource`;\n\n try {\n const response = await fetch(rootUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n const metadata = (await response.json()) as ProtectedResourceMetadata;\n // Check if the discovered resource matches what we're looking for\n if (metadata.resource === resourceUrl) {\n return metadata;\n }\n // If there's no path component, return root metadata\n // (e.g., looking for http://example.com and found it)\n if (!path) {\n return metadata;\n }\n // If requested URL starts with metadata.resource, the root metadata applies to sub-paths\n // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)\n if (resourceUrl.startsWith(metadata.resource)) {\n // Still try sub-path location to see if there's more specific metadata\n // But save root metadata as fallback\n const rootMetadata = metadata;\n\n // Try sub-path location for more specific metadata\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n try {\n const subPathResponse = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (subPathResponse.ok) {\n return (await subPathResponse.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Sub-path failed, use root metadata\n }\n\n // Return root metadata as it applies to this resource\n return rootMetadata;\n }\n // Otherwise, try sub-path location before giving up\n }\n } catch {\n // Continue to sub-path location\n }\n\n // Strategy 2: Try sub-path location (MCP spec extension)\n // Only try if there's a path component\n if (path) {\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n\n try {\n const response = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Fall through to return null\n }\n }\n\n // Neither location found or resource didn't match\n return null;\n } catch (_error) {\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\nasync function discoverProtectedResourceMetadataFromHeader(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n let header = response.headers.get('www-authenticate');\n if (!header) {\n const postResponse = await fetch(resourceUrl, {\n method: 'POST',\n headers: { Accept: 'application/json', Connection: 'close', 'Content-Type': 'application/json' },\n body: '{}',\n });\n header = postResponse.headers.get('www-authenticate');\n }\n\n if (!header) return null;\n\n const match = header.match(/resource_metadata=\"([^\"]+)\"/i);\n if (!match || !match[1]) return null;\n\n const metadataUrl = match[1];\n const metadataResponse = await fetch(metadataUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return (await metadataResponse.json()) as ProtectedResourceMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Probes .well-known/oauth-authorization-server endpoint\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)\n * @returns AuthorizationServerMetadata if discovered, null otherwise\n *\n * @example\n * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');\n * // Returns: { issuer: \"https://todoist.com\", authorization_endpoint: \"...\", ... }\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string): Promise<AuthorizationServerMetadata | null> {\n try {\n const origin = getOrigin(authServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await fetch(wellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!response.ok) {\n return null;\n }\n\n return (await response.json()) as AuthorizationServerMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth Authorization Server Issuer from resource response (RFC 9207)\n *\n * @param resourceUrl - URL of the protected resource\n * @returns Issuer URL if present in WWW-Authenticate header, null otherwise\n */\nexport async function discoverAuthorizationServerIssuer(resourceUrl: string): Promise<string | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n const header = response.headers.get('www-authenticate');\n if (!header) return null;\n\n const match = header.match(/(?:authorization_server|issuer)=\"([^\"]+)\"/i);\n if (!match) return null;\n\n return match[1] ?? null;\n } catch (_error) {\n return null;\n }\n}\n"],"names":["getOrigin","url","URL","origin","getPath","parsed","pathname","discoverProtectedResourceMetadata","resourceUrl","headerMetadata","discoverProtectedResourceMetadataFromHeader","path","rootUrl","response","fetch","method","headers","Accept","Connection","ok","metadata","json","resource","startsWith","rootMetadata","subPathUrl","subPathResponse","_error","header","get","postResponse","body","match","metadataUrl","metadataResponse","discoverAuthorizationServerMetadata","authServerUrl","wellKnownUrl","discoverAuthorizationServerIssuer"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/auth/rfc9728-discovery.ts"],"sourcesContent":["/**\n * RFC 9728 Protected Resource Metadata Discovery\n * Probes .well-known/oauth-protected-resource endpoint\n */\n\nimport { joinWellKnown, normalizeUrl } from '../lib/url-utils.ts';\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // â 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // â 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Extract path from a URL (without origin)\n * @param url - Full URL\n * @returns Path component (e.g., \"/mcp\", \"/api/v1/mcp\") or empty string if no path\n */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n // pathname includes leading slash, e.g., \"/mcp\"\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Normalize a resource URL by stripping query/hash and trailing slashes.\n */\n/**\n * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Probes .well-known/oauth-protected-resource endpoint\n *\n * Discovery Strategy:\n * 1. Try origin root: {origin}/.well-known/oauth-protected-resource\n * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}\n *\n * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns ProtectedResourceMetadata if discovered, null otherwise\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');\n * // Returns: { resource: \"https://ai.todoist.net/mcp\", authorization_servers: [\"https://todoist.com\"] }\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const normalizedResourceUrl = normalizeUrl(resourceUrl);\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl);\n if (headerMetadata) return headerMetadata;\n\n // Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)\n const localWellKnownUrl = joinWellKnown(normalizedResourceUrl, '/.well-known/oauth-protected-resource');\n try {\n const response = await fetch(localWellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Continue to origin-based discovery\n }\n\n const origin = getOrigin(normalizedResourceUrl);\n const path = getPath(normalizedResourceUrl);\n\n // Strategy 1: Try root location (REQUIRED by RFC 9728)\n const rootUrl = `${origin}/.well-known/oauth-protected-resource`;\n\n try {\n const response = await fetch(rootUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n const metadata = (await response.json()) as ProtectedResourceMetadata;\n // Check if the discovered resource matches what we're looking for\n if (metadata.resource === normalizedResourceUrl) {\n return metadata;\n }\n // If there's no path component, return root metadata\n // (e.g., looking for http://example.com and found it)\n if (!path) {\n return metadata;\n }\n // If requested URL starts with metadata.resource, the root metadata applies to sub-paths\n // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)\n if (normalizedResourceUrl.startsWith(metadata.resource)) {\n // Still try sub-path location to see if there's more specific metadata\n // But save root metadata as fallback\n const rootMetadata = metadata;\n\n // Try sub-path location for more specific metadata\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n try {\n const subPathResponse = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (subPathResponse.ok) {\n return (await subPathResponse.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Sub-path failed, use root metadata\n }\n\n // Return root metadata as it applies to this resource\n return rootMetadata;\n }\n // Otherwise, try sub-path location before giving up\n }\n } catch {\n // Continue to sub-path location\n }\n\n // Strategy 2: Try sub-path location (MCP spec extension)\n // Only try if there's a path component\n if (path) {\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n\n try {\n const response = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Fall through to return null\n }\n }\n\n // Neither location found or resource didn't match\n return null;\n } catch (_error) {\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\nasync function discoverProtectedResourceMetadataFromHeader(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n let header = response.headers.get('www-authenticate');\n if (!header) {\n const postResponse = await fetch(resourceUrl, {\n method: 'POST',\n headers: { Accept: 'application/json', Connection: 'close', 'Content-Type': 'application/json' },\n body: '{}',\n });\n header = postResponse.headers.get('www-authenticate');\n }\n\n if (!header) return null;\n\n const match = header.match(/resource_metadata=\"([^\"]+)\"/i);\n if (!match || !match[1]) return null;\n\n const metadataUrl = match[1];\n const metadataResponse = await fetch(metadataUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return (await metadataResponse.json()) as ProtectedResourceMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Probes .well-known/oauth-authorization-server endpoint\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)\n * @returns AuthorizationServerMetadata if discovered, null otherwise\n *\n * @example\n * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');\n * // Returns: { issuer: \"https://todoist.com\", authorization_endpoint: \"...\", ... }\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string): Promise<AuthorizationServerMetadata | null> {\n try {\n const normalizedAuthServerUrl = normalizeUrl(authServerUrl);\n const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');\n const localResponse = await fetch(localWellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (localResponse.ok) {\n return (await localResponse.json()) as AuthorizationServerMetadata;\n }\n\n const origin = getOrigin(normalizedAuthServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await fetch(wellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!response.ok) {\n return null;\n }\n\n return (await response.json()) as AuthorizationServerMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth Authorization Server Issuer from resource response (RFC 9207)\n *\n * @param resourceUrl - URL of the protected resource\n * @returns Issuer URL if present in WWW-Authenticate header, null otherwise\n */\nexport async function discoverAuthorizationServerIssuer(resourceUrl: string): Promise<string | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n const header = response.headers.get('www-authenticate');\n if (!header) return null;\n\n const match = header.match(/(?:authorization_server|issuer)=\"([^\"]+)\"/i);\n if (!match) return null;\n\n return match[1] ?? null;\n } catch (_error) {\n return null;\n }\n}\n"],"names":["joinWellKnown","normalizeUrl","getOrigin","url","URL","origin","getPath","parsed","pathname","discoverProtectedResourceMetadata","resourceUrl","normalizedResourceUrl","headerMetadata","discoverProtectedResourceMetadataFromHeader","localWellKnownUrl","response","fetch","method","headers","Accept","Connection","ok","json","path","rootUrl","metadata","resource","startsWith","rootMetadata","subPathUrl","subPathResponse","_error","header","get","postResponse","body","match","metadataUrl","metadataResponse","discoverAuthorizationServerMetadata","authServerUrl","normalizedAuthServerUrl","localResponse","wellKnownUrl","discoverAuthorizationServerIssuer"],"mappings":"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"}
|
|
@@ -18,6 +18,15 @@ interface RegistryLike {
|
|
|
18
18
|
servers: Map<string, ServerProcess>;
|
|
19
19
|
}
|
|
20
20
|
import { type Logger } from '../utils/logger.js';
|
|
21
|
+
/**
|
|
22
|
+
* Extract the "server base" by removing a trailing `/mcp` path segment if present.
|
|
23
|
+
* Examples:
|
|
24
|
+
* - https://example.com/mcp -> https://example.com
|
|
25
|
+
* - https://example.com/sheets/mcp -> https://example.com/sheets
|
|
26
|
+
* - https://example.com/sheets/mcp/ -> https://example.com/sheets
|
|
27
|
+
* - https://example.com/sheets -> https://example.com/sheets
|
|
28
|
+
*/
|
|
29
|
+
export declare function extractBaseUrl(mcpUrl: string): string;
|
|
21
30
|
/**
|
|
22
31
|
* Connect MCP SDK client to server with full readiness handling.
|
|
23
32
|
* @internal - Use registry.connect() instead
|
|
@@ -31,12 +31,28 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
|
|
|
31
31
|
]);
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* Extract base
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
34
|
+
* Extract the "server base" by removing a trailing `/mcp` path segment if present.
|
|
35
|
+
* Examples:
|
|
36
|
+
* - https://example.com/mcp -> https://example.com
|
|
37
|
+
* - https://example.com/sheets/mcp -> https://example.com/sheets
|
|
38
|
+
* - https://example.com/sheets/mcp/ -> https://example.com/sheets
|
|
39
|
+
* - https://example.com/sheets -> https://example.com/sheets
|
|
40
|
+
*/ export function extractBaseUrl(mcpUrl) {
|
|
38
41
|
const url = new URL(mcpUrl);
|
|
39
|
-
|
|
42
|
+
// Ignore query/hash for base URL purposes
|
|
43
|
+
url.search = '';
|
|
44
|
+
url.hash = '';
|
|
45
|
+
// Normalize path segments (removes empty segments from leading/trailing slashes)
|
|
46
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
47
|
+
// If last segment is exactly "mcp", drop it
|
|
48
|
+
if (segments[segments.length - 1] === 'mcp') {
|
|
49
|
+
segments.pop();
|
|
50
|
+
}
|
|
51
|
+
// Rebuild pathname; empty means root
|
|
52
|
+
url.pathname = segments.length ? `/${segments.join('/')}` : '';
|
|
53
|
+
// Return without trailing slash (except root origin)
|
|
54
|
+
const out = url.origin + url.pathname;
|
|
55
|
+
return out === url.origin ? out : out.replace(/\/+$/, '');
|
|
40
56
|
}
|
|
41
57
|
/**
|
|
42
58
|
* Infer transport type from server configuration with validation.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/connection/connect-client.ts"],"sourcesContent":["/**\n * connect-mcp-client.ts\n *\n * Helper to connect MCP SDK clients to servers with intelligent transport inference.\n * Automatically detects transport type from URL protocol or type field.\n */\n\nimport '../monkey-patches.ts';\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport getPort from 'get-port';\nimport { probeAuthCapabilities } from '../auth/index.ts';\nimport { DcrAuthenticator, type DcrAuthenticatorOptions } from '../dcr/index.ts';\nimport type { ServerProcess } from '../spawn/spawn-server.ts';\nimport type { ServersConfig } from '../spawn/spawn-servers.ts';\n\n/**\n * Minimal interface for connecting to servers.\n * Only needs config and servers map for connection logic.\n */\ninterface RegistryLike {\n config: ServersConfig;\n servers: Map<string, ServerProcess>;\n}\n\nimport type { McpServerEntry, TransportType } from '../types.ts';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { ExistingProcessTransport } from './existing-process-transport.ts';\nimport { waitForHttpReady } from './wait-for-http-ready.ts';\n\n/**\n * Wrap promise with timeout - throws if promise takes too long\n * Clears timeout when promise completes to prevent hanging event loop\n * @param promise - Promise to wrap\n * @param ms - Timeout in milliseconds\n * @param operation - Description of operation for error message\n * @returns Promise result or timeout error\n */\nasync function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {\n let timeoutId: NodeJS.Timeout;\n\n return Promise.race([\n promise.finally(() => clearTimeout(timeoutId)),\n new Promise<T>((_, reject) => {\n timeoutId = setTimeout(() => reject(new Error(`Timeout after ${ms}ms: ${operation}`)), ms);\n }),\n ]);\n}\n\n/**\n * Extract base URL from MCP server URL\n * @param mcpUrl - Full MCP endpoint URL (e.g., https://example.com/mcp)\n * @returns Base URL (e.g., https://example.com)\n */\nfunction extractBaseUrl(mcpUrl: string): string {\n const url = new URL(mcpUrl);\n return `${url.protocol}//${url.host}`;\n}\n\n/**\n * Infer transport type from server configuration with validation.\n *\n * Priority:\n * 1. Explicit type field (if present)\n * 2. URL protocol (if URL present): http://, https://\n * 3. Default to 'stdio' (if neither present)\n *\n * @param config - Server configuration\n * @returns Transport type\n * @throws Error if configuration is invalid or has conflicts\n */\nfunction inferTransportType(config: McpServerEntry): TransportType {\n // Priority 1: Explicit type field\n if (config.type) {\n // Validate consistency with URL if both present\n if (config.url) {\n const url = new URL(config.url);\n const protocol = url.protocol;\n\n if ((protocol === 'http:' || protocol === 'https:') && config.type !== 'http' && config.type !== 'sse-ide') {\n throw new Error(`Conflicting transport: URL protocol '${protocol}' requires type 'http', but got '${config.type}'`);\n }\n }\n\n // Return normalized type\n if (config.type === 'http' || config.type === 'sse-ide') return 'http';\n if (config.type === 'stdio') return 'stdio';\n\n throw new Error(`Unsupported transport type: ${config.type}`);\n }\n\n // Priority 2: Infer from URL protocol\n if (config.url) {\n const url = new URL(config.url);\n const protocol = url.protocol;\n\n if (protocol === 'http:' || protocol === 'https:') {\n return 'http';\n }\n throw new Error(`Unsupported URL protocol: ${protocol}`);\n }\n\n // Priority 3: Default to stdio\n return 'stdio';\n}\n\n/**\n * Connect MCP SDK client to server with full readiness handling.\n * @internal - Use registry.connect() instead\n *\n * **Completely handles readiness**: transport availability + MCP protocol handshake.\n *\n * Transport is intelligently inferred and handled:\n * - **Stdio servers**: Direct MCP connect (fast for spawned processes)\n * - **HTTP servers**: Transport polling (/mcp endpoint) + MCP connect\n * - **Registry result**: Handles both spawned and external servers\n *\n * Returns only when server is fully MCP-ready (initialize handshake complete).\n *\n * @param registryOrConfig - Result from createServerRegistry() or servers config object\n * @param serverName - Server name from servers config\n * @returns Connected MCP SDK Client (guaranteed ready)\n *\n * @example\n * // Using registry (recommended)\n * const registry = createServerRegistry({ echo: { command: 'node', args: ['server.ts'] } });\n * const client = await registry.connect('echo');\n * // Server is fully ready - transport available + MCP handshake complete\n *\n * @example\n * // HTTP server readiness (waits for /mcp polling + MCP handshake)\n * const registry = createServerRegistry(\n * { http: { type: 'http', url: 'http://localhost:3000/mcp', start: {...} } },\n * { dialects: ['start'] }\n * );\n * const client = await registry.connect('http');\n * // 1. Waits for HTTP server to respond on /mcp\n * // 2. Performs MCP initialize handshake\n * // 3. Returns ready client\n */\nexport async function connectMcpClient(\n registryOrConfig: RegistryLike | ServersConfig,\n serverName: string,\n options?: {\n dcrAuthenticator?: Partial<DcrAuthenticatorOptions>;\n logger?: Logger;\n }\n): Promise<Client> {\n // Detect whether we have a RegistryLike instance or just config\n const isRegistry = 'servers' in registryOrConfig && registryOrConfig.servers instanceof Map;\n const serversConfig = isRegistry ? (registryOrConfig as RegistryLike).config : registryOrConfig;\n const registry = isRegistry ? (registryOrConfig as RegistryLike) : undefined;\n const logger = options?.logger ?? defaultLogger;\n\n const serverConfig = serversConfig[serverName];\n\n if (!serverConfig) {\n const available = Object.keys(serversConfig).join(', ');\n throw new Error(`Server '${serverName}' not found in config. Available servers: ${available || 'none'}`);\n }\n\n // Infer transport type with validation\n const transportType = inferTransportType(serverConfig);\n\n // Create MCP client\n const client = new Client({ name: 'mcp-cli-client', version: '1.0.0' }, { capabilities: {} });\n\n // Connect based on inferred transport\n if (transportType === 'stdio') {\n // Check if we have a spawned process in the registry\n const serverHandle = registry?.servers.get(serverName);\n\n if (serverHandle) {\n // Reuse the already-spawned process\n const transport = new ExistingProcessTransport(serverHandle.process);\n await client.connect(transport);\n } else {\n // No registry or server not in registry - spawn new process directly\n // This is the standard fallback when process management is not used\n if (!serverConfig.command) {\n throw new Error(`Server '${serverName}' has stdio transport but missing 'command' field`);\n }\n\n const transport = new StdioClientTransport({\n command: serverConfig.command,\n args: serverConfig.args || [],\n env: serverConfig.env || {},\n });\n\n // client.connect() performs initialize handshake - when it resolves, server is ready\n await client.connect(transport);\n }\n } else if (transportType === 'http') {\n if (!('url' in serverConfig) || !serverConfig.url) {\n throw new Error(`Server '${serverName}' has http transport but missing 'url' field`);\n }\n\n // Check if this is a freshly spawned HTTP server (from registry)\n // that might not be ready yet - transport readiness check needed\n const isSpawnedHttp = registry?.servers.has(serverName);\n\n if (isSpawnedHttp) {\n logger.debug(`[connectMcpClient] waiting for HTTP server '${serverName}' at ${serverConfig.url}`);\n await waitForHttpReady(serverConfig.url);\n logger.debug(`[connectMcpClient] HTTP server '${serverName}' ready`);\n }\n\n const url = new URL(serverConfig.url);\n\n // Check for DCR support and handle authentication automatically\n const baseUrl = extractBaseUrl(serverConfig.url);\n const capabilities = await withTimeout(probeAuthCapabilities(baseUrl), 5000, 'DCR capability discovery');\n\n let authToken: string | undefined;\n\n if (capabilities.supportsDcr) {\n logger.debug(`đ Server '${serverName}' supports DCR authentication`);\n\n // Get available port and create the exact redirect URI to use\n const port = await getPort();\n const redirectUri = `http://localhost:${port}/callback`;\n\n // Handle authentication using DcrAuthenticator with fully resolved redirectUri\n const authenticator = new DcrAuthenticator({\n headless: false,\n redirectUri,\n logger,\n ...options?.dcrAuthenticator,\n });\n\n // Ensure we have valid tokens (performs DCR + OAuth if needed)\n const tokens = await authenticator.ensureAuthenticated(baseUrl, capabilities);\n authToken = tokens.accessToken;\n\n logger.debug(`â
Authentication complete for '${serverName}'`);\n } else {\n logger.debug(`âšī¸ Server '${serverName}' does not support DCR - connecting without authentication`);\n }\n\n try {\n // Try modern Streamable HTTP first (protocol version 2025-03-26)\n // Merge static headers from config with DCR auth headers (DCR Authorization takes precedence)\n const staticHeaders = serverConfig.headers || {};\n const dcrHeaders = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n const mergedHeaders = { ...staticHeaders, ...dcrHeaders };\n\n const transportOptions =\n Object.keys(mergedHeaders).length > 0\n ? {\n requestInit: {\n headers: mergedHeaders,\n },\n }\n : undefined;\n\n const transport = new StreamableHTTPClientTransport(url, transportOptions);\n // Type assertion: SDK transport has sessionId: string | undefined but Transport expects string\n // This is safe at runtime - the undefined is valid per MCP spec\n await withTimeout(client.connect(transport as unknown as Transport), 30000, 'StreamableHTTP connection');\n } catch (error) {\n // Fall back to SSE transport (MCP protocol version 2024-11-05)\n // SSE is a standard MCP transport used by many servers (e.g., FastMCP ecosystem)\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Fast-fail: Don't try SSE if connection was refused (server not running)\n // Check error.cause.code for ECONNREFUSED (fetch errors wrap the actual error in cause)\n const cause = error instanceof Error ? (error as Error & { cause?: { code?: string } }).cause : undefined;\n const isConnectionRefused = cause?.code === 'ECONNREFUSED' || errorMessage.includes('Connection refused');\n\n if (isConnectionRefused) {\n // Clean up client resources before throwing\n await client.close().catch(() => {});\n throw new Error(`Server not running at ${url}`);\n }\n\n // Check for known errors that indicate SSE fallback is needed\n const shouldFallback =\n errorMessage.includes('Missing session ID') || // FastMCP specific\n errorMessage.includes('404') || // Server doesn't have streamable HTTP endpoint\n errorMessage.includes('405'); // Method not allowed\n\n if (shouldFallback) {\n logger.warn(`Streamable HTTP failed (${errorMessage}), falling back to SSE transport`);\n } else {\n logger.warn('Streamable HTTP connection failed, trying SSE transport as fallback');\n }\n\n // Create new client for SSE transport (required per SDK pattern)\n const sseClient = new Client({ name: 'mcp-cli-client', version: '1.0.0' }, { capabilities: {} });\n\n // SSE transport with merged headers (static + DCR auth)\n // Reuse the same header merging logic as Streamable HTTP\n const staticHeaders = serverConfig.headers || {};\n const dcrHeaders = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n const mergedHeaders = { ...staticHeaders, ...dcrHeaders };\n\n const sseTransportOptions =\n Object.keys(mergedHeaders).length > 0\n ? {\n requestInit: {\n headers: mergedHeaders,\n },\n }\n : undefined;\n\n const sseTransport = new SSEClientTransport(url, sseTransportOptions);\n\n try {\n await withTimeout(sseClient.connect(sseTransport), 30000, 'SSE connection');\n // Return SSE client instead of original\n return sseClient;\n } catch (sseError) {\n // SSE connection failed - clean up both clients before throwing\n await Promise.all([client.close().catch(() => {}), sseClient.close().catch(() => {})]);\n throw sseError;\n }\n }\n }\n\n return client; // Guaranteed ready when returned\n}\n"],"names":["Client","SSEClientTransport","StdioClientTransport","StreamableHTTPClientTransport","getPort","probeAuthCapabilities","DcrAuthenticator","logger","defaultLogger","ExistingProcessTransport","waitForHttpReady","withTimeout","promise","ms","operation","timeoutId","Promise","race","finally","clearTimeout","_","reject","setTimeout","Error","extractBaseUrl","mcpUrl","url","URL","protocol","host","inferTransportType","config","type","connectMcpClient","registryOrConfig","serverName","options","isRegistry","servers","Map","serversConfig","registry","undefined","serverConfig","available","Object","keys","join","transportType","client","name","version","capabilities","serverHandle","get","transport","process","connect","command","args","env","isSpawnedHttp","has","debug","baseUrl","authToken","supportsDcr","port","redirectUri","authenticator","headless","dcrAuthenticator","tokens","ensureAuthenticated","accessToken","staticHeaders","headers","dcrHeaders","Authorization","mergedHeaders","transportOptions","length","requestInit","error","errorMessage","message","String","cause","isConnectionRefused","code","includes","close","catch","shouldFallback","warn","sseClient","sseTransportOptions","sseTransport","sseError","all"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/connection/connect-client.ts"],"sourcesContent":["/**\n * connect-mcp-client.ts\n *\n * Helper to connect MCP SDK clients to servers with intelligent transport inference.\n * Automatically detects transport type from URL protocol or type field.\n */\n\nimport '../monkey-patches.ts';\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport getPort from 'get-port';\nimport { probeAuthCapabilities } from '../auth/index.ts';\nimport { DcrAuthenticator, type DcrAuthenticatorOptions } from '../dcr/index.ts';\nimport type { ServerProcess } from '../spawn/spawn-server.ts';\nimport type { ServersConfig } from '../spawn/spawn-servers.ts';\n\n/**\n * Minimal interface for connecting to servers.\n * Only needs config and servers map for connection logic.\n */\ninterface RegistryLike {\n config: ServersConfig;\n servers: Map<string, ServerProcess>;\n}\n\nimport type { McpServerEntry, TransportType } from '../types.ts';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { ExistingProcessTransport } from './existing-process-transport.ts';\nimport { waitForHttpReady } from './wait-for-http-ready.ts';\n\n/**\n * Wrap promise with timeout - throws if promise takes too long\n * Clears timeout when promise completes to prevent hanging event loop\n * @param promise - Promise to wrap\n * @param ms - Timeout in milliseconds\n * @param operation - Description of operation for error message\n * @returns Promise result or timeout error\n */\nasync function withTimeout<T>(promise: Promise<T>, ms: number, operation: string): Promise<T> {\n let timeoutId: NodeJS.Timeout;\n\n return Promise.race([\n promise.finally(() => clearTimeout(timeoutId)),\n new Promise<T>((_, reject) => {\n timeoutId = setTimeout(() => reject(new Error(`Timeout after ${ms}ms: ${operation}`)), ms);\n }),\n ]);\n}\n\n/**\n * Extract the \"server base\" by removing a trailing `/mcp` path segment if present.\n * Examples:\n * - https://example.com/mcp -> https://example.com\n * - https://example.com/sheets/mcp -> https://example.com/sheets\n * - https://example.com/sheets/mcp/ -> https://example.com/sheets\n * - https://example.com/sheets -> https://example.com/sheets\n */\nexport function extractBaseUrl(mcpUrl: string): string {\n const url = new URL(mcpUrl);\n\n // Ignore query/hash for base URL purposes\n url.search = '';\n url.hash = '';\n\n // Normalize path segments (removes empty segments from leading/trailing slashes)\n const segments = url.pathname.split('/').filter(Boolean);\n\n // If last segment is exactly \"mcp\", drop it\n if (segments[segments.length - 1] === 'mcp') {\n segments.pop();\n }\n\n // Rebuild pathname; empty means root\n url.pathname = segments.length ? `/${segments.join('/')}` : '';\n\n // Return without trailing slash (except root origin)\n const out = url.origin + url.pathname;\n return out === url.origin ? out : out.replace(/\\/+$/, '');\n}\n\n/**\n * Infer transport type from server configuration with validation.\n *\n * Priority:\n * 1. Explicit type field (if present)\n * 2. URL protocol (if URL present): http://, https://\n * 3. Default to 'stdio' (if neither present)\n *\n * @param config - Server configuration\n * @returns Transport type\n * @throws Error if configuration is invalid or has conflicts\n */\nfunction inferTransportType(config: McpServerEntry): TransportType {\n // Priority 1: Explicit type field\n if (config.type) {\n // Validate consistency with URL if both present\n if (config.url) {\n const url = new URL(config.url);\n const protocol = url.protocol;\n\n if ((protocol === 'http:' || protocol === 'https:') && config.type !== 'http' && config.type !== 'sse-ide') {\n throw new Error(`Conflicting transport: URL protocol '${protocol}' requires type 'http', but got '${config.type}'`);\n }\n }\n\n // Return normalized type\n if (config.type === 'http' || config.type === 'sse-ide') return 'http';\n if (config.type === 'stdio') return 'stdio';\n\n throw new Error(`Unsupported transport type: ${config.type}`);\n }\n\n // Priority 2: Infer from URL protocol\n if (config.url) {\n const url = new URL(config.url);\n const protocol = url.protocol;\n\n if (protocol === 'http:' || protocol === 'https:') {\n return 'http';\n }\n throw new Error(`Unsupported URL protocol: ${protocol}`);\n }\n\n // Priority 3: Default to stdio\n return 'stdio';\n}\n\n/**\n * Connect MCP SDK client to server with full readiness handling.\n * @internal - Use registry.connect() instead\n *\n * **Completely handles readiness**: transport availability + MCP protocol handshake.\n *\n * Transport is intelligently inferred and handled:\n * - **Stdio servers**: Direct MCP connect (fast for spawned processes)\n * - **HTTP servers**: Transport polling (/mcp endpoint) + MCP connect\n * - **Registry result**: Handles both spawned and external servers\n *\n * Returns only when server is fully MCP-ready (initialize handshake complete).\n *\n * @param registryOrConfig - Result from createServerRegistry() or servers config object\n * @param serverName - Server name from servers config\n * @returns Connected MCP SDK Client (guaranteed ready)\n *\n * @example\n * // Using registry (recommended)\n * const registry = createServerRegistry({ echo: { command: 'node', args: ['server.ts'] } });\n * const client = await registry.connect('echo');\n * // Server is fully ready - transport available + MCP handshake complete\n *\n * @example\n * // HTTP server readiness (waits for /mcp polling + MCP handshake)\n * const registry = createServerRegistry(\n * { http: { type: 'http', url: 'http://localhost:3000/mcp', start: {...} } },\n * { dialects: ['start'] }\n * );\n * const client = await registry.connect('http');\n * // 1. Waits for HTTP server to respond on /mcp\n * // 2. Performs MCP initialize handshake\n * // 3. Returns ready client\n */\nexport async function connectMcpClient(\n registryOrConfig: RegistryLike | ServersConfig,\n serverName: string,\n options?: {\n dcrAuthenticator?: Partial<DcrAuthenticatorOptions>;\n logger?: Logger;\n }\n): Promise<Client> {\n // Detect whether we have a RegistryLike instance or just config\n const isRegistry = 'servers' in registryOrConfig && registryOrConfig.servers instanceof Map;\n const serversConfig = isRegistry ? (registryOrConfig as RegistryLike).config : registryOrConfig;\n const registry = isRegistry ? (registryOrConfig as RegistryLike) : undefined;\n const logger = options?.logger ?? defaultLogger;\n\n const serverConfig = serversConfig[serverName];\n\n if (!serverConfig) {\n const available = Object.keys(serversConfig).join(', ');\n throw new Error(`Server '${serverName}' not found in config. Available servers: ${available || 'none'}`);\n }\n\n // Infer transport type with validation\n const transportType = inferTransportType(serverConfig);\n\n // Create MCP client\n const client = new Client({ name: 'mcp-cli-client', version: '1.0.0' }, { capabilities: {} });\n\n // Connect based on inferred transport\n if (transportType === 'stdio') {\n // Check if we have a spawned process in the registry\n const serverHandle = registry?.servers.get(serverName);\n\n if (serverHandle) {\n // Reuse the already-spawned process\n const transport = new ExistingProcessTransport(serverHandle.process);\n await client.connect(transport);\n } else {\n // No registry or server not in registry - spawn new process directly\n // This is the standard fallback when process management is not used\n if (!serverConfig.command) {\n throw new Error(`Server '${serverName}' has stdio transport but missing 'command' field`);\n }\n\n const transport = new StdioClientTransport({\n command: serverConfig.command,\n args: serverConfig.args || [],\n env: serverConfig.env || {},\n });\n\n // client.connect() performs initialize handshake - when it resolves, server is ready\n await client.connect(transport);\n }\n } else if (transportType === 'http') {\n if (!('url' in serverConfig) || !serverConfig.url) {\n throw new Error(`Server '${serverName}' has http transport but missing 'url' field`);\n }\n\n // Check if this is a freshly spawned HTTP server (from registry)\n // that might not be ready yet - transport readiness check needed\n const isSpawnedHttp = registry?.servers.has(serverName);\n\n if (isSpawnedHttp) {\n logger.debug(`[connectMcpClient] waiting for HTTP server '${serverName}' at ${serverConfig.url}`);\n await waitForHttpReady(serverConfig.url);\n logger.debug(`[connectMcpClient] HTTP server '${serverName}' ready`);\n }\n\n const url = new URL(serverConfig.url);\n\n // Check for DCR support and handle authentication automatically\n const baseUrl = extractBaseUrl(serverConfig.url);\n const capabilities = await withTimeout(probeAuthCapabilities(baseUrl), 5000, 'DCR capability discovery');\n\n let authToken: string | undefined;\n\n if (capabilities.supportsDcr) {\n logger.debug(`đ Server '${serverName}' supports DCR authentication`);\n\n // Get available port and create the exact redirect URI to use\n const port = await getPort();\n const redirectUri = `http://localhost:${port}/callback`;\n\n // Handle authentication using DcrAuthenticator with fully resolved redirectUri\n const authenticator = new DcrAuthenticator({\n headless: false,\n redirectUri,\n logger,\n ...options?.dcrAuthenticator,\n });\n\n // Ensure we have valid tokens (performs DCR + OAuth if needed)\n const tokens = await authenticator.ensureAuthenticated(baseUrl, capabilities);\n authToken = tokens.accessToken;\n\n logger.debug(`â
Authentication complete for '${serverName}'`);\n } else {\n logger.debug(`âšī¸ Server '${serverName}' does not support DCR - connecting without authentication`);\n }\n\n try {\n // Try modern Streamable HTTP first (protocol version 2025-03-26)\n // Merge static headers from config with DCR auth headers (DCR Authorization takes precedence)\n const staticHeaders = serverConfig.headers || {};\n const dcrHeaders = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n const mergedHeaders = { ...staticHeaders, ...dcrHeaders };\n\n const transportOptions =\n Object.keys(mergedHeaders).length > 0\n ? {\n requestInit: {\n headers: mergedHeaders,\n },\n }\n : undefined;\n\n const transport = new StreamableHTTPClientTransport(url, transportOptions);\n // Type assertion: SDK transport has sessionId: string | undefined but Transport expects string\n // This is safe at runtime - the undefined is valid per MCP spec\n await withTimeout(client.connect(transport as unknown as Transport), 30000, 'StreamableHTTP connection');\n } catch (error) {\n // Fall back to SSE transport (MCP protocol version 2024-11-05)\n // SSE is a standard MCP transport used by many servers (e.g., FastMCP ecosystem)\n const errorMessage = error instanceof Error ? error.message : String(error);\n\n // Fast-fail: Don't try SSE if connection was refused (server not running)\n // Check error.cause.code for ECONNREFUSED (fetch errors wrap the actual error in cause)\n const cause = error instanceof Error ? (error as Error & { cause?: { code?: string } }).cause : undefined;\n const isConnectionRefused = cause?.code === 'ECONNREFUSED' || errorMessage.includes('Connection refused');\n\n if (isConnectionRefused) {\n // Clean up client resources before throwing\n await client.close().catch(() => {});\n throw new Error(`Server not running at ${url}`);\n }\n\n // Check for known errors that indicate SSE fallback is needed\n const shouldFallback =\n errorMessage.includes('Missing session ID') || // FastMCP specific\n errorMessage.includes('404') || // Server doesn't have streamable HTTP endpoint\n errorMessage.includes('405'); // Method not allowed\n\n if (shouldFallback) {\n logger.warn(`Streamable HTTP failed (${errorMessage}), falling back to SSE transport`);\n } else {\n logger.warn('Streamable HTTP connection failed, trying SSE transport as fallback');\n }\n\n // Create new client for SSE transport (required per SDK pattern)\n const sseClient = new Client({ name: 'mcp-cli-client', version: '1.0.0' }, { capabilities: {} });\n\n // SSE transport with merged headers (static + DCR auth)\n // Reuse the same header merging logic as Streamable HTTP\n const staticHeaders = serverConfig.headers || {};\n const dcrHeaders = authToken ? { Authorization: `Bearer ${authToken}` } : {};\n const mergedHeaders = { ...staticHeaders, ...dcrHeaders };\n\n const sseTransportOptions =\n Object.keys(mergedHeaders).length > 0\n ? {\n requestInit: {\n headers: mergedHeaders,\n },\n }\n : undefined;\n\n const sseTransport = new SSEClientTransport(url, sseTransportOptions);\n\n try {\n await withTimeout(sseClient.connect(sseTransport), 30000, 'SSE connection');\n // Return SSE client instead of original\n return sseClient;\n } catch (sseError) {\n // SSE connection failed - clean up both clients before throwing\n await Promise.all([client.close().catch(() => {}), sseClient.close().catch(() => {})]);\n throw sseError;\n }\n }\n }\n\n return client; // Guaranteed ready when returned\n}\n"],"names":["Client","SSEClientTransport","StdioClientTransport","StreamableHTTPClientTransport","getPort","probeAuthCapabilities","DcrAuthenticator","logger","defaultLogger","ExistingProcessTransport","waitForHttpReady","withTimeout","promise","ms","operation","timeoutId","Promise","race","finally","clearTimeout","_","reject","setTimeout","Error","extractBaseUrl","mcpUrl","url","URL","search","hash","segments","pathname","split","filter","Boolean","length","pop","join","out","origin","replace","inferTransportType","config","type","protocol","connectMcpClient","registryOrConfig","serverName","options","isRegistry","servers","Map","serversConfig","registry","undefined","serverConfig","available","Object","keys","transportType","client","name","version","capabilities","serverHandle","get","transport","process","connect","command","args","env","isSpawnedHttp","has","debug","baseUrl","authToken","supportsDcr","port","redirectUri","authenticator","headless","dcrAuthenticator","tokens","ensureAuthenticated","accessToken","staticHeaders","headers","dcrHeaders","Authorization","mergedHeaders","transportOptions","requestInit","error","errorMessage","message","String","cause","isConnectionRefused","code","includes","close","catch","shouldFallback","warn","sseClient","sseTransportOptions","sseTransport","sseError","all"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function normalizeUrl(input) {
|
|
2
|
+
try {
|
|
3
|
+
const url = new URL(input);
|
|
4
|
+
url.search = '';
|
|
5
|
+
url.hash = '';
|
|
6
|
+
url.pathname = url.pathname.replace(/\/+$/, '');
|
|
7
|
+
return url.origin + url.pathname;
|
|
8
|
+
} catch {
|
|
9
|
+
return input.replace(/\/+$/, '');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function joinWellKnown(baseUrl, suffix) {
|
|
13
|
+
return `${normalizeUrl(baseUrl)}${suffix}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/Projects/mcp-z/client/src/lib/url-utils.ts"],"sourcesContent":["export function normalizeUrl(input: string): string {\n try {\n const url = new URL(input);\n url.search = '';\n url.hash = '';\n url.pathname = url.pathname.replace(/\\/+$/, '');\n return url.origin + url.pathname;\n } catch {\n return input.replace(/\\/+$/, '');\n }\n}\n\nexport function joinWellKnown(baseUrl: string, suffix: string): string {\n return `${normalizeUrl(baseUrl)}${suffix}`;\n}\n"],"names":["normalizeUrl","input","url","URL","search","hash","pathname","replace","origin","joinWellKnown","baseUrl","suffix"],"mappings":"AAAA,OAAO,SAASA,aAAaC,KAAa;IACxC,IAAI;QACF,MAAMC,MAAM,IAAIC,IAAIF;QACpBC,IAAIE,MAAM,GAAG;QACbF,IAAIG,IAAI,GAAG;QACXH,IAAII,QAAQ,GAAGJ,IAAII,QAAQ,CAACC,OAAO,CAAC,QAAQ;QAC5C,OAAOL,IAAIM,MAAM,GAAGN,IAAII,QAAQ;IAClC,EAAE,OAAM;QACN,OAAOL,MAAMM,OAAO,CAAC,QAAQ;IAC/B;AACF;AAEA,OAAO,SAASE,cAAcC,OAAe,EAAEC,MAAc;IAC3D,OAAO,GAAGX,aAAaU,WAAWC,QAAQ;AAC5C"}
|