Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpServer.ts
5282 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 { normalizeDriveLetter } from '../../../../base/common/labels.js';10import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';11import { LRUCache } from '../../../../base/common/map.js';12import { Schemas } from '../../../../base/common/network.js';13import { mapValues } from '../../../../base/common/objects.js';14import { autorun, autorunSelfDisposable, derived, disposableObservableValue, IDerivedReader, IObservable, IReader, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js';15import { basename } from '../../../../base/common/resources.js';16import { URI } from '../../../../base/common/uri.js';17import { createURITransformer } from '../../../../base/common/uriTransformer.js';18import { generateUuid } from '../../../../base/common/uuid.js';19import { localize } from '../../../../nls.js';20import { ICommandService } from '../../../../platform/commands/common/commands.js';21import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';22import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js';23import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js';24import { IOpenerService } from '../../../../platform/opener/common/opener.js';25import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';26import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';27import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';28import { IEditorService } from '../../../services/editor/common/editorService.js';29import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';30import { IExtensionService } from '../../../services/extensions/common/extensions.js';31import { IOutputService } from '../../../services/output/common/output.js';32import { ToolProgress } from '../../chat/common/tools/languageModelToolsService.js';33import { mcpActivationEvent } from './mcpConfiguration.js';34import { McpDevModeServerAttache } from './mcpDevMode.js';35import { McpIcons, parseAndValidateMcpIcon, StoredMcpIcons } from './mcpIcons.js';36import { IMcpRegistry } from './mcpRegistryTypes.js';37import { McpServerRequestHandler } from './mcpServerRequestHandler.js';38import { McpTaskManager } from './mcpTaskManager.js';39import { ElicitationKind, 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, McpToolVisibility, MpcResponseError, UserInteractionRequiredError } from './mcpTypes.js';40import { MCP } from './modelContextProtocol.js';41import { McpApps } from './modelContextProtocolApps.js';42import { UriTemplate } from './uriTemplate.js';4344type ServerBootData = {45supportsLogging: boolean;46supportsPrompts: boolean;47supportsResources: boolean;48toolCount: number;49serverName: string;50serverVersion: string;51};52type ServerBootClassification = {53owner: 'connor4312';54comment: 'Details the capabilities of the MCP server';55supportsLogging: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports logging' };56supportsPrompts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports prompts' };57supportsResources: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports resource' };58toolCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tools the server advertises' };59serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };60serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };61};6263type ElicitationTelemetryData = {64serverName: string;65serverVersion: string;66};6768type ElicitationTelemetryClassification = {69owner: 'connor4312';70comment: 'Triggered when elictation is requested';71serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };72serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };73};7475export type McpServerInstallData = {76serverName: string;77source: 'gallery' | 'local';78scope: string;79success: boolean;80error?: string;81hasInputs: boolean;82};8384export type McpServerInstallClassification = {85owner: 'connor4312';86comment: 'MCP server installation event tracking';87serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server being installed' };88source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation source (gallery or local)' };89scope: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation scope (user, workspace, etc.)' };90success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether installation succeeded' };91error?: { classification: 'CallstackOrException'; purpose: 'FeatureInsight'; comment: 'Error message if installation failed' };92hasInputs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server requires input configuration' };93};9495type ServerBootState = {96state: string;97time: number;98};99type ServerBootStateClassification = {100owner: 'connor4312';101comment: 'Details the capabilities of the MCP server';102state: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The server outcome' };103time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Duration in milliseconds to reach that state' };104};105106type StoredMcpPrompt = MCP.Prompt & { _icons: StoredMcpIcons };107108interface IToolCacheEntry {109readonly serverName: string | undefined;110readonly serverInstructions: string | undefined;111readonly serverIcons: StoredMcpIcons;112113readonly trustedAtNonce: string | undefined;114115readonly nonce: string | undefined;116/** Cached tools so we can show what's available before it's started */117readonly tools: readonly ValidatedMcpTool[];118/** Cached prompts */119readonly prompts: readonly StoredMcpPrompt[] | undefined;120/** Cached capabilities */121readonly capabilities: McpCapability | undefined;122}123124const emptyToolEntry: IToolCacheEntry = {125serverName: undefined,126serverIcons: [],127serverInstructions: undefined,128trustedAtNonce: undefined,129nonce: undefined,130tools: [],131prompts: undefined,132capabilities: undefined,133};134135interface IServerCacheEntry {136readonly servers: readonly McpServerDefinition.Serialized[];137}138139const toolInvalidCharRe = /[^a-z0-9_-]/gi;140141export class McpServerMetadataCache extends Disposable {142private didChange = false;143private readonly cache = new LRUCache<string, IToolCacheEntry>(128);144private readonly extensionServers = new Map</* collection ID */string, IServerCacheEntry>();145146constructor(147scope: StorageScope,148@IStorageService storageService: IStorageService,149) {150super();151152type StoredType = {153extensionServers: [string, IServerCacheEntry][];154serverTools: [string, IToolCacheEntry][];155};156157const storageKey = 'mcpToolCache';158this._register(storageService.onWillSaveState(() => {159if (this.didChange) {160storageService.store(storageKey, {161extensionServers: [...this.extensionServers],162serverTools: this.cache.toJSON(),163} satisfies StoredType, scope, StorageTarget.MACHINE);164this.didChange = false;165}166}));167168try {169const cached: StoredType | undefined = storageService.getObject(storageKey, scope);170this.extensionServers = new Map(cached?.extensionServers ?? []);171cached?.serverTools?.forEach(([k, v]) => this.cache.set(k, v));172} catch {173// ignored174}175}176177/** Resets the cache for primitives and extension servers */178reset() {179this.cache.clear();180this.extensionServers.clear();181this.didChange = true;182}183184/** Gets cached primitives for a server (used before a server is running) */185get(definitionId: string) {186return this.cache.get(definitionId);187}188189/** Sets cached primitives for a server */190store(definitionId: string, entry: Partial<IToolCacheEntry>): void {191const prev = this.get(definitionId) || emptyToolEntry;192this.cache.set(definitionId, { ...prev, ...entry });193this.didChange = true;194}195196/** Gets cached servers for a collection (used for extensions, before the extension activates) */197getServers(collectionId: string) {198return this.extensionServers.get(collectionId);199}200201/** Sets cached servers for a collection */202storeServers(collectionId: string, entry: IServerCacheEntry | undefined): void {203if (entry) {204this.extensionServers.set(collectionId, entry);205} else {206this.extensionServers.delete(collectionId);207}208this.didChange = true;209}210}211212type ValidatedMcpTool = MCP.Tool & {213_icons: StoredMcpIcons;214215/**216* Tool name as published by the MCP server. This may217* be different than the one in {@link definition} due to name normalization218* in {@link McpServer._getValidatedTools}.219*/220serverToolName: string;221222/**223* Visibility of the tool, parsed from `_meta.ui.visibility`.224* Defaults to Model | App if not specified.225*/226visibility: McpToolVisibility;227228/**229* UI resource URI if this tool has an associated MCP App UI.230* Parsed from `_meta.ui.resourceUri`.231*/232uiResourceUri?: string;233};234235interface StoredServerMetadata {236readonly serverName: string | undefined;237readonly serverInstructions: string | undefined;238readonly serverIcons: StoredMcpIcons | undefined;239}240241interface ServerMetadata {242readonly serverName: string | undefined;243readonly serverInstructions: string | undefined;244readonly icons: IMcpIcons;245}246247class CachedPrimitive<T, C> {248/**249* @param _definitionId Server definition ID250* @param _cache Metadata cache instance251* @param _fromStaticDefinition Static definition that came with the server.252* This should ONLY have a value if it should be used instead of whatever253* is currently in the cache.254* @param _fromCache Pull the value from the cache entry.255* @param _toT Transform the value to the observable type.256* @param defaultValue Default value if no cache entry.257*/258constructor(259private readonly _definitionId: string,260private readonly _cache: McpServerMetadataCache,261private readonly _fromStaticDefinition: IObservable<C | undefined> | undefined,262private readonly _fromCache: (entry: IToolCacheEntry) => C,263private readonly _toT: (values: C, reader: IDerivedReader<void>) => T,264private readonly defaultValue: C,265) { }266267public get fromCache(): { nonce: string | undefined; data: C } | undefined {268const c = this._cache.get(this._definitionId);269return c ? { data: this._fromCache(c), nonce: c.nonce } : undefined;270}271272public hasStaticDefinition(reader: IReader | undefined) {273return !!this._fromStaticDefinition?.read(reader);274}275276public readonly fromServerPromise = observableValue<ObservablePromise<{277readonly data: C;278readonly nonce: string | undefined;279}> | undefined>(this, undefined);280281private readonly fromServer = derived(reader => this.fromServerPromise.read(reader)?.promiseResult.read(reader)?.data);282283public readonly value: IObservable<T> = derived(reader => {284const serverTools = this.fromServer.read(reader);285const definitions = serverTools?.data ?? this._fromStaticDefinition?.read(reader) ?? this.fromCache?.data ?? this.defaultValue;286return this._toT(definitions, reader);287});288}289290export class McpServer extends Disposable implements IMcpServer {291/** Shared task manager that survives reconnections */292private readonly _taskManager = this._register(new McpTaskManager());293294/**295* Helper function to call the function on the handler once it's online. The296* connection started if it is not already.297*/298public static async callOn<R>(server: IMcpServer, fn: (handler: McpServerRequestHandler) => Promise<R>, token: CancellationToken = CancellationToken.None): Promise<R> {299await server.start({ promptType: 'all-untrusted' }); // idempotent300301let ranOnce = false;302let d: IDisposable;303304const callPromise = new Promise<R>((resolve, reject) => {305306d = autorun(reader => {307const connection = server.connection.read(reader);308if (!connection || ranOnce) {309return;310}311312const handler = connection.handler.read(reader);313if (!handler) {314const state = connection.state.read(reader);315if (state.state === McpConnectionState.Kind.Error) {316reject(new McpConnectionFailedError(`MCP server could not be started: ${state.message}`));317return;318} else if (state.state === McpConnectionState.Kind.Stopped) {319reject(new McpConnectionFailedError('MCP server has stopped'));320return;321} else {322// keep waiting for handler323return;324}325}326327resolve(fn(handler));328ranOnce = true; // aggressive prevent multiple racey calls, don't dispose because autorun is sync329});330});331332return raceCancellationError(callPromise, token).finally(() => d.dispose());333}334335public readonly collection: McpCollectionReference;336private readonly _connectionSequencer = new Sequencer();337private readonly _connection = this._register(disposableObservableValue<IMcpServerConnection | undefined>(this, undefined));338339public readonly connection = this._connection;340public readonly connectionState: IObservable<McpConnectionState> = derived(reader => this._connection.read(reader)?.state.read(reader) ?? { state: McpConnectionState.Kind.Stopped });341342343private readonly _capabilities: CachedPrimitive<number | undefined, number | undefined>;344public get capabilities() {345return this._capabilities.value;346}347348private readonly _tools: CachedPrimitive<readonly IMcpTool[], readonly ValidatedMcpTool[]>;349public get tools() {350return this._tools.value;351}352353private readonly _prompts: CachedPrimitive<readonly IMcpPrompt[], readonly StoredMcpPrompt[]>;354public get prompts() {355return this._prompts.value;356}357358private readonly _serverMetadata: CachedPrimitive<ServerMetadata, StoredServerMetadata | undefined>;359public get serverMetadata() {360return this._serverMetadata.value;361}362363public get trustedAtNonce() {364return this._primitiveCache.get(this.definition.id)?.trustedAtNonce;365}366367public set trustedAtNonce(nonce: string | undefined) {368this._primitiveCache.store(this.definition.id, { trustedAtNonce: nonce });369}370371private readonly _fullDefinitions: IObservable<{372server: McpServerDefinition | undefined;373collection: McpCollectionDefinition | undefined;374}>;375376public readonly cacheState = derived(reader => {377const currentNonce = () => this._fullDefinitions.read(reader)?.server?.cacheNonce;378const stateWhenServingFromCache = () => {379if (this._tools.hasStaticDefinition(reader)) {380return McpServerCacheState.Cached;381}382383if (!this._tools.fromCache) {384return McpServerCacheState.Unknown;385}386387return currentNonce() === this._tools.fromCache.nonce ? McpServerCacheState.Cached : McpServerCacheState.Outdated;388};389390const fromServer = this._tools.fromServerPromise.read(reader);391const connectionState = this.connectionState.read(reader);392const isIdle = McpConnectionState.canBeStarted(connectionState.state) || !fromServer;393if (isIdle) {394return stateWhenServingFromCache();395}396397const fromServerResult = fromServer?.promiseResult.read(reader);398if (!fromServerResult) {399return this._tools.fromCache ? McpServerCacheState.RefreshingFromCached : McpServerCacheState.RefreshingFromUnknown;400}401402if (fromServerResult.error) {403return stateWhenServingFromCache();404}405406return fromServerResult.data?.nonce === currentNonce() ? McpServerCacheState.Live : McpServerCacheState.Outdated;407});408409public get logger(): ILogger {410return this._logger;411}412413private readonly _loggerId: string;414private readonly _logger: ILogger;415private _lastModeDebugged = false;416/** Count of running tool calls, used to detect if sampling is during an LM call */417public runningToolCalls = new Set<IMcpToolCallContext>();418419constructor(420initialCollection: McpCollectionDefinition,421public readonly definition: McpDefinitionReference,422explicitRoots: URI[] | undefined,423private readonly _requiresExtensionActivation: boolean | undefined,424private readonly _primitiveCache: McpServerMetadataCache,425toolPrefix: string,426@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,427@IWorkspaceContextService workspacesService: IWorkspaceContextService,428@IExtensionService private readonly _extensionService: IExtensionService,429@ILoggerService private readonly _loggerService: ILoggerService,430@IOutputService private readonly _outputService: IOutputService,431@ITelemetryService private readonly _telemetryService: ITelemetryService,432@ICommandService private readonly _commandService: ICommandService,433@IInstantiationService private readonly _instantiationService: IInstantiationService,434@INotificationService private readonly _notificationService: INotificationService,435@IOpenerService private readonly _openerService: IOpenerService,436@IMcpSamplingService private readonly _samplingService: IMcpSamplingService,437@IMcpElicitationService private readonly _elicitationService: IMcpElicitationService,438@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,439) {440super();441442this.collection = initialCollection;443this._fullDefinitions = this._mcpRegistry.getServerDefinition(this.collection, this.definition);444this._loggerId = `mcpServer.${definition.id}`;445this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` }));446447const that = this;448this._register(this._instantiationService.createInstance(McpDevModeServerAttache, this, { get lastModeDebugged() { return that._lastModeDebugged; } }));449450// If the logger is disposed but not deregistered, then the disposed instance451// is reused and no-ops. todo@sandy081 this seems like a bug.452this._register(toDisposable(() => _loggerService.deregisterLogger(this._loggerId)));453454// 1. Reflect workspaces into the MCP roots455const workspaces = explicitRoots456? observableValue(this, explicitRoots.map(uri => ({ uri, name: basename(uri) })))457: observableFromEvent(458this,459workspacesService.onDidChangeWorkspaceFolders,460() => workspacesService.getWorkspace().folders,461);462463const uriTransformer = environmentService.remoteAuthority ? createURITransformer(environmentService.remoteAuthority) : undefined;464465this._register(autorun(reader => {466const cnx = this._connection.read(reader)?.handler.read(reader);467if (!cnx) {468return;469}470471cnx.roots = workspaces.read(reader)472.filter(w => w.uri.authority === (initialCollection.remoteAuthority || ''))473.map(w => {474let uri = URI.from(uriTransformer?.transformIncoming(w.uri) ?? w.uri);475if (uri.scheme === Schemas.file) { // #271812476uri = URI.file(normalizeDriveLetter(uri.fsPath, true));477}478479return { name: w.name, uri: uri.toString() };480});481}));482483// 2. Populate this.tools when we connect to a server.484this._register(autorun(reader => {485const cnx = this._connection.read(reader);486const handler = cnx?.handler.read(reader);487if (handler) {488this._populateLiveData(handler, cnx?.definition.cacheNonce, reader.store);489} else if (this._tools) {490this.resetLiveData();491}492}));493494const staticMetadata = derived(reader => {495const def = this._fullDefinitions.read(reader).server;496return def && def.cacheNonce !== this._tools.fromCache?.nonce ? def.staticMetadata : undefined;497});498499// 3. Publish tools500this._tools = new CachedPrimitive<readonly IMcpTool[], readonly ValidatedMcpTool[]>(501this.definition.id,502this._primitiveCache,503staticMetadata504.map(m => {505const tools = m?.tools?.filter(t => t.availability === McpServerStaticToolAvailability.Initial).map(t => t.definition);506return tools?.length ? new ObservablePromise(this._getValidatedTools(tools)) : undefined;507})508.map((o, reader) => o?.promiseResult.read(reader)?.data),509(entry) => entry.tools,510(entry) => entry.map(def => this._instantiationService.createInstance(McpTool, this, toolPrefix, def)).sort((a, b) => a.compare(b)),511[],512);513514// 4. Publish prompts515this._prompts = new CachedPrimitive<readonly IMcpPrompt[], readonly StoredMcpPrompt[]>(516this.definition.id,517this._primitiveCache,518undefined,519(entry) => entry.prompts || [],520(entry) => entry.map(e => new McpPrompt(this, e)),521[],522);523524this._serverMetadata = new CachedPrimitive<ServerMetadata, StoredServerMetadata | undefined>(525this.definition.id,526this._primitiveCache,527staticMetadata.map(m => m ? this._toStoredMetadata(m?.serverInfo, m?.instructions) : undefined),528(entry) => ({ serverName: entry.serverName, serverInstructions: entry.serverInstructions, serverIcons: entry.serverIcons }),529(entry) => ({ serverName: entry?.serverName, serverInstructions: entry?.serverInstructions, icons: McpIcons.fromStored(entry?.serverIcons) }),530undefined,531);532533this._capabilities = new CachedPrimitive<number | undefined, number | undefined>(534this.definition.id,535this._primitiveCache,536staticMetadata.map(m => m?.capabilities !== undefined ? encodeCapabilities(m.capabilities) : undefined),537(entry) => entry.capabilities,538(entry) => entry,539undefined,540);541}542543public readDefinitions(): IObservable<{ server: McpServerDefinition | undefined; collection: McpCollectionDefinition | undefined }> {544return this._fullDefinitions;545}546547public showOutput(preserveFocus?: boolean) {548this._loggerService.setVisibility(this._loggerId, true);549return this._outputService.showChannel(this._loggerId, preserveFocus);550}551552public resources(token?: CancellationToken): AsyncIterable<IMcpResource[]> {553const cts = new CancellationTokenSource(token);554return new AsyncIterableProducer<IMcpResource[]>(async emitter => {555await McpServer.callOn(this, async (handler) => {556for await (const resource of handler.listResourcesIterable({}, cts.token)) {557emitter.emitOne(resource.map(r => new McpResource(this, r, McpIcons.fromParsed(this._parseIcons(r)))));558if (cts.token.isCancellationRequested) {559return;560}561}562});563}, () => cts.dispose(true));564}565566public resourceTemplates(token?: CancellationToken): Promise<IMcpResourceTemplate[]> {567return McpServer.callOn(this, async (handler) => {568const templates = await handler.listResourceTemplates({}, token);569return templates.map(t => new McpResourceTemplate(this, t, McpIcons.fromParsed(this._parseIcons(t))));570}, token);571}572573public start({ interaction, autoTrustChanges, promptType, debug, errorOnUserInteraction }: IMcpServerStartOpts = {}): Promise<McpConnectionState> {574interaction?.participants.set(this.definition.id, { s: 'unknown' });575576return this._connectionSequencer.queue<McpConnectionState>(async () => {577const activationEvent = mcpActivationEvent(this.collection.id.slice(extensionMcpCollectionPrefix.length));578if (this._requiresExtensionActivation && !this._extensionService.activationEventIsDone(activationEvent)) {579await this._extensionService.activateByEvent(activationEvent);580await Promise.all(this._mcpRegistry.delegates.get()581.map(r => r.waitForInitialProviderPromises()));582// This can happen if the server was created from a cached MCP server seen583// from an extension, but then it wasn't registered when the extension activated.584if (this._store.isDisposed) {585return { state: McpConnectionState.Kind.Stopped };586}587}588589let connection = this._connection.get();590if (connection && McpConnectionState.canBeStarted(connection.state.get().state)) {591connection.dispose();592connection = undefined;593this._connection.set(connection, undefined);594}595596if (!connection) {597this._lastModeDebugged = !!debug;598const that = this;599connection = await this._mcpRegistry.resolveConnection({600interaction,601autoTrustChanges,602promptType,603trustNonceBearer: {604get trustedAtNonce() { return that.trustedAtNonce; },605set trustedAtNonce(nonce: string | undefined) { that.trustedAtNonce = nonce; }606},607logger: this._logger,608collectionRef: this.collection,609definitionRef: this.definition,610debug,611errorOnUserInteraction,612taskManager: this._taskManager,613});614if (!connection) {615return { state: McpConnectionState.Kind.Stopped };616}617618if (this._store.isDisposed) {619connection.dispose();620return { state: McpConnectionState.Kind.Stopped };621}622623this._connection.set(connection, undefined);624625if (connection.definition.devMode) {626this.showOutput();627}628}629630const start = Date.now();631let state = await connection.start({632createMessageRequestHandler: (params, token) => this._samplingService.sample({633isDuringToolCall: this.runningToolCalls.size > 0,634server: this,635params,636}, token).then(r => r.sample),637elicitationRequestHandler: async (req, token) => {638const serverInfo = connection.handler.get()?.serverInfo;639if (serverInfo) {640this._telemetryService.publicLog2<ElicitationTelemetryData, ElicitationTelemetryClassification>('mcp.elicitationRequested', {641serverName: serverInfo.name,642serverVersion: serverInfo.version,643});644}645646const r = await this._elicitationService.elicit(this, Iterable.first(this.runningToolCalls), req, token || CancellationToken.None);647r.dispose();648return r.value;649}650});651652this._telemetryService.publicLog2<ServerBootState, ServerBootStateClassification>('mcp/serverBootState', {653state: McpConnectionState.toKindString(state.state),654time: Date.now() - start,655});656657if (state.state === McpConnectionState.Kind.Error) {658this.showInteractiveError(connection, state, debug);659}660661// MCP servers that need auth can 'start' but will stop with an interaction-needed662// error they first make a request. In this case, wait until the handler fully663// initializes before resolving (throwing if it ends up needing auth)664if (errorOnUserInteraction && state.state === McpConnectionState.Kind.Running) {665let disposable: IDisposable;666state = await new Promise<McpConnectionState>((resolve, reject) => {667disposable = autorun(reader => {668const handler = connection.handler.read(reader);669if (handler) {670resolve(state);671}672673const s = connection.state.read(reader);674if (s.state === McpConnectionState.Kind.Stopped && s.reason === 'needs-user-interaction') {675reject(new UserInteractionRequiredError('auth'));676}677678if (!McpConnectionState.isRunning(s)) {679resolve(s);680}681});682}).finally(() => disposable.dispose());683}684685return state;686}).finally(() => {687interaction?.participants.set(this.definition.id, { s: 'resolved' });688});689}690691private showInteractiveError(cnx: IMcpServerConnection, error: McpConnectionState.Error, debug?: boolean) {692if (error.code === 'ENOENT' && cnx.launchDefinition.type === McpServerTransportType.Stdio) {693let docsLink: string | undefined;694switch (cnx.launchDefinition.command) {695case 'uvx':696docsLink = `https://aka.ms/vscode-mcp-install/uvx`;697break;698case 'npx':699docsLink = `https://aka.ms/vscode-mcp-install/npx`;700break;701case 'dnx':702docsLink = `https://aka.ms/vscode-mcp-install/dnx`;703break;704case 'dotnet':705docsLink = `https://aka.ms/vscode-mcp-install/dotnet`;706break;707}708709const options: IPromptChoice[] = [{710label: localize('mcp.command.showOutput', "Show Output"),711run: () => this.showOutput(),712}];713714if (cnx.definition.devMode?.debug?.type === 'debugpy' && debug) {715this._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, {716label: localize('mcpViewDocs', 'View Docs'),717run: () => this._openerService.open(URI.parse('https://aka.ms/vscode-mcp-install/debugpy')),718}]);719return;720}721722if (docsLink) {723options.push({724label: localize('mcpServerInstall', 'Install {0}', cnx.launchDefinition.command),725run: () => this._openerService.open(URI.parse(docsLink)),726});727}728729this._notificationService.prompt(Severity.Error, localize('mcpServerNotFound', 'The command "{0}" needed to run {1} was not found.', cnx.launchDefinition.command, cnx.definition.label), options);730} else {731this._notificationService.warn(localize('mcpServerError', 'The MCP server {0} could not be started: {1}', cnx.definition.label, error.message));732}733}734735public stop(): Promise<void> {736return this._connection.get()?.stop() || Promise.resolve();737}738739/** Waits for any ongoing tools to be refreshed before resolving. */740public awaitToolRefresh() {741return new Promise<void>(resolve => {742autorunSelfDisposable(reader => {743const promise = this._tools.fromServerPromise.read(reader);744const result = promise?.promiseResult.read(reader);745if (result) {746resolve();747}748});749});750}751752private resetLiveData() {753transaction(tx => {754this._tools.fromServerPromise.set(undefined, tx);755this._prompts.fromServerPromise.set(undefined, tx);756});757}758759private async _normalizeTool(originalTool: MCP.Tool): Promise<ValidatedMcpTool | { error: string[] }> {760// Parse MCP Apps UI metadata from _meta.ui761const uiMeta = originalTool._meta?.ui as McpApps.McpUiToolMeta | undefined;762763// Compute visibility from _meta.ui.visibility, defaulting to Model | App764let visibility: McpToolVisibility = McpToolVisibility.Model | McpToolVisibility.App;765if (uiMeta?.visibility && Array.isArray(uiMeta.visibility)) {766visibility &= 0;767768if (uiMeta.visibility.includes('model')) {769visibility |= McpToolVisibility.Model;770}771if (uiMeta.visibility.includes('app')) {772visibility |= McpToolVisibility.App;773}774}775776const tool: ValidatedMcpTool = {777...originalTool,778serverToolName: originalTool.name,779_icons: this._parseIcons(originalTool),780visibility,781uiResourceUri: uiMeta?.resourceUri,782};783if (!tool.description) {784// Ensure a description is provided for each tool, #243919785this._logger.warn(`Tool ${tool.name} does not have a description. Tools must be accurately described to be called`);786tool.description = '<empty>';787}788789if (toolInvalidCharRe.test(tool.name)) {790this._logger.warn(`Tool ${JSON.stringify(tool.name)} is invalid. Tools names may only contain [a-z0-9_-]`);791tool.name = tool.name.replace(toolInvalidCharRe, '_');792}793794type JsonDiagnostic = { message: string; range: { line: number; character: number }[] };795796let diagnostics: JsonDiagnostic[] = [];797const toolJson = JSON.stringify(tool.inputSchema);798try {799const schemaUri = URI.parse('https://json-schema.org/draft-07/schema');800diagnostics = await this._commandService.executeCommand<JsonDiagnostic[]>('json.validate', schemaUri, toolJson) || [];801} catch (e) {802// ignored (error in json extension?);803}804805if (!diagnostics.length) {806return tool;807}808809// because it's all one line from JSON.stringify, we can treat characters as offsets.810const tree = json.parseTree(toolJson);811const messages = diagnostics.map(d => {812const node = json.findNodeAtOffset(tree, d.range[0].character);813const path = node && `/${json.getNodePath(node).join('/')}`;814return d.message + (path ? ` (at ${path})` : '');815});816817return { error: messages };818}819820private async _getValidatedTools(tools: MCP.Tool[]): Promise<ValidatedMcpTool[]> {821let error = '';822823const validations = await Promise.all(tools.map(t => this._normalizeTool(t)));824const validated: ValidatedMcpTool[] = [];825for (const [i, result] of validations.entries()) {826if ('error' in result) {827error += localize('mcpBadSchema.tool', 'Tool `{0}` has invalid JSON parameters:', tools[i].name) + '\n';828for (const message of result.error) {829error += `\t- ${message}\n`;830}831error += `\t- Schema: ${JSON.stringify(tools[i].inputSchema)}\n\n`;832} else {833validated.push(result);834}835}836837if (error) {838this._logger.warn(`${tools.length - validated.length} tools have invalid JSON schemas and will be omitted`);839warnInvalidTools(this._instantiationService, this.definition.label, error);840}841842return validated;843}844845/**846* Parses incoming MCP icons and returns the resulting 'stored' record. Note847* that this requires an active MCP server connection since we validate848* against some of that connection's data. The icons may however be stored849* and rehydrated later.850*/851private _parseIcons(icons: MCP.Icons) {852const cnx = this._connection.get();853if (!cnx) {854return [];855}856857return parseAndValidateMcpIcon(icons, cnx.launchDefinition, this._logger);858}859860private _setServerTools(nonce: string | undefined, toolsPromise: Promise<MCP.Tool[]>, tx: ITransaction | undefined) {861const toolPromiseSafe = toolsPromise.then(async tools => {862this._logger.info(`Discovered ${tools.length} tools`);863const data = await this._getValidatedTools(tools);864this._primitiveCache.store(this.definition.id, { tools: data, nonce });865return { data, nonce };866});867this._tools.fromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx);868return toolPromiseSafe;869}870871private _setServerPrompts(nonce: string | undefined, promptsPromise: Promise<MCP.Prompt[]>, tx: ITransaction | undefined) {872const promptsPromiseSafe = promptsPromise.then((result): { data: StoredMcpPrompt[]; nonce: string | undefined } => {873const data: StoredMcpPrompt[] = result.map(prompt => ({874...prompt,875_icons: this._parseIcons(prompt)876}));877this._primitiveCache.store(this.definition.id, { prompts: data, nonce });878return { data, nonce };879});880881this._prompts.fromServerPromise.set(new ObservablePromise(promptsPromiseSafe), tx);882return promptsPromiseSafe;883}884885private _toStoredMetadata(serverInfo?: MCP.Implementation, instructions?: string): StoredServerMetadata {886return {887serverName: serverInfo ? serverInfo.title || serverInfo.name : undefined,888serverInstructions: instructions,889serverIcons: serverInfo ? this._parseIcons(serverInfo) : undefined,890};891}892893private _setServerMetadata(894nonce: string | undefined,895{ serverInfo, instructions, capabilities }: { serverInfo: MCP.Implementation; instructions: string | undefined; capabilities: MCP.ServerCapabilities },896tx: ITransaction | undefined,897) {898const serverMetadata: StoredServerMetadata = this._toStoredMetadata(serverInfo, instructions);899this._serverMetadata.fromServerPromise.set(ObservablePromise.resolved({ nonce, data: serverMetadata }), tx);900901const capabilitiesEncoded = encodeCapabilities(capabilities);902this._capabilities.fromServerPromise.set(ObservablePromise.resolved({ data: capabilitiesEncoded, nonce }), tx);903this._primitiveCache.store(this.definition.id, { ...serverMetadata, nonce, capabilities: capabilitiesEncoded });904}905906private _populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) {907const cts = new CancellationTokenSource();908store.add(toDisposable(() => cts.dispose(true)));909910const updateTools = (tx: ITransaction | undefined) => {911const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]);912return this._setServerTools(cacheNonce, toolPromise, tx);913};914915const updatePrompts = (tx: ITransaction | undefined) => {916const promptsPromise = handler.capabilities.prompts ? handler.listPrompts({}, cts.token) : Promise.resolve([]);917return this._setServerPrompts(cacheNonce, promptsPromise, tx);918};919920store.add(handler.onDidChangeToolList(() => {921this._logger.info('Tool list changed, refreshing tools...');922updateTools(undefined);923}));924925store.add(handler.onDidChangePromptList(() => {926this._logger.info('Prompts list changed, refreshing prompts...');927updatePrompts(undefined);928}));929930transaction(tx => {931this._setServerMetadata(cacheNonce, { serverInfo: handler.serverInfo, instructions: handler.serverInstructions, capabilities: handler.capabilities }, tx);932updatePrompts(tx);933const toolUpdate = updateTools(tx);934935toolUpdate.then(tools => {936this._telemetryService.publicLog2<ServerBootData, ServerBootClassification>('mcp/serverBoot', {937supportsLogging: !!handler.capabilities.logging,938supportsPrompts: !!handler.capabilities.prompts,939supportsResources: !!handler.capabilities.resources,940toolCount: tools.data.length,941serverName: handler.serverInfo.name,942serverVersion: handler.serverInfo.version,943});944});945});946}947}948949class McpPrompt implements IMcpPrompt {950readonly id: string;951readonly name: string;952readonly description?: string;953readonly title?: string;954readonly arguments: readonly MCP.PromptArgument[];955readonly icons: IMcpIcons;956957constructor(958private readonly _server: McpServer,959private readonly _definition: StoredMcpPrompt,960) {961this.id = mcpPromptReplaceSpecialChars(this._server.definition.label + '.' + _definition.name);962this.name = _definition.name;963this.title = _definition.title;964this.description = _definition.description;965this.arguments = _definition.arguments || [];966this.icons = McpIcons.fromStored(this._definition._icons);967}968969async resolve(args: Record<string, string>, token?: CancellationToken): Promise<IMcpPromptMessage[]> {970const result = await McpServer.callOn(this._server, h => h.getPrompt({ name: this._definition.name, arguments: args }, token), token);971return result.messages;972}973974async complete(argument: string, prefix: string, alreadyResolved: Record<string, string>, token?: CancellationToken): Promise<string[]> {975const result = await McpServer.callOn(this._server, h => h.complete({976ref: { type: 'ref/prompt', name: this._definition.name },977argument: { name: argument, value: prefix },978context: { arguments: alreadyResolved },979}, token), token);980return result.completion.values;981}982}983984function encodeCapabilities(cap: MCP.ServerCapabilities): McpCapability {985let out = 0;986if (cap.logging) { out |= McpCapability.Logging; }987if (cap.completions) { out |= McpCapability.Completions; }988if (cap.prompts) {989out |= McpCapability.Prompts;990if (cap.prompts.listChanged) {991out |= McpCapability.PromptsListChanged;992}993}994if (cap.resources) {995out |= McpCapability.Resources;996if (cap.resources.subscribe) {997out |= McpCapability.ResourcesSubscribe;998}999if (cap.resources.listChanged) {1000out |= McpCapability.ResourcesListChanged;1001}1002}1003if (cap.tools) {1004out |= McpCapability.Tools;1005if (cap.tools.listChanged) {1006out |= McpCapability.ToolsListChanged;1007}1008}1009return out;1010}10111012export class McpTool implements IMcpTool {10131014readonly id: string;1015readonly referenceName: string;1016readonly icons: IMcpIcons;1017readonly visibility: McpToolVisibility;10181019public get definition(): MCP.Tool { return this._definition; }1020public get uiResourceUri(): string | undefined { return this._definition.uiResourceUri; }10211022constructor(1023private readonly _server: McpServer,1024idPrefix: string,1025private readonly _definition: ValidatedMcpTool,1026@IMcpElicitationService private readonly _elicitationService: IMcpElicitationService,1027) {1028this.referenceName = _definition.name.replaceAll('.', '_');1029this.id = (idPrefix + _definition.name).replaceAll('.', '_').slice(0, McpToolName.MaxLength);1030this.icons = McpIcons.fromStored(this._definition._icons);1031this.visibility = _definition.visibility ?? (McpToolVisibility.Model | McpToolVisibility.App);1032}10331034async call(params: Record<string, unknown>, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {1035if (context) { this._server.runningToolCalls.add(context); }1036try {1037return await this._callWithProgress(params, undefined, context, token);1038} finally {1039if (context) { this._server.runningToolCalls.delete(context); }1040}1041}10421043async callWithProgress(params: Record<string, unknown>, progress: ToolProgress, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {1044if (context) { this._server.runningToolCalls.add(context); }1045try {1046return await this._callWithProgress(params, progress, context, token);1047} finally {1048if (context) { this._server.runningToolCalls.delete(context); }1049}1050}10511052_callWithProgress(params: Record<string, unknown>, progress: ToolProgress | undefined, context?: IMcpToolCallContext, token = CancellationToken.None, allowRetry = true): Promise<MCP.CallToolResult> {1053// serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it.1054const name = this._definition.serverToolName ?? this._definition.name;1055const progressToken = progress ? generateUuid() : undefined;1056const store = new DisposableStore();10571058return McpServer.callOn(this._server, async h => {1059if (progress) {1060store.add(h.onDidReceiveProgressNotification((e) => {1061if (e.params.progressToken === progressToken) {1062progress.report({1063message: e.params.message,1064progress: e.params.total !== undefined && e.params.progress !== undefined ? e.params.progress / e.params.total : undefined,1065});1066}1067}));1068}10691070const meta: Record<string, unknown> = { progressToken };1071if (context?.chatSessionId) {1072meta['vscode.conversationId'] = context.chatSessionId;1073}1074if (context?.chatRequestId) {1075meta['vscode.requestId'] = context.chatRequestId;1076}10771078const taskHint = this._definition.execution?.taskSupport;1079const serverSupportsTasksForTools = h.capabilities.tasks?.requests?.tools?.call !== undefined;1080const shouldUseTask = serverSupportsTasksForTools && (taskHint === 'required' || taskHint === 'optional');10811082try {1083const result = await h.callTool({1084name,1085arguments: params,1086task: shouldUseTask ? {} : undefined,1087_meta: meta,1088}, token);10891090// Wait for tools to refresh for dynamic servers (#261611)1091await this._server.awaitToolRefresh();10921093return result;1094} catch (err) {1095// Handle URL elicitation required error1096if (err instanceof MpcResponseError && err.code === MCP.URL_ELICITATION_REQUIRED && allowRetry) {1097await this._handleElicitationErr(err, context, token);1098return this._callWithProgress(params, progress, context, token, false);1099}11001101const state = this._server.connectionState.get();1102if (allowRetry && state.state === McpConnectionState.Kind.Error && state.shouldRetry) {1103return this._callWithProgress(params, progress, context, token, false);1104} else {1105throw err;1106}1107} finally {1108store.dispose();1109}1110}, token);1111}11121113private async _handleElicitationErr(err: MpcResponseError, context: IMcpToolCallContext | undefined, token: CancellationToken) {1114const elicitations = (err.data as MCP.URLElicitationRequiredError['error']['data'])?.elicitations;1115if (Array.isArray(elicitations) && elicitations.length > 0) {1116for (const elicitation of elicitations) {1117const elicitResult = await this._elicitationService.elicit(this._server, context, elicitation, token);11181119try {1120if (elicitResult.value.action !== 'accept') {1121throw err;1122}11231124if (elicitResult.kind === ElicitationKind.URL) {1125await elicitResult.wait;1126}1127} finally {1128elicitResult.dispose();1129}1130}1131}1132}11331134compare(other: IMcpTool): number {1135return this._definition.name.localeCompare(other.definition.name);1136}1137}11381139function warnInvalidTools(instaService: IInstantiationService, serverName: string, errorText: string) {1140instaService.invokeFunction((accessor) => {1141const notificationService = accessor.get(INotificationService);1142const editorService = accessor.get(IEditorService);1143notificationService.notify({1144severity: Severity.Warning,1145message: localize('mcpBadSchema', 'MCP server `{0}` has tools with invalid parameters which will be omitted.', serverName),1146actions: {1147primary: [{1148class: undefined,1149enabled: true,1150id: 'mcpBadSchema.show',1151tooltip: '',1152label: localize('mcpBadSchema.show', 'Show'),1153run: () => {1154editorService.openEditor({1155resource: undefined,1156contents: errorText,1157});1158}1159}]1160}1161});1162});1163}11641165class McpResource implements IMcpResource {1166readonly uri: URI;1167readonly mcpUri: string;1168readonly name: string;1169readonly description: string | undefined;1170readonly mimeType: string | undefined;1171readonly sizeInBytes: number | undefined;1172readonly title: string | undefined;11731174constructor(1175server: McpServer,1176original: MCP.Resource,1177public readonly icons: IMcpIcons,1178) {1179this.mcpUri = original.uri;1180this.title = original.title;1181this.uri = McpResourceURI.fromServer(server.definition, original.uri);1182this.name = original.name;1183this.description = original.description;1184this.mimeType = original.mimeType;1185this.sizeInBytes = original.size;1186}1187}11881189class McpResourceTemplate implements IMcpResourceTemplate {1190readonly name: string;1191readonly title?: string | undefined;1192readonly description?: string;1193readonly mimeType?: string;1194readonly template: UriTemplate;11951196constructor(1197private readonly _server: McpServer,1198private readonly _definition: MCP.ResourceTemplate,1199public readonly icons: IMcpIcons,1200) {1201this.name = _definition.name;1202this.description = _definition.description;1203this.mimeType = _definition.mimeType;1204this.title = _definition.title;1205this.template = UriTemplate.parse(_definition.uriTemplate);1206}12071208public resolveURI(vars: Record<string, unknown>): URI {1209const serverUri = this.template.resolve(vars);1210return McpResourceURI.fromServer(this._server.definition, serverUri);1211}12121213async complete(templatePart: string, prefix: string, alreadyResolved: Record<string, string | string[]>, token?: CancellationToken): Promise<string[]> {1214const result = await McpServer.callOn(this._server, h => h.complete({1215ref: { type: 'ref/resource', uri: this._definition.uriTemplate },1216argument: { name: templatePart, value: prefix },1217context: {1218arguments: mapValues(alreadyResolved, v => Array.isArray(v) ? v.join('/') : v),1219},1220}, token), token);1221return result.completion.values;1222}1223}122412251226