Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/mcp/node/mcpGatewayService.ts
5241 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 * as http from 'http';
7
import { DeferredPromise } from '../../../base/common/async.js';
8
import { Disposable } from '../../../base/common/lifecycle.js';
9
import { URI } from '../../../base/common/uri.js';
10
import { generateUuid } from '../../../base/common/uuid.js';
11
import { ILogService } from '../../log/common/log.js';
12
import { IMcpGatewayInfo, IMcpGatewayService } from '../common/mcpGateway.js';
13
14
/**
15
* Node.js implementation of the MCP Gateway Service.
16
*
17
* Creates and manages an HTTP server on localhost that provides MCP gateway endpoints.
18
* The server is shared among all gateways and uses ref-counting for lifecycle management.
19
*/
20
export class McpGatewayService extends Disposable implements IMcpGatewayService {
21
declare readonly _serviceBrand: undefined;
22
23
private _server: http.Server | undefined;
24
private _port: number | undefined;
25
private readonly _gateways = new Map<string, McpGatewayRoute>();
26
/** Maps gatewayId to clientId for tracking ownership */
27
private readonly _gatewayToClient = new Map<string, unknown>();
28
private _serverStartPromise: Promise<void> | undefined;
29
30
constructor(
31
@ILogService private readonly _logService: ILogService,
32
) {
33
super();
34
}
35
36
async createGateway(clientId: unknown): Promise<IMcpGatewayInfo> {
37
// Ensure server is running
38
await this._ensureServer();
39
40
if (this._port === undefined) {
41
throw new Error('[McpGatewayService] Server failed to start, port is undefined');
42
}
43
44
// Generate a secure random ID for the gateway route
45
const gatewayId = generateUuid();
46
47
// Create the gateway route
48
const gateway = new McpGatewayRoute(gatewayId);
49
this._gateways.set(gatewayId, gateway);
50
51
// Track client ownership if clientId provided (for cleanup on disconnect)
52
if (clientId) {
53
this._gatewayToClient.set(gatewayId, clientId);
54
this._logService.info(`[McpGatewayService] Created gateway at http://127.0.0.1:${this._port}/gateway/${gatewayId} for client ${clientId}`);
55
} else {
56
this._logService.warn(`[McpGatewayService] Created gateway without client tracking at http://127.0.0.1:${this._port}/gateway/${gatewayId}`);
57
}
58
59
const address = URI.parse(`http://127.0.0.1:${this._port}/gateway/${gatewayId}`);
60
61
return {
62
address,
63
gatewayId,
64
};
65
}
66
67
async disposeGateway(gatewayId: string): Promise<void> {
68
const gateway = this._gateways.get(gatewayId);
69
if (!gateway) {
70
this._logService.warn(`[McpGatewayService] Attempted to dispose unknown gateway: ${gatewayId}`);
71
return;
72
}
73
74
this._gateways.delete(gatewayId);
75
this._gatewayToClient.delete(gatewayId);
76
this._logService.info(`[McpGatewayService] Disposed gateway: ${gatewayId}`);
77
78
// If no more gateways, shut down the server
79
if (this._gateways.size === 0) {
80
this._stopServer();
81
}
82
}
83
84
disposeGatewaysForClient(clientId: unknown): void {
85
const gatewaysToDispose: string[] = [];
86
87
for (const [gatewayId, ownerClientId] of this._gatewayToClient) {
88
if (ownerClientId === clientId) {
89
gatewaysToDispose.push(gatewayId);
90
}
91
}
92
93
if (gatewaysToDispose.length > 0) {
94
this._logService.info(`[McpGatewayService] Disposing ${gatewaysToDispose.length} gateway(s) for disconnected client ${clientId}`);
95
96
for (const gatewayId of gatewaysToDispose) {
97
this._gateways.delete(gatewayId);
98
this._gatewayToClient.delete(gatewayId);
99
}
100
101
// If no more gateways, shut down the server
102
if (this._gateways.size === 0) {
103
this._stopServer();
104
}
105
}
106
}
107
108
private async _ensureServer(): Promise<void> {
109
if (this._server?.listening) {
110
return;
111
}
112
113
// If server is already starting, wait for it
114
if (this._serverStartPromise) {
115
return this._serverStartPromise;
116
}
117
118
this._serverStartPromise = this._startServer();
119
try {
120
await this._serverStartPromise;
121
} finally {
122
this._serverStartPromise = undefined;
123
}
124
}
125
126
private async _startServer(): Promise<void> {
127
const deferredPromise = new DeferredPromise<void>();
128
129
this._server = http.createServer((req, res) => {
130
this._handleRequest(req, res);
131
});
132
133
const portTimeout = setTimeout(() => {
134
deferredPromise.error(new Error('[McpGatewayService] Timeout waiting for server to start'));
135
}, 5000);
136
137
this._server.on('listening', () => {
138
const address = this._server!.address();
139
if (typeof address === 'string') {
140
this._port = parseInt(address);
141
} else if (address instanceof Object) {
142
this._port = address.port;
143
} else {
144
clearTimeout(portTimeout);
145
deferredPromise.error(new Error('[McpGatewayService] Unable to determine port'));
146
return;
147
}
148
149
clearTimeout(portTimeout);
150
this._logService.info(`[McpGatewayService] Server started on port ${this._port}`);
151
deferredPromise.complete();
152
});
153
154
this._server.on('error', (err: NodeJS.ErrnoException) => {
155
if (err.code === 'EADDRINUSE') {
156
this._logService.warn('[McpGatewayService] Port in use, retrying with random port...');
157
// Try with a random port
158
this._server!.listen(0, '127.0.0.1');
159
return;
160
}
161
clearTimeout(portTimeout);
162
this._logService.error(`[McpGatewayService] Server error: ${err}`);
163
deferredPromise.error(err);
164
});
165
166
// Use dynamic port assignment (port 0)
167
this._server.listen(0, '127.0.0.1');
168
169
return deferredPromise.p;
170
}
171
172
private _stopServer(): void {
173
if (!this._server) {
174
return;
175
}
176
177
this._logService.info('[McpGatewayService] Stopping server (no more gateways)');
178
179
this._server.close(err => {
180
if (err) {
181
this._logService.error(`[McpGatewayService] Error closing server: ${err}`);
182
} else {
183
this._logService.info('[McpGatewayService] Server stopped');
184
}
185
});
186
187
this._server = undefined;
188
this._port = undefined;
189
}
190
191
private _handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
192
const url = new URL(req.url!, `http://${req.headers.host}`);
193
const pathParts = url.pathname.split('/').filter(Boolean);
194
195
// Expected path: /gateway/{gatewayId}
196
if (pathParts.length >= 2 && pathParts[0] === 'gateway') {
197
const gatewayId = pathParts[1];
198
const gateway = this._gateways.get(gatewayId);
199
200
if (gateway) {
201
gateway.handleRequest(req, res);
202
return;
203
}
204
}
205
206
// Not found
207
res.writeHead(404, { 'Content-Type': 'application/json' });
208
res.end(JSON.stringify({ error: 'Gateway not found' }));
209
}
210
211
override dispose(): void {
212
this._stopServer();
213
this._gateways.clear();
214
super.dispose();
215
}
216
}
217
218
/**
219
* Represents a single MCP gateway route.
220
* This is a stub implementation that will be expanded later.
221
*/
222
class McpGatewayRoute {
223
constructor(
224
public readonly gatewayId: string,
225
) { }
226
227
handleRequest(_req: http.IncomingMessage, res: http.ServerResponse): void {
228
// Stub implementation - return 501 Not Implemented
229
res.writeHead(501, { 'Content-Type': 'application/json' });
230
res.end(JSON.stringify({
231
jsonrpc: '2.0',
232
error: {
233
code: -32601,
234
message: 'MCP Gateway not yet implemented',
235
},
236
}));
237
}
238
}
239
240