@@ -55,6 +55,7 @@ type ServerBootStateClassification = {
55
55
} ;
56
56
57
57
interface IToolCacheEntry {
58
+ readonly nonce : string | undefined ;
58
59
/** Cached tools so we can show what's available before it's started */
59
60
readonly tools : readonly IValidatedMcpTool [ ] ;
60
61
}
@@ -109,13 +110,13 @@ export class McpServerMetadataCache extends Disposable {
109
110
}
110
111
111
112
/** 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 ) ;
114
115
}
115
116
116
117
/** 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 } ) ;
119
120
this . didChange = true ;
120
121
}
121
122
@@ -154,25 +155,45 @@ export class McpServer extends Disposable implements IMcpServer {
154
155
private get toolsFromCache ( ) {
155
156
return this . _toolCache . getTools ( this . definition . id ) ;
156
157
}
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 ) ;
158
162
private readonly toolsFromServer = derived ( reader => this . toolsFromServerPromise . read ( reader ) ?. promiseResult . read ( reader ) ?. data ) ;
159
163
160
164
public readonly tools : IObservable < readonly IMcpTool [ ] > ;
161
165
162
166
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
+
163
180
const fromServer = this . toolsFromServerPromise . read ( reader ) ;
164
181
const connectionState = this . connectionState . read ( reader ) ;
165
182
const isIdle = McpConnectionState . canBeStarted ( connectionState . state ) && ! fromServer ;
166
183
if ( isIdle ) {
167
- return this . toolsFromCache ? McpServerToolsState . Cached : McpServerToolsState . Unknown ;
184
+ return stateWhenServingFromCache ( ) ;
168
185
}
169
186
170
187
const fromServerResult = fromServer ?. promiseResult . read ( reader ) ;
171
188
if ( ! fromServerResult ) {
172
189
return this . toolsFromCache ? McpServerToolsState . RefreshingFromCached : McpServerToolsState . RefreshingFromUnknown ;
173
190
}
174
191
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 ;
176
197
} ) ;
177
198
178
199
private readonly _loggerId : string ;
@@ -228,27 +249,20 @@ export class McpServer extends Disposable implements IMcpServer {
228
249
229
250
// 2. Populate this.tools when we connect to a server.
230
251
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 ) ;
234
256
} else {
235
257
this . resetLiveData ( ) ;
236
258
}
237
259
} ) ) ;
238
260
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
248
262
const toolPrefix = this . _mcpRegistry . collectionToolPrefix ( this . collection ) ;
249
263
this . tools = derived ( reader => {
250
264
const serverTools = this . toolsFromServer . read ( reader ) ;
251
- const definitions = serverTools ?? this . toolsFromCache ?? [ ] ;
265
+ const definitions = serverTools ?. tools ?? this . toolsFromCache ?. tools ?? [ ] ;
252
266
const prefix = toolPrefix . read ( reader ) ;
253
267
return definitions . map ( def => new McpTool ( this , prefix , def ) ) . sort ( ( a , b ) => a . compare ( b ) ) ;
254
268
} ) ;
@@ -384,7 +398,7 @@ export class McpServer extends Disposable implements IMcpServer {
384
398
return validated ;
385
399
}
386
400
387
- private populateLiveData ( handler : McpServerRequestHandler , store : DisposableStore ) {
401
+ private populateLiveData ( handler : McpServerRequestHandler , cacheNonce : string | undefined , store : DisposableStore ) {
388
402
const cts = new CancellationTokenSource ( ) ;
389
403
store . add ( toDisposable ( ( ) => cts . dispose ( true ) ) ) ;
390
404
@@ -394,11 +408,11 @@ export class McpServer extends Disposable implements IMcpServer {
394
408
const toolPromise = handler . capabilities . tools ? handler . listTools ( { } , cts . token ) : Promise . resolve ( [ ] ) ;
395
409
const toolPromiseSafe = toolPromise . then ( async tools => {
396
410
handler . logger . info ( `Discovered ${ tools . length } tools` ) ;
397
- return this . _getValidatedTools ( handler , tools ) ;
411
+ return { tools : await this . _getValidatedTools ( handler , tools ) , nonce : cacheNonce } ;
398
412
} ) ;
399
413
this . toolsFromServerPromise . set ( new ObservablePromise ( toolPromiseSafe ) , tx ) ;
400
414
401
- return [ toolPromise ] ;
415
+ return [ toolPromiseSafe ] ;
402
416
} ;
403
417
404
418
store . add ( handler . onDidChangeToolList ( ( ) => {
@@ -411,7 +425,9 @@ export class McpServer extends Disposable implements IMcpServer {
411
425
promises = updateTools ( tx ) ;
412
426
} ) ;
413
427
414
- Promise . all ( promises ! ) . then ( ( [ tools ] ) => {
428
+ Promise . all ( promises ! ) . then ( ( [ { tools } ] ) => {
429
+ this . _toolCache . storeTools ( this . definition . id , cacheNonce , tools ) ;
430
+
415
431
this . _telemetryService . publicLog2 < ServerBootData , ServerBootClassification > ( 'mcp/serverBoot' , {
416
432
supportsLogging : ! ! handler . capabilities . logging ,
417
433
supportsPrompts : ! ! handler . capabilities . prompts ,
0 commit comments