diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index 030b9e4547f..c93065c1b53 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -9,11 +9,12 @@ import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { ISettableObservable, observableValue } from '../../../base/common/observable.js'; import { LogLevel } from '../../../platform/log/common/log.js'; import { IMcpMessageTransport, IMcpRegistry } from '../../contrib/mcp/common/mcpRegistryTypes.js'; -import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; +import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; import { ExtensionHostKind, extensionHostKindToString } from '../../services/extensions/common/extensionHostKind.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; -import { ExtHostContext, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; +import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; +import { ExtHostContext, ExtHostMcpShape, MainContext, MainThreadMcpShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadMcp) export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @@ -21,6 +22,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { private _serverIdCounter = 0; private readonly _servers = new Map(); + private readonly _proxy: Proxied; private readonly _collectionDefinitions = this._register(new DisposableMap; @@ -32,7 +34,7 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, ) { super(); - const proxy = _extHostContext.getProxy(ExtHostContext.ExtHostMcp); + const proxy = this._proxy = _extHostContext.getProxy(ExtHostContext.ExtHostMcp); this._register(this._mcpRegistry.registerDelegate({ // Prefer Node.js extension hosts when they're available. No CORS issues etc. priority: _extHostContext.extensionHostKind === ExtensionHostKind.LocalWebWorker ? 0 : 1, @@ -73,6 +75,10 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { const serverDefinitions = observableValue('mcpServers', servers); const handle = this._mcpRegistry.registerCollection({ ...collection, + resolveServerLanch: collection.canResolveLaunch ? (async def => { + const r = await this._proxy.$resolveMcpLaunch(collection.id, def.label); + return r ? McpServerLaunch.fromSerialized(r) : undefined; + }) : undefined, remoteAuthority: this._extHostContext.remoteAuthority, serverDefinitions, }); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 0db23694cd0..3165c3c4a79 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1830,7 +1830,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I AISearchKeyword: AISearchKeyword, TextSearchCompleteMessageTypeNew: TextSearchCompleteMessageType, ChatErrorLevel: extHostTypes.ChatErrorLevel, - McpSSEServerDefinition: extHostTypes.McpSSEServerDefinition, + McpHttpServerDefinition: extHostTypes.McpHttpServerDefinition, McpStdioServerDefinition: extHostTypes.McpStdioServerDefinition, }; }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 23b2026b543..28953da827b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2977,6 +2977,7 @@ export interface ExtHostTestingShape { } export interface ExtHostMcpShape { + $resolveMcpLaunch(collectionId: string, label: string): Promise; $startMcp(id: number, launch: McpServerLaunch.Serialized): void; $stopMcp(id: number): void; $sendMessage(id: number, message: string): void; @@ -2987,7 +2988,7 @@ export interface MainThreadMcpShape { $onDidChangeState(id: number, state: McpConnectionState): void; $onDidPublishLog(id: number, level: LogLevel, log: string): void; $onDidReceiveMessage(id: number, message: string): void; - $upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, servers: Dto[]): void; + $upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, servers: McpServerDefinition.Serialized[]): void; $deleteMcpCollection(collectionId: string): void; } diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 9d04a4b0362..e8ccfc48f4d 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -15,6 +15,7 @@ import { StorageScope } from '../../../platform/storage/common/storage.js'; import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportHTTP, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import { ExtHostMcpShape, MainContext, MainThreadMcpShape } from './extHost.protocol.js'; import { IExtHostRpcService } from './extHostRpcService.js'; +import * as Convert from './extHostTypeConverters.js'; export const IExtHostMpcService = createDecorator('IExtHostMpcService'); @@ -26,6 +27,11 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService protected _proxy: MainThreadMcpShape; private readonly _initialProviderPromises = new Set>(); private readonly _sseEventSources = this._register(new DisposableMap()); + private readonly _unresolvedMcpServers = new Map(); + constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, ) { @@ -61,6 +67,24 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService await Promise.all(this._initialProviderPromises); } + async $resolveMcpLaunch(collectionId: string, label: string): Promise { + const rec = this._unresolvedMcpServers.get(collectionId); + if (!rec) { + return; + } + + const server = rec.servers.find(s => s.label === label); + if (!server) { + return; + } + if (!rec.provider.resolveMcpServerDefinition) { + return Convert.McpServerDefinition.from(server); + } + + const resolved = await rec.provider.resolveMcpServerDefinition(server, CancellationToken.None); + return resolved ? Convert.McpServerDefinition.from(resolved) : undefined; + } + /** {@link vscode.lm.registerMcpConfigurationProvider} */ public registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpConfigurationProvider): IDisposable { const store = new DisposableStore(); @@ -74,37 +98,21 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService id: extensionPrefixedIdentifier(extension.identifier, id), isTrustedByDefault: true, label: metadata?.label ?? extension.displayName ?? extension.name, - scope: StorageScope.WORKSPACE + scope: StorageScope.WORKSPACE, + canResolveLaunch: typeof provider.resolveMcpServerDefinition === 'function', + extensionId: extension.identifier.value, }; const update = async () => { - const list = await provider.provideMcpServerDefinitions(CancellationToken.None); + this._unresolvedMcpServers.set(mcp.id, { servers: list ?? [], provider }); - function isSSEConfig(candidate: vscode.McpServerDefinition): candidate is vscode.McpSSEServerDefinition { - return !!(candidate as vscode.McpSSEServerDefinition).uri; - } - - const servers: McpServerDefinition[] = []; - + const servers: McpServerDefinition.Serialized[] = []; for (const item of list ?? []) { servers.push({ id: ExtensionIdentifier.toKey(extension.identifier), label: item.label, - launch: isSSEConfig(item) - ? { - type: McpServerTransportType.HTTP, - uri: item.uri, - headers: item.headers, - } - : { - type: McpServerTransportType.Stdio, - cwd: item.cwd, - args: item.args, - command: item.command, - env: item.env, - envFile: undefined, - } + launch: Convert.McpServerDefinition.from(item) }); } @@ -112,6 +120,7 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService }; store.add(toDisposable(() => { + this._unresolvedMcpServers.delete(mcp.id); this._proxy.$deleteMcpCollection(mcp.id); })); diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 75c7f54d600..a42e18e171b 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -63,6 +63,7 @@ import { getPrivateApiFor } from './extHostTestingPrivateApi.js'; import * as types from './extHostTypes.js'; import { LanguageModelPromptTsxPart, LanguageModelTextPart } from './extHostTypes.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; +import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; export namespace Command { @@ -3256,3 +3257,28 @@ export namespace IconPath { return iconPath; } } + +export namespace McpServerDefinition { + function isHttpConfig(candidate: vscode.McpServerDefinition): candidate is vscode.McpHttpServerDefinition { + return !!(candidate as vscode.McpHttpServerDefinition).uri; + } + + export function from(item: vscode.McpServerDefinition): McpServerLaunch.Serialized { + return McpServerLaunch.toSerialized( + isHttpConfig(item) + ? { + type: McpServerTransportType.HTTP, + uri: item.uri, + headers: Object.entries(item.headers), + } + : { + type: McpServerTransportType.Stdio, + cwd: item.cwd, + args: item.args, + command: item.command, + env: item.env, + envFile: undefined, + } + ); + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 9546d32946a..3d3d1d224a3 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -5158,11 +5158,11 @@ export class McpStdioServerDefinition implements vscode.McpStdioServerDefinition ) { } } -export class McpSSEServerDefinition implements vscode.McpSSEServerDefinition { - headers: [string, string][] = []; +export class McpHttpServerDefinition implements vscode.McpHttpServerDefinition { constructor( public label: string, - public uri: URI + public uri: URI, + public headers: Record = {}, ) { } } //#endregion diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index 42990525bef..65ba4094e3a 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -326,7 +326,12 @@ export class McpRegistry extends Disposable implements IMcpRegistry { } public async resolveConnection({ collectionRef, definitionRef, forceTrust, logger }: IMcpResolveConnectionOptions): Promise { - const collection = this._collections.get().find(c => c.id === collectionRef.id); + let collection = this._collections.get().find(c => c.id === collectionRef.id); + if (collection?.lazy) { + await collection.lazy.load(); + collection = this._collections.get().find(c => c.id === collectionRef.id); + } + const definition = collection?.serverDefinitions.get().find(s => s.id === definitionRef.id); if (!collection || !definition) { throw new Error(`Collection or definition not found for ${collectionRef.id} and ${definitionRef.id}`); @@ -356,7 +361,14 @@ export class McpRegistry extends Disposable implements IMcpRegistry { } } - let launch: McpServerLaunch | undefined; + let launch: McpServerLaunch | undefined = definition.launch; + if (collection.resolveServerLanch) { + launch = await collection.resolveServerLanch(definition); + if (!launch) { + return undefined; // interaction cancelled by user + } + } + try { launch = await this._replaceVariablesInLaunch(definition, definition.launch); } catch (e) { diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 7d06c30764e..a590925c9a8 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -44,6 +44,9 @@ export interface McpCollectionDefinition { /** Scope where associated collection info should be stored. */ readonly scope: StorageScope; + /** Resolves a server definition. If present, always called before a server starts. */ + resolveServerLanch?(definition: McpServerDefinition): Promise; + /** For lazy-loaded collections only: */ readonly lazy?: { /** True if `serverDefinitions` were loaded from the cache */ @@ -78,6 +81,8 @@ export namespace McpCollectionDefinition { readonly label: string; readonly isTrustedByDefault: boolean; readonly scope: StorageScope; + readonly canResolveLaunch: boolean; + readonly extensionId: string; } export function equals(a: McpCollectionDefinition, b: McpCollectionDefinition): boolean { diff --git a/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts b/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts index 28a9c7b2f36..93db89adced 100644 --- a/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.mcpConfigurationProvider.d.ts @@ -6,37 +6,110 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/243522 + /** + * McpStdioServerDefinition represents an MCP server available by running + * a local process and listening to its stdin and stdout streams. + */ export class McpStdioServerDefinition { - + /** + * The human-readable name of the server. + */ label: string; + /** + * The working directory used to start the server. + */ cwd?: Uri; + + /** + * The command used to start the server. Node.js-based servers may use + * `process.execPath` to use the editor's version of Node.js to run the script. + */ command: string; - args: readonly string[]; + /** + * Additional command-line arguments passed to the server. + */ + args: string[]; + + /** + * Optional additional environment information for the server. Variables + * in this environment will overwrite or remove (if null) the default + * environment variables. + */ env: Record; - constructor(label: string, command: string, args: string[], env: { [key: string]: string }); + constructor(label: string, command: string, args?: string[], env?: Record); } - export class McpSSEServerDefinition { - + /** + * McpHttpServerDefinition represents an MCP server available using the + * Streamable HTTP transport. + */ + export class McpHttpServerDefinition { + /** + * The human-readable name of the server. + */ label: string; + /** + * The URI of the server. The editor will make a POST request to this URI + * to begin each session. + */ uri: Uri; - headers: [string, string][]; + /** + * Optional additional heads included with each request to the server. + */ + headers: Record; - constructor(label: string, uri: Uri); + /** + * @param label The human-readable name of the server. + * @param uri The URI of the server. + * @param headers Optional additional heads included with each request to the server. + */ + constructor(label: string, uri: Uri, headers?: Record); } - export type McpServerDefinition = McpStdioServerDefinition | McpSSEServerDefinition; - - export interface McpConfigurationProvider { + export type McpServerDefinition = McpStdioServerDefinition | McpHttpServerDefinition; + /** + * A type that can provide server configurations. This may only be used in + * conjunction with `contributes.modelContextServerCollections` in the + * extension's package.json. + * + * To allow the editor to cache available servers, extensions should register + * this before `activate()` resolves. + */ + export interface McpConfigurationProvider { + /** + * Optional event fired to signal that the set of available servers has changed. + */ onDidChange?: Event; - provideMcpServerDefinitions(token: CancellationToken): ProviderResult; + /** + * Provides available MCP servers. The editor will call this method eagerly + * to ensure the availability of servers for the language model, and so + * extensions should not take actions which would require user + * interaction, such as authentication. + * + * @param token A cancellation token. + * @returns An array of MCP available MCP servers + */ + provideMcpServerDefinitions(token: CancellationToken): ProviderResult; + /** + * This function will be called when the editor needs to start MCP server. + * At this point, the extension may take any actions which may require user + * interaction, such as authentication. + * + * The extension may return undefined on error to indicate that the server + * should not be started. + * + * @param server The MCP server to resolve + * @param token A cancellation token. + * @returns The given, resolved server or thenable that resolves to such. + */ + resolveMcpServerDefinition?(server: T, token: CancellationToken): ProviderResult; } namespace lm {