2025-05-28 00:43:23 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* @license
|
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
2025-11-20 16:51:25 -05:00
|
|
|
|
import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv';
|
|
|
|
|
|
import type {
|
|
|
|
|
|
jsonSchemaValidator,
|
|
|
|
|
|
JsonSchemaType,
|
|
|
|
|
|
JsonSchemaValidator,
|
|
|
|
|
|
} from '@modelcontextprotocol/sdk/validation/types.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
|
import type { SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
|
|
|
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
2025-09-25 21:42:21 -07:00
|
|
|
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
|
import type { StreamableHTTPClientTransportOptions } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
|
|
|
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
2025-09-25 21:42:21 -07:00
|
|
|
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
|
import type {
|
2025-07-25 20:56:33 +00:00
|
|
|
|
GetPromptResult,
|
2025-09-25 21:42:21 -07:00
|
|
|
|
Prompt,
|
2025-12-09 03:43:12 +01:00
|
|
|
|
ReadResourceResult,
|
|
|
|
|
|
Resource,
|
2025-08-26 00:04:53 +02:00
|
|
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
|
|
|
|
import {
|
2025-12-09 03:43:12 +01:00
|
|
|
|
ListResourcesResultSchema,
|
2025-08-08 16:29:06 -07:00
|
|
|
|
ListRootsRequestSchema,
|
2025-12-09 03:43:12 +01:00
|
|
|
|
ReadResourceResultSchema,
|
|
|
|
|
|
ResourceListChangedNotificationSchema,
|
2025-12-04 13:04:38 -08:00
|
|
|
|
ToolListChangedNotificationSchema,
|
2026-01-27 13:50:27 +01:00
|
|
|
|
PromptListChangedNotificationSchema,
|
2026-02-18 12:46:12 -08:00
|
|
|
|
ProgressNotificationSchema,
|
2025-11-17 12:03:48 -05:00
|
|
|
|
type Tool as McpTool,
|
2025-07-25 20:56:33 +00:00
|
|
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
2026-02-05 16:37:28 -05:00
|
|
|
|
import { ApprovalMode, PolicyDecision } from '../policy/types.js';
|
2025-05-28 00:43:23 -07:00
|
|
|
|
import { parse } from 'shell-quote';
|
2026-02-11 15:06:28 -05:00
|
|
|
|
import type { Config, MCPServerConfig } from '../config/config.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
|
import { AuthProviderType } from '../config/config.js';
|
2025-07-24 10:37:39 -07:00
|
|
|
|
import { GoogleCredentialProvider } from '../mcp/google-auth-provider.js';
|
2025-09-27 10:12:24 +02:00
|
|
|
|
import { ServiceAccountImpersonationProvider } from '../mcp/sa-impersonation-provider.js';
|
2025-05-28 00:43:23 -07:00
|
|
|
|
import { DiscoveredMCPTool } from './mcp-tool.js';
|
2026-02-05 18:03:32 +01:00
|
|
|
|
import { XcodeMcpBridgeFixTransport } from './xcode-mcp-fix-transport.js';
|
2025-07-25 20:56:33 +00:00
|
|
|
|
|
2025-11-17 12:03:48 -05:00
|
|
|
|
import type { CallableTool, FunctionCall, Part, Tool } from '@google/genai';
|
2025-09-25 21:42:21 -07:00
|
|
|
|
import { basename } from 'node:path';
|
|
|
|
|
|
import { pathToFileURL } from 'node:url';
|
2026-02-18 12:46:12 -08:00
|
|
|
|
import { randomUUID } from 'node:crypto';
|
2025-11-26 12:08:19 -08:00
|
|
|
|
import type { McpAuthProvider } from '../mcp/auth-provider.js';
|
2025-07-22 09:34:56 -04:00
|
|
|
|
import { MCPOAuthProvider } from '../mcp/oauth-provider.js';
|
|
|
|
|
|
import { MCPOAuthTokenStorage } from '../mcp/oauth-token-storage.js';
|
2025-09-25 21:42:21 -07:00
|
|
|
|
import { OAuthUtils } from '../mcp/oauth-utils.js';
|
|
|
|
|
|
import type { PromptRegistry } from '../prompts/prompt-registry.js';
|
2025-12-02 20:01:33 -05:00
|
|
|
|
import {
|
|
|
|
|
|
getErrorMessage,
|
|
|
|
|
|
isAuthenticationError,
|
|
|
|
|
|
UnauthorizedError,
|
|
|
|
|
|
} from '../utils/errors.js';
|
2025-08-26 00:04:53 +02:00
|
|
|
|
import type {
|
|
|
|
|
|
Unsubscribe,
|
|
|
|
|
|
WorkspaceContext,
|
|
|
|
|
|
} from '../utils/workspaceContext.js';
|
2026-02-18 12:46:12 -08:00
|
|
|
|
import { getToolCallContext } from '../utils/toolCallContext.js';
|
2025-09-25 21:42:21 -07:00
|
|
|
|
import type { ToolRegistry } from './tool-registry.js';
|
2025-10-21 16:35:22 -04:00
|
|
|
|
import { debugLogger } from '../utils/debugLogger.js';
|
2026-01-04 17:11:43 -05:00
|
|
|
|
import { type MessageBus } from '../confirmation-bus/message-bus.js';
|
2025-10-27 16:46:35 -07:00
|
|
|
|
import { coreEvents } from '../utils/events.js';
|
2025-12-09 03:43:12 +01:00
|
|
|
|
import type { ResourceRegistry } from '../resources/resource-registry.js';
|
2025-12-22 19:18:27 -08:00
|
|
|
|
import {
|
|
|
|
|
|
sanitizeEnvironment,
|
|
|
|
|
|
type EnvironmentSanitizationConfig,
|
|
|
|
|
|
} from '../services/environmentSanitization.js';
|
2026-02-11 16:07:51 -08:00
|
|
|
|
import {
|
|
|
|
|
|
GEMINI_CLI_IDENTIFICATION_ENV_VAR,
|
|
|
|
|
|
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
|
|
|
|
|
|
} from '../services/shellExecutionService.js';
|
2025-05-28 00:43:23 -07:00
|
|
|
|
|
2025-06-08 21:52:11 -07:00
|
|
|
|
export const MCP_DEFAULT_TIMEOUT_MSEC = 10 * 60 * 1000; // default to 10 minutes
|
|
|
|
|
|
|
2025-07-25 20:56:33 +00:00
|
|
|
|
export type DiscoveredMCPPrompt = Prompt & {
|
|
|
|
|
|
serverName: string;
|
|
|
|
|
|
invoke: (params: Record<string, unknown>) => Promise<GetPromptResult>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-07 15:06:18 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Enum representing the connection status of an MCP server
|
|
|
|
|
|
*/
|
|
|
|
|
|
export enum MCPServerStatus {
|
|
|
|
|
|
/** Server is disconnected or experiencing errors */
|
|
|
|
|
|
DISCONNECTED = 'disconnected',
|
2025-10-03 09:23:55 -07:00
|
|
|
|
/** Server is actively disconnecting */
|
|
|
|
|
|
DISCONNECTING = 'disconnecting',
|
2025-06-07 15:06:18 -04:00
|
|
|
|
/** Server is in the process of connecting */
|
|
|
|
|
|
CONNECTING = 'connecting',
|
|
|
|
|
|
/** Server is connected and ready to use */
|
|
|
|
|
|
CONNECTED = 'connected',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-10 08:47:46 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Enum representing the overall MCP discovery state
|
|
|
|
|
|
*/
|
|
|
|
|
|
export enum MCPDiscoveryState {
|
|
|
|
|
|
/** Discovery has not started yet */
|
|
|
|
|
|
NOT_STARTED = 'not_started',
|
|
|
|
|
|
/** Discovery is currently in progress */
|
|
|
|
|
|
IN_PROGRESS = 'in_progress',
|
|
|
|
|
|
/** Discovery has completed (with or without errors) */
|
|
|
|
|
|
COMPLETED = 'completed',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-18 12:46:12 -08:00
|
|
|
|
/**
|
|
|
|
|
|
* Interface for reporting progress from MCP tool calls.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface McpProgressReporter {
|
|
|
|
|
|
registerProgressToken(token: string | number, callId: string): void;
|
|
|
|
|
|
unregisterProgressToken(token: string | number): void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-19 21:03:19 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* A client for a single MCP server.
|
|
|
|
|
|
*
|
|
|
|
|
|
* This class is responsible for connecting to, discovering tools from, and
|
|
|
|
|
|
* managing the state of a single MCP server.
|
|
|
|
|
|
*/
|
2026-02-18 12:46:12 -08:00
|
|
|
|
export class McpClient implements McpProgressReporter {
|
2025-10-03 09:23:55 -07:00
|
|
|
|
private client: Client | undefined;
|
2025-08-19 21:03:19 +02:00
|
|
|
|
private transport: Transport | undefined;
|
|
|
|
|
|
private status: MCPServerStatus = MCPServerStatus.DISCONNECTED;
|
2025-12-09 03:43:12 +01:00
|
|
|
|
private isRefreshingTools: boolean = false;
|
|
|
|
|
|
private pendingToolRefresh: boolean = false;
|
|
|
|
|
|
private isRefreshingResources: boolean = false;
|
|
|
|
|
|
private pendingResourceRefresh: boolean = false;
|
2026-01-27 13:50:27 +01:00
|
|
|
|
private isRefreshingPrompts: boolean = false;
|
|
|
|
|
|
private pendingPromptRefresh: boolean = false;
|
2025-08-19 21:03:19 +02:00
|
|
|
|
|
2026-02-18 12:46:12 -08:00
|
|
|
|
/**
|
|
|
|
|
|
* Map of progress tokens to tool call IDs.
|
|
|
|
|
|
* This allows us to route progress notifications to the correct tool call.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private readonly progressTokenToCallId = new Map<string | number, string>();
|
|
|
|
|
|
|
2025-08-19 21:03:19 +02:00
|
|
|
|
constructor(
|
|
|
|
|
|
private readonly serverName: string,
|
|
|
|
|
|
private readonly serverConfig: MCPServerConfig,
|
|
|
|
|
|
private readonly toolRegistry: ToolRegistry,
|
|
|
|
|
|
private readonly promptRegistry: PromptRegistry,
|
2025-12-09 03:43:12 +01:00
|
|
|
|
private readonly resourceRegistry: ResourceRegistry,
|
2025-08-19 21:03:19 +02:00
|
|
|
|
private readonly workspaceContext: WorkspaceContext,
|
2025-12-04 13:04:38 -08:00
|
|
|
|
private readonly cliConfig: Config,
|
2025-08-19 21:03:19 +02:00
|
|
|
|
private readonly debugMode: boolean,
|
2026-01-20 22:01:18 +00:00
|
|
|
|
private readonly clientVersion: string,
|
2025-12-04 13:04:38 -08:00
|
|
|
|
private readonly onToolsUpdated?: (signal?: AbortSignal) => Promise<void>,
|
2025-10-03 09:23:55 -07:00
|
|
|
|
) {}
|
2025-08-19 21:03:19 +02:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Connects to the MCP server.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async connect(): Promise<void> {
|
2025-10-03 09:23:55 -07:00
|
|
|
|
if (this.status !== MCPServerStatus.DISCONNECTED) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Can only connect when the client is disconnected, current state is ${this.status}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-08-19 21:03:19 +02:00
|
|
|
|
this.updateStatus(MCPServerStatus.CONNECTING);
|
|
|
|
|
|
try {
|
2026-02-10 17:00:36 -05:00
|
|
|
|
this.client = await connectToMcpServer(
|
2026-01-20 22:01:18 +00:00
|
|
|
|
this.clientVersion,
|
2025-10-03 09:23:55 -07:00
|
|
|
|
this.serverName,
|
|
|
|
|
|
this.serverConfig,
|
|
|
|
|
|
this.debugMode,
|
|
|
|
|
|
this.workspaceContext,
|
2025-12-22 19:18:27 -08:00
|
|
|
|
this.cliConfig.sanitizationConfig,
|
2025-10-03 09:23:55 -07:00
|
|
|
|
);
|
2025-12-04 13:04:38 -08:00
|
|
|
|
|
2025-12-09 03:43:12 +01:00
|
|
|
|
this.registerNotificationHandlers();
|
2025-12-04 13:04:38 -08:00
|
|
|
|
|
2025-10-03 09:23:55 -07:00
|
|
|
|
const originalOnError = this.client.onerror;
|
2026-02-10 17:00:36 -05:00
|
|
|
|
this.client.onerror = (error) => {
|
2025-10-03 09:23:55 -07:00
|
|
|
|
if (this.status !== MCPServerStatus.CONNECTED) {
|
2025-08-19 21:03:19 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-03 09:23:55 -07:00
|
|
|
|
if (originalOnError) originalOnError(error);
|
2025-10-27 16:46:35 -07:00
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
|
|
|
|
|
`MCP ERROR (${this.serverName})`,
|
|
|
|
|
|
error,
|
|
|
|
|
|
);
|
2025-08-19 21:03:19 +02:00
|
|
|
|
this.updateStatus(MCPServerStatus.DISCONNECTED);
|
|
|
|
|
|
};
|
|
|
|
|
|
this.updateStatus(MCPServerStatus.CONNECTED);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
this.updateStatus(MCPServerStatus.DISCONNECTED);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Discovers tools and prompts from the MCP server.
|
|
|
|
|
|
*/
|
2025-08-28 15:46:27 -07:00
|
|
|
|
async discover(cliConfig: Config): Promise<void> {
|
2025-10-14 01:57:10 +09:00
|
|
|
|
this.assertConnected();
|
2025-08-19 21:03:19 +02:00
|
|
|
|
|
2026-01-27 13:50:27 +01:00
|
|
|
|
const prompts = await this.fetchPrompts();
|
2025-08-28 15:46:27 -07:00
|
|
|
|
const tools = await this.discoverTools(cliConfig);
|
2025-12-09 03:43:12 +01:00
|
|
|
|
const resources = await this.discoverResources();
|
|
|
|
|
|
this.updateResourceRegistry(resources);
|
2025-08-19 21:03:19 +02:00
|
|
|
|
|
2025-12-09 03:43:12 +01:00
|
|
|
|
if (prompts.length === 0 && tools.length === 0 && resources.length === 0) {
|
|
|
|
|
|
throw new Error('No prompts, tools, or resources found on the server.');
|
2025-08-19 21:03:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 13:50:27 +01:00
|
|
|
|
for (const prompt of prompts) {
|
|
|
|
|
|
this.promptRegistry.registerPrompt(prompt);
|
|
|
|
|
|
}
|
2025-08-19 21:03:19 +02:00
|
|
|
|
for (const tool of tools) {
|
|
|
|
|
|
this.toolRegistry.registerTool(tool);
|
|
|
|
|
|
}
|
2025-11-05 15:38:44 -08:00
|
|
|
|
this.toolRegistry.sortTools();
|
2025-08-19 21:03:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Disconnects from the MCP server.
|
|
|
|
|
|
*/
|
|
|
|
|
|
async disconnect(): Promise<void> {
|
2025-10-03 09:23:55 -07:00
|
|
|
|
if (this.status !== MCPServerStatus.CONNECTED) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-30 11:05:49 -07:00
|
|
|
|
this.toolRegistry.removeMcpToolsByServer(this.serverName);
|
2025-11-04 07:51:18 -08:00
|
|
|
|
this.promptRegistry.removePromptsByServer(this.serverName);
|
2025-12-09 03:43:12 +01:00
|
|
|
|
this.resourceRegistry.removeResourcesByServer(this.serverName);
|
2025-10-03 09:23:55 -07:00
|
|
|
|
this.updateStatus(MCPServerStatus.DISCONNECTING);
|
|
|
|
|
|
const client = this.client;
|
|
|
|
|
|
this.client = undefined;
|
2025-08-19 21:03:19 +02:00
|
|
|
|
if (this.transport) {
|
|
|
|
|
|
await this.transport.close();
|
|
|
|
|
|
}
|
2025-10-03 09:23:55 -07:00
|
|
|
|
if (client) {
|
|
|
|
|
|
await client.close();
|
|
|
|
|
|
}
|
2025-08-19 21:03:19 +02:00
|
|
|
|
this.updateStatus(MCPServerStatus.DISCONNECTED);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Returns the current status of the client.
|
|
|
|
|
|
*/
|
|
|
|
|
|
getStatus(): MCPServerStatus {
|
|
|
|
|
|
return this.status;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updateStatus(status: MCPServerStatus): void {
|
|
|
|
|
|
this.status = status;
|
|
|
|
|
|
updateMCPServerStatus(this.serverName, status);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-03 09:23:55 -07:00
|
|
|
|
private assertConnected(): void {
|
|
|
|
|
|
if (this.status !== MCPServerStatus.CONNECTED) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Client is not connected, must connect before interacting with the server. Current state is ${this.status}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-08-19 21:03:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 13:04:38 -08:00
|
|
|
|
private async discoverTools(
|
|
|
|
|
|
cliConfig: Config,
|
|
|
|
|
|
options?: { timeout?: number; signal?: AbortSignal },
|
|
|
|
|
|
): Promise<DiscoveredMCPTool[]> {
|
2025-10-03 09:23:55 -07:00
|
|
|
|
this.assertConnected();
|
2025-08-28 15:46:27 -07:00
|
|
|
|
return discoverTools(
|
|
|
|
|
|
this.serverName,
|
|
|
|
|
|
this.serverConfig,
|
2025-10-03 09:23:55 -07:00
|
|
|
|
this.client!,
|
2025-08-28 15:46:27 -07:00
|
|
|
|
cliConfig,
|
2025-10-28 09:20:57 -07:00
|
|
|
|
this.toolRegistry.getMessageBus(),
|
2026-02-18 12:46:12 -08:00
|
|
|
|
{
|
|
|
|
|
|
...(options ?? {
|
|
|
|
|
|
timeout: this.serverConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
|
|
|
|
|
}),
|
|
|
|
|
|
progressReporter: this,
|
2025-12-04 13:04:38 -08:00
|
|
|
|
},
|
2025-08-28 15:46:27 -07:00
|
|
|
|
);
|
2025-08-19 21:03:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 13:50:27 +01:00
|
|
|
|
private async fetchPrompts(options?: {
|
|
|
|
|
|
signal?: AbortSignal;
|
|
|
|
|
|
}): Promise<DiscoveredMCPPrompt[]> {
|
2025-10-03 09:23:55 -07:00
|
|
|
|
this.assertConnected();
|
2026-01-27 13:50:27 +01:00
|
|
|
|
return discoverPrompts(this.serverName, this.client!, options);
|
2025-08-19 21:03:19 +02:00
|
|
|
|
}
|
2025-11-04 07:51:18 -08:00
|
|
|
|
|
2025-12-09 03:43:12 +01:00
|
|
|
|
private async discoverResources(): Promise<Resource[]> {
|
|
|
|
|
|
this.assertConnected();
|
|
|
|
|
|
return discoverResources(this.serverName, this.client!);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private updateResourceRegistry(resources: Resource[]): void {
|
|
|
|
|
|
this.resourceRegistry.setResourcesForServer(this.serverName, resources);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-08 13:56:26 -08:00
|
|
|
|
async readResource(
|
|
|
|
|
|
uri: string,
|
|
|
|
|
|
options?: { signal?: AbortSignal },
|
|
|
|
|
|
): Promise<ReadResourceResult> {
|
2025-12-09 03:43:12 +01:00
|
|
|
|
this.assertConnected();
|
|
|
|
|
|
return this.client!.request(
|
|
|
|
|
|
{
|
|
|
|
|
|
method: 'resources/read',
|
|
|
|
|
|
params: { uri },
|
|
|
|
|
|
},
|
|
|
|
|
|
ReadResourceResultSchema,
|
2026-02-08 13:56:26 -08:00
|
|
|
|
options,
|
2025-12-09 03:43:12 +01:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Registers notification handlers for dynamic updates from the MCP server.
|
|
|
|
|
|
* This includes handlers for tool list changes and resource list changes.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private registerNotificationHandlers(): void {
|
|
|
|
|
|
if (!this.client) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const capabilities = this.client.getServerCapabilities();
|
|
|
|
|
|
|
|
|
|
|
|
if (capabilities?.tools?.listChanged) {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Server '${this.serverName}' supports tool updates. Listening for changes...`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
this.client.setNotificationHandler(
|
|
|
|
|
|
ToolListChangedNotificationSchema,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`🔔 Received tool update notification from '${this.serverName}'`,
|
|
|
|
|
|
);
|
|
|
|
|
|
await this.refreshTools();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (capabilities?.resources?.listChanged) {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Server '${this.serverName}' supports resource updates. Listening for changes...`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
this.client.setNotificationHandler(
|
|
|
|
|
|
ResourceListChangedNotificationSchema,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`🔔 Received resource update notification from '${this.serverName}'`,
|
|
|
|
|
|
);
|
|
|
|
|
|
await this.refreshResources();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-27 13:50:27 +01:00
|
|
|
|
|
|
|
|
|
|
if (capabilities?.prompts?.listChanged) {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Server '${this.serverName}' supports prompt updates. Listening for changes...`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
this.client.setNotificationHandler(
|
|
|
|
|
|
PromptListChangedNotificationSchema,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`🔔 Received prompt update notification from '${this.serverName}'`,
|
|
|
|
|
|
);
|
|
|
|
|
|
await this.refreshPrompts();
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-02-18 12:46:12 -08:00
|
|
|
|
|
|
|
|
|
|
this.client.setNotificationHandler(
|
|
|
|
|
|
ProgressNotificationSchema,
|
|
|
|
|
|
(notification) => {
|
|
|
|
|
|
const { progressToken, progress, total, message } = notification.params;
|
|
|
|
|
|
const callId = this.progressTokenToCallId.get(progressToken);
|
|
|
|
|
|
|
|
|
|
|
|
if (callId) {
|
|
|
|
|
|
coreEvents.emitMcpProgress({
|
|
|
|
|
|
serverName: this.serverName,
|
|
|
|
|
|
callId,
|
|
|
|
|
|
progressToken,
|
|
|
|
|
|
progress,
|
|
|
|
|
|
total,
|
|
|
|
|
|
message,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-12-09 03:43:12 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Refreshes the resources for this server by re-querying the MCP `resources/list` endpoint.
|
|
|
|
|
|
*
|
|
|
|
|
|
* This method implements a **Coalescing Pattern** to handle rapid bursts of notifications
|
|
|
|
|
|
* (e.g., during server startup or bulk updates) without overwhelming the server or
|
|
|
|
|
|
* creating race conditions in the ResourceRegistry.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async refreshResources(): Promise<void> {
|
|
|
|
|
|
if (this.isRefreshingResources) {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Resource refresh for '${this.serverName}' is already in progress. Pending update.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
this.pendingResourceRefresh = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.isRefreshingResources = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
do {
|
|
|
|
|
|
this.pendingResourceRefresh = false;
|
|
|
|
|
|
|
|
|
|
|
|
if (this.status !== MCPServerStatus.CONNECTED || !this.client) break;
|
|
|
|
|
|
|
|
|
|
|
|
const timeoutMs = this.serverConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC;
|
|
|
|
|
|
const abortController = new AbortController();
|
|
|
|
|
|
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|
|
|
|
|
|
|
|
|
|
|
|
let newResources;
|
|
|
|
|
|
try {
|
|
|
|
|
|
newResources = await this.discoverResources();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
debugLogger.error(
|
|
|
|
|
|
`Resource discovery failed during refresh: ${getErrorMessage(err)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.updateResourceRegistry(newResources);
|
|
|
|
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'info',
|
|
|
|
|
|
`Resources updated for server: ${this.serverName}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
} while (this.pendingResourceRefresh);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
debugLogger.error(
|
|
|
|
|
|
`Critical error in resource refresh loop for ${this.serverName}: ${getErrorMessage(error)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.isRefreshingResources = false;
|
|
|
|
|
|
this.pendingResourceRefresh = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-18 12:46:12 -08:00
|
|
|
|
/**
|
|
|
|
|
|
* Registers a progress token for a tool call.
|
|
|
|
|
|
*/
|
|
|
|
|
|
registerProgressToken(token: string | number, callId: string): void {
|
|
|
|
|
|
this.progressTokenToCallId.set(token, callId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Unregisters a progress token.
|
|
|
|
|
|
*/
|
|
|
|
|
|
unregisterProgressToken(token: string | number): void {
|
|
|
|
|
|
this.progressTokenToCallId.delete(token);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-27 13:50:27 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Refreshes prompts for this server by re-querying the MCP `prompts/list` endpoint.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async refreshPrompts(): Promise<void> {
|
|
|
|
|
|
if (this.isRefreshingPrompts) {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Prompt refresh for '${this.serverName}' is already in progress. Pending update.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
this.pendingPromptRefresh = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.isRefreshingPrompts = true;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
do {
|
|
|
|
|
|
this.pendingPromptRefresh = false;
|
|
|
|
|
|
|
|
|
|
|
|
if (this.status !== MCPServerStatus.CONNECTED || !this.client) break;
|
|
|
|
|
|
|
|
|
|
|
|
const timeoutMs = this.serverConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC;
|
|
|
|
|
|
const abortController = new AbortController();
|
|
|
|
|
|
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const newPrompts = await this.fetchPrompts({
|
|
|
|
|
|
signal: abortController.signal,
|
|
|
|
|
|
});
|
|
|
|
|
|
this.promptRegistry.removePromptsByServer(this.serverName);
|
|
|
|
|
|
for (const prompt of newPrompts) {
|
|
|
|
|
|
this.promptRegistry.registerPrompt(prompt);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
debugLogger.error(
|
|
|
|
|
|
`Prompt discovery failed during refresh: ${getErrorMessage(err)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'info',
|
|
|
|
|
|
`Prompts updated for server: ${this.serverName}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
} while (this.pendingPromptRefresh);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
debugLogger.error(
|
|
|
|
|
|
`Critical error in prompt refresh loop for ${this.serverName}: ${getErrorMessage(error)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.isRefreshingPrompts = false;
|
|
|
|
|
|
this.pendingPromptRefresh = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-04 07:51:18 -08:00
|
|
|
|
getServerConfig(): MCPServerConfig {
|
|
|
|
|
|
return this.serverConfig;
|
|
|
|
|
|
}
|
2025-11-26 13:08:47 -05:00
|
|
|
|
|
|
|
|
|
|
getInstructions(): string | undefined {
|
|
|
|
|
|
return this.client?.getInstructions();
|
|
|
|
|
|
}
|
2025-12-04 13:04:38 -08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Refreshes the tools for this server by re-querying the MCP `tools/list` endpoint.
|
|
|
|
|
|
*
|
|
|
|
|
|
* This method implements a **Coalescing Pattern** to handle rapid bursts of notifications
|
|
|
|
|
|
* (e.g., during server startup or bulk updates) without overwhelming the server or
|
|
|
|
|
|
* creating race conditions in the global ToolRegistry.
|
|
|
|
|
|
*/
|
|
|
|
|
|
private async refreshTools(): Promise<void> {
|
2025-12-09 03:43:12 +01:00
|
|
|
|
if (this.isRefreshingTools) {
|
2025-12-04 13:04:38 -08:00
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Tool refresh for '${this.serverName}' is already in progress. Pending update.`,
|
|
|
|
|
|
);
|
2025-12-09 03:43:12 +01:00
|
|
|
|
this.pendingToolRefresh = true;
|
2025-12-04 13:04:38 -08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 03:43:12 +01:00
|
|
|
|
this.isRefreshingTools = true;
|
2025-12-04 13:04:38 -08:00
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
do {
|
2025-12-09 03:43:12 +01:00
|
|
|
|
this.pendingToolRefresh = false;
|
2025-12-04 13:04:38 -08:00
|
|
|
|
|
|
|
|
|
|
if (this.status !== MCPServerStatus.CONNECTED || !this.client) break;
|
|
|
|
|
|
|
|
|
|
|
|
const timeoutMs = this.serverConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC;
|
|
|
|
|
|
const abortController = new AbortController();
|
|
|
|
|
|
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|
|
|
|
|
|
|
|
|
|
|
|
let newTools;
|
|
|
|
|
|
try {
|
|
|
|
|
|
newTools = await this.discoverTools(this.cliConfig, {
|
|
|
|
|
|
signal: abortController.signal,
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
debugLogger.error(
|
|
|
|
|
|
`Discovery failed during refresh: ${getErrorMessage(err)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.toolRegistry.removeMcpToolsByServer(this.serverName);
|
|
|
|
|
|
|
|
|
|
|
|
for (const tool of newTools) {
|
|
|
|
|
|
this.toolRegistry.registerTool(tool);
|
|
|
|
|
|
}
|
|
|
|
|
|
this.toolRegistry.sortTools();
|
|
|
|
|
|
|
|
|
|
|
|
if (this.onToolsUpdated) {
|
|
|
|
|
|
await this.onToolsUpdated(abortController.signal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'info',
|
|
|
|
|
|
`Tools updated for server: ${this.serverName}`,
|
|
|
|
|
|
);
|
2025-12-09 03:43:12 +01:00
|
|
|
|
} while (this.pendingToolRefresh);
|
2025-12-04 13:04:38 -08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
debugLogger.error(
|
|
|
|
|
|
`Critical error in refresh loop for ${this.serverName}: ${getErrorMessage(error)}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
} finally {
|
2025-12-09 03:43:12 +01:00
|
|
|
|
this.isRefreshingTools = false;
|
|
|
|
|
|
this.pendingToolRefresh = false;
|
2025-12-04 13:04:38 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-08-19 21:03:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-07 15:06:18 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Map to track the status of each MCP server within the core package
|
|
|
|
|
|
*/
|
2025-07-25 20:56:33 +00:00
|
|
|
|
const serverStatuses: Map<string, MCPServerStatus> = new Map();
|
2025-06-07 15:06:18 -04:00
|
|
|
|
|
2025-06-10 08:47:46 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Track the overall MCP discovery state
|
|
|
|
|
|
*/
|
|
|
|
|
|
let mcpDiscoveryState: MCPDiscoveryState = MCPDiscoveryState.NOT_STARTED;
|
|
|
|
|
|
|
2025-07-22 09:34:56 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Map to track which MCP servers have been discovered to require OAuth
|
|
|
|
|
|
*/
|
|
|
|
|
|
export const mcpServerRequiresOAuth: Map<string, boolean> = new Map();
|
|
|
|
|
|
|
2025-06-07 15:06:18 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Event listeners for MCP server status changes
|
|
|
|
|
|
*/
|
|
|
|
|
|
type StatusChangeListener = (
|
|
|
|
|
|
serverName: string,
|
|
|
|
|
|
status: MCPServerStatus,
|
|
|
|
|
|
) => void;
|
|
|
|
|
|
const statusChangeListeners: StatusChangeListener[] = [];
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Add a listener for MCP server status changes
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function addMCPStatusChangeListener(
|
|
|
|
|
|
listener: StatusChangeListener,
|
|
|
|
|
|
): void {
|
|
|
|
|
|
statusChangeListeners.push(listener);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Remove a listener for MCP server status changes
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function removeMCPStatusChangeListener(
|
|
|
|
|
|
listener: StatusChangeListener,
|
|
|
|
|
|
): void {
|
|
|
|
|
|
const index = statusChangeListeners.indexOf(listener);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
statusChangeListeners.splice(index, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Update the status of an MCP server
|
|
|
|
|
|
*/
|
2025-08-19 21:03:19 +02:00
|
|
|
|
export function updateMCPServerStatus(
|
2025-06-07 15:06:18 -04:00
|
|
|
|
serverName: string,
|
|
|
|
|
|
status: MCPServerStatus,
|
|
|
|
|
|
): void {
|
2025-07-25 20:56:33 +00:00
|
|
|
|
serverStatuses.set(serverName, status);
|
2025-06-07 15:06:18 -04:00
|
|
|
|
// Notify all listeners
|
|
|
|
|
|
for (const listener of statusChangeListeners) {
|
|
|
|
|
|
listener(serverName, status);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get the current status of an MCP server
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getMCPServerStatus(serverName: string): MCPServerStatus {
|
2025-07-25 20:56:33 +00:00
|
|
|
|
return serverStatuses.get(serverName) || MCPServerStatus.DISCONNECTED;
|
2025-06-07 15:06:18 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Get all MCP server statuses
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getAllMCPServerStatuses(): Map<string, MCPServerStatus> {
|
2025-07-25 20:56:33 +00:00
|
|
|
|
return new Map(serverStatuses);
|
2025-06-07 15:06:18 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-10 08:47:46 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* Get the current MCP discovery state
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getMCPDiscoveryState(): MCPDiscoveryState {
|
|
|
|
|
|
return mcpDiscoveryState;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-22 09:34:56 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* 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: string): string | null {
|
|
|
|
|
|
// 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: string,
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
|
|
|
|
|
wwwAuthenticate: string,
|
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
|
try {
|
2025-10-21 16:35:22 -04:00
|
|
|
|
debugLogger.log(`🔐 '${mcpServerName}' requires OAuth authentication`);
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
|
|
|
|
|
// 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);
|
2025-08-21 16:05:45 +09:00
|
|
|
|
} 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}`;
|
2025-07-22 09:34:56 -04:00
|
|
|
|
oauthConfig = await OAuthUtils.discoverOAuthConfig(baseUrl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!oauthConfig) {
|
2025-10-27 16:46:35 -07:00
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
|
|
|
|
|
`Could not configure OAuth for '${mcpServerName}' - please authenticate manually with /mcp auth ${mcpServerName}`,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// OAuth configuration discovered - proceed with authentication
|
|
|
|
|
|
|
|
|
|
|
|
// Create OAuth configuration for authentication
|
|
|
|
|
|
const oauthAuthConfig = {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
authorizationUrl: oauthConfig.authorizationUrl,
|
2026-02-18 14:38:04 -08:00
|
|
|
|
issuer: oauthConfig.issuer,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
tokenUrl: oauthConfig.tokenUrl,
|
|
|
|
|
|
scopes: oauthConfig.scopes || [],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Perform OAuth authentication
|
2025-08-15 15:14:48 -04:00
|
|
|
|
// Pass the server URL for proper discovery
|
|
|
|
|
|
const serverUrl = mcpServerConfig.httpUrl || mcpServerConfig.url;
|
2025-10-21 16:35:22 -04:00
|
|
|
|
debugLogger.log(
|
2025-07-22 09:34:56 -04:00
|
|
|
|
`Starting OAuth authentication for server '${mcpServerName}'...`,
|
|
|
|
|
|
);
|
2025-09-04 16:42:47 -04:00
|
|
|
|
const authProvider = new MCPOAuthProvider(new MCPOAuthTokenStorage());
|
|
|
|
|
|
await authProvider.authenticate(mcpServerName, oauthAuthConfig, serverUrl);
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-10-21 16:35:22 -04:00
|
|
|
|
debugLogger.log(
|
2025-07-22 09:34:56 -04:00
|
|
|
|
`OAuth authentication successful for server '${mcpServerName}'`,
|
|
|
|
|
|
);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
2025-10-27 16:46:35 -07:00
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
2025-07-22 09:34:56 -04:00
|
|
|
|
`Failed to handle automatic OAuth for server '${mcpServerName}': ${getErrorMessage(error)}`,
|
2025-10-27 16:46:35 -07:00
|
|
|
|
error,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-10 17:06:31 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Create RequestInit for TransportOptions.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param mcpServerConfig The MCP server configuration
|
|
|
|
|
|
* @param headers Additional headers
|
|
|
|
|
|
*/
|
|
|
|
|
|
function createTransportRequestInit(
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
|
|
|
|
|
headers: Record<string, string>,
|
|
|
|
|
|
): RequestInit {
|
|
|
|
|
|
return {
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
...mcpServerConfig.headers,
|
|
|
|
|
|
...headers,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-19 10:23:01 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Create an AuthProvider for the MCP Transport.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param mcpServerConfig The MCP server configuration
|
|
|
|
|
|
*/
|
2025-11-26 12:08:19 -08:00
|
|
|
|
function createAuthProvider(
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
|
|
|
|
|
): McpAuthProvider | undefined {
|
2025-11-19 10:23:01 -05:00
|
|
|
|
if (
|
|
|
|
|
|
mcpServerConfig.authProviderType ===
|
|
|
|
|
|
AuthProviderType.SERVICE_ACCOUNT_IMPERSONATION
|
|
|
|
|
|
) {
|
|
|
|
|
|
return new ServiceAccountImpersonationProvider(mcpServerConfig);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (
|
|
|
|
|
|
mcpServerConfig.authProviderType === AuthProviderType.GOOGLE_CREDENTIALS
|
|
|
|
|
|
) {
|
|
|
|
|
|
return new GoogleCredentialProvider(mcpServerConfig);
|
|
|
|
|
|
}
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-22 09:34:56 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* 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: string,
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
|
|
|
|
|
accessToken: string,
|
|
|
|
|
|
): Promise<StreamableHTTPClientTransport | SSEClientTransport | null> {
|
|
|
|
|
|
try {
|
2025-11-19 10:23:01 -05:00
|
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
|
Authorization: `Bearer ${accessToken}`,
|
|
|
|
|
|
};
|
|
|
|
|
|
const transportOptions:
|
|
|
|
|
|
| StreamableHTTPClientTransportOptions
|
|
|
|
|
|
| SSEClientTransportOptions = {
|
|
|
|
|
|
requestInit: createTransportRequestInit(mcpServerConfig, headers),
|
|
|
|
|
|
};
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-12-02 20:01:33 -05:00
|
|
|
|
return createUrlTransport(mcpServerName, mcpServerConfig, transportOptions);
|
2025-07-22 09:34:56 -04:00
|
|
|
|
} catch (error) {
|
2025-10-27 16:46:35 -07:00
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
2025-07-22 09:34:56 -04:00
|
|
|
|
`Failed to create OAuth transport for server '${mcpServerName}': ${getErrorMessage(error)}`,
|
2025-10-27 16:46:35 -07:00
|
|
|
|
error,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* 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.
|
|
|
|
|
|
*/
|
2025-09-27 10:12:24 +02:00
|
|
|
|
|
2025-06-03 00:40:51 -07:00
|
|
|
|
export async function discoverMcpTools(
|
2026-01-20 22:01:18 +00:00
|
|
|
|
clientVersion: string,
|
2025-06-03 00:40:51 -07:00
|
|
|
|
mcpServers: Record<string, MCPServerConfig>,
|
|
|
|
|
|
mcpServerCommand: string | undefined,
|
|
|
|
|
|
toolRegistry: ToolRegistry,
|
2025-07-25 20:56:33 +00:00
|
|
|
|
promptRegistry: PromptRegistry,
|
2025-07-14 06:42:22 +02:00
|
|
|
|
debugMode: boolean,
|
2025-08-08 16:29:06 -07:00
|
|
|
|
workspaceContext: WorkspaceContext,
|
2025-08-28 15:46:27 -07:00
|
|
|
|
cliConfig: Config,
|
2025-06-03 00:40:51 -07:00
|
|
|
|
): Promise<void> {
|
2025-06-10 08:47:46 -04:00
|
|
|
|
mcpDiscoveryState = MCPDiscoveryState.IN_PROGRESS;
|
|
|
|
|
|
try {
|
2025-07-14 11:19:33 -07:00
|
|
|
|
mcpServers = populateMcpServerCommand(mcpServers, mcpServerCommand);
|
2025-05-28 00:43:23 -07:00
|
|
|
|
|
2025-06-10 08:47:46 -04:00
|
|
|
|
const discoveryPromises = Object.entries(mcpServers).map(
|
|
|
|
|
|
([mcpServerName, mcpServerConfig]) =>
|
2025-07-14 06:42:22 +02:00
|
|
|
|
connectAndDiscover(
|
2026-01-20 22:01:18 +00:00
|
|
|
|
clientVersion,
|
2025-07-14 06:42:22 +02:00
|
|
|
|
mcpServerName,
|
|
|
|
|
|
mcpServerConfig,
|
|
|
|
|
|
toolRegistry,
|
2025-07-25 20:56:33 +00:00
|
|
|
|
promptRegistry,
|
2025-07-14 06:42:22 +02:00
|
|
|
|
debugMode,
|
2025-08-08 16:29:06 -07:00
|
|
|
|
workspaceContext,
|
2025-08-28 15:46:27 -07:00
|
|
|
|
cliConfig,
|
2025-07-14 06:42:22 +02:00
|
|
|
|
),
|
2025-06-10 08:47:46 -04:00
|
|
|
|
);
|
|
|
|
|
|
await Promise.all(discoveryPromises);
|
2025-07-14 11:19:33 -07:00
|
|
|
|
} finally {
|
2025-06-10 08:47:46 -04:00
|
|
|
|
mcpDiscoveryState = MCPDiscoveryState.COMPLETED;
|
|
|
|
|
|
}
|
2025-05-28 00:43:23 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 16:51:25 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* A tolerant JSON Schema validator for MCP tool output schemas.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Some MCP servers (e.g. third‑party extensions) return complex schemas that
|
|
|
|
|
|
* include `$defs` / `$ref` chains which can occasionally trip AJV's resolver,
|
|
|
|
|
|
* causing discovery to fail. This wrapper keeps the default AJV validator for
|
|
|
|
|
|
* normal operation but falls back to a no‑op validator any time schema
|
|
|
|
|
|
* compilation throws, so we can still list and use the tool while emitting a
|
|
|
|
|
|
* debug log.
|
|
|
|
|
|
*/
|
|
|
|
|
|
class LenientJsonSchemaValidator implements jsonSchemaValidator {
|
|
|
|
|
|
private readonly ajvValidator = new AjvJsonSchemaValidator();
|
|
|
|
|
|
|
|
|
|
|
|
getValidator<T>(schema: JsonSchemaType): JsonSchemaValidator<T> {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return this.ajvValidator.getValidator<T>(schema);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
debugLogger.warn(
|
|
|
|
|
|
`Failed to compile MCP tool output schema (${
|
|
|
|
|
|
(schema as Record<string, unknown>)?.['$id'] ?? '<no $id>'
|
|
|
|
|
|
}): ${error instanceof Error ? error.message : String(error)}. ` +
|
|
|
|
|
|
'Skipping output validation for this tool.',
|
|
|
|
|
|
);
|
|
|
|
|
|
return (input: unknown) => ({
|
|
|
|
|
|
valid: true as const,
|
2026-02-10 00:10:15 +00:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
2025-11-20 16:51:25 -05:00
|
|
|
|
data: input as T,
|
|
|
|
|
|
errorMessage: undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
/** Visible for Testing */
|
|
|
|
|
|
export function populateMcpServerCommand(
|
|
|
|
|
|
mcpServers: Record<string, MCPServerConfig>,
|
|
|
|
|
|
mcpServerCommand: string | undefined,
|
|
|
|
|
|
): Record<string, MCPServerConfig> {
|
|
|
|
|
|
if (mcpServerCommand) {
|
|
|
|
|
|
const cmd = mcpServerCommand;
|
2026-02-10 00:10:15 +00:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
2025-07-14 11:19:33 -07:00
|
|
|
|
const args = parse(cmd, process.env) as string[];
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-04 09:13:02 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 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
|
|
|
|
|
|
*/
|
2025-07-14 11:19:33 -07:00
|
|
|
|
export async function connectAndDiscover(
|
2026-01-20 22:01:18 +00:00
|
|
|
|
clientVersion: string,
|
2025-05-28 00:43:23 -07:00
|
|
|
|
mcpServerName: string,
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
2025-06-03 00:40:51 -07:00
|
|
|
|
toolRegistry: ToolRegistry,
|
2025-07-25 20:56:33 +00:00
|
|
|
|
promptRegistry: PromptRegistry,
|
2025-07-14 06:42:22 +02:00
|
|
|
|
debugMode: boolean,
|
2025-08-08 16:29:06 -07:00
|
|
|
|
workspaceContext: WorkspaceContext,
|
2025-08-28 15:46:27 -07:00
|
|
|
|
cliConfig: Config,
|
2025-05-28 00:43:23 -07:00
|
|
|
|
): Promise<void> {
|
2025-06-07 15:06:18 -04:00
|
|
|
|
updateMCPServerStatus(mcpServerName, MCPServerStatus.CONNECTING);
|
|
|
|
|
|
|
2025-08-04 17:38:23 -04:00
|
|
|
|
let mcpClient: Client | undefined;
|
2025-07-14 11:19:33 -07:00
|
|
|
|
try {
|
2026-02-10 17:00:36 -05:00
|
|
|
|
mcpClient = await connectToMcpServer(
|
2026-01-20 22:01:18 +00:00
|
|
|
|
clientVersion,
|
2025-07-14 11:19:33 -07:00
|
|
|
|
mcpServerName,
|
|
|
|
|
|
mcpServerConfig,
|
|
|
|
|
|
debugMode,
|
2025-08-08 16:29:06 -07:00
|
|
|
|
workspaceContext,
|
2025-12-22 19:18:27 -08:00
|
|
|
|
cliConfig.sanitizationConfig,
|
2025-07-14 11:19:33 -07:00
|
|
|
|
);
|
2025-06-30 01:09:08 +01:00
|
|
|
|
|
2026-02-10 17:00:36 -05:00
|
|
|
|
mcpClient.onerror = (error) => {
|
2025-10-27 16:46:35 -07:00
|
|
|
|
coreEvents.emitFeedback('error', `MCP ERROR (${mcpServerName}):`, error);
|
2025-08-04 17:38:23 -04:00
|
|
|
|
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Attempt to discover both prompts and tools
|
2026-01-27 13:50:27 +01:00
|
|
|
|
const prompts = await discoverPrompts(mcpServerName, mcpClient);
|
2025-08-04 17:38:23 -04:00
|
|
|
|
const tools = await discoverTools(
|
|
|
|
|
|
mcpServerName,
|
|
|
|
|
|
mcpServerConfig,
|
|
|
|
|
|
mcpClient,
|
2025-08-28 15:46:27 -07:00
|
|
|
|
cliConfig,
|
2025-10-28 09:20:57 -07:00
|
|
|
|
toolRegistry.getMessageBus(),
|
2025-12-04 13:04:38 -08:00
|
|
|
|
{ timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC },
|
2025-08-04 17:38:23 -04:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
2026-01-27 13:50:27 +01:00
|
|
|
|
// Register any discovered prompts and tools
|
|
|
|
|
|
for (const prompt of prompts) {
|
|
|
|
|
|
promptRegistry.registerPrompt(prompt);
|
|
|
|
|
|
}
|
2025-08-04 17:38:23 -04:00
|
|
|
|
for (const tool of tools) {
|
|
|
|
|
|
toolRegistry.registerTool(tool);
|
2025-07-11 15:59:42 -04:00
|
|
|
|
}
|
2025-11-05 15:38:44 -08:00
|
|
|
|
toolRegistry.sortTools();
|
2025-07-14 11:19:33 -07:00
|
|
|
|
} catch (error) {
|
2025-08-04 17:38:23 -04:00
|
|
|
|
if (mcpClient) {
|
2025-12-05 16:12:49 -08:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
2025-08-04 17:38:23 -04:00
|
|
|
|
mcpClient.close();
|
|
|
|
|
|
}
|
2025-10-27 16:46:35 -07:00
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
2025-07-25 20:56:33 +00:00
|
|
|
|
`Error connecting to MCP server '${mcpServerName}': ${getErrorMessage(
|
|
|
|
|
|
error,
|
|
|
|
|
|
)}`,
|
2025-10-27 16:46:35 -07:00
|
|
|
|
error,
|
2025-07-19 12:44:51 -07:00
|
|
|
|
);
|
2025-06-07 15:06:18 -04:00
|
|
|
|
updateMCPServerStatus(mcpServerName, MCPServerStatus.DISCONNECTED);
|
2025-05-28 00:43:23 -07:00
|
|
|
|
}
|
2025-07-14 11:19:33 -07:00
|
|
|
|
}
|
2025-05-28 00:43:23 -07:00
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* 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.
|
2025-10-28 09:20:57 -07:00
|
|
|
|
* @param cliConfig The CLI configuration object.
|
|
|
|
|
|
* @param messageBus Optional message bus for policy engine integration.
|
2025-07-14 11:19:33 -07:00
|
|
|
|
* @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: string,
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
|
|
|
|
|
mcpClient: Client,
|
2025-08-28 15:46:27 -07:00
|
|
|
|
cliConfig: Config,
|
2026-01-04 17:11:43 -05:00
|
|
|
|
messageBus: MessageBus,
|
2026-02-18 12:46:12 -08:00
|
|
|
|
options?: {
|
|
|
|
|
|
timeout?: number;
|
|
|
|
|
|
signal?: AbortSignal;
|
|
|
|
|
|
progressReporter?: McpProgressReporter;
|
|
|
|
|
|
},
|
2025-07-14 11:19:33 -07:00
|
|
|
|
): Promise<DiscoveredMCPTool[]> {
|
|
|
|
|
|
try {
|
2025-10-02 22:47:58 +03:00
|
|
|
|
// Only request tools if the server supports them.
|
|
|
|
|
|
if (mcpClient.getServerCapabilities()?.tools == null) return [];
|
|
|
|
|
|
|
2025-12-04 13:04:38 -08:00
|
|
|
|
const response = await mcpClient.listTools({}, options);
|
2025-07-14 11:19:33 -07:00
|
|
|
|
const discoveredTools: DiscoveredMCPTool[] = [];
|
2025-11-17 12:03:48 -05:00
|
|
|
|
for (const toolDef of response.tools) {
|
2025-07-30 17:16:21 -07:00
|
|
|
|
try {
|
2025-11-17 12:03:48 -05:00
|
|
|
|
if (!isEnabled(toolDef, mcpServerName, mcpServerConfig)) {
|
2025-07-30 17:16:21 -07:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 12:03:48 -05:00
|
|
|
|
const mcpCallableTool = new McpCallableTool(
|
|
|
|
|
|
mcpClient,
|
|
|
|
|
|
toolDef,
|
|
|
|
|
|
mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
2026-02-18 12:46:12 -08:00
|
|
|
|
options?.progressReporter,
|
2025-11-17 12:03:48 -05:00
|
|
|
|
);
|
|
|
|
|
|
|
2026-02-05 16:37:28 -05:00
|
|
|
|
// Extract readOnlyHint from annotations
|
|
|
|
|
|
const isReadOnly = toolDef.annotations?.readOnlyHint === true;
|
|
|
|
|
|
|
2025-10-28 09:20:57 -07:00
|
|
|
|
const tool = new DiscoveredMCPTool(
|
|
|
|
|
|
mcpCallableTool,
|
|
|
|
|
|
mcpServerName,
|
2025-11-17 12:03:48 -05:00
|
|
|
|
toolDef.name,
|
|
|
|
|
|
toolDef.description ?? '',
|
|
|
|
|
|
toolDef.inputSchema ?? { type: 'object', properties: {} },
|
2026-01-04 17:11:43 -05:00
|
|
|
|
messageBus,
|
2025-10-28 09:20:57 -07:00
|
|
|
|
mcpServerConfig.trust,
|
2026-02-05 16:37:28 -05:00
|
|
|
|
isReadOnly,
|
2025-10-28 09:20:57 -07:00
|
|
|
|
undefined,
|
|
|
|
|
|
cliConfig,
|
2025-10-31 06:50:22 -07:00
|
|
|
|
mcpServerConfig.extension?.name,
|
2025-10-28 09:20:57 -07:00
|
|
|
|
mcpServerConfig.extension?.id,
|
2025-07-30 17:16:21 -07:00
|
|
|
|
);
|
2025-10-28 09:20:57 -07:00
|
|
|
|
|
2026-02-05 16:37:28 -05:00
|
|
|
|
// If the tool is read-only, allow it in Plan mode
|
|
|
|
|
|
if (isReadOnly) {
|
|
|
|
|
|
cliConfig.getPolicyEngine().addRule({
|
|
|
|
|
|
toolName: tool.getFullyQualifiedName(),
|
|
|
|
|
|
decision: PolicyDecision.ASK_USER,
|
|
|
|
|
|
priority: 50, // Match priority of built-in plan tools
|
|
|
|
|
|
modes: [ApprovalMode.PLAN],
|
|
|
|
|
|
source: `MCP Annotation (readOnlyHint) - ${mcpServerName}`,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 09:20:57 -07:00
|
|
|
|
discoveredTools.push(tool);
|
2025-07-30 17:16:21 -07:00
|
|
|
|
} catch (error) {
|
2025-10-27 16:46:35 -07:00
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
2025-07-30 17:16:21 -07:00
|
|
|
|
`Error discovering tool: '${
|
2025-11-17 12:03:48 -05:00
|
|
|
|
toolDef.name
|
2026-02-10 00:10:15 +00:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
2025-07-30 17:16:21 -07:00
|
|
|
|
}' from MCP server '${mcpServerName}': ${(error as Error).message}`,
|
2025-10-27 16:46:35 -07:00
|
|
|
|
error,
|
2025-07-30 17:16:21 -07:00
|
|
|
|
);
|
2025-07-14 11:19:33 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return discoveredTools;
|
|
|
|
|
|
} catch (error) {
|
2025-08-04 17:38:23 -04:00
|
|
|
|
if (
|
|
|
|
|
|
error instanceof Error &&
|
|
|
|
|
|
!error.message?.includes('Method not found')
|
|
|
|
|
|
) {
|
2025-10-27 16:46:35 -07:00
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
2025-08-04 17:38:23 -04:00
|
|
|
|
`Error discovering tools from ${mcpServerName}: ${getErrorMessage(
|
|
|
|
|
|
error,
|
|
|
|
|
|
)}`,
|
2025-10-27 16:46:35 -07:00
|
|
|
|
error,
|
2025-08-04 17:38:23 -04:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return [];
|
2025-07-14 06:42:22 +02:00
|
|
|
|
}
|
2025-07-14 11:19:33 -07:00
|
|
|
|
}
|
2025-07-14 06:42:22 +02:00
|
|
|
|
|
2025-11-17 12:03:48 -05:00
|
|
|
|
class McpCallableTool implements CallableTool {
|
|
|
|
|
|
constructor(
|
|
|
|
|
|
private readonly client: Client,
|
|
|
|
|
|
private readonly toolDef: McpTool,
|
|
|
|
|
|
private readonly timeout: number,
|
2026-02-18 12:46:12 -08:00
|
|
|
|
private readonly progressReporter?: McpProgressReporter,
|
2025-11-17 12:03:48 -05:00
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
|
|
async tool(): Promise<Tool> {
|
|
|
|
|
|
return {
|
|
|
|
|
|
functionDeclarations: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: this.toolDef.name,
|
|
|
|
|
|
description: this.toolDef.description,
|
|
|
|
|
|
parametersJsonSchema: this.toolDef.inputSchema,
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async callTool(functionCalls: FunctionCall[]): Promise<Part[]> {
|
|
|
|
|
|
// We only expect one function call at a time for MCP tools in this context
|
|
|
|
|
|
if (functionCalls.length !== 1) {
|
|
|
|
|
|
throw new Error('McpCallableTool only supports single function call');
|
|
|
|
|
|
}
|
|
|
|
|
|
const call = functionCalls[0];
|
|
|
|
|
|
|
2026-02-18 12:46:12 -08:00
|
|
|
|
const progressToken = randomUUID();
|
|
|
|
|
|
const context = getToolCallContext();
|
|
|
|
|
|
if (context && this.progressReporter) {
|
|
|
|
|
|
this.progressReporter.registerProgressToken(
|
|
|
|
|
|
progressToken,
|
|
|
|
|
|
context.callId,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-17 12:03:48 -05:00
|
|
|
|
try {
|
|
|
|
|
|
const result = await this.client.callTool(
|
|
|
|
|
|
{
|
|
|
|
|
|
name: call.name!,
|
2026-02-10 00:10:15 +00:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
2025-11-17 12:03:48 -05:00
|
|
|
|
arguments: call.args as Record<string, unknown>,
|
2026-02-18 12:46:12 -08:00
|
|
|
|
_meta: { progressToken },
|
2025-11-17 12:03:48 -05:00
|
|
|
|
},
|
|
|
|
|
|
undefined,
|
|
|
|
|
|
{ timeout: this.timeout },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
functionResponse: {
|
|
|
|
|
|
name: call.name,
|
|
|
|
|
|
response: result,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// Return error in the format expected by DiscoveredMCPTool
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
functionResponse: {
|
|
|
|
|
|
name: call.name,
|
|
|
|
|
|
response: {
|
|
|
|
|
|
error: {
|
|
|
|
|
|
message: error instanceof Error ? error.message : String(error),
|
|
|
|
|
|
isError: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
2026-02-18 12:46:12 -08:00
|
|
|
|
} finally {
|
|
|
|
|
|
if (this.progressReporter) {
|
|
|
|
|
|
this.progressReporter.unregisterProgressToken(progressToken);
|
|
|
|
|
|
}
|
2025-11-17 12:03:48 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-25 20:56:33 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 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: string,
|
|
|
|
|
|
mcpClient: Client,
|
2026-01-27 13:50:27 +01:00
|
|
|
|
options?: { signal?: AbortSignal },
|
|
|
|
|
|
): Promise<DiscoveredMCPPrompt[]> {
|
|
|
|
|
|
// Only request prompts if the server supports them.
|
|
|
|
|
|
if (mcpClient.getServerCapabilities()?.prompts == null) return [];
|
2025-08-05 15:50:30 -07:00
|
|
|
|
|
2026-01-27 13:50:27 +01:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await mcpClient.listPrompts({}, options);
|
|
|
|
|
|
return response.prompts.map((prompt) => ({
|
|
|
|
|
|
...prompt,
|
|
|
|
|
|
serverName: mcpServerName,
|
|
|
|
|
|
invoke: (params: Record<string, unknown>) =>
|
|
|
|
|
|
invokeMcpPrompt(mcpServerName, mcpClient, prompt.name, params),
|
|
|
|
|
|
}));
|
2025-07-25 20:56:33 +00:00
|
|
|
|
} catch (error) {
|
2026-01-27 13:50:27 +01:00
|
|
|
|
// It's okay if the method is not found, which is a common case.
|
|
|
|
|
|
if (error instanceof Error && error.message?.includes('Method not found')) {
|
|
|
|
|
|
return [];
|
2025-07-25 20:56:33 +00:00
|
|
|
|
}
|
2026-01-27 13:50:27 +01:00
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
|
|
|
|
|
`Error discovering prompts from ${mcpServerName}: ${getErrorMessage(
|
|
|
|
|
|
error,
|
|
|
|
|
|
)}`,
|
|
|
|
|
|
error,
|
|
|
|
|
|
);
|
|
|
|
|
|
throw error;
|
2025-07-25 20:56:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 03:43:12 +01:00
|
|
|
|
export async function discoverResources(
|
|
|
|
|
|
mcpServerName: string,
|
|
|
|
|
|
mcpClient: Client,
|
|
|
|
|
|
): Promise<Resource[]> {
|
|
|
|
|
|
if (mcpClient.getServerCapabilities()?.resources == null) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const resources = await listResources(mcpServerName, mcpClient);
|
|
|
|
|
|
return resources;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function listResources(
|
|
|
|
|
|
mcpServerName: string,
|
|
|
|
|
|
mcpClient: Client,
|
|
|
|
|
|
): Promise<Resource[]> {
|
|
|
|
|
|
const resources: Resource[] = [];
|
|
|
|
|
|
let cursor: string | undefined;
|
|
|
|
|
|
try {
|
|
|
|
|
|
do {
|
|
|
|
|
|
const response = await mcpClient.request(
|
|
|
|
|
|
{
|
|
|
|
|
|
method: 'resources/list',
|
|
|
|
|
|
params: cursor ? { cursor } : {},
|
|
|
|
|
|
},
|
|
|
|
|
|
ListResourcesResultSchema,
|
|
|
|
|
|
);
|
|
|
|
|
|
resources.push(...(response.resources ?? []));
|
|
|
|
|
|
cursor = response.nextCursor ?? undefined;
|
|
|
|
|
|
} while (cursor);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error instanceof Error && error.message?.includes('Method not found')) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
|
|
|
|
|
`Error discovering resources from ${mcpServerName}: ${getErrorMessage(
|
|
|
|
|
|
error,
|
|
|
|
|
|
)}`,
|
|
|
|
|
|
error,
|
|
|
|
|
|
);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
return resources;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-25 20:56:33 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 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: string,
|
|
|
|
|
|
mcpClient: Client,
|
|
|
|
|
|
promptName: string,
|
|
|
|
|
|
promptParams: Record<string, unknown>,
|
|
|
|
|
|
): Promise<GetPromptResult> {
|
|
|
|
|
|
try {
|
2025-11-17 12:03:48 -05:00
|
|
|
|
const sanitizedParams: Record<string, string> = {};
|
|
|
|
|
|
for (const [key, value] of Object.entries(promptParams)) {
|
|
|
|
|
|
if (value !== undefined && value !== null) {
|
|
|
|
|
|
sanitizedParams[key] = String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const response = await mcpClient.getPrompt({
|
|
|
|
|
|
name: promptName,
|
|
|
|
|
|
arguments: sanitizedParams,
|
|
|
|
|
|
});
|
2025-07-25 20:56:33 +00:00
|
|
|
|
|
|
|
|
|
|
return response;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
error instanceof Error &&
|
|
|
|
|
|
!error.message?.includes('Method not found')
|
|
|
|
|
|
) {
|
2025-10-27 16:46:35 -07:00
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'error',
|
2025-07-25 20:56:33 +00:00
|
|
|
|
`Error invoking prompt '${promptName}' from ${mcpServerName} ${promptParams}: ${getErrorMessage(
|
|
|
|
|
|
error,
|
|
|
|
|
|
)}`,
|
2025-10-27 16:46:35 -07:00
|
|
|
|
error,
|
2025-07-25 20:56:33 +00:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 16:05:45 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* @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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 20:01:33 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Helper function to retrieve a stored OAuth token for an MCP server.
|
|
|
|
|
|
* Handles token validation and refresh automatically.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param serverName The name of the MCP server
|
|
|
|
|
|
* @returns The valid access token, or null if no token is stored
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function getStoredOAuthToken(serverName: string): Promise<string | null> {
|
|
|
|
|
|
const tokenStorage = new MCPOAuthTokenStorage();
|
|
|
|
|
|
const credentials = await tokenStorage.getCredentials(serverName);
|
|
|
|
|
|
if (!credentials) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const authProvider = new MCPOAuthProvider(tokenStorage);
|
|
|
|
|
|
return authProvider.getValidToken(serverName, {
|
|
|
|
|
|
// Pass client ID if available
|
|
|
|
|
|
clientId: credentials.clientId,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Helper function to create an SSE transport with optional OAuth authentication.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param config The MCP server configuration
|
|
|
|
|
|
* @param accessToken Optional OAuth access token for authentication
|
|
|
|
|
|
* @returns A configured SSE transport ready for connection
|
|
|
|
|
|
*/
|
|
|
|
|
|
function createSSETransportWithAuth(
|
|
|
|
|
|
config: MCPServerConfig,
|
|
|
|
|
|
accessToken?: string | null,
|
|
|
|
|
|
): SSEClientTransport {
|
|
|
|
|
|
const headers = {
|
|
|
|
|
|
...config.headers,
|
|
|
|
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const options: SSEClientTransportOptions = {};
|
|
|
|
|
|
if (Object.keys(headers).length > 0) {
|
|
|
|
|
|
options.requestInit = { headers };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new SSEClientTransport(new URL(config.url!), options);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Helper function to connect a client using SSE transport with optional OAuth.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param client The MCP client to connect
|
|
|
|
|
|
* @param config The MCP server configuration
|
|
|
|
|
|
* @param accessToken Optional OAuth access token for authentication
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function connectWithSSETransport(
|
|
|
|
|
|
client: Client,
|
|
|
|
|
|
config: MCPServerConfig,
|
|
|
|
|
|
accessToken?: string | null,
|
2026-02-10 17:00:36 -05:00
|
|
|
|
): Promise<void> {
|
2025-12-02 20:01:33 -05:00
|
|
|
|
const transport = createSSETransportWithAuth(config, accessToken);
|
|
|
|
|
|
await client.connect(transport, {
|
|
|
|
|
|
timeout: config.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Helper function to show authentication required message and throw error.
|
|
|
|
|
|
* Checks if there's a stored token that was rejected (requires re-auth).
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param serverName The name of the MCP server
|
|
|
|
|
|
* @throws Always throws an error with authentication instructions
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function showAuthRequiredMessage(serverName: string): Promise<never> {
|
|
|
|
|
|
const hasRejectedToken = !!(await getStoredOAuthToken(serverName));
|
|
|
|
|
|
|
|
|
|
|
|
const message = hasRejectedToken
|
|
|
|
|
|
? `MCP server '${serverName}' rejected stored OAuth token. Please re-authenticate using: /mcp auth ${serverName}`
|
|
|
|
|
|
: `MCP server '${serverName}' requires authentication using: /mcp auth ${serverName}`;
|
|
|
|
|
|
|
|
|
|
|
|
coreEvents.emitFeedback('info', message);
|
|
|
|
|
|
throw new UnauthorizedError(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Helper function to retry connection with OAuth token after authentication.
|
|
|
|
|
|
* Handles both HTTP and SSE transports based on what previously failed.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param client The MCP client to connect
|
|
|
|
|
|
* @param serverName The name of the MCP server
|
|
|
|
|
|
* @param config The MCP server configuration
|
|
|
|
|
|
* @param accessToken The OAuth access token to use
|
|
|
|
|
|
* @param httpReturned404 Whether the HTTP transport returned 404 (indicating SSE-only server)
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function retryWithOAuth(
|
|
|
|
|
|
client: Client,
|
|
|
|
|
|
serverName: string,
|
|
|
|
|
|
config: MCPServerConfig,
|
|
|
|
|
|
accessToken: string,
|
|
|
|
|
|
httpReturned404: boolean,
|
2026-02-10 17:00:36 -05:00
|
|
|
|
): Promise<void> {
|
2025-12-02 20:01:33 -05:00
|
|
|
|
if (httpReturned404) {
|
|
|
|
|
|
// HTTP returned 404, only try SSE
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Retrying SSE connection to '${serverName}' with OAuth token...`,
|
|
|
|
|
|
);
|
2026-02-10 17:00:36 -05:00
|
|
|
|
await connectWithSSETransport(client, config, accessToken);
|
2025-12-02 20:01:33 -05:00
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Successfully connected to '${serverName}' using SSE with OAuth.`,
|
|
|
|
|
|
);
|
2026-02-10 17:00:36 -05:00
|
|
|
|
return;
|
2025-12-02 20:01:33 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HTTP returned 401, try HTTP with OAuth first
|
|
|
|
|
|
debugLogger.log(`Retrying connection to '${serverName}' with OAuth token...`);
|
|
|
|
|
|
|
|
|
|
|
|
const httpTransport = await createTransportWithOAuth(
|
|
|
|
|
|
serverName,
|
|
|
|
|
|
config,
|
|
|
|
|
|
accessToken,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!httpTransport) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Failed to create OAuth transport for server '${serverName}'`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await client.connect(httpTransport, {
|
|
|
|
|
|
timeout: config.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
|
|
|
|
|
});
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Successfully connected to '${serverName}' using HTTP with OAuth.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (httpError) {
|
|
|
|
|
|
await httpTransport.close();
|
|
|
|
|
|
|
|
|
|
|
|
// If HTTP+OAuth returns 404 and auto-detection enabled, try SSE+OAuth
|
|
|
|
|
|
if (
|
|
|
|
|
|
String(httpError).includes('404') &&
|
|
|
|
|
|
config.url &&
|
|
|
|
|
|
!config.type &&
|
|
|
|
|
|
!config.httpUrl
|
|
|
|
|
|
) {
|
|
|
|
|
|
debugLogger.log(`HTTP with OAuth returned 404, trying SSE with OAuth...`);
|
2026-02-10 17:00:36 -05:00
|
|
|
|
await connectWithSSETransport(client, config, accessToken);
|
2025-12-02 20:01:33 -05:00
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Successfully connected to '${serverName}' using SSE with OAuth.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw httpError;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
/**
|
|
|
|
|
|
* 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.
|
2026-02-10 17:00:36 -05:00
|
|
|
|
* @returns A promise that resolves to a connected MCP `Client` instance.
|
2025-07-14 11:19:33 -07:00
|
|
|
|
* @throws An error if the connection fails or the configuration is invalid.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export async function connectToMcpServer(
|
2026-01-20 22:01:18 +00:00
|
|
|
|
clientVersion: string,
|
2025-07-14 11:19:33 -07:00
|
|
|
|
mcpServerName: string,
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
|
|
|
|
|
debugMode: boolean,
|
2025-08-08 16:29:06 -07:00
|
|
|
|
workspaceContext: WorkspaceContext,
|
2025-12-22 19:18:27 -08:00
|
|
|
|
sanitizationConfig: EnvironmentSanitizationConfig,
|
2026-02-10 17:00:36 -05:00
|
|
|
|
): Promise<Client> {
|
2025-11-20 16:51:25 -05:00
|
|
|
|
const mcpClient = new Client(
|
|
|
|
|
|
{
|
|
|
|
|
|
name: 'gemini-cli-mcp-client',
|
2026-01-20 22:01:18 +00:00
|
|
|
|
version: clientVersion,
|
2025-11-20 16:51:25 -05:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
// Use a tolerant validator so bad output schemas don't block discovery.
|
|
|
|
|
|
jsonSchemaValidator: new LenientJsonSchemaValidator(),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2025-05-28 00:43:23 -07:00
|
|
|
|
|
2025-08-08 16:29:06 -07:00
|
|
|
|
mcpClient.registerCapabilities({
|
2025-08-18 14:09:02 -07:00
|
|
|
|
roots: {
|
|
|
|
|
|
listChanged: true,
|
|
|
|
|
|
},
|
2025-08-08 16:29:06 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
mcpClient.setRequestHandler(ListRootsRequestSchema, async () => {
|
|
|
|
|
|
const roots = [];
|
|
|
|
|
|
for (const dir of workspaceContext.getDirectories()) {
|
|
|
|
|
|
roots.push({
|
|
|
|
|
|
uri: pathToFileURL(dir).toString(),
|
|
|
|
|
|
name: basename(dir),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
roots,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-18 14:09:02 -07:00
|
|
|
|
let unlistenDirectories: Unsubscribe | undefined =
|
|
|
|
|
|
workspaceContext.onDirectoriesChanged(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await mcpClient.notification({
|
|
|
|
|
|
method: 'notifications/roots/list_changed',
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
// If this fails, its almost certainly because the connection was closed
|
|
|
|
|
|
// and we should just stop listening for future directory changes.
|
|
|
|
|
|
unlistenDirectories?.();
|
|
|
|
|
|
unlistenDirectories = undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Attempt to pro-actively unsubscribe if the mcp client closes. This API is
|
|
|
|
|
|
// very brittle though so we don't have any guarantees, hence the try/catch
|
|
|
|
|
|
// above as well.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Be a good steward and don't just bash over onclose.
|
|
|
|
|
|
const oldOnClose = mcpClient.onclose;
|
|
|
|
|
|
mcpClient.onclose = () => {
|
|
|
|
|
|
oldOnClose?.();
|
|
|
|
|
|
unlistenDirectories?.();
|
|
|
|
|
|
unlistenDirectories = undefined;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-12-02 20:01:33 -05:00
|
|
|
|
let firstAttemptError: Error | null = null;
|
|
|
|
|
|
let httpReturned404 = false; // Track if HTTP returned 404 to skip it in OAuth retry
|
|
|
|
|
|
let sseError: Error | null = null; // Track SSE fallback error
|
|
|
|
|
|
|
2025-05-28 00:43:23 -07:00
|
|
|
|
try {
|
2025-07-22 09:34:56 -04:00
|
|
|
|
const transport = await createTransport(
|
2025-07-14 11:19:33 -07:00
|
|
|
|
mcpServerName,
|
|
|
|
|
|
mcpServerConfig,
|
|
|
|
|
|
debugMode,
|
2025-12-22 19:18:27 -08:00
|
|
|
|
sanitizationConfig,
|
2025-07-14 11:19:33 -07:00
|
|
|
|
);
|
|
|
|
|
|
try {
|
|
|
|
|
|
await mcpClient.connect(transport, {
|
|
|
|
|
|
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
|
|
|
|
|
});
|
2026-02-10 17:00:36 -05:00
|
|
|
|
return mcpClient;
|
2025-07-14 11:19:33 -07:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
await transport.close();
|
2026-02-10 00:10:15 +00:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
2025-12-02 20:01:33 -05:00
|
|
|
|
firstAttemptError = error as Error;
|
2025-07-14 11:19:33 -07:00
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
2025-12-02 20:01:33 -05:00
|
|
|
|
} catch (initialError) {
|
|
|
|
|
|
let error = initialError;
|
|
|
|
|
|
|
|
|
|
|
|
// Check if this is a 401 error FIRST (before attempting SSE fallback)
|
|
|
|
|
|
// This ensures OAuth flow happens before we try SSE
|
|
|
|
|
|
if (isAuthenticationError(error) && hasNetworkTransport(mcpServerConfig)) {
|
|
|
|
|
|
// Continue to OAuth handling below (after SSE fallback section)
|
|
|
|
|
|
} else if (
|
|
|
|
|
|
// If not 401, and HTTP failed with url without explicit type, try SSE fallback
|
|
|
|
|
|
firstAttemptError &&
|
|
|
|
|
|
mcpServerConfig.url &&
|
|
|
|
|
|
!mcpServerConfig.type &&
|
|
|
|
|
|
!mcpServerConfig.httpUrl
|
|
|
|
|
|
) {
|
|
|
|
|
|
// Check if HTTP returned 404 - if so, we know it's not an HTTP server
|
|
|
|
|
|
httpReturned404 = String(firstAttemptError).includes('404');
|
|
|
|
|
|
|
|
|
|
|
|
const logMessage = httpReturned404
|
|
|
|
|
|
? `HTTP returned 404, trying SSE transport...`
|
|
|
|
|
|
: `HTTP connection failed, attempting SSE fallback...`;
|
|
|
|
|
|
debugLogger.log(`MCP server '${mcpServerName}': ${logMessage}`);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Try SSE with stored OAuth token if available
|
|
|
|
|
|
// This ensures that SSE fallback works for authenticated servers
|
2026-02-10 17:00:36 -05:00
|
|
|
|
await connectWithSSETransport(
|
2025-12-02 20:01:33 -05:00
|
|
|
|
mcpClient,
|
|
|
|
|
|
mcpServerConfig,
|
|
|
|
|
|
await getStoredOAuthToken(mcpServerName),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`MCP server '${mcpServerName}': Successfully connected using SSE transport.`,
|
|
|
|
|
|
);
|
2026-02-10 17:00:36 -05:00
|
|
|
|
return mcpClient;
|
2025-12-02 20:01:33 -05:00
|
|
|
|
} catch (sseFallbackError) {
|
2026-02-10 00:10:15 +00:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
2025-12-02 20:01:33 -05:00
|
|
|
|
sseError = sseFallbackError as Error;
|
|
|
|
|
|
|
|
|
|
|
|
// If SSE also returned 401, handle OAuth below
|
|
|
|
|
|
if (isAuthenticationError(sseError)) {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`MCP server '${mcpServerName}': SSE returned 401, OAuth authentication required.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
// Update error to be the SSE error for OAuth handling
|
|
|
|
|
|
error = sseError;
|
|
|
|
|
|
// Continue to OAuth handling below
|
|
|
|
|
|
} else {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`MCP server '${mcpServerName}': SSE fallback also failed.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
// Both failed without 401, throw the original error
|
|
|
|
|
|
throw firstAttemptError;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-22 09:34:56 -04:00
|
|
|
|
// Check if this is a 401 error that might indicate OAuth is required
|
2025-12-02 20:01:33 -05:00
|
|
|
|
if (isAuthenticationError(error) && hasNetworkTransport(mcpServerConfig)) {
|
2025-07-22 09:34:56 -04:00
|
|
|
|
mcpServerRequiresOAuth.set(mcpServerName, true);
|
2025-12-02 20:01:33 -05:00
|
|
|
|
|
|
|
|
|
|
// Only trigger automatic OAuth if explicitly enabled in config
|
|
|
|
|
|
// Otherwise, show error and tell user to run /mcp auth command
|
|
|
|
|
|
const shouldTriggerOAuth = mcpServerConfig.oauth?.enabled;
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
|
|
|
|
|
if (!shouldTriggerOAuth) {
|
2025-12-02 20:01:33 -05:00
|
|
|
|
await showAuthRequiredMessage(mcpServerName);
|
2025-07-22 09:34:56 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Try to extract www-authenticate header from the error
|
2025-12-02 20:01:33 -05:00
|
|
|
|
const errorString = String(error);
|
2025-07-22 09:34:56 -04:00
|
|
|
|
let wwwAuthenticate = extractWWWAuthenticateHeader(errorString);
|
2025-06-10 08:47:46 -04:00
|
|
|
|
|
2025-07-22 09:34:56 -04:00
|
|
|
|
// If we didn't get the header from the error string, try to get it from the server
|
2025-08-21 16:05:45 +09:00
|
|
|
|
if (!wwwAuthenticate && hasNetworkTransport(mcpServerConfig)) {
|
2025-10-21 16:35:22 -04:00
|
|
|
|
debugLogger.log(
|
2025-07-22 09:34:56 -04:00
|
|
|
|
`No www-authenticate header in error, trying to fetch it from server...`,
|
|
|
|
|
|
);
|
|
|
|
|
|
try {
|
2025-08-21 16:05:45 +09:00
|
|
|
|
const urlToFetch = mcpServerConfig.httpUrl || mcpServerConfig.url!;
|
2025-12-02 20:01:33 -05:00
|
|
|
|
|
|
|
|
|
|
// Determine correct Accept header based on what transport failed
|
|
|
|
|
|
let acceptHeader: string;
|
|
|
|
|
|
if (mcpServerConfig.httpUrl) {
|
|
|
|
|
|
acceptHeader = 'application/json';
|
|
|
|
|
|
} else if (mcpServerConfig.type === 'http') {
|
|
|
|
|
|
acceptHeader = 'application/json';
|
|
|
|
|
|
} else if (mcpServerConfig.type === 'sse') {
|
|
|
|
|
|
acceptHeader = 'text/event-stream';
|
|
|
|
|
|
} else if (httpReturned404) {
|
|
|
|
|
|
// HTTP failed with 404, SSE returned 401 - use SSE header
|
|
|
|
|
|
acceptHeader = 'text/event-stream';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// HTTP returned 401 - use HTTP header
|
|
|
|
|
|
acceptHeader = 'application/json';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 16:05:45 +09:00
|
|
|
|
const response = await fetch(urlToFetch, {
|
2025-07-22 09:34:56 -04:00
|
|
|
|
method: 'HEAD',
|
|
|
|
|
|
headers: {
|
2025-12-02 20:01:33 -05:00
|
|
|
|
Accept: acceptHeader,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
},
|
|
|
|
|
|
signal: AbortSignal.timeout(5000),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (response.status === 401) {
|
|
|
|
|
|
wwwAuthenticate = response.headers.get('www-authenticate');
|
|
|
|
|
|
if (wwwAuthenticate) {
|
2025-10-21 16:35:22 -04:00
|
|
|
|
debugLogger.log(
|
2025-07-22 09:34:56 -04:00
|
|
|
|
`Found www-authenticate header from server: ${wwwAuthenticate}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (fetchError) {
|
2025-10-21 16:35:22 -04:00
|
|
|
|
debugLogger.debug(
|
2025-08-21 16:05:45 +09:00
|
|
|
|
`Failed to fetch www-authenticate header: ${getErrorMessage(
|
|
|
|
|
|
fetchError,
|
|
|
|
|
|
)}`,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (wwwAuthenticate) {
|
2025-10-21 16:35:22 -04:00
|
|
|
|
debugLogger.log(
|
2025-07-22 09:34:56 -04:00
|
|
|
|
`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
|
2025-12-02 20:01:33 -05:00
|
|
|
|
const accessToken = await getStoredOAuthToken(mcpServerName);
|
|
|
|
|
|
if (!accessToken) {
|
2025-07-22 09:34:56 -04:00
|
|
|
|
throw new Error(
|
2025-12-02 20:01:33 -05:00
|
|
|
|
`Failed to get OAuth token for server '${mcpServerName}'`,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-12-02 20:01:33 -05:00
|
|
|
|
|
2026-02-10 17:00:36 -05:00
|
|
|
|
await retryWithOAuth(
|
2025-12-02 20:01:33 -05:00
|
|
|
|
mcpClient,
|
|
|
|
|
|
mcpServerName,
|
|
|
|
|
|
mcpServerConfig,
|
|
|
|
|
|
accessToken,
|
|
|
|
|
|
httpReturned404,
|
|
|
|
|
|
);
|
2026-02-10 17:00:36 -05:00
|
|
|
|
return mcpClient;
|
2025-07-22 09:34:56 -04:00
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Failed to handle automatic OAuth for server '${mcpServerName}'`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// No www-authenticate header found, but we got a 401
|
2025-12-02 20:01:33 -05:00
|
|
|
|
// Only try OAuth discovery when OAuth is explicitly enabled in config
|
|
|
|
|
|
const shouldTryDiscovery = mcpServerConfig.oauth?.enabled;
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
|
|
|
|
|
if (!shouldTryDiscovery) {
|
2025-12-02 20:01:33 -05:00
|
|
|
|
await showAuthRequiredMessage(mcpServerName);
|
2025-07-22 09:34:56 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 16:05:45 +09:00
|
|
|
|
// For SSE/HTTP servers, try to discover OAuth configuration from the base URL
|
2025-10-21 16:35:22 -04:00
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`🔍 Attempting OAuth discovery for '${mcpServerName}'...`,
|
|
|
|
|
|
);
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-08-21 16:05:45 +09:00
|
|
|
|
if (hasNetworkTransport(mcpServerConfig)) {
|
|
|
|
|
|
const serverUrl = new URL(
|
|
|
|
|
|
mcpServerConfig.httpUrl || mcpServerConfig.url!,
|
|
|
|
|
|
);
|
|
|
|
|
|
const baseUrl = `${serverUrl.protocol}//${serverUrl.host}`;
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-10-27 16:46:35 -07:00
|
|
|
|
// Try to discover OAuth configuration from the base URL
|
|
|
|
|
|
const oauthConfig = await OAuthUtils.discoverOAuthConfig(baseUrl);
|
|
|
|
|
|
if (oauthConfig) {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Discovered OAuth configuration from base URL for server '${mcpServerName}'`,
|
|
|
|
|
|
);
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-10-27 16:46:35 -07:00
|
|
|
|
// Create OAuth configuration for authentication
|
|
|
|
|
|
const oauthAuthConfig = {
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
authorizationUrl: oauthConfig.authorizationUrl,
|
2026-02-18 14:38:04 -08:00
|
|
|
|
issuer: oauthConfig.issuer,
|
2025-10-27 16:46:35 -07:00
|
|
|
|
tokenUrl: oauthConfig.tokenUrl,
|
|
|
|
|
|
scopes: oauthConfig.scopes || [],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Perform OAuth authentication
|
|
|
|
|
|
// Pass the server URL for proper discovery
|
|
|
|
|
|
const authServerUrl =
|
|
|
|
|
|
mcpServerConfig.httpUrl || mcpServerConfig.url;
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Starting OAuth authentication for server '${mcpServerName}'...`,
|
|
|
|
|
|
);
|
|
|
|
|
|
const authProvider = new MCPOAuthProvider(
|
|
|
|
|
|
new MCPOAuthTokenStorage(),
|
|
|
|
|
|
);
|
|
|
|
|
|
await authProvider.authenticate(
|
|
|
|
|
|
mcpServerName,
|
|
|
|
|
|
oauthAuthConfig,
|
|
|
|
|
|
authServerUrl,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Retry connection with OAuth token
|
2025-12-02 20:01:33 -05:00
|
|
|
|
const accessToken = await getStoredOAuthToken(mcpServerName);
|
|
|
|
|
|
if (!accessToken) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Failed to get OAuth token for server '${mcpServerName}'`,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
);
|
2025-12-02 20:01:33 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create transport with OAuth token
|
|
|
|
|
|
const oauthTransport = await createTransportWithOAuth(
|
|
|
|
|
|
mcpServerName,
|
|
|
|
|
|
mcpServerConfig,
|
|
|
|
|
|
accessToken,
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!oauthTransport) {
|
2025-07-22 09:34:56 -04:00
|
|
|
|
throw new Error(
|
2025-12-02 20:01:33 -05:00
|
|
|
|
`Failed to create OAuth transport for server '${mcpServerName}'`,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-12-02 20:01:33 -05:00
|
|
|
|
|
|
|
|
|
|
await mcpClient.connect(oauthTransport, {
|
|
|
|
|
|
timeout: mcpServerConfig.timeout ?? MCP_DEFAULT_TIMEOUT_MSEC,
|
|
|
|
|
|
});
|
|
|
|
|
|
// Connection successful with OAuth
|
2026-02-10 17:00:36 -05:00
|
|
|
|
return mcpClient;
|
2025-10-27 16:46:35 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`OAuth configuration failed for '${mcpServerName}'. Please authenticate manually with /mcp auth ${mcpServerName}`,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`MCP server '${mcpServerName}' requires authentication. Please configure OAuth or check server settings.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Handle other connection errors
|
2025-12-02 20:01:33 -05:00
|
|
|
|
// Re-throw the original error to preserve its structure
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-12-02 20:01:33 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* Helper function to create the appropriate transport based on config
|
|
|
|
|
|
* This handles the logic for httpUrl/url/type consistently
|
|
|
|
|
|
*/
|
|
|
|
|
|
function createUrlTransport(
|
|
|
|
|
|
mcpServerName: string,
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
|
|
|
|
|
transportOptions:
|
|
|
|
|
|
| StreamableHTTPClientTransportOptions
|
|
|
|
|
|
| SSEClientTransportOptions,
|
|
|
|
|
|
): StreamableHTTPClientTransport | SSEClientTransport {
|
|
|
|
|
|
// Priority 1: httpUrl (deprecated)
|
|
|
|
|
|
if (mcpServerConfig.httpUrl) {
|
|
|
|
|
|
if (mcpServerConfig.url) {
|
|
|
|
|
|
debugLogger.warn(
|
|
|
|
|
|
`MCP server '${mcpServerName}': Both 'httpUrl' and 'url' are configured. ` +
|
|
|
|
|
|
`Using deprecated 'httpUrl'. Please migrate to 'url' with 'type: "http"'.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return new StreamableHTTPClientTransport(
|
|
|
|
|
|
new URL(mcpServerConfig.httpUrl),
|
|
|
|
|
|
transportOptions,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-12-02 20:01:33 -05:00
|
|
|
|
// Priority 2 & 3: url with explicit type
|
|
|
|
|
|
if (mcpServerConfig.url && mcpServerConfig.type) {
|
|
|
|
|
|
if (mcpServerConfig.type === 'http') {
|
|
|
|
|
|
return new StreamableHTTPClientTransport(
|
|
|
|
|
|
new URL(mcpServerConfig.url),
|
|
|
|
|
|
transportOptions,
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (mcpServerConfig.type === 'sse') {
|
|
|
|
|
|
return new SSEClientTransport(
|
|
|
|
|
|
new URL(mcpServerConfig.url),
|
|
|
|
|
|
transportOptions,
|
|
|
|
|
|
);
|
2025-06-16 14:37:09 +08:00
|
|
|
|
}
|
2025-05-28 00:43:23 -07:00
|
|
|
|
}
|
2025-12-02 20:01:33 -05:00
|
|
|
|
|
|
|
|
|
|
// Priority 4: url without type (default to HTTP)
|
|
|
|
|
|
if (mcpServerConfig.url) {
|
|
|
|
|
|
return new StreamableHTTPClientTransport(
|
|
|
|
|
|
new URL(mcpServerConfig.url),
|
|
|
|
|
|
transportOptions,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw new Error(`No URL configured for MCP server '${mcpServerName}'`);
|
2025-07-14 11:19:33 -07:00
|
|
|
|
}
|
2025-12-02 20:01:33 -05:00
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
/** Visible for Testing */
|
2025-07-22 09:34:56 -04:00
|
|
|
|
export async function createTransport(
|
2025-07-14 11:19:33 -07:00
|
|
|
|
mcpServerName: string,
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
|
|
|
|
|
debugMode: boolean,
|
2025-12-22 19:18:27 -08:00
|
|
|
|
sanitizationConfig: EnvironmentSanitizationConfig,
|
2025-07-22 09:34:56 -04:00
|
|
|
|
): Promise<Transport> {
|
2025-11-19 10:23:01 -05:00
|
|
|
|
const noUrl = !mcpServerConfig.url && !mcpServerConfig.httpUrl;
|
|
|
|
|
|
if (noUrl) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
mcpServerConfig.authProviderType === AuthProviderType.GOOGLE_CREDENTIALS
|
|
|
|
|
|
) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`URL must be provided in the config for Google Credentials provider`,
|
2025-09-27 10:12:24 +02:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-11-19 10:23:01 -05:00
|
|
|
|
if (
|
|
|
|
|
|
mcpServerConfig.authProviderType ===
|
|
|
|
|
|
AuthProviderType.SERVICE_ACCOUNT_IMPERSONATION
|
|
|
|
|
|
) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`No URL configured for ServiceAccountImpersonation MCP Server`,
|
2025-07-24 10:37:39 -07:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-19 10:23:01 -05:00
|
|
|
|
if (mcpServerConfig.httpUrl || mcpServerConfig.url) {
|
|
|
|
|
|
const authProvider = createAuthProvider(mcpServerConfig);
|
2025-11-26 12:08:19 -08:00
|
|
|
|
const headers: Record<string, string> =
|
|
|
|
|
|
(await authProvider?.getRequestHeaders?.()) ?? {};
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-11-19 10:23:01 -05:00
|
|
|
|
if (authProvider === undefined) {
|
|
|
|
|
|
// Check if we have OAuth configuration or stored tokens
|
|
|
|
|
|
let accessToken: string | null = null;
|
2025-12-02 20:01:33 -05:00
|
|
|
|
if (mcpServerConfig.oauth?.enabled && mcpServerConfig.oauth) {
|
2025-11-19 10:23:01 -05:00
|
|
|
|
const tokenStorage = new MCPOAuthTokenStorage();
|
|
|
|
|
|
const mcpAuthProvider = new MCPOAuthProvider(tokenStorage);
|
|
|
|
|
|
accessToken = await mcpAuthProvider.getValidToken(
|
|
|
|
|
|
mcpServerName,
|
|
|
|
|
|
mcpServerConfig.oauth,
|
|
|
|
|
|
);
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-11-19 10:23:01 -05:00
|
|
|
|
if (!accessToken) {
|
2025-12-02 20:01:33 -05:00
|
|
|
|
// Emit info message (not error) since this is expected behavior
|
|
|
|
|
|
coreEvents.emitFeedback(
|
|
|
|
|
|
'info',
|
|
|
|
|
|
`MCP server '${mcpServerName}' requires authentication using: /mcp auth ${mcpServerName}`,
|
2025-11-19 10:23:01 -05:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Check if we have stored OAuth tokens for this server (from previous authentication)
|
2025-12-02 20:01:33 -05:00
|
|
|
|
accessToken = await getStoredOAuthToken(mcpServerName);
|
|
|
|
|
|
if (accessToken) {
|
|
|
|
|
|
debugLogger.log(
|
|
|
|
|
|
`Found stored OAuth token for server '${mcpServerName}'`,
|
|
|
|
|
|
);
|
2025-11-19 10:23:01 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-02 20:01:33 -05:00
|
|
|
|
if (accessToken) {
|
2025-11-19 10:23:01 -05:00
|
|
|
|
headers['Authorization'] = `Bearer ${accessToken}`;
|
2025-07-22 09:34:56 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-02 13:39:25 -07:00
|
|
|
|
|
2025-11-19 10:23:01 -05:00
|
|
|
|
const transportOptions:
|
|
|
|
|
|
| StreamableHTTPClientTransportOptions
|
|
|
|
|
|
| SSEClientTransportOptions = {
|
|
|
|
|
|
requestInit: createTransportRequestInit(mcpServerConfig, headers),
|
|
|
|
|
|
authProvider,
|
|
|
|
|
|
};
|
2025-07-22 09:34:56 -04:00
|
|
|
|
|
2025-12-02 20:01:33 -05:00
|
|
|
|
return createUrlTransport(mcpServerName, mcpServerConfig, transportOptions);
|
2025-07-14 11:19:33 -07:00
|
|
|
|
}
|
2025-05-29 16:13:11 -07:00
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
if (mcpServerConfig.command) {
|
2026-02-05 18:03:32 +01:00
|
|
|
|
let transport: Transport = new StdioClientTransport({
|
2025-07-14 11:19:33 -07:00
|
|
|
|
command: mcpServerConfig.command,
|
|
|
|
|
|
args: mcpServerConfig.args || [],
|
2026-02-11 15:06:28 -05:00
|
|
|
|
env: {
|
|
|
|
|
|
...sanitizeEnvironment(process.env, sanitizationConfig),
|
|
|
|
|
|
...(mcpServerConfig.env || {}),
|
2026-02-11 16:07:51 -08:00
|
|
|
|
[GEMINI_CLI_IDENTIFICATION_ENV_VAR]:
|
|
|
|
|
|
GEMINI_CLI_IDENTIFICATION_ENV_VAR_VALUE,
|
2026-02-11 15:06:28 -05:00
|
|
|
|
} as Record<string, string>,
|
2025-07-14 11:19:33 -07:00
|
|
|
|
cwd: mcpServerConfig.cwd,
|
|
|
|
|
|
stderr: 'pipe',
|
|
|
|
|
|
});
|
2026-02-05 18:03:32 +01:00
|
|
|
|
|
|
|
|
|
|
// Fix for Xcode 26.3 mcpbridge non-compliant responses
|
|
|
|
|
|
// It returns JSON in `content` instead of `structuredContent`
|
|
|
|
|
|
if (
|
|
|
|
|
|
mcpServerConfig.command === 'xcrun' &&
|
|
|
|
|
|
mcpServerConfig.args?.includes('mcpbridge')
|
|
|
|
|
|
) {
|
|
|
|
|
|
transport = new XcodeMcpBridgeFixTransport(transport);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
if (debugMode) {
|
2026-02-05 18:03:32 +01:00
|
|
|
|
// The `XcodeMcpBridgeFixTransport` wrapper hides the underlying `StdioClientTransport`,
|
|
|
|
|
|
// which exposes `stderr` for debug logging. We need to unwrap it to attach the listener.
|
|
|
|
|
|
|
|
|
|
|
|
const underlyingTransport =
|
|
|
|
|
|
transport instanceof XcodeMcpBridgeFixTransport
|
2026-02-10 00:10:15 +00:00
|
|
|
|
? // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-type-assertion
|
2026-02-05 18:03:32 +01:00
|
|
|
|
(transport as any).transport
|
|
|
|
|
|
: transport;
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
underlyingTransport instanceof StdioClientTransport &&
|
|
|
|
|
|
underlyingTransport.stderr
|
|
|
|
|
|
) {
|
|
|
|
|
|
underlyingTransport.stderr.on('data', (data) => {
|
|
|
|
|
|
const stderrStr = data.toString().trim();
|
|
|
|
|
|
debugLogger.debug(
|
|
|
|
|
|
`[DEBUG] [MCP STDERR (${mcpServerName})]: `,
|
|
|
|
|
|
stderrStr,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-07-14 11:19:33 -07:00
|
|
|
|
}
|
|
|
|
|
|
return transport;
|
|
|
|
|
|
}
|
2025-06-02 13:39:25 -07:00
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
throw new Error(
|
|
|
|
|
|
`Invalid configuration: missing httpUrl (for Streamable HTTP), url (for SSE), and command (for stdio).`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-06-02 13:39:25 -07:00
|
|
|
|
|
2025-11-17 12:03:48 -05:00
|
|
|
|
interface NamedTool {
|
|
|
|
|
|
name?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
/** Visible for testing */
|
|
|
|
|
|
export function isEnabled(
|
2025-11-17 12:03:48 -05:00
|
|
|
|
funcDecl: NamedTool,
|
2025-07-14 11:19:33 -07:00
|
|
|
|
mcpServerName: string,
|
|
|
|
|
|
mcpServerConfig: MCPServerConfig,
|
|
|
|
|
|
): boolean {
|
|
|
|
|
|
if (!funcDecl.name) {
|
2025-10-21 16:35:22 -04:00
|
|
|
|
debugLogger.warn(
|
2025-07-14 11:19:33 -07:00
|
|
|
|
`Discovered a function declaration without a name from MCP server '${mcpServerName}'. Skipping.`,
|
2025-05-28 00:43:23 -07:00
|
|
|
|
);
|
2025-07-14 11:19:33 -07:00
|
|
|
|
return false;
|
2025-06-02 13:39:25 -07:00
|
|
|
|
}
|
2025-07-14 11:19:33 -07:00
|
|
|
|
const { includeTools, excludeTools } = mcpServerConfig;
|
2025-06-02 13:39:25 -07:00
|
|
|
|
|
2025-07-14 11:19:33 -07:00
|
|
|
|
// excludeTools takes precedence over includeTools
|
|
|
|
|
|
if (excludeTools && excludeTools.includes(funcDecl.name)) {
|
|
|
|
|
|
return false;
|
2025-05-28 00:43:23 -07:00
|
|
|
|
}
|
2025-07-14 11:19:33 -07:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
!includeTools ||
|
|
|
|
|
|
includeTools.some(
|
|
|
|
|
|
(tool) => tool === funcDecl.name || tool.startsWith(`${funcDecl.name}(`),
|
|
|
|
|
|
)
|
|
|
|
|
|
);
|
2025-05-28 00:43:23 -07:00
|
|
|
|
}
|