diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts index b8f61856..32cffae1 100644 --- a/packages/core/src/tools/mcp-client.test.ts +++ b/packages/core/src/tools/mcp-client.test.ts @@ -12,6 +12,7 @@ import { isEnabled, hasValidTypes, McpClient, + hasNetworkTransport, } from './mcp-client.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import * as SdkClientStdioLib from '@modelcontextprotocol/sdk/client/stdio.js'; @@ -566,4 +567,34 @@ describe('mcp-client', () => { expect(hasValidTypes(schema)).toBe(true); }); }); + + describe('hasNetworkTransport', () => { + it('should return true if only url is provided', () => { + const config = { url: 'http://example.com' }; + expect(hasNetworkTransport(config)).toBe(true); + }); + + it('should return true if only httpUrl is provided', () => { + const config = { httpUrl: 'http://example.com' }; + expect(hasNetworkTransport(config)).toBe(true); + }); + + it('should return true if both url and httpUrl are provided', () => { + const config = { + url: 'http://example.com/sse', + httpUrl: 'http://example.com/http', + }; + expect(hasNetworkTransport(config)).toBe(true); + }); + + it('should return false if neither url nor httpUrl is provided', () => { + const config = { command: 'do-something' }; + expect(hasNetworkTransport(config)).toBe(false); + }); + + it('should return false for an empty config object', () => { + const config = {}; + expect(hasNetworkTransport(config)).toBe(false); + }); + }); }); diff --git a/packages/core/src/tools/mcp-client.ts b/packages/core/src/tools/mcp-client.ts index ede0d036..51634b98 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -325,15 +325,12 @@ async function handleAutomaticOAuth( OAuthUtils.parseWWWAuthenticateHeader(wwwAuthenticate); if (resourceMetadataUri) { oauthConfig = await OAuthUtils.discoverOAuthConfig(resourceMetadataUri); - } else if (mcpServerConfig.url) { - // Fallback: try to discover OAuth config from the base URL for SSE - const sseUrl = new URL(mcpServerConfig.url); - const baseUrl = `${sseUrl.protocol}//${sseUrl.host}`; - oauthConfig = await OAuthUtils.discoverOAuthConfig(baseUrl); - } else if (mcpServerConfig.httpUrl) { - // Fallback: try to discover OAuth config from the base URL for HTTP - const httpUrl = new URL(mcpServerConfig.httpUrl); - const baseUrl = `${httpUrl.protocol}//${httpUrl.host}`; + } else if (hasNetworkTransport(mcpServerConfig)) { + // Fallback: try to discover OAuth config from the base URL + const serverUrl = new URL( + mcpServerConfig.httpUrl || mcpServerConfig.url!, + ); + const baseUrl = `${serverUrl.protocol}//${serverUrl.host}`; oauthConfig = await OAuthUtils.discoverOAuthConfig(baseUrl); } @@ -783,6 +780,16 @@ export async function invokeMcpPrompt( } } +/** + * @visiblefortesting + * Checks if the MCP server configuration has a network transport URL (SSE or HTTP). + * @param config The MCP server configuration. + * @returns True if a `url` or `httpUrl` is present, false otherwise. + */ +export function hasNetworkTransport(config: MCPServerConfig): boolean { + return !!(config.url || config.httpUrl); +} + /** * Creates and connects an MCP client to a server based on the provided configuration. * It determines the appropriate transport (Stdio, SSE, or Streamable HTTP) and @@ -879,10 +886,7 @@ export async function connectToMcpServer( } catch (error) { // Check if this is a 401 error that might indicate OAuth is required const errorString = String(error); - if ( - errorString.includes('401') && - (mcpServerConfig.httpUrl || mcpServerConfig.url) - ) { + if (errorString.includes('401') && hasNetworkTransport(mcpServerConfig)) { mcpServerRequiresOAuth.set(mcpServerName, true); // Only trigger automatic OAuth discovery for HTTP servers or when OAuth is explicitly configured // For SSE servers, we should not trigger new OAuth flows automatically @@ -922,15 +926,18 @@ export async function connectToMcpServer( let wwwAuthenticate = extractWWWAuthenticateHeader(errorString); // If we didn't get the header from the error string, try to get it from the server - if (!wwwAuthenticate && mcpServerConfig.url) { + if (!wwwAuthenticate && hasNetworkTransport(mcpServerConfig)) { console.log( `No www-authenticate header in error, trying to fetch it from server...`, ); try { - const response = await fetch(mcpServerConfig.url, { + const urlToFetch = mcpServerConfig.httpUrl || mcpServerConfig.url!; + const response = await fetch(urlToFetch, { method: 'HEAD', headers: { - Accept: 'text/event-stream', + Accept: mcpServerConfig.httpUrl + ? 'application/json' + : 'text/event-stream', }, signal: AbortSignal.timeout(5000), }); @@ -945,7 +952,9 @@ export async function connectToMcpServer( } } catch (fetchError) { console.debug( - `Failed to fetch www-authenticate header: ${getErrorMessage(fetchError)}`, + `Failed to fetch www-authenticate header: ${getErrorMessage( + fetchError, + )}`, ); } } @@ -1071,12 +1080,14 @@ export async function connectToMcpServer( ); } - // For SSE servers, try to discover OAuth configuration from the base URL + // For SSE/HTTP servers, try to discover OAuth configuration from the base URL console.log(`🔍 Attempting OAuth discovery for '${mcpServerName}'...`); - if (mcpServerConfig.url) { - const sseUrl = new URL(mcpServerConfig.url); - const baseUrl = `${sseUrl.protocol}//${sseUrl.host}`; + if (hasNetworkTransport(mcpServerConfig)) { + const serverUrl = new URL( + mcpServerConfig.httpUrl || mcpServerConfig.url!, + ); + const baseUrl = `${serverUrl.protocol}//${serverUrl.host}`; try { // Try to discover OAuth configuration from the base URL @@ -1096,14 +1107,15 @@ export async function connectToMcpServer( // Perform OAuth authentication // Pass the server URL for proper discovery - const serverUrl = mcpServerConfig.httpUrl || mcpServerConfig.url; + const authServerUrl = + mcpServerConfig.httpUrl || mcpServerConfig.url; console.log( `Starting OAuth authentication for server '${mcpServerName}'...`, ); await MCPOAuthProvider.authenticate( mcpServerName, oauthAuthConfig, - serverUrl, + authServerUrl, ); // Retry connection with OAuth token