Skip to content

Commit 4ccec89

Browse files
authored
mcp: finish wiring up the 'version' on ext MCP collections (#247096)
1 parent 544674f commit 4ccec89

File tree

5 files changed

+50
-26
lines changed

5 files changed

+50
-26
lines changed

src/vs/workbench/api/common/extHostMcp.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export class ExtHostMcpService extends Disposable implements IExtHostMpcService
112112
servers.push({
113113
id: ExtensionIdentifier.toKey(extension.identifier),
114114
label: item.label,
115+
cacheNonce: item.version,
115116
launch: Convert.McpServerDefinition.from(item)
116117
});
117118
}

src/vs/workbench/contrib/mcp/browser/mcpCommands.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
247247
let thisState = DisplayedState.None;
248248
switch (server.toolsState.read(reader)) {
249249
case McpServerToolsState.Unknown:
250+
case McpServerToolsState.Outdated:
250251
if (server.trusted.read(reader) === false) {
251252
thisState = DisplayedState.None;
252253
} else {
@@ -324,7 +325,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo
324325

325326
const { state, servers } = displayedState.get();
326327
if (state === DisplayedState.NewTools) {
327-
servers.forEach(server => server.start());
328+
servers.forEach(server => server.stop().then(() => server.start()));
328329
mcpService.activateCollections();
329330
} else if (state === DisplayedState.Refreshing) {
330331
servers.at(-1)?.showOutput();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class McpContextKeysController extends Disposable implements IWorkbenchCo
5656
}
5757

5858
const toolState = s.toolsState.read(r);
59-
return toolState === McpServerToolsState.Unknown || toolState === McpServerToolsState.RefreshingFromUnknown;
59+
return toolState === McpServerToolsState.Unknown || toolState === McpServerToolsState.Outdated || toolState === McpServerToolsState.RefreshingFromUnknown;
6060
}));
6161
}));
6262
}

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

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type ServerBootStateClassification = {
5555
};
5656

5757
interface IToolCacheEntry {
58+
readonly nonce: string | undefined;
5859
/** Cached tools so we can show what's available before it's started */
5960
readonly tools: readonly IValidatedMcpTool[];
6061
}
@@ -109,13 +110,13 @@ export class McpServerMetadataCache extends Disposable {
109110
}
110111

111112
/** Gets cached tools for a server (used before a server is running) */
112-
getTools(definitionId: string): readonly IValidatedMcpTool[] | undefined {
113-
return this.cache.get(definitionId)?.tools;
113+
getTools(definitionId: string) {
114+
return this.cache.get(definitionId);
114115
}
115116

116117
/** Sets cached tools for a server */
117-
storeTools(definitionId: string, tools: readonly IValidatedMcpTool[]): void {
118-
this.cache.set(definitionId, { ...this.cache.get(definitionId), tools });
118+
storeTools(definitionId: string, nonce: string | undefined, tools: readonly IValidatedMcpTool[]): void {
119+
this.cache.set(definitionId, { ...this.cache.get(definitionId), nonce, tools });
119120
this.didChange = true;
120121
}
121122

@@ -154,25 +155,45 @@ export class McpServer extends Disposable implements IMcpServer {
154155
private get toolsFromCache() {
155156
return this._toolCache.getTools(this.definition.id);
156157
}
157-
private readonly toolsFromServerPromise = observableValue<ObservablePromise<readonly IValidatedMcpTool[]> | undefined>(this, undefined);
158+
private readonly toolsFromServerPromise = observableValue<ObservablePromise<{
159+
readonly tools: IValidatedMcpTool[];
160+
readonly nonce: string | undefined;
161+
}> | undefined>(this, undefined);
158162
private readonly toolsFromServer = derived(reader => this.toolsFromServerPromise.read(reader)?.promiseResult.read(reader)?.data);
159163

160164
public readonly tools: IObservable<readonly IMcpTool[]>;
161165

162166
public readonly toolsState = derived(reader => {
167+
const currentNonce = () => this._mcpRegistry.collections.read(reader)
168+
.find(c => c.id === this.collection.id)
169+
?.serverDefinitions.read(reader)
170+
.find(d => d.id === this.definition.id)
171+
?.cacheNonce;
172+
const stateWhenServingFromCache = () => {
173+
if (!this.toolsFromCache) {
174+
return McpServerToolsState.Unknown;
175+
}
176+
177+
return currentNonce() === this.toolsFromCache.nonce ? McpServerToolsState.Cached : McpServerToolsState.Outdated;
178+
};
179+
163180
const fromServer = this.toolsFromServerPromise.read(reader);
164181
const connectionState = this.connectionState.read(reader);
165182
const isIdle = McpConnectionState.canBeStarted(connectionState.state) && !fromServer;
166183
if (isIdle) {
167-
return this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown;
184+
return stateWhenServingFromCache();
168185
}
169186

170187
const fromServerResult = fromServer?.promiseResult.read(reader);
171188
if (!fromServerResult) {
172189
return this.toolsFromCache ? McpServerToolsState.RefreshingFromCached : McpServerToolsState.RefreshingFromUnknown;
173190
}
174191

175-
return fromServerResult.error ? (this.toolsFromCache ? McpServerToolsState.Cached : McpServerToolsState.Unknown) : McpServerToolsState.Live;
192+
if (fromServerResult.error) {
193+
return stateWhenServingFromCache();
194+
}
195+
196+
return fromServerResult.data?.nonce === currentNonce() ? McpServerToolsState.Live : McpServerToolsState.Outdated;
176197
});
177198

178199
private readonly _loggerId: string;
@@ -228,27 +249,20 @@ export class McpServer extends Disposable implements IMcpServer {
228249

229250
// 2. Populate this.tools when we connect to a server.
230251
this._register(autorunWithStore((reader, store) => {
231-
const cnx = this._connection.read(reader)?.handler.read(reader);
232-
if (cnx) {
233-
this.populateLiveData(cnx, store);
252+
const cnx = this._connection.read(reader);
253+
const handler = cnx?.handler.read(reader);
254+
if (handler) {
255+
this.populateLiveData(handler, cnx?.definition.cacheNonce, store);
234256
} else {
235257
this.resetLiveData();
236258
}
237259
}));
238260

239-
// 3. Update the cache when tools update
240-
this._register(autorun(reader => {
241-
const tools = this.toolsFromServer.read(reader);
242-
if (tools) {
243-
this._toolCache.storeTools(definition.id, tools);
244-
}
245-
}));
246-
247-
// 4. Publish tools
261+
// 3. Publish tools
248262
const toolPrefix = this._mcpRegistry.collectionToolPrefix(this.collection);
249263
this.tools = derived(reader => {
250264
const serverTools = this.toolsFromServer.read(reader);
251-
const definitions = serverTools ?? this.toolsFromCache ?? [];
265+
const definitions = serverTools?.tools ?? this.toolsFromCache?.tools ?? [];
252266
const prefix = toolPrefix.read(reader);
253267
return definitions.map(def => new McpTool(this, prefix, def)).sort((a, b) => a.compare(b));
254268
});
@@ -384,7 +398,7 @@ export class McpServer extends Disposable implements IMcpServer {
384398
return validated;
385399
}
386400

387-
private populateLiveData(handler: McpServerRequestHandler, store: DisposableStore) {
401+
private populateLiveData(handler: McpServerRequestHandler, cacheNonce: string | undefined, store: DisposableStore) {
388402
const cts = new CancellationTokenSource();
389403
store.add(toDisposable(() => cts.dispose(true)));
390404

@@ -394,11 +408,11 @@ export class McpServer extends Disposable implements IMcpServer {
394408
const toolPromise = handler.capabilities.tools ? handler.listTools({}, cts.token) : Promise.resolve([]);
395409
const toolPromiseSafe = toolPromise.then(async tools => {
396410
handler.logger.info(`Discovered ${tools.length} tools`);
397-
return this._getValidatedTools(handler, tools);
411+
return { tools: await this._getValidatedTools(handler, tools), nonce: cacheNonce };
398412
});
399413
this.toolsFromServerPromise.set(new ObservablePromise(toolPromiseSafe), tx);
400414

401-
return [toolPromise];
415+
return [toolPromiseSafe];
402416
};
403417

404418
store.add(handler.onDidChangeToolList(() => {
@@ -411,7 +425,9 @@ export class McpServer extends Disposable implements IMcpServer {
411425
promises = updateTools(tx);
412426
});
413427

414-
Promise.all(promises!).then(([tools]) => {
428+
Promise.all(promises!).then(([{ tools }]) => {
429+
this._toolCache.storeTools(this.definition.id, cacheNonce, tools);
430+
415431
this._telemetryService.publicLog2<ServerBootData, ServerBootClassification>('mcp/serverBoot', {
416432
supportsLogging: !!handler.capabilities.logging,
417433
supportsPrompts: !!handler.capabilities.prompts,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ export interface McpServerDefinition {
104104
readonly roots?: URI[] | undefined;
105105
/** If set, allows configuration variables to be resolved in the {@link launch} with the given context */
106106
readonly variableReplacement?: McpServerDefinitionVariableReplacement;
107+
/** Nonce used for caching the server. Changing the nonce will indicate that tools need to be refreshed. */
108+
readonly cacheNonce?: string;
107109

108110
readonly presentation?: {
109111
/** Sort order of the definition. */
@@ -117,6 +119,7 @@ export namespace McpServerDefinition {
117119
export interface Serialized {
118120
readonly id: string;
119121
readonly label: string;
122+
readonly cacheNonce?: string;
120123
readonly launch: McpServerLaunch.Serialized;
121124
readonly variableReplacement?: McpServerDefinitionVariableReplacement.Serialized;
122125
}
@@ -129,6 +132,7 @@ export namespace McpServerDefinition {
129132
return {
130133
id: def.id,
131134
label: def.label,
135+
cacheNonce: def.cacheNonce,
132136
launch: McpServerLaunch.fromSerialized(def.launch),
133137
variableReplacement: def.variableReplacement ? McpServerDefinitionVariableReplacement.fromSerialized(def.variableReplacement) : undefined,
134138
};
@@ -233,6 +237,8 @@ export const enum McpServerToolsState {
233237
Unknown,
234238
/** Tools were read from the cache */
235239
Cached,
240+
/** Tools were read from the cache or live, but they may be outdated. */
241+
Outdated,
236242
/** Tools are refreshing for the first time */
237243
RefreshingFromUnknown,
238244
/** Tools are refreshing and the current tools are cached */

0 commit comments

Comments
 (0)