Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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}] POST body: ${body}`);

let message: JsonRpcMessage | JsonRpcMessage[];
try {
message = JSON.parse(body) as JsonRpcMessage | JsonRpcMessage[];
} catch (error) {
Comment thread
joshspicer marked this conversation as resolved.
Outdated
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} client(s): ${payload}`);
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