Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/inProcHttpServer.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
import type { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
8
import type * as express from 'express';
9
import * as fs from 'fs/promises';
10
import * as os from 'os';
11
import * as path from 'path';
12
import * as vscode from 'vscode';
13
import { ILogger } from '../../../../platform/log/common/logService';
14
import { Disposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';
15
import { generateUuid } from '../../../../util/vs/base/common/uuid';
16
import { ICopilotCLISessionTracker } from './copilotCLISessionTracker';
17
18
interface McpProviderOptions {
19
id: string;
20
serverLabel: string;
21
serverVersion: string;
22
registerTools: (server: McpServer, sessionId: string) => Promise<void> | void;
23
registerPushNotifications?: () => Promise<void> | void;
24
}
25
26
class AsyncLazy<T> {
27
private _value: T | undefined;
28
private _promise: Promise<T> | undefined;
29
30
constructor(private readonly factory: () => Promise<T>) { }
31
32
get value(): Promise<T> {
33
if (this._value !== undefined) {
34
return Promise.resolve(this._value);
35
}
36
37
if (this._promise) {
38
return this._promise;
39
}
40
41
this._promise = this.factory().then(value => {
42
this._value = value;
43
return value;
44
});
45
46
return this._promise;
47
}
48
}
49
50
export class InProcHttpServer extends Disposable {
51
private readonly _transports: Record<string, StreamableHTTPServerTransport> = {};
52
private readonly _onDidClientConnect = this._register(new vscode.EventEmitter<string>());
53
public readonly onDidClientConnect = this._onDidClientConnect.event;
54
private readonly _onDidClientDisconnect = this._register(new vscode.EventEmitter<string>());
55
public readonly onDidClientDisconnect = this._onDidClientDisconnect.event;
56
constructor(
57
private readonly _logger: ILogger,
58
private readonly _sessionTracker: ICopilotCLISessionTracker,
59
) {
60
super();
61
}
62
63
broadcastNotification(method: string, params: Record<string, unknown>): void {
64
const message = {
65
jsonrpc: '2.0' as const,
66
method,
67
params,
68
};
69
70
for (const sessionId in this._transports) {
71
this._transports[sessionId].send(message).catch(() => {
72
this._logger.debug(`Failed to send notification "${method}" to client ${sessionId}`);
73
});
74
}
75
}
76
77
sendNotification(sessionId: string, method: string, params: Record<string, unknown>): void {
78
const transport = this._getTransport(sessionId);
79
if (!transport) {
80
this._logger.debug(`Cannot send notification "${method}": session ${sessionId} not found`);
81
return;
82
}
83
84
const message = {
85
jsonrpc: '2.0' as const,
86
method,
87
params,
88
};
89
90
transport.send(message).catch(() => {
91
this._logger.debug(`Failed to send notification "${method}" to client ${sessionId}`);
92
});
93
}
94
95
getConnectedSessionIds(): readonly string[] {
96
return Object.keys(this._transports);
97
}
98
99
async start(
100
mcpOptions: McpProviderOptions,
101
): Promise<{ serverUri: vscode.Uri; headers: Record<string, string> }> {
102
let socketPath: string | undefined;
103
104
this._logger.debug(`Starting MCP HTTP server for ${mcpOptions.serverLabel}...`);
105
106
try {
107
const nonce = generateUuid();
108
socketPath = await getRandomSocketPath();
109
this._logger.trace(`Generated socket path: ${socketPath}`);
110
111
const expressModule = (await expressLazy.value) as unknown as {
112
default?: typeof import('express');
113
} & typeof import('express');
114
const expressApp = expressModule.default || expressModule;
115
116
const app: express.Application = (expressApp as () => express.Application)();
117
118
// MCP requests like open_diff include full file contents which can exceed the default ~100KB limit
119
app.use(expressApp.json({ limit: '10mb' }));
120
app.use((req: express.Request, res: express.Response, next: express.NextFunction) =>
121
this._authMiddleware(nonce, req, res, next),
122
);
123
124
app.post('/mcp', (req: express.Request, res: express.Response) => this._handlePost(mcpOptions, req, res));
125
app.get('/mcp', (req: express.Request, res: express.Response) => this._handleGetDelete(req, res));
126
app.delete('/mcp', (req: express.Request, res: express.Response) => this._handleGetDelete(req, res));
127
128
const httpServer = app.listen(socketPath);
129
this._logger.debug('HTTP server listening on socket');
130
131
// Register push notifications if provided
132
if (mcpOptions.registerPushNotifications) {
133
this._logger.debug('Registering push notifications...');
134
await Promise.resolve(mcpOptions.registerPushNotifications());
135
}
136
137
this._register(toDisposable(() => {
138
this._logger.info('Shutting down MCP server...');
139
for (const sessionId in this._transports) {
140
void this._transports[sessionId].close();
141
this._unregisterTransport(sessionId);
142
}
143
144
if (httpServer.listening) {
145
httpServer.close();
146
httpServer.closeAllConnections();
147
}
148
149
void tryCleanupSocket(socketPath);
150
this._logger.debug('MCP server shutdown complete');
151
}));
152
return {
153
serverUri: vscode.Uri.from({
154
scheme: os.platform() === 'win32' ? 'pipe' : 'unix',
155
path: socketPath,
156
fragment: '/mcp',
157
}),
158
headers: {
159
Authorization: `Nonce ${nonce}`,
160
},
161
};
162
} catch (err) {
163
void tryCleanupSocket(socketPath);
164
throw err;
165
}
166
}
167
168
private _registerTransport(sessionId: string, transport: StreamableHTTPServerTransport): void {
169
this._transports[sessionId] = transport;
170
this._onDidClientConnect.fire(sessionId);
171
this._logger.info(`Client connected: ${sessionId}`);
172
}
173
174
private _unregisterTransport(sessionId: string): void {
175
delete this._transports[sessionId];
176
this._onDidClientDisconnect.fire(sessionId);
177
this._logger.info(`Client disconnected: ${sessionId}`);
178
}
179
180
private _getTransport(sessionId: string): StreamableHTTPServerTransport | undefined {
181
return this._transports[sessionId];
182
}
183
184
private _authMiddleware(nonce: string, req: express.Request, res: express.Response, next: express.NextFunction): void {
185
if (req.headers.authorization !== `Nonce ${nonce}`) {
186
this._logger.debug(`Unauthorized request to ${req.method} ${req.path}`);
187
res.status(401).send('Unauthorized');
188
return;
189
}
190
191
next();
192
}
193
194
private async _handlePost(mcpOptions: McpProviderOptions, req: express.Request, res: express.Response): Promise<void> {
195
const sessionId = req.headers['mcp-session-id'] ?? req.headers['x-copilot-session-id'];
196
if (Array.isArray(sessionId) || !sessionId || typeof sessionId !== 'string') {
197
res.status(400).json({
198
jsonrpc: '2.0',
199
error: { code: -32000, message: 'Bad Request: Session ID must be a single, defined, string value' },
200
id: null,
201
});
202
return;
203
}
204
this._logger.trace(`POST /mcp request, sessionId: ${sessionId ?? '(none)'}`);
205
206
const isInitializeRequest = await isInitializeRequestLazy.value;
207
const { StreamableHTTPServerTransport } = await streamableHttpLazy.value;
208
209
let transport: StreamableHTTPServerTransport;
210
const existingTransport = sessionId ? this._getTransport(sessionId) : undefined;
211
if (sessionId && existingTransport) {
212
if (isInitializeRequest(req.body)) {
213
this._logger.debug(`Rejecting duplicate initialize for session ${sessionId}`);
214
res.status(409).json({
215
jsonrpc: '2.0',
216
error: {
217
code: -32000,
218
message: 'Conflict: A connection for this session already exists',
219
},
220
id: null,
221
});
222
return;
223
}
224
transport = existingTransport;
225
} else if (sessionId && isInitializeRequest(req.body)) {
226
this._logger.debug('Creating new MCP session...');
227
const clientPid = parseInt(req.headers['x-copilot-pid'] as string, 10);
228
const clientPpid = parseInt(req.headers['x-copilot-parent-pid'] as string, 10);
229
let sessionRegistration: { dispose(): void } | undefined;
230
transport = new StreamableHTTPServerTransport({
231
sessionIdGenerator: () => sessionId,
232
onsessioninitialized: (mcpSessionId) => {
233
this._registerTransport(mcpSessionId, transport);
234
if (!isNaN(clientPid) && !isNaN(clientPpid)) {
235
sessionRegistration = this._sessionTracker.registerSession(mcpSessionId, { pid: clientPid, ppid: clientPpid });
236
}
237
},
238
onsessionclosed: closedSessionId => {
239
this._unregisterTransport(closedSessionId);
240
sessionRegistration?.dispose();
241
},
242
enableDnsRebindingProtection: true,
243
allowedHosts: ['localhost'],
244
});
245
246
const { McpServer } = await mcpServerLazy.value;
247
const server = new McpServer({
248
name: mcpOptions.id,
249
title: mcpOptions.serverLabel,
250
version: mcpOptions.serverVersion,
251
});
252
253
try {
254
this._logger.debug('Registering MCP tools...');
255
await Promise.resolve(mcpOptions.registerTools(server, sessionId));
256
} catch (err) {
257
const errMsg = err instanceof Error ? err.message : String(err);
258
this._logger.error(`Failed to register MCP tools: ${errMsg}`);
259
res.status(500).json({
260
jsonrpc: '2.0',
261
error: {
262
code: -32000,
263
message: `Failed to register MCP tools: ${errMsg}`,
264
},
265
id: null,
266
});
267
return;
268
}
269
270
await server.connect(transport);
271
} else {
272
this._logger.debug('Bad request: No valid session ID provided');
273
res.status(400).json({
274
jsonrpc: '2.0',
275
error: {
276
code: -32000,
277
message: 'Bad Request: No valid session ID provided',
278
},
279
id: null,
280
});
281
return;
282
}
283
284
await transport.handleRequest(req, res, req.body);
285
}
286
287
private async _handleGetDelete(req: express.Request, res: express.Response): Promise<void> {
288
const sessionId = req.headers['mcp-session-id'] as string | undefined;
289
this._logger.trace(`${req.method} /mcp request, sessionId: ${sessionId ?? '(none)'}`);
290
291
const transport = sessionId ? this._getTransport(sessionId) : undefined;
292
if (!sessionId || !transport) {
293
this._logger.debug(`Invalid or missing session ID for ${req.method} request`);
294
res.status(400).send('Invalid or missing session ID');
295
return;
296
}
297
298
await transport.handleRequest(req, res);
299
}
300
}
301
302
async function getRandomSocketPath(): Promise<string> {
303
if (os.platform() === 'win32') {
304
return `\\\\.\\pipe\\mcp-${generateUuid()}.sock`;
305
} else {
306
const prefix = path.join(os.tmpdir(), 'mcp-');
307
const tempDir = await fs.mkdtemp(prefix);
308
await fs.chmod(tempDir, 0o700);
309
return path.join(tempDir, 'mcp.sock');
310
}
311
}
312
313
async function tryCleanupSocket(socketPath: string | undefined): Promise<void> {
314
try {
315
if (os.platform() === 'win32') {
316
return;
317
}
318
319
if (!socketPath) {
320
return;
321
}
322
323
const dir = path.dirname(socketPath);
324
await fs.rm(dir, { recursive: true, force: true });
325
} catch {
326
// Best effort
327
}
328
}
329
330
const expressLazy = new AsyncLazy(async () => await import('express'));
331
const streamableHttpLazy = new AsyncLazy(async () => await import('@modelcontextprotocol/sdk/server/streamableHttp.js'));
332
const mcpServerLazy = new AsyncLazy(async () => await import('@modelcontextprotocol/sdk/server/mcp.js'));
333
const isInitializeRequestLazy = new AsyncLazy(async () => {
334
const { isInitializeRequest } = await import('@modelcontextprotocol/sdk/types.js');
335
return isInitializeRequest;
336
});
337
338