McpGateway extensive logging (#299043)

* McpGateway extensive logging

* update

* update
This commit is contained in:
Josh Spicer
2026-03-03 14:21:47 -08:00
committed by GitHub
parent c5d8f77e74
commit 6acf4df97e
10 changed files with 139 additions and 28 deletions
+1 -1
View File
@@ -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
+12 -2
View File
@@ -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<TContext> extends Disposable implements IServerCh
constructor(
private readonly _ipcServer: IPCServer<TContext>,
@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<T>(_ctx: TContext, _event: string): Event<T> {
@@ -30,6 +35,9 @@ export class McpGatewayChannel<TContext> extends Disposable implements IServerCh
}
async call<T>(ctx: TContext, command: string, args?: unknown): Promise<T> {
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<TContext> extends Disposable implements IServerCh
readResource: (serverIndex, uri) => brokerChannel.call<MCP.ReadResourceResult>('readResource', { serverIndex, uri }),
listResourceTemplates: () => brokerChannel.call<readonly IGatewayServerResourceTemplates[]>('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;
}
+22 -3
View File
@@ -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));
}
+29 -3
View File
@@ -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<unknown> {
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<string, unknown>
: {};
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<MCP.ListResourcesResult> {
@@ -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 };
}
}
@@ -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<IMcpGatewayResult | undefined> {
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<IMcpGatewayService>(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);
}
};
@@ -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));
}
}
@@ -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<T>(_ctx: unknown, command: string, arg?: unknown, cancellationToken?: CancellationToken): Promise<T> {
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<readonly MCP.Tool[]> {
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<string, unknown>, token: CancellationToken = CancellationToken.None): Promise<IGatewayCallToolResult> {
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<readonly IGatewayServerResources[]> {
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<MCP.ReadResourceResult> {
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<readonly IGatewayServerResourceTemplates[]> {
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;
}
}
@@ -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<IMcpGatewayService>(
mainProcessService.getChannel(McpGatewayChannelName)
@@ -31,6 +33,7 @@ export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService {
}
async createGateway(inRemote: boolean): Promise<IMcpGatewayResult | undefined> {
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<IMcpGatewayResult> {
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<IMcpGatewayResult | undefined> {
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<IMcpGatewayService>(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);
}
};
@@ -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));
}
}
@@ -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',