Merge pull request #293209 from microsoft/connor4312/mcp-gateway-1

mcp: initial data flow for MCP gateway
This commit is contained in:
Connor Peet
2026-02-05 12:46:09 -08:00
committed by GitHub
16 changed files with 663 additions and 0 deletions

View File

@@ -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),);

View File

@@ -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>('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<TContext>(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<TContext>(context: TContext): Promise<IMcpGatewayInfo>;
/**
* 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<void>;
}

View File

@@ -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<string> {
constructor(
ipcServer: IPCServer,
@IMcpGatewayService private readonly mcpGatewayService: IMcpGatewayService
) {
super();
this._register(ipcServer.onDidRemoveConnection(c => mcpGatewayService.disposeGatewaysForClient(c.ctx)));
}
listen<T>(_ctx: string, _event: string): Event<T> {
throw new Error('Invalid listen');
}
async call<T>(ctx: string, command: string, args?: unknown): Promise<T> {
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}`);
}
}

View File

@@ -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<TContext> extends Disposable implements IServerChannel<TContext> {
constructor(
ipcServer: IPCServer<TContext>,
@IMcpGatewayService private readonly mcpGatewayService: IMcpGatewayService
) {
super();
this._register(ipcServer.onDidRemoveConnection(c => mcpGatewayService.disposeGatewaysForClient(c.ctx)));
}
listen<T>(_ctx: TContext, _event: string): Event<T> {
throw new Error('Invalid listen');
}
async call<T>(ctx: TContext, command: string, args?: unknown): Promise<T> {
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}`);
}
}

View File

@@ -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<string, McpGatewayRoute>();
/** Maps gatewayId to clientId for tracking ownership */
private readonly _gatewayToClient = new Map<string, unknown>();
private _serverStartPromise: Promise<void> | undefined;
constructor(
@ILogService private readonly _logService: ILogService,
) {
super();
}
async createGateway(clientId: unknown): Promise<IMcpGatewayInfo> {
// 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<void> {
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<void> {
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<void> {
const deferredPromise = new DeferredPromise<void>();
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',
},
}));
}
}

View File

@@ -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<RemoteAgentConnectionContext>, socketServer));
const remoteFileSystemChannel = disposables.add(new RemoteAgentFileSystemProviderChannel(logService, environmentService, configurationService));
socketServer.registerChannel(REMOTE_FILE_SYSTEM_CHANNEL_NAME, remoteFileSystemChannel);

View File

@@ -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<readonly McpServerDefinition[]>;
dispose(): void;
}>());
private readonly _gateways = this._register(new DisposableMap<string, IMcpGatewayResult>());
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<IAuthMetadataSource, McpAuthSetupClassification>('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<boolean> {
const message = recreatingSession
? nls.localize('confirmRelogin', "The MCP Server Definition '{0}' wants you to authenticate to {1}.", mcpLabel, providerLabel)

View File

@@ -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);

View File

@@ -3280,6 +3280,8 @@ export interface MainThreadMcpShape {
$getTokenFromServerMetadata(id: number, authDetails: IMcpAuthenticationDetails, options?: IMcpAuthenticationOptions): Promise<string | undefined>;
$getTokenForProviderId(id: number, providerId: string, scopes: string[], options?: IMcpAuthenticationOptions): Promise<string | undefined>;
$logMcpAuthSetup(data: IAuthMetadataSource): void;
$startMcpGateway(): Promise<{ address: UriComponents; gatewayId: string } | undefined>;
$disposeMcpGateway(gatewayId: string): void;
}
export interface MainThreadDataChannelsShape extends IDisposable {

View File

@@ -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<vscode.McpGateway | undefined>;
}
const serverDataValidation = vObj({
@@ -253,6 +256,24 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService
return store;
}
/** {@link vscode.lm.startMcpGateway} */
public async startMcpGateway(): Promise<vscode.McpGateway | undefined> {
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 {

View File

@@ -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));

View File

@@ -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<IMcpGatewayResult | undefined> {
// 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<IMcpGatewayService>(channel);
const info = await service.createGateway(undefined);
return {
address: URI.revive(info.address),
dispose: () => {
service.disposeGateway(info.gatewayId);
}
};
});
}
}

View File

@@ -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>('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<IMcpGatewayResult | undefined>;
}

View File

@@ -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);

View File

@@ -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<IMcpGatewayService>(
mainProcessService.getChannel(McpGatewayChannelName)
);
}
async createGateway(inRemote: boolean): Promise<IMcpGatewayResult | undefined> {
if (inRemote) {
return this._createRemoteGateway();
} else {
return this._createLocalGateway();
}
}
private async _createLocalGateway(): Promise<IMcpGatewayResult> {
const info = await this._localPlatformService.createGateway(undefined);
return {
address: URI.revive(info.address),
dispose: () => {
this._localPlatformService.disposeGateway(info.gatewayId);
}
};
}
private async _createRemoteGateway(): Promise<IMcpGatewayResult | undefined> {
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<IMcpGatewayService>(channel);
const info = await service.createGateway(undefined);
return {
address: URI.revive(info.address),
dispose: () => {
service.disposeGateway(info.gatewayId);
}
};
});
}
}

View File

@@ -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<void>;
/**
* 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<McpGateway | undefined>;
}
}