Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpServer.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { AsyncIterableProducer, raceCancellationError, Sequencer } from '../../../../base/common/async.js';6import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';7import { Iterable } from '../../../../base/common/iterator.js';8import * as json from '../../../../base/common/json.js';9import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';10import { LRUCache } from '../../../../base/common/map.js';11import { mapValues } from '../../../../base/common/objects.js';12import { autorun, derived, disposableObservableValue, IDerivedReader, IObservable, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js';13import { basename } from '../../../../base/common/resources.js';14import { URI } from '../../../../base/common/uri.js';15import { generateUuid } from '../../../../base/common/uuid.js';16import { localize } from '../../../../nls.js';17import { ICommandService } from '../../../../platform/commands/common/commands.js';18import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';19import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js';20import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js';21import { IOpenerService } from '../../../../platform/opener/common/opener.js';22import { IRemoteAuthorityResolverService } from '../../../../platform/remote/common/remoteAuthorityResolver.js';23import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';24import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';25import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';26import { IEditorService } from '../../../services/editor/common/editorService.js';27import { IExtensionService } from '../../../services/extensions/common/extensions.js';28import { IOutputService } from '../../../services/output/common/output.js';29import { ToolProgress } from '../../chat/common/languageModelToolsService.js';30import { mcpActivationEvent } from './mcpConfiguration.js';31import { McpDevModeServerAttache } from './mcpDevMode.js';32import { IMcpRegistry } from './mcpRegistryTypes.js';33import { McpServerRequestHandler } from './mcpServerRequestHandler.js';34import { extensionMcpCollectionPrefix, IMcpElicitationService, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerTransportType, McpToolName } from './mcpTypes.js';35import { MCP } from './modelContextProtocol.js';36import { UriTemplate } from './uriTemplate.js';3738type ServerBootData = {39supportsLogging: boolean;40supportsPrompts: boolean;41supportsResources: boolean;42toolCount: number;43serverName: string;44serverVersion: string;45};46type ServerBootClassification = {47owner: 'connor4312';48comment: 'Details the capabilities of the MCP server';49supportsLogging: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports logging' };50supportsPrompts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports prompts' };51supportsResources: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports resource' };52toolCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tools the server advertises' };53serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };54serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };55};5657type ElicitationTelemetryData = {58serverName: string;59serverVersion: string;60};6162type ElicitationTelemetryClassification = {63owner: 'connor4312';64comment: 'Triggered when elictation is requested';65serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };66serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };67};6869export type McpServerInstallData = {70serverName: string;71source: 'gallery' | 'local';72scope: string;73success: boolean;74error?: string;75hasInputs: boolean;76};7778export type McpServerInstallClassification = {79owner: 'connor4312';80comment: 'MCP server installation event tracking';81serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server being installed' };82source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation source (gallery or local)' };83scope: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation scope (user, workspace, etc.)' };84success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether installation succeeded' };85error?: { classification: 'CallstackOrException'; purpose: 'FeatureInsight'; comment: 'Error message if installation failed' };86hasInputs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server requires input configuration' };87};8889type ServerBootState = {90state: string;91time: number;92};93type ServerBootStateClassification = {94owner: 'connor4312';95comment: 'Details the capabilities of the MCP server';96state: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The server outcome' };97time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Duration in milliseconds to reach that state' };98};99100interface IToolCacheEntry {101readonly serverName: string | undefined;102readonly serverInstructions: string | undefined;103readonly trustedAtNonce: string | undefined;104105readonly nonce: string | undefined;106/** Cached tools so we can show what's available before it's started */107readonly tools: readonly IValidatedMcpTool[];108/** Cached prompts */109readonly prompts: readonly MCP.Prompt[] | undefined;110/** Cached capabilities */111readonly capabilities: McpCapability | undefined;112}113114const emptyToolEntry: IToolCacheEntry = {115serverName: undefined,116serverInstructions: undefined,117trustedAtNonce: undefined,118nonce: undefined,119tools: [],120prompts: undefined,121capabilities: undefined,122};123124interface IServerCacheEntry {125readonly servers: readonly McpServerDefinition.Serialized[];126}127128const toolInvalidCharRe = /[^a-z0-9_-]/gi;129130export class McpServerMetadataCache extends Disposable {131private didChange = false;132private readonly cache = new LRUCache<string, IToolCacheEntry>(128);133private readonly extensionServers = new Map</* collection ID */string, IServerCacheEntry>();134135constructor(136scope: StorageScope,137@IStorageService storageService: IStorageService,138) {139super();140141type StoredType = {142extensionServers: [string, IServerCacheEntry][];143serverTools: [string, IToolCacheEntry][];144};145146const storageKey = 'mcpToolCache';147this._register(storageService.onWillSaveState(() => {148if (this.didChange) {149storageService.store(storageKey, {150extensionServers: [...this.extensionServers],151serverTools: this.cache.toJSON(),152} satisfies StoredType, scope, StorageTarget.MACHINE);153this.didChange = false;154}155}));156157try {158const cached: StoredType | undefined = storageService.getObject(storageKey, scope);159this.extensionServers = new Map(cached?.extensionServers ?? []);160cached?.serverTools?.forEach(([k, v]) => this.cache.set(k, v));161} catch {162// ignored163}164}165166/** Resets the cache for primitives and extension servers */167reset() {168this.cache.clear();169this.extensionServers.clear();170this.didChange = true;171}172173/** Gets cached primitives for a server (used before a server is running) */174get(definitionId: string) {175return this.cache.get(definitionId);176}177178/** Sets cached primitives for a server */179store(definitionId: string, entry: Partial<IToolCacheEntry>): void {180const prev = this.get(definitionId) || emptyToolEntry;181this.cache.set(definitionId, { ...prev, ...entry });182this.didChange = true;183}184185/** Gets cached servers for a collection (used for extensions, before the extension activates) */186getServers(collectionId: string) {187return this.extensionServers.get(collectionId);188}189190/** Sets cached servers for a collection */191storeServers(collectionId: string, entry: IServerCacheEntry | undefined): void {192if (entry) {193this.extensionServers.set(collectionId, entry);194} else {195this.extensionServers.delete(collectionId);196}197this.didChange = true;198}199}200201interface IValidatedMcpTool extends MCP.Tool {202/**203* Tool name as published by the MCP server. This may204* be different than the one in {@link definition} due to name normalization205* in {@link McpServer._getValidatedTools}.206*/207serverToolName: string;208}209210interface ServerMetadata {211readonly serverName: string | undefined;212readonly serverInstructions: string | undefined;213}214215class CachedPrimitive<T, C> {216constructor(217private readonly _definitionId: string,218private readonly _cache: McpServerMetadataCache,219private readonly _fromCache: (entry: IToolCacheEntry) => C,220private readonly _toT: (values: C, reader: IDerivedReader<void>) => T,221private readonly defaultValue: C,222) { }223224public get fromCache(): { nonce: string | undefined; data: C } | undefined {225const c = this._cache.get(this._definitionId);226return c ? { data: this._fromCache(c), nonce: c.nonce } : undefined;227}228229public readonly fromServerPromise = observableValue<ObservablePromise<{230readonly data: C;231readonly nonce: string | undefined;232}> | undefined>(this, undefined);233234private readonly fromServer = derived(reader => this.fromServerPromise.read(reader)?.promiseResult.read(reader)?.data);235236public readonly value: IObservable<T> = derived(reader => {237const serverTools = this.fromServer.read(reader);238const definitions = serverTools?.data ?? this.fromCache?.data ?? this.defaultValue;239return this._toT(definitions, reader);240});241}242243export class McpServer extends Disposable implements IMcpServer {244/**245* Helper function to call the function on the handler once it's online. The246* connection started if it is not already.247*/248public static async callOn<R>(server: IMcpServer, fn: (handler: McpServerRequestHandler) => Promise<R>, token: CancellationToken = CancellationToken.None): Promise<R> {249await server.start({ promptType: 'all-untrusted' }); // idempotent250251let ranOnce = false;252let d: IDisposable;253254const callPromise = new Promise<R>((resolve, reject) => {255256d = autorun(reader => {257const connection = server.connection.read(reader);258if (!connection || ranOnce) {259return;260}261262const handler = connection.handler.read(reader);263if (!handler) {264const state = connection.state.read(reader);265if (state.state === McpConnectionState.Kind.Error) {266reject(new McpConnectionFailedError(`MCP server could not be started: ${state.message}`));267return;268} else if (state.state === McpConnectionState.Kind.Stopped) {269reject(new McpConnectionFailedError('MCP server has stopped'));270return;271} else {272// keep waiting for handler273return;274}275}276277resolve(fn(handler));278ranOnce = true; // aggressive prevent multiple racey calls, don't dispose because autorun is sync279});280});281282return raceCancellationError(callPromise, token).finally(() => d.dispose());283}284285private readonly _connectionSequencer = new Sequencer();286private readonly _connection = this._register(disposableObservableValue<IMcpServerConnection | undefined>(this, undefined));287288public readonly connection = this._connection;289public readonly connectionState: IObservable<McpConnectionState> = derived(reader => this._connection.read(reader)?.state.read(reader) ?? { state: McpConnectionState.Kind.Stopped });290291292private readonly _capabilities = observableValue<number | undefined>('mcpserver.capabilities', undefined);293public get capabilities() {294return this._capabilities;295}296297private readonly _tools: CachedPrimitive<readonly IMcpTool[], readonly IValidatedMcpTool[]>;298public get tools() {299return this._tools.value;300}301302private readonly _prompts: CachedPrimitive<readonly IMcpPrompt[], readonly MCP.Prompt[]>;303public get prompts() {304return this._prompts.value;305}306307private readonly _serverMetadata: CachedPrimitive<ServerMetadata, ServerMetadata | undefined>;308public get serverMetadata() {309return this._serverMetadata.value;310}311312public get trustedAtNonce() {313return this._primitiveCache.get(this.definition.id)?.trustedAtNonce;314}315316public set trustedAtNonce(nonce: string | undefined) {317this._primitiveCache.store(this.definition.id, { trustedAtNonce: nonce });318}319320private readonly _fullDefinitions: IObservable<{321server: McpServerDefinition | undefined;322collection: McpCollectionDefinition | undefined;323}>;324325public readonly cacheState = derived(reader => {326const currentNonce = () => this._fullDefinitions.read(reader)?.server?.cacheNonce;327const stateWhenServingFromCache = () => {328if (!this._tools.fromCache) {329return McpServerCacheState.Unknown;330}331332return currentNonce() === this._tools.fromCache.nonce ? McpServerCacheState.Cached : McpServerCacheState.Outdated;333};334335const fromServer = this._tools.fromServerPromise.read(reader);336const connectionState = this.connectionState.read(reader);337const isIdle = McpConnectionState.canBeStarted(connectionState.state) || !fromServer;338if (isIdle) {339return stateWhenServingFromCache();340}341342const fromServerResult = fromServer?.promiseResult.read(reader);343if (!fromServerResult) {344return this._tools.fromCache ? McpServerCacheState.RefreshingFromCached : McpServerCacheState.RefreshingFromUnknown;345}346347if (fromServerResult.error) {348return stateWhenServingFromCache();349}350351return fromServerResult.data?.nonce === currentNonce() ? McpServerCacheState.Live : McpServerCacheState.Outdated;352});353354private readonly _loggerId: string;355private readonly _logger: ILogger;356private _lastModeDebugged = false;357/** Count of running tool calls, used to detect if sampling is during an LM call */358public runningToolCalls = new Set<IMcpToolCallContext>();359360constructor(361public readonly collection: McpCollectionReference,362public readonly definition: McpDefinitionReference,363explicitRoots: URI[] | undefined,364private readonly _requiresExtensionActivation: boolean | undefined,365private readonly _primitiveCache: McpServerMetadataCache,366toolPrefix: string,367@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,368@IWorkspaceContextService workspacesService: IWorkspaceContextService,369@IExtensionService private readonly _extensionService: IExtensionService,370@ILoggerService private readonly _loggerService: ILoggerService,371@IOutputService private readonly _outputService: IOutputService,372@ITelemetryService private readonly _telemetryService: ITelemetryService,373@ICommandService private readonly _commandService: ICommandService,374@IInstantiationService private readonly _instantiationService: IInstantiationService,375@INotificationService private readonly _notificationService: INotificationService,376@IOpenerService private readonly _openerService: IOpenerService,377@IMcpSamplingService private readonly _samplingService: IMcpSamplingService,378@IMcpElicitationService private readonly _elicitationService: IMcpElicitationService,379@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,380) {381super();382383this._fullDefinitions = this._mcpRegistry.getServerDefinition(this.collection, this.definition);384this._loggerId = `mcpServer.${definition.id}`;385this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` }));386387const that = this;388this._register(this._instantiationService.createInstance(McpDevModeServerAttache, this, { get lastModeDebugged() { return that._lastModeDebugged; } }));389390// If the logger is disposed but not deregistered, then the disposed instance391// is reused and no-ops. todo@sandy081 this seems like a bug.392this._register(toDisposable(() => _loggerService.deregisterLogger(this._loggerId)));393394// 1. Reflect workspaces into the MCP roots395const workspaces = explicitRoots396? observableValue(this, explicitRoots.map(uri => ({ uri, name: basename(uri) })))397: observableFromEvent(398this,399workspacesService.onDidChangeWorkspaceFolders,400() => workspacesService.getWorkspace().folders,401);402403const workspacesWithCanonicalURIs = derived(reader => {404const folders = workspaces.read(reader);405return new ObservablePromise((async () => {406let uris = folders.map(f => f.uri);407try {408uris = await Promise.all(uris.map(u => this._remoteAuthorityResolverService.getCanonicalURI(u)));409} catch (error) {410this._logger.error(`Failed to resolve workspace folder URIs: ${error}`);411}412return uris.map((uri, i): MCP.Root => ({ uri: uri.toString(), name: folders[i].name }));413})());414}).recomputeInitiallyAndOnChange(this._store);415416this._register(autorun(reader => {417const cnx = this._connection.read(reader)?.handler.read(reader);418if (!cnx) {419return;420}421422const roots = workspacesWithCanonicalURIs.read(reader).promiseResult.read(reader);423if (roots?.data) {424cnx.roots = roots.data;425}426}));427428// 2. Populate this.tools when we connect to a server.429this._register(autorun(reader => {430const cnx = this._connection.read(reader);431const handler = cnx?.handler.read(reader);432if (handler) {433this.populateLiveData(handler, cnx?.definition.cacheNonce, reader.store);434} else if (this._tools) {435this.resetLiveData();436}437}));438439// 3. Publish tools440this._tools = new CachedPrimitive<readonly IMcpTool[], readonly IValidatedMcpTool[]>(441this.definition.id,442this._primitiveCache,443(entry) => entry.tools,444(entry) => entry.map(def => new McpTool(this, toolPrefix, def)).sort((a, b) => a.compare(b)),445[],446);447448// 4. Publish promtps449this._prompts = new CachedPrimitive<readonly IMcpPrompt[], readonly MCP.Prompt[]>(450this.definition.id,451this._primitiveCache,452(entry) => entry.prompts || [],453(entry) => entry.map(e => new McpPrompt(this, e)),454[],455);456457this._serverMetadata = new CachedPrimitive<ServerMetadata, ServerMetadata | undefined>(458this.definition.id,459this._primitiveCache,460(entry) => ({ serverName: entry.serverName, serverInstructions: entry.serverInstructions }),461(entry) => ({ serverName: entry?.serverName, serverInstructions: entry?.serverInstructions }),462undefined,463);464465this._capabilities.set(this._primitiveCache.get(this.definition.id)?.capabilities, undefined);466}467468public readDefinitions(): IObservable<{ server: McpServerDefinition | undefined; collection: McpCollectionDefinition | undefined }> {469return this._fullDefinitions;470}471472public showOutput(preserveFocus?: boolean) {473this._loggerService.setVisibility(this._loggerId, true);474return this._outputService.showChannel(this._loggerId, preserveFocus);475}476477public resources(token?: CancellationToken): AsyncIterable<IMcpResource[]> {478const cts = new CancellationTokenSource(token);479return new AsyncIterableProducer<IMcpResource[]>(async emitter => {480await McpServer.callOn(this, async (handler) => {481for await (const resource of handler.listResourcesIterable({}, cts.token)) {482emitter.emitOne(resource.map(r => new McpResource(this, r)));483if (cts.token.isCancellationRequested) {484return;485}486}487});488}, () => cts.dispose(true));489}490491public resourceTemplates(token?: CancellationToken): Promise<IMcpResourceTemplate[]> {492return McpServer.callOn(this, async (handler) => {493const templates = await handler.listResourceTemplates({}, token);494return templates.map(t => new McpResourceTemplate(this, t));495}, token);496}497498public start({ interaction, autoTrustChanges, promptType, debug }: IMcpServerStartOpts = {}): Promise<McpConnectionState> {499interaction?.participants.set(this.definition.id, { s: 'unknown' });500501return this._connectionSequencer.queue<McpConnectionState>(async () => {502const activationEvent = mcpActivationEvent(this.collection.id.slice(extensionMcpCollectionPrefix.length));503if (this._requiresExtensionActivation && !this._extensionService.activationEventIsDone(activationEvent)) {504await this._extensionService.activateByEvent(activationEvent);505await Promise.all(this._mcpRegistry.delegates.get()506.map(r => r.waitForInitialProviderPromises()));507// This can happen if the server was created from a cached MCP server seen508// from an extension, but then it wasn't registered when the extension activated.509if (this._store.isDisposed) {510return { state: McpConnectionState.Kind.Stopped };511}512}513514let connection = this._connection.get();515if (connection && McpConnectionState.canBeStarted(connection.state.get().state)) {516connection.dispose();517connection = undefined;518this._connection.set(connection, undefined);519}520521if (!connection) {522this._lastModeDebugged = !!debug;523const that = this;524connection = await this._mcpRegistry.resolveConnection({525interaction,526autoTrustChanges,527promptType,528trustNonceBearer: {529get trustedAtNonce() { return that.trustedAtNonce; },530set trustedAtNonce(nonce: string | undefined) { that.trustedAtNonce = nonce; }531},532logger: this._logger,533collectionRef: this.collection,534definitionRef: this.definition,535debug,536});537if (!connection) {538return { state: McpConnectionState.Kind.Stopped };539}540541if (this._store.isDisposed) {542connection.dispose();543return { state: McpConnectionState.Kind.Stopped };544}545546this._connection.set(connection, undefined);547}548549if (connection.definition.devMode) {550this.showOutput();551}552553const start = Date.now();554const state = await connection.start({555createMessageRequestHandler: params => this._samplingService.sample({556isDuringToolCall: this.runningToolCalls.size > 0,557server: this,558params,559}).then(r => r.sample),560elicitationRequestHandler: req => {561const serverInfo = connection.handler.get()?.serverInfo;562if (serverInfo) {563this._telemetryService.publicLog2<ElicitationTelemetryData, ElicitationTelemetryClassification>('mcp.elicitationRequested', {564serverName: serverInfo.name,565serverVersion: serverInfo.version,566});567}568569return this._elicitationService.elicit(this, Iterable.first(this.runningToolCalls), req, CancellationToken.None);570}571});572573this._telemetryService.publicLog2<ServerBootState, ServerBootStateClassification>('mcp/serverBootState', {574state: McpConnectionState.toKindString(state.state),575time: Date.now() - start,576});577578if (state.state === McpConnectionState.Kind.Error) {579this.showInteractiveError(connection, state, debug);580}581582return state;583}).finally(() => {584interaction?.participants.set(this.definition.id, { s: 'resolved' });585});586}587588private showInteractiveError(cnx: IMcpServerConnection, error: McpConnectionState.Error, debug?: boolean) {589if (error.code === 'ENOENT' && cnx.launchDefinition.type === McpServerTransportType.Stdio) {590let docsLink: string | undefined;591switch (cnx.launchDefinition.command) {592case 'uvx':593docsLink = `https://aka.ms/vscode-mcp-install/uvx`;594break;595case 'npx':596docsLink = `https://aka.ms/vscode-mcp-install/npx`;597break;598case 'dnx':599docsLink = `https://aka.ms/vscode-mcp-install/dnx`;600break;601case 'dotnet':602docsLink = `https://aka.ms/vscode-mcp-install/dotnet`;603break;604}605606const options: IPromptChoice[] = [{607label: localize('mcp.command.showOutput', "Show Output"),608run: () => this.showOutput(),609}];610611if (cnx.definition.devMode?.debug?.type === 'debugpy' && debug) {612this._notificationService.prompt(Severity.Error, localize('mcpDebugPyHelp', 'The command "{0}" was not found. You can specify the path to debugpy in the `dev.debug.debugpyPath` option.', cnx.launchDefinition.command, cnx.definition.label), [...options, {613label: localize('mcpViewDocs', 'View Docs'),614run: () => this._openerService.open(URI.parse('https://aka.ms/vscode-mcp-install/debugpy')),615}]);616return;617}618619if (docsLink) {620options.push({621label: localize('mcpServerInstall', 'Install {0}', cnx.launchDefinition.command),622run: () => this._openerService.open(URI.parse(docsLink)),623});624}625626this._notificationService.prompt(Severity.Error, localize('mcpServerNotFound', 'The command "{0}" needed to run {1} was not found.', cnx.launchDefinition.command, cnx.definition.label), options);627} else {628this._notificationService.warn(localize('mcpServerError', 'The MCP server {0} could not be started: {1}', cnx.definition.label, error.message));629}630}631632public stop(): Promise<void> {633return this._connection.get()?.stop() || Promise.resolve();634}635636private resetLiveData() {637transaction(tx => {638this._tools.fromServerPromise.set(undefined, tx);639this._prompts.fromServerPromise.set(undefined, tx);640});641}642643private async _normalizeTool(originalTool: MCP.Tool): Promise<IValidatedMcpTool | { error: string[] }> {644const tool: IValidatedMcpTool = { ...originalTool, serverToolName: originalTool.name };645if (!tool.description) {646// Ensure a description is provided for each tool, #243919647this._logger.warn(`Tool ${tool.name} does not have a description. Tools must be accurately described to be called`);648tool.description = '<empty>';649}650651if (toolInvalidCharRe.test(tool.name)) {652this._logger.warn(`Tool ${JSON.stringify(tool.name)} is invalid. Tools names may only contain [a-z0-9_-]`);653tool.name = tool.name.replace(toolInvalidCharRe, '_');654}655656type JsonDiagnostic = { message: string; range: { line: number; character: number }[] };657658let diagnostics: JsonDiagnostic[] = [];659const toolJson = JSON.stringify(tool.inputSchema);660try {661const schemaUri = URI.parse('https://json-schema.org/draft-07/schema');662diagnostics = await this._commandService.executeCommand<JsonDiagnostic[]>('json.validate', schemaUri, toolJson) || [];663} catch (e) {664// ignored (error in json extension?);665}666667if (!diagnostics.length) {668return tool;669}670671// because it's all one line from JSON.stringify, we can treat characters as offsets.672const tree = json.parseTree(toolJson);673const messages = diagnostics.map(d => {674const node = json.findNodeAtOffset(tree, d.range[0].character);675const path = node && `/${json.getNodePath(node).join('/')}`;676return d.message + (path ? ` (at ${path})` : '');677});678679return { error: messages };680}681682private async _getValidatedTools(handler: McpServerRequestHandler, tools: MCP.Tool[]): Promise<IValidatedMcpTool[]> {683let error = '';684685const validations = await Promise.all(tools.map(t => this._normalizeTool(t)));686const validated: IValidatedMcpTool[] = [];687for (const [i, result] of validations.entries()) {688if ('error' in result) {689error += localize('mcpBadSchema.tool', 'Tool `{0}` has invalid JSON parameters:', tools[i].name) + '\n';690for (const message of result.error) {691error += `\t- ${message}\n`;692}693error += `\t- Schema: ${JSON.stringify(tools[i].inputSchema)}\n\n`;694} else {695validated.push(result);696}697}698699if (error) {700handler.logger.warn(`${tools.length - validated.length} tools have invalid JSON schemas and will be omitted`);701warnInvalidTools(this._instantiationService, this.definition.label, error);702}703704return validated;705}706707private populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) {708const cts = new CancellationTokenSource();709store.add(toDisposable(() => cts.dispose(true)));710711// todo: add more than just tools here712713const updateTools = (tx: ITransaction | undefined) => {714const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]);715const toolPromiseSafe = toolPromise.then(async tools => {716handler.logger.info(`Discovered ${tools.length} tools`);717return { data: await this._getValidatedTools(handler, tools), nonce: cacheNonce };718});719this._tools.fromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx);720return toolPromiseSafe;721};722723const updatePrompts = (tx: ITransaction | undefined) => {724const promptsPromise = handler.capabilities.prompts ? handler.listPrompts({}, cts.token) : Promise.resolve([]);725const promptsPromiseSafe = promptsPromise.then(data => ({ data, nonce: cacheNonce }));726this._prompts.fromServerPromise.set(new ObservablePromise(promptsPromiseSafe), tx);727return promptsPromiseSafe;728};729730store.add(handler.onDidChangeToolList(() => {731handler.logger.info('Tool list changed, refreshing tools...');732updateTools(undefined);733}));734735store.add(handler.onDidChangePromptList(() => {736handler.logger.info('Prompts list changed, refreshing prompts...');737updatePrompts(undefined);738}));739740const metadataPromise = new ObservablePromise(Promise.resolve({741nonce: cacheNonce,742data: {743serverName: handler.serverInfo.title || handler.serverInfo.name,744serverInstructions: handler.serverInstructions,745},746}));747748transaction(tx => {749// note: all update* methods must use tx synchronously750const capabilities = encodeCapabilities(handler.capabilities);751this._primitiveCache.store(this.definition.id, {752serverName: handler.serverInfo.title || handler.serverInfo.name,753serverInstructions: handler.serverInstructions,754capabilities,755});756757this._capabilities.set(capabilities, tx);758759this._serverMetadata.fromServerPromise.set(metadataPromise, tx);760761Promise.all([updateTools(tx), updatePrompts(tx)]).then(([{ data: tools }, { data: prompts }]) => {762this._primitiveCache.store(this.definition.id, {763nonce: cacheNonce,764tools,765prompts,766capabilities,767});768769this._telemetryService.publicLog2<ServerBootData, ServerBootClassification>('mcp/serverBoot', {770supportsLogging: !!handler.capabilities.logging,771supportsPrompts: !!handler.capabilities.prompts,772supportsResources: !!handler.capabilities.resources,773toolCount: tools.length,774serverName: handler.serverInfo.name,775serverVersion: handler.serverInfo.version,776});777});778});779}780}781782class McpPrompt implements IMcpPrompt {783readonly id: string;784readonly name: string;785readonly description?: string;786readonly title?: string;787readonly arguments: readonly MCP.PromptArgument[];788789constructor(790private readonly _server: McpServer,791private readonly _definition: MCP.Prompt,792) {793this.id = mcpPromptReplaceSpecialChars(this._server.definition.label + '.' + _definition.name);794this.name = _definition.name;795this.title = _definition.title;796this.description = _definition.description;797this.arguments = _definition.arguments || [];798}799800async resolve(args: Record<string, string>, token?: CancellationToken): Promise<IMcpPromptMessage[]> {801const result = await McpServer.callOn(this._server, h => h.getPrompt({ name: this._definition.name, arguments: args }, token), token);802return result.messages;803}804805async complete(argument: string, prefix: string, alreadyResolved: Record<string, string>, token?: CancellationToken): Promise<string[]> {806const result = await McpServer.callOn(this._server, h => h.complete({807ref: { type: 'ref/prompt', name: this._definition.name },808argument: { name: argument, value: prefix },809context: { arguments: alreadyResolved },810}, token), token);811return result.completion.values;812}813}814815function encodeCapabilities(cap: MCP.ServerCapabilities): McpCapability {816let out = 0;817if (cap.logging) { out |= McpCapability.Logging; }818if (cap.completions) { out |= McpCapability.Completions; }819if (cap.prompts) {820out |= McpCapability.Prompts;821if (cap.prompts.listChanged) {822out |= McpCapability.PromptsListChanged;823}824}825if (cap.resources) {826out |= McpCapability.Resources;827if (cap.resources.subscribe) {828out |= McpCapability.ResourcesSubscribe;829}830if (cap.resources.listChanged) {831out |= McpCapability.ResourcesListChanged;832}833}834if (cap.tools) {835out |= McpCapability.Tools;836if (cap.tools.listChanged) {837out |= McpCapability.ToolsListChanged;838}839}840return out;841}842843export class McpTool implements IMcpTool {844845readonly id: string;846readonly referenceName: string;847848public get definition(): MCP.Tool { return this._definition; }849850constructor(851private readonly _server: McpServer,852idPrefix: string,853private readonly _definition: IValidatedMcpTool,854) {855this.referenceName = _definition.name.replaceAll('.', '_');856this.id = (idPrefix + _definition.name).replaceAll('.', '_').slice(0, McpToolName.MaxLength);857}858859async call(params: Record<string, unknown>, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {860// serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it.861const name = this._definition.serverToolName ?? this._definition.name;862if (context) { this._server.runningToolCalls.add(context); }863try {864return await McpServer.callOn(this._server, h => h.callTool({ name, arguments: params }, token), token);865} finally {866if (context) { this._server.runningToolCalls.delete(context); }867}868}869870async callWithProgress(params: Record<string, unknown>, progress: ToolProgress, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {871if (context) { this._server.runningToolCalls.add(context); }872try {873return await this._callWithProgress(params, progress, token);874} finally {875if (context) { this._server.runningToolCalls.delete(context); }876}877}878879_callWithProgress(params: Record<string, unknown>, progress: ToolProgress, token?: CancellationToken, allowRetry = true): Promise<MCP.CallToolResult> {880// serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it.881const name = this._definition.serverToolName ?? this._definition.name;882const progressToken = generateUuid();883884return McpServer.callOn(this._server, h => {885let lastProgressN = 0;886const listener = h.onDidReceiveProgressNotification((e) => {887if (e.params.progressToken === progressToken) {888progress.report({889message: e.params.message,890increment: e.params.progress - lastProgressN,891total: e.params.total,892});893lastProgressN = e.params.progress;894}895});896897return h.callTool({ name, arguments: params, _meta: { progressToken } }, token)898.finally(() => listener.dispose())899.catch(err => {900const state = this._server.connectionState.get();901if (allowRetry && state.state === McpConnectionState.Kind.Error && state.shouldRetry) {902return this._callWithProgress(params, progress, token, false);903} else {904throw err;905}906});907}, token);908}909910compare(other: IMcpTool): number {911return this._definition.name.localeCompare(other.definition.name);912}913}914915function warnInvalidTools(instaService: IInstantiationService, serverName: string, errorText: string) {916instaService.invokeFunction((accessor) => {917const notificationService = accessor.get(INotificationService);918const editorService = accessor.get(IEditorService);919notificationService.notify({920severity: Severity.Warning,921message: localize('mcpBadSchema', 'MCP server `{0}` has tools with invalid parameters which will be omitted.', serverName),922actions: {923primary: [{924class: undefined,925enabled: true,926id: 'mcpBadSchema.show',927tooltip: '',928label: localize('mcpBadSchema.show', 'Show'),929run: () => {930editorService.openEditor({931resource: undefined,932contents: errorText,933});934}935}]936}937});938});939}940941class McpResource implements IMcpResource {942readonly uri: URI;943readonly mcpUri: string;944readonly name: string;945readonly description: string | undefined;946readonly mimeType: string | undefined;947readonly sizeInBytes: number | undefined;948readonly title: string | undefined;949950constructor(951server: McpServer,952original: MCP.Resource,953) {954this.mcpUri = original.uri;955this.title = original.title;956this.uri = McpResourceURI.fromServer(server.definition, original.uri);957this.name = original.name;958this.description = original.description;959this.mimeType = original.mimeType;960this.sizeInBytes = original.size;961}962}963964class McpResourceTemplate implements IMcpResourceTemplate {965readonly name: string;966readonly title?: string | undefined;967readonly description?: string;968readonly mimeType?: string;969readonly template: UriTemplate;970971constructor(972private readonly _server: McpServer,973private readonly _definition: MCP.ResourceTemplate,974) {975this.name = _definition.name;976this.description = _definition.description;977this.mimeType = _definition.mimeType;978this.title = _definition.title;979this.template = UriTemplate.parse(_definition.uriTemplate);980}981982public resolveURI(vars: Record<string, unknown>): URI {983const serverUri = this.template.resolve(vars);984return McpResourceURI.fromServer(this._server.definition, serverUri);985}986987async complete(templatePart: string, prefix: string, alreadyResolved: Record<string, string | string[]>, token?: CancellationToken): Promise<string[]> {988const result = await McpServer.callOn(this._server, h => h.complete({989ref: { type: 'ref/resource', uri: this._definition.uriTemplate },990argument: { name: templatePart, value: prefix },991context: {992arguments: mapValues(alreadyResolved, v => Array.isArray(v) ? v.join('/') : v),993},994}, token), token);995return result.completion.values;996}997}9989991000