src/vs/platform/mcp/node/mcpGatewaySession.ts
287 LOC · 255 covered · 32 uncovered · 66 ranges · 18 concepts · 18 introducers · 16 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
mcpGatewaySession.ts ×16
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as http from 'http';
import {
IJsonRpcNotification, IJsonRpcRequest,
isJsonRpcNotification, isJsonRpcResponse, JsonRpcError, JsonRpcMessage, JsonRpcProtocol, JsonRpcResponse
} from '../../../base/common/jsonRpcProtocol.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { hasKey } from '../../../base/common/types.js';
import { ILogger } from '../../log/common/log.js';
import { IMcpGatewaySingleServerInvoker } from '../common/mcpGateway.js';
import { MCP } from '../common/modelContextProtocol.js';
const MCP_LATEST_PROTOCOL_VERSION = '2025-11-25';
const MCP_SUPPORTED_PROTOCOL_VERSIONS = [
'2025-11-25',
'2025-06-18',
'2025-03-26',
'2024-11-05',
'2024-10-07',
];
const MCP_INVALID_REQUEST = -32600;
const MCP_METHOD_NOT_FOUND = -32601;
const MCP_INVALID_PARAMS = -32602;
export class McpGatewaySession extends Disposable {
private readonly _rpc: JsonRpcProtocol;
private readonly _sseClients = new Set<http.ServerResponse>();
private _lastEventId = 0;
private _isInitialized = false;
constructor(
public readonly id: string,
private readonly _logService: ILogger,
private readonly _onDidDispose: () => void,
private readonly _serverInvoker: IMcpGatewaySingleServerInvoker,
) {
super();
this._rpc = this._register(new JsonRpcProtocol(
message => this._handleOutgoingMessage(message),
{
handleRequest: request => this._handleRequest(request),
handleNotification: notification => this._handleNotification(notification),
}
));
this._register(this._serverInvoker.onDidChangeTools(() => {
return;
}
this._logService.info(`[McpGateway][session ${this.id}] Tools changed, notifying client`);
this._rpc.sendNotification({ method: 'notifications/tools/list_changed' });
this._register(this._serverInvoker.onDidChangeResources(() => {
return;
}
this._logService.info(`[McpGateway][session ${this.id}] Resources changed, notifying client`);
this._rpc.sendNotification({ method: 'notifications/resources/list_changed' });
}
public attachSseClient(_req: http.IncomingMessage, res: http.ServerResponse): void {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
});
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})`);
});
}
public async handleIncoming(message: JsonRpcMessage | JsonRpcMessage[]): Promise<JsonRpcResponse[]> {
}
public override dispose(): void {
this._logService.info(`[McpGateway][session ${this.id}] Disposing session (SSE clients: ${this._sseClients.size})`);
for (const client of this._sseClients) {
client.end();
}
}
this._onDidDispose();
super.dispose();
}
private _handleOutgoingMessage(message: JsonRpcMessage): void {
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;
}
this._logService.warn('[McpGatewayService] Ignored unsupported outgoing gateway message');
private _broadcastSse(message: JsonRpcMessage): void {
this._logService.debug(`[McpGateway][session ${this.id}] No SSE clients to broadcast to, dropping message`);
mcpGatewaySession.ts ×2
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}`,
'event: message',
...lines.map(line => `data: ${line}`),
'',
''
].join('\n');
for (const client of [...this._sseClients]) {
if (client.destroyed || client.writableEnded) {
this._sseClients.delete(client);
continue;
}
client.write(data);
}
private async _handleRequest(request: IJsonRpcRequest): Promise<unknown> {
this._logService.debug(`[McpGateway][session ${this.id}] <-- request: ${request.method} (id=${String(request.id)})`);
mcpGatewaySession.ts ×11
if (request.method === 'initialize') {
}
if (!this._isInitialized) {
this._logService.warn(`[McpGateway][session ${this.id}] Rejected request '${request.method}': session not initialized`);
mcpGatewaySession.ts ×1
throw new JsonRpcError(MCP_INVALID_REQUEST, 'Session is not initialized');
}
switch (request.method) {
case 'ping':
return {};
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}`);
mcpGatewaySession.ts ×4
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' });
}
}
private _handleInitialize(request: IJsonRpcRequest): MCP.InitializeResult {
const params = typeof request.params === 'object' && request.params ? request.params as Record<string, unknown> : undefined;
mcpGatewaySession.ts ×4
const clientVersion = typeof params?.protocolVersion === 'string' ? params.protocolVersion : undefined;
const clientInfo = params?.clientInfo as { name?: string; version?: string } | undefined;
const negotiatedVersion = clientVersion && MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion)
this._logService.info(`[McpGateway] Initialize: client=${clientInfo?.name ?? 'unknown'}/${clientInfo?.version ?? '?'}, clientProtocol=${clientVersion ?? '(none)'}, negotiated=${negotiatedVersion}`);
if (clientVersion && clientVersion !== negotiatedVersion) {
this._logService.warn(`[McpGateway] Client requested unsupported protocol version '${clientVersion}', falling back to '${negotiatedVersion}'`);
mcpGatewaySession.ts ×1
}
return {
protocolVersion: negotiatedVersion,
capabilities: {
tools: {
listChanged: true,
},
resources: {
listChanged: true,
},
},
serverInfo: {
name: 'VS Code MCP Gateway',
version: '1.0.0',
}
};
}
private async _handleCallTool(request: IJsonRpcRequest): Promise<MCP.CallToolResult> {
const params = typeof request.params === 'object' && request.params ? request.params as Record<string, unknown> : undefined;
mcpGatewaySession.ts ×8
if (!params || typeof params.name !== 'string') {
throw new JsonRpcError(MCP_INVALID_PARAMS, 'Missing tool call params');
}
if (params.arguments && typeof params.arguments !== 'object') {
throw new JsonRpcError(MCP_INVALID_PARAMS, 'Invalid tool call arguments');
}
const argumentsValue = (params.arguments && typeof params.arguments === 'object')
? 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 = await this._serverInvoker.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;
} catch (error) {
this._logService.error(`[McpGateway][session ${this.id}] Tool '${params.name}' invocation failed`, error);
throw new JsonRpcError(MCP_INVALID_PARAMS, String(error));
}
private async _handleListTools(): Promise<MCP.ListToolsResult> {
this._logService.debug(`[McpGateway][session ${this.id}] Listed ${tools.length} tool(s): [${tools.map(t => t.name).join(', ')}]`);
return { tools: tools as MCP.Tool[] };
}
private async _handleListResources(): Promise<MCP.ListResourcesResult> {
this._logService.debug(`[McpGateway][session ${this.id}] Listed ${resources.length} resource(s)`);
return { resources: resources as MCP.Resource[] };
}
private async _handleReadResource(request: IJsonRpcRequest): Promise<MCP.ReadResourceResult> {
const params = typeof request.params === 'object' && request.params ? request.params as Record<string, unknown> : undefined;
mcpGatewaySession.ts ×4
if (!params || typeof params.uri !== 'string') {
throw new JsonRpcError(MCP_INVALID_PARAMS, 'Missing resource URI');
}
this._logService.debug(`[McpGateway][session ${this.id}] Reading resource '${params.uri}'`);
try {
const result = await this._serverInvoker.readResource(params.uri);
this._logService.debug(`[McpGateway][session ${this.id}] Resource read returned ${result.contents.length} content(s)`);
return result;
} catch (error) {
this._logService.error(`[McpGateway][session ${this.id}] Resource read failed for '${params.uri}'`, error);
throw new JsonRpcError(MCP_INVALID_PARAMS, String(error));
}
private async _handleListResourceTemplates(): Promise<MCP.ListResourceTemplatesResult> {
const resourceTemplates = await this._serverInvoker.listResourceTemplates();
mcpGatewaySession.ts ×2
this._logService.debug(`[McpGateway][session ${this.id}] Listed ${resourceTemplates.length} resource template(s)`);
return { resourceTemplates: resourceTemplates as MCP.ResourceTemplate[] };
}
export function isInitializeMessage(message: JsonRpcMessage | JsonRpcMessage[]): boolean {
const first = Array.isArray(message) ? message[0] : message;
if (!first || !hasKey(first, { method: true })) {
return false;
}
return first.method === 'initialize';
}