feat(mcp-client): Handle 401 error for httpUrl (#6640)

This commit is contained in:
Yoichiro Tanaka 2025-08-21 16:05:45 +09:00 committed by GitHub
parent a64394a4fa
commit 63f9e86bc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 66 additions and 23 deletions

View File

@ -12,6 +12,7 @@ import {
isEnabled, isEnabled,
hasValidTypes, hasValidTypes,
McpClient, McpClient,
hasNetworkTransport,
} from './mcp-client.js'; } from './mcp-client.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import * as SdkClientStdioLib from '@modelcontextprotocol/sdk/client/stdio.js'; import * as SdkClientStdioLib from '@modelcontextprotocol/sdk/client/stdio.js';
@ -566,4 +567,34 @@ describe('mcp-client', () => {
expect(hasValidTypes(schema)).toBe(true); 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);
});
});
}); });

View File

@ -325,15 +325,12 @@ async function handleAutomaticOAuth(
OAuthUtils.parseWWWAuthenticateHeader(wwwAuthenticate); OAuthUtils.parseWWWAuthenticateHeader(wwwAuthenticate);
if (resourceMetadataUri) { if (resourceMetadataUri) {
oauthConfig = await OAuthUtils.discoverOAuthConfig(resourceMetadataUri); oauthConfig = await OAuthUtils.discoverOAuthConfig(resourceMetadataUri);
} else if (mcpServerConfig.url) { } else if (hasNetworkTransport(mcpServerConfig)) {
// Fallback: try to discover OAuth config from the base URL for SSE // Fallback: try to discover OAuth config from the base URL
const sseUrl = new URL(mcpServerConfig.url); const serverUrl = new URL(
const baseUrl = `${sseUrl.protocol}//${sseUrl.host}`; mcpServerConfig.httpUrl || mcpServerConfig.url!,
oauthConfig = await OAuthUtils.discoverOAuthConfig(baseUrl); );
} else if (mcpServerConfig.httpUrl) { const baseUrl = `${serverUrl.protocol}//${serverUrl.host}`;
// Fallback: try to discover OAuth config from the base URL for HTTP
const httpUrl = new URL(mcpServerConfig.httpUrl);
const baseUrl = `${httpUrl.protocol}//${httpUrl.host}`;
oauthConfig = await OAuthUtils.discoverOAuthConfig(baseUrl); 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. * 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 * It determines the appropriate transport (Stdio, SSE, or Streamable HTTP) and
@ -879,10 +886,7 @@ export async function connectToMcpServer(
} catch (error) { } catch (error) {
// Check if this is a 401 error that might indicate OAuth is required // Check if this is a 401 error that might indicate OAuth is required
const errorString = String(error); const errorString = String(error);
if ( if (errorString.includes('401') && hasNetworkTransport(mcpServerConfig)) {
errorString.includes('401') &&
(mcpServerConfig.httpUrl || mcpServerConfig.url)
) {
mcpServerRequiresOAuth.set(mcpServerName, true); mcpServerRequiresOAuth.set(mcpServerName, true);
// Only trigger automatic OAuth discovery for HTTP servers or when OAuth is explicitly configured // 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 // For SSE servers, we should not trigger new OAuth flows automatically
@ -922,15 +926,18 @@ export async function connectToMcpServer(
let wwwAuthenticate = extractWWWAuthenticateHeader(errorString); let wwwAuthenticate = extractWWWAuthenticateHeader(errorString);
// If we didn't get the header from the error string, try to get it from the server // 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( console.log(
`No www-authenticate header in error, trying to fetch it from server...`, `No www-authenticate header in error, trying to fetch it from server...`,
); );
try { try {
const response = await fetch(mcpServerConfig.url, { const urlToFetch = mcpServerConfig.httpUrl || mcpServerConfig.url!;
const response = await fetch(urlToFetch, {
method: 'HEAD', method: 'HEAD',
headers: { headers: {
Accept: 'text/event-stream', Accept: mcpServerConfig.httpUrl
? 'application/json'
: 'text/event-stream',
}, },
signal: AbortSignal.timeout(5000), signal: AbortSignal.timeout(5000),
}); });
@ -945,7 +952,9 @@ export async function connectToMcpServer(
} }
} catch (fetchError) { } catch (fetchError) {
console.debug( 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}'...`); console.log(`🔍 Attempting OAuth discovery for '${mcpServerName}'...`);
if (mcpServerConfig.url) { if (hasNetworkTransport(mcpServerConfig)) {
const sseUrl = new URL(mcpServerConfig.url); const serverUrl = new URL(
const baseUrl = `${sseUrl.protocol}//${sseUrl.host}`; mcpServerConfig.httpUrl || mcpServerConfig.url!,
);
const baseUrl = `${serverUrl.protocol}//${serverUrl.host}`;
try { try {
// Try to discover OAuth configuration from the base URL // Try to discover OAuth configuration from the base URL
@ -1096,14 +1107,15 @@ export async function connectToMcpServer(
// Perform OAuth authentication // Perform OAuth authentication
// Pass the server URL for proper discovery // Pass the server URL for proper discovery
const serverUrl = mcpServerConfig.httpUrl || mcpServerConfig.url; const authServerUrl =
mcpServerConfig.httpUrl || mcpServerConfig.url;
console.log( console.log(
`Starting OAuth authentication for server '${mcpServerName}'...`, `Starting OAuth authentication for server '${mcpServerName}'...`,
); );
await MCPOAuthProvider.authenticate( await MCPOAuthProvider.authenticate(
mcpServerName, mcpServerName,
oauthAuthConfig, oauthAuthConfig,
serverUrl, authServerUrl,
); );
// Retry connection with OAuth token // Retry connection with OAuth token