diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index a3efce99744..afd3b1fcf45 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -121,6 +121,9 @@ import { normalizeNFC } from '../../base/common/normalization.js'; import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js'; import { INativeMcpDiscoveryHelperService, NativeMcpDiscoveryHelperChannelName } from '../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; import { NativeMcpDiscoveryHelperService } from '../../platform/mcp/node/nativeMcpDiscoveryHelperService.js'; +import { IMcpGatewayService, McpGatewayChannelName } from '../../platform/mcp/common/mcpGateway.js'; +import { McpGatewayService } from '../../platform/mcp/node/mcpGatewayService.js'; +import { McpGatewayChannel } from '../../platform/mcp/node/mcpGatewayChannel.js'; import { IWebContentExtractorService } from '../../platform/webContentExtractor/common/webContentExtractor.js'; import { NativeWebContentExtractorService } from '../../platform/webContentExtractor/electron-main/webContentExtractorService.js'; import ErrorTelemetry from '../../platform/telemetry/electron-main/errorTelemetry.js'; @@ -1114,6 +1117,7 @@ export class CodeApplication extends Disposable { // MCP services.set(INativeMcpDiscoveryHelperService, new SyncDescriptor(NativeMcpDiscoveryHelperService)); + services.set(IMcpGatewayService, new SyncDescriptor(McpGatewayService)); // Dev Only: CSS service (for ESM) @@ -1235,6 +1239,8 @@ 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))); + mainProcessElectronServer.registerChannel(McpGatewayChannelName, mcpGatewayChannel); // Logger const loggerChannel = new LoggerChannel(accessor.get(ILoggerMainService),); diff --git a/src/vs/platform/mcp/common/mcpGateway.ts b/src/vs/platform/mcp/common/mcpGateway.ts new file mode 100644 index 00000000000..816824dcd43 --- /dev/null +++ b/src/vs/platform/mcp/common/mcpGateway.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../base/common/uri.js'; +import { createDecorator } from '../../instantiation/common/instantiation.js'; + +export const IMcpGatewayService = createDecorator('IMcpGatewayService'); + +export const McpGatewayChannelName = 'mcpGateway'; + +/** + * Result of creating an MCP gateway. + */ +export interface IMcpGatewayInfo { + /** + * The address of the HTTP endpoint for this gateway. + */ + readonly address: URI; + + /** + * The unique identifier for this gateway, used for disposal. + */ + readonly gatewayId: string; +} + +/** + * Service that manages MCP gateway HTTP endpoints in the main process (or remote server). + * + * The gateway provides an HTTP server that external processes can connect + * to in order to interact with MCP servers known to the editor. The server + * is shared among all gateways and is automatically torn down when the + * last gateway is disposed. + */ +export interface IMcpGatewayService { + readonly _serviceBrand: undefined; + + /** + * Disposes all gateways associated with a given client context (e.g., client ID or connection). + * @param context The client context whose gateways should be disposed. + * @return A disposable that can be used to unregister the client context from future cleanup (e.g., if the context is reused). + */ + disposeGatewaysForClient(context: TContext): void; + + /** + * Creates a new MCP gateway endpoint. + * + * The gateway is assigned a secure random route ID to make the endpoint + * URL unguessable without authentication. + * + * @param context Optional context (e.g., client ID) to associate with the gateway for cleanup purposes. + * @returns A promise that resolves to the gateway info if successful. + */ + createGateway(context: TContext): Promise; + + /** + * Disposes a previously created gateway. + * + * When the last gateway is disposed, the underlying HTTP server is shut down. + * + * @param gatewayId The unique identifier of the gateway to dispose. + */ + disposeGateway(gatewayId: string): Promise; +} diff --git a/src/vs/platform/mcp/electron-main/mcpGatewayMainChannel.ts b/src/vs/platform/mcp/electron-main/mcpGatewayMainChannel.ts new file mode 100644 index 00000000000..8c6c30ed144 --- /dev/null +++ b/src/vs/platform/mcp/electron-main/mcpGatewayMainChannel.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 { IMcpGatewayService } from '../common/mcpGateway.js'; + +/** + * IPC channel for the MCP Gateway service in the electron-main process. + * + * This channel tracks which client (identified by ctx) creates gateways, + * enabling cleanup when a client disconnects (e.g., window crash). + */ +export class McpGatewayMainChannel extends Disposable implements IServerChannel { + + constructor( + ipcServer: IPCServer, + @IMcpGatewayService private readonly mcpGatewayService: IMcpGatewayService + ) { + super(); + this._register(ipcServer.onDidRemoveConnection(c => mcpGatewayService.disposeGatewaysForClient(c.ctx))); + } + + listen(_ctx: string, _event: string): Event { + throw new Error('Invalid listen'); + } + + async call(ctx: string, command: string, args?: unknown): Promise { + switch (command) { + case 'createGateway': { + // Use the context (client ID) to track gateway ownership + const result = await this.mcpGatewayService.createGateway(ctx); + return result as T; + } + case 'disposeGateway': { + await this.mcpGatewayService.disposeGateway(args as string); + return undefined as T; + } + } + throw new Error(`Invalid call: ${command}`); + } +} diff --git a/src/vs/platform/mcp/node/mcpGatewayChannel.ts b/src/vs/platform/mcp/node/mcpGatewayChannel.ts new file mode 100644 index 00000000000..5c0fefefe33 --- /dev/null +++ b/src/vs/platform/mcp/node/mcpGatewayChannel.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 { IMcpGatewayService } from '../common/mcpGateway.js'; + +/** + * IPC channel for the MCP Gateway service, used by the remote server. + * + * This channel tracks which client (identified by reconnectionToken) creates gateways, + * enabling cleanup when a client disconnects. + */ +export class McpGatewayChannel extends Disposable implements IServerChannel { + + constructor( + ipcServer: IPCServer, + @IMcpGatewayService private readonly mcpGatewayService: IMcpGatewayService + ) { + super(); + this._register(ipcServer.onDidRemoveConnection(c => mcpGatewayService.disposeGatewaysForClient(c.ctx))); + } + + listen(_ctx: TContext, _event: string): Event { + throw new Error('Invalid listen'); + } + + async call(ctx: TContext, command: string, args?: unknown): Promise { + switch (command) { + case 'createGateway': { + const result = await this.mcpGatewayService.createGateway(ctx); + return result as T; + } + case 'disposeGateway': { + await this.mcpGatewayService.disposeGateway(args as string); + return undefined as T; + } + } + + throw new Error(`Invalid call: ${command}`); + } +} diff --git a/src/vs/platform/mcp/node/mcpGatewayService.ts b/src/vs/platform/mcp/node/mcpGatewayService.ts new file mode 100644 index 00000000000..e4ee1fe42bb --- /dev/null +++ b/src/vs/platform/mcp/node/mcpGatewayService.ts @@ -0,0 +1,238 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as http from 'http'; +import { DeferredPromise } from '../../../base/common/async.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { URI } from '../../../base/common/uri.js'; +import { generateUuid } from '../../../base/common/uuid.js'; +import { ILogService } from '../../log/common/log.js'; +import { IMcpGatewayInfo, IMcpGatewayService } from '../common/mcpGateway.js'; + +/** + * Node.js implementation of the MCP Gateway Service. + * + * Creates and manages an HTTP server on localhost that provides MCP gateway endpoints. + * The server is shared among all gateways and uses ref-counting for lifecycle management. + */ +export class McpGatewayService extends Disposable implements IMcpGatewayService { + declare readonly _serviceBrand: undefined; + + private _server: http.Server | undefined; + private _port: number | undefined; + private readonly _gateways = new Map(); + /** Maps gatewayId to clientId for tracking ownership */ + private readonly _gatewayToClient = new Map(); + private _serverStartPromise: Promise | undefined; + + constructor( + @ILogService private readonly _logService: ILogService, + ) { + super(); + } + + async createGateway(clientId: unknown): Promise { + // Ensure server is running + await this._ensureServer(); + + if (this._port === undefined) { + throw new Error('[McpGatewayService] Server failed to start, port is undefined'); + } + + // Generate a secure random ID for the gateway route + const gatewayId = generateUuid(); + + // Create the gateway route + const gateway = new McpGatewayRoute(gatewayId); + this._gateways.set(gatewayId, gateway); + + // Track client ownership if clientId provided (for cleanup on disconnect) + if (clientId) { + this._gatewayToClient.set(gatewayId, clientId); + this._logService.info(`[McpGatewayService] Created gateway at http://127.0.0.1:${this._port}/gateway/${gatewayId} for client ${clientId}`); + } else { + this._logService.warn(`[McpGatewayService] Created gateway without client tracking at http://127.0.0.1:${this._port}/gateway/${gatewayId}`); + } + + const address = URI.parse(`http://127.0.0.1:${this._port}/gateway/${gatewayId}`); + + return { + address, + gatewayId, + }; + } + + async disposeGateway(gatewayId: string): Promise { + const gateway = this._gateways.get(gatewayId); + if (!gateway) { + this._logService.warn(`[McpGatewayService] Attempted to dispose unknown gateway: ${gatewayId}`); + return; + } + + this._gateways.delete(gatewayId); + this._gatewayToClient.delete(gatewayId); + this._logService.info(`[McpGatewayService] Disposed gateway: ${gatewayId}`); + + // If no more gateways, shut down the server + if (this._gateways.size === 0) { + this._stopServer(); + } + } + + disposeGatewaysForClient(clientId: unknown): void { + const gatewaysToDispose: string[] = []; + + for (const [gatewayId, ownerClientId] of this._gatewayToClient) { + if (ownerClientId === clientId) { + gatewaysToDispose.push(gatewayId); + } + } + + if (gatewaysToDispose.length > 0) { + this._logService.info(`[McpGatewayService] Disposing ${gatewaysToDispose.length} gateway(s) for disconnected client ${clientId}`); + + for (const gatewayId of gatewaysToDispose) { + this._gateways.delete(gatewayId); + this._gatewayToClient.delete(gatewayId); + } + + // If no more gateways, shut down the server + if (this._gateways.size === 0) { + this._stopServer(); + } + } + } + + private async _ensureServer(): Promise { + if (this._server?.listening) { + return; + } + + // If server is already starting, wait for it + if (this._serverStartPromise) { + return this._serverStartPromise; + } + + this._serverStartPromise = this._startServer(); + try { + await this._serverStartPromise; + } finally { + this._serverStartPromise = undefined; + } + } + + private async _startServer(): Promise { + const deferredPromise = new DeferredPromise(); + + this._server = http.createServer((req, res) => { + this._handleRequest(req, res); + }); + + const portTimeout = setTimeout(() => { + deferredPromise.error(new Error('[McpGatewayService] Timeout waiting for server to start')); + }, 5000); + + this._server.on('listening', () => { + const address = this._server!.address(); + if (typeof address === 'string') { + this._port = parseInt(address); + } else if (address instanceof Object) { + this._port = address.port; + } else { + clearTimeout(portTimeout); + deferredPromise.error(new Error('[McpGatewayService] Unable to determine port')); + return; + } + + clearTimeout(portTimeout); + this._logService.info(`[McpGatewayService] Server started on port ${this._port}`); + deferredPromise.complete(); + }); + + this._server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + this._logService.warn('[McpGatewayService] Port in use, retrying with random port...'); + // Try with a random port + this._server!.listen(0, '127.0.0.1'); + return; + } + clearTimeout(portTimeout); + this._logService.error(`[McpGatewayService] Server error: ${err}`); + deferredPromise.error(err); + }); + + // Use dynamic port assignment (port 0) + this._server.listen(0, '127.0.0.1'); + + return deferredPromise.p; + } + + private _stopServer(): void { + if (!this._server) { + return; + } + + this._logService.info('[McpGatewayService] Stopping server (no more gateways)'); + + this._server.close(err => { + if (err) { + this._logService.error(`[McpGatewayService] Error closing server: ${err}`); + } else { + this._logService.info('[McpGatewayService] Server stopped'); + } + }); + + this._server = undefined; + this._port = undefined; + } + + private _handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void { + const url = new URL(req.url!, `http://${req.headers.host}`); + const pathParts = url.pathname.split('/').filter(Boolean); + + // Expected path: /gateway/{gatewayId} + if (pathParts.length >= 2 && pathParts[0] === 'gateway') { + const gatewayId = pathParts[1]; + const gateway = this._gateways.get(gatewayId); + + if (gateway) { + gateway.handleRequest(req, res); + return; + } + } + + // Not found + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Gateway not found' })); + } + + override dispose(): void { + this._stopServer(); + this._gateways.clear(); + super.dispose(); + } +} + +/** + * Represents a single MCP gateway route. + * This is a stub implementation that will be expanded later. + */ +class McpGatewayRoute { + constructor( + public readonly gatewayId: string, + ) { } + + handleRequest(_req: http.IncomingMessage, res: http.ServerResponse): void { + // Stub implementation - return 501 Not Implemented + res.writeHead(501, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32601, + message: 'MCP Gateway not yet implemented', + }, + })); + } +} diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index 34b41d8d00b..053aa4395a6 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -83,6 +83,9 @@ import { TelemetryLogAppender } from '../../platform/telemetry/common/telemetryL import { INativeMcpDiscoveryHelperService, NativeMcpDiscoveryHelperChannelName } from '../../platform/mcp/common/nativeMcpDiscoveryHelper.js'; import { NativeMcpDiscoveryHelperChannel } from '../../platform/mcp/node/nativeMcpDiscoveryHelperChannel.js'; import { NativeMcpDiscoveryHelperService } from '../../platform/mcp/node/nativeMcpDiscoveryHelperService.js'; +import { IMcpGatewayService, McpGatewayChannelName } from '../../platform/mcp/common/mcpGateway.js'; +import { McpGatewayService } from '../../platform/mcp/node/mcpGatewayService.js'; +import { McpGatewayChannel } from '../../platform/mcp/node/mcpGatewayChannel.js'; import { IExtensionGalleryManifestService } from '../../platform/extensionManagement/common/extensionGalleryManifest.js'; import { ExtensionGalleryManifestIPCService } from '../../platform/extensionManagement/common/extensionGalleryManifestServiceIpc.js'; import { IAllowedMcpServersService, IMcpGalleryService, IMcpManagementService } from '../../platform/mcp/common/mcpManagement.js'; @@ -209,6 +212,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IAllowedExtensionsService, new SyncDescriptor(AllowedExtensionsService)); services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(INativeMcpDiscoveryHelperService, new SyncDescriptor(NativeMcpDiscoveryHelperService)); + services.set(IMcpGatewayService, new SyncDescriptor(McpGatewayService)); const instantiationService: IInstantiationService = new InstantiationService(services); services.set(ILanguagePackService, instantiationService.createInstance(NativeLanguagePackService)); @@ -247,6 +251,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken socketServer.registerChannel(RemoteExtensionsScannerChannelName, new RemoteExtensionsScannerChannel(remoteExtensionsScanner, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); socketServer.registerChannel(NativeMcpDiscoveryHelperChannelName, instantiationService.createInstance(NativeMcpDiscoveryHelperChannel, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority))); + socketServer.registerChannel(McpGatewayChannelName, instantiationService.createInstance(McpGatewayChannel, socketServer)); const remoteFileSystemChannel = disposables.add(new RemoteAgentFileSystemProviderChannel(logService, environmentService, configurationService)); socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel); diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index f09b6b867fe..f71ccdba8fe 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -11,12 +11,14 @@ import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '. import { autorun, ISettableObservable, observableValue } from '../../../base/common/observable.js'; import Severity from '../../../base/common/severity.js'; import { URI } from '../../../base/common/uri.js'; +import { generateUuid } from '../../../base/common/uuid.js'; import * as nls from '../../../nls.js'; import { ContextKeyExpr, IContextKeyService } from '../../../platform/contextkey/common/contextkey.js'; import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js'; import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; import { LogLevel } from '../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js'; +import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../../contrib/mcp/common/mcpGatewayService.js'; import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js'; import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType, McpServerTrust, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; @@ -44,6 +46,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { servers: ISettableObservable; dispose(): void; }>()); + private readonly _gateways = this._register(new DisposableMap()); constructor( private readonly _extHostContext: IExtHostContext, @@ -57,6 +60,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @IExtensionService private readonly _extensionService: IExtensionService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IWorkbenchMcpGatewayService private readonly _mcpGatewayService: IWorkbenchMcpGatewayService, ) { super(); this._register(_authenticationService.onDidChangeSessions(e => this._onDidChangeAuthSessions(e.providerId, e.label))); @@ -397,6 +401,30 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { this._telemetryService.publicLog2('mcp/authSetup', data); } + async $startMcpGateway(): Promise<{ address: URI; gatewayId: string } | undefined> { + const result = await this._mcpGatewayService.createGateway(this._extHostContext.extensionHostKind === ExtensionHostKind.Remote); + if (!result) { + return undefined; + } + + if (this._store.isDisposed) { + result.dispose(); + return undefined; + } + + const gatewayId = generateUuid(); + this._gateways.set(gatewayId, result); + + return { + address: result.address, + gatewayId, + }; + } + + $disposeMcpGateway(gatewayId: string): void { + this._gateways.deleteAndDispose(gatewayId); + } + private async loginPrompt(mcpLabel: string, providerLabel: string, recreatingSession: boolean): Promise { const message = recreatingSession ? nls.localize('confirmRelogin', "The MCP Server Definition '{0}' wants you to authenticate to {1}.", mcpLabel, providerLabel) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fccab633ec5..0d04a8f0382 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1736,6 +1736,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'mcpServerDefinitions'); return extHostMcp.mcpServerDefinitions; }, + startMcpGateway() { + checkProposedApiEnabled(extension, 'mcpServerDefinitions'); + return extHostMcp.startMcpGateway(); + }, onDidChangeChatRequestTools(...args) { checkProposedApiEnabled(extension, 'chatParticipantAdditions'); return _asExtensionEvent(extHostChatAgents2.onDidChangeChatRequestTools)(...args); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index e233de77dd8..268fb1c1571 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3280,6 +3280,8 @@ export interface MainThreadMcpShape { $getTokenFromServerMetadata(id: number, authDetails: IMcpAuthenticationDetails, options?: IMcpAuthenticationOptions): Promise; $getTokenForProviderId(id: number, providerId: string, scopes: string[], options?: IMcpAuthenticationOptions): Promise; $logMcpAuthSetup(data: IAuthMetadataSource): void; + $startMcpGateway(): Promise<{ address: UriComponents; gatewayId: string } | undefined>; + $disposeMcpGateway(gatewayId: string): void; } export interface MainThreadDataChannelsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index e148c5d0628..27e6c334d6b 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -40,6 +40,9 @@ export interface IExtHostMpcService extends ExtHostMcpShape { /** Returns all MCP server definitions known to the editor. */ readonly mcpServerDefinitions: readonly vscode.McpServerDefinition[]; + + /** Starts an MCP gateway that exposes MCP servers via an HTTP endpoint. */ + startMcpGateway(): Promise; } const serverDataValidation = vObj({ @@ -253,6 +256,24 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService return store; } + + /** {@link vscode.lm.startMcpGateway} */ + public async startMcpGateway(): Promise { + const result = await this._proxy.$startMcpGateway(); + if (!result) { + return undefined; + } + + const address = URI.revive(result.address); + const gatewayId = result.gatewayId; + + return { + address, + dispose: () => { + this._proxy.$disposeMcpGateway(gatewayId); + } + }; + } } const enum HttpMode { diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index d0d8260bb51..0dfb08c73e8 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -33,6 +33,8 @@ import { McpResourceFilesystem } from '../common/mcpResourceFilesystem.js'; import { McpSamplingService } from '../common/mcpSamplingService.js'; import { McpService } from '../common/mcpService.js'; import { IMcpElicitationService, IMcpSamplingService, IMcpService, IMcpWorkbenchService } from '../common/mcpTypes.js'; +import { IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; +import { BrowserMcpGatewayService } from './mcpGatewayService.js'; import { McpAddContextContribution } from './mcpAddContextContribution.js'; import { AddConfigurationAction, EditStoredInput, InstallFromManifestAction, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, McpConfirmationServerOptionsCommand, MCPServerActionRendering, McpServerOptionsCommand, McpSkipCurrentAutostartCommand, McpStartPromptingServerCommand, OpenRemoteUserMcpResourceCommand, OpenUserMcpResourceCommand, OpenWorkspaceFolderMcpResourceCommand, OpenWorkspaceMcpResourceCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowInstalledMcpServersCommand, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; import { McpDiscovery } from './mcpDiscovery.js'; @@ -51,6 +53,7 @@ registerSingleton(IMcpWorkbenchService, McpWorkbenchService, InstantiationType.E registerSingleton(IMcpDevModeDebugging, McpDevModeDebugging, InstantiationType.Delayed); registerSingleton(IMcpSamplingService, McpSamplingService, InstantiationType.Delayed); registerSingleton(IMcpElicitationService, McpElicitationService, InstantiationType.Delayed); +registerSingleton(IWorkbenchMcpGatewayService, BrowserMcpGatewayService, InstantiationType.Delayed); mcpDiscoveryRegistry.register(new SyncDescriptor(RemoteNativeMpcDiscovery)); mcpDiscoveryRegistry.register(new SyncDescriptor(InstalledMcpServersDiscovery)); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts new file mode 100644 index 00000000000..49bb014e5d9 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; +import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; +import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; +import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; + +/** + * Browser implementation of the MCP Gateway Service. + * + * In browser/serverless web environments without a remote connection, + * there is no Node.js process available to create an HTTP server. + * + * When running with a remote connection, the gateway is created on the + * remote server via IPC. + */ +export class BrowserMcpGatewayService implements IWorkbenchMcpGatewayService { + declare readonly _serviceBrand: undefined; + + constructor( + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + ) { } + + async createGateway(inRemote: boolean): Promise { + // Browser can only create gateways in remote environment + if (!inRemote) { + return undefined; + } + + const connection = this._remoteAgentService.getConnection(); + if (!connection) { + // Serverless web environment - no gateway available + return undefined; + } + + // Use the remote server's gateway service + return connection.withChannel(McpGatewayChannelName, async channel => { + const service = ProxyChannel.toService(channel); + const info = await service.createGateway(undefined); + + return { + address: URI.revive(info.address), + dispose: () => { + service.disposeGateway(info.gatewayId); + } + }; + }); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/common/mcpGatewayService.ts new file mode 100644 index 00000000000..7376729aa00 --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpGatewayService.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export const IWorkbenchMcpGatewayService = createDecorator('IWorkbenchMcpGatewayService'); + +/** + * Result of creating an MCP gateway, which is itself disposable. + */ +export interface IMcpGatewayResult extends IDisposable { + /** + * The address of the HTTP endpoint for this gateway. + */ + readonly address: URI; +} + +/** + * Service that manages MCP gateway HTTP endpoints in the workbench. + * + * The gateway provides an HTTP server that external processes can connect + * to in order to interact with MCP servers known to the editor. The server + * is shared among all gateways and is automatically torn down when the + * last gateway is disposed. + */ +export interface IWorkbenchMcpGatewayService { + readonly _serviceBrand: undefined; + + /** + * Creates a new MCP gateway endpoint. + * + * The gateway is assigned a secure random route ID to make the endpoint + * URL unguessable without authentication. + * + * @param inRemote Whether to create the gateway in the remote environment. + * If true, the gateway is created on the remote server (requires a remote connection). + * If false, the gateway is created locally (requires a local Node process, e.g., desktop). + * @returns A promise that resolves to the gateway result if successful, + * or `undefined` if the requested environment is not available. + */ + createGateway(inRemote: boolean): Promise; +} diff --git a/src/vs/workbench/contrib/mcp/electron-browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/electron-browser/mcp.contribution.ts index 37553b1eac3..701c78f4c08 100644 --- a/src/vs/workbench/contrib/mcp/electron-browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/electron-browser/mcp.contribution.ts @@ -6,9 +6,12 @@ import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; +import { IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; import { IMcpDevModeDebugging } from '../common/mcpDevMode.js'; import { McpDevModeDebuggingNode } from './mcpDevModeDebuggingNode.js'; import { NativeMcpDiscovery } from './nativeMpcDiscovery.js'; +import { WorkbenchMcpGatewayService } from './mcpGatewayService.js'; mcpDiscoveryRegistry.register(new SyncDescriptor(NativeMcpDiscovery)); registerSingleton(IMcpDevModeDebugging, McpDevModeDebuggingNode, InstantiationType.Delayed); +registerSingleton(IWorkbenchMcpGatewayService, WorkbenchMcpGatewayService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts new file mode 100644 index 00000000000..2af2c0b4adf --- /dev/null +++ b/src/vs/workbench/contrib/mcp/electron-browser/mcpGatewayService.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../base/common/uri.js'; +import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'; +import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; +import { IMcpGatewayService, McpGatewayChannelName } from '../../../../platform/mcp/common/mcpGateway.js'; +import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; +import { IMcpGatewayResult, IWorkbenchMcpGatewayService } from '../common/mcpGatewayService.js'; + +/** + * Electron workbench implementation of the MCP Gateway Service. + * + * This implementation can create gateways either in the main process (local) + * or on a remote server (if connected). + */ +export class WorkbenchMcpGatewayService implements IWorkbenchMcpGatewayService { + declare readonly _serviceBrand: undefined; + + private readonly _localPlatformService: IMcpGatewayService; + + constructor( + @IMainProcessService mainProcessService: IMainProcessService, + @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, + ) { + this._localPlatformService = ProxyChannel.toService( + mainProcessService.getChannel(McpGatewayChannelName) + ); + } + + async createGateway(inRemote: boolean): Promise { + if (inRemote) { + return this._createRemoteGateway(); + } else { + return this._createLocalGateway(); + } + } + + private async _createLocalGateway(): Promise { + const info = await this._localPlatformService.createGateway(undefined); + + return { + address: URI.revive(info.address), + dispose: () => { + this._localPlatformService.disposeGateway(info.gatewayId); + } + }; + } + + private async _createRemoteGateway(): Promise { + const connection = this._remoteAgentService.getConnection(); + if (!connection) { + // No remote connection - cannot create remote gateway + return undefined; + } + + return connection.withChannel(McpGatewayChannelName, async channel => { + const service = ProxyChannel.toService(channel); + const info = await service.createGateway(undefined); + + return { + address: URI.revive(info.address), + dispose: () => { + service.disposeGateway(info.gatewayId); + } + }; + }); + } +} diff --git a/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts b/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts index c0d4dc2b702..5fa6524edda 100644 --- a/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts +++ b/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts @@ -7,6 +7,19 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/288777 @DonJayamanne + /** + * Represents an MCP gateway that exposes MCP servers via HTTP. + * The gateway provides an HTTP endpoint that external processes can connect + * to in order to interact with MCP servers known to the editor. + */ + export interface McpGateway extends Disposable { + /** + * The address of the HTTP MCP server endpoint. + * External processes can connect to this URI to interact with MCP servers. + */ + readonly address: Uri; + } + /** * Namespace for language model related functionality. */ @@ -27,5 +40,20 @@ declare module 'vscode' { * definitions from any source. */ export const onDidChangeMcpServerDefinitions: Event; + + /** + * Starts an MCP gateway that exposes MCP servers via an HTTP endpoint. + * + * The gateway creates a localhost HTTP server that external processes (such as + * CLI-based agent loops) can connect to in order to interact with MCP servers + * that the editor knows about. + * + * The HTTP server is shared among all gateways and is automatically torn down + * when the last gateway is disposed. + * + * @returns A promise that resolves to an {@link McpGateway} if successful, + * or `undefined` if no Node process is available (e.g., in serverless web environments). + */ + export function startMcpGateway(): Thenable; } }