diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 5c308453ec5..525f6195420 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -26,6 +26,7 @@ "fsChunks", "interactive", "languageStatusText", + "mcpServerDefinitions", "nativeWindowHandle", "notebookDeprecated", "notebookLiveShare", diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index d9bb1d86281..cf9ff526955 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -290,6 +290,9 @@ const _allApiProposals = { markdownAlertSyntax: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.markdownAlertSyntax.d.ts', }, + mcpServerDefinitions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts', + }, mcpToolDefinitions: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mcpToolDefinitions.d.ts', }, diff --git a/src/vs/workbench/api/browser/mainThreadMcp.ts b/src/vs/workbench/api/browser/mainThreadMcp.ts index e0b0fe9410b..5dede3a3d9c 100644 --- a/src/vs/workbench/api/browser/mainThreadMcp.ts +++ b/src/vs/workbench/api/browser/mainThreadMcp.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { mapFindFirst } from '../../../base/common/arraysFind.js'; -import { disposableTimeout } from '../../../base/common/async.js'; +import { disposableTimeout, RunOnceScheduler } from '../../../base/common/async.js'; import { CancellationError } from '../../../base/common/errors.js'; import { Emitter } from '../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js'; -import { ISettableObservable, observableValue } from '../../../base/common/observable.js'; +import { autorun, ISettableObservable, observableValue } from '../../../base/common/observable.js'; import Severity from '../../../base/common/severity.js'; import { URI } from '../../../base/common/uri.js'; import * as nls from '../../../nls.js'; @@ -98,6 +98,35 @@ export class MainThreadMcp extends Disposable implements MainThreadMcpShape { return launch; }, })); + + // Subscribe to MCP server definition changes and notify ext host + const onDidChangeMcpServerDefinitionsTrigger = this._register(new RunOnceScheduler(() => this._proxy.$onDidChangeMcpServerDefinitions(), 500)); + this._register(autorun(reader => { + const collections = this._mcpRegistry.collections.read(reader); + // Read all server definitions to track changes + for (const collection of collections) { + collection.serverDefinitions.read(reader); + } + // Notify ext host that definitions changed (it will re-fetch if needed) + if (!onDidChangeMcpServerDefinitionsTrigger.isScheduled()) { + onDidChangeMcpServerDefinitionsTrigger.schedule(); + } + })); + } + + /** Returns all MCP server definitions known to the editor. */ + $getMcpServerDefinitions(): Promise { + const collections = this._mcpRegistry.collections.get(); + const allServers: McpServerDefinition.Serialized[] = []; + + for (const collection of collections) { + const servers = collection.serverDefinitions.get(); + for (const server of servers) { + allServers.push(McpServerDefinition.toSerialized(server)); + } + } + + return Promise.resolve(allServers); } $upsertMcpCollection(collection: McpCollectionDefinition.FromExtHost, serversDto: McpServerDefinition.Serialized[]): void { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 38849ed2a4a..61bda2cda0d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1630,6 +1630,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerMcpServerDefinitionProvider(id, provider) { return extHostMcp.registerMcpConfigurationProvider(extension, id, provider); }, + onDidChangeMcpServerDefinitions: (...args) => { + checkProposedApiEnabled(extension, 'mcpServerDefinitions'); + return _asExtensionEvent(extHostMcp.onDidChangeMcpServerDefinitions)(...args); + }, + get mcpServerDefinitions() { + checkProposedApiEnabled(extension, 'mcpServerDefinitions'); + return extHostMcp.mcpServerDefinitions; + }, 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 498dc1cd162..f6be2fb99ef 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3163,6 +3163,8 @@ export interface IStartMcpOptions { errorOnUserInteraction?: boolean; } + + export interface ExtHostMcpShape { $substituteVariables(workspaceFolder: UriComponents | undefined, value: McpServerLaunch.Serialized): Promise; $resolveMcpLaunch(collectionId: string, label: string): Promise; @@ -3170,6 +3172,8 @@ export interface ExtHostMcpShape { $stopMcp(id: number): void; $sendMessage(id: number, message: string): void; $waitForInitialCollectionProviders(): Promise; + /** Notification that MCP server definitions have changed. ExtHost should re-fetch. */ + $onDidChangeMcpServerDefinitions(): void; } export interface IMcpAuthenticationDetails { @@ -3210,6 +3214,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; + /** Returns all MCP server definitions known to the editor. */ + $getMcpServerDefinitions(): Promise; } export interface MainThreadDataChannelsShape extends IDisposable { diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index 5079b4f9c8f..d9aacf2fa91 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import { DeferredPromise, raceCancellationError, Sequencer, timeout } from '../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; import { CancellationError } from '../../../base/common/errors.js'; +import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { AUTH_SCOPE_SEPARATOR, fetchAuthorizationServerMetadata, fetchResourceMetadata, getDefaultMetadataForUrl, IAuthorizationProtectedResourceMetadata, IAuthorizationServerMetadata, parseWWWAuthenticateHeader, scopesMatch } from '../../../base/common/oauth.js'; import { SSEParser } from '../../../base/common/sseParser.js'; @@ -33,6 +34,12 @@ export const IExtHostMpcService = createDecorator('IExtHostM export interface IExtHostMpcService extends ExtHostMcpShape { registerMcpConfigurationProvider(extension: IExtensionDescription, id: string, provider: vscode.McpServerDefinitionProvider): IDisposable; + + /** Event that fires when the set of MCP server definitions changes. */ + readonly onDidChangeMcpServerDefinitions: Event; + + /** Returns all MCP server definitions known to the editor. */ + readonly mcpServerDefinitions: readonly vscode.McpServerDefinition[]; } const serverDataValidation = vObj({ @@ -65,6 +72,12 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService servers: vscode.McpServerDefinition[]; }>(); + // MCP server definitions synced from main thread + private readonly _onDidChangeMcpServerDefinitions = this._register(new Emitter()); + readonly onDidChangeMcpServerDefinitions: Event = this._onDidChangeMcpServerDefinitions.event; + private _mcpServerDefinitions: readonly vscode.McpServerDefinition[] = []; + private _mcpServerDefinitionsInitialized = false; + constructor( @IExtHostRpcService extHostRpc: IExtHostRpcService, @ILogService protected readonly _logService: ILogService, @@ -76,6 +89,31 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService this._proxy = extHostRpc.getProxy(MainContext.MainThreadMcp); } + /** Returns all MCP server definitions known to the editor. */ + get mcpServerDefinitions(): readonly vscode.McpServerDefinition[] { + if (!this._mcpServerDefinitionsInitialized) { + this._mcpServerDefinitionsInitialized = true; + // Fetch asynchronously in background and update when ready + this._proxy.$getMcpServerDefinitions().then(dtos => { + this._mcpServerDefinitions = dtos.map(dto => Convert.McpServerDefinition.to(dto)); + this._onDidChangeMcpServerDefinitions.fire(); + }).catch(err => this._logService.error('Failed to fetch MCP server definitions:', err)); + } + return this._mcpServerDefinitions; + } + + /** Called by main thread to notify that MCP server definitions have changed. */ + $onDidChangeMcpServerDefinitions(): void { + if (!this._mcpServerDefinitionsInitialized) { + return; + } + // Re-fetch from main thread + this._proxy.$getMcpServerDefinitions().then(dtos => { + this._mcpServerDefinitions = dtos.map(dto => Convert.McpServerDefinition.to(dto)); + this._onDidChangeMcpServerDefinitions.fire(); + }).catch(err => this._logService.error('Failed to fetch MCP server definitions:', err)); + } + $startMcp(id: number, opts: IStartMcpOptions): void { this._startMcp(id, McpServerLaunch.fromSerialized(opts.launch), opts.defaultCwd && URI.revive(opts.defaultCwd), opts.errorOnUserInteraction); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index c22c4e08f77..0ef0f0979e2 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -50,7 +50,7 @@ import { IToolInvocationContext, IToolResult, IToolResultInputOutputDetails, ITo import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { IChatMessageDataPart, IChatResponseDataPart, IChatResponsePromptTsxPart, IChatResponseTextPart } from '../../contrib/chat/common/languageModels.js'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; -import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; +import { McpServerDefinition as McpServerDefinitionType, McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; import * as notebooks from '../../contrib/notebook/common/notebookCommon.js'; import { CellEditType } from '../../contrib/notebook/common/notebookCommon.js'; import { ICellRange } from '../../contrib/notebook/common/notebookRange.js'; @@ -3768,6 +3768,31 @@ export namespace McpServerDefinition { } ); } + + /** Converts from the IPC DTO to the API type. */ + export function to(dto: McpServerDefinitionType.Serialized): vscode.McpServerDefinition { + const launch = McpServerLaunch.fromSerialized(dto.launch); + if (launch.type === McpServerTransportType.HTTP) { + return new types.McpHttpServerDefinition( + dto.label, + launch.uri, + Object.fromEntries(launch.headers), + dto.cacheNonce === '$$NONE' ? undefined : dto.cacheNonce, + ); + } else { + const result = new types.McpStdioServerDefinition( + dto.label, + launch.command, + [...launch.args], + Object.fromEntries(Object.entries(launch.env).map(([key, value]) => [key, value === null ? null : String(value)])), + dto.cacheNonce === '$$NONE' ? undefined : dto.cacheNonce, + ); + if (launch.cwd) { + result.cwd = URI.file(launch.cwd); + } + return result; + } + } } export namespace SourceControlInputBoxValidationType { diff --git a/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts b/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts new file mode 100644 index 00000000000..c0d4dc2b702 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.mcpServerDefinitions.d.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/288777 @DonJayamanne + + /** + * Namespace for language model related functionality. + */ + export namespace lm { + /** + * All MCP server definitions known to the editor. This includes + * servers defined in user and workspace mcp.json files as well as those + * provided by extensions. + * + * Consumers should listen to {@link onDidChangeMcpServerDefinitions} and + * re-read this property when it fires. + */ + export const mcpServerDefinitions: readonly McpServerDefinition[]; + + /** + * Event that fires when the set of MCP server definitions changes. + * This can be due to additions, deletions, or modifications of server + * definitions from any source. + */ + export const onDidChangeMcpServerDefinitions: Event; + } +}