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
5282 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 { normalizeDriveLetter } from '../../../../base/common/labels.js';
11
import { Disposable, DisposableStore, IDisposable, 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, 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 { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
23
import { ILogger, ILoggerService } from '../../../../platform/log/common/log.js';
24
import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js';
25
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
26
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
27
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
28
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
29
import { IEditorService } from '../../../services/editor/common/editorService.js';
30
import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js';
31
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
32
import { IOutputService } from '../../../services/output/common/output.js';
33
import { ToolProgress } from '../../chat/common/tools/languageModelToolsService.js';
34
import { mcpActivationEvent } from './mcpConfiguration.js';
35
import { McpDevModeServerAttache } from './mcpDevMode.js';
36
import { McpIcons, parseAndValidateMcpIcon, StoredMcpIcons } from './mcpIcons.js';
37
import { IMcpRegistry } from './mcpRegistryTypes.js';
38
import { McpServerRequestHandler } from './mcpServerRequestHandler.js';
39
import { McpTaskManager } from './mcpTaskManager.js';
40
import { ElicitationKind, extensionMcpCollectionPrefix, IMcpElicitationService, IMcpIcons, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerStaticToolAvailability, McpServerTransportType, McpToolName, McpToolVisibility, MpcResponseError, UserInteractionRequiredError } from './mcpTypes.js';
41
import { MCP } from './modelContextProtocol.js';
42
import { McpApps } from './modelContextProtocolApps.js';
43
import { UriTemplate } from './uriTemplate.js';
44
45
type ServerBootData = {
46
supportsLogging: boolean;
47
supportsPrompts: boolean;
48
supportsResources: boolean;
49
toolCount: number;
50
serverName: string;
51
serverVersion: string;
52
};
53
type ServerBootClassification = {
54
owner: 'connor4312';
55
comment: 'Details the capabilities of the MCP server';
56
supportsLogging: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports logging' };
57
supportsPrompts: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports prompts' };
58
supportsResources: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server supports resource' };
59
toolCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The number of tools the server advertises' };
60
serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };
61
serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };
62
};
63
64
type ElicitationTelemetryData = {
65
serverName: string;
66
serverVersion: string;
67
};
68
69
type ElicitationTelemetryClassification = {
70
owner: 'connor4312';
71
comment: 'Triggered when elictation is requested';
72
serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server' };
73
serverVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the MCP server' };
74
};
75
76
export type McpServerInstallData = {
77
serverName: string;
78
source: 'gallery' | 'local';
79
scope: string;
80
success: boolean;
81
error?: string;
82
hasInputs: boolean;
83
};
84
85
export type McpServerInstallClassification = {
86
owner: 'connor4312';
87
comment: 'MCP server installation event tracking';
88
serverName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The name of the MCP server being installed' };
89
source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation source (gallery or local)' };
90
scope: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Installation scope (user, workspace, etc.)' };
91
success: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether installation succeeded' };
92
error?: { classification: 'CallstackOrException'; purpose: 'FeatureInsight'; comment: 'Error message if installation failed' };
93
hasInputs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the server requires input configuration' };
94
};
95
96
type ServerBootState = {
97
state: string;
98
time: number;
99
};
100
type ServerBootStateClassification = {
101
owner: 'connor4312';
102
comment: 'Details the capabilities of the MCP server';
103
state: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The server outcome' };
104
time: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Duration in milliseconds to reach that state' };
105
};
106
107
type StoredMcpPrompt = MCP.Prompt & { _icons: StoredMcpIcons };
108
109
interface IToolCacheEntry {
110
readonly serverName: string | undefined;
111
readonly serverInstructions: string | undefined;
112
readonly serverIcons: StoredMcpIcons;
113
114
readonly trustedAtNonce: string | undefined;
115
116
readonly nonce: string | undefined;
117
/** Cached tools so we can show what's available before it's started */
118
readonly tools: readonly ValidatedMcpTool[];
119
/** Cached prompts */
120
readonly prompts: readonly StoredMcpPrompt[] | undefined;
121
/** Cached capabilities */
122
readonly capabilities: McpCapability | undefined;
123
}
124
125
const emptyToolEntry: IToolCacheEntry = {
126
serverName: undefined,
127
serverIcons: [],
128
serverInstructions: undefined,
129
trustedAtNonce: undefined,
130
nonce: undefined,
131
tools: [],
132
prompts: undefined,
133
capabilities: undefined,
134
};
135
136
interface IServerCacheEntry {
137
readonly servers: readonly McpServerDefinition.Serialized[];
138
}
139
140
const toolInvalidCharRe = /[^a-z0-9_-]/gi;
141
142
export class McpServerMetadataCache extends Disposable {
143
private didChange = false;
144
private readonly cache = new LRUCache<string, IToolCacheEntry>(128);
145
private readonly extensionServers = new Map</* collection ID */string, IServerCacheEntry>();
146
147
constructor(
148
scope: StorageScope,
149
@IStorageService storageService: IStorageService,
150
) {
151
super();
152
153
type StoredType = {
154
extensionServers: [string, IServerCacheEntry][];
155
serverTools: [string, IToolCacheEntry][];
156
};
157
158
const storageKey = 'mcpToolCache';
159
this._register(storageService.onWillSaveState(() => {
160
if (this.didChange) {
161
storageService.store(storageKey, {
162
extensionServers: [...this.extensionServers],
163
serverTools: this.cache.toJSON(),
164
} satisfies StoredType, scope, StorageTarget.MACHINE);
165
this.didChange = false;
166
}
167
}));
168
169
try {
170
const cached: StoredType | undefined = storageService.getObject(storageKey, scope);
171
this.extensionServers = new Map(cached?.extensionServers ?? []);
172
cached?.serverTools?.forEach(([k, v]) => this.cache.set(k, v));
173
} catch {
174
// ignored
175
}
176
}
177
178
/** Resets the cache for primitives and extension servers */
179
reset() {
180
this.cache.clear();
181
this.extensionServers.clear();
182
this.didChange = true;
183
}
184
185
/** Gets cached primitives for a server (used before a server is running) */
186
get(definitionId: string) {
187
return this.cache.get(definitionId);
188
}
189
190
/** Sets cached primitives for a server */
191
store(definitionId: string, entry: Partial<IToolCacheEntry>): void {
192
const prev = this.get(definitionId) || emptyToolEntry;
193
this.cache.set(definitionId, { ...prev, ...entry });
194
this.didChange = true;
195
}
196
197
/** Gets cached servers for a collection (used for extensions, before the extension activates) */
198
getServers(collectionId: string) {
199
return this.extensionServers.get(collectionId);
200
}
201
202
/** Sets cached servers for a collection */
203
storeServers(collectionId: string, entry: IServerCacheEntry | undefined): void {
204
if (entry) {
205
this.extensionServers.set(collectionId, entry);
206
} else {
207
this.extensionServers.delete(collectionId);
208
}
209
this.didChange = true;
210
}
211
}
212
213
type ValidatedMcpTool = MCP.Tool & {
214
_icons: StoredMcpIcons;
215
216
/**
217
* Tool name as published by the MCP server. This may
218
* be different than the one in {@link definition} due to name normalization
219
* in {@link McpServer._getValidatedTools}.
220
*/
221
serverToolName: string;
222
223
/**
224
* Visibility of the tool, parsed from `_meta.ui.visibility`.
225
* Defaults to Model | App if not specified.
226
*/
227
visibility: McpToolVisibility;
228
229
/**
230
* UI resource URI if this tool has an associated MCP App UI.
231
* Parsed from `_meta.ui.resourceUri`.
232
*/
233
uiResourceUri?: string;
234
};
235
236
interface StoredServerMetadata {
237
readonly serverName: string | undefined;
238
readonly serverInstructions: string | undefined;
239
readonly serverIcons: StoredMcpIcons | undefined;
240
}
241
242
interface ServerMetadata {
243
readonly serverName: string | undefined;
244
readonly serverInstructions: string | undefined;
245
readonly icons: IMcpIcons;
246
}
247
248
class CachedPrimitive<T, C> {
249
/**
250
* @param _definitionId Server definition ID
251
* @param _cache Metadata cache instance
252
* @param _fromStaticDefinition Static definition that came with the server.
253
* This should ONLY have a value if it should be used instead of whatever
254
* is currently in the cache.
255
* @param _fromCache Pull the value from the cache entry.
256
* @param _toT Transform the value to the observable type.
257
* @param defaultValue Default value if no cache entry.
258
*/
259
constructor(
260
private readonly _definitionId: string,
261
private readonly _cache: McpServerMetadataCache,
262
private readonly _fromStaticDefinition: IObservable<C | undefined> | undefined,
263
private readonly _fromCache: (entry: IToolCacheEntry) => C,
264
private readonly _toT: (values: C, reader: IDerivedReader<void>) => T,
265
private readonly defaultValue: C,
266
) { }
267
268
public get fromCache(): { nonce: string | undefined; data: C } | undefined {
269
const c = this._cache.get(this._definitionId);
270
return c ? { data: this._fromCache(c), nonce: c.nonce } : undefined;
271
}
272
273
public hasStaticDefinition(reader: IReader | undefined) {
274
return !!this._fromStaticDefinition?.read(reader);
275
}
276
277
public readonly fromServerPromise = observableValue<ObservablePromise<{
278
readonly data: C;
279
readonly nonce: string | undefined;
280
}> | undefined>(this, undefined);
281
282
private readonly fromServer = derived(reader => this.fromServerPromise.read(reader)?.promiseResult.read(reader)?.data);
283
284
public readonly value: IObservable<T> = derived(reader => {
285
const serverTools = this.fromServer.read(reader);
286
const definitions = serverTools?.data ?? this._fromStaticDefinition?.read(reader) ?? this.fromCache?.data ?? this.defaultValue;
287
return this._toT(definitions, reader);
288
});
289
}
290
291
export class McpServer extends Disposable implements IMcpServer {
292
/** Shared task manager that survives reconnections */
293
private readonly _taskManager = this._register(new McpTaskManager());
294
295
/**
296
* Helper function to call the function on the handler once it's online. The
297
* connection started if it is not already.
298
*/
299
public static async callOn<R>(server: IMcpServer, fn: (handler: McpServerRequestHandler) => Promise<R>, token: CancellationToken = CancellationToken.None): Promise<R> {
300
await server.start({ promptType: 'all-untrusted' }); // idempotent
301
302
let ranOnce = false;
303
let d: IDisposable;
304
305
const callPromise = new Promise<R>((resolve, reject) => {
306
307
d = autorun(reader => {
308
const connection = server.connection.read(reader);
309
if (!connection || ranOnce) {
310
return;
311
}
312
313
const handler = connection.handler.read(reader);
314
if (!handler) {
315
const state = connection.state.read(reader);
316
if (state.state === McpConnectionState.Kind.Error) {
317
reject(new McpConnectionFailedError(`MCP server could not be started: ${state.message}`));
318
return;
319
} else if (state.state === McpConnectionState.Kind.Stopped) {
320
reject(new McpConnectionFailedError('MCP server has stopped'));
321
return;
322
} else {
323
// keep waiting for handler
324
return;
325
}
326
}
327
328
resolve(fn(handler));
329
ranOnce = true; // aggressive prevent multiple racey calls, don't dispose because autorun is sync
330
});
331
});
332
333
return raceCancellationError(callPromise, token).finally(() => d.dispose());
334
}
335
336
public readonly collection: McpCollectionReference;
337
private readonly _connectionSequencer = new Sequencer();
338
private readonly _connection = this._register(disposableObservableValue<IMcpServerConnection | undefined>(this, undefined));
339
340
public readonly connection = this._connection;
341
public readonly connectionState: IObservable<McpConnectionState> = derived(reader => this._connection.read(reader)?.state.read(reader) ?? { state: McpConnectionState.Kind.Stopped });
342
343
344
private readonly _capabilities: CachedPrimitive<number | undefined, number | undefined>;
345
public get capabilities() {
346
return this._capabilities.value;
347
}
348
349
private readonly _tools: CachedPrimitive<readonly IMcpTool[], readonly ValidatedMcpTool[]>;
350
public get tools() {
351
return this._tools.value;
352
}
353
354
private readonly _prompts: CachedPrimitive<readonly IMcpPrompt[], readonly StoredMcpPrompt[]>;
355
public get prompts() {
356
return this._prompts.value;
357
}
358
359
private readonly _serverMetadata: CachedPrimitive<ServerMetadata, StoredServerMetadata | undefined>;
360
public get serverMetadata() {
361
return this._serverMetadata.value;
362
}
363
364
public get trustedAtNonce() {
365
return this._primitiveCache.get(this.definition.id)?.trustedAtNonce;
366
}
367
368
public set trustedAtNonce(nonce: string | undefined) {
369
this._primitiveCache.store(this.definition.id, { trustedAtNonce: nonce });
370
}
371
372
private readonly _fullDefinitions: IObservable<{
373
server: McpServerDefinition | undefined;
374
collection: McpCollectionDefinition | undefined;
375
}>;
376
377
public readonly cacheState = derived(reader => {
378
const currentNonce = () => this._fullDefinitions.read(reader)?.server?.cacheNonce;
379
const stateWhenServingFromCache = () => {
380
if (this._tools.hasStaticDefinition(reader)) {
381
return McpServerCacheState.Cached;
382
}
383
384
if (!this._tools.fromCache) {
385
return McpServerCacheState.Unknown;
386
}
387
388
return currentNonce() === this._tools.fromCache.nonce ? McpServerCacheState.Cached : McpServerCacheState.Outdated;
389
};
390
391
const fromServer = this._tools.fromServerPromise.read(reader);
392
const connectionState = this.connectionState.read(reader);
393
const isIdle = McpConnectionState.canBeStarted(connectionState.state) || !fromServer;
394
if (isIdle) {
395
return stateWhenServingFromCache();
396
}
397
398
const fromServerResult = fromServer?.promiseResult.read(reader);
399
if (!fromServerResult) {
400
return this._tools.fromCache ? McpServerCacheState.RefreshingFromCached : McpServerCacheState.RefreshingFromUnknown;
401
}
402
403
if (fromServerResult.error) {
404
return stateWhenServingFromCache();
405
}
406
407
return fromServerResult.data?.nonce === currentNonce() ? McpServerCacheState.Live : McpServerCacheState.Outdated;
408
});
409
410
public get logger(): ILogger {
411
return this._logger;
412
}
413
414
private readonly _loggerId: string;
415
private readonly _logger: ILogger;
416
private _lastModeDebugged = false;
417
/** Count of running tool calls, used to detect if sampling is during an LM call */
418
public runningToolCalls = new Set<IMcpToolCallContext>();
419
420
constructor(
421
initialCollection: McpCollectionDefinition,
422
public readonly definition: McpDefinitionReference,
423
explicitRoots: URI[] | undefined,
424
private readonly _requiresExtensionActivation: boolean | undefined,
425
private readonly _primitiveCache: McpServerMetadataCache,
426
toolPrefix: string,
427
@IMcpRegistry private readonly _mcpRegistry: IMcpRegistry,
428
@IWorkspaceContextService workspacesService: IWorkspaceContextService,
429
@IExtensionService private readonly _extensionService: IExtensionService,
430
@ILoggerService private readonly _loggerService: ILoggerService,
431
@IOutputService private readonly _outputService: IOutputService,
432
@ITelemetryService private readonly _telemetryService: ITelemetryService,
433
@ICommandService private readonly _commandService: ICommandService,
434
@IInstantiationService private readonly _instantiationService: IInstantiationService,
435
@INotificationService private readonly _notificationService: INotificationService,
436
@IOpenerService private readonly _openerService: IOpenerService,
437
@IMcpSamplingService private readonly _samplingService: IMcpSamplingService,
438
@IMcpElicitationService private readonly _elicitationService: IMcpElicitationService,
439
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
440
) {
441
super();
442
443
this.collection = initialCollection;
444
this._fullDefinitions = this._mcpRegistry.getServerDefinition(this.collection, this.definition);
445
this._loggerId = `mcpServer.${definition.id}`;
446
this._logger = this._register(_loggerService.createLogger(this._loggerId, { hidden: true, name: `MCP: ${definition.label}` }));
447
448
const that = this;
449
this._register(this._instantiationService.createInstance(McpDevModeServerAttache, this, { get lastModeDebugged() { return that._lastModeDebugged; } }));
450
451
// If the logger is disposed but not deregistered, then the disposed instance
452
// is reused and no-ops. todo@sandy081 this seems like a bug.
453
this._register(toDisposable(() => _loggerService.deregisterLogger(this._loggerId)));
454
455
// 1. Reflect workspaces into the MCP roots
456
const workspaces = explicitRoots
457
? observableValue(this, explicitRoots.map(uri => ({ uri, name: basename(uri) })))
458
: observableFromEvent(
459
this,
460
workspacesService.onDidChangeWorkspaceFolders,
461
() => workspacesService.getWorkspace().folders,
462
);
463
464
const uriTransformer = environmentService.remoteAuthority ? createURITransformer(environmentService.remoteAuthority) : undefined;
465
466
this._register(autorun(reader => {
467
const cnx = this._connection.read(reader)?.handler.read(reader);
468
if (!cnx) {
469
return;
470
}
471
472
cnx.roots = workspaces.read(reader)
473
.filter(w => w.uri.authority === (initialCollection.remoteAuthority || ''))
474
.map(w => {
475
let uri = URI.from(uriTransformer?.transformIncoming(w.uri) ?? w.uri);
476
if (uri.scheme === Schemas.file) { // #271812
477
uri = URI.file(normalizeDriveLetter(uri.fsPath, true));
478
}
479
480
return { name: w.name, uri: uri.toString() };
481
});
482
}));
483
484
// 2. Populate this.tools when we connect to a server.
485
this._register(autorun(reader => {
486
const cnx = this._connection.read(reader);
487
const handler = cnx?.handler.read(reader);
488
if (handler) {
489
this._populateLiveData(handler, cnx?.definition.cacheNonce, reader.store);
490
} else if (this._tools) {
491
this.resetLiveData();
492
}
493
}));
494
495
const staticMetadata = derived(reader => {
496
const def = this._fullDefinitions.read(reader).server;
497
return def && def.cacheNonce !== this._tools.fromCache?.nonce ? def.staticMetadata : undefined;
498
});
499
500
// 3. Publish tools
501
this._tools = new CachedPrimitive<readonly IMcpTool[], readonly ValidatedMcpTool[]>(
502
this.definition.id,
503
this._primitiveCache,
504
staticMetadata
505
.map(m => {
506
const tools = m?.tools?.filter(t => t.availability === McpServerStaticToolAvailability.Initial).map(t => t.definition);
507
return tools?.length ? new ObservablePromise(this._getValidatedTools(tools)) : undefined;
508
})
509
.map((o, reader) => o?.promiseResult.read(reader)?.data),
510
(entry) => entry.tools,
511
(entry) => entry.map(def => this._instantiationService.createInstance(McpTool, this, toolPrefix, def)).sort((a, b) => a.compare(b)),
512
[],
513
);
514
515
// 4. Publish prompts
516
this._prompts = new CachedPrimitive<readonly IMcpPrompt[], readonly StoredMcpPrompt[]>(
517
this.definition.id,
518
this._primitiveCache,
519
undefined,
520
(entry) => entry.prompts || [],
521
(entry) => entry.map(e => new McpPrompt(this, e)),
522
[],
523
);
524
525
this._serverMetadata = new CachedPrimitive<ServerMetadata, StoredServerMetadata | undefined>(
526
this.definition.id,
527
this._primitiveCache,
528
staticMetadata.map(m => m ? this._toStoredMetadata(m?.serverInfo, m?.instructions) : undefined),
529
(entry) => ({ serverName: entry.serverName, serverInstructions: entry.serverInstructions, serverIcons: entry.serverIcons }),
530
(entry) => ({ serverName: entry?.serverName, serverInstructions: entry?.serverInstructions, icons: McpIcons.fromStored(entry?.serverIcons) }),
531
undefined,
532
);
533
534
this._capabilities = new CachedPrimitive<number | undefined, number | undefined>(
535
this.definition.id,
536
this._primitiveCache,
537
staticMetadata.map(m => m?.capabilities !== undefined ? encodeCapabilities(m.capabilities) : undefined),
538
(entry) => entry.capabilities,
539
(entry) => entry,
540
undefined,
541
);
542
}
543
544
public readDefinitions(): IObservable<{ server: McpServerDefinition | undefined; collection: McpCollectionDefinition | undefined }> {
545
return this._fullDefinitions;
546
}
547
548
public showOutput(preserveFocus?: boolean) {
549
this._loggerService.setVisibility(this._loggerId, true);
550
return this._outputService.showChannel(this._loggerId, preserveFocus);
551
}
552
553
public resources(token?: CancellationToken): AsyncIterable<IMcpResource[]> {
554
const cts = new CancellationTokenSource(token);
555
return new AsyncIterableProducer<IMcpResource[]>(async emitter => {
556
await McpServer.callOn(this, async (handler) => {
557
for await (const resource of handler.listResourcesIterable({}, cts.token)) {
558
emitter.emitOne(resource.map(r => new McpResource(this, r, McpIcons.fromParsed(this._parseIcons(r)))));
559
if (cts.token.isCancellationRequested) {
560
return;
561
}
562
}
563
});
564
}, () => cts.dispose(true));
565
}
566
567
public resourceTemplates(token?: CancellationToken): Promise<IMcpResourceTemplate[]> {
568
return McpServer.callOn(this, async (handler) => {
569
const templates = await handler.listResourceTemplates({}, token);
570
return templates.map(t => new McpResourceTemplate(this, t, McpIcons.fromParsed(this._parseIcons(t))));
571
}, token);
572
}
573
574
public start({ interaction, autoTrustChanges, promptType, debug, errorOnUserInteraction }: IMcpServerStartOpts = {}): Promise<McpConnectionState> {
575
interaction?.participants.set(this.definition.id, { s: 'unknown' });
576
577
return this._connectionSequencer.queue<McpConnectionState>(async () => {
578
const activationEvent = mcpActivationEvent(this.collection.id.slice(extensionMcpCollectionPrefix.length));
579
if (this._requiresExtensionActivation && !this._extensionService.activationEventIsDone(activationEvent)) {
580
await this._extensionService.activateByEvent(activationEvent);
581
await Promise.all(this._mcpRegistry.delegates.get()
582
.map(r => r.waitForInitialProviderPromises()));
583
// This can happen if the server was created from a cached MCP server seen
584
// from an extension, but then it wasn't registered when the extension activated.
585
if (this._store.isDisposed) {
586
return { state: McpConnectionState.Kind.Stopped };
587
}
588
}
589
590
let connection = this._connection.get();
591
if (connection && McpConnectionState.canBeStarted(connection.state.get().state)) {
592
connection.dispose();
593
connection = undefined;
594
this._connection.set(connection, undefined);
595
}
596
597
if (!connection) {
598
this._lastModeDebugged = !!debug;
599
const that = this;
600
connection = await this._mcpRegistry.resolveConnection({
601
interaction,
602
autoTrustChanges,
603
promptType,
604
trustNonceBearer: {
605
get trustedAtNonce() { return that.trustedAtNonce; },
606
set trustedAtNonce(nonce: string | undefined) { that.trustedAtNonce = nonce; }
607
},
608
logger: this._logger,
609
collectionRef: this.collection,
610
definitionRef: this.definition,
611
debug,
612
errorOnUserInteraction,
613
taskManager: this._taskManager,
614
});
615
if (!connection) {
616
return { state: McpConnectionState.Kind.Stopped };
617
}
618
619
if (this._store.isDisposed) {
620
connection.dispose();
621
return { state: McpConnectionState.Kind.Stopped };
622
}
623
624
this._connection.set(connection, undefined);
625
626
if (connection.definition.devMode) {
627
this.showOutput();
628
}
629
}
630
631
const start = Date.now();
632
let state = await connection.start({
633
createMessageRequestHandler: (params, token) => this._samplingService.sample({
634
isDuringToolCall: this.runningToolCalls.size > 0,
635
server: this,
636
params,
637
}, token).then(r => r.sample),
638
elicitationRequestHandler: async (req, token) => {
639
const serverInfo = connection.handler.get()?.serverInfo;
640
if (serverInfo) {
641
this._telemetryService.publicLog2<ElicitationTelemetryData, ElicitationTelemetryClassification>('mcp.elicitationRequested', {
642
serverName: serverInfo.name,
643
serverVersion: serverInfo.version,
644
});
645
}
646
647
const r = await this._elicitationService.elicit(this, Iterable.first(this.runningToolCalls), req, token || CancellationToken.None);
648
r.dispose();
649
return r.value;
650
}
651
});
652
653
this._telemetryService.publicLog2<ServerBootState, ServerBootStateClassification>('mcp/serverBootState', {
654
state: McpConnectionState.toKindString(state.state),
655
time: Date.now() - start,
656
});
657
658
if (state.state === McpConnectionState.Kind.Error) {
659
this.showInteractiveError(connection, state, debug);
660
}
661
662
// MCP servers that need auth can 'start' but will stop with an interaction-needed
663
// error they first make a request. In this case, wait until the handler fully
664
// initializes before resolving (throwing if it ends up needing auth)
665
if (errorOnUserInteraction && state.state === McpConnectionState.Kind.Running) {
666
let disposable: IDisposable;
667
state = await new Promise<McpConnectionState>((resolve, reject) => {
668
disposable = autorun(reader => {
669
const handler = connection.handler.read(reader);
670
if (handler) {
671
resolve(state);
672
}
673
674
const s = connection.state.read(reader);
675
if (s.state === McpConnectionState.Kind.Stopped && s.reason === 'needs-user-interaction') {
676
reject(new UserInteractionRequiredError('auth'));
677
}
678
679
if (!McpConnectionState.isRunning(s)) {
680
resolve(s);
681
}
682
});
683
}).finally(() => disposable.dispose());
684
}
685
686
return state;
687
}).finally(() => {
688
interaction?.participants.set(this.definition.id, { s: 'resolved' });
689
});
690
}
691
692
private showInteractiveError(cnx: IMcpServerConnection, error: McpConnectionState.Error, debug?: boolean) {
693
if (error.code === 'ENOENT' && cnx.launchDefinition.type === McpServerTransportType.Stdio) {
694
let docsLink: string | undefined;
695
switch (cnx.launchDefinition.command) {
696
case 'uvx':
697
docsLink = `https://aka.ms/vscode-mcp-install/uvx`;
698
break;
699
case 'npx':
700
docsLink = `https://aka.ms/vscode-mcp-install/npx`;
701
break;
702
case 'dnx':
703
docsLink = `https://aka.ms/vscode-mcp-install/dnx`;
704
break;
705
case 'dotnet':
706
docsLink = `https://aka.ms/vscode-mcp-install/dotnet`;
707
break;
708
}
709
710
const options: IPromptChoice[] = [{
711
label: localize('mcp.command.showOutput', "Show Output"),
712
run: () => this.showOutput(),
713
}];
714
715
if (cnx.definition.devMode?.debug?.type === 'debugpy' && debug) {
716
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, {
717
label: localize('mcpViewDocs', 'View Docs'),
718
run: () => this._openerService.open(URI.parse('https://aka.ms/vscode-mcp-install/debugpy')),
719
}]);
720
return;
721
}
722
723
if (docsLink) {
724
options.push({
725
label: localize('mcpServerInstall', 'Install {0}', cnx.launchDefinition.command),
726
run: () => this._openerService.open(URI.parse(docsLink)),
727
});
728
}
729
730
this._notificationService.prompt(Severity.Error, localize('mcpServerNotFound', 'The command "{0}" needed to run {1} was not found.', cnx.launchDefinition.command, cnx.definition.label), options);
731
} else {
732
this._notificationService.warn(localize('mcpServerError', 'The MCP server {0} could not be started: {1}', cnx.definition.label, error.message));
733
}
734
}
735
736
public stop(): Promise<void> {
737
return this._connection.get()?.stop() || Promise.resolve();
738
}
739
740
/** Waits for any ongoing tools to be refreshed before resolving. */
741
public awaitToolRefresh() {
742
return new Promise<void>(resolve => {
743
autorunSelfDisposable(reader => {
744
const promise = this._tools.fromServerPromise.read(reader);
745
const result = promise?.promiseResult.read(reader);
746
if (result) {
747
resolve();
748
}
749
});
750
});
751
}
752
753
private resetLiveData() {
754
transaction(tx => {
755
this._tools.fromServerPromise.set(undefined, tx);
756
this._prompts.fromServerPromise.set(undefined, tx);
757
});
758
}
759
760
private async _normalizeTool(originalTool: MCP.Tool): Promise<ValidatedMcpTool | { error: string[] }> {
761
// Parse MCP Apps UI metadata from _meta.ui
762
const uiMeta = originalTool._meta?.ui as McpApps.McpUiToolMeta | undefined;
763
764
// Compute visibility from _meta.ui.visibility, defaulting to Model | App
765
let visibility: McpToolVisibility = McpToolVisibility.Model | McpToolVisibility.App;
766
if (uiMeta?.visibility && Array.isArray(uiMeta.visibility)) {
767
visibility &= 0;
768
769
if (uiMeta.visibility.includes('model')) {
770
visibility |= McpToolVisibility.Model;
771
}
772
if (uiMeta.visibility.includes('app')) {
773
visibility |= McpToolVisibility.App;
774
}
775
}
776
777
const tool: ValidatedMcpTool = {
778
...originalTool,
779
serverToolName: originalTool.name,
780
_icons: this._parseIcons(originalTool),
781
visibility,
782
uiResourceUri: uiMeta?.resourceUri,
783
};
784
if (!tool.description) {
785
// Ensure a description is provided for each tool, #243919
786
this._logger.warn(`Tool ${tool.name} does not have a description. Tools must be accurately described to be called`);
787
tool.description = '<empty>';
788
}
789
790
if (toolInvalidCharRe.test(tool.name)) {
791
this._logger.warn(`Tool ${JSON.stringify(tool.name)} is invalid. Tools names may only contain [a-z0-9_-]`);
792
tool.name = tool.name.replace(toolInvalidCharRe, '_');
793
}
794
795
type JsonDiagnostic = { message: string; range: { line: number; character: number }[] };
796
797
let diagnostics: JsonDiagnostic[] = [];
798
const toolJson = JSON.stringify(tool.inputSchema);
799
try {
800
const schemaUri = URI.parse('https://json-schema.org/draft-07/schema');
801
diagnostics = await this._commandService.executeCommand<JsonDiagnostic[]>('json.validate', schemaUri, toolJson) || [];
802
} catch (e) {
803
// ignored (error in json extension?);
804
}
805
806
if (!diagnostics.length) {
807
return tool;
808
}
809
810
// because it's all one line from JSON.stringify, we can treat characters as offsets.
811
const tree = json.parseTree(toolJson);
812
const messages = diagnostics.map(d => {
813
const node = json.findNodeAtOffset(tree, d.range[0].character);
814
const path = node && `/${json.getNodePath(node).join('/')}`;
815
return d.message + (path ? ` (at ${path})` : '');
816
});
817
818
return { error: messages };
819
}
820
821
private async _getValidatedTools(tools: MCP.Tool[]): Promise<ValidatedMcpTool[]> {
822
let error = '';
823
824
const validations = await Promise.all(tools.map(t => this._normalizeTool(t)));
825
const validated: ValidatedMcpTool[] = [];
826
for (const [i, result] of validations.entries()) {
827
if ('error' in result) {
828
error += localize('mcpBadSchema.tool', 'Tool `{0}` has invalid JSON parameters:', tools[i].name) + '\n';
829
for (const message of result.error) {
830
error += `\t- ${message}\n`;
831
}
832
error += `\t- Schema: ${JSON.stringify(tools[i].inputSchema)}\n\n`;
833
} else {
834
validated.push(result);
835
}
836
}
837
838
if (error) {
839
this._logger.warn(`${tools.length - validated.length} tools have invalid JSON schemas and will be omitted`);
840
warnInvalidTools(this._instantiationService, this.definition.label, error);
841
}
842
843
return validated;
844
}
845
846
/**
847
* Parses incoming MCP icons and returns the resulting 'stored' record. Note
848
* that this requires an active MCP server connection since we validate
849
* against some of that connection's data. The icons may however be stored
850
* and rehydrated later.
851
*/
852
private _parseIcons(icons: MCP.Icons) {
853
const cnx = this._connection.get();
854
if (!cnx) {
855
return [];
856
}
857
858
return parseAndValidateMcpIcon(icons, cnx.launchDefinition, this._logger);
859
}
860
861
private _setServerTools(nonce: string | undefined, toolsPromise: Promise<MCP.Tool[]>, tx: ITransaction | undefined) {
862
const toolPromiseSafe = toolsPromise.then(async tools => {
863
this._logger.info(`Discovered ${tools.length} tools`);
864
const data = await this._getValidatedTools(tools);
865
this._primitiveCache.store(this.definition.id, { tools: data, nonce });
866
return { data, nonce };
867
});
868
this._tools.fromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx);
869
return toolPromiseSafe;
870
}
871
872
private _setServerPrompts(nonce: string | undefined, promptsPromise: Promise<MCP.Prompt[]>, tx: ITransaction | undefined) {
873
const promptsPromiseSafe = promptsPromise.then((result): { data: StoredMcpPrompt[]; nonce: string | undefined } => {
874
const data: StoredMcpPrompt[] = result.map(prompt => ({
875
...prompt,
876
_icons: this._parseIcons(prompt)
877
}));
878
this._primitiveCache.store(this.definition.id, { prompts: data, nonce });
879
return { data, nonce };
880
});
881
882
this._prompts.fromServerPromise.set(new ObservablePromise(promptsPromiseSafe), tx);
883
return promptsPromiseSafe;
884
}
885
886
private _toStoredMetadata(serverInfo?: MCP.Implementation, instructions?: string): StoredServerMetadata {
887
return {
888
serverName: serverInfo ? serverInfo.title || serverInfo.name : undefined,
889
serverInstructions: instructions,
890
serverIcons: serverInfo ? this._parseIcons(serverInfo) : undefined,
891
};
892
}
893
894
private _setServerMetadata(
895
nonce: string | undefined,
896
{ serverInfo, instructions, capabilities }: { serverInfo: MCP.Implementation; instructions: string | undefined; capabilities: MCP.ServerCapabilities },
897
tx: ITransaction | undefined,
898
) {
899
const serverMetadata: StoredServerMetadata = this._toStoredMetadata(serverInfo, instructions);
900
this._serverMetadata.fromServerPromise.set(ObservablePromise.resolved({ nonce, data: serverMetadata }), tx);
901
902
const capabilitiesEncoded = encodeCapabilities(capabilities);
903
this._capabilities.fromServerPromise.set(ObservablePromise.resolved({ data: capabilitiesEncoded, nonce }), tx);
904
this._primitiveCache.store(this.definition.id, { ...serverMetadata, nonce, capabilities: capabilitiesEncoded });
905
}
906
907
private _populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) {
908
const cts = new CancellationTokenSource();
909
store.add(toDisposable(() => cts.dispose(true)));
910
911
const updateTools = (tx: ITransaction | undefined) => {
912
const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]);
913
return this._setServerTools(cacheNonce, toolPromise, tx);
914
};
915
916
const updatePrompts = (tx: ITransaction | undefined) => {
917
const promptsPromise = handler.capabilities.prompts ? handler.listPrompts({}, cts.token) : Promise.resolve([]);
918
return this._setServerPrompts(cacheNonce, promptsPromise, tx);
919
};
920
921
store.add(handler.onDidChangeToolList(() => {
922
this._logger.info('Tool list changed, refreshing tools...');
923
updateTools(undefined);
924
}));
925
926
store.add(handler.onDidChangePromptList(() => {
927
this._logger.info('Prompts list changed, refreshing prompts...');
928
updatePrompts(undefined);
929
}));
930
931
transaction(tx => {
932
this._setServerMetadata(cacheNonce, { serverInfo: handler.serverInfo, instructions: handler.serverInstructions, capabilities: handler.capabilities }, tx);
933
updatePrompts(tx);
934
const toolUpdate = updateTools(tx);
935
936
toolUpdate.then(tools => {
937
this._telemetryService.publicLog2<ServerBootData, ServerBootClassification>('mcp/serverBoot', {
938
supportsLogging: !!handler.capabilities.logging,
939
supportsPrompts: !!handler.capabilities.prompts,
940
supportsResources: !!handler.capabilities.resources,
941
toolCount: tools.data.length,
942
serverName: handler.serverInfo.name,
943
serverVersion: handler.serverInfo.version,
944
});
945
});
946
});
947
}
948
}
949
950
class McpPrompt implements IMcpPrompt {
951
readonly id: string;
952
readonly name: string;
953
readonly description?: string;
954
readonly title?: string;
955
readonly arguments: readonly MCP.PromptArgument[];
956
readonly icons: IMcpIcons;
957
958
constructor(
959
private readonly _server: McpServer,
960
private readonly _definition: StoredMcpPrompt,
961
) {
962
this.id = mcpPromptReplaceSpecialChars(this._server.definition.label + '.' + _definition.name);
963
this.name = _definition.name;
964
this.title = _definition.title;
965
this.description = _definition.description;
966
this.arguments = _definition.arguments || [];
967
this.icons = McpIcons.fromStored(this._definition._icons);
968
}
969
970
async resolve(args: Record<string, string>, token?: CancellationToken): Promise<IMcpPromptMessage[]> {
971
const result = await McpServer.callOn(this._server, h => h.getPrompt({ name: this._definition.name, arguments: args }, token), token);
972
return result.messages;
973
}
974
975
async complete(argument: string, prefix: string, alreadyResolved: Record<string, string>, token?: CancellationToken): Promise<string[]> {
976
const result = await McpServer.callOn(this._server, h => h.complete({
977
ref: { type: 'ref/prompt', name: this._definition.name },
978
argument: { name: argument, value: prefix },
979
context: { arguments: alreadyResolved },
980
}, token), token);
981
return result.completion.values;
982
}
983
}
984
985
function encodeCapabilities(cap: MCP.ServerCapabilities): McpCapability {
986
let out = 0;
987
if (cap.logging) { out |= McpCapability.Logging; }
988
if (cap.completions) { out |= McpCapability.Completions; }
989
if (cap.prompts) {
990
out |= McpCapability.Prompts;
991
if (cap.prompts.listChanged) {
992
out |= McpCapability.PromptsListChanged;
993
}
994
}
995
if (cap.resources) {
996
out |= McpCapability.Resources;
997
if (cap.resources.subscribe) {
998
out |= McpCapability.ResourcesSubscribe;
999
}
1000
if (cap.resources.listChanged) {
1001
out |= McpCapability.ResourcesListChanged;
1002
}
1003
}
1004
if (cap.tools) {
1005
out |= McpCapability.Tools;
1006
if (cap.tools.listChanged) {
1007
out |= McpCapability.ToolsListChanged;
1008
}
1009
}
1010
return out;
1011
}
1012
1013
export class McpTool implements IMcpTool {
1014
1015
readonly id: string;
1016
readonly referenceName: string;
1017
readonly icons: IMcpIcons;
1018
readonly visibility: McpToolVisibility;
1019
1020
public get definition(): MCP.Tool { return this._definition; }
1021
public get uiResourceUri(): string | undefined { return this._definition.uiResourceUri; }
1022
1023
constructor(
1024
private readonly _server: McpServer,
1025
idPrefix: string,
1026
private readonly _definition: ValidatedMcpTool,
1027
@IMcpElicitationService private readonly _elicitationService: IMcpElicitationService,
1028
) {
1029
this.referenceName = _definition.name.replaceAll('.', '_');
1030
this.id = (idPrefix + _definition.name).replaceAll('.', '_').slice(0, McpToolName.MaxLength);
1031
this.icons = McpIcons.fromStored(this._definition._icons);
1032
this.visibility = _definition.visibility ?? (McpToolVisibility.Model | McpToolVisibility.App);
1033
}
1034
1035
async call(params: Record<string, unknown>, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {
1036
if (context) { this._server.runningToolCalls.add(context); }
1037
try {
1038
return await this._callWithProgress(params, undefined, context, token);
1039
} finally {
1040
if (context) { this._server.runningToolCalls.delete(context); }
1041
}
1042
}
1043
1044
async callWithProgress(params: Record<string, unknown>, progress: ToolProgress, context?: IMcpToolCallContext, token?: CancellationToken): Promise<MCP.CallToolResult> {
1045
if (context) { this._server.runningToolCalls.add(context); }
1046
try {
1047
return await this._callWithProgress(params, progress, context, token);
1048
} finally {
1049
if (context) { this._server.runningToolCalls.delete(context); }
1050
}
1051
}
1052
1053
_callWithProgress(params: Record<string, unknown>, progress: ToolProgress | undefined, context?: IMcpToolCallContext, token = CancellationToken.None, allowRetry = true): Promise<MCP.CallToolResult> {
1054
// serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it.
1055
const name = this._definition.serverToolName ?? this._definition.name;
1056
const progressToken = progress ? generateUuid() : undefined;
1057
const store = new DisposableStore();
1058
1059
return McpServer.callOn(this._server, async h => {
1060
if (progress) {
1061
store.add(h.onDidReceiveProgressNotification((e) => {
1062
if (e.params.progressToken === progressToken) {
1063
progress.report({
1064
message: e.params.message,
1065
progress: e.params.total !== undefined && e.params.progress !== undefined ? e.params.progress / e.params.total : undefined,
1066
});
1067
}
1068
}));
1069
}
1070
1071
const meta: Record<string, unknown> = { progressToken };
1072
if (context?.chatSessionId) {
1073
meta['vscode.conversationId'] = context.chatSessionId;
1074
}
1075
if (context?.chatRequestId) {
1076
meta['vscode.requestId'] = context.chatRequestId;
1077
}
1078
1079
const taskHint = this._definition.execution?.taskSupport;
1080
const serverSupportsTasksForTools = h.capabilities.tasks?.requests?.tools?.call !== undefined;
1081
const shouldUseTask = serverSupportsTasksForTools && (taskHint === 'required' || taskHint === 'optional');
1082
1083
try {
1084
const result = await h.callTool({
1085
name,
1086
arguments: params,
1087
task: shouldUseTask ? {} : undefined,
1088
_meta: meta,
1089
}, token);
1090
1091
// Wait for tools to refresh for dynamic servers (#261611)
1092
await this._server.awaitToolRefresh();
1093
1094
return result;
1095
} catch (err) {
1096
// Handle URL elicitation required error
1097
if (err instanceof MpcResponseError && err.code === MCP.URL_ELICITATION_REQUIRED && allowRetry) {
1098
await this._handleElicitationErr(err, context, token);
1099
return this._callWithProgress(params, progress, context, token, false);
1100
}
1101
1102
const state = this._server.connectionState.get();
1103
if (allowRetry && state.state === McpConnectionState.Kind.Error && state.shouldRetry) {
1104
return this._callWithProgress(params, progress, context, token, false);
1105
} else {
1106
throw err;
1107
}
1108
} finally {
1109
store.dispose();
1110
}
1111
}, token);
1112
}
1113
1114
private async _handleElicitationErr(err: MpcResponseError, context: IMcpToolCallContext | undefined, token: CancellationToken) {
1115
const elicitations = (err.data as MCP.URLElicitationRequiredError['error']['data'])?.elicitations;
1116
if (Array.isArray(elicitations) && elicitations.length > 0) {
1117
for (const elicitation of elicitations) {
1118
const elicitResult = await this._elicitationService.elicit(this._server, context, elicitation, token);
1119
1120
try {
1121
if (elicitResult.value.action !== 'accept') {
1122
throw err;
1123
}
1124
1125
if (elicitResult.kind === ElicitationKind.URL) {
1126
await elicitResult.wait;
1127
}
1128
} finally {
1129
elicitResult.dispose();
1130
}
1131
}
1132
}
1133
}
1134
1135
compare(other: IMcpTool): number {
1136
return this._definition.name.localeCompare(other.definition.name);
1137
}
1138
}
1139
1140
function warnInvalidTools(instaService: IInstantiationService, serverName: string, errorText: string) {
1141
instaService.invokeFunction((accessor) => {
1142
const notificationService = accessor.get(INotificationService);
1143
const editorService = accessor.get(IEditorService);
1144
notificationService.notify({
1145
severity: Severity.Warning,
1146
message: localize('mcpBadSchema', 'MCP server `{0}` has tools with invalid parameters which will be omitted.', serverName),
1147
actions: {
1148
primary: [{
1149
class: undefined,
1150
enabled: true,
1151
id: 'mcpBadSchema.show',
1152
tooltip: '',
1153
label: localize('mcpBadSchema.show', 'Show'),
1154
run: () => {
1155
editorService.openEditor({
1156
resource: undefined,
1157
contents: errorText,
1158
});
1159
}
1160
}]
1161
}
1162
});
1163
});
1164
}
1165
1166
class McpResource implements IMcpResource {
1167
readonly uri: URI;
1168
readonly mcpUri: string;
1169
readonly name: string;
1170
readonly description: string | undefined;
1171
readonly mimeType: string | undefined;
1172
readonly sizeInBytes: number | undefined;
1173
readonly title: string | undefined;
1174
1175
constructor(
1176
server: McpServer,
1177
original: MCP.Resource,
1178
public readonly icons: IMcpIcons,
1179
) {
1180
this.mcpUri = original.uri;
1181
this.title = original.title;
1182
this.uri = McpResourceURI.fromServer(server.definition, original.uri);
1183
this.name = original.name;
1184
this.description = original.description;
1185
this.mimeType = original.mimeType;
1186
this.sizeInBytes = original.size;
1187
}
1188
}
1189
1190
class McpResourceTemplate implements IMcpResourceTemplate {
1191
readonly name: string;
1192
readonly title?: string | undefined;
1193
readonly description?: string;
1194
readonly mimeType?: string;
1195
readonly template: UriTemplate;
1196
1197
constructor(
1198
private readonly _server: McpServer,
1199
private readonly _definition: MCP.ResourceTemplate,
1200
public readonly icons: IMcpIcons,
1201
) {
1202
this.name = _definition.name;
1203
this.description = _definition.description;
1204
this.mimeType = _definition.mimeType;
1205
this.title = _definition.title;
1206
this.template = UriTemplate.parse(_definition.uriTemplate);
1207
}
1208
1209
public resolveURI(vars: Record<string, unknown>): URI {
1210
const serverUri = this.template.resolve(vars);
1211
return McpResourceURI.fromServer(this._server.definition, serverUri);
1212
}
1213
1214
async complete(templatePart: string, prefix: string, alreadyResolved: Record<string, string | string[]>, token?: CancellationToken): Promise<string[]> {
1215
const result = await McpServer.callOn(this._server, h => h.complete({
1216
ref: { type: 'ref/resource', uri: this._definition.uriTemplate },
1217
argument: { name: templatePart, value: prefix },
1218
context: {
1219
arguments: mapValues(alreadyResolved, v => Array.isArray(v) ? v.join('/') : v),
1220
},
1221
}, token), token);
1222
return result.completion.values;
1223
}
1224
}
1225
1226