feat(mcp-client): Handle 401 error for httpUrl (#6640)
This commit is contained in:
parent
a64394a4fa
commit
63f9e86bc3
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue