diff --git a/package.json b/package.json index 299c470a67a..5a5610d6a72 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.106.0", - "distro": "d5a36b1bb1ce3814ad7e416d17ce669df183eeb6", + "distro": "b4c6e5cbae656f37b06e74d2f613f44f05730ace", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/common/observableInternal/utils/promise.ts b/src/vs/base/common/observableInternal/utils/promise.ts index e36c6be95e1..a6493858f6c 100644 --- a/src/vs/base/common/observableInternal/utils/promise.ts +++ b/src/vs/base/common/observableInternal/utils/promise.ts @@ -41,6 +41,10 @@ export class ObservablePromise { return new ObservablePromise(fn()); } + public static resolved(value: T): ObservablePromise { + return new ObservablePromise(Promise.resolve(value)); + } + private readonly _value = observableValue | undefined>(this, undefined); /** diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 52179154197..3eec8979844 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -274,6 +274,9 @@ const _allApiProposals = { mappedEditsProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mappedEditsProvider.d.ts', }, + mcpToolDefinitions: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.mcpToolDefinitions.d.ts', + }, multiDocumentHighlightProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.multiDocumentHighlightProvider.d.ts', }, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index a542aed9209..67a604b3816 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1935,7 +1935,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TextSearchCompleteMessageTypeNew: TextSearchCompleteMessageType, ChatErrorLevel: extHostTypes.ChatErrorLevel, McpHttpServerDefinition: extHostTypes.McpHttpServerDefinition, + McpHttpServerDefinition2: extHostTypes.McpHttpServerDefinition, McpStdioServerDefinition: extHostTypes.McpStdioServerDefinition, + McpStdioServerDefinition2: extHostTypes.McpStdioServerDefinition, + McpToolAvailability: extHostTypes.McpToolAvailability, SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind }; }; diff --git a/src/vs/workbench/api/common/extHostMcp.ts b/src/vs/workbench/api/common/extHostMcp.ts index f5cd5fc926d..8c83a873af5 100644 --- a/src/vs/workbench/api/common/extHostMcp.ts +++ b/src/vs/workbench/api/common/extHostMcp.ts @@ -16,7 +16,7 @@ import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/ex import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { canLog, ILogService, LogLevel } from '../../../platform/log/common/log.js'; import { StorageScope } from '../../../platform/storage/common/storage.js'; -import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerTransportHTTP, McpServerTransportType, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; +import { extensionPrefixedIdentifier, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch, McpServerStaticMetadata, McpServerStaticToolAvailability, McpServerTransportHTTP, McpServerTransportType, UserInteractionRequiredError } from '../../contrib/mcp/common/mcpTypes.js'; import { MCP } from '../../contrib/mcp/common/modelContextProtocol.js'; import { ExtHostMcpShape, IMcpAuthenticationDetails, IStartMcpOptions, MainContext, MainThreadMcpShape } from './extHost.protocol.js'; import { IExtHostInitDataService } from './extHostInitDataService.js'; @@ -24,6 +24,8 @@ import { IExtHostRpcService } from './extHostRpcService.js'; import * as Convert from './extHostTypeConverters.js'; import { IExtHostVariableResolverProvider } from './extHostVariableResolverService.js'; import { IExtHostWorkspace } from './extHostWorkspace.js'; +import { isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import { McpHttpServerDefinition, McpStdioServerDefinition, McpToolAvailability } from './extHostTypes.js'; export const IExtHostMpcService = createDecorator('IExtHostMpcService'); @@ -140,10 +142,25 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService id = id + i; } + let staticMetadata: McpServerStaticMetadata | undefined; + const castAs2 = item as McpStdioServerDefinition | McpHttpServerDefinition; + if (isProposedApiEnabled(extension, 'mcpToolDefinitions') && castAs2.metadata) { + staticMetadata = { + capabilities: castAs2.metadata.capabilities as MCP.ServerCapabilities, + instructions: castAs2.metadata.instructions, + serverInfo: castAs2.metadata.serverInfo as MCP.Implementation, + tools: castAs2.metadata.tools?.map(t => ({ + availability: t.availability === McpToolAvailability.Dynamic ? McpServerStaticToolAvailability.Dynamic : McpServerStaticToolAvailability.Initial, + definition: t.definition as MCP.Tool, + })), + }; + } + servers.push({ id, label: item.label, cacheNonce: item.version || '$$NONE', + staticMetadata, launch: Convert.McpServerDefinition.from(item), }); } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index fb0e0bcb5ad..dec735ba8e0 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3841,6 +3841,11 @@ export enum KeywordRecognitionStatus { //#endregion //#region MCP +export enum McpToolAvailability { + Initial = 0, + Dynamic = 1, +} + export class McpStdioServerDefinition implements vscode.McpStdioServerDefinition { cwd?: URI; @@ -3850,6 +3855,7 @@ export class McpStdioServerDefinition implements vscode.McpStdioServerDefinition public args: string[], public env: Record = {}, public version?: string, + public metadata?: vscode.McpServerMetadata, ) { } } @@ -3859,6 +3865,7 @@ export class McpHttpServerDefinition implements vscode.McpHttpServerDefinition { public uri: URI, public headers: Record = {}, public version?: string, + public metadata?: vscode.McpServerMetadata, ) { } } //#endregion diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index f238ef01c4e..66b370cc0b1 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -109,6 +109,8 @@ export class ListMcpServerCommand extends Action2 { const pick = quickInput.createQuickPick({ useSeparators: true }); pick.placeholder = localize('mcp.selectServer', 'Select an MCP Server'); + mcpService.activateCollections(); + store.add(pick); store.add(autorun(reader => { diff --git a/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts b/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts index 7ecad35937a..b15ca909029 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts @@ -10,12 +10,11 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { equals } from '../../../../base/common/objects.js'; -import { autorun, autorunSelfDisposable } from '../../../../base/common/observable.js'; +import { autorun } from '../../../../base/common/observable.js'; import { basename } from '../../../../base/common/resources.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IImageResizeService } from '../../../../platform/imageResize/common/imageResizeService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -26,7 +25,7 @@ import { ChatResponseResource, getAttachableImageExtension } from '../../chat/co import { LanguageModelPartAudience } from '../../chat/common/languageModels.js'; import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultInputOutputDetails, ToolDataSource, ToolProgress, ToolSet } from '../../chat/common/languageModelToolsService.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; -import { IMcpServer, IMcpService, IMcpTool, IMcpToolResourceLinkContents, McpResourceURI, McpServerCacheState, McpToolResourceLinkMimeType } from './mcpTypes.js'; +import { IMcpServer, IMcpService, IMcpTool, IMcpToolResourceLinkContents, McpResourceURI, McpToolResourceLinkMimeType } from './mcpTypes.js'; import { mcpServerToSourceData } from './mcpTypesUtils.js'; interface ISyncedToolData { @@ -89,25 +88,6 @@ export class McpLanguageModelToolContribution extends Disposable implements IWor const collectionObservable = this._mcpRegistry.collections.map(collections => collections.find(c => c.id === server.collection.id)); - // If the server is extension-provided and was marked outdated automatically start it - store.add(autorunSelfDisposable(reader => { - const collection = collectionObservable.read(reader); - if (!collection) { - return; - } - - if (!(collection.source instanceof ExtensionIdentifier)) { - reader.dispose(); - return; - } - - const cacheState = server.cacheState.read(reader); - if (cacheState === McpServerCacheState.Unknown || cacheState === McpServerCacheState.Outdated) { - reader.dispose(); - server.start(); - } - })); - store.add(autorun(reader => { const toDelete = new Set(tools.keys()); diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 0bd6db9155b..07cc7064901 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -10,7 +10,7 @@ import * as json from '../../../../base/common/json.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; import { mapValues } from '../../../../base/common/objects.js'; -import { autorun, autorunSelfDisposable, derived, disposableObservableValue, IDerivedReader, IObservable, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js'; +import { autorun, autorunSelfDisposable, derived, disposableObservableValue, IDerivedReader, IObservable, IReader, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js'; import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { createURITransformer } from '../../../../base/common/uriTransformer.js'; @@ -34,7 +34,7 @@ import { McpDevModeServerAttache } from './mcpDevMode.js'; import { McpIcons, parseAndValidateMcpIcon, StoredMcpIcons } from './mcpIcons.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; -import { extensionMcpCollectionPrefix, IMcpElicitationService, IMcpIcons, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerTransportType, McpToolName, UserInteractionRequiredError } from './mcpTypes.js'; +import { extensionMcpCollectionPrefix, IMcpElicitationService, IMcpIcons, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerStaticToolAvailability, McpServerTransportType, McpToolName, UserInteractionRequiredError } from './mcpTypes.js'; import { MCP } from './modelContextProtocol.js'; import { UriTemplate } from './uriTemplate.js'; @@ -230,9 +230,20 @@ interface ServerMetadata { } class CachedPrimitive { + /** + * @param _definitionId Server definition ID + * @param _cache Metadata cache instance + * @param _fromStaticDefinition Static definition that came with the server. + * This should ONLY have a value if it should be used instead of whatever + * is currently in the cache. + * @param _fromCache Pull the value from the cache entry. + * @param _toT Transform the value to the observable type. + * @param defaultValue Default value if no cache entry. + */ constructor( private readonly _definitionId: string, private readonly _cache: McpServerMetadataCache, + private readonly _fromStaticDefinition: IObservable | undefined, private readonly _fromCache: (entry: IToolCacheEntry) => C, private readonly _toT: (values: C, reader: IDerivedReader) => T, private readonly defaultValue: C, @@ -243,6 +254,10 @@ class CachedPrimitive { return c ? { data: this._fromCache(c), nonce: c.nonce } : undefined; } + public hasStaticDefinition(reader: IReader | undefined) { + return !!this._fromStaticDefinition?.read(reader); + } + public readonly fromServerPromise = observableValue { public readonly value: IObservable = derived(reader => { const serverTools = this.fromServer.read(reader); - const definitions = serverTools?.data ?? this.fromCache?.data ?? this.defaultValue; + const definitions = serverTools?.data ?? this._fromStaticDefinition?.read(reader) ?? this.fromCache?.data ?? this.defaultValue; return this._toT(definitions, reader); }); } @@ -307,9 +322,9 @@ export class McpServer extends Disposable implements IMcpServer { public readonly connectionState: IObservable = derived(reader => this._connection.read(reader)?.state.read(reader) ?? { state: McpConnectionState.Kind.Stopped }); - private readonly _capabilities = observableValue('mcpserver.capabilities', undefined); + private readonly _capabilities: CachedPrimitive; public get capabilities() { - return this._capabilities; + return this._capabilities.value; } private readonly _tools: CachedPrimitive; @@ -343,6 +358,10 @@ export class McpServer extends Disposable implements IMcpServer { public readonly cacheState = derived(reader => { const currentNonce = () => this._fullDefinitions.read(reader)?.server?.cacheNonce; const stateWhenServingFromCache = () => { + if (this._tools.hasStaticDefinition(reader)) { + return McpServerCacheState.Cached; + } + if (!this._tools.fromCache) { return McpServerCacheState.Unknown; } @@ -440,16 +459,27 @@ export class McpServer extends Disposable implements IMcpServer { const cnx = this._connection.read(reader); const handler = cnx?.handler.read(reader); if (handler) { - this.populateLiveData(handler, cnx?.definition.cacheNonce, reader.store); + this._populateLiveData(handler, cnx?.definition.cacheNonce, reader.store); } else if (this._tools) { this.resetLiveData(); } })); + const staticMetadata = derived(reader => { + const def = this._fullDefinitions.read(reader).server; + return def && def.cacheNonce !== this._tools.fromCache?.nonce ? def.staticMetadata : undefined; + }); + // 3. Publish tools this._tools = new CachedPrimitive( this.definition.id, this._primitiveCache, + staticMetadata + .map(m => { + const tools = m?.tools?.filter(t => t.availability === McpServerStaticToolAvailability.Initial).map(t => t.definition); + return tools?.length ? new ObservablePromise(this._getValidatedTools(tools)) : undefined; + }) + .map((o, reader) => o?.promiseResult.read(reader)?.data), (entry) => entry.tools, (entry) => entry.map(def => new McpTool(this, toolPrefix, def)).sort((a, b) => a.compare(b)), [], @@ -459,6 +489,7 @@ export class McpServer extends Disposable implements IMcpServer { this._prompts = new CachedPrimitive( this.definition.id, this._primitiveCache, + undefined, (entry) => entry.prompts || [], (entry) => entry.map(e => new McpPrompt(this, e)), [], @@ -467,12 +498,20 @@ export class McpServer extends Disposable implements IMcpServer { this._serverMetadata = new CachedPrimitive( this.definition.id, this._primitiveCache, + staticMetadata.map(m => m ? this._toStoredMetadata(m?.serverInfo, m?.instructions) : undefined), (entry) => ({ serverName: entry.serverName, serverInstructions: entry.serverInstructions, serverIcons: entry.serverIcons }), (entry) => ({ serverName: entry?.serverName, serverInstructions: entry?.serverInstructions, icons: McpIcons.fromStored(entry?.serverIcons) }), undefined, ); - this._capabilities.set(this._primitiveCache.get(this.definition.id)?.capabilities, undefined); + this._capabilities = new CachedPrimitive( + this.definition.id, + this._primitiveCache, + staticMetadata.map(m => m?.capabilities !== undefined ? encodeCapabilities(m.capabilities) : undefined), + (entry) => entry.capabilities, + (entry) => entry, + undefined, + ); } public readDefinitions(): IObservable<{ server: McpServerDefinition | undefined; collection: McpCollectionDefinition | undefined }> { @@ -731,7 +770,7 @@ export class McpServer extends Disposable implements IMcpServer { return { error: messages }; } - private async _getValidatedTools(handler: McpServerRequestHandler, tools: MCP.Tool[]): Promise { + private async _getValidatedTools(tools: MCP.Tool[]): Promise { let error = ''; const validations = await Promise.all(tools.map(t => this._normalizeTool(t))); @@ -749,7 +788,7 @@ export class McpServer extends Disposable implements IMcpServer { } if (error) { - handler.logger.warn(`${tools.length - validated.length} tools have invalid JSON schemas and will be omitted`); + this._logger.warn(`${tools.length - validated.length} tools have invalid JSON schemas and will be omitted`); warnInvalidTools(this._instantiationService, this.definition.label, error); } @@ -771,76 +810,87 @@ export class McpServer extends Disposable implements IMcpServer { return parseAndValidateMcpIcon(icons, cnx.launchDefinition, this._logger); } - private populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) { + private _setServerTools(nonce: string | undefined, toolsPromise: Promise, tx: ITransaction | undefined) { + const toolPromiseSafe = toolsPromise.then(async tools => { + this._logger.info(`Discovered ${tools.length} tools`); + const data = await this._getValidatedTools(tools); + this._primitiveCache.store(this.definition.id, { tools: data, nonce }); + return { data, nonce }; + }); + this._tools.fromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx); + return toolPromiseSafe; + } + + private _setServerPrompts(nonce: string | undefined, promptsPromise: Promise, tx: ITransaction | undefined) { + const promptsPromiseSafe = promptsPromise.then((result): { data: StoredMcpPrompt[]; nonce: string | undefined } => { + const data: StoredMcpPrompt[] = result.map(prompt => ({ + ...prompt, + _icons: this._parseIcons(prompt) + })); + this._primitiveCache.store(this.definition.id, { prompts: data, nonce }); + return { data, nonce }; + }); + + this._prompts.fromServerPromise.set(new ObservablePromise(promptsPromiseSafe), tx); + return promptsPromiseSafe; + } + + private _toStoredMetadata(serverInfo?: MCP.Implementation, instructions?: string): StoredServerMetadata { + return { + serverName: serverInfo ? serverInfo.title || serverInfo.name : undefined, + serverInstructions: instructions, + serverIcons: serverInfo ? this._parseIcons(serverInfo) : undefined, + }; + } + + private _setServerMetadata( + nonce: string | undefined, + { serverInfo, instructions, capabilities }: { serverInfo: MCP.Implementation; instructions: string | undefined; capabilities: MCP.ServerCapabilities }, + tx: ITransaction | undefined, + ) { + const serverMetadata: StoredServerMetadata = this._toStoredMetadata(serverInfo, instructions); + this._serverMetadata.fromServerPromise.set(ObservablePromise.resolved({ nonce, data: serverMetadata }), tx); + + const capabilitiesEncoded = encodeCapabilities(capabilities); + this._capabilities.fromServerPromise.set(ObservablePromise.resolved({ data: capabilitiesEncoded, nonce }), tx); + this._primitiveCache.store(this.definition.id, { ...serverMetadata, nonce, capabilities: capabilitiesEncoded }); + } + + private _populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) { const cts = new CancellationTokenSource(); store.add(toDisposable(() => cts.dispose(true))); - // todo: add more than just tools here - const updateTools = (tx: ITransaction | undefined) => { const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]); - const toolPromiseSafe = toolPromise.then(async tools => { - handler.logger.info(`Discovered ${tools.length} tools`); - return { data: await this._getValidatedTools(handler, tools), nonce: cacheNonce }; - }); - this._tools.fromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx); - return toolPromiseSafe; + return this._setServerTools(cacheNonce, toolPromise, tx); }; const updatePrompts = (tx: ITransaction | undefined) => { const promptsPromise = handler.capabilities.prompts ? handler.listPrompts({}, cts.token) : Promise.resolve([]); - const promptsPromiseSafe = promptsPromise.then(data => ({ - data: data.map(prompt => ({ - ...prompt, - _icons: this._parseIcons(prompt) - })), - nonce: cacheNonce - })); - this._prompts.fromServerPromise.set(new ObservablePromise(promptsPromiseSafe), tx); - return promptsPromiseSafe; + return this._setServerPrompts(cacheNonce, promptsPromise, tx); }; store.add(handler.onDidChangeToolList(() => { - handler.logger.info('Tool list changed, refreshing tools...'); + this._logger.info('Tool list changed, refreshing tools...'); updateTools(undefined); })); store.add(handler.onDidChangePromptList(() => { - handler.logger.info('Prompts list changed, refreshing prompts...'); + this._logger.info('Prompts list changed, refreshing prompts...'); updatePrompts(undefined); })); - const serverMetadata = { - serverName: handler.serverInfo.title || handler.serverInfo.name, - serverInstructions: handler.serverInstructions, - serverIcons: this._parseIcons(handler.serverInfo), - }; - - const metadataPromise = new ObservablePromise(Promise.resolve({ - nonce: cacheNonce, - data: serverMetadata, - })); - transaction(tx => { - // note: all update* methods must use tx synchronously - const capabilities = encodeCapabilities(handler.capabilities); - this._primitiveCache.store(this.definition.id, { ...serverMetadata, capabilities }); - this._capabilities.set(capabilities, tx); - this._serverMetadata.fromServerPromise.set(metadataPromise, tx); - - Promise.all([updateTools(tx), updatePrompts(tx)]).then(([{ data: tools }, { data: prompts }]) => { - this._primitiveCache.store(this.definition.id, { - nonce: cacheNonce, - tools, - prompts, - capabilities, - }); + this._setServerMetadata(cacheNonce, { serverInfo: handler.serverInfo, instructions: handler.serverInstructions, capabilities: handler.capabilities }, tx); + updatePrompts(tx); + const toolUpdate = updateTools(tx); + toolUpdate.then(tools => { this._telemetryService.publicLog2('mcp/serverBoot', { supportsLogging: !!handler.capabilities.logging, supportsPrompts: !!handler.capabilities.prompts, supportsResources: !!handler.capabilities.resources, - toolCount: tools.length, + toolCount: tools.data.length, serverName: handler.serverInfo.name, serverVersion: handler.serverInfo.version, }); diff --git a/src/vs/workbench/contrib/mcp/common/mcpService.ts b/src/vs/workbench/contrib/mcp/common/mcpService.ts index 7b261f00f10..4b3d6cb4bd7 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpService.ts @@ -146,20 +146,7 @@ export class McpService extends Disposable implements IMcpService { } public async activateCollections(): Promise { - const collectionIds = await this._activateCollections(); - - // Discover any newly-collected servers with unknown tools - const todo: Promise[] = []; - for (const { object: server } of this._servers.get()) { - if (collectionIds.has(server.collection.id)) { - const state = server.cacheState.get(); - if (state === McpServerCacheState.Unknown) { - todo.push(server.start()); - } - } - } - - await Promise.all(todo); + await this._activateCollections(); } private async _activateCollections() { diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 794a2485ae4..ca79e79d5ec 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -129,6 +129,9 @@ export interface McpServerDefinition { readonly cacheNonce: string; /** Dev mode configuration for the server */ readonly devMode?: IMcpDevModeConfig; + /** Static description of server tools/data, used to hydrate the cache. */ + readonly staticMetadata?: McpServerStaticMetadata; + readonly presentation?: { /** Sort order of the definition. */ @@ -138,6 +141,20 @@ export interface McpServerDefinition { }; } +export const enum McpServerStaticToolAvailability { + /** Tool is expected to be present as soon as the server is started. */ + Initial, + /** Tool may be present later. */ + Dynamic, +} + +export interface McpServerStaticMetadata { + tools?: { availability: McpServerStaticToolAvailability; definition: MCP.Tool }[]; + instructions?: string; + capabilities?: MCP.ServerCapabilities; + serverInfo?: MCP.Implementation; +} + export namespace McpServerDefinition { export interface Serialized { readonly id: string; @@ -145,6 +162,7 @@ export namespace McpServerDefinition { readonly cacheNonce: string; readonly launch: McpServerLaunch.Serialized; readonly variableReplacement?: McpServerDefinitionVariableReplacement.Serialized; + readonly staticMetadata?: McpServerStaticMetadata; } export function toSerialized(def: McpServerDefinition): McpServerDefinition.Serialized { @@ -156,6 +174,7 @@ export namespace McpServerDefinition { id: def.id, label: def.label, cacheNonce: def.cacheNonce, + staticMetadata: def.staticMetadata, launch: McpServerLaunch.fromSerialized(def.launch), variableReplacement: def.variableReplacement ? McpServerDefinitionVariableReplacement.fromSerialized(def.variableReplacement) : undefined, }; @@ -229,7 +248,7 @@ export interface IMcpService { /** Cancels any current autostart @internal */ cancelAutostart(): void; - /** Activatese extensions and runs their MCP servers. */ + /** Activates extension-providing MCP servers that have not yet been discovered. */ activateCollections(): Promise; } diff --git a/src/vscode-dts/vscode.proposed.mcpToolDefinitions.d.ts b/src/vscode-dts/vscode.proposed.mcpToolDefinitions.d.ts new file mode 100644 index 00000000000..6c806264304 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.mcpToolDefinitions.d.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * 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/272000 @connor4312 + + /** + * Defines when a {@link McpServerLanguageModelToolDefinition} is available + * for calling. + */ + export enum McpToolAvailability { + /** + * The MCP tool is available when the server starts up. + */ + Initial = 0, + + /** + * The MCP tool is conditionally available when certain preconditions are met. + */ + Dynamic = 1, + } + + /** + * The definition for a tool an MCP server provides. Extensions may provide + * this as part of their server metadata to allow the editor to defer + * starting the server until it's called by a language model. + */ + export interface McpServerLanguageModelToolDefinition { + /** + * The definition of the tool as it appears on the MCP protocol. This should + * be an object that includes the `inputSchema` and `name`, + * among other optional properties. + */ + definition?: unknown; + + /** + * An indicator for when the tool is available for calling. + */ + availability: McpToolAvailability; + } + + /** + * Metadata which the editor can use to hydrate information about the server + * prior to starting it. The extension can provide tools and basic server + * instructions as they would be expected to appear on MCP itself. + * + * Once a server is started, the observed values will be cached and take + * precedence over those statically declared here unless and until the + * server's {@link McpStdioServerDefinition.version version} is updated. If + * you can ensure the metadata is always accurate and do not otherwise have + * a server `version` to use, it is reasonable to set the server `version` + * to a hash of this object to ensure the cache tracks the {@link McpServerMetadata}. + */ + export interface McpServerMetadata { + /** + * Tools the MCP server exposes. + */ + tools?: McpServerLanguageModelToolDefinition[]; + + /** + * MCP server instructions as it would appear on the `initialize` result in the protocol. + */ + instructions?: string; + + /** + * MCP server capabilities as they would appear on the `initialize` result in the protocol. + */ + capabilities?: unknown; + + /** + * MCP server info as it would appear on the `initialize` result in the protocol. + */ + serverInfo?: unknown; + } + + + export class McpStdioServerDefinition2 extends McpStdioServerDefinition { + metadata?: McpServerMetadata; + constructor(label: string, command: string, args?: string[], env?: Record, version?: string, metadata?: McpServerMetadata); + } + + export class McpHttpServerDefinition2 extends McpHttpServerDefinition { + metadata?: McpServerMetadata; + constructor(label: string, uri: Uri, headers?: Record, version?: string, metadata?: McpServerMetadata); + } +}