Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/vs/code/electron-main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions src/vs/platform/mcp/node/mcpGatewayChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -19,17 +20,24 @@ 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);
}));
Comment thread
joshspicer marked this conversation as resolved.
}

listen<T>(_ctx: TContext, _event: string): Event<T> {
throw new Error('Invalid listen');
}

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);
Expand All @@ -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;
}
Expand Down
25 changes: 22 additions & 3 deletions src/vs/platform/mcp/node/mcpGatewayService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
Expand Down Expand Up @@ -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];
Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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();
}
Expand All @@ -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);
Expand All @@ -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);
}

Expand All @@ -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;
Expand All @@ -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');
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -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));
}
Expand Down
32 changes: 29 additions & 3 deletions src/vs/platform/mcp/node/mcpGatewaySession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}));

Expand All @@ -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' });
}));
}
Expand All @@ -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})`);
});
}

Expand All @@ -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();
Expand All @@ -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;
}
Expand All @@ -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);
Comment thread
joshspicer marked this conversation as resolved.
const data = [
`id: ${eventId}`,
Expand All @@ -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');
}

Expand All @@ -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' });
}
Expand Down Expand Up @@ -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> {
Expand All @@ -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 };
}

Expand All @@ -314,16 +337,18 @@ 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,
uri: encodeGatewayResourceUri(content.uri, serverIndex),
})),
};
} 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));
}
}
Expand All @@ -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 };
}
}
Expand Down
Loading
Loading