diff options
| author | Yoichiro Tanaka <[email protected]> | 2025-08-21 16:05:45 +0900 | 
|---|---|---|
| committer | GitHub <[email protected]> | 2025-08-21 07:05:45 +0000 | 
| commit | 63f9e86bc3b7c7007be1a73f0cadb59d8bbd1fdc (patch) | |
| tree | 31ae7bbc418a85af0e3a9e939960b7941f39ea49 | |
| parent | a64394a4fa16b4920754a85f5f134dbab3a7990f (diff) | |
feat(mcp-client): Handle 401 error for httpUrl (#6640)
| -rw-r--r-- | packages/core/src/tools/mcp-client.test.ts | 31 | ||||
| -rw-r--r-- | packages/core/src/tools/mcp-client.ts | 58 | 
2 files changed, 66 insertions, 23 deletions
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);      } @@ -784,6 +781,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   * establishes a connection. It also applies a patch to handle request timeouts. @@ -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  | 
