buroventures-harald-code-core
Version:
Harald Code Core - Core functionality for AI-powered coding assistant
765 lines • 36.5 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport, } from '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport, } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { ListPromptsResultSchema, GetPromptResultSchema, } from '@modelcontextprotocol/sdk/types.js';
import { parse } from 'shell-quote';
import { AuthProviderType } from '../config/config.js';
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
import { DiscoveredMCPTool } from './mcp-tool.js';
import { mcpToTool } from '@google/genai';
import { MCPOAuthProvider } from '../mcp/oauth-provider.js';
import { OAuthUtils } from '../mcp/oauth-utils.js';
import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js';
import { getErrorMessage } from '../utils/errors.js';
export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes
/**
* Enum representing the connection status of an MCP server
*/
export var MCPServerStatus;
(function (MCPServerStatus) {
/** Server is disconnected or experiencing errors */
MCPServerStatus["DISCONNECTED"] = "disconnected";
/** Server is in the process of connecting */
MCPServerStatus["CONNECTING"] = "connecting";
/** Server is connected and ready to use */
MCPServerStatus["CONNECTED"] = "connected";
})(MCPServerStatus || (MCPServerStatus = {}));
/**
* Enum representing the overall MCP discovery state
*/
export var MCPDiscoveryState;
(function (MCPDiscoveryState) {
/** Discovery has not started yet */
MCPDiscoveryState["NOT_STARTED"] = "not_started";
/** Discovery is currently in progress */
MCPDiscoveryState["IN_PROGRESS"] = "in_progress";
/** Discovery has completed (with or without errors) */
MCPDiscoveryState["COMPLETED"] = "completed";
})(MCPDiscoveryState || (MCPDiscoveryState = {}));
/**
* Map to track the status of each MCP server within the core package
*/
const serverStatuses = new Map();
/**
* Track the overall MCP discovery state
*/
let mcpDiscoveryState = MCPDiscoveryState.NOT_STARTED;
/**
* Map to track which MCP servers have been discovered to require OAuth
*/
export const mcpServerRequiresOAuth = new Map();
const statusChangeListeners = [];
/**
* Add a listener for MCP server status changes
*/
export function addMCPStatusChangeListener(listener) {
statusChangeListeners.push(listener);
}
/**
* Remove a listener for MCP server status changes
*/
export function removeMCPStatusChangeListener(listener) {
const index = statusChangeListeners.indexOf(listener);
if (index !== -1) {
statusChangeListeners.splice(index, 1);
}
}
/**
* Update the status of an MCP server
*/
function updateMCPServerStatus(serverName, status) {
serverStatuses.set(serverName, status);
// Notify all listeners
for (const listener of statusChangeListeners) {
listener(serverName, status);
}
}
/**
* Get the current status of an MCP server
*/
export function getMCPServerStatus(serverName) {
return serverStatuses.get(serverName) || MCPServerStatus.DISCONNECTED;
}
/**
* Get all MCP server statuses
*/
export function getAllMCPServerStatuses() {
return new Map(serverStatuses);
}
/**
* Get the current MCP discovery state
*/
export function getMCPDiscoveryState() {
return mcpDiscoveryState;
}
/**
* Extract WWW-Authenticate header from error message string.
* This is a more robust approach than regex matching.
*
* @param errorString The error message string
* @returns The www-authenticate header value if found, null otherwise
*/
function extractWWWAuthenticateHeader(errorString) {
// Try multiple patterns to extract the header
const patterns = [
/www-authenticate:\s*([^\n\r]+)/i,
/WWW-Authenticate:\s*([^\n\r]+)/i,
/"www-authenticate":\s*"([^"]+)"/i,
/'www-authenticate':\s*'([^']+)'/i,
];
for (const pattern of patterns) {
const match = errorString.match(pattern);
if (match) {
return match[1].trim();
}
}
return null;
}
/**
* Handle automatic OAuth discovery and authentication for a server.
*
* @param mcpServerName The name of the MCP server
* @param mcpServerConfig The MCP server configuration
* @param wwwAuthenticate The www-authenticate header value
* @returns True if OAuth was successfully configured and authenticated, false otherwise
*/
async function handleAutomaticOAuth(mcpServerName, mcpServerConfig, wwwAuthenticate) {
try {
console.log(`🔐 '${mcpServerName}' requires OAuth authentication`);
// Always try to parse the resource metadata URI from the www-authenticate header
let oauthConfig;
const resourceMetadataUri = 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}`;
oauthConfig = await OAuthUtils.discoverOAuthConfig(baseUrl);
}
if (!oauthConfig) {
console.error(`❌ Could not configure OAuth for '${mcpServerName}' - please authenticate manually with /mcp auth ${mcpServerName}`);
return false;
}
// OAuth configuration discovered - proceed with authentication
// Create OAuth configuration for authentication
const oauthAuthConfig = {
enabled: true,
authorizationUrl: oauthConfig.authorizationUrl,
tokenUrl: oauthConfig.tokenUrl,
scopes: oauthConfig.scopes || [],
};
// Perform OAuth authentication
console.log(`Starting OAuth authentication for server '${mcpServerName}'...`);
await MCPOAuthProvider.authenticate(mcpServerName, oauthAuthConfig);
console.log(`OAuth authentication successful for server '${mcpServerName}'`);
return true;
}
catch (error) {
console.error(`Failed to handle automatic OAuth for server '${mcpServerName}': ${getErrorMessage(error)}`);
return false;
}
}
/**
* Create a transport with OAuth token for the given server configuration.
*
* @param mcpServerName The name of the MCP server
* @param mcpServerConfig The MCP server configuration
* @param accessToken The OAuth access token
* @returns The transport with OAuth token, or null if creation fails
*/
async function createTransportWithOAuth(mcpServerName, mcpServerConfig, accessToken) {
try {
if (mcpServerConfig.httpUrl) {
// Create HTTP transport with OAuth token
const oauthTransportOptions = {
requestInit: {
headers: {
...mcpServerConfig.headers,
Authorization: `Bearer ${accessToken}`,
},
},
};
return new StreamableHTTPClientTransport(new URL(mcpServerConfig.httpUrl), oauthTransportOptions);
}
else if (mcpServerConfig.url) {
// Create SSE transport with OAuth token in Authorization header
return new SSEClientTransport(new URL(mcpServerConfig.url), {
requestInit: {
headers: {
...mcpServerConfig.headers,
Authorization: `Bearer ${accessToken}`,
},
},
});
}
return null;
}
catch (error) {
console.error(`Failed to create OAuth transport for server '${mcpServerName}': ${getErrorMessage(error)}`);
return null;
}
}
/**
* Discovers tools from all configured MCP servers and registers them with the tool registry.
* It orchestrates the connection and discovery process for each server defined in the
* configuration, as well as any server specified via a command-line argument.
*
* @param mcpServers A record of named MCP server configurations.
* @param mcpServerCommand An optional command string for a dynamically specified MCP server.
* @param toolRegistry The central registry where discovered tools will be registered.
* @returns A promise that resolves when the discovery process has been attempted for all servers.
*/
export async function discoverMcpTools(mcpServers, mcpServerCommand, toolRegistry, promptRegistry, debugMode) {
mcpDiscoveryState = MCPDiscoveryState.IN_PROGRESS;
try {
mcpServers = populateMcpServerCommand(mcpServers, mcpServerCommand);
const discoveryPromises = Object.entries(mcpServers).map(([mcpServerName, mcpServerConfig]) => connectAndDiscover(mcpServerName, mcpServerConfig, toolRegistry, promptRegistry, debugMode));
await Promise.all(discoveryPromises);
}
finally {
mcpDiscoveryState = MCPDiscoveryState.COMPLETED;
}
}
/** Visible for Testing */
export function populateMcpServerCommand(mcpServers, mcpServerCommand) {
if (mcpServerCommand) {
const cmd = mcpServerCommand;
const args = parse(cmd, process.env);
if (args.some((arg) => typeof arg !== 'string')) {
throw new Error('failed to parse mcpServerCommand: ' + cmd);
}
// use generic server name 'mcp'
mcpServers['mcp'] = {
command: args[0],
args: args.slice(1),
};
}
return mcpServers;
}
/**
* Connects to an MCP server and discovers available tools, registering them with the tool registry.
* This function handles the complete lifecycle of connecting to a server, discovering tools,
* and cleaning up resources if no tools are found.
*
* @param mcpServerName The name identifier for this MCP server
* @param mcpServerConfig Configuration object containing connection details
* @param toolRegistry The registry to register discovered tools with
* @returns Promise that resolves when discovery is complete
*/
export async function connectAndDiscover(mcpServerName, mcpServerConfig, toolRegistry, promptRegistry, debugMode) {
updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTING);
let mcpClient;
try {
mcpClient = await connectToMcpServer(mcpServerName, mcpServerConfig, debugMode);
mcpClient.onerror = (error) => {
console.error(`MCP ERROR (${mcpServerName}):`, error.toString());
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
};
// Attempt to discover both prompts and tools
const prompts = await discoverPrompts(mcpServerName, mcpClient, promptRegistry);
const tools = await discoverTools(mcpServerName, mcpServerConfig, mcpClient);
// If we have neither prompts nor tools, it's a failed discovery
if (prompts.length === 0 && tools.length === 0) {
throw new Error('No prompts or tools found on the server.');
}
// If we found anything, the server is connected
updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTED);
// Register any discovered tools
for (const tool of tools) {
toolRegistry.registerTool(tool);
}
}
catch (error) {
if (mcpClient) {
mcpClient.close();
}
console.error(`Error connecting to MCP server '${mcpServerName}': ${getErrorMessage(error)}`);
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
}
}
/**
* Discovers and sanitizes tools from a connected MCP client.
* It retrieves function declarations from the client, filters out disabled tools,
* generates valid names for them, and wraps them in `DiscoveredMCPTool` instances.
*
* @param mcpServerName The name of the MCP server.
* @param mcpServerConfig The configuration for the MCP server.
* @param mcpClient The active MCP client instance.
* @returns A promise that resolves to an array of discovered and enabled tools.
* @throws An error if no enabled tools are found or if the server provides invalid function declarations.
*/
export async function discoverTools(mcpServerName, mcpServerConfig, mcpClient) {
try {
const mcpCallableTool = mcpToTool(mcpClient);
const tool = await mcpCallableTool.tool();
if (!Array.isArray(tool.functionDeclarations)) {
// This is a valid case for a prompt-only server
return [];
}
const discoveredTools = [];
for (const funcDecl of tool.functionDeclarations) {
try {
if (!isEnabled(funcDecl, mcpServerName, mcpServerConfig)) {
continue;
}
discoveredTools.push(new DiscoveredMCPTool(mcpCallableTool, mcpServerName, funcDecl.name, funcDecl.description ?? '', funcDecl.parametersJsonSchema ?? { type: 'object', properties: {} }, mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC, mcpServerConfig.trust));
}
catch (error) {
console.error(`Error discovering tool: '${funcDecl.name}' from MCP server '${mcpServerName}': ${error.message}`);
}
}
return discoveredTools;
}
catch (error) {
if (error instanceof Error &&
!error.message?.includes('Method not found')) {
console.error(`Error discovering tools from ${mcpServerName}: ${getErrorMessage(error)}`);
}
return [];
}
}
/**
* Discovers and logs prompts from a connected MCP client.
* It retrieves prompt declarations from the client and logs their names.
*
* @param mcpServerName The name of the MCP server.
* @param mcpClient The active MCP client instance.
*/
export async function discoverPrompts(mcpServerName, mcpClient, promptRegistry) {
try {
const response = await mcpClient.request({ method: 'prompts/list', params: {} }, ListPromptsResultSchema);
for (const prompt of response.prompts) {
promptRegistry.registerPrompt({
...prompt,
serverName: mcpServerName,
invoke: (params) => invokeMcpPrompt(mcpServerName, mcpClient, prompt.name, params),
});
}
return response.prompts;
}
catch (error) {
// It's okay if this fails, not all servers will have prompts.
// Don't log an error if the method is not found, which is a common case.
if (error instanceof Error &&
!error.message?.includes('Method not found')) {
console.error(`Error discovering prompts from ${mcpServerName}: ${getErrorMessage(error)}`);
}
return [];
}
}
/**
* Invokes a prompt on a connected MCP client.
*
* @param mcpServerName The name of the MCP server.
* @param mcpClient The active MCP client instance.
* @param promptName The name of the prompt to invoke.
* @param promptParams The parameters to pass to the prompt.
* @returns A promise that resolves to the result of the prompt invocation.
*/
export async function invokeMcpPrompt(mcpServerName, mcpClient, promptName, promptParams) {
try {
const response = await mcpClient.request({
method: 'prompts/get',
params: {
name: promptName,
arguments: promptParams,
},
}, GetPromptResultSchema);
return response;
}
catch (error) {
if (error instanceof Error &&
!error.message?.includes('Method not found')) {
console.error(`Error invoking prompt '${promptName}' from ${mcpServerName} ${promptParams}: ${getErrorMessage(error)}`);
}
throw error;
}
}
/**
* 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.
*
* @param mcpServerName The name of the MCP server, used for logging and identification.
* @param mcpServerConfig The configuration specifying how to connect to the server.
* @returns A promise that resolves to a connected MCP `Client` instance.
* @throws An error if the connection fails or the configuration is invalid.
*/
export async function connectToMcpServer(mcpServerName, mcpServerConfig, debugMode) {
const mcpClient = new Client({
name: 'qwen-code-mcp-client',
version: '0.0.1',
});
// patch Client.callTool to use request timeout as genai McpCallTool.callTool does not do it
// TODO: remove this hack once GenAI SDK does callTool with request options
if ('callTool' in mcpClient) {
const origCallTool = mcpClient.callTool.bind(mcpClient);
mcpClient.callTool = function (params, resultSchema, options) {
return origCallTool(params, resultSchema, {
...options,
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
});
};
}
try {
const transport = await createTransport(mcpServerName, mcpServerConfig, debugMode);
try {
await mcpClient.connect(transport, {
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
});
return mcpClient;
}
catch (error) {
await transport.close();
throw error;
}
}
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)) {
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
const shouldTriggerOAuth = mcpServerConfig.httpUrl || mcpServerConfig.oauth?.enabled;
if (!shouldTriggerOAuth) {
// For SSE servers without explicit OAuth config, if a token was found but rejected, report it accurately.
const credentials = await MCPOAuthTokenStorage.getToken(mcpServerName);
if (credentials) {
const hasStoredTokens = await MCPOAuthProvider.getValidToken(mcpServerName, {
// Pass client ID if available
clientId: credentials.clientId,
});
if (hasStoredTokens) {
console.log(`Stored OAuth token for SSE server '${mcpServerName}' was rejected. ` +
`Please re-authenticate using: /mcp auth ${mcpServerName}`);
}
else {
console.log(`401 error received for SSE server '${mcpServerName}' without OAuth configuration. ` +
`Please authenticate using: /mcp auth ${mcpServerName}`);
}
}
throw new Error(`401 error received for SSE server '${mcpServerName}' without OAuth configuration. ` +
`Please authenticate using: /mcp auth ${mcpServerName}`);
}
// Try to extract www-authenticate header from the error
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) {
console.log(`No www-authenticate header in error, trying to fetch it from server...`);
try {
const response = await fetch(mcpServerConfig.url, {
method: 'HEAD',
headers: {
Accept: 'text/event-stream',
},
signal: AbortSignal.timeout(5000),
});
if (response.status === 401) {
wwwAuthenticate = response.headers.get('www-authenticate');
if (wwwAuthenticate) {
console.log(`Found www-authenticate header from server: ${wwwAuthenticate}`);
}
}
}
catch (fetchError) {
console.debug(`Failed to fetch www-authenticate header: ${getErrorMessage(fetchError)}`);
}
}
if (wwwAuthenticate) {
console.log(`Received 401 with www-authenticate header: ${wwwAuthenticate}`);
// Try automatic OAuth discovery and authentication
const oauthSuccess = await handleAutomaticOAuth(mcpServerName, mcpServerConfig, wwwAuthenticate);
if (oauthSuccess) {
// Retry connection with OAuth token
console.log(`Retrying connection to '${mcpServerName}' with OAuth token...`);
// Get the valid token - we need to create a proper OAuth config
// The token should already be available from the authentication process
const credentials = await MCPOAuthTokenStorage.getToken(mcpServerName);
if (credentials) {
const accessToken = await MCPOAuthProvider.getValidToken(mcpServerName, {
// Pass client ID if available
clientId: credentials.clientId,
});
if (accessToken) {
// Create transport with OAuth token
const oauthTransport = await createTransportWithOAuth(mcpServerName, mcpServerConfig, accessToken);
if (oauthTransport) {
try {
await mcpClient.connect(oauthTransport, {
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
});
// Connection successful with OAuth
return mcpClient;
}
catch (retryError) {
console.error(`Failed to connect with OAuth token: ${getErrorMessage(retryError)}`);
throw retryError;
}
}
else {
console.error(`Failed to create OAuth transport for server '${mcpServerName}'`);
throw new Error(`Failed to create OAuth transport for server '${mcpServerName}'`);
}
}
else {
console.error(`Failed to get OAuth token for server '${mcpServerName}'`);
throw new Error(`Failed to get OAuth token for server '${mcpServerName}'`);
}
}
else {
console.error(`Failed to get credentials for server '${mcpServerName}' after successful OAuth authentication`);
throw new Error(`Failed to get credentials for server '${mcpServerName}' after successful OAuth authentication`);
}
}
else {
console.error(`Failed to handle automatic OAuth for server '${mcpServerName}'`);
throw new Error(`Failed to handle automatic OAuth for server '${mcpServerName}'`);
}
}
else {
// No www-authenticate header found, but we got a 401
// Only try OAuth discovery for HTTP servers or when OAuth is explicitly configured
// For SSE servers, we should not trigger new OAuth flows automatically
const shouldTryDiscovery = mcpServerConfig.httpUrl || mcpServerConfig.oauth?.enabled;
if (!shouldTryDiscovery) {
const credentials = await MCPOAuthTokenStorage.getToken(mcpServerName);
if (credentials) {
const hasStoredTokens = await MCPOAuthProvider.getValidToken(mcpServerName, {
// Pass client ID if available
clientId: credentials.clientId,
});
if (hasStoredTokens) {
console.log(`Stored OAuth token for SSE server '${mcpServerName}' was rejected. ` +
`Please re-authenticate using: /mcp auth ${mcpServerName}`);
}
else {
console.log(`401 error received for SSE server '${mcpServerName}' without OAuth configuration. ` +
`Please authenticate using: /mcp auth ${mcpServerName}`);
}
}
throw new Error(`401 error received for SSE server '${mcpServerName}' without OAuth configuration. ` +
`Please authenticate using: /mcp auth ${mcpServerName}`);
}
// For SSE 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}`;
try {
// Try to discover OAuth configuration from the base URL
const oauthConfig = await OAuthUtils.discoverOAuthConfig(baseUrl);
if (oauthConfig) {
console.log(`Discovered OAuth configuration from base URL for server '${mcpServerName}'`);
// Create OAuth configuration for authentication
const oauthAuthConfig = {
enabled: true,
authorizationUrl: oauthConfig.authorizationUrl,
tokenUrl: oauthConfig.tokenUrl,
scopes: oauthConfig.scopes || [],
};
// Perform OAuth authentication
console.log(`Starting OAuth authentication for server '${mcpServerName}'...`);
await MCPOAuthProvider.authenticate(mcpServerName, oauthAuthConfig);
// Retry connection with OAuth token
const credentials = await MCPOAuthTokenStorage.getToken(mcpServerName);
if (credentials) {
const accessToken = await MCPOAuthProvider.getValidToken(mcpServerName, {
// Pass client ID if available
clientId: credentials.clientId,
});
if (accessToken) {
// Create transport with OAuth token
const oauthTransport = await createTransportWithOAuth(mcpServerName, mcpServerConfig, accessToken);
if (oauthTransport) {
try {
await mcpClient.connect(oauthTransport, {
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
});
// Connection successful with OAuth
return mcpClient;
}
catch (retryError) {
console.error(`Failed to connect with OAuth token: ${getErrorMessage(retryError)}`);
throw retryError;
}
}
else {
console.error(`Failed to create OAuth transport for server '${mcpServerName}'`);
throw new Error(`Failed to create OAuth transport for server '${mcpServerName}'`);
}
}
else {
console.error(`Failed to get OAuth token for server '${mcpServerName}'`);
throw new Error(`Failed to get OAuth token for server '${mcpServerName}'`);
}
}
else {
console.error(`Failed to get stored credentials for server '${mcpServerName}'`);
throw new Error(`Failed to get stored credentials for server '${mcpServerName}'`);
}
}
else {
console.error(`❌ Could not configure OAuth for '${mcpServerName}' - please authenticate manually with /mcp auth ${mcpServerName}`);
throw new Error(`OAuth configuration failed for '${mcpServerName}'. Please authenticate manually with /mcp auth ${mcpServerName}`);
}
}
catch (discoveryError) {
console.error(`❌ OAuth discovery failed for '${mcpServerName}' - please authenticate manually with /mcp auth ${mcpServerName}`);
throw discoveryError;
}
}
else {
console.error(`❌ '${mcpServerName}' requires authentication but no OAuth configuration found`);
throw new Error(`MCP server '${mcpServerName}' requires authentication. Please configure OAuth or check server settings.`);
}
}
}
else {
// Handle other connection errors
// Create a concise error message
const errorMessage = error.message || String(error);
const isNetworkError = errorMessage.includes('ENOTFOUND') ||
errorMessage.includes('ECONNREFUSED');
let conciseError;
if (isNetworkError) {
conciseError = `Cannot connect to '${mcpServerName}' - server may be down or URL incorrect`;
}
else {
conciseError = `Connection failed for '${mcpServerName}': ${errorMessage}`;
}
if (process.env.SANDBOX) {
conciseError += ` (check sandbox availability)`;
}
throw new Error(conciseError);
}
}
}
/** Visible for Testing */
export async function createTransport(mcpServerName, mcpServerConfig, debugMode) {
if (mcpServerConfig.authProviderType === AuthProviderType.GOOGLE_CREDENTIALS) {
const provider = new GoogleCredentialProvider(mcpServerConfig);
const transportOptions = {
authProvider: provider,
};
if (mcpServerConfig.httpUrl) {
return new StreamableHTTPClientTransport(new URL(mcpServerConfig.httpUrl), transportOptions);
}
else if (mcpServerConfig.url) {
return new SSEClientTransport(new URL(mcpServerConfig.url), transportOptions);
}
throw new Error('No URL configured for Google Credentials MCP server');
}
// Check if we have OAuth configuration or stored tokens
let accessToken = null;
let hasOAuthConfig = mcpServerConfig.oauth?.enabled;
if (hasOAuthConfig && mcpServerConfig.oauth) {
accessToken = await MCPOAuthProvider.getValidToken(mcpServerName, mcpServerConfig.oauth);
if (!accessToken) {
console.error(`MCP server '${mcpServerName}' requires OAuth authentication. ` +
`Please authenticate using the /mcp auth command.`);
throw new Error(`MCP server '${mcpServerName}' requires OAuth authentication. ` +
`Please authenticate using the /mcp auth command.`);
}
}
else {
// Check if we have stored OAuth tokens for this server (from previous authentication)
const credentials = await MCPOAuthTokenStorage.getToken(mcpServerName);
if (credentials) {
accessToken = await MCPOAuthProvider.getValidToken(mcpServerName, {
// Pass client ID if available
clientId: credentials.clientId,
});
if (accessToken) {
hasOAuthConfig = true;
console.log(`Found stored OAuth token for server '${mcpServerName}'`);
}
}
}
if (mcpServerConfig.httpUrl) {
const transportOptions = {};
// Set up headers with OAuth token if available
if (hasOAuthConfig && accessToken) {
transportOptions.requestInit = {
headers: {
...mcpServerConfig.headers,
Authorization: `Bearer ${accessToken}`,
},
};
}
else if (mcpServerConfig.headers) {
transportOptions.requestInit = {
headers: mcpServerConfig.headers,
};
}
return new StreamableHTTPClientTransport(new URL(mcpServerConfig.httpUrl), transportOptions);
}
if (mcpServerConfig.url) {
const transportOptions = {};
// Set up headers with OAuth token if available
if (hasOAuthConfig && accessToken) {
transportOptions.requestInit = {
headers: {
...mcpServerConfig.headers,
Authorization: `Bearer ${accessToken}`,
},
};
}
else if (mcpServerConfig.headers) {
transportOptions.requestInit = {
headers: mcpServerConfig.headers,
};
}
return new SSEClientTransport(new URL(mcpServerConfig.url), transportOptions);
}
if (mcpServerConfig.command) {
const transport = new StdioClientTransport({
command: mcpServerConfig.command,
args: mcpServerConfig.args || [],
env: {
...process.env,
...(mcpServerConfig.env || {}),
},
cwd: mcpServerConfig.cwd,
stderr: 'pipe',
});
if (debugMode) {
transport.stderr.on('data', (data) => {
const stderrStr = data.toString().trim();
console.debug(`[DEBUG] [MCP STDERR (${mcpServerName})]: `, stderrStr);
});
}
return transport;
}
throw new Error(`Invalid configuration: missing httpUrl (for Streamable HTTP), url (for SSE), and command (for stdio).`);
}
/** Visible for testing */
export function isEnabled(funcDecl, mcpServerName, mcpServerConfig) {
if (!funcDecl.name) {
console.warn(`Discovered a function declaration without a name from MCP server '${mcpServerName}'. Skipping.`);
return false;
}
const { includeTools, excludeTools } = mcpServerConfig;
// excludeTools takes precedence over includeTools
if (excludeTools && excludeTools.includes(funcDecl.name)) {
return false;
}
return (!includeTools ||
includeTools.some((tool) => tool === funcDecl.name || tool.startsWith(`${funcDecl.name}(`)));
}
//# sourceMappingURL=mcp-client.js.map