src/vs/workbench/contrib/mcp/common/mcpServer.ts

1539 LOC · 956 covered · 583 uncovered · 130 ranges · 111 concepts · 7 introducers · 74 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- mcpServer.ts ×62
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { AsyncIterableProducer, raceCancellationError, Sequencer } from '../../../../base/common/async.js';
7 > import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
8 > import { Iterable } from '../../../../base/common/iterator.js';
9 > import * as json from '../../../../base/common/json.js';
10 > import { normalizeDriveLetter } from '../../../../base/common/labels.js';
11 > import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
12 > import { LRUCache } from '../../../../base/common/map.js';
13 > import { Schemas } from '../../../../base/common/network.js';
14 > import { mapValues } from '../../../../base/common/objects.js';
15 > import { autorun, autorunSelfDisposable, derived, derivedDisposable, disposableObservableValue, IDerivedReader, IObservable, IReader, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js';
16 > import { basename } from '../../../../base/common/resources.js';
17 > import { URI } from '../../../../base/common/uri.js';
18 > import { createURITransformer } from '../../../../base/common/uriTransformer.js';
19 > import { generateUuid } from '../../../../base/common/uuid.js';
20 > import { localize } from '../../../../nls.js';
21 > import { ICommandService } from '../../../../platform/commands/common/commands.js';
22 > import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
23 > import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
24 > import { IMcpServerIdentity } from '../../../../platform/mcp/common/allowedMcpServers.js';
25 > import { IAllowedMcpServersService } from '../../../../platform/mcp/common/mcpManagement.js';
26 > import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js';
27 > import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js';
28 > import { IOpenerService } from '../../../../platform/opener/common/opener.js';
29 > import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
30 > import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
31 > import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
32 > import { ConfigurationResolverExpression } from '../../../services/configurationResolver/common/configurationResolverExpression.js';
33 > import { IEditorService } from '../../../services/editor/common/editorService.js';
34 > import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
35 > import { IExtensionService } from '../../../services/extensions/common/extensions.js';
36 > import { IOutputService } from '../../../services/output/common/output.js';
37 > import { chatSessionResourceToId } from '../../chat/common/model/chatUri.js';
38 > import { ToolProgress } from '../../chat/common/tools/languageModelToolsService.js';
39 > import { mcpActivationEvent } from './mcpConfiguration.js';
40 > import { McpDevModeServerAttache } from './mcpDevMode.js';
41 > import { McpIcons, parseAndValidateMcpIcon, StoredMcpIcons } from './mcpIcons.js';
42 > import { IMcpRegistry } from './mcpRegistryTypes.js';
43 > import { IMcpSandboxService } from './mcpSandboxService.js';
44 > import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
45 > import { McpTaskManager } from './mcpTaskManager.js';
46 > import { ElicitationKind, extensionMcpCollectionPrefix, IMcpElicitationService, IMcpIcons, IMcpPotentialSandboxBlock, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerLaunch, McpServerStaticToolAvailability, McpServerTransportType, McpToolName, McpToolVisibility, MpcResponseError, UserInteractionRequiredError } from './mcpTypes.js';
47 > import { ContributionEnablementState, IEnablementModel } from '../../chat/common/enablement.js';
48 > import { MCP } from './modelContextProtocol.js';
49 > import { McpApps } from './modelContextProtocolApps.js';
50 > import { UriTemplate } from '../../../../base/common/uriTemplate.js';
51 >
52 > type ServerBootData = {
53 > supportsLogging: boolean;
54 > supportsPrompts: boolean;
55 > supportsResources: boolean;
56 > toolCount: number;
57 > serverName: string;
58 > serverVersion: string;
59 > };
60 > type ServerBootClassification = {
61 > owner: 'connor4312';
62 > comment: 'Details the capabilities of the MCP server';
63 > supportsLogging: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports logging' };
64 > supportsPrompts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports prompts' };
65 > supportsResources: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports resource' };
66 > toolCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tools the server advertises' };
67 > serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };
68 > serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };
69 > };
70 >
71 > type ElicitationTelemetryData = {
72 > serverName: string;
73 > serverVersion: string;
74 > };
75 >
76 > type ElicitationTelemetryClassification = {
77 > owner: 'connor4312';
78 > comment: 'Triggered when elictation is requested';
79 > serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };
80 > serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };
81 > };
82 >
83 > export type McpServerInstallData = {
84 > serverName: string;
85 > source: 'gallery' | 'local';
86 > scope: string;
87 > success: boolean;
88 > error?: string;
89 > hasInputs: boolean;
90 > };
91 >
92 > export type McpServerInstallClassification = {
93 > owner: 'connor4312';
94 > comment: 'MCP server installation event tracking';
95 > serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server being installed' };
96 > source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation source (gallery or local)' };
97 > scope: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation scope (user, workspace, etc.)' };
98 > success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether installation succeeded' };
99 > error?: { classification: 'CallstackOrException'; purpose: 'FeatureInsight'; comment: 'Error message if installation failed' };
100 > hasInputs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server requires input configuration' };
101 > };
102 >
103 > type ServerBootState = {
104 > state: string;
105 > time: number;
106 > };
107 > type ServerBootStateClassification = {
108 > owner: 'connor4312';
109 > comment: 'Details the capabilities of the MCP server';
110 > state: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The server outcome' };
111 > time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Duration in milliseconds to reach that state' };
112 > };
113 >
114 > type StoredMcpPrompt = MCP.Prompt & { _icons: StoredMcpIcons };
115 >
116 > interface IToolCacheEntry {
117 > readonly serverName: string | undefined;
118 > readonly serverInstructions: string | undefined;
119 > readonly serverIcons: StoredMcpIcons;
120 >
121 > readonly trustedAtNonce: string | undefined;
122 >
123 > readonly nonce: string | undefined;
124 > /** Cached tools so we can show what's available before it's started */
125 > readonly tools: readonly ValidatedMcpTool[];
126 > /** Cached prompts */
127 > readonly prompts: readonly StoredMcpPrompt[] | undefined;
128 > /** Cached capabilities */
129 > readonly capabilities: McpCapability | undefined;
130 > }
131 >
132 > const emptyToolEntry: IToolCacheEntry = {
133 > serverName: undefined,
134 > serverIcons: [],
135 > serverInstructions: undefined,
136 > trustedAtNonce: undefined,
137 > nonce: undefined,
138 > tools: [],
139 > prompts: undefined,
140 > capabilities: undefined,
141 > };
142 >
143 > interface IServerCacheEntry {
144 > readonly servers: readonly McpServerDefinition.Serialized[];
145 > }
146 >
147 > const toolInvalidCharRe = /[^a-z0-9_-]/gi;
148 >
149 > export class McpServerMetadataCache extends Disposable {
150 > private didChange = false;
151 > private readonly cache = new LRUCache<string, IToolCacheEntry>(128);
152 > private readonly extensionServers = new Map</* collection ID */string, IServerCacheEntry>();
153 >
154 > constructor(
155 > scope: StorageScope, mcpServer.ts ×23
156 > @IStorageService storageService: IStorageService,
157 > ) {
158 > super();
159 >
160 > type StoredType = {
161 > extensionServers: [string, IServerCacheEntry][];
162 > serverTools: [string, IToolCacheEntry][];
163 > };
164 >
165 > const storageKey = 'mcpToolCache';
166 > this._register(storageService.onWillSaveState(() => {
167 if (this.didChange) {
168 storageService.store(storageKey, {
169 extensionServers: [...this.extensionServers],
170 serverTools: this.cache.toJSON(),
171 } satisfies StoredType, scope, StorageTarget.MACHINE);
172 this.didChange = false;
173 }
174 > })); mcpServer.ts ×23
175 >
176 > try {
177 > const cached: StoredType | undefined = storageService.getObject(storageKey, scope);
178 > this.extensionServers = new Map(cached?.extensionServers ?? []);
179 > cached?.serverTools?.forEach(([k, v]) => this.cache.set(k, v));
180 > } catch {
181 // ignored
182 }
185 > /** Resets the cache for primitives and extension servers */
186 > reset() {
187 this.cache.clear();
188 this.extensionServers.clear();
189 this.didChange = true;
190 }
192 > /** Gets cached primitives for a server (used before a server is running) */
193 > get(definitionId: string) {
194 > return this.cache.get(definitionId); mcpServer.ts ×23
195 > }
197 > /** Sets cached primitives for a server */
198 > store(definitionId: string, entry: Partial<IToolCacheEntry>): void {
199 > const prev = this.get(definitionId) || emptyToolEntry; mcpServer.ts ×36
200 > this.cache.set(definitionId, { ...prev, ...entry });
201 > this.didChange = true;
202 > }
204 > /** Gets cached servers for a collection (used for extensions, before the extension activates) */
205 > getServers(collectionId: string) {
206 return this.extensionServers.get(collectionId);
207 }
209 > /** Sets cached servers for a collection */
210 > storeServers(collectionId: string, entry: IServerCacheEntry | undefined): void {
211 if (entry) {
212 this.extensionServers.set(collectionId, entry);
213 } else {
214 this.extensionServers.delete(collectionId);
215 }
216 this.didChange = true;
217 }
219 >
220 > /**
221 > * Shared across all {@link McpServer}s. Each server `take`s the name it wants
222 > * to base its tool prefix on (announced `serverInfo.title`/`name` when known,
223 > * otherwise the mcp.json key) and gets back a stable, collision-resolved prefix
224 > * observable. When a server's preferred name changes (e.g. after the live
225 > * `serverInfo` arrives), it simply takes again and disposes the previous
226 > * reference; other servers that share the name keep the suffix they were
227 > * already assigned. See #299749.
228 > */
229 > export class McpPrefixGenerator {
230 > private readonly _buckets = new Map<string, { usedIndexes: Set<number>; size: number }>(); mcpServer.ts ×3
232 > take(name: string): IReference<string> {
233 > const safeName = name.toLowerCase().replace(/[^a-z0-9_.-]+/g, '_').slice(0, McpToolName.MaxPrefixLen - McpToolName.Prefix.length - 1); mcpServer.ts ×3
234 > let bucket = this._buckets.get(safeName);
235 > if (!bucket) {
236 > bucket = { usedIndexes: new Set(), size: 0 };
237 > this._buckets.set(safeName, bucket);
238 > }
239 >
240 > let index = 1;
241 > while (bucket.usedIndexes.has(index)) {
242 > index++; mcpServer.ts ×1
243 > }
244 > bucket.usedIndexes.add(index); mcpServer.ts ×3
245 > bucket.size++;
246 >
247 > // Trim safeName for this output if a multi-digit suffix would push us past
248 > // MaxPrefixLen. The bucket is keyed on the un-trimmed safeName so collisions
249 > // are still detected consistently across indexes.
250 > const suffix = (index === 1 ? '' : String(index)) + '_';
251 > const maxNameLen = McpToolName.MaxPrefixLen - McpToolName.Prefix.length - suffix.length;
252 > const prefix = McpToolName.Prefix + safeName.slice(0, maxNameLen) + suffix;
253 >
254 > return {
255 > object: prefix,
256 > dispose: () => {
257 > bucket!.usedIndexes.delete(index);
258 > bucket!.size--;
259 > if (bucket!.size === 0) {
260 > this._buckets.delete(safeName);
261 > }
262 > },
263 > };
264 > }
266 >
267 > type ValidatedMcpTool = MCP.Tool & {
268 > _icons: StoredMcpIcons;
269 >
270 > /**
271 > * Tool name as published by the MCP server. This may
272 > * be different than the one in {@link definition} due to name normalization
273 > * in {@link McpServer._getValidatedTools}.
274 > */
275 > serverToolName: string;
276 >
277 > /**
278 > * Visibility of the tool, parsed from `_meta.ui.visibility`.
279 > * Defaults to Model | App if not specified.
280 > */
281 > visibility: McpToolVisibility;
282 >
283 > /**
284 > * UI resource URI if this tool has an associated MCP App UI.
285 > * Parsed from `_meta.ui.resourceUri`.
286 > */
287 > uiResourceUri?: string;
288 > };
289 >
290 > interface StoredServerMetadata {
291 > readonly serverName: string | undefined;
292 > readonly serverInstructions: string | undefined;
293 > readonly serverIcons: StoredMcpIcons | undefined;
294 > }
295 >
296 > interface ServerMetadata {
297 > readonly serverName: string | undefined;
298 > readonly serverInstructions: string | undefined;
299 > readonly icons: IMcpIcons;
300 > }
301 >
302 > class CachedPrimitive<T, C> {
303 > /**
304 > * @param _definitionId Server definition ID
305 > * @param _cache Metadata cache instance
306 > * @param _fromStaticDefinition Static definition that came with the server.
307 > * This should ONLY have a value if it should be used instead of whatever
308 > * is currently in the cache.
309 > * @param _fromCache Pull the value from the cache entry.
310 > * @param _toT Transform the value to the observable type.
311 > * @param defaultValue Default value if no cache entry.
312 > */
313 > constructor(
314 > private readonly _definitionId: string, mcpServer.ts ×23
315 > private readonly _cache: McpServerMetadataCache,
316 > private readonly _fromStaticDefinition: IObservable<C | undefined> | undefined,
317 > private readonly _fromCache: (entry: IToolCacheEntry) => C,
318 > private readonly _toT: (values: C, reader: IDerivedReader<void>) => T,
319 > private readonly defaultValue: C,
320 > ) { }
321 >
322 > public get fromCache(): { nonce: string | undefined; data: C } | undefined {
323 > const c = this._cache.get(this._definitionId);
324 > return c ? { data: this._fromCache(c), nonce: c.nonce } : undefined;
325 > }
326 >
327 > public hasStaticDefinition(reader: IReader | undefined) {
328 return !!this._fromStaticDefinition?.read(reader);
329 }
331 > public readonly fromServerPromise = observableValue<ObservablePromise<{
332 > readonly data: C;
333 > readonly nonce: string | undefined;
334 > }> | undefined>(this, undefined);
335 >
336 > private readonly fromServer = derived(reader => this.fromServerPromise.read(reader)?.promiseResult.read(reader)?.data);
337 >
338 > public readonly value: IObservable<T> = derived(reader => {
339 > const serverTools = this.fromServer.read(reader);
340 > const definitions = serverTools?.data ?? this._fromStaticDefinition?.read(reader) ?? this.fromCache?.data ?? this.defaultValue;
341 > return this._toT(definitions, reader);
342 > });
344 >
345 > export class McpServer extends Disposable implements IMcpServer {
346 > /** Shared task manager that survives reconnections */
347 > private readonly _taskManager = this._register(new McpTaskManager());
348 >
349 > /**
350 > * Helper function to call the function on the handler once it's online. The
351 > * connection started if it is not already.
352 > */
353 > public static async callOn<R>(server: IMcpServer, fn: (handler: McpServerRequestHandler, connection: IMcpServerConnection) => Promise<R>, token: CancellationToken = CancellationToken.None): Promise<R> {
354 > await server.start({ promptType: 'all-untrusted' }); // idempotent
355 >
356 > let ranOnce = false;
357 > let d: IDisposable;
358 >
359 > const callPromise = new Promise<R>((resolve, reject) => {
360 >
361 > d = autorun(reader => {
362 > if (ranOnce) {
363 > return; mcpServer.ts ×4
364 > }
366 > const connection = server.connection.read(reader);
367 > if (!connection) {
368 > // No live connection: the server may be blocked by policy (its connection is torn mcpServer.ts ×4
369 > // down while blocked) or stopped. Surface the terminal state instead of waiting forever.
370 > const state = server.connectionState.read(reader);
371 > if (state.state === McpConnectionState.Kind.Error) {
372 > reject(new McpConnectionFailedError(`MCP server could not be started: ${state.message}`));
373 > } else if (state.state === McpConnectionState.Kind.Stopped) {
374 > reject(new McpConnectionFailedError('MCP server has stopped'));
375 > }
376 > return;
377 > }
379 > const handler = connection.handler.read(reader);
380 > if (!handler) {
381 > const state = connection.state.read(reader);
382 > if (state.state === McpConnectionState.Kind.Error) {
383 > reject(new McpConnectionFailedError(`MCP server could not be started: ${state.message}`)); mcpServer.ts ×4
384 > return;
385 > } else if (state.state === McpConnectionState.Kind.Stopped) { mcpServer.ts ×62
386 > reject(new McpConnectionFailedError('MCP server has stopped')); mcpServer.ts ×4
387 > return;
388 > } else { mcpServer.ts ×62
389 > // keep waiting for handler
390 > return;
391 > }
392 > }
393 >
394 > resolve(fn(handler, connection));
395 > ranOnce = true; // aggressive prevent multiple racey calls, don't dispose because autorun is sync
396 > });
397 > });
398 >
399 > return raceCancellationError(callPromise, token).finally(() => d.dispose());
400 > }
401 >
402 > public readonly collection: McpCollectionReference;
403 > private readonly _connectionSequencer = new Sequencer();
404 > private readonly _connection = this._register(disposableObservableValue<IMcpServerConnection | undefined>(this, undefined));
405 >
406 > public readonly connection = this._connection;
407 >
408 > /**
409 > * Reactively evaluates the `chat.mcp.allowedServers` / `chat.mcp.deniedServers` policy against
410 > * this server's identity. Holds an error state while blocked, `undefined` while allowed.
411 > *
412 > * Being a derived, it recomputes whenever the policy changes (via {@link _policyEpoch}), the
413 > * server definition changes, or a connection resolves — so it always evaluates the *resolved*
414 > * launch of a live connection and falls back to the definition otherwise. This also means a
415 > * blocked server surfaces the block at rest (before any start), which hides its cached tools
416 > * and prompts and lets the UI show the reason.
417 > *
418 > * Initialized in the constructor because it depends on the injected allowed-servers service.
419 > */
420 > private readonly _policyEpoch: IObservable<void>;
421 > private readonly _policyBlock: IObservable<McpConnectionState.Error | undefined>;
422 > public readonly connectionState: IObservable<McpConnectionState> = derived(reader => this._policyBlock.read(reader) ?? this._connection.read(reader)?.state.read(reader) ?? { state: McpConnectionState.Kind.Stopped });
423 >
424 >
425 > private readonly _capabilities: CachedPrimitive<number | undefined, number | undefined>;
426 > public get capabilities() {
427 > return this._capabilities.value; mcpServer.ts ×36
428 > }
430 > private readonly _tools: CachedPrimitive<readonly IMcpTool[], readonly ValidatedMcpTool[]>;
431 > /** Cached tools are suppressed while the server is blocked by policy so they cannot be listed, referenced, or executed. */
432 > private readonly _gatedTools: IObservable<readonly IMcpTool[]> = derived(reader => this._policyBlock.read(reader) ? [] : this._tools.value.read(reader));
433 > public get tools() {
434 return this._gatedTools;
435 }
437 > private readonly _prompts: CachedPrimitive<readonly IMcpPrompt[], readonly StoredMcpPrompt[]>;
438 > /** Cached prompts are suppressed while the server is blocked by policy. */
439 > private readonly _gatedPrompts: IObservable<readonly IMcpPrompt[]> = derived(reader => this._policyBlock.read(reader) ? [] : this._prompts.value.read(reader));
440 > public get prompts() {
441 return this._gatedPrompts;
442 }
444 > private readonly _serverMetadata: CachedPrimitive<ServerMetadata, StoredServerMetadata | undefined>;
445 > public get serverMetadata() {
446 return this._serverMetadata.value;
447 }
449 > public get trustedAtNonce() {
450 return this._primitiveCache.get(this.definition.id)?.trustedAtNonce;
451 }
453 > public set trustedAtNonce(nonce: string | undefined) {
454 this._primitiveCache.store(this.definition.id, { trustedAtNonce: nonce });
455 }
457 > private readonly _fullDefinitions: IObservable<{
458 > server: McpServerDefinition | undefined;
459 > collection: McpCollectionDefinition | undefined;
460 > }>;
461 >
462 > public readonly cacheState = derived(reader => {
463 > const currentNonce = () => this._fullDefinitions.read(reader)?.server?.cacheNonce; mcpServer.ts ×1
464 > const stateWhenServingFromCache = () => {
465 > if (this._tools.hasStaticDefinition(reader)) {
466 > return McpServerCacheState.Cached;
467 > }
468 >
469 > if (!this._tools.fromCache) {
470 > return McpServerCacheState.Unknown;
471 > }
472 >
473 > return currentNonce() === this._tools.fromCache.nonce ? McpServerCacheState.Cached : McpServerCacheState.Outdated;
474 > };
475 >
476 > const fromServer = this._tools.fromServerPromise.read(reader);
477 > const connectionState = this.connectionState.read(reader);
478 > const isIdle = McpConnectionState.canBeStarted(connectionState.state) || !fromServer;
479 > if (isIdle) {
480 > return stateWhenServingFromCache();
481 > }
482 >
483 > const fromServerResult = fromServer?.promiseResult.read(reader);
484 > if (!fromServerResult) {
485 > return this._tools.fromCache ? McpServerCacheState.RefreshingFromCached : McpServerCacheState.RefreshingFromUnknown;
486 > }
487 >
488 > if (fromServerResult.error) {
489 > return stateWhenServingFromCache();
490 > }
491 >
492 > return fromServerResult.data?.nonce === currentNonce() ? McpServerCacheState.Live : McpServerCacheState.Outdated;
493 > }); mcpServer.ts ×62
494 >
495 > public get logger(): ILogger {
496 return this._logger;
497 }
499 > private readonly _loggerId: string;
500 > private readonly _logger: ILogger;
501 > private _lastModeDebugged = false;
502 > private _isQuietStart = false;
503 > private _isSandboxSuggestionDialogVisible = false;
504 > private _potentialSandboxBlocks: IMcpPotentialSandboxBlock[] = [];
505 > private _potentialSandboxBlockListener = this._register(new MutableDisposable<IDisposable>());
506 > /** Count of running tool calls, used to detect if sampling is during an LM call */
507 > public runningToolCalls = new Set<IMcpToolCallContext>();
508 >
509 > public readonly enablement: IObservable<ContributionEnablementState>;
510 >
511 > constructor(
512 > initialCollection: McpCollectionDefinition, mcpServer.ts ×23
513 > public readonly definition: McpDefinitionReference,
514 > explicitRoots: URI[] | undefined,
515 > private readonly _requiresExtensionActivation: boolean | undefined,
516 > private readonly _primitiveCache: McpServerMetadataCache,
517 > prefixGenerator: McpPrefixGenerator,
518 > enablementModel: IEnablementModel,
519 > @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
520 > @IAllowedMcpServersService private readonly _allowedMcpServersService: IAllowedMcpServersService,
521 > @IWorkspaceContextService workspacesService: IWorkspaceContextService,
522 > @IExtensionService private readonly _extensionService: IExtensionService,
523 > @ILoggerService private readonly _loggerService: ILoggerService,
524 > @IOutputService private readonly _outputService: IOutputService,
525 > @ITelemetryService private readonly _telemetryService: ITelemetryService,
526 > @ICommandService private readonly _commandService: ICommandService,
527 > @IInstantiationService private readonly _instantiationService: IInstantiationService,
528 > @IDialogService private readonly _dialogService: IDialogService,
529 > @INotificationService private readonly _notificationService: INotificationService,
530 > @IOpenerService private readonly _openerService: IOpenerService,
531 > @IMcpSamplingService private readonly _samplingService: IMcpSamplingService,
532 > @IMcpElicitationService private readonly _elicitationService: IMcpElicitationService,
533 > @IMcpSandboxService private readonly _mcpSandboxService: IMcpSandboxService,
534 > @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
535 > ) {
536 > super();
537 >
538 > this.collection = initialCollection;
539 > this._fullDefinitions = this._mcpRegistry.getServerDefinition(this.collection, this.definition);
540 > this.enablement = derived(r => enablementModel.readEnabled(definition.id, r));
541 >
542 > this._policyEpoch = observableFromEvent(this, this._allowedMcpServersService.onDidChangeAllowedMcpServers, () => undefined);
543 > this._policyBlock = derived<McpConnectionState.Error | undefined>(this, reader => {
544 > this._policyEpoch.read(reader);
545 > const connection = this._connection.read(reader);
546 > if (connection) {
547 > // Authoritative: the connection carries the fully resolved launch. mcpServer.ts ×36
548 > return this._evaluatePolicy(this._identityFromLaunch(connection.launchDefinition));
549 > }
550 > // At rest, only decide when we have a concrete, fully-resolved launch. If the definition mcpServer.ts ×23
551 > // has not been provided yet (e.g. a lazy/extension server before activation) or the launch
552 > // still contains unresolved `${...}` variables (inputs, workspace or env vars), a
553 > // URL/command allow/deny rule cannot be matched reliably, so defer the decision to start()
554 > // — which re-checks the fully resolved launch — to avoid over-eagerly blocking (and hiding
555 > // the cached tools of) a server that will actually be allowed once resolved. `chat.mcp.access`
556 > // and deny-by-name are still enforced at start(), and access also by the enablement layer.
557 > const launch = this._fullDefinitions.read(reader).server?.launch;
558 > if (!launch) {
559 return undefined;
560 }
561 > const identity = this._identityFromLaunch(launch); mcpServer.ts ×23
562 > if (McpServer._hasUnresolvedVariables(identity)) {
563 return undefined;
564 }
565 > return this._evaluatePolicy(identity); mcpServer.ts ×23
566 > });
567 >
568 > // Stop a live connection when the policy blocks it (e.g. the policy was tightened while the
569 > // server was running). The block itself is evaluated reactively by `_policyBlock`, which also
570 > // hides cached tools/prompts and surfaces the reason in the UI.
571 > this._register(autorun(reader => {
572 > if (this._policyBlock.read(reader) && this._connection.read(undefined)) {
573 this._connection.set(undefined, undefined); // disposes and stops the connection
574 }
575 > })); mcpServer.ts ×23
576 >
577 > this._loggerId = `mcpServer.${definition.id}`;
578 > this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` }));
579 >
580 > const that = this;
581 > this._register(this._instantiationService.createInstance(McpDevModeServerAttache, this, { get lastModeDebugged() { return that._lastModeDebugged; } }));
582 >
583 > // If the logger is disposed but not deregistered, then the disposed instance
584 > // is reused and no-ops. todo@sandy081 this seems like a bug.
585 > this._register(toDisposable(() => _loggerService.deregisterLogger(this._loggerId)));
586 >
587 > // 1. Reflect workspaces into the MCP roots
588 > const workspaces = explicitRoots
589 ? observableValue(this, explicitRoots.map(uri => ({ uri, name: basename(uri) })))
590 > : observableFromEvent( mcpServer.ts ×23
591 > this,
592 > workspacesService.onDidChangeWorkspaceFolders,
593 > () => workspacesService.getWorkspace().folders,
594 > );
595 >
596 > const uriTransformer = environmentService.remoteAuthority ? createURITransformer(environmentService.remoteAuthority) : undefined;
597 >
598 > this._register(autorun(reader => {
599 > const cnx = this._connection.read(reader)?.handler.read(reader);
600 > if (!cnx) {
601 > return;
602 > }
604 > cnx.roots = workspaces.read(reader)
605 > .filter(w => w.uri.authority === (initialCollection.remoteAuthority || ''))
606 > .map(w => {
607 > let uri = URI.from(uriTransformer?.transformIncoming(w.uri) ?? w.uri);
608 > if (uri.scheme === Schemas.file) { // #271812
609 > uri = URI.file(normalizeDriveLetter(uri.fsPath, true));
610 > }
611 >
612 > return { name: w.name, uri: uri.toString() };
613 > });
614 > })); mcpServer.ts ×23
615 >
616 > // 2. Populate this.tools when we connect to a server.
617 > this._register(autorun(reader => {
618 > const cnx = this._connection.read(reader);
619 > const handler = cnx?.handler.read(reader);
620 > if (handler) {
621 > this._populateLiveData(handler, cnx?.definition.cacheNonce, reader.store); mcpServer.ts ×36
622 > } else if (this._tools) { mcpServer.ts ×23
623 > this.resetLiveData(); mcpServer.ts ×36
624 > }
625 > })); mcpServer.ts ×23
626 >
627 > this._register(autorun(reader => {
628 > const cnx = this._connection.read(reader);
629 > this._potentialSandboxBlockListener.value = cnx?.onPotentialSandboxBlock(block => this.recordPotentialSandboxBlock(block));
630 > }));
631 >
632 > const staticMetadata = derived(reader => {
633 > const def = this._fullDefinitions.read(reader).server;
634 > return def && def.cacheNonce !== this._tools.fromCache?.nonce ? def.staticMetadata : undefined;
635 > });
636 >
637 > this._serverMetadata = new CachedPrimitive<ServerMetadata, StoredServerMetadata | undefined>(
638 > this.definition.id,
639 > this._primitiveCache,
640 > staticMetadata.map(m => m ? this._toStoredMetadata(m?.serverInfo, m?.instructions) : undefined),
641 > (entry) => ({ serverName: entry.serverName, serverInstructions: entry.serverInstructions, serverIcons: entry.serverIcons }),
642 > (entry) => ({ serverName: entry?.serverName, serverInstructions: entry?.serverInstructions, icons: McpIcons.fromStored(entry?.serverIcons) }),
643 > undefined,
644 > );
645 >
646 > // Form the tool prefix from the server-announced name when known so that
647 > // registry-style mcp.json keys like `io.github.upstash/context7` don't end
648 > // up in `mcp_io_github_ups_*` truncated names. See #299749.
649 > const preferredName = derived(reader => this._serverMetadata.value.read(reader)?.serverName || this.definition.label);
650 > const prefixRef = derivedDisposable(reader => prefixGenerator.take(preferredName.read(reader)));
651 > const toolPrefix = prefixRef.map(ref => ref.object);
652 >
653 > // 3. Publish tools
654 > this._tools = new CachedPrimitive<readonly IMcpTool[], readonly ValidatedMcpTool[]>(
655 > this.definition.id,
656 > this._primitiveCache,
657 > staticMetadata
658 > .map(m => {
659 const tools = m?.tools?.filter(t => t.availability === McpServerStaticToolAvailability.Initial).map(t => t.definition);
660 return tools?.length ? new ObservablePromise(this._getValidatedTools(tools)) : undefined;
662 > .map((o, reader) => o?.promiseResult.read(reader)?.data),
663 > (entry) => entry.tools,
664 > (entry, reader) => entry.map(def => this._instantiationService.createInstance(McpTool, this, toolPrefix.read(reader), def)).sort((a, b) => a.compare(b)),
665 > [],
666 > );
667 >
668 > // 4. Publish prompts
669 > this._prompts = new CachedPrimitive<readonly IMcpPrompt[], readonly StoredMcpPrompt[]>(
670 > this.definition.id,
671 > this._primitiveCache,
672 > undefined,
673 > (entry) => entry.prompts || [],
674 > (entry) => entry.map(e => new McpPrompt(this, e)),
675 > [],
676 > );
677 >
678 > this._capabilities = new CachedPrimitive<number | undefined, number | undefined>(
679 > this.definition.id,
680 > this._primitiveCache,
681 > staticMetadata.map(m => m?.capabilities !== undefined ? encodeCapabilities(m.capabilities) : undefined),
682 > (entry) => entry.capabilities,
683 > (entry) => entry,
684 > undefined,
685 > );
686 >
687 > // Hold the prefix for the lifetime of the server so its tool name stays
688 > // stable even when no one is currently observing the tools list.
689 > prefixRef.recomputeInitiallyAndOnChange(this._store);
690 > }
692 > public readDefinitions(): IObservable<{ server: McpServerDefinition | undefined; collection: McpCollectionDefinition | undefined }> {
693 > return this._fullDefinitions; mcpServer.ts ×23
694 > }
696 > public showOutput(preserveFocus?: boolean) {
697 this._loggerService.setVisibility(this._loggerId, true);
698 return this._outputService.showChannel(this._loggerId, preserveFocus);
699 }
701 > public resources(token?: CancellationToken): AsyncIterable<IMcpResource[]> {
702 const cts = new CancellationTokenSource(token);
703 return new AsyncIterableProducer<IMcpResource[]>(async emitter => {
704 await McpServer.callOn(this, async (handler) => {
705 for await (const resource of handler.listResourcesIterable({}, cts.token)) {
706 emitter.emitOne(resource.map(r => new McpResource(this, r, McpIcons.fromParsed(this._parseIcons(r)))));
707 if (cts.token.isCancellationRequested) {
708 return;
709 }
710 }
711 });
712 }, () => cts.dispose(true));
713 }
715 > public resourceTemplates(token?: CancellationToken): Promise<IMcpResourceTemplate[]> {
716 return McpServer.callOn(this, async (handler) => {
717 const templates = await handler.listResourceTemplates({}, token);
718 return templates.map(t => new McpResourceTemplate(this, t, McpIcons.fromParsed(this._parseIcons(t))));
719 }, token);
720 }
722 > private _identityFromLaunch(launch: McpServerLaunch | undefined): IMcpServerIdentity {
723 > if (launch?.type === McpServerTransportType.HTTP) { mcpServer.ts ×23
724 return { name: this.definition.label, url: launch.uri.toString(true) };
725 }
726 > if (launch?.type === McpServerTransportType.Stdio) { mcpServer.ts ×23
727 > // `launch.command`/`launch.args` are typed as non-nullable but can be `undefined` at
728 > // runtime when they originate from user/discovery configuration that omitted the field.
729 > // When `command` is present, build the full command line (defaulting `args` to an empty
730 > // array and dropping any non-string entries); the produced `IMcpServerIdentity.command`
731 > // then never contains a non-string entry, which would otherwise break policy matching and
732 > // the unresolved-variable check. Use a string check so a valid-but-empty command string is
733 > // preserved while malformed non-string command values are dropped. When `command` is absent
734 > // the full command line is unknown, so omit the field entirely rather than matching on args
735 > // alone (which could collide with unrelated servers).
736 > return typeof launch.command === 'string'
737 > ? { name: this.definition.label, command: [launch.command, ...(launch.args ?? []).filter(arg => typeof arg === 'string')] }
738 : { name: this.definition.label };
740 return { name: this.definition.label };
743 > private _evaluatePolicy(identity: IMcpServerIdentity): McpConnectionState.Error | undefined {
744 > const allowed = this._allowedMcpServersService.isServerAllowed(identity); mcpServer.ts ×23
745 > return allowed === true ? undefined : { state: McpConnectionState.Kind.Error, message: allowed.value };
746 > }
748 > /**
749 > * Whether the URL/command fields matched by the policy still contain unresolved `${...}`
750 > * configuration variables. When they do, matching against allow/deny URL or command rules is
751 > * unreliable, so the block is deferred until the launch is resolved. The server name is used
752 > * verbatim and is not considered here.
753 > */
754 > private static _hasUnresolvedVariables(identity: IMcpServerIdentity): boolean {
755 > const variableMarker = ConfigurationResolverExpression.VARIABLE_LHS; mcpServer.ts ×23
756 > return !!identity.url?.includes(variableMarker) || !!identity.command?.some(arg => arg.includes(variableMarker));
757 > }
759 > public start({ interaction, autoTrustChanges, promptType, debug, errorOnUserInteraction }: IMcpServerStartOpts = {}): Promise<McpConnectionState> {
760 > interaction?.participants.set(this.definition.id, { s: 'unknown' }); mcpServer.ts ×36
761 >
762 > return this._connectionSequencer.queue<McpConnectionState>(async () => {
763 > // Evaluated against the definition here (no connection yet). `_policyBlock` re-evaluates
764 > // against the resolved launch once the connection exists (checked again below).
765 > const preStartBlock = this._policyBlock.get();
766 > if (preStartBlock) {
767 return preStartBlock;
768 }
770 > const activationEvent = mcpActivationEvent(this.collection.id.slice(extensionMcpCollectionPrefix.length));
771 > if (this._requiresExtensionActivation && !this._extensionService.activationEventIsDone(activationEvent)) {
772 await this._extensionService.activateByEvent(activationEvent);
773 await Promise.all(this._mcpRegistry.delegates.get()
774 .map(r => r.waitForInitialProviderPromises()));
775 // This can happen if the server was created from a cached MCP server seen
776 // from an extension, but then it wasn't registered when the extension activated.
777 if (this._store.isDisposed) {
778 return { state: McpConnectionState.Kind.Stopped };
779 }
780 }
782 > let connection = this._connection.get();
783 > this._isQuietStart = !!errorOnUserInteraction;
784 > if (connection && McpConnectionState.canBeStarted(connection.state.get().state)) {
785 connection.dispose();
786 connection = undefined;
787 this._connection.set(connection, undefined);
788 }
790 > if (!connection) {
791 > this._lastModeDebugged = !!debug;
792 > const that = this;
793 > connection = await this._mcpRegistry.resolveConnection({
794 > interaction,
795 > autoTrustChanges,
796 > promptType,
797 > trustNonceBearer: {
798 > get trustedAtNonce() { return that.trustedAtNonce; },
799 > set trustedAtNonce(nonce: string | undefined) { that.trustedAtNonce = nonce; }
800 > },
801 > logger: this._logger,
802 > collectionRef: this.collection,
803 > definitionRef: this.definition,
804 > debug,
805 > errorOnUserInteraction,
806 > taskManager: this._taskManager,
807 > });
808 > if (!connection) {
809 return { state: McpConnectionState.Kind.Stopped };
810 }
812 > if (this._store.isDisposed) {
813 connection.dispose();
814 return { state: McpConnectionState.Kind.Stopped };
815 }
817 > this._connection.set(connection, undefined);
818 >
819 > if (connection.definition.devMode) {
820 this.showOutput();
821 }
823 >
824 > // Re-evaluate the policy against the *resolved* launch definition. Extension activation and
825 > // variable/input substitution during resolution can change the URL or command, so the
826 > // identity that actually launches may differ from the one checked before resolution.
827 > // `_policyBlock` now sees the live connection and uses its resolved launch.
828 > const resolvedBlock = this._policyBlock.get();
829 > if (resolvedBlock) {
830 this._connection.set(undefined, undefined); // dispose the just-resolved connection
831 return resolvedBlock;
832 }
834 > this._potentialSandboxBlocks.length = 0;
835 >
836 > const start = Date.now();
837 > let state = await connection.start({
838 > createMessageRequestHandler: (params, token) => this._samplingService.sample({
839 isDuringToolCall: this.runningToolCalls.size > 0,
840 server: this,
841 params,
842 }, token).then(r => r.sample),
843 > elicitationRequestHandler: async (req, token) => { mcpServer.ts ×36
844 const serverInfo = connection.handler.get()?.serverInfo;
845 if (serverInfo) {
846 this._telemetryService.publicLog2<ElicitationTelemetryData, ElicitationTelemetryClassification>('mcp.elicitationRequested', {
847 serverName: serverInfo.name,
848 serverVersion: serverInfo.version,
849 });
850 }
851
852 const r = await this._elicitationService.elicit(this, Iterable.first(this.runningToolCalls), req, token || CancellationToken.None);
853 r.dispose();
854 return r.value;
855 }
856 > }); mcpServer.ts ×36
857 >
858 > this._telemetryService.publicLog2<ServerBootState, ServerBootStateClassification>('mcp/serverBootState', {
859 > state: McpConnectionState.toKindString(state.state),
860 > time: Date.now() - start,
861 > });
862 >
863 > // MCP servers that need auth can 'start' but will stop with an interaction-needed
864 > // error they first make a request. In this case, wait until the handler fully
865 > // initializes before resolving (throwing if it ends up needing auth)
866 > if (errorOnUserInteraction && state.state === McpConnectionState.Kind.Running) {
867 let disposable: IDisposable;
868 state = await new Promise<McpConnectionState>((resolve, reject) => {
869 disposable = autorun(reader => {
870 const handler = connection.handler.read(reader);
871 if (handler) {
872 resolve(state);
873 }
874
875 const s = connection.state.read(reader);
876 if (s.state === McpConnectionState.Kind.Stopped && s.reason === 'needs-user-interaction') {
877 reject(new UserInteractionRequiredError('auth'));
878 }
879
880 if (!McpConnectionState.isRunning(s)) {
881 resolve(s);
882 }
883 });
884 }).finally(() => disposable.dispose());
885 }
887 > if (state.state === McpConnectionState.Kind.Error) {
888 let disposable: IDisposable;
889 state = await new Promise<McpConnectionState>((resolve, reject) => {
890 disposable = autorun(reader => {
891 const cnx = this._connection.read(reader);
892 const state = cnx?.state.read(reader);
893 if (cnx && state?.state === McpConnectionState.Kind.Error) {
894 if (!this._isQuietStart) {
895 this.showInteractiveError(cnx, state, this._lastModeDebugged);
896 } else {
897 reject(new UserInteractionRequiredError('start'));
898 }
899 }
900 });
901 }).finally(() => disposable.dispose());
902 }
904 > return state;
905 > }).finally(() => {
906 > interaction?.participants.set(this.definition.id, { s: 'resolved' });
907 > });
908 > }
910 > private showInteractiveError(cnx: IMcpServerConnection, error: McpConnectionState.Error, debug?: boolean) {
911 if (cnx.definition.sandboxEnabled) {
912 if (!this.showSandboxConfigSuggestionFromPotentialBlocks(cnx, this._potentialSandboxBlocks)) {
913 this._notificationService.warn(localize('mcpServerError', 'The MCP server {0} could not be started: {1}', cnx.definition.label, error.message));
914 }
915 return;
916 }
917 if (error.code === 'ENOENT' && cnx.launchDefinition.type === McpServerTransportType.Stdio) {
918 let docsLink: string | undefined;
919 switch (cnx.launchDefinition.command) {
920 case 'uvx':
921 docsLink = `https://aka.ms/vscode-mcp-install/uvx`;
922 break;
923 case 'npx':
924 docsLink = `https://aka.ms/vscode-mcp-install/npx`;
925 break;
926 case 'dnx':
927 docsLink = `https://aka.ms/vscode-mcp-install/dnx`;
928 break;
929 case 'dotnet':
930 docsLink = `https://aka.ms/vscode-mcp-install/dotnet`;
931 break;
932 }
933
934 const options: IPromptChoice[] = [{
935 label: localize('mcp.command.showOutput', "Show Output"),
936 run: () => this.showOutput(),
937 }];
938
939 if (cnx.definition.devMode?.debug?.type === 'debugpy' && debug) {
940 this._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, {
941 label: localize('mcpViewDocs', 'View Docs'),
942 run: () => this._openerService.open(URI.parse('https://aka.ms/vscode-mcp-install/debugpy')),
943 }]);
944 return;
945 }
946
947 if (docsLink) {
948 options.push({
949 label: localize('mcpServerInstall', 'Install {0}', cnx.launchDefinition.command),
950 run: () => this._openerService.open(URI.parse(docsLink)),
951 });
952 }
953
954 this._notificationService.prompt(Severity.Error, localize('mcpServerNotFound', 'The command "{0}" needed to run {1} was not found.', cnx.launchDefinition.command, cnx.definition.label), options);
955 } else {
956 this._notificationService.warn(localize('mcpServerError', 'The MCP server {0} could not be started: {1}', cnx.definition.label, error.message));
957 }
958 }
960 > public showSandboxConfigSuggestionFromPotentialBlocks(cnx: IMcpServerConnection, potentialBlocks: readonly IMcpPotentialSandboxBlock[]): boolean {
961 if (!cnx.definition.sandboxEnabled || !potentialBlocks.length || this._isSandboxSuggestionDialogVisible) {
962 return false;
963 }
964 if (this._isQuietStart) {
965 throw new UserInteractionRequiredError('sandbox-suggestion');
966 }
967
968 const existingSandboxConfig = this._fullDefinitions.get().collection?.sandbox;
969 const suggestion = this._mcpSandboxService.getSandboxConfigSuggestionMessage(cnx.definition.label, potentialBlocks, existingSandboxConfig);
970 if (!suggestion) {
971 // clear potential blocks as there are no suggestions for them.
972 this._removePotentialSandboxBlocks(potentialBlocks);
973 return false;
974 }
975
976 this._confirmAndApplySandboxConfigSuggestion(cnx, potentialBlocks, suggestion);
977 return true;
978 }
980 > private _confirmAndApplySandboxConfigSuggestion(cnx: IMcpServerConnection, potentialBlocks: readonly IMcpPotentialSandboxBlock[], suggestion: NonNullable<ReturnType<IMcpSandboxService['getSandboxConfigSuggestionMessage']>>): void {
981 const mcpResource = cnx.definition.presentation?.origin?.uri ?? this.collection.presentation?.origin;
982 const configTarget = this._fullDefinitions.get().collection?.configTarget;
983 this._isSandboxSuggestionDialogVisible = true;
984
985 void this._dialogService.confirm({
986 type: 'warning',
987 message: localize('mcpSandboxSuggestion.confirm.message', "Update sandbox configuration in mcp.json for {0}?", cnx.definition.label),
988 detail: suggestion.message,
989 primaryButton: localize('mcpSandboxSuggestion.confirm.yes', "Yes"),
990 cancelButton: localize('mcpSandboxSuggestion.confirm.no', "No"),
991 }).then(async result => {
992 if (!result.confirmed) {
993 return;
994 }
995
996 if (!mcpResource || configTarget === undefined) {
997 this._notificationService.warn(localize('mcpSandboxSuggestion.apply.unavailable', "Couldn't determine where to update sandbox configuration for {0}.", cnx.definition.label));
998 return;
999 }
1000
1001 try {
1002 const updated = await this._mcpSandboxService.applySandboxConfigSuggestion(cnx.definition, mcpResource, configTarget, potentialBlocks, suggestion.sandboxConfig);
1003 if (updated) {
1004 this._removePotentialSandboxBlocks(potentialBlocks);
1005 this._notificationService.info(localize('mcpSandboxSuggestion.apply.success', "Updated sandbox configuration for {0} in mcp.json. Restart server.", cnx.definition.label));
1006 }
1007 } catch (e) {
1008 this._notificationService.error(localize('mcpSandboxSuggestion.apply.error', "Failed to update sandbox configuration for {0}: {1}", cnx.definition.label, e instanceof Error ? e.message : String(e)));
1009 }
1010 }).finally(() => {
1011 this._isSandboxSuggestionDialogVisible = false;
1012 });
1013 }
1015 > public recordPotentialSandboxBlock(block: IMcpPotentialSandboxBlock): void {
1016 this._potentialSandboxBlocks.push(block);
1017 if (this._potentialSandboxBlocks.length > 200) {
1018 this._potentialSandboxBlocks.splice(0, this._potentialSandboxBlocks.length - 200);
1019 }
1020
1021 const connection = this._connection.get();
1022 if (connection?.state.get().state === McpConnectionState.Kind.Running) {
1023 this.showSandboxConfigSuggestionFromPotentialBlocks(connection, this._potentialSandboxBlocks);
1024 }
1025 }
1027 > private _removePotentialSandboxBlocks(blocks: readonly IMcpPotentialSandboxBlock[]): void {
1028 if (!blocks.length || !this._potentialSandboxBlocks.length) {
1029 return;
1030 }
1031
1032 const toRemove = new Set(blocks);
1033 this._potentialSandboxBlocks = this._potentialSandboxBlocks.filter(block => !toRemove.has(block));
1034 }
1036 > public stop(): Promise<void> {
1037 return this._connection.get()?.stop() || Promise.resolve();
1038 }
1040 > /** Waits for any ongoing tools to be refreshed before resolving. */
1041 > public awaitToolRefresh() {
1042 return new Promise<void>(resolve => {
1043 autorunSelfDisposable(reader => {
1044 const promise = this._tools.fromServerPromise.read(reader);
1045 const result = promise?.promiseResult.read(reader);
1046 if (result) {
1047 resolve();
1048 }
1049 });
1050 });
1051 }
1053 > private resetLiveData() {
1054 > transaction(tx => { mcpServer.ts ×36
1055 > this._tools.fromServerPromise.set(undefined, tx);
1056 > this._prompts.fromServerPromise.set(undefined, tx);
1057 > });
1058 > }
1060 > private async _normalizeTool(originalTool: MCP.Tool): Promise<ValidatedMcpTool | { error: string[] }> {
1061 // Parse MCP Apps UI metadata from _meta.ui
1062 const uiMeta = originalTool._meta?.ui as McpApps.McpUiToolMeta | undefined;
1063
1064 // Compute visibility from _meta.ui.visibility, defaulting to Model | App
1065 let visibility: McpToolVisibility = McpToolVisibility.Model | McpToolVisibility.App;
1066 if (uiMeta?.visibility && Array.isArray(uiMeta.visibility)) {
1067 visibility &= 0;
1068
1069 if (uiMeta.visibility.includes('model')) {
1070 visibility |= McpToolVisibility.Model;
1071 }
1072 if (uiMeta.visibility.includes('app')) {
1073 visibility |= McpToolVisibility.App;
1074 }
1075 }
1076
1077 const tool: ValidatedMcpTool = {
1078 ...originalTool,
1079 serverToolName: originalTool.name,
1080 _icons: this._parseIcons(originalTool),
1081 visibility,
1082 uiResourceUri: uiMeta?.resourceUri,
1083 };
1084 if (!tool.description) {
1085 // Ensure a description is provided for each tool, #243919
1086 this._logger.warn(`Tool ${tool.name} does not have a description. Tools must be accurately described to be called`);
1087 tool.description = '<empty>';
1088 }
1089
1090 if (toolInvalidCharRe.test(tool.name)) {
1091 this._logger.warn(`Tool ${JSON.stringify(tool.name)} is invalid. Tools names may only contain [a-z0-9_-]`);
1092 tool.name = tool.name.replace(toolInvalidCharRe, '_');
1093 }
1094
1095 // Per MCP spec, properties is optional. But JSON Schema Draft 7 requires
1096 // it for object types. Normalize the schema to include an empty properties
1097 // object if not present. https://github.com/microsoft/vscode/issues/251723
1098 if (tool.inputSchema && !tool.inputSchema.properties) {
1099 tool.inputSchema = { ...tool.inputSchema, properties: {} };
1100 }
1101
1102 type JsonDiagnostic = { message: string; range: { line: number; character: number }[] };
1103
1104 let diagnostics: JsonDiagnostic[] = [];
1105 const toolJson = JSON.stringify(tool.inputSchema);
1106 try {
1107 const schemaUri = URI.parse('https://json-schema.org/draft-07/schema');
1108 diagnostics = await this._commandService.executeCommand<JsonDiagnostic[]>('json.validate', schemaUri, toolJson) || [];
1109 } catch (e) {
1110 // ignored (error in json extension?);
1111 }
1112
1113 if (!diagnostics.length) {
1114 return tool;
1115 }
1116
1117 // because it's all one line from JSON.stringify, we can treat characters as offsets.
1118 const tree = json.parseTree(toolJson);
1119 const messages = diagnostics.map(d => {
1120 const node = json.findNodeAtOffset(tree, d.range[0].character);
1121 const path = node && `/${json.getNodePath(node).join('/')}`;
1122 return d.message + (path ? ` (at ${path})` : '');
1123 });
1124
1125 return { error: messages };
1126 }
1128 > private async _getValidatedTools(tools: MCP.Tool[]): Promise<ValidatedMcpTool[]> {
1129 let error = '';
1130
1131 const validations = await Promise.all(tools.map(t => this._normalizeTool(t)));
1132 const validated: ValidatedMcpTool[] = [];
1133 for (const [i, result] of validations.entries()) {
1134 if ('error' in result) {
1135 error += localize('mcpBadSchema.tool', 'Tool `{0}` has invalid JSON parameters:', tools[i].name) + '\n';
1136 for (const message of result.error) {
1137 error += `\t- ${message}\n`;
1138 }
1139 error += `\t- Schema: ${JSON.stringify(tools[i].inputSchema)}\n\n`;
1140 } else {
1141 validated.push(result);
1142 }
1143 }
1144
1145 if (error) {
1146 this._logger.warn(`${tools.length - validated.length} tools have invalid JSON schemas and will be omitted`);
1147 warnInvalidTools(this._instantiationService, this.definition.label, error);
1148 }
1149
1150 return validated;
1151 }
1153 > /**
1154 > * Parses incoming MCP icons and returns the resulting 'stored' record. Note
1155 > * that this requires an active MCP server connection since we validate
1156 > * against some of that connection's data. The icons may however be stored
1157 > * and rehydrated later.
1158 > */
1159 > private _parseIcons(icons: MCP.Icons) {
1160 > const cnx = this._connection.get(); mcpServer.ts ×36
1161 > if (!cnx) {
1162 return [];
1163 }
1165 > return parseAndValidateMcpIcon(icons, cnx.launchDefinition, this._logger);
1166 > }
1168 > private _setServerTools(nonce: string | undefined, toolsPromise: Promise<MCP.Tool[]>, tx: ITransaction | undefined) {
1169 > const toolPromiseSafe = toolsPromise.then(async tools => { mcpServer.ts ×36
1170 this._logger.info(`Discovered ${tools.length} tools`);
1171 const data = await this._getValidatedTools(tools);
1172 this._primitiveCache.store(this.definition.id, { tools: data, nonce });
1173 return { data, nonce };
1174 > }); mcpServer.ts ×36
1175 > this._tools.fromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx);
1176 > return toolPromiseSafe;
1177 > }
1179 > private _setServerPrompts(nonce: string | undefined, promptsPromise: Promise<MCP.Prompt[]>, tx: ITransaction | undefined) {
1180 > const promptsPromiseSafe = promptsPromise.then((result): { data: StoredMcpPrompt[]; nonce: string | undefined } => { mcpServer.ts ×36
1181 > const data: StoredMcpPrompt[] = result.map(prompt => ({
1182 ...prompt,
1183 _icons: this._parseIcons(prompt)
1184 > })); mcpServer.ts ×36
1185 > this._primitiveCache.store(this.definition.id, { prompts: data, nonce });
1186 > return { data, nonce };
1187 > });
1188 >
1189 > this._prompts.fromServerPromise.set(new ObservablePromise(promptsPromiseSafe), tx);
1190 > return promptsPromiseSafe;
1191 > }
1193 > private _toStoredMetadata(serverInfo?: MCP.Implementation, instructions?: string): StoredServerMetadata {
1194 > return { mcpServer.ts ×36
1195 > serverName: serverInfo ? serverInfo.title || serverInfo.name : undefined,
1196 > serverInstructions: instructions,
1197 > serverIcons: serverInfo ? this._parseIcons(serverInfo) : undefined,
1198 > };
1199 > }
1201 > private _setServerMetadata(
1202 > nonce: string | undefined, mcpServer.ts ×36
1203 > { serverInfo, instructions, capabilities }: { serverInfo: MCP.Implementation; instructions: string | undefined; capabilities: MCP.ServerCapabilities },
1204 > tx: ITransaction | undefined,
1205 > ) {
1206 > const serverMetadata: StoredServerMetadata = this._toStoredMetadata(serverInfo, instructions);
1207 > this._serverMetadata.fromServerPromise.set(ObservablePromise.resolved({ nonce, data: serverMetadata }), tx);
1208 >
1209 > const capabilitiesEncoded = encodeCapabilities(capabilities);
1210 > this._capabilities.fromServerPromise.set(ObservablePromise.resolved({ data: capabilitiesEncoded, nonce }), tx);
1211 > this._primitiveCache.store(this.definition.id, { ...serverMetadata, nonce, capabilities: capabilitiesEncoded });
1212 > }
1214 > private _populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) {
1215 > const cts = new CancellationTokenSource(); mcpServer.ts ×36
1216 > store.add(toDisposable(() => cts.dispose(true)));
1217 >
1218 > const updateTools = (tx: ITransaction | undefined) => {
1219 > const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]);
1220 > return this._setServerTools(cacheNonce, toolPromise, tx);
1221 > };
1222 >
1223 > const updatePrompts = (tx: ITransaction | undefined) => {
1224 > const promptsPromise = handler.capabilities.prompts ? handler.listPrompts({}, cts.token) : Promise.resolve([]);
1225 > return this._setServerPrompts(cacheNonce, promptsPromise, tx);
1226 > };
1227 >
1228 > store.add(handler.onDidChangeToolList(() => {
1229 this._logger.info('Tool list changed, refreshing tools...');
1230 updateTools(undefined);
1231 > })); mcpServer.ts ×36
1232 >
1233 > store.add(handler.onDidChangePromptList(() => {
1234 this._logger.info('Prompts list changed, refreshing prompts...');
1235 updatePrompts(undefined);
1236 > })); mcpServer.ts ×36
1237 >
1238 > transaction(tx => {
1239 > this._setServerMetadata(cacheNonce, { serverInfo: handler.serverInfo, instructions: handler.serverInstructions, capabilities: handler.capabilities }, tx);
1240 > updatePrompts(tx);
1241 > const toolUpdate = updateTools(tx);
1242 >
1243 > toolUpdate.then(tools => {
1244 this._telemetryService.publicLog2<ServerBootData, ServerBootClassification>('mcp/serverBoot', {
1245 supportsLogging: !!handler.capabilities.logging,
1246 supportsPrompts: !!handler.capabilities.prompts,
1247 supportsResources: !!handler.capabilities.resources,
1248 toolCount: tools.data.length,
1249 serverName: handler.serverInfo.name,
1250 serverVersion: handler.serverInfo.version,
1251 });
1252 > }); mcpServer.ts ×36
1253 > });
1254 > }
1256 >
1257 > class McpPrompt implements IMcpPrompt {
1258 > readonly id: string;
1259 > readonly name: string;
1260 > readonly description?: string;
1261 > readonly title?: string;
1262 > readonly arguments: readonly MCP.PromptArgument[];
1263 > readonly icons: IMcpIcons;
1264 >
1265 > constructor(
1266 private readonly _server: McpServer,
1267 private readonly _definition: StoredMcpPrompt,
1268 ) {
1269 this.id = mcpPromptReplaceSpecialChars(this._server.definition.label + '.' + _definition.name);
1270 this.name = _definition.name;
1271 this.title = _definition.title;
1272 this.description = _definition.description;
1273 this.arguments = _definition.arguments || [];
1274 this.icons = McpIcons.fromStored(this._definition._icons);
1275 }
1277 > async resolve(args: Record<string, string>, token?: CancellationToken): Promise<IMcpPromptMessage[]> {
1278 const result = await McpServer.callOn(this._server, h => h.getPrompt({ name: this._definition.name, arguments: args }, token), token);
1279 return result.messages;
1280 }
1282 > async complete(argument: string, prefix: string, alreadyResolved: Record<string, string>, token?: CancellationToken): Promise<string[]> {
1283 const result = await McpServer.callOn(this._server, h => h.complete({
1284 ref: { type: 'ref/prompt', name: this._definition.name },
1285 argument: { name: argument, value: prefix },
1286 context: { arguments: alreadyResolved },
1287 }, token), token);
1288 return result.completion.values;
1289 }
1291 >
1292 > function encodeCapabilities(cap: MCP.ServerCapabilities): McpCapability { mcpServer.ts ×36
1293 > let out = 0;
1294 > if (cap.logging) { out |= McpCapability.Logging; }
1295 > if (cap.completions) { out |= McpCapability.Completions; }
1296 > if (cap.prompts) {
1297 out |= McpCapability.Prompts;
1298 if (cap.prompts.listChanged) {
1299 out |= McpCapability.PromptsListChanged;
1300 }
1301 }
1302 > if (cap.resources) { mcpServer.ts ×36
1303 > out |= McpCapability.Resources;
1304 > if (cap.resources.subscribe) {
1305 out |= McpCapability.ResourcesSubscribe;
1306 }
1307 > if (cap.resources.listChanged) { mcpServer.ts ×36
1308 out |= McpCapability.ResourcesListChanged;
1309 }
1311 > if (cap.tools) {
1312 > out |= McpCapability.Tools;
1313 > if (cap.tools.listChanged) {
1314 out |= McpCapability.ToolsListChanged;
1315 }
1317 > return out;
1318 > }
1320 > export class McpTool implements IMcpTool {
1321 >
1322 > readonly id: string;
1323 > readonly referenceName: string;
1324 > readonly icons: IMcpIcons;
1325 > readonly visibility: McpToolVisibility;
1326 >
1327 > public get definition(): MCP.Tool { return this._definition; }
1328 > public get uiResourceUri(): string | undefined { return this._definition.uiResourceUri; }
1329 >
1330 > constructor(
1331 private readonly _server: McpServer,
1332 idPrefix: string,
1333 private readonly _definition: ValidatedMcpTool,
1334 @IMcpElicitationService private readonly _elicitationService: IMcpElicitationService,
1335 ) {
1336 this.referenceName = _definition.name.replaceAll('.', '_');
1337 this.id = (idPrefix + _definition.name).replaceAll('.', '_').slice(0, McpToolName.MaxLength);
1338 this.icons = McpIcons.fromStored(this._definition._icons);
1339 this.visibility = _definition.visibility ?? (McpToolVisibility.Model | McpToolVisibility.App);
1340 }
1342 > async call(params: Record<string, unknown>, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {
1343 if (context) { this._server.runningToolCalls.add(context); }
1344 try {
1345 return await this._callWithProgress(params, undefined, context, token);
1346 } finally {
1347 if (context) { this._server.runningToolCalls.delete(context); }
1348 }
1349 }
1351 > async callWithProgress(params: Record<string, unknown>, progress: ToolProgress, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {
1352 if (context) { this._server.runningToolCalls.add(context); }
1353 try {
1354 return await this._callWithProgress(params, progress, context, token);
1355 } finally {
1356 if (context) { this._server.runningToolCalls.delete(context); }
1357 }
1358 }
1360 > _callWithProgress(params: Record<string, unknown>, progress: ToolProgress | undefined, context?: IMcpToolCallContext, token = CancellationToken.None, allowRetry = true): Promise<MCP.CallToolResult> {
1361 // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it.
1362 const name = this._definition.serverToolName ?? this._definition.name;
1363 const progressToken = progress ? generateUuid() : undefined;
1364 const store = new DisposableStore();
1365
1366 return McpServer.callOn(this._server, async h => {
1367 if (progress) {
1368 store.add(h.onDidReceiveProgressNotification((e) => {
1369 if (e.params.progressToken === progressToken) {
1370 progress.report({
1371 message: e.params.message,
1372 progress: e.params.total !== undefined && e.params.progress !== undefined ? e.params.progress / e.params.total : undefined,
1373 });
1374 }
1375 }));
1376 }
1377
1378 const meta: Record<string, unknown> = { progressToken };
1379 if (context?.chatSessionResource) {
1380 meta['vscode.conversationId'] = chatSessionResourceToId(context.chatSessionResource);
1381 }
1382 if (context?.chatRequestId) {
1383 meta['vscode.requestId'] = context.chatRequestId;
1384 }
1385 // Propagate W3C trace context to the MCP server (MCP SEP-414) so server-side
1386 // spans can be correlated with the client trace.
1387 if (context?.traceparent) {
1388 meta['traceparent'] = context.traceparent;
1389 if (context.tracestate) {
1390 meta['tracestate'] = context.tracestate;
1391 }
1392 }
1393
1394 const taskHint = this._definition.execution?.taskSupport;
1395 const serverSupportsTasksForTools = h.capabilities.tasks?.requests?.tools?.call !== undefined;
1396 const shouldUseTask = serverSupportsTasksForTools && (taskHint === 'required' || taskHint === 'optional');
1397
1398 try {
1399 const result = await h.callTool({
1400 name,
1401 arguments: params,
1402 task: shouldUseTask ? {} : undefined,
1403 _meta: meta,
1404 }, token, progress ? (message) => progress.report({ message }) : undefined);
1405
1406 // Wait for tools to refresh for dynamic servers (#261611)
1407 await this._server.awaitToolRefresh();
1408
1409 return result;
1410 } catch (err) {
1411 // Handle URL elicitation required error
1412 if (err instanceof MpcResponseError && err.code === MCP.URL_ELICITATION_REQUIRED && allowRetry) {
1413 await this._handleElicitationErr(err, context, token);
1414 return this._callWithProgress(params, progress, context, token, false);
1415 }
1416
1417 const state = this._server.connectionState.get();
1418 if (allowRetry && state.state === McpConnectionState.Kind.Error && state.shouldRetry) {
1419 return this._callWithProgress(params, progress, context, token, false);
1420 } else {
1421 throw err;
1422 }
1423 } finally {
1424 store.dispose();
1425 }
1426 }, token);
1427 }
1429 > private async _handleElicitationErr(err: MpcResponseError, context: IMcpToolCallContext | undefined, token: CancellationToken) {
1430 const elicitations = (err.data as MCP.URLElicitationRequiredError['error']['data'])?.elicitations;
1431 if (Array.isArray(elicitations) && elicitations.length > 0) {
1432 for (const elicitation of elicitations) {
1433 const elicitResult = await this._elicitationService.elicit(this._server, context, elicitation, token);
1434
1435 try {
1436 if (elicitResult.value.action !== 'accept') {
1437 throw err;
1438 }
1439
1440 if (elicitResult.kind === ElicitationKind.URL) {
1441 await elicitResult.wait;
1442 }
1443 } finally {
1444 elicitResult.dispose();
1445 }
1446 }
1447 }
1448 }
1450 > compare(other: IMcpTool): number {
1451 return this._definition.name.localeCompare(other.definition.name);
1452 }
1454 >
1455 function warnInvalidTools(instaService: IInstantiationService, serverName: string, errorText: string) {
1456 instaService.invokeFunction((accessor) => {
1457 const notificationService = accessor.get(INotificationService);
1458 const editorService = accessor.get(IEditorService);
1459 notificationService.notify({
1460 severity: Severity.Warning,
1461 message: localize('mcpBadSchema', 'MCP server `{0}` has tools with invalid parameters which will be omitted.', serverName),
1462 actions: {
1463 primary: [{
1464 class: undefined,
1465 enabled: true,
1466 id: 'mcpBadSchema.show',
1467 tooltip: '',
1468 label: localize('mcpBadSchema.show', 'Show'),
1469 run: () => {
1470 editorService.openEditor({
1471 resource: undefined,
1472 contents: errorText,
1473 });
1474 }
1475 }]
1476 }
1477 });
1478 });
1479 }
1481 > class McpResource implements IMcpResource {
1482 > readonly uri: URI;
1483 > readonly mcpUri: string;
1484 > readonly name: string;
1485 > readonly description: string | undefined;
1486 > readonly mimeType: string | undefined;
1487 > readonly sizeInBytes: number | undefined;
1488 > readonly title: string | undefined;
1489 >
1490 > constructor(
1491 server: McpServer,
1492 original: MCP.Resource,
1493 public readonly icons: IMcpIcons,
1494 ) {
1495 this.mcpUri = original.uri;
1496 this.title = original.title;
1497 this.uri = McpResourceURI.fromServer(server.definition, original.uri);
1498 this.name = original.name;
1499 this.description = original.description;
1500 this.mimeType = original.mimeType;
1501 this.sizeInBytes = original.size;
1502 }
1504 >
1505 > class McpResourceTemplate implements IMcpResourceTemplate {
1506 > readonly name: string;
1507 > readonly title?: string | undefined;
1508 > readonly description?: string;
1509 > readonly mimeType?: string;
1510 > readonly template: UriTemplate;
1511 >
1512 > constructor(
1513 private readonly _server: McpServer,
1514 private readonly _definition: MCP.ResourceTemplate,
1515 public readonly icons: IMcpIcons,
1516 ) {
1517 this.name = _definition.name;
1518 this.description = _definition.description;
1519 this.mimeType = _definition.mimeType;
1520 this.title = _definition.title;
1521 this.template = UriTemplate.parse(_definition.uriTemplate);
1522 }
1524 > public resolveURI(vars: Record<string, unknown>): URI {
1525 const serverUri = this.template.resolve(vars);
1526 return McpResourceURI.fromServer(this._server.definition, serverUri);
1527 }
1529 > async complete(templatePart: string, prefix: string, alreadyResolved: Record<string, string | string[]>, token?: CancellationToken): Promise<string[]> {
1530 const result = await McpServer.callOn(this._server, h => h.complete({
1531 ref: { type: 'ref/resource', uri: this._definition.uriTemplate },
1532 argument: { name: templatePart, value: prefix },
1533 context: {
1534 arguments: mapValues(alreadyResolved, v => Array.isArray(v) ? v.join('/') : v),
1535 },
1536 }, token), token);
1537 return result.completion.values;
1538 }