Path: blob/main/src/vs/platform/mcp/node/mcpGatewayService.ts
5241 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as http from 'http';6import { DeferredPromise } from '../../../base/common/async.js';7import { Disposable } from '../../../base/common/lifecycle.js';8import { URI } from '../../../base/common/uri.js';9import { generateUuid } from '../../../base/common/uuid.js';10import { ILogService } from '../../log/common/log.js';11import { IMcpGatewayInfo, IMcpGatewayService } from '../common/mcpGateway.js';1213/**14* Node.js implementation of the MCP Gateway Service.15*16* Creates and manages an HTTP server on localhost that provides MCP gateway endpoints.17* The server is shared among all gateways and uses ref-counting for lifecycle management.18*/19export class McpGatewayService extends Disposable implements IMcpGatewayService {20declare readonly _serviceBrand: undefined;2122private _server: http.Server | undefined;23private _port: number | undefined;24private readonly _gateways = new Map<string, McpGatewayRoute>();25/** Maps gatewayId to clientId for tracking ownership */26private readonly _gatewayToClient = new Map<string, unknown>();27private _serverStartPromise: Promise<void> | undefined;2829constructor(30@ILogService private readonly _logService: ILogService,31) {32super();33}3435async createGateway(clientId: unknown): Promise<IMcpGatewayInfo> {36// Ensure server is running37await this._ensureServer();3839if (this._port === undefined) {40throw new Error('[McpGatewayService] Server failed to start, port is undefined');41}4243// Generate a secure random ID for the gateway route44const gatewayId = generateUuid();4546// Create the gateway route47const gateway = new McpGatewayRoute(gatewayId);48this._gateways.set(gatewayId, gateway);4950// Track client ownership if clientId provided (for cleanup on disconnect)51if (clientId) {52this._gatewayToClient.set(gatewayId, clientId);53this._logService.info(`[McpGatewayService] Created gateway at http://127.0.0.1:${this._port}/gateway/${gatewayId} for client ${clientId}`);54} else {55this._logService.warn(`[McpGatewayService] Created gateway without client tracking at http://127.0.0.1:${this._port}/gateway/${gatewayId}`);56}5758const address = URI.parse(`http://127.0.0.1:${this._port}/gateway/${gatewayId}`);5960return {61address,62gatewayId,63};64}6566async disposeGateway(gatewayId: string): Promise<void> {67const gateway = this._gateways.get(gatewayId);68if (!gateway) {69this._logService.warn(`[McpGatewayService] Attempted to dispose unknown gateway: ${gatewayId}`);70return;71}7273this._gateways.delete(gatewayId);74this._gatewayToClient.delete(gatewayId);75this._logService.info(`[McpGatewayService] Disposed gateway: ${gatewayId}`);7677// If no more gateways, shut down the server78if (this._gateways.size === 0) {79this._stopServer();80}81}8283disposeGatewaysForClient(clientId: unknown): void {84const gatewaysToDispose: string[] = [];8586for (const [gatewayId, ownerClientId] of this._gatewayToClient) {87if (ownerClientId === clientId) {88gatewaysToDispose.push(gatewayId);89}90}9192if (gatewaysToDispose.length > 0) {93this._logService.info(`[McpGatewayService] Disposing ${gatewaysToDispose.length} gateway(s) for disconnected client ${clientId}`);9495for (const gatewayId of gatewaysToDispose) {96this._gateways.delete(gatewayId);97this._gatewayToClient.delete(gatewayId);98}99100// If no more gateways, shut down the server101if (this._gateways.size === 0) {102this._stopServer();103}104}105}106107private async _ensureServer(): Promise<void> {108if (this._server?.listening) {109return;110}111112// If server is already starting, wait for it113if (this._serverStartPromise) {114return this._serverStartPromise;115}116117this._serverStartPromise = this._startServer();118try {119await this._serverStartPromise;120} finally {121this._serverStartPromise = undefined;122}123}124125private async _startServer(): Promise<void> {126const deferredPromise = new DeferredPromise<void>();127128this._server = http.createServer((req, res) => {129this._handleRequest(req, res);130});131132const portTimeout = setTimeout(() => {133deferredPromise.error(new Error('[McpGatewayService] Timeout waiting for server to start'));134}, 5000);135136this._server.on('listening', () => {137const address = this._server!.address();138if (typeof address === 'string') {139this._port = parseInt(address);140} else if (address instanceof Object) {141this._port = address.port;142} else {143clearTimeout(portTimeout);144deferredPromise.error(new Error('[McpGatewayService] Unable to determine port'));145return;146}147148clearTimeout(portTimeout);149this._logService.info(`[McpGatewayService] Server started on port ${this._port}`);150deferredPromise.complete();151});152153this._server.on('error', (err: NodeJS.ErrnoException) => {154if (err.code === 'EADDRINUSE') {155this._logService.warn('[McpGatewayService] Port in use, retrying with random port...');156// Try with a random port157this._server!.listen(0, '127.0.0.1');158return;159}160clearTimeout(portTimeout);161this._logService.error(`[McpGatewayService] Server error: ${err}`);162deferredPromise.error(err);163});164165// Use dynamic port assignment (port 0)166this._server.listen(0, '127.0.0.1');167168return deferredPromise.p;169}170171private _stopServer(): void {172if (!this._server) {173return;174}175176this._logService.info('[McpGatewayService] Stopping server (no more gateways)');177178this._server.close(err => {179if (err) {180this._logService.error(`[McpGatewayService] Error closing server: ${err}`);181} else {182this._logService.info('[McpGatewayService] Server stopped');183}184});185186this._server = undefined;187this._port = undefined;188}189190private _handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {191const url = new URL(req.url!, `http://${req.headers.host}`);192const pathParts = url.pathname.split('/').filter(Boolean);193194// Expected path: /gateway/{gatewayId}195if (pathParts.length >= 2 && pathParts[0] === 'gateway') {196const gatewayId = pathParts[1];197const gateway = this._gateways.get(gatewayId);198199if (gateway) {200gateway.handleRequest(req, res);201return;202}203}204205// Not found206res.writeHead(404, { 'Content-Type': 'application/json' });207res.end(JSON.stringify({ error: 'Gateway not found' }));208}209210override dispose(): void {211this._stopServer();212this._gateways.clear();213super.dispose();214}215}216217/**218* Represents a single MCP gateway route.219* This is a stub implementation that will be expanded later.220*/221class McpGatewayRoute {222constructor(223public readonly gatewayId: string,224) { }225226handleRequest(_req: http.IncomingMessage, res: http.ServerResponse): void {227// Stub implementation - return 501 Not Implemented228res.writeHead(501, { 'Content-Type': 'application/json' });229res.end(JSON.stringify({230jsonrpc: '2.0',231error: {232code: -32601,233message: 'MCP Gateway not yet implemented',234},235}));236}237}238239240