@mcp-z/client 2.0.1 → 2.2.0

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.
Files changed (39) hide show
  1. package/README.md +22 -0
  2. package/dist/cjs/auth/capability-discovery.js +11 -5
  3. package/dist/cjs/auth/capability-discovery.js.map +1 -1
  4. package/dist/cjs/auth/types.d.cts +11 -0
  5. package/dist/cjs/auth/types.d.ts +11 -0
  6. package/dist/cjs/auth/types.js.map +1 -1
  7. package/dist/cjs/connection/connect-client.d.cts +11 -9
  8. package/dist/cjs/connection/connect-client.d.ts +11 -9
  9. package/dist/cjs/connection/connect-client.js +25 -38
  10. package/dist/cjs/connection/connect-client.js.map +1 -1
  11. package/dist/cjs/dcr/dcr-authenticator.d.cts +30 -4
  12. package/dist/cjs/dcr/dcr-authenticator.d.ts +30 -4
  13. package/dist/cjs/dcr/dcr-authenticator.js +57 -24
  14. package/dist/cjs/dcr/dcr-authenticator.js.map +1 -1
  15. package/dist/cjs/index.d.cts +2 -0
  16. package/dist/cjs/index.d.ts +2 -0
  17. package/dist/cjs/index.js +8 -2
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/lib/url-utils.d.cts +17 -0
  20. package/dist/cjs/lib/url-utils.d.ts +17 -0
  21. package/dist/cjs/lib/url-utils.js +20 -0
  22. package/dist/cjs/lib/url-utils.js.map +1 -1
  23. package/dist/esm/auth/capability-discovery.js +11 -5
  24. package/dist/esm/auth/capability-discovery.js.map +1 -1
  25. package/dist/esm/auth/types.d.ts +11 -0
  26. package/dist/esm/auth/types.js.map +1 -1
  27. package/dist/esm/connection/connect-client.d.ts +11 -9
  28. package/dist/esm/connection/connect-client.js +30 -33
  29. package/dist/esm/connection/connect-client.js.map +1 -1
  30. package/dist/esm/dcr/dcr-authenticator.d.ts +30 -4
  31. package/dist/esm/dcr/dcr-authenticator.js +56 -23
  32. package/dist/esm/dcr/dcr-authenticator.js.map +1 -1
  33. package/dist/esm/index.d.ts +2 -0
  34. package/dist/esm/index.js +4 -1
  35. package/dist/esm/index.js.map +1 -1
  36. package/dist/esm/lib/url-utils.d.ts +17 -0
  37. package/dist/esm/lib/url-utils.js +32 -0
  38. package/dist/esm/lib/url-utils.js.map +1 -1
  39. package/package.json +1 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/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 { isLoopbackUrl } from './discovery-fetch.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, authorizationResponseIssSupported: metadata.authorization_response_iss_parameter_supported === true };\n\n if (metadata.issuer) {\n capabilities.issuer = metadata.issuer;\n }\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[] | undefined, allowLoopback: boolean): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl, { allowLoopback });\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 // Trust signal for every authorization-server fetch below: whether the\n // configured MCP server is itself loopback, never a remote-supplied URL.\n const allowLoopback = isLoopbackUrl(normalizedBaseUrl);\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, allowLoopback);\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, allowLoopback);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, undefined, allowLoopback);\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, undefined, allowLoopback);\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","isLoopbackUrl","discoverAuthorizationServerIssuer","discoverAuthorizationServerMetadata","discoverProtectedResourceMetadata","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","supportsDcr","registration_endpoint","capabilities","authorizationResponseIssSupported","authorization_response_iss_parameter_supported","issuer","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","allowLoopback","probeAuthCapabilities","baseUrl","normalizedBaseUrl","resourceMetadata","authorization_servers","issuerCapabilities","undefined","originCapabilities","_error"],"mappings":"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"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/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 { isLoopbackUrl } from './discovery-fetch.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[], resource?: string): AuthCapabilities {\n const supportsDcr = !!metadata.registration_endpoint;\n const capabilities: AuthCapabilities = { supportsDcr, authorizationResponseIssSupported: metadata.authorization_response_iss_parameter_supported === true };\n\n if (metadata.issuer) {\n capabilities.issuer = metadata.issuer;\n }\n\n // Carried from the RFC 9728 document, not from the URL we were given: the\n // resource server names itself, and that name is what RFC 8707 audience-binds\n // a token to.\n if (resource) {\n capabilities.resource = resource;\n }\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[] | undefined, allowLoopback: boolean, resource?: string): Promise<AuthCapabilities | null> {\n const metadata = await discoverAuthorizationServerMetadata(authServerUrl, { allowLoopback });\n if (!metadata) return null;\n return buildCapabilities(metadata, scopes, resource);\n}\n\nexport async function probeAuthCapabilities(baseUrl: string): Promise<AuthCapabilities> {\n try {\n const normalizedBaseUrl = normalizeUrl(baseUrl);\n // Trust signal for every authorization-server fetch below: whether the\n // configured MCP server is itself loopback, never a remote-supplied URL.\n const allowLoopback = isLoopbackUrl(normalizedBaseUrl);\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, allowLoopback, resourceMetadata.resource);\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, allowLoopback, resourceMetadata.resource);\n if (issuerCapabilities) return issuerCapabilities;\n }\n }\n\n const issuer = await discoverAuthorizationServerIssuer(normalizedBaseUrl);\n if (issuer) {\n const issuerCapabilities = await resolveCapabilitiesFromAuthorizationServer(issuer, undefined, allowLoopback);\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, undefined, allowLoopback);\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","isLoopbackUrl","discoverAuthorizationServerIssuer","discoverAuthorizationServerMetadata","discoverProtectedResourceMetadata","getOrigin","url","URL","origin","buildCapabilities","metadata","scopes","resource","supportsDcr","registration_endpoint","capabilities","authorizationResponseIssSupported","authorization_response_iss_parameter_supported","issuer","registrationEndpoint","authorization_endpoint","authorizationEndpoint","token_endpoint","tokenEndpoint","introspection_endpoint","introspectionEndpoint","length","scopes_supported","resolveCapabilitiesFromAuthorizationServer","authServerUrl","allowLoopback","probeAuthCapabilities","baseUrl","normalizedBaseUrl","resourceMetadata","authorization_servers","issuerCapabilities","undefined","originCapabilities","_error"],"mappings":"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"}
@@ -91,6 +91,17 @@ export interface AuthCapabilities {
91
91
  supportsDcr: boolean;
92
92
  /** Issuer identifier from the authorization server metadata (RFC 8414) */
93
93
  issuer?: string;
94
+ /**
95
+ * The protected resource's canonical identifier, from the RFC 9728 metadata
96
+ * document's `resource` field. This is what an RFC 8707 `resource` indicator
97
+ * must carry, and it is the resource server's own statement of its identity -
98
+ * not the URL we happened to dial, and never the base URL discovery was
99
+ * performed against, which has any `/mcp` segment stripped off it.
100
+ *
101
+ * Absent when no protected-resource metadata was published and the
102
+ * authorization server was reached by direct RFC 8414 discovery instead.
103
+ */
104
+ resource?: string;
94
105
  /** Whether the authorization response carries an `iss` parameter (RFC 9207) */
95
106
  authorizationResponseIssSupported?: boolean;
96
107
  /** DCR client registration endpoint */
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/types.ts"],"sourcesContent":["/**\n * Shared types for OAuth and DCR authentication\n */\n\n/**\n * OAuth callback result from authorization server\n */\nexport interface CallbackResult {\n /** Authorization code from OAuth server */\n code: string;\n /** State parameter for CSRF protection */\n state?: string;\n /** Issuer identifier of the authorization server that minted the response (RFC 9207) */\n iss?: string;\n}\n\n/**\n * PKCE (Proof Key for Code Exchange) parameters (RFC 7636)\n * Used to secure OAuth 2.0 authorization code flow for public clients\n */\nexport interface PkceParams {\n /** Code verifier - cryptographically random string (43-128 characters) */\n codeVerifier: string;\n /** Code challenge - derived from code verifier using challenge method */\n codeChallenge: string;\n /** Code challenge method - S256 (SHA-256) or plain */\n codeChallengeMethod: 'S256' | 'plain';\n}\n\n/**\n * OAuth token set with access and refresh tokens\n */\nexport interface TokenSet {\n /** Access token for API requests */\n accessToken: string;\n /** Refresh token for obtaining new access tokens */\n refreshToken: string;\n /** Timestamp when access token expires (milliseconds since epoch) */\n expiresAt: number;\n /** Scopes granted for this token set */\n scopes?: string[];\n /** Client ID used for DCR registration (stored for future use) */\n clientId?: string;\n /** Client secret used for DCR registration (stored for future use) */\n clientSecret?: string;\n /** Issuer identifier of the authorization server these credentials belong to (SEP-2352) */\n issuer?: string;\n}\n\n/**\n * OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Response from .well-known/oauth-protected-resource endpoint\n */\nexport interface ProtectedResourceMetadata {\n /** The protected resource identifier */\n resource: string;\n /** List of authorization server URLs that can issue tokens for this resource */\n authorization_servers: string[];\n /** Optional list of scopes supported by this resource */\n scopes_supported?: string[];\n /** Optional list of bearer token methods supported (header, query, body) */\n bearer_methods_supported?: string[];\n}\n\n/**\n * OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Response from .well-known/oauth-authorization-server endpoint\n */\nexport interface AuthorizationServerMetadata {\n /** The authorization server's issuer identifier */\n issuer?: string;\n /** URL of the authorization endpoint */\n authorization_endpoint?: string;\n /** URL of the token endpoint */\n token_endpoint?: string;\n /** URL of the client registration endpoint (DCR - RFC 7591) */\n registration_endpoint?: string;\n /** URL of the token introspection endpoint */\n introspection_endpoint?: string;\n /** List of OAuth scopes supported by the authorization server */\n scopes_supported?: string[];\n /** Response types supported (code, token, etc.) */\n response_types_supported?: string[];\n /** Grant types supported (authorization_code, refresh_token, etc.) */\n grant_types_supported?: string[];\n /** Token endpoint authentication methods supported */\n token_endpoint_auth_methods_supported?: string[];\n /** Whether the authorization response carries an `iss` parameter (RFC 9207) */\n authorization_response_iss_parameter_supported?: boolean;\n}\n\n/**\n * OAuth server capabilities discovered from .well-known endpoint\n */\nexport interface AuthCapabilities {\n /** Whether the server supports Dynamic Client Registration (RFC 7591) */\n supportsDcr: boolean;\n /** Issuer identifier from the authorization server metadata (RFC 8414) */\n issuer?: string;\n /** Whether the authorization response carries an `iss` parameter (RFC 9207) */\n authorizationResponseIssSupported?: boolean;\n /** DCR client registration endpoint */\n registrationEndpoint?: string;\n /** OAuth authorization endpoint */\n authorizationEndpoint?: string;\n /** OAuth token endpoint */\n tokenEndpoint?: string;\n /** Token introspection endpoint */\n introspectionEndpoint?: string;\n /** Supported OAuth scopes */\n scopes?: string[];\n}\n\n/**\n * Client credentials from DCR registration\n */\nexport interface ClientCredentials {\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Timestamp when client was registered */\n issuedAt?: number;\n}\n\n/**\n * Options for DCR client registration\n */\nexport interface DcrRegistrationOptions {\n /** Redirect URI for OAuth callback, from the loopback listener the caller has already bound (RFC 8252) */\n redirectUri: string;\n /** Client name to register */\n clientName?: string;\n /**\n * Loopback trust grant for the registration_endpoint fetch (SSRF\n * mitigation - see `src/auth/discovery-fetch.ts`). Compute this from the\n * MCP server the caller is actually talking to, never from\n * `registrationEndpoint` itself (which is typically sourced from\n * remote-controlled AS metadata). Defaults to `false`.\n */\n allowLoopback?: boolean;\n}\n\n/**\n * Options for OAuth authorization flow\n */\nexport interface OAuthFlowOptions {\n /** Port for OAuth callback listener (required - use get-port to find available port) */\n port: number;\n /** Issuer identifier discovered before the flow starts; the `iss` in the authorization response must match it (RFC 9207) */\n issuer: string;\n /** Canonical resource server URI, sent as `resource` on the authorization, token, and refresh requests (RFC 8707) */\n resource: string;\n /** Redirect URI for OAuth callback (optional - will be built from port if not provided) */\n redirectUri?: string;\n /** OAuth scopes to request */\n scopes?: string[];\n /** Whether the authorization server advertises `authorization_response_iss_parameter_supported` (RFC 9207) */\n authorizationResponseIssSupported?: boolean;\n /** Enable PKCE (RFC 7636) - recommended for all clients, required for public clients */\n pkce?: boolean;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Timeout for callback (milliseconds) */\n timeout?: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: import('../utils/logger.ts').Logger;\n /**\n * Loopback trust grant for the token endpoint fetch (SSRF mitigation - see\n * `src/auth/discovery-fetch.ts`). Compute this from the MCP server the\n * caller is actually talking to, never from `tokenEndpoint` itself (which\n * is typically sourced from remote-controlled AS metadata). Defaults to\n * `false`.\n */\n allowLoopback?: boolean;\n}\n"],"names":[],"mappings":"AAAA;;CAEC,GAED;;CAEC,GAyID;;CAEC,GACD,WA6BC"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/types.ts"],"sourcesContent":["/**\n * Shared types for OAuth and DCR authentication\n */\n\n/**\n * OAuth callback result from authorization server\n */\nexport interface CallbackResult {\n /** Authorization code from OAuth server */\n code: string;\n /** State parameter for CSRF protection */\n state?: string;\n /** Issuer identifier of the authorization server that minted the response (RFC 9207) */\n iss?: string;\n}\n\n/**\n * PKCE (Proof Key for Code Exchange) parameters (RFC 7636)\n * Used to secure OAuth 2.0 authorization code flow for public clients\n */\nexport interface PkceParams {\n /** Code verifier - cryptographically random string (43-128 characters) */\n codeVerifier: string;\n /** Code challenge - derived from code verifier using challenge method */\n codeChallenge: string;\n /** Code challenge method - S256 (SHA-256) or plain */\n codeChallengeMethod: 'S256' | 'plain';\n}\n\n/**\n * OAuth token set with access and refresh tokens\n */\nexport interface TokenSet {\n /** Access token for API requests */\n accessToken: string;\n /** Refresh token for obtaining new access tokens */\n refreshToken: string;\n /** Timestamp when access token expires (milliseconds since epoch) */\n expiresAt: number;\n /** Scopes granted for this token set */\n scopes?: string[];\n /** Client ID used for DCR registration (stored for future use) */\n clientId?: string;\n /** Client secret used for DCR registration (stored for future use) */\n clientSecret?: string;\n /** Issuer identifier of the authorization server these credentials belong to (SEP-2352) */\n issuer?: string;\n}\n\n/**\n * OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Response from .well-known/oauth-protected-resource endpoint\n */\nexport interface ProtectedResourceMetadata {\n /** The protected resource identifier */\n resource: string;\n /** List of authorization server URLs that can issue tokens for this resource */\n authorization_servers: string[];\n /** Optional list of scopes supported by this resource */\n scopes_supported?: string[];\n /** Optional list of bearer token methods supported (header, query, body) */\n bearer_methods_supported?: string[];\n}\n\n/**\n * OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Response from .well-known/oauth-authorization-server endpoint\n */\nexport interface AuthorizationServerMetadata {\n /** The authorization server's issuer identifier */\n issuer?: string;\n /** URL of the authorization endpoint */\n authorization_endpoint?: string;\n /** URL of the token endpoint */\n token_endpoint?: string;\n /** URL of the client registration endpoint (DCR - RFC 7591) */\n registration_endpoint?: string;\n /** URL of the token introspection endpoint */\n introspection_endpoint?: string;\n /** List of OAuth scopes supported by the authorization server */\n scopes_supported?: string[];\n /** Response types supported (code, token, etc.) */\n response_types_supported?: string[];\n /** Grant types supported (authorization_code, refresh_token, etc.) */\n grant_types_supported?: string[];\n /** Token endpoint authentication methods supported */\n token_endpoint_auth_methods_supported?: string[];\n /** Whether the authorization response carries an `iss` parameter (RFC 9207) */\n authorization_response_iss_parameter_supported?: boolean;\n}\n\n/**\n * OAuth server capabilities discovered from .well-known endpoint\n */\nexport interface AuthCapabilities {\n /** Whether the server supports Dynamic Client Registration (RFC 7591) */\n supportsDcr: boolean;\n /** Issuer identifier from the authorization server metadata (RFC 8414) */\n issuer?: string;\n /**\n * The protected resource's canonical identifier, from the RFC 9728 metadata\n * document's `resource` field. This is what an RFC 8707 `resource` indicator\n * must carry, and it is the resource server's own statement of its identity -\n * not the URL we happened to dial, and never the base URL discovery was\n * performed against, which has any `/mcp` segment stripped off it.\n *\n * Absent when no protected-resource metadata was published and the\n * authorization server was reached by direct RFC 8414 discovery instead.\n */\n resource?: string;\n /** Whether the authorization response carries an `iss` parameter (RFC 9207) */\n authorizationResponseIssSupported?: boolean;\n /** DCR client registration endpoint */\n registrationEndpoint?: string;\n /** OAuth authorization endpoint */\n authorizationEndpoint?: string;\n /** OAuth token endpoint */\n tokenEndpoint?: string;\n /** Token introspection endpoint */\n introspectionEndpoint?: string;\n /** Supported OAuth scopes */\n scopes?: string[];\n}\n\n/**\n * Client credentials from DCR registration\n */\nexport interface ClientCredentials {\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Timestamp when client was registered */\n issuedAt?: number;\n}\n\n/**\n * Options for DCR client registration\n */\nexport interface DcrRegistrationOptions {\n /** Redirect URI for OAuth callback, from the loopback listener the caller has already bound (RFC 8252) */\n redirectUri: string;\n /** Client name to register */\n clientName?: string;\n /**\n * Loopback trust grant for the registration_endpoint fetch (SSRF\n * mitigation - see `src/auth/discovery-fetch.ts`). Compute this from the\n * MCP server the caller is actually talking to, never from\n * `registrationEndpoint` itself (which is typically sourced from\n * remote-controlled AS metadata). Defaults to `false`.\n */\n allowLoopback?: boolean;\n}\n\n/**\n * Options for OAuth authorization flow\n */\nexport interface OAuthFlowOptions {\n /** Port for OAuth callback listener (required - use get-port to find available port) */\n port: number;\n /** Issuer identifier discovered before the flow starts; the `iss` in the authorization response must match it (RFC 9207) */\n issuer: string;\n /** Canonical resource server URI, sent as `resource` on the authorization, token, and refresh requests (RFC 8707) */\n resource: string;\n /** Redirect URI for OAuth callback (optional - will be built from port if not provided) */\n redirectUri?: string;\n /** OAuth scopes to request */\n scopes?: string[];\n /** Whether the authorization server advertises `authorization_response_iss_parameter_supported` (RFC 9207) */\n authorizationResponseIssSupported?: boolean;\n /** Enable PKCE (RFC 7636) - recommended for all clients, required for public clients */\n pkce?: boolean;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Timeout for callback (milliseconds) */\n timeout?: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: import('../utils/logger.ts').Logger;\n /**\n * Loopback trust grant for the token endpoint fetch (SSRF mitigation - see\n * `src/auth/discovery-fetch.ts`). Compute this from the MCP server the\n * caller is actually talking to, never from `tokenEndpoint` itself (which\n * is typically sourced from remote-controlled AS metadata). Defaults to\n * `false`.\n */\n allowLoopback?: boolean;\n}\n"],"names":[],"mappings":"AAAA;;CAEC,GAED;;CAEC,GAoJD;;CAEC,GACD,WA6BC"}
@@ -4,6 +4,7 @@
4
4
  * Helper to connect MCP SDK clients to servers with intelligent transport inference.
5
5
  * Automatically detects transport type from URL protocol or type field.
6
6
  */
7
+ import type { VersionNegotiationOptions } from '@modelcontextprotocol/client';
7
8
  import { Client } from '@modelcontextprotocol/client';
8
9
  import { type DcrAuthenticatorOptions } from '../dcr/index.js';
9
10
  import type { ServerProcess } from '../spawn/spawn-server.js';
@@ -17,15 +18,6 @@ interface RegistryLike {
17
18
  servers: Map<string, ServerProcess>;
18
19
  }
19
20
  import { type Logger } from '../utils/logger.js';
20
- /**
21
- * Extract the "server base" by removing a trailing `/mcp` path segment if present.
22
- * Examples:
23
- * - https://example.com/mcp -> https://example.com
24
- * - https://example.com/sheets/mcp -> https://example.com/sheets
25
- * - https://example.com/sheets/mcp/ -> https://example.com/sheets
26
- * - https://example.com/sheets -> https://example.com/sheets
27
- */
28
- export declare function extractBaseUrl(mcpUrl: string): string;
29
21
  /**
30
22
  * Connect MCP SDK client to server with full readiness handling.
31
23
  * @internal - Use registry.connect() instead
@@ -41,6 +33,15 @@ export declare function extractBaseUrl(mcpUrl: string): string;
41
33
  *
42
34
  * @param registryOrConfig - Result from createServerRegistry() or servers config object
43
35
  * @param serverName - Server name from servers config
36
+ * @param options - Connection options (see below)
37
+ * @param options.dcrAuthenticator - DCR authenticator options
38
+ * @param options.logger - Logger for connection diagnostics
39
+ * @param options.versionNegotiation - SDK protocol version negotiation (protocol revision
40
+ * 2026-07-28 and later). Omitted by default, which keeps the SDK's `'legacy'` mode:
41
+ * the plain 2025 connect sequence. Pass `{ mode: 'auto' }` to probe the server and
42
+ * fall back to 2025 when it cannot serve the modern era, or `{ mode: { pin: '2026-07-28' } }`
43
+ * to require the pinned revision (a server that cannot serve it fails the connect with
44
+ * a typed era-negotiation error).
44
45
  * @returns Connected MCP SDK Client (guaranteed ready)
45
46
  *
46
47
  * @example
@@ -63,5 +64,6 @@ export declare function extractBaseUrl(mcpUrl: string): string;
63
64
  export declare function connectMcpClient(registryOrConfig: RegistryLike | ServersConfig, serverName: string, options?: {
64
65
  dcrAuthenticator?: Partial<DcrAuthenticatorOptions>;
65
66
  logger?: Logger;
67
+ versionNegotiation?: VersionNegotiationOptions;
66
68
  }): Promise<Client>;
67
69
  export {};
@@ -9,6 +9,7 @@ import getPort from 'get-port';
9
9
  import { probeAuthCapabilities } from '../auth/index.js';
10
10
  import { DCR_CAPABILTY_DISCOVERY_TIMEOUT } from '../constants.js';
11
11
  import { DcrAuthenticator } from '../dcr/index.js';
12
+ import { normalizeUrl } from '../lib/url-utils.js';
12
13
  import { logger as defaultLogger } from '../utils/logger.js';
13
14
  import { ExistingProcessTransport } from './existing-process-transport.js';
14
15
  import { waitForHttpReady } from './wait-for-http-ready.js';
@@ -28,30 +29,6 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
28
29
  })
29
30
  ]);
30
31
  }
31
- /**
32
- * Extract the "server base" by removing a trailing `/mcp` path segment if present.
33
- * Examples:
34
- * - https://example.com/mcp -> https://example.com
35
- * - https://example.com/sheets/mcp -> https://example.com/sheets
36
- * - https://example.com/sheets/mcp/ -> https://example.com/sheets
37
- * - https://example.com/sheets -> https://example.com/sheets
38
- */ export function extractBaseUrl(mcpUrl) {
39
- const url = new URL(mcpUrl);
40
- // Ignore query/hash for base URL purposes
41
- url.search = '';
42
- url.hash = '';
43
- // Normalize path segments (removes empty segments from leading/trailing slashes)
44
- const segments = url.pathname.split('/').filter(Boolean);
45
- // If last segment is exactly "mcp", drop it
46
- if (segments[segments.length - 1] === 'mcp') {
47
- segments.pop();
48
- }
49
- // Rebuild pathname; empty means root
50
- url.pathname = segments.length ? `/${segments.join('/')}` : '';
51
- // Return without trailing slash (except root origin)
52
- const out = url.origin + url.pathname;
53
- return out === url.origin ? out : out.replace(/\/+$/, '');
54
- }
55
32
  /**
56
33
  * Infer transport type from server configuration with validation.
57
34
  *
@@ -106,6 +83,15 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
106
83
  *
107
84
  * @param registryOrConfig - Result from createServerRegistry() or servers config object
108
85
  * @param serverName - Server name from servers config
86
+ * @param options - Connection options (see below)
87
+ * @param options.dcrAuthenticator - DCR authenticator options
88
+ * @param options.logger - Logger for connection diagnostics
89
+ * @param options.versionNegotiation - SDK protocol version negotiation (protocol revision
90
+ * 2026-07-28 and later). Omitted by default, which keeps the SDK's `'legacy'` mode:
91
+ * the plain 2025 connect sequence. Pass `{ mode: 'auto' }` to probe the server and
92
+ * fall back to 2025 when it cannot serve the modern era, or `{ mode: { pin: '2026-07-28' } }`
93
+ * to require the pinned revision (a server that cannot serve it fails the connect with
94
+ * a typed era-negotiation error).
109
95
  * @returns Connected MCP SDK Client (guaranteed ready)
110
96
  *
111
97
  * @example
@@ -138,13 +124,20 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
138
124
  }
139
125
  // Infer transport type with validation
140
126
  const transportType = inferTransportType(serverConfig);
127
+ // SDK client options for both transports (main + SSE fallback). versionNegotiation is
128
+ // omitted rather than set to undefined so the default stays the SDK's 'legacy' mode —
129
+ // the plain 2025 connect sequence — for callers that do not pass it.
130
+ const clientOptions = {
131
+ capabilities: {}
132
+ };
133
+ if ((options === null || options === void 0 ? void 0 : options.versionNegotiation) !== undefined) {
134
+ clientOptions.versionNegotiation = options.versionNegotiation;
135
+ }
141
136
  // Create MCP client
142
137
  const client = new Client({
143
138
  name: 'mcp-cli-client',
144
139
  version: '1.0.0'
145
- }, {
146
- capabilities: {}
147
- });
140
+ }, clientOptions);
148
141
  // Connect based on inferred transport
149
142
  if (transportType === 'stdio') {
150
143
  // Check if we have a spawned process in the registry
@@ -181,8 +174,14 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
181
174
  }
182
175
  const url = new URL(serverConfig.url);
183
176
  // Check for DCR support and handle authentication automatically
184
- const baseUrl = extractBaseUrl(serverConfig.url);
185
- const capabilities = await withTimeout(probeAuthCapabilities(baseUrl), DCR_CAPABILTY_DISCOVERY_TIMEOUT, 'DCR capability discovery');
177
+ // The canonical MCP server URI, path segment and all. Both calls below need
178
+ // the server's identity, not its deployment root: discovery uses the path to
179
+ // find resource-specific metadata (RFC 9728 sub-path), and the authenticator
180
+ // audience-binds tokens to it (RFC 8707). Handing either the `/mcp`-stripped
181
+ // base names a different resource, which authorization servers that validate
182
+ // the `resource` indicator reject as `invalid_target`.
183
+ const mcpServerUrl = normalizeUrl(serverConfig.url);
184
+ const capabilities = await withTimeout(probeAuthCapabilities(mcpServerUrl), DCR_CAPABILTY_DISCOVERY_TIMEOUT, 'DCR capability discovery');
186
185
  let authToken;
187
186
  if (capabilities.supportsDcr) {
188
187
  logger.debug(`🔐 Server '${serverName}' supports DCR authentication`);
@@ -197,7 +196,7 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
197
196
  ...options === null || options === void 0 ? void 0 : options.dcrAuthenticator
198
197
  });
199
198
  // Ensure we have valid tokens (performs DCR + OAuth if needed)
200
- const tokens = await authenticator.ensureAuthenticated(baseUrl, capabilities);
199
+ const tokens = await authenticator.ensureAuthenticated(mcpServerUrl, capabilities);
201
200
  authToken = tokens.accessToken;
202
201
  logger.debug(`✅ Authentication complete for '${serverName}'`);
203
202
  } else {
@@ -249,9 +248,7 @@ import { waitForHttpReady } from './wait-for-http-ready.js';
249
248
  const sseClient = new Client({
250
249
  name: 'mcp-cli-client',
251
250
  version: '1.0.0'
252
- }, {
253
- capabilities: {}
254
- });
251
+ }, clientOptions);
255
252
  // SSE transport with merged headers (static + DCR auth)
256
253
  // Reuse the same header merging logic as Streamable HTTP
257
254
  const staticHeaders = serverConfig.headers || {};
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/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 type { Transport } from '@modelcontextprotocol/client';\nimport { Client, SSEClientTransport, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';\nimport { StdioClientTransport } from '@modelcontextprotocol/client/stdio';\nimport getPort from 'get-port';\nimport { probeAuthCapabilities } from '../auth/index.ts';\nimport { DCR_CAPABILTY_DISCOVERY_TIMEOUT } from '../constants.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: ServersConfig = isRegistry ? (registryOrConfig as RegistryLike).config : (registryOrConfig as ServersConfig);\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), DCR_CAPABILTY_DISCOVERY_TIMEOUT, '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","StreamableHTTPClientTransport","StdioClientTransport","getPort","probeAuthCapabilities","DCR_CAPABILTY_DISCOVERY_TIMEOUT","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":"AAAA;;;;;CAKC,GAGD,SAASA,MAAM,EAAEC,kBAAkB,EAAEC,6BAA6B,QAAQ,+BAA+B;AACzG,SAASC,oBAAoB,QAAQ,qCAAqC;AAC1E,OAAOC,aAAa,WAAW;AAC/B,SAASC,qBAAqB,QAAQ,mBAAmB;AACzD,SAASC,+BAA+B,QAAQ,kBAAkB;AAClE,SAASC,gBAAgB,QAAsC,kBAAkB;AAcjF,SAASC,UAAUC,aAAa,QAAqB,qBAAqB;AAC1E,SAASC,wBAAwB,QAAQ,kCAAkC;AAC3E,SAASC,gBAAgB,QAAQ,2BAA2B;AAE5D;;;;;;;CAOC,GACD,eAAeC,YAAeC,OAAmB,EAAEC,EAAU,EAAEC,SAAiB;IAC9E,IAAIC;IAEJ,OAAOC,QAAQC,IAAI,CAAC;QAClBL,QAAQM,OAAO,CAAC,IAAMC,aAAaJ;QACnC,IAAIC,QAAW,CAACI,GAAGC;YACjBN,YAAYO,WAAW,IAAMD,OAAO,IAAIE,MAAM,CAAC,cAAc,EAAEV,GAAG,IAAI,EAAEC,WAAW,IAAID;QACzF;KACD;AACH;AAEA;;;;;;;CAOC,GACD,OAAO,SAASW,eAAeC,MAAc;IAC3C,MAAMC,MAAM,IAAIC,IAAIF;IAEpB,0CAA0C;IAC1CC,IAAIE,MAAM,GAAG;IACbF,IAAIG,IAAI,GAAG;IAEX,iFAAiF;IACjF,MAAMC,WAAWJ,IAAIK,QAAQ,CAACC,KAAK,CAAC,KAAKC,MAAM,CAACC;IAEhD,4CAA4C;IAC5C,IAAIJ,QAAQ,CAACA,SAASK,MAAM,GAAG,EAAE,KAAK,OAAO;QAC3CL,SAASM,GAAG;IACd;IAEA,qCAAqC;IACrCV,IAAIK,QAAQ,GAAGD,SAASK,MAAM,GAAG,CAAC,CAAC,EAAEL,SAASO,IAAI,CAAC,MAAM,GAAG;IAE5D,qDAAqD;IACrD,MAAMC,MAAMZ,IAAIa,MAAM,GAAGb,IAAIK,QAAQ;IACrC,OAAOO,QAAQZ,IAAIa,MAAM,GAAGD,MAAMA,IAAIE,OAAO,CAAC,QAAQ;AACxD;AAEA;;;;;;;;;;;CAWC,GACD,SAASC,mBAAmBC,MAAsB;IAChD,kCAAkC;IAClC,IAAIA,OAAOC,IAAI,EAAE;QACf,gDAAgD;QAChD,IAAID,OAAOhB,GAAG,EAAE;YACd,MAAMA,MAAM,IAAIC,IAAIe,OAAOhB,GAAG;YAC9B,MAAMkB,WAAWlB,IAAIkB,QAAQ;YAE7B,IAAI,AAACA,CAAAA,aAAa,WAAWA,aAAa,QAAO,KAAMF,OAAOC,IAAI,KAAK,UAAUD,OAAOC,IAAI,KAAK,WAAW;gBAC1G,MAAM,IAAIpB,MAAM,CAAC,qCAAqC,EAAEqB,SAAS,iCAAiC,EAAEF,OAAOC,IAAI,CAAC,CAAC,CAAC;YACpH;QACF;QAEA,yBAAyB;QACzB,IAAID,OAAOC,IAAI,KAAK,UAAUD,OAAOC,IAAI,KAAK,WAAW,OAAO;QAChE,IAAID,OAAOC,IAAI,KAAK,SAAS,OAAO;QAEpC,MAAM,IAAIpB,MAAM,CAAC,4BAA4B,EAAEmB,OAAOC,IAAI,EAAE;IAC9D;IAEA,sCAAsC;IACtC,IAAID,OAAOhB,GAAG,EAAE;QACd,MAAMA,MAAM,IAAIC,IAAIe,OAAOhB,GAAG;QAC9B,MAAMkB,WAAWlB,IAAIkB,QAAQ;QAE7B,IAAIA,aAAa,WAAWA,aAAa,UAAU;YACjD,OAAO;QACT;QACA,MAAM,IAAIrB,MAAM,CAAC,0BAA0B,EAAEqB,UAAU;IACzD;IAEA,+BAA+B;IAC/B,OAAO;AACT;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCC,GACD,OAAO,eAAeC,iBACpBC,gBAA8C,EAC9CC,UAAkB,EAClBC,OAGC;;IAED,gEAAgE;IAChE,MAAMC,aAAa,aAAaH,oBAAoBA,iBAAiBI,OAAO,YAAYC;IACxF,MAAMC,gBAA+BH,aAAa,AAACH,iBAAkCJ,MAAM,GAAII;IAC/F,MAAMO,WAAWJ,aAAcH,mBAAoCQ;IACnE,MAAM/C,iBAASyC,oBAAAA,8BAAAA,QAASzC,MAAM,uCAAIC;IAElC,MAAM+C,eAAeH,aAAa,CAACL,WAAW;IAE9C,IAAI,CAACQ,cAAc;QACjB,MAAMC,YAAYC,OAAOC,IAAI,CAACN,eAAef,IAAI,CAAC;QAClD,MAAM,IAAId,MAAM,CAAC,QAAQ,EAAEwB,WAAW,0CAA0C,EAAES,aAAa,QAAQ;IACzG;IAEA,uCAAuC;IACvC,MAAMG,gBAAgBlB,mBAAmBc;IAEzC,oBAAoB;IACpB,MAAMK,SAAS,IAAI7D,OAAO;QAAE8D,MAAM;QAAkBC,SAAS;IAAQ,GAAG;QAAEC,cAAc,CAAC;IAAE;IAE3F,sCAAsC;IACtC,IAAIJ,kBAAkB,SAAS;QAC7B,qDAAqD;QACrD,MAAMK,eAAeX,qBAAAA,+BAAAA,SAAUH,OAAO,CAACe,GAAG,CAAClB;QAE3C,IAAIiB,cAAc;YAChB,oCAAoC;YACpC,MAAME,YAAY,IAAIzD,yBAAyBuD,aAAaG,OAAO;YACnE,MAAMP,OAAOQ,OAAO,CAACF;QACvB,OAAO;YACL,qEAAqE;YACrE,oEAAoE;YACpE,IAAI,CAACX,aAAac,OAAO,EAAE;gBACzB,MAAM,IAAI9C,MAAM,CAAC,QAAQ,EAAEwB,WAAW,iDAAiD,CAAC;YAC1F;YAEA,MAAMmB,YAAY,IAAIhE,qBAAqB;gBACzCmE,SAASd,aAAac,OAAO;gBAC7BC,MAAMf,aAAae,IAAI,IAAI,EAAE;gBAC7BC,KAAKhB,aAAagB,GAAG,IAAI,CAAC;YAC5B;YAEA,qFAAqF;YACrF,MAAMX,OAAOQ,OAAO,CAACF;QACvB;IACF,OAAO,IAAIP,kBAAkB,QAAQ;QACnC,IAAI,CAAE,CAAA,SAASJ,YAAW,KAAM,CAACA,aAAa7B,GAAG,EAAE;YACjD,MAAM,IAAIH,MAAM,CAAC,QAAQ,EAAEwB,WAAW,4CAA4C,CAAC;QACrF;QAEA,iEAAiE;QACjE,iEAAiE;QACjE,MAAMyB,gBAAgBnB,qBAAAA,+BAAAA,SAAUH,OAAO,CAACuB,GAAG,CAAC1B;QAE5C,IAAIyB,eAAe;YACjBjE,OAAOmE,KAAK,CAAC,CAAC,4CAA4C,EAAE3B,WAAW,KAAK,EAAEQ,aAAa7B,GAAG,EAAE;YAChG,MAAMhB,iBAAiB6C,aAAa7B,GAAG;YACvCnB,OAAOmE,KAAK,CAAC,CAAC,gCAAgC,EAAE3B,WAAW,OAAO,CAAC;QACrE;QAEA,MAAMrB,MAAM,IAAIC,IAAI4B,aAAa7B,GAAG;QAEpC,gEAAgE;QAChE,MAAMiD,UAAUnD,eAAe+B,aAAa7B,GAAG;QAC/C,MAAMqC,eAAe,MAAMpD,YAAYP,sBAAsBuE,UAAUtE,iCAAiC;QAExG,IAAIuE;QAEJ,IAAIb,aAAac,WAAW,EAAE;YAC5BtE,OAAOmE,KAAK,CAAC,CAAC,WAAW,EAAE3B,WAAW,6BAA6B,CAAC;YAEpE,8DAA8D;YAC9D,MAAM+B,OAAO,MAAM3E;YACnB,MAAM4E,cAAc,CAAC,iBAAiB,EAAED,KAAK,SAAS,CAAC;YAEvD,+EAA+E;YAC/E,MAAME,gBAAgB,IAAI1E,iBAAiB;gBACzC2E,UAAU;gBACVF;gBACAxE;mBACGyC,oBAAAA,8BAAAA,QAASkC,gBAAgB,AAA5B;YACF;YAEA,+DAA+D;YAC/D,MAAMC,SAAS,MAAMH,cAAcI,mBAAmB,CAACT,SAASZ;YAChEa,YAAYO,OAAOE,WAAW;YAE9B9E,OAAOmE,KAAK,CAAC,CAAC,+BAA+B,EAAE3B,WAAW,CAAC,CAAC;QAC9D,OAAO;YACLxC,OAAOmE,KAAK,CAAC,CAAC,YAAY,EAAE3B,WAAW,0DAA0D,CAAC;QACpG;QAEA,IAAI;YACF,iEAAiE;YACjE,8FAA8F;YAC9F,MAAMuC,gBAAgB/B,aAAagC,OAAO,IAAI,CAAC;YAC/C,MAAMC,aAAaZ,YAAY;gBAAEa,eAAe,CAAC,OAAO,EAAEb,WAAW;YAAC,IAAI,CAAC;YAC3E,MAAMc,gBAAgB;gBAAE,GAAGJ,aAAa;gBAAE,GAAGE,UAAU;YAAC;YAExD,MAAMG,mBACJlC,OAAOC,IAAI,CAACgC,eAAevD,MAAM,GAAG,IAChC;gBACEyD,aAAa;oBACXL,SAASG;gBACX;YACF,IACApC;YAEN,MAAMY,YAAY,IAAIjE,8BAA8ByB,KAAKiE;YACzD,+FAA+F;YAC/F,gEAAgE;YAChE,MAAMhF,YAAYiD,OAAOQ,OAAO,CAACF,YAAoC,OAAO;QAC9E,EAAE,OAAO2B,OAAO;YACd,+DAA+D;YAC/D,iFAAiF;YACjF,MAAMC,eAAeD,iBAAiBtE,QAAQsE,MAAME,OAAO,GAAGC,OAAOH;YAErE,0EAA0E;YAC1E,wFAAwF;YACxF,MAAMI,QAAQJ,iBAAiBtE,QAAQ,AAACsE,MAAgDI,KAAK,GAAG3C;YAChG,MAAM4C,sBAAsBD,CAAAA,kBAAAA,4BAAAA,MAAOE,IAAI,MAAK,kBAAkBL,aAAaM,QAAQ,CAAC;YAEpF,IAAIF,qBAAqB;gBACvB,4CAA4C;gBAC5C,MAAMtC,OAAOyC,KAAK,GAAGC,KAAK,CAAC,KAAO;gBAClC,MAAM,IAAI/E,MAAM,CAAC,sBAAsB,EAAEG,KAAK;YAChD;YAEA,8DAA8D;YAC9D,MAAM6E,iBACJT,aAAaM,QAAQ,CAAC,yBAAyB,mBAAmB;YAClEN,aAAaM,QAAQ,CAAC,UAAU,+CAA+C;YAC/EN,aAAaM,QAAQ,CAAC,QAAQ,qBAAqB;YAErD,IAAIG,gBAAgB;gBAClBhG,OAAOiG,IAAI,CAAC,CAAC,wBAAwB,EAAEV,aAAa,gCAAgC,CAAC;YACvF,OAAO;gBACLvF,OAAOiG,IAAI,CAAC;YACd;YAEA,iEAAiE;YACjE,MAAMC,YAAY,IAAI1G,OAAO;gBAAE8D,MAAM;gBAAkBC,SAAS;YAAQ,GAAG;gBAAEC,cAAc,CAAC;YAAE;YAE9F,wDAAwD;YACxD,yDAAyD;YACzD,MAAMuB,gBAAgB/B,aAAagC,OAAO,IAAI,CAAC;YAC/C,MAAMC,aAAaZ,YAAY;gBAAEa,eAAe,CAAC,OAAO,EAAEb,WAAW;YAAC,IAAI,CAAC;YAC3E,MAAMc,gBAAgB;gBAAE,GAAGJ,aAAa;gBAAE,GAAGE,UAAU;YAAC;YAExD,MAAMkB,sBACJjD,OAAOC,IAAI,CAACgC,eAAevD,MAAM,GAAG,IAChC;gBACEyD,aAAa;oBACXL,SAASG;gBACX;YACF,IACApC;YAEN,MAAMqD,eAAe,IAAI3G,mBAAmB0B,KAAKgF;YAEjD,IAAI;gBACF,MAAM/F,YAAY8F,UAAUrC,OAAO,CAACuC,eAAe,OAAO;gBAC1D,wCAAwC;gBACxC,OAAOF;YACT,EAAE,OAAOG,UAAU;gBACjB,gEAAgE;gBAChE,MAAM5F,QAAQ6F,GAAG,CAAC;oBAACjD,OAAOyC,KAAK,GAAGC,KAAK,CAAC,KAAO;oBAAIG,UAAUJ,KAAK,GAAGC,KAAK,CAAC,KAAO;iBAAG;gBACrF,MAAMM;YACR;QACF;IACF;IAEA,OAAOhD,QAAQ,iCAAiC;AAClD"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/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 type { ClientOptions, Transport, VersionNegotiationOptions } from '@modelcontextprotocol/client';\nimport { Client, SSEClientTransport, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';\nimport { StdioClientTransport } from '@modelcontextprotocol/client/stdio';\nimport getPort from 'get-port';\nimport { probeAuthCapabilities } from '../auth/index.ts';\nimport { DCR_CAPABILTY_DISCOVERY_TIMEOUT } from '../constants.ts';\nimport { DcrAuthenticator, type DcrAuthenticatorOptions } from '../dcr/index.ts';\nimport { normalizeUrl } from '../lib/url-utils.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 * 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 * @param options - Connection options (see below)\n * @param options.dcrAuthenticator - DCR authenticator options\n * @param options.logger - Logger for connection diagnostics\n * @param options.versionNegotiation - SDK protocol version negotiation (protocol revision\n * 2026-07-28 and later). Omitted by default, which keeps the SDK's `'legacy'` mode:\n * the plain 2025 connect sequence. Pass `{ mode: 'auto' }` to probe the server and\n * fall back to 2025 when it cannot serve the modern era, or `{ mode: { pin: '2026-07-28' } }`\n * to require the pinned revision (a server that cannot serve it fails the connect with\n * a typed era-negotiation error).\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 versionNegotiation?: VersionNegotiationOptions;\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: ServersConfig = isRegistry ? (registryOrConfig as RegistryLike).config : (registryOrConfig as ServersConfig);\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 // SDK client options for both transports (main + SSE fallback). versionNegotiation is\n // omitted rather than set to undefined so the default stays the SDK's 'legacy' mode —\n // the plain 2025 connect sequence — for callers that do not pass it.\n const clientOptions: ClientOptions = { capabilities: {} };\n if (options?.versionNegotiation !== undefined) {\n clientOptions.versionNegotiation = options.versionNegotiation;\n }\n\n // Create MCP client\n const client = new Client({ name: 'mcp-cli-client', version: '1.0.0' }, clientOptions);\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 // The canonical MCP server URI, path segment and all. Both calls below need\n // the server's identity, not its deployment root: discovery uses the path to\n // find resource-specific metadata (RFC 9728 sub-path), and the authenticator\n // audience-binds tokens to it (RFC 8707). Handing either the `/mcp`-stripped\n // base names a different resource, which authorization servers that validate\n // the `resource` indicator reject as `invalid_target`.\n const mcpServerUrl = normalizeUrl(serverConfig.url);\n const capabilities = await withTimeout(probeAuthCapabilities(mcpServerUrl), DCR_CAPABILTY_DISCOVERY_TIMEOUT, '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(mcpServerUrl, 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' }, clientOptions);\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","StreamableHTTPClientTransport","StdioClientTransport","getPort","probeAuthCapabilities","DCR_CAPABILTY_DISCOVERY_TIMEOUT","DcrAuthenticator","normalizeUrl","logger","defaultLogger","ExistingProcessTransport","waitForHttpReady","withTimeout","promise","ms","operation","timeoutId","Promise","race","finally","clearTimeout","_","reject","setTimeout","Error","inferTransportType","config","type","url","URL","protocol","connectMcpClient","registryOrConfig","serverName","options","isRegistry","servers","Map","serversConfig","registry","undefined","serverConfig","available","Object","keys","join","transportType","clientOptions","capabilities","versionNegotiation","client","name","version","serverHandle","get","transport","process","connect","command","args","env","isSpawnedHttp","has","debug","mcpServerUrl","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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"}
@@ -39,7 +39,9 @@ export declare class DcrAuthenticator {
39
39
  * Ensure server is authenticated, performing DCR and OAuth if needed
40
40
  * Proactively refreshes tokens if they're within 5 minutes of expiry
41
41
  *
42
- * @param baseUrl - Base URL of the server (e.g., https://example.com)
42
+ * @param mcpServerUrl - The MCP server's canonical URL, exactly as configured,
43
+ * with its path intact (`https://example.com/mcp`). Not a deployment root:
44
+ * the path is what identifies the resource an issued token is bound to.
43
45
  * @param capabilities - Auth capabilities from .well-known endpoint
44
46
  * @returns Valid token set ready to use
45
47
  *
@@ -48,11 +50,30 @@ export declare class DcrAuthenticator {
48
50
  * @example
49
51
  * const authenticator = new DcrAuthenticator({ redirectUri: 'http://localhost:3000/callback' });
50
52
  * const tokens = await authenticator.ensureAuthenticated(
51
- * 'https://example.com',
53
+ * 'https://example.com/mcp',
52
54
  * capabilities
53
55
  * );
54
56
  */
55
- ensureAuthenticated(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet>;
57
+ ensureAuthenticated(mcpServerUrl: string, capabilities: AuthCapabilities): Promise<TokenSet>;
58
+ /**
59
+ * The three things a server URL is used for here, kept apart on purpose.
60
+ *
61
+ * They were one value once, and collapsing them is what sent an authorization
62
+ * server the wrong audience: `resource` was derived from the deployment root,
63
+ * so a server at `https://host/mcp` was asked to mint a token for
64
+ * `https://host`, and any server that validates the indicator answered
65
+ * `invalid_target`.
66
+ *
67
+ * - `serverBaseUrl` builds the server's own endpoints (`/oauth/verify`), so a
68
+ * trailing `/mcp` comes off.
69
+ * - `resource` is the RFC 8707 audience. The resource server names itself in
70
+ * its RFC 9728 metadata; that name wins. Only when no such document exists
71
+ * do we fall back to the URL we were configured with.
72
+ * - `storeKey` identifies the credential locally. It stays the configured URL
73
+ * rather than the discovered `resource`, so it can be computed without a
74
+ * network round trip - `deleteTokens` has only the URL to work from.
75
+ */
76
+ private resolveUrls;
56
77
  /**
57
78
  * Handle authentication for self-hosted DCR servers
58
79
  * Self-hosted servers manage their own token storage via /oauth/verify
@@ -68,9 +89,14 @@ export declare class DcrAuthenticator {
68
89
  private refreshTokens;
69
90
  /**
70
91
  * Deletes both stored token families for a server, across every issuer they were bound to.
92
+ *
93
+ * Takes the same `mcpServerUrl` that {@link DcrAuthenticator.ensureAuthenticated}
94
+ * was given - keys are built from the configured URL, never from the discovered
95
+ * RFC 8707 resource, precisely so this can find them without doing discovery.
96
+ *
71
97
  * @throws CredentialBindingError if the configured store cannot enumerate keys.
72
98
  */
73
- deleteTokens(baseUrl: string): Promise<void>;
99
+ deleteTokens(mcpServerUrl: string): Promise<void>;
74
100
  /** Discards a stored credential that is not bound to `issuer` (SEP-2352), including one stored before issuer binding existed. */
75
101
  private loadTokens;
76
102
  private buildFlowOptions;
@@ -7,7 +7,7 @@ import Keyv from 'keyv';
7
7
  import { KeyvFile } from 'keyv-file';
8
8
  import { isLoopbackUrl } from '../auth/discovery-fetch.js';
9
9
  import { InteractiveOAuthFlow } from '../auth/interactive-oauth-flow.js';
10
- import { normalizeUrl } from '../lib/url-utils.js';
10
+ import { extractBaseUrl, normalizeUrl } from '../lib/url-utils.js';
11
11
  import { logger as defaultLogger } from '../utils/logger.js';
12
12
  import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
13
13
  /**
@@ -35,13 +35,13 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
35
35
  /**
36
36
  * Detect if server is self-hosted DCR (vs external OAuth provider)
37
37
  * Self-hosted servers have their own OAuth endpoints and manage token storage
38
- */ async detectSelfHostedMode(baseUrl) {
38
+ */ async detectSelfHostedMode(mcpServerUrl) {
39
39
  try {
40
40
  // Self-hosted DCR servers typically run their own OAuth server
41
41
  // Check if this is a self-hosted instance by testing OAuth metadata
42
- // For now, assume self-hosted if baseUrl matches common localhost patterns
42
+ // For now, assume self-hosted if the URL matches common localhost patterns
43
43
  // TODO: Implement proper self-hosted detection logic
44
- return baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1');
44
+ return mcpServerUrl.includes('localhost') || mcpServerUrl.includes('127.0.0.1');
45
45
  } catch (_error) {
46
46
  return false; // Assume external mode if detection fails
47
47
  }
@@ -50,7 +50,9 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
50
50
  * Ensure server is authenticated, performing DCR and OAuth if needed
51
51
  * Proactively refreshes tokens if they're within 5 minutes of expiry
52
52
  *
53
- * @param baseUrl - Base URL of the server (e.g., https://example.com)
53
+ * @param mcpServerUrl - The MCP server's canonical URL, exactly as configured,
54
+ * with its path intact (`https://example.com/mcp`). Not a deployment root:
55
+ * the path is what identifies the resource an issued token is bound to.
54
56
  * @param capabilities - Auth capabilities from .well-known endpoint
55
57
  * @returns Valid token set ready to use
56
58
  *
@@ -59,33 +61,59 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
59
61
  * @example
60
62
  * const authenticator = new DcrAuthenticator({ redirectUri: 'http://localhost:3000/callback' });
61
63
  * const tokens = await authenticator.ensureAuthenticated(
62
- * 'https://example.com',
64
+ * 'https://example.com/mcp',
63
65
  * capabilities
64
66
  * );
65
- */ async ensureAuthenticated(baseUrl, capabilities) {
67
+ */ async ensureAuthenticated(mcpServerUrl, capabilities) {
66
68
  // Auto-detect server mode
67
- const isSelfHosted = await this.detectSelfHostedMode(baseUrl);
69
+ const isSelfHosted = await this.detectSelfHostedMode(mcpServerUrl);
68
70
  if (isSelfHosted) {
69
- return this.ensureAuthenticatedSelfHosted(baseUrl, capabilities);
71
+ return this.ensureAuthenticatedSelfHosted(mcpServerUrl, capabilities);
70
72
  }
71
- return this.ensureAuthenticatedExternal(baseUrl, capabilities);
73
+ return this.ensureAuthenticatedExternal(mcpServerUrl, capabilities);
74
+ }
75
+ /**
76
+ * The three things a server URL is used for here, kept apart on purpose.
77
+ *
78
+ * They were one value once, and collapsing them is what sent an authorization
79
+ * server the wrong audience: `resource` was derived from the deployment root,
80
+ * so a server at `https://host/mcp` was asked to mint a token for
81
+ * `https://host`, and any server that validates the indicator answered
82
+ * `invalid_target`.
83
+ *
84
+ * - `serverBaseUrl` builds the server's own endpoints (`/oauth/verify`), so a
85
+ * trailing `/mcp` comes off.
86
+ * - `resource` is the RFC 8707 audience. The resource server names itself in
87
+ * its RFC 9728 metadata; that name wins. Only when no such document exists
88
+ * do we fall back to the URL we were configured with.
89
+ * - `storeKey` identifies the credential locally. It stays the configured URL
90
+ * rather than the discovered `resource`, so it can be computed without a
91
+ * network round trip - `deleteTokens` has only the URL to work from.
92
+ */ resolveUrls(mcpServerUrl, capabilities) {
93
+ var _capabilities_resource;
94
+ const storeKey = normalizeUrl(mcpServerUrl);
95
+ return {
96
+ serverBaseUrl: extractBaseUrl(mcpServerUrl),
97
+ resource: (_capabilities_resource = capabilities.resource) !== null && _capabilities_resource !== void 0 ? _capabilities_resource : storeKey,
98
+ storeKey
99
+ };
72
100
  }
73
101
  /**
74
102
  * Handle authentication for self-hosted DCR servers
75
103
  * Self-hosted servers manage their own token storage via /oauth/verify
76
- */ async ensureAuthenticatedSelfHosted(baseUrl, capabilities) {
104
+ */ async ensureAuthenticatedSelfHosted(mcpServerUrl, capabilities) {
77
105
  // Loopback trust for every discovery-derived fetch below, computed from
78
106
  // the server we're talking to, never from capabilities' endpoints (see discovery-fetch.ts).
79
- const allowLoopback = isLoopbackUrl(baseUrl);
107
+ const allowLoopback = isLoopbackUrl(mcpServerUrl);
80
108
  const issuer = requireIssuer(capabilities);
81
- const resource = normalizeUrl(baseUrl);
82
- const dcrTokenKey = `dcr-tokens:${issuer}:${resource}`;
109
+ const { serverBaseUrl, resource, storeKey } = this.resolveUrls(mcpServerUrl, capabilities);
110
+ const dcrTokenKey = `dcr-tokens:${issuer}:${storeKey}`;
83
111
  // 1. Check for existing DCR tokens (different from external tokens)
84
112
  let tokens = await this.loadTokens(dcrTokenKey, issuer);
85
113
  if (tokens) {
86
114
  // 2. Verify token is still valid by calling /oauth/verify
87
115
  try {
88
- const verifyUrl = `${baseUrl}/oauth/verify`;
116
+ const verifyUrl = `${serverBaseUrl}/oauth/verify`;
89
117
  const verifyResponse = await fetch(verifyUrl, {
90
118
  headers: {
91
119
  Authorization: `Bearer ${tokens.accessToken}`,
@@ -124,7 +152,7 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
124
152
  tokens = await this.oauthFlow.performAuthFlow(capabilities.authorizationEndpoint, capabilities.tokenEndpoint, client.clientId, client.clientSecret, flowOptions);
125
153
  // For self-hosted mode, verify the token works with /oauth/verify immediately
126
154
  try {
127
- const verifyUrl = `${baseUrl}/oauth/verify`;
155
+ const verifyUrl = `${serverBaseUrl}/oauth/verify`;
128
156
  const verifyResponse = await fetch(verifyUrl, {
129
157
  headers: {
130
158
  Authorization: `Bearer ${tokens.accessToken}`,
@@ -151,12 +179,12 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
151
179
  this.logger.debug('✅ Self-hosted DCR authentication successful, tokens saved');
152
180
  return tokens;
153
181
  }
154
- /** Handles authentication for external (non-self-hosted) OAuth providers. */ async ensureAuthenticatedExternal(baseUrl, capabilities) {
182
+ /** Handles authentication for external (non-self-hosted) OAuth providers. */ async ensureAuthenticatedExternal(mcpServerUrl, capabilities) {
155
183
  // See ensureAuthenticatedSelfHosted - same loopback trust rule.
156
- const allowLoopback = isLoopbackUrl(baseUrl);
184
+ const allowLoopback = isLoopbackUrl(mcpServerUrl);
157
185
  const issuer = requireIssuer(capabilities);
158
- const resource = normalizeUrl(baseUrl);
159
- const tokenKey = `tokens:${issuer}:${resource}`;
186
+ const { resource, storeKey } = this.resolveUrls(mcpServerUrl, capabilities);
187
+ const tokenKey = `tokens:${issuer}:${storeKey}`;
160
188
  // 1. Check for existing tokens
161
189
  let tokens = await this.loadTokens(tokenKey, issuer);
162
190
  if (tokens) {
@@ -223,9 +251,14 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
223
251
  }
224
252
  /**
225
253
  * Deletes both stored token families for a server, across every issuer they were bound to.
254
+ *
255
+ * Takes the same `mcpServerUrl` that {@link DcrAuthenticator.ensureAuthenticated}
256
+ * was given - keys are built from the configured URL, never from the discovered
257
+ * RFC 8707 resource, precisely so this can find them without doing discovery.
258
+ *
226
259
  * @throws CredentialBindingError if the configured store cannot enumerate keys.
227
- */ async deleteTokens(baseUrl) {
228
- const suffix = `:${normalizeUrl(baseUrl)}`;
260
+ */ async deleteTokens(mcpServerUrl) {
261
+ const suffix = `:${normalizeUrl(mcpServerUrl)}`;
229
262
  if (!this.tokenStore.iterator) {
230
263
  throw new CredentialBindingError('Token store does not support key enumeration, which issuer-keyed credentials require');
231
264
  }
@@ -234,7 +267,7 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
234
267
  await this.tokenStore.delete(key);
235
268
  }
236
269
  }
237
- this.logger.debug(`🗑️ Deleted tokens for ${baseUrl}`);
270
+ this.logger.debug(`🗑️ Deleted tokens for ${mcpServerUrl}`);
238
271
  }
239
272
  /** Discards a stored credential that is not bound to `issuer` (SEP-2352), including one stored before issuer binding existed. */ async loadTokens(key, issuer) {
240
273
  const tokens = await this.tokenStore.get(key);