mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Merge pull request #293209 from microsoft/connor4312/mcp-gateway-1
mcp: initial data flow for MCP gateway
This commit is contained in:
@@ -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),);
|
||||
|
||||
65
src/vs/platform/mcp/common/mcpGateway.ts
Normal file
65
src/vs/platform/mcp/common/mcpGateway.ts
Normal 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>;
|
||||
}
|
||||
45
src/vs/platform/mcp/electron-main/mcpGatewayMainChannel.ts
Normal file
45
src/vs/platform/mcp/electron-main/mcpGatewayMainChannel.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
45
src/vs/platform/mcp/node/mcpGatewayChannel.ts
Normal file
45
src/vs/platform/mcp/node/mcpGatewayChannel.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
238
src/vs/platform/mcp/node/mcpGatewayService.ts
Normal file
238
src/vs/platform/mcp/node/mcpGatewayService.ts
Normal 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',
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
53
src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts
Normal file
53
src/vs/workbench/contrib/mcp/browser/mcpGatewayService.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
46
src/vs/workbench/contrib/mcp/common/mcpGatewayService.ts
Normal file
46
src/vs/workbench/contrib/mcp/common/mcpGatewayService.ts
Normal 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>;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user