From 6acf4df97ef0f7e5d293b81c0bdbbe4e3cd67aba Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:21:47 -0800 Subject: [PATCH] McpGateway extensive logging (#299043) * McpGateway extensive logging * update * update --- src/vs/code/electron-main/app.ts | 2 +- src/vs/platform/mcp/node/mcpGatewayChannel.ts | 14 +++++- src/vs/platform/mcp/node/mcpGatewayService.ts | 25 +++++++++-- src/vs/platform/mcp/node/mcpGatewaySession.ts | 32 +++++++++++-- .../contrib/mcp/browser/mcpGatewayService.ts | 13 +++++- .../mcpGatewayToolBrokerContribution.ts | 4 +- .../mcp/common/mcpGatewayToolBrokerChannel.ts | 45 ++++++++++++++++--- .../mcp/electron-browser/mcpGatewayService.ts | 17 +++++-- .../mcpGatewayToolBrokerContribution.ts | 4 +- .../mcpGatewayToolBrokerChannel.test.ts | 11 ++--- 10 files changed, 139 insertions(+), 28 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 78641318c5f..ab9fb26c1d2 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -1288,7 +1288,7 @@ export class CodeApplication extends Disposable { // MCP const mcpDiscoveryChannel = ProxyChannel.fromService(accessor.get(INativeMcpDiscoveryHelperService), disposables); mainProcessElectronServer.registerChannel(NativeMcpDiscoveryHelperChannelName, mcpDiscoveryChannel); - const mcpGatewayChannel = this._register(new McpGatewayChannel(mainProcessElectronServer, accessor.get(IMcpGatewayService))); + const mcpGatewayChannel = this._register(new McpGatewayChannel(mainProcessElectronServer, accessor.get(IMcpGatewayService), accessor.get(ILoggerMainService))); mainProcessElectronServer.registerChannel(McpGatewayChannelName, mcpGatewayChannel); // Logger diff --git a/src/vs/platform/mcp/node/mcpGatewayChannel.ts b/src/vs/platform/mcp/node/mcpGatewayChannel.ts index 0b0ce1edb0a..78d0e93a3f0 100644 --- a/src/vs/platform/mcp/node/mcpGatewayChannel.ts +++ b/src/vs/platform/mcp/node/mcpGatewayChannel.ts @@ -6,6 +6,7 @@ import { Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { IPCServer, IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; +import { ILoggerService } from '../../log/common/log.js'; import { IGatewayCallToolResult, IGatewayServerResources, IGatewayServerResourceTemplates, IMcpGatewayService, McpGatewayToolBrokerChannelName } from '../common/mcpGateway.js'; import { MCP } from '../common/modelContextProtocol.js'; @@ -19,10 +20,14 @@ export class McpGatewayChannel extends Disposable implements IServerCh constructor( private readonly _ipcServer: IPCServer, - @IMcpGatewayService private readonly mcpGatewayService: IMcpGatewayService + @IMcpGatewayService private readonly mcpGatewayService: IMcpGatewayService, + @ILoggerService private readonly _loggerService: ILoggerService, ) { super(); - this._register(_ipcServer.onDidRemoveConnection(c => mcpGatewayService.disposeGatewaysForClient(c.ctx))); + this._register(_ipcServer.onDidRemoveConnection(c => { + this._loggerService.getLogger('mcpGateway')?.info(`[McpGateway][Channel] Client disconnected: ${c.ctx}, cleaning up gateways`); + mcpGatewayService.disposeGatewaysForClient(c.ctx); + })); } listen(_ctx: TContext, _event: string): Event { @@ -30,6 +35,9 @@ export class McpGatewayChannel extends Disposable implements IServerCh } async call(ctx: TContext, command: string, args?: unknown): Promise { + const logger = this._loggerService.getLogger('mcpGateway'); + logger?.debug(`[McpGateway][Channel] IPC call: ${command} from client ${ctx}`); + switch (command) { case 'createGateway': { const brokerChannel = ipcChannelForContext(this._ipcServer, ctx); @@ -42,9 +50,11 @@ export class McpGatewayChannel extends Disposable implements IServerCh readResource: (serverIndex, uri) => brokerChannel.call('readResource', { serverIndex, uri }), listResourceTemplates: () => brokerChannel.call('listResourceTemplates'), }); + logger?.info(`[McpGateway][Channel] Gateway created: ${result.gatewayId} for client ${ctx}`); return result as T; } case 'disposeGateway': { + logger?.info(`[McpGateway][Channel] Disposing gateway: ${args as string} for client ${ctx}`); await this.mcpGatewayService.disposeGateway(args as string); return undefined as T; } diff --git a/src/vs/platform/mcp/node/mcpGatewayService.ts b/src/vs/platform/mcp/node/mcpGatewayService.ts index c86b23f21db..6131c7677da 100644 --- a/src/vs/platform/mcp/node/mcpGatewayService.ts +++ b/src/vs/platform/mcp/node/mcpGatewayService.ts @@ -56,6 +56,7 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService const gateway = new McpGatewayRoute(gatewayId, this._logger, toolInvoker); this._gateways.set(gatewayId, gateway); + this._logger.info(`[McpGatewayService] Active gateways: ${this._gateways.size}`); // Track client ownership if clientId provided (for cleanup on disconnect) if (clientId) { @@ -83,7 +84,7 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService gateway.dispose(); this._gateways.delete(gatewayId); this._gatewayToClient.delete(gatewayId); - this._logger.info(`[McpGatewayService] Disposed gateway: ${gatewayId}`); + this._logger.info(`[McpGatewayService] Disposed gateway: ${gatewayId} (remaining: ${this._gateways.size})`); // If no more gateways, shut down the server if (this._gateways.size === 0) { @@ -101,7 +102,7 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService } if (gatewaysToDispose.length > 0) { - this._logger.info(`[McpGatewayService] Disposing ${gatewaysToDispose.length} gateway(s) for disconnected client ${clientId}`); + this._logger.info(`[McpGatewayService] Disposing ${gatewaysToDispose.length} gateway(s) for disconnected client ${clientId}: [${gatewaysToDispose.join(', ')}]`); for (const gatewayId of gatewaysToDispose) { this._gateways.get(gatewayId)?.dispose(); @@ -204,6 +205,8 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService const url = new URL(req.url!, `http://${req.headers.host}`); const pathParts = url.pathname.split('/').filter(Boolean); + this._logger.debug(`[McpGatewayService] ${req.method} ${url.pathname} (active gateways: ${this._gateways.size})`); + // Expected path: /gateway/{gatewayId} if (pathParts.length >= 2 && pathParts[0] === 'gateway') { const gatewayId = pathParts[1]; @@ -216,11 +219,13 @@ export class McpGatewayService extends Disposable implements IMcpGatewayService } // Not found + this._logger.warn(`[McpGatewayService] ${req.method} ${url.pathname}: gateway not found`); res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Gateway not found' })); } override dispose(): void { + this._logger.info(`[McpGatewayService] Disposing service (gateways: ${this._gateways.size})`); this._stopServer(); for (const gateway of this._gateways.values()) { gateway.dispose(); @@ -247,6 +252,8 @@ class McpGatewayRoute extends Disposable { } handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void { + this._logger.debug(`[McpGateway][route ${this.gatewayId}] ${req.method} request (sessions: ${this._sessions.size})`); + if (req.method === 'POST') { void this._handlePost(req, res); return; @@ -266,6 +273,7 @@ class McpGatewayRoute extends Disposable { } public override dispose(): void { + this._logger.info(`[McpGateway][route ${this.gatewayId}] Disposing route (sessions: ${this._sessions.size})`); for (const session of this._sessions.values()) { session.dispose(); } @@ -286,6 +294,7 @@ class McpGatewayRoute extends Disposable { return; } + this._logger.info(`[McpGateway][route ${this.gatewayId}] Deleting session ${sessionId}`); session.dispose(); this._sessions.delete(sessionId); res.writeHead(204); @@ -305,6 +314,7 @@ class McpGatewayRoute extends Disposable { return; } + this._logger.info(`[McpGateway][route ${this.gatewayId}] SSE connection requested for session ${sessionId}`); session.attachSseClient(req, res); } @@ -315,10 +325,13 @@ class McpGatewayRoute extends Disposable { return; } + this._logger.debug(`[McpGateway][route ${this.gatewayId}] Handling POST`); + let message: JsonRpcMessage | JsonRpcMessage[]; try { message = JSON.parse(body) as JsonRpcMessage | JsonRpcMessage[]; } catch (error) { + this._logger.warn(`[McpGateway][route ${this.gatewayId}] JSON parse error: ${error instanceof Error ? error.message : String(error)}`); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(JsonRpcProtocol.createParseError('Parse error', error instanceof Error ? error.message : String(error)))); return; @@ -339,13 +352,16 @@ class McpGatewayRoute extends Disposable { }; if (responses.length === 0) { + this._logger.debug(`[McpGateway][route ${this.gatewayId}] POST response: 202 (no content)`); res.writeHead(202, headers); res.end(); return; } + const responseBody = JSON.stringify(Array.isArray(message) ? responses : responses[0]); + this._logger.debug(`[McpGateway][route ${this.gatewayId}] POST response: 200, body: ${responseBody}`); res.writeHead(200, headers); - res.end(JSON.stringify(Array.isArray(message) ? responses : responses[0])); + res.end(responseBody); } catch (error) { this._logger.error('[McpGatewayService] Failed handling gateway request', error); this._respondHttpError(res, 500, 'Internal server error'); @@ -356,6 +372,7 @@ class McpGatewayRoute extends Disposable { if (headerSessionId) { const existing = this._sessions.get(headerSessionId); if (!existing) { + this._logger.warn(`[McpGateway][route ${this.gatewayId}] Session not found: ${headerSessionId}`); this._respondHttpError(res, 404, 'Session not found'); return undefined; } @@ -369,6 +386,7 @@ class McpGatewayRoute extends Disposable { } const sessionId = generateUuid(); + this._logger.info(`[McpGateway][route ${this.gatewayId}] Creating new session ${sessionId}`); const session = new McpGatewaySession(sessionId, this._logger, () => { this._sessions.delete(sessionId); }, this._toolInvoker); @@ -377,6 +395,7 @@ class McpGatewayRoute extends Disposable { } private _respondHttpError(res: http.ServerResponse, statusCode: number, error: string): void { + this._logger.debug(`[McpGateway][route ${this.gatewayId}] HTTP error response: ${statusCode} ${error}`); res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: statusCode, message: error } } satisfies JsonRpcMessage)); } diff --git a/src/vs/platform/mcp/node/mcpGatewaySession.ts b/src/vs/platform/mcp/node/mcpGatewaySession.ts index f35223a15a3..579b0184495 100644 --- a/src/vs/platform/mcp/node/mcpGatewaySession.ts +++ b/src/vs/platform/mcp/node/mcpGatewaySession.ts @@ -105,6 +105,7 @@ export class McpGatewaySession extends Disposable { return; } + this._logService.info(`[McpGateway][session ${this.id}] Tools changed, notifying client`); this._rpc.sendNotification({ method: 'notifications/tools/list_changed' }); })); @@ -113,6 +114,7 @@ export class McpGatewaySession extends Disposable { return; } + this._logService.info(`[McpGateway][session ${this.id}] Resources changed, notifying client`); this._rpc.sendNotification({ method: 'notifications/resources/list_changed' }); })); } @@ -126,9 +128,11 @@ export class McpGatewaySession extends Disposable { res.write(': connected\n\n'); this._sseClients.add(res); + this._logService.info(`[McpGateway][session ${this.id}] SSE client attached (total: ${this._sseClients.size})`); res.on('close', () => { this._sseClients.delete(res); + this._logService.info(`[McpGateway][session ${this.id}] SSE client detached (total: ${this._sseClients.size})`); }); } @@ -145,6 +149,7 @@ export class McpGatewaySession extends Disposable { } public override dispose(): void { + this._logService.info(`[McpGateway][session ${this.id}] Disposing session (SSE clients: ${this._sseClients.size})`); for (const client of this._sseClients) { if (!client.destroyed) { client.end(); @@ -160,10 +165,12 @@ export class McpGatewaySession extends Disposable { if (this._isCollectingPostResponses) { this._pendingResponses.push(message); } + this._logService.debug(`[McpGateway][session ${this.id}] --> response: ${JSON.stringify(message)}`); return; } if (isJsonRpcNotification(message)) { + this._logService.debug(`[McpGateway][session ${this.id}] --> notification: ${(message as IJsonRpcNotification).method}`); this._broadcastSse(message); return; } @@ -173,11 +180,13 @@ export class McpGatewaySession extends Disposable { private _broadcastSse(message: JsonRpcMessage): void { if (this._sseClients.size === 0) { + this._logService.debug(`[McpGateway][session ${this.id}] No SSE clients to broadcast to, dropping message`); return; } const payload = JSON.stringify(message); const eventId = String(++this._lastEventId); + this._logService.debug(`[McpGateway][session ${this.id}] Broadcasting SSE event id=${eventId} to ${this._sseClients.size}`); const lines = payload.split(/\r?\n/g); const data = [ `id: ${eventId}`, @@ -198,11 +207,14 @@ export class McpGatewaySession extends Disposable { } private async _handleRequest(request: IJsonRpcRequest): Promise { + this._logService.debug(`[McpGateway][session ${this.id}] <-- request: ${request.method} (id=${String(request.id)})`); + if (request.method === 'initialize') { return this._handleInitialize(request); } if (!this._isInitialized) { + this._logService.warn(`[McpGateway][session ${this.id}] Rejected request '${request.method}': session not initialized`); throw new JsonRpcError(MCP_INVALID_REQUEST, 'Session is not initialized'); } @@ -220,13 +232,17 @@ export class McpGatewaySession extends Disposable { case 'resources/templates/list': return this._handleListResourceTemplates(); default: + this._logService.warn(`[McpGateway][session ${this.id}] Unknown method: ${request.method}`); throw new JsonRpcError(MCP_METHOD_NOT_FOUND, `Method not found: ${request.method}`); } } private _handleNotification(notification: IJsonRpcNotification): void { + this._logService.debug(`[McpGateway][session ${this.id}] <-- notification: ${notification.method}`); + if (notification.method === 'notifications/initialized') { this._isInitialized = true; + this._logService.info(`[McpGateway][session ${this.id}] Session initialized`); this._rpc.sendNotification({ method: 'notifications/tools/list_changed' }); this._rpc.sendNotification({ method: 'notifications/resources/list_changed' }); } @@ -276,21 +292,27 @@ export class McpGatewaySession extends Disposable { ? params.arguments as Record : {}; + this._logService.debug(`[McpGateway][session ${this.id}] Calling tool '${params.name}' with args: ${JSON.stringify(argumentsValue)}`); + try { const { result, serverIndex } = await this._toolInvoker.callTool(params.name, argumentsValue); + this._logService.debug(`[McpGateway][session ${this.id}] Tool '${params.name}' completed (isError=${result.isError ?? false}, content blocks=${result.content.length})`); return { ...result, content: encodeResourceUrisInContent(result.content, serverIndex), }; } catch (error) { - this._logService.error('[McpGatewayService] Tool call invocation failed', error); + this._logService.error(`[McpGateway][session ${this.id}] Tool '${params.name}' invocation failed`, error); throw new JsonRpcError(MCP_INVALID_PARAMS, String(error)); } } private _handleListTools(): unknown { return this._toolInvoker.listTools() - .then(tools => ({ tools })); + .then(tools => { + this._logService.debug(`[McpGateway][session ${this.id}] Listed ${tools.length} tool(s): [${tools.map(t => t.name).join(', ')}]`); + return { tools }; + }); } private async _handleListResources(): Promise { @@ -304,6 +326,7 @@ export class McpGatewaySession extends Disposable { }); } } + this._logService.debug(`[McpGateway][session ${this.id}] Listed ${allResources.length} resource(s) from ${serverResults.length} server(s)`); return { resources: allResources }; } @@ -314,8 +337,10 @@ export class McpGatewaySession extends Disposable { } const { serverIndex, originalUri } = decodeGatewayResourceUri(params.uri); + this._logService.debug(`[McpGateway][session ${this.id}] Reading resource '${originalUri}' from server ${serverIndex}`); try { const result = await this._toolInvoker.readResource(serverIndex, originalUri); + this._logService.debug(`[McpGateway][session ${this.id}] Resource read returned ${result.contents.length} content(s)`); return { contents: result.contents.map(content => ({ ...content, @@ -323,7 +348,7 @@ export class McpGatewaySession extends Disposable { })), }; } catch (error) { - this._logService.error('[McpGatewayService] Resource read failed', error); + this._logService.error(`[McpGateway][session ${this.id}] Resource read failed for '${originalUri}'`, error); throw new JsonRpcError(MCP_INVALID_PARAMS, String(error)); } } @@ -339,6 +364,7 @@ export class McpGatewaySession extends Disposable { }); } } + this._logService.debug(`[McpGateway][session ${this.id}] Listed ${allTemplates.length} resource template(s) from ${serverResults.length} server(s)`); return { resourceTemplates: allTemplates }; } } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts index 49bb014e5d9..4d2d9df2c49 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts @@ -5,6 +5,7 @@ import { URI } from '../../../../base/common/uri.js'; import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; @@ -23,28 +24,36 @@ export class BrowserMcpGatewayService implements IWorkbenchMcpGatewayService { constructor( @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + @ILogService private readonly _logService: ILogService, ) { } async createGateway(inRemote: boolean): Promise { + this._logService.debug(`[McpGateway][BrowserWorkbench] createGateway requested (inRemote=${inRemote})`); + // Browser can only create gateways in remote environment if (!inRemote) { + this._logService.info('[McpGateway][BrowserWorkbench] Cannot create local gateway in browser environment'); return undefined; } const connection = this._remoteAgentService.getConnection(); if (!connection) { - // Serverless web environment - no gateway available + this._logService.info('[McpGateway][BrowserWorkbench] No remote connection available (serverless web)'); return undefined; } + this._logService.info('[McpGateway][BrowserWorkbench] Creating remote gateway via remote server'); // Use the remote server's gateway service return connection.withChannel(McpGatewayChannelName, async channel => { const service = ProxyChannel.toService(channel); const info = await service.createGateway(undefined); + const address = URI.revive(info.address); + this._logService.info(`[McpGateway][BrowserWorkbench] Remote gateway created: ${address}`); return { - address: URI.revive(info.address), + address, dispose: () => { + this._logService.info(`[McpGateway][BrowserWorkbench] Disposing remote gateway: ${info.gatewayId}`); service.disposeGateway(info.gatewayId); } }; diff --git a/src/vs/workbench/contrib/mcp/browser/mcpGatewayToolBrokerContribution.ts b/src/vs/workbench/contrib/mcp/browser/mcpGatewayToolBrokerContribution.ts index 175bb839214..4c041d05f47 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpGatewayToolBrokerContribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpGatewayToolBrokerContribution.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { McpGatewayToolBrokerChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IMcpService } from '../common/mcpTypes.js'; @@ -13,7 +14,8 @@ export class McpGatewayToolBrokerContribution implements IWorkbenchContribution constructor( @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IMcpService mcpService: IMcpService, + @ILogService logService: ILogService, ) { - remoteAgentService.getConnection()?.registerChannel(McpGatewayToolBrokerChannelName, new McpGatewayToolBrokerChannel(mcpService)); + remoteAgentService.getConnection()?.registerChannel(McpGatewayToolBrokerChannelName, new McpGatewayToolBrokerChannel(mcpService, logService)); } } diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts index 88a1da8b4b6..d1b0a61d23a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayToolBrokerChannel.ts @@ -8,6 +8,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { IGatewayCallToolResult, IGatewayServerResources, IGatewayServerResourceTemplates } from '../../../../platform/mcp/common/mcpGateway.js'; import { MCP } from '../../../../platform/mcp/common/modelContextProtocol.js'; import { McpServer } from './mcpServer.js'; @@ -32,8 +33,10 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh constructor( private readonly _mcpService: IMcpService, + private readonly _logService: ILogService, ) { super(); + this._logService.debug('[McpGateway][ToolBroker] Initialized'); let toolsInitialized = false; this._register(autorun(reader => { @@ -42,6 +45,7 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh } if (toolsInitialized) { + this._logService.debug('[McpGateway][ToolBroker] Tools changed, firing onDidChangeTools'); this._onDidChangeTools.fire(); } else { toolsInitialized = true; @@ -55,6 +59,7 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh } if (resourcesInitialized) { + this._logService.debug('[McpGateway][ToolBroker] Resources changed, firing onDidChangeResources'); this._onDidChangeResources.fire(); } else { resourcesInitialized = true; @@ -93,6 +98,8 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh } async call(_ctx: unknown, command: string, arg?: unknown, cancellationToken?: CancellationToken): Promise { + this._logService.debug(`[McpGateway][ToolBroker] IPC call: ${command}`); + switch (command) { case 'listTools': { const tools = await this._listTools(); @@ -124,11 +131,13 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh private async _listTools(): Promise { const mcpTools: MCP.Tool[] = []; const servers = this._mcpService.servers.get(); + this._logService.debug(`[McpGateway][ToolBroker] listTools: ${servers.length} server(s) known`); await Promise.all(servers.map(server => this._ensureServerReady(server))); for (const server of servers) { const cacheState = server.cacheState.get(); if (cacheState !== McpServerCacheState.Live && cacheState !== McpServerCacheState.Cached && cacheState !== McpServerCacheState.RefreshingFromCached) { + this._logService.debug(`[McpGateway][ToolBroker] Skipping server '${server.definition.id}' (cacheState=${cacheState})`); continue; } @@ -141,58 +150,74 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh } } + this._logService.debug(`[McpGateway][ToolBroker] listTools result: ${mcpTools.length} tool(s): [${mcpTools.map(t => t.name).join(', ')}]`); return mcpTools; } private async _callTool(name: string, args: Record, token: CancellationToken = CancellationToken.None): Promise { + this._logService.debug(`[McpGateway][ToolBroker] callTool '${name}' with args: ${JSON.stringify(args)}`); + for (const server of this._mcpService.servers.get()) { const tool = server.tools.get().find(t => t.definition.name === name && (t.visibility & McpToolVisibility.Model) ); if (tool) { + this._logService.debug(`[McpGateway][ToolBroker] Found tool '${name}' on server '${server.definition.id}' (index=${this._getServerIndex(server)})`); const result = await tool.call(args, undefined, token); + this._logService.debug(`[McpGateway][ToolBroker] Tool '${name}' completed (isError=${result.isError ?? false}, content blocks=${result.content.length})`); return { result, serverIndex: this._getServerIndex(server) }; } } + this._logService.warn(`[McpGateway][ToolBroker] Tool '${name}' not found on any server`); throw new Error(`Unknown tool: ${name}`); } private async _listResources(): Promise { const results: IGatewayServerResources[] = []; const servers = this._mcpService.servers.get(); + this._logService.debug(`[McpGateway][ToolBroker] listResources: ${servers.length} server(s) known`); + await Promise.all(servers.map(async server => { await this._ensureServerReady(server); const capabilities = server.capabilities.get(); if (!capabilities || !(capabilities & McpCapability.Resources)) { + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' has no resource capability, skipping`); return; } try { const resources = await McpServer.callOn(server, h => h.listResources()); + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' (index=${this._getServerIndex(server)}) listed ${resources.length} resource(s)`); results.push({ serverIndex: this._getServerIndex(server), resources }); - } catch { - // Server failed; skip + } catch (error) { + this._logService.warn(`[McpGateway][ToolBroker] Server '${server.definition.id}' failed to list resources`, error); } })); + this._logService.debug(`[McpGateway][ToolBroker] listResources result: ${results.length} server(s) with resources`); return results; } private async _readResource(serverIndex: number, uri: string, token: CancellationToken = CancellationToken.None): Promise { const server = this._getServerByIndex(serverIndex); if (!server) { + this._logService.warn(`[McpGateway][ToolBroker] readResource: unknown server index ${serverIndex}`); throw new Error(`Unknown server index: ${serverIndex}`); } - return McpServer.callOn(server, h => h.readResource({ uri }, token), token); + this._logService.debug(`[McpGateway][ToolBroker] readResource '${uri}' from server '${server.definition.id}' (index=${serverIndex})`); + const result = await McpServer.callOn(server, h => h.readResource({ uri }, token), token); + this._logService.debug(`[McpGateway][ToolBroker] readResource returned ${result.contents.length} content(s)`); + return result; } private async _listResourceTemplates(): Promise { const results: IGatewayServerResourceTemplates[] = []; const servers = this._mcpService.servers.get(); + this._logService.debug(`[McpGateway][ToolBroker] listResourceTemplates: ${servers.length} server(s) known`); await Promise.all(servers.map(async server => { await this._ensureServerReady(server); @@ -204,12 +229,14 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh try { const resourceTemplates = await McpServer.callOn(server, h => h.listResourceTemplates()); + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' (index=${this._getServerIndex(server)}) listed ${resourceTemplates.length} resource template(s)`); results.push({ serverIndex: this._getServerIndex(server), resourceTemplates }); - } catch { - // Server failed; skip + } catch (error) { + this._logService.warn(`[McpGateway][ToolBroker] Server '${server.definition.id}' failed to list resource templates`, error); } })); + this._logService.debug(`[McpGateway][ToolBroker] listResourceTemplates result: ${results.length} server(s) with templates`); return results; } @@ -219,12 +246,16 @@ export class McpGatewayToolBrokerChannel extends Disposable implements IServerCh return true; } + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' not ready (cacheState=${cacheState}), starting...`); try { - return await startServerAndWaitForLiveTools(server, { + const ready = await startServerAndWaitForLiveTools(server, { promptType: 'all-untrusted', errorOnUserInteraction: true, }); - } catch { + this._logService.debug(`[McpGateway][ToolBroker] Server '${server.definition.id}' ready=${ready}`); + return ready; + } catch (error) { + this._logService.warn(`[McpGateway][ToolBroker] Server '${server.definition.id}' failed to start`, error); return false; } } diff --git a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts index 2af2c0b4adf..71d0dcdc64b 100644 --- a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts +++ b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts @@ -6,6 +6,7 @@ import { URI } from '../../../../base/common/uri.js'; import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; @@ -24,6 +25,7 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { constructor( @IMainProcessService mainProcessService: IMainProcessService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + @ILogService private readonly _logService: ILogService, ) { this._localPlatformService = ProxyChannel.toService( mainProcessService.getChannel(McpGatewayChannelName) @@ -31,6 +33,7 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { } async createGateway(inRemote: boolean): Promise { + this._logService.debug(`[McpGateway][Workbench] createGateway requested (inRemote=${inRemote})`); if (inRemote) { return this._createRemoteGateway(); } else { @@ -39,11 +42,15 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { } private async _createLocalGateway(): Promise { + this._logService.info('[McpGateway][Workbench] Creating local gateway via main process'); const info = await this._localPlatformService.createGateway(undefined); + const address = URI.revive(info.address); + this._logService.info(`[McpGateway][Workbench] Local gateway created: ${address}`); return { - address: URI.revive(info.address), + address, dispose: () => { + this._logService.info(`[McpGateway][Workbench] Disposing local gateway: ${info.gatewayId}`); this._localPlatformService.disposeGateway(info.gatewayId); } }; @@ -52,17 +59,21 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { private async _createRemoteGateway(): Promise { const connection = this._remoteAgentService.getConnection(); if (!connection) { - // No remote connection - cannot create remote gateway + this._logService.info('[McpGateway][Workbench] No remote connection available for remote gateway'); return undefined; } + this._logService.info('[McpGateway][Workbench] Creating remote gateway via remote server'); return connection.withChannel(McpGatewayChannelName, async channel => { const service = ProxyChannel.toService(channel); const info = await service.createGateway(undefined); + const address = URI.revive(info.address); + this._logService.info(`[McpGateway][Workbench] Remote gateway created: ${address}`); return { - address: URI.revive(info.address), + address, dispose: () => { + this._logService.info(`[McpGateway][Workbench] Disposing remote gateway: ${info.gatewayId}`); service.disposeGateway(info.gatewayId); } }; diff --git a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayToolBrokerContribution.ts b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayToolBrokerContribution.ts index af994c82fe3..778d53de5f6 100644 --- a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayToolBrokerContribution.ts +++ b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayToolBrokerContribution.ts @@ -5,6 +5,7 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { McpGatewayToolBrokerChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; import { IMcpService } from '../common/mcpTypes.js'; import { McpGatewayToolBrokerChannel } from '../common/mcpGatewayToolBrokerChannel.js'; @@ -13,7 +14,8 @@ export class McpGatewayToolBrokerContribution implements IWorkbenchContribution constructor( @IMainProcessService mainProcessService: IMainProcessService, @IMcpService mcpService: IMcpService, + @ILogService logService: ILogService, ) { - mainProcessService.registerChannel(McpGatewayToolBrokerChannelName, new McpGatewayToolBrokerChannel(mcpService)); + mainProcessService.registerChannel(McpGatewayToolBrokerChannelName, new McpGatewayToolBrokerChannel(mcpService, logService)); } } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts index 53124c8acc3..f102628f86a 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpGatewayToolBrokerChannel.test.ts @@ -7,6 +7,7 @@ import assert from 'assert'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { observableValue } from '../../../../../base/common/observable.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; import { IGatewayCallToolResult } from '../../../../../platform/mcp/common/mcpGateway.js'; import { MCP } from '../../common/modelContextProtocol.js'; import { McpGatewayToolBrokerChannel } from '../../common/mcpGatewayToolBrokerChannel.js'; @@ -18,7 +19,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('lists model-visible tools with namespaced identities', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const serverA = createServer('collectionA', 'serverA', [ createTool('mcp_serverA_echo', async () => ({ content: [{ type: 'text', text: 'A' }] })), @@ -43,7 +44,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('routes tool calls by namespaced identity', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const invoked: string[] = []; const serverA = createServer('collectionA', 'serverA', [ @@ -79,7 +80,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('emits onDidChangeTools when tool lists change', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const server = createServer('collectionA', 'serverA', [ createTool('echo', async () => ({ content: [{ type: 'text', text: 'A' }] })), ]); @@ -104,7 +105,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('does not start server when cache state is live', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const server = createServer( 'collectionA', @@ -122,7 +123,7 @@ suite('McpGatewayToolBrokerChannel', () => { test('starts server when cache state is unknown', async () => { const mcpService = new TestMcpService(); - const channel = new McpGatewayToolBrokerChannel(mcpService); + const channel = new McpGatewayToolBrokerChannel(mcpService, new NullLogService()); const server = createServer( 'collectionA',