Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/mcp/common/mcpServer.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
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 { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
11
import { LRUCache } from '../../../../base/common/map.js';
12
import { mapValues } from '../../../../base/common/objects.js';
13
import { autorun, derived, disposableObservableValue, IDerivedReader, IObservable, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js';
14
import { basename } from '../../../../base/common/resources.js';
15
import { URI } from '../../../../base/common/uri.js';
16
import { generateUuid } from '../../../../base/common/uuid.js';
17
import { localize } from '../../../../nls.js';
18
import { ICommandService } from '../../../../platform/commands/common/commands.js';
19
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
20
import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js';
21
import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js';
22
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
23
import { IRemoteAuthorityResolverService } from '../../../../platform/remote/common/remoteAuthorityResolver.js';
24
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
25
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
26
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
27
import { IEditorService } from '../../../services/editor/common/editorService.js';
28
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
29
import { IOutputService } from '../../../services/output/common/output.js';
30
import { ToolProgress } from '../../chat/common/languageModelToolsService.js';
31
import { mcpActivationEvent } from './mcpConfiguration.js';
32
import { McpDevModeServerAttache } from './mcpDevMode.js';
33
import { IMcpRegistry } from './mcpRegistryTypes.js';
34
import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
35
import { extensionMcpCollectionPrefix, IMcpElicitationService, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerTransportType, McpToolName } from './mcpTypes.js';
36
import { MCP } from './modelContextProtocol.js';
37
import { UriTemplate } from './uriTemplate.js';
38
39
type ServerBootData = {
40
supportsLogging: boolean;
41
supportsPrompts: boolean;
42
supportsResources: boolean;
43
toolCount: number;
44
serverName: string;
45
serverVersion: string;
46
};
47
type ServerBootClassification = {
48
owner: 'connor4312';
49
comment: 'Details the capabilities of the MCP server';
50
supportsLogging: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports logging' };
51
supportsPrompts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports prompts' };
52
supportsResources: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports resource' };
53
toolCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tools the server advertises' };
54
serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };
55
serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };
56
};
57
58
type ElicitationTelemetryData = {
59
serverName: string;
60
serverVersion: string;
61
};
62
63
type ElicitationTelemetryClassification = {
64
owner: 'connor4312';
65
comment: 'Triggered when elictation is requested';
66
serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };
67
serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };
68
};
69
70
export type McpServerInstallData = {
71
serverName: string;
72
source: 'gallery' | 'local';
73
scope: string;
74
success: boolean;
75
error?: string;
76
hasInputs: boolean;
77
};
78
79
export type McpServerInstallClassification = {
80
owner: 'connor4312';
81
comment: 'MCP server installation event tracking';
82
serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server being installed' };
83
source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation source (gallery or local)' };
84
scope: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation scope (user, workspace, etc.)' };
85
success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether installation succeeded' };
86
error?: { classification: 'CallstackOrException'; purpose: 'FeatureInsight'; comment: 'Error message if installation failed' };
87
hasInputs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server requires input configuration' };
88
};
89
90
type ServerBootState = {
91
state: string;
92
time: number;
93
};
94
type ServerBootStateClassification = {
95
owner: 'connor4312';
96
comment: 'Details the capabilities of the MCP server';
97
state: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The server outcome' };
98
time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Duration in milliseconds to reach that state' };
99
};
100
101
interface IToolCacheEntry {
102
readonly serverName: string | undefined;
103
readonly serverInstructions: string | undefined;
104
readonly trustedAtNonce: string | undefined;
105
106
readonly nonce: string | undefined;
107
/** Cached tools so we can show what's available before it's started */
108
readonly tools: readonly IValidatedMcpTool[];
109
/** Cached prompts */
110
readonly prompts: readonly MCP.Prompt[] | undefined;
111
/** Cached capabilities */
112
readonly capabilities: McpCapability | undefined;
113
}
114
115
const emptyToolEntry: IToolCacheEntry = {
116
serverName: undefined,
117
serverInstructions: undefined,
118
trustedAtNonce: undefined,
119
nonce: undefined,
120
tools: [],
121
prompts: undefined,
122
capabilities: undefined,
123
};
124
125
interface IServerCacheEntry {
126
readonly servers: readonly McpServerDefinition.Serialized[];
127
}
128
129
const toolInvalidCharRe = /[^a-z0-9_-]/gi;
130
131
export class McpServerMetadataCache extends Disposable {
132
private didChange = false;
133
private readonly cache = new LRUCache<string, IToolCacheEntry>(128);
134
private readonly extensionServers = new Map</* collection ID */string, IServerCacheEntry>();
135
136
constructor(
137
scope: StorageScope,
138
@IStorageService storageService: IStorageService,
139
) {
140
super();
141
142
type StoredType = {
143
extensionServers: [string, IServerCacheEntry][];
144
serverTools: [string, IToolCacheEntry][];
145
};
146
147
const storageKey = 'mcpToolCache';
148
this._register(storageService.onWillSaveState(() => {
149
if (this.didChange) {
150
storageService.store(storageKey, {
151
extensionServers: [...this.extensionServers],
152
serverTools: this.cache.toJSON(),
153
} satisfies StoredType, scope, StorageTarget.MACHINE);
154
this.didChange = false;
155
}
156
}));
157
158
try {
159
const cached: StoredType | undefined = storageService.getObject(storageKey, scope);
160
this.extensionServers = new Map(cached?.extensionServers ?? []);
161
cached?.serverTools?.forEach(([k, v]) => this.cache.set(k, v));
162
} catch {
163
// ignored
164
}
165
}
166
167
/** Resets the cache for primitives and extension servers */
168
reset() {
169
this.cache.clear();
170
this.extensionServers.clear();
171
this.didChange = true;
172
}
173
174
/** Gets cached primitives for a server (used before a server is running) */
175
get(definitionId: string) {
176
return this.cache.get(definitionId);
177
}
178
179
/** Sets cached primitives for a server */
180
store(definitionId: string, entry: Partial<IToolCacheEntry>): void {
181
const prev = this.get(definitionId) || emptyToolEntry;
182
this.cache.set(definitionId, { ...prev, ...entry });
183
this.didChange = true;
184
}
185
186
/** Gets cached servers for a collection (used for extensions, before the extension activates) */
187
getServers(collectionId: string) {
188
return this.extensionServers.get(collectionId);
189
}
190
191
/** Sets cached servers for a collection */
192
storeServers(collectionId: string, entry: IServerCacheEntry | undefined): void {
193
if (entry) {
194
this.extensionServers.set(collectionId, entry);
195
} else {
196
this.extensionServers.delete(collectionId);
197
}
198
this.didChange = true;
199
}
200
}
201
202
interface IValidatedMcpTool extends MCP.Tool {
203
/**
204
* Tool name as published by the MCP server. This may
205
* be different than the one in {@link definition} due to name normalization
206
* in {@link McpServer._getValidatedTools}.
207
*/
208
serverToolName: string;
209
}
210
211
interface ServerMetadata {
212
readonly serverName: string | undefined;
213
readonly serverInstructions: string | undefined;
214
}
215
216
class CachedPrimitive<T, C> {
217
constructor(
218
private readonly _definitionId: string,
219
private readonly _cache: McpServerMetadataCache,
220
private readonly _fromCache: (entry: IToolCacheEntry) => C,
221
private readonly _toT: (values: C, reader: IDerivedReader<void>) => T,
222
private readonly defaultValue: C,
223
) { }
224
225
public get fromCache(): { nonce: string | undefined; data: C } | undefined {
226
const c = this._cache.get(this._definitionId);
227
return c ? { data: this._fromCache(c), nonce: c.nonce } : undefined;
228
}
229
230
public readonly fromServerPromise = observableValue<ObservablePromise<{
231
readonly data: C;
232
readonly nonce: string | undefined;
233
}> | undefined>(this, undefined);
234
235
private readonly fromServer = derived(reader => this.fromServerPromise.read(reader)?.promiseResult.read(reader)?.data);
236
237
public readonly value: IObservable<T> = derived(reader => {
238
const serverTools = this.fromServer.read(reader);
239
const definitions = serverTools?.data ?? this.fromCache?.data ?? this.defaultValue;
240
return this._toT(definitions, reader);
241
});
242
}
243
244
export class McpServer extends Disposable implements IMcpServer {
245
/**
246
* Helper function to call the function on the handler once it's online. The
247
* connection started if it is not already.
248
*/
249
public static async callOn<R>(server: IMcpServer, fn: (handler: McpServerRequestHandler) => Promise<R>, token: CancellationToken = CancellationToken.None): Promise<R> {
250
await server.start({ promptType: 'all-untrusted' }); // idempotent
251
252
let ranOnce = false;
253
let d: IDisposable;
254
255
const callPromise = new Promise<R>((resolve, reject) => {
256
257
d = autorun(reader => {
258
const connection = server.connection.read(reader);
259
if (!connection || ranOnce) {
260
return;
261
}
262
263
const handler = connection.handler.read(reader);
264
if (!handler) {
265
const state = connection.state.read(reader);
266
if (state.state === McpConnectionState.Kind.Error) {
267
reject(new McpConnectionFailedError(`MCP server could not be started: ${state.message}`));
268
return;
269
} else if (state.state === McpConnectionState.Kind.Stopped) {
270
reject(new McpConnectionFailedError('MCP server has stopped'));
271
return;
272
} else {
273
// keep waiting for handler
274
return;
275
}
276
}
277
278
resolve(fn(handler));
279
ranOnce = true; // aggressive prevent multiple racey calls, don't dispose because autorun is sync
280
});
281
});
282
283
return raceCancellationError(callPromise, token).finally(() => d.dispose());
284
}
285
286
private readonly _connectionSequencer = new Sequencer();
287
private readonly _connection = this._register(disposableObservableValue<IMcpServerConnection | undefined>(this, undefined));
288
289
public readonly connection = this._connection;
290
public readonly connectionState: IObservable<McpConnectionState> = derived(reader => this._connection.read(reader)?.state.read(reader) ?? { state: McpConnectionState.Kind.Stopped });
291
292
293
private readonly _capabilities = observableValue<number | undefined>('mcpserver.capabilities', undefined);
294
public get capabilities() {
295
return this._capabilities;
296
}
297
298
private readonly _tools: CachedPrimitive<readonly IMcpTool[], readonly IValidatedMcpTool[]>;
299
public get tools() {
300
return this._tools.value;
301
}
302
303
private readonly _prompts: CachedPrimitive<readonly IMcpPrompt[], readonly MCP.Prompt[]>;
304
public get prompts() {
305
return this._prompts.value;
306
}
307
308
private readonly _serverMetadata: CachedPrimitive<ServerMetadata, ServerMetadata | undefined>;
309
public get serverMetadata() {
310
return this._serverMetadata.value;
311
}
312
313
public get trustedAtNonce() {
314
return this._primitiveCache.get(this.definition.id)?.trustedAtNonce;
315
}
316
317
public set trustedAtNonce(nonce: string | undefined) {
318
this._primitiveCache.store(this.definition.id, { trustedAtNonce: nonce });
319
}
320
321
private readonly _fullDefinitions: IObservable<{
322
server: McpServerDefinition | undefined;
323
collection: McpCollectionDefinition | undefined;
324
}>;
325
326
public readonly cacheState = derived(reader => {
327
const currentNonce = () => this._fullDefinitions.read(reader)?.server?.cacheNonce;
328
const stateWhenServingFromCache = () => {
329
if (!this._tools.fromCache) {
330
return McpServerCacheState.Unknown;
331
}
332
333
return currentNonce() === this._tools.fromCache.nonce ? McpServerCacheState.Cached : McpServerCacheState.Outdated;
334
};
335
336
const fromServer = this._tools.fromServerPromise.read(reader);
337
const connectionState = this.connectionState.read(reader);
338
const isIdle = McpConnectionState.canBeStarted(connectionState.state) || !fromServer;
339
if (isIdle) {
340
return stateWhenServingFromCache();
341
}
342
343
const fromServerResult = fromServer?.promiseResult.read(reader);
344
if (!fromServerResult) {
345
return this._tools.fromCache ? McpServerCacheState.RefreshingFromCached : McpServerCacheState.RefreshingFromUnknown;
346
}
347
348
if (fromServerResult.error) {
349
return stateWhenServingFromCache();
350
}
351
352
return fromServerResult.data?.nonce === currentNonce() ? McpServerCacheState.Live : McpServerCacheState.Outdated;
353
});
354
355
private readonly _loggerId: string;
356
private readonly _logger: ILogger;
357
private _lastModeDebugged = false;
358
/** Count of running tool calls, used to detect if sampling is during an LM call */
359
public runningToolCalls = new Set<IMcpToolCallContext>();
360
361
constructor(
362
public readonly collection: McpCollectionReference,
363
public readonly definition: McpDefinitionReference,
364
explicitRoots: URI[] | undefined,
365
private readonly _requiresExtensionActivation: boolean | undefined,
366
private readonly _primitiveCache: McpServerMetadataCache,
367
toolPrefix: string,
368
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
369
@IWorkspaceContextService workspacesService: IWorkspaceContextService,
370
@IExtensionService private readonly _extensionService: IExtensionService,
371
@ILoggerService private readonly _loggerService: ILoggerService,
372
@IOutputService private readonly _outputService: IOutputService,
373
@ITelemetryService private readonly _telemetryService: ITelemetryService,
374
@ICommandService private readonly _commandService: ICommandService,
375
@IInstantiationService private readonly _instantiationService: IInstantiationService,
376
@INotificationService private readonly _notificationService: INotificationService,
377
@IOpenerService private readonly _openerService: IOpenerService,
378
@IMcpSamplingService private readonly _samplingService: IMcpSamplingService,
379
@IMcpElicitationService private readonly _elicitationService: IMcpElicitationService,
380
@IRemoteAuthorityResolverService private readonly _remoteAuthorityResolverService: IRemoteAuthorityResolverService,
381
) {
382
super();
383
384
this._fullDefinitions = this._mcpRegistry.getServerDefinition(this.collection, this.definition);
385
this._loggerId = `mcpServer.${definition.id}`;
386
this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` }));
387
388
const that = this;
389
this._register(this._instantiationService.createInstance(McpDevModeServerAttache, this, { get lastModeDebugged() { return that._lastModeDebugged; } }));
390
391
// If the logger is disposed but not deregistered, then the disposed instance
392
// is reused and no-ops. todo@sandy081 this seems like a bug.
393
this._register(toDisposable(() => _loggerService.deregisterLogger(this._loggerId)));
394
395
// 1. Reflect workspaces into the MCP roots
396
const workspaces = explicitRoots
397
? observableValue(this, explicitRoots.map(uri => ({ uri, name: basename(uri) })))
398
: observableFromEvent(
399
this,
400
workspacesService.onDidChangeWorkspaceFolders,
401
() => workspacesService.getWorkspace().folders,
402
);
403
404
const workspacesWithCanonicalURIs = derived(reader => {
405
const folders = workspaces.read(reader);
406
return new ObservablePromise((async () => {
407
let uris = folders.map(f => f.uri);
408
try {
409
uris = await Promise.all(uris.map(u => this._remoteAuthorityResolverService.getCanonicalURI(u)));
410
} catch (error) {
411
this._logger.error(`Failed to resolve workspace folder URIs: ${error}`);
412
}
413
return uris.map((uri, i): MCP.Root => ({ uri: uri.toString(), name: folders[i].name }));
414
})());
415
}).recomputeInitiallyAndOnChange(this._store);
416
417
this._register(autorun(reader => {
418
const cnx = this._connection.read(reader)?.handler.read(reader);
419
if (!cnx) {
420
return;
421
}
422
423
const roots = workspacesWithCanonicalURIs.read(reader).promiseResult.read(reader);
424
if (roots?.data) {
425
cnx.roots = roots.data;
426
}
427
}));
428
429
// 2. Populate this.tools when we connect to a server.
430
this._register(autorun(reader => {
431
const cnx = this._connection.read(reader);
432
const handler = cnx?.handler.read(reader);
433
if (handler) {
434
this.populateLiveData(handler, cnx?.definition.cacheNonce, reader.store);
435
} else if (this._tools) {
436
this.resetLiveData();
437
}
438
}));
439
440
// 3. Publish tools
441
this._tools = new CachedPrimitive<readonly IMcpTool[], readonly IValidatedMcpTool[]>(
442
this.definition.id,
443
this._primitiveCache,
444
(entry) => entry.tools,
445
(entry) => entry.map(def => new McpTool(this, toolPrefix, def)).sort((a, b) => a.compare(b)),
446
[],
447
);
448
449
// 4. Publish promtps
450
this._prompts = new CachedPrimitive<readonly IMcpPrompt[], readonly MCP.Prompt[]>(
451
this.definition.id,
452
this._primitiveCache,
453
(entry) => entry.prompts || [],
454
(entry) => entry.map(e => new McpPrompt(this, e)),
455
[],
456
);
457
458
this._serverMetadata = new CachedPrimitive<ServerMetadata, ServerMetadata | undefined>(
459
this.definition.id,
460
this._primitiveCache,
461
(entry) => ({ serverName: entry.serverName, serverInstructions: entry.serverInstructions }),
462
(entry) => ({ serverName: entry?.serverName, serverInstructions: entry?.serverInstructions }),
463
undefined,
464
);
465
466
this._capabilities.set(this._primitiveCache.get(this.definition.id)?.capabilities, undefined);
467
}
468
469
public readDefinitions(): IObservable<{ server: McpServerDefinition | undefined; collection: McpCollectionDefinition | undefined }> {
470
return this._fullDefinitions;
471
}
472
473
public showOutput(preserveFocus?: boolean) {
474
this._loggerService.setVisibility(this._loggerId, true);
475
return this._outputService.showChannel(this._loggerId, preserveFocus);
476
}
477
478
public resources(token?: CancellationToken): AsyncIterable<IMcpResource[]> {
479
const cts = new CancellationTokenSource(token);
480
return new AsyncIterableProducer<IMcpResource[]>(async emitter => {
481
await McpServer.callOn(this, async (handler) => {
482
for await (const resource of handler.listResourcesIterable({}, cts.token)) {
483
emitter.emitOne(resource.map(r => new McpResource(this, r)));
484
if (cts.token.isCancellationRequested) {
485
return;
486
}
487
}
488
});
489
}, () => cts.dispose(true));
490
}
491
492
public resourceTemplates(token?: CancellationToken): Promise<IMcpResourceTemplate[]> {
493
return McpServer.callOn(this, async (handler) => {
494
const templates = await handler.listResourceTemplates({}, token);
495
return templates.map(t => new McpResourceTemplate(this, t));
496
}, token);
497
}
498
499
public start({ interaction, autoTrustChanges, promptType, debug }: IMcpServerStartOpts = {}): Promise<McpConnectionState> {
500
interaction?.participants.set(this.definition.id, { s: 'unknown' });
501
502
return this._connectionSequencer.queue<McpConnectionState>(async () => {
503
const activationEvent = mcpActivationEvent(this.collection.id.slice(extensionMcpCollectionPrefix.length));
504
if (this._requiresExtensionActivation && !this._extensionService.activationEventIsDone(activationEvent)) {
505
await this._extensionService.activateByEvent(activationEvent);
506
await Promise.all(this._mcpRegistry.delegates.get()
507
.map(r => r.waitForInitialProviderPromises()));
508
// This can happen if the server was created from a cached MCP server seen
509
// from an extension, but then it wasn't registered when the extension activated.
510
if (this._store.isDisposed) {
511
return { state: McpConnectionState.Kind.Stopped };
512
}
513
}
514
515
let connection = this._connection.get();
516
if (connection && McpConnectionState.canBeStarted(connection.state.get().state)) {
517
connection.dispose();
518
connection = undefined;
519
this._connection.set(connection, undefined);
520
}
521
522
if (!connection) {
523
this._lastModeDebugged = !!debug;
524
const that = this;
525
connection = await this._mcpRegistry.resolveConnection({
526
interaction,
527
autoTrustChanges,
528
promptType,
529
trustNonceBearer: {
530
get trustedAtNonce() { return that.trustedAtNonce; },
531
set trustedAtNonce(nonce: string | undefined) { that.trustedAtNonce = nonce; }
532
},
533
logger: this._logger,
534
collectionRef: this.collection,
535
definitionRef: this.definition,
536
debug,
537
});
538
if (!connection) {
539
return { state: McpConnectionState.Kind.Stopped };
540
}
541
542
if (this._store.isDisposed) {
543
connection.dispose();
544
return { state: McpConnectionState.Kind.Stopped };
545
}
546
547
this._connection.set(connection, undefined);
548
}
549
550
if (connection.definition.devMode) {
551
this.showOutput();
552
}
553
554
const start = Date.now();
555
const state = await connection.start({
556
createMessageRequestHandler: params => this._samplingService.sample({
557
isDuringToolCall: this.runningToolCalls.size > 0,
558
server: this,
559
params,
560
}).then(r => r.sample),
561
elicitationRequestHandler: req => {
562
const serverInfo = connection.handler.get()?.serverInfo;
563
if (serverInfo) {
564
this._telemetryService.publicLog2<ElicitationTelemetryData, ElicitationTelemetryClassification>('mcp.elicitationRequested', {
565
serverName: serverInfo.name,
566
serverVersion: serverInfo.version,
567
});
568
}
569
570
return this._elicitationService.elicit(this, Iterable.first(this.runningToolCalls), req, CancellationToken.None);
571
}
572
});
573
574
this._telemetryService.publicLog2<ServerBootState, ServerBootStateClassification>('mcp/serverBootState', {
575
state: McpConnectionState.toKindString(state.state),
576
time: Date.now() - start,
577
});
578
579
if (state.state === McpConnectionState.Kind.Error) {
580
this.showInteractiveError(connection, state, debug);
581
}
582
583
return state;
584
}).finally(() => {
585
interaction?.participants.set(this.definition.id, { s: 'resolved' });
586
});
587
}
588
589
private showInteractiveError(cnx: IMcpServerConnection, error: McpConnectionState.Error, debug?: boolean) {
590
if (error.code === 'ENOENT' && cnx.launchDefinition.type === McpServerTransportType.Stdio) {
591
let docsLink: string | undefined;
592
switch (cnx.launchDefinition.command) {
593
case 'uvx':
594
docsLink = `https://aka.ms/vscode-mcp-install/uvx`;
595
break;
596
case 'npx':
597
docsLink = `https://aka.ms/vscode-mcp-install/npx`;
598
break;
599
case 'dnx':
600
docsLink = `https://aka.ms/vscode-mcp-install/dnx`;
601
break;
602
case 'dotnet':
603
docsLink = `https://aka.ms/vscode-mcp-install/dotnet`;
604
break;
605
}
606
607
const options: IPromptChoice[] = [{
608
label: localize('mcp.command.showOutput', "Show Output"),
609
run: () => this.showOutput(),
610
}];
611
612
if (cnx.definition.devMode?.debug?.type === 'debugpy' && debug) {
613
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, {
614
label: localize('mcpViewDocs', 'View Docs'),
615
run: () => this._openerService.open(URI.parse('https://aka.ms/vscode-mcp-install/debugpy')),
616
}]);
617
return;
618
}
619
620
if (docsLink) {
621
options.push({
622
label: localize('mcpServerInstall', 'Install {0}', cnx.launchDefinition.command),
623
run: () => this._openerService.open(URI.parse(docsLink)),
624
});
625
}
626
627
this._notificationService.prompt(Severity.Error, localize('mcpServerNotFound', 'The command "{0}" needed to run {1} was not found.', cnx.launchDefinition.command, cnx.definition.label), options);
628
} else {
629
this._notificationService.warn(localize('mcpServerError', 'The MCP server {0} could not be started: {1}', cnx.definition.label, error.message));
630
}
631
}
632
633
public stop(): Promise<void> {
634
return this._connection.get()?.stop() || Promise.resolve();
635
}
636
637
private resetLiveData() {
638
transaction(tx => {
639
this._tools.fromServerPromise.set(undefined, tx);
640
this._prompts.fromServerPromise.set(undefined, tx);
641
});
642
}
643
644
private async _normalizeTool(originalTool: MCP.Tool): Promise<IValidatedMcpTool | { error: string[] }> {
645
const tool: IValidatedMcpTool = { ...originalTool, serverToolName: originalTool.name };
646
if (!tool.description) {
647
// Ensure a description is provided for each tool, #243919
648
this._logger.warn(`Tool ${tool.name} does not have a description. Tools must be accurately described to be called`);
649
tool.description = '<empty>';
650
}
651
652
if (toolInvalidCharRe.test(tool.name)) {
653
this._logger.warn(`Tool ${JSON.stringify(tool.name)} is invalid. Tools names may only contain [a-z0-9_-]`);
654
tool.name = tool.name.replace(toolInvalidCharRe, '_');
655
}
656
657
type JsonDiagnostic = { message: string; range: { line: number; character: number }[] };
658
659
let diagnostics: JsonDiagnostic[] = [];
660
const toolJson = JSON.stringify(tool.inputSchema);
661
try {
662
const schemaUri = URI.parse('https://json-schema.org/draft-07/schema');
663
diagnostics = await this._commandService.executeCommand<JsonDiagnostic[]>('json.validate', schemaUri, toolJson) || [];
664
} catch (e) {
665
// ignored (error in json extension?);
666
}
667
668
if (!diagnostics.length) {
669
return tool;
670
}
671
672
// because it's all one line from JSON.stringify, we can treat characters as offsets.
673
const tree = json.parseTree(toolJson);
674
const messages = diagnostics.map(d => {
675
const node = json.findNodeAtOffset(tree, d.range[0].character);
676
const path = node && `/${json.getNodePath(node).join('/')}`;
677
return d.message + (path ? ` (at ${path})` : '');
678
});
679
680
return { error: messages };
681
}
682
683
private async _getValidatedTools(handler: McpServerRequestHandler, tools: MCP.Tool[]): Promise<IValidatedMcpTool[]> {
684
let error = '';
685
686
const validations = await Promise.all(tools.map(t => this._normalizeTool(t)));
687
const validated: IValidatedMcpTool[] = [];
688
for (const [i, result] of validations.entries()) {
689
if ('error' in result) {
690
error += localize('mcpBadSchema.tool', 'Tool `{0}` has invalid JSON parameters:', tools[i].name) + '\n';
691
for (const message of result.error) {
692
error += `\t- ${message}\n`;
693
}
694
error += `\t- Schema: ${JSON.stringify(tools[i].inputSchema)}\n\n`;
695
} else {
696
validated.push(result);
697
}
698
}
699
700
if (error) {
701
handler.logger.warn(`${tools.length - validated.length} tools have invalid JSON schemas and will be omitted`);
702
warnInvalidTools(this._instantiationService, this.definition.label, error);
703
}
704
705
return validated;
706
}
707
708
private populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) {
709
const cts = new CancellationTokenSource();
710
store.add(toDisposable(() => cts.dispose(true)));
711
712
// todo: add more than just tools here
713
714
const updateTools = (tx: ITransaction | undefined) => {
715
const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]);
716
const toolPromiseSafe = toolPromise.then(async tools => {
717
handler.logger.info(`Discovered ${tools.length} tools`);
718
return { data: await this._getValidatedTools(handler, tools), nonce: cacheNonce };
719
});
720
this._tools.fromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx);
721
return toolPromiseSafe;
722
};
723
724
const updatePrompts = (tx: ITransaction | undefined) => {
725
const promptsPromise = handler.capabilities.prompts ? handler.listPrompts({}, cts.token) : Promise.resolve([]);
726
const promptsPromiseSafe = promptsPromise.then(data => ({ data, nonce: cacheNonce }));
727
this._prompts.fromServerPromise.set(new ObservablePromise(promptsPromiseSafe), tx);
728
return promptsPromiseSafe;
729
};
730
731
store.add(handler.onDidChangeToolList(() => {
732
handler.logger.info('Tool list changed, refreshing tools...');
733
updateTools(undefined);
734
}));
735
736
store.add(handler.onDidChangePromptList(() => {
737
handler.logger.info('Prompts list changed, refreshing prompts...');
738
updatePrompts(undefined);
739
}));
740
741
const metadataPromise = new ObservablePromise(Promise.resolve({
742
nonce: cacheNonce,
743
data: {
744
serverName: handler.serverInfo.title || handler.serverInfo.name,
745
serverInstructions: handler.serverInstructions,
746
},
747
}));
748
749
transaction(tx => {
750
// note: all update* methods must use tx synchronously
751
const capabilities = encodeCapabilities(handler.capabilities);
752
this._primitiveCache.store(this.definition.id, {
753
serverName: handler.serverInfo.title || handler.serverInfo.name,
754
serverInstructions: handler.serverInstructions,
755
capabilities,
756
});
757
758
this._capabilities.set(capabilities, tx);
759
760
this._serverMetadata.fromServerPromise.set(metadataPromise, tx);
761
762
Promise.all([updateTools(tx), updatePrompts(tx)]).then(([{ data: tools }, { data: prompts }]) => {
763
this._primitiveCache.store(this.definition.id, {
764
nonce: cacheNonce,
765
tools,
766
prompts,
767
capabilities,
768
});
769
770
this._telemetryService.publicLog2<ServerBootData, ServerBootClassification>('mcp/serverBoot', {
771
supportsLogging: !!handler.capabilities.logging,
772
supportsPrompts: !!handler.capabilities.prompts,
773
supportsResources: !!handler.capabilities.resources,
774
toolCount: tools.length,
775
serverName: handler.serverInfo.name,
776
serverVersion: handler.serverInfo.version,
777
});
778
});
779
});
780
}
781
}
782
783
class McpPrompt implements IMcpPrompt {
784
readonly id: string;
785
readonly name: string;
786
readonly description?: string;
787
readonly title?: string;
788
readonly arguments: readonly MCP.PromptArgument[];
789
790
constructor(
791
private readonly _server: McpServer,
792
private readonly _definition: MCP.Prompt,
793
) {
794
this.id = mcpPromptReplaceSpecialChars(this._server.definition.label + '.' + _definition.name);
795
this.name = _definition.name;
796
this.title = _definition.title;
797
this.description = _definition.description;
798
this.arguments = _definition.arguments || [];
799
}
800
801
async resolve(args: Record<string, string>, token?: CancellationToken): Promise<IMcpPromptMessage[]> {
802
const result = await McpServer.callOn(this._server, h => h.getPrompt({ name: this._definition.name, arguments: args }, token), token);
803
return result.messages;
804
}
805
806
async complete(argument: string, prefix: string, alreadyResolved: Record<string, string>, token?: CancellationToken): Promise<string[]> {
807
const result = await McpServer.callOn(this._server, h => h.complete({
808
ref: { type: 'ref/prompt', name: this._definition.name },
809
argument: { name: argument, value: prefix },
810
context: { arguments: alreadyResolved },
811
}, token), token);
812
return result.completion.values;
813
}
814
}
815
816
function encodeCapabilities(cap: MCP.ServerCapabilities): McpCapability {
817
let out = 0;
818
if (cap.logging) { out |= McpCapability.Logging; }
819
if (cap.completions) { out |= McpCapability.Completions; }
820
if (cap.prompts) {
821
out |= McpCapability.Prompts;
822
if (cap.prompts.listChanged) {
823
out |= McpCapability.PromptsListChanged;
824
}
825
}
826
if (cap.resources) {
827
out |= McpCapability.Resources;
828
if (cap.resources.subscribe) {
829
out |= McpCapability.ResourcesSubscribe;
830
}
831
if (cap.resources.listChanged) {
832
out |= McpCapability.ResourcesListChanged;
833
}
834
}
835
if (cap.tools) {
836
out |= McpCapability.Tools;
837
if (cap.tools.listChanged) {
838
out |= McpCapability.ToolsListChanged;
839
}
840
}
841
return out;
842
}
843
844
export class McpTool implements IMcpTool {
845
846
readonly id: string;
847
readonly referenceName: string;
848
849
public get definition(): MCP.Tool { return this._definition; }
850
851
constructor(
852
private readonly _server: McpServer,
853
idPrefix: string,
854
private readonly _definition: IValidatedMcpTool,
855
) {
856
this.referenceName = _definition.name.replaceAll('.', '_');
857
this.id = (idPrefix + _definition.name).replaceAll('.', '_').slice(0, McpToolName.MaxLength);
858
}
859
860
async call(params: Record<string, unknown>, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {
861
// serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it.
862
const name = this._definition.serverToolName ?? this._definition.name;
863
if (context) { this._server.runningToolCalls.add(context); }
864
try {
865
return await McpServer.callOn(this._server, h => h.callTool({ name, arguments: params }, token), token);
866
} finally {
867
if (context) { this._server.runningToolCalls.delete(context); }
868
}
869
}
870
871
async callWithProgress(params: Record<string, unknown>, progress: ToolProgress, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {
872
if (context) { this._server.runningToolCalls.add(context); }
873
try {
874
return await this._callWithProgress(params, progress, token);
875
} finally {
876
if (context) { this._server.runningToolCalls.delete(context); }
877
}
878
}
879
880
_callWithProgress(params: Record<string, unknown>, progress: ToolProgress, token?: CancellationToken, allowRetry = true): Promise<MCP.CallToolResult> {
881
// serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it.
882
const name = this._definition.serverToolName ?? this._definition.name;
883
const progressToken = generateUuid();
884
885
return McpServer.callOn(this._server, h => {
886
let lastProgressN = 0;
887
const listener = h.onDidReceiveProgressNotification((e) => {
888
if (e.params.progressToken === progressToken) {
889
progress.report({
890
message: e.params.message,
891
increment: e.params.progress - lastProgressN,
892
total: e.params.total,
893
});
894
lastProgressN = e.params.progress;
895
}
896
});
897
898
return h.callTool({ name, arguments: params, _meta: { progressToken } }, token)
899
.finally(() => listener.dispose())
900
.catch(err => {
901
const state = this._server.connectionState.get();
902
if (allowRetry && state.state === McpConnectionState.Kind.Error && state.shouldRetry) {
903
return this._callWithProgress(params, progress, token, false);
904
} else {
905
throw err;
906
}
907
});
908
}, token);
909
}
910
911
compare(other: IMcpTool): number {
912
return this._definition.name.localeCompare(other.definition.name);
913
}
914
}
915
916
function warnInvalidTools(instaService: IInstantiationService, serverName: string, errorText: string) {
917
instaService.invokeFunction((accessor) => {
918
const notificationService = accessor.get(INotificationService);
919
const editorService = accessor.get(IEditorService);
920
notificationService.notify({
921
severity: Severity.Warning,
922
message: localize('mcpBadSchema', 'MCP server `{0}` has tools with invalid parameters which will be omitted.', serverName),
923
actions: {
924
primary: [{
925
class: undefined,
926
enabled: true,
927
id: 'mcpBadSchema.show',
928
tooltip: '',
929
label: localize('mcpBadSchema.show', 'Show'),
930
run: () => {
931
editorService.openEditor({
932
resource: undefined,
933
contents: errorText,
934
});
935
}
936
}]
937
}
938
});
939
});
940
}
941
942
class McpResource implements IMcpResource {
943
readonly uri: URI;
944
readonly mcpUri: string;
945
readonly name: string;
946
readonly description: string | undefined;
947
readonly mimeType: string | undefined;
948
readonly sizeInBytes: number | undefined;
949
readonly title: string | undefined;
950
951
constructor(
952
server: McpServer,
953
original: MCP.Resource,
954
) {
955
this.mcpUri = original.uri;
956
this.title = original.title;
957
this.uri = McpResourceURI.fromServer(server.definition, original.uri);
958
this.name = original.name;
959
this.description = original.description;
960
this.mimeType = original.mimeType;
961
this.sizeInBytes = original.size;
962
}
963
}
964
965
class McpResourceTemplate implements IMcpResourceTemplate {
966
readonly name: string;
967
readonly title?: string | undefined;
968
readonly description?: string;
969
readonly mimeType?: string;
970
readonly template: UriTemplate;
971
972
constructor(
973
private readonly _server: McpServer,
974
private readonly _definition: MCP.ResourceTemplate,
975
) {
976
this.name = _definition.name;
977
this.description = _definition.description;
978
this.mimeType = _definition.mimeType;
979
this.title = _definition.title;
980
this.template = UriTemplate.parse(_definition.uriTemplate);
981
}
982
983
public resolveURI(vars: Record<string, unknown>): URI {
984
const serverUri = this.template.resolve(vars);
985
return McpResourceURI.fromServer(this._server.definition, serverUri);
986
}
987
988
async complete(templatePart: string, prefix: string, alreadyResolved: Record<string, string | string[]>, token?: CancellationToken): Promise<string[]> {
989
const result = await McpServer.callOn(this._server, h => h.complete({
990
ref: { type: 'ref/resource', uri: this._definition.uriTemplate },
991
argument: { name: templatePart, value: prefix },
992
context: {
993
arguments: mapValues(alreadyResolved, v => Array.isArray(v) ? v.join('/') : v),
994
},
995
}, token), token);
996
return result.completion.values;
997
}
998
}
999
1000