Skip to content

Commit 67facd2

Browse files
fix: respect disabled listChanged capabilities on v1.x
Preserve explicit listChanged:false settings in McpServer, gate list-changed notifications on the advertised capability, and add regression coverage for both capability values and runtime notification behavior. Amp-Thread-ID: https://ampcode.com/threads/T-019dba2f-f1e6-76fe-a320-f21f965bbe00 Co-authored-by: Amp <amp@ampcode.com>
1 parent bf1e022 commit 67facd2

3 files changed

Lines changed: 221 additions & 9 deletions

File tree

src/server/index.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,10 @@ export class Server<
464464
return this._clientVersion;
465465
}
466466

467-
private getCapabilities(): ServerCapabilities {
467+
/**
468+
* Returns the current server capabilities.
469+
*/
470+
public getCapabilities(): ServerCapabilities {
468471
return this._capabilities;
469472
}
470473

@@ -654,16 +657,22 @@ export class Server<
654657
}
655658

656659
async sendResourceListChanged() {
657-
return this.notification({
658-
method: 'notifications/resources/list_changed'
659-
});
660+
if (this._capabilities.resources?.listChanged) {
661+
return this.notification({
662+
method: 'notifications/resources/list_changed'
663+
});
664+
}
660665
}
661666

662667
async sendToolListChanged() {
663-
return this.notification({ method: 'notifications/tools/list_changed' });
668+
if (this._capabilities.tools?.listChanged) {
669+
return this.notification({ method: 'notifications/tools/list_changed' });
670+
}
664671
}
665672

666673
async sendPromptListChanged() {
667-
return this.notification({ method: 'notifications/prompts/list_changed' });
674+
if (this._capabilities.prompts?.listChanged) {
675+
return this.notification({ method: 'notifications/prompts/list_changed' });
676+
}
668677
}
669678
}

src/server/mcp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export class McpServer {
131131

132132
this.server.registerCapabilities({
133133
tools: {
134-
listChanged: true
134+
listChanged: this.server.getCapabilities().tools?.listChanged ?? true
135135
}
136136
});
137137

@@ -493,7 +493,7 @@ export class McpServer {
493493

494494
this.server.registerCapabilities({
495495
resources: {
496-
listChanged: true
496+
listChanged: this.server.getCapabilities().resources?.listChanged ?? true
497497
}
498498
});
499499

@@ -573,7 +573,7 @@ export class McpServer {
573573

574574
this.server.registerCapabilities({
575575
prompts: {
576-
listChanged: true
576+
listChanged: this.server.getCapabilities().prompts?.listChanged ?? true
577577
}
578578
});
579579

test/server/mcp.test.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,209 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
252252
expect(capabilities?.extensions).toBeDefined();
253253
expect(capabilities?.extensions?.['io.modelcontextprotocol/test-extension']).toEqual({ streaming: true });
254254
});
255+
256+
test('should expose current server capabilities via the underlying server', () => {
257+
const mcpServer = new McpServer(
258+
{
259+
name: 'test server',
260+
version: '1.0'
261+
},
262+
{ capabilities: { tools: { listChanged: false } } }
263+
);
264+
265+
expect(mcpServer.server.getCapabilities()).toEqual({
266+
tools: { listChanged: false }
267+
});
268+
});
269+
270+
/***
271+
* Test: listChanged capability should default to true when not specified
272+
*/
273+
test('should default tools.listChanged to true when not explicitly set', async () => {
274+
const mcpServer = new McpServer({
275+
name: 'test server',
276+
version: '1.0'
277+
});
278+
const client = new Client({
279+
name: 'test client',
280+
version: '1.0'
281+
});
282+
283+
mcpServer.registerTool('test', {}, async () => ({
284+
content: [{ type: 'text', text: 'Test' }]
285+
}));
286+
287+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
288+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
289+
290+
const capabilities = client.getServerCapabilities();
291+
expect(capabilities?.tools?.listChanged).toBe(true);
292+
});
293+
294+
/***
295+
* Test: listChanged capability should respect explicit false setting
296+
*/
297+
test('should respect tools.listChanged: false when explicitly set', async () => {
298+
const mcpServer = new McpServer({ name: 'test server', version: '1.0' }, { capabilities: { tools: { listChanged: false } } });
299+
const client = new Client({
300+
name: 'test client',
301+
version: '1.0'
302+
});
303+
304+
mcpServer.registerTool('test', {}, async () => ({
305+
content: [{ type: 'text', text: 'Test' }]
306+
}));
307+
308+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
309+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
310+
311+
const capabilities = client.getServerCapabilities();
312+
expect(capabilities?.tools?.listChanged).toBe(false);
313+
});
314+
315+
/***
316+
* Test: resources.listChanged should respect explicit false setting
317+
*/
318+
test('should respect resources.listChanged: false when explicitly set', async () => {
319+
const mcpServer = new McpServer(
320+
{
321+
name: 'test server',
322+
version: '1.0'
323+
},
324+
{ capabilities: { resources: { listChanged: false } } }
325+
);
326+
const client = new Client({
327+
name: 'test client',
328+
version: '1.0'
329+
});
330+
331+
mcpServer.registerResource('test-resource', 'test://resource', {}, async () => ({
332+
contents: [{ uri: 'test://resource', text: 'Test' }]
333+
}));
334+
335+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
336+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
337+
338+
const capabilities = client.getServerCapabilities();
339+
expect(capabilities?.resources?.listChanged).toBe(false);
340+
});
341+
342+
/***
343+
* Test: prompts.listChanged should respect explicit false setting
344+
*/
345+
test('should respect prompts.listChanged: false when explicitly set', async () => {
346+
const mcpServer = new McpServer({ name: 'test server', version: '1.0' }, { capabilities: { prompts: { listChanged: false } } });
347+
const client = new Client({
348+
name: 'test client',
349+
version: '1.0'
350+
});
351+
352+
mcpServer.registerPrompt('test-prompt', {}, async () => ({
353+
messages: [{ role: 'assistant', content: { type: 'text', text: 'Test' } }]
354+
}));
355+
356+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
357+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
358+
359+
const capabilities = client.getServerCapabilities();
360+
expect(capabilities?.prompts?.listChanged).toBe(false);
361+
});
362+
363+
/***
364+
* Test: explicit false should suppress tool list changed notifications
365+
*/
366+
test('should not send tools list changed notifications when tools.listChanged is false', async () => {
367+
const notifications: Notification[] = [];
368+
const mcpServer = new McpServer({ name: 'test server', version: '1.0' }, { capabilities: { tools: { listChanged: false } } });
369+
const client = new Client({
370+
name: 'test client',
371+
version: '1.0'
372+
});
373+
client.fallbackNotificationHandler = async notification => {
374+
notifications.push(notification);
375+
};
376+
377+
mcpServer.registerTool('test', {}, async () => ({
378+
content: [{ type: 'text', text: 'Test' }]
379+
}));
380+
381+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
382+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
383+
384+
mcpServer.registerTool('test-2', {}, async () => ({
385+
content: [{ type: 'text', text: 'Test 2' }]
386+
}));
387+
388+
await new Promise(resolve => setTimeout(resolve, 50));
389+
390+
expect(notifications.filter(notification => notification.method === 'notifications/tools/list_changed')).toHaveLength(0);
391+
});
392+
393+
/***
394+
* Test: explicit false should suppress resource list changed notifications
395+
*/
396+
test('should not send resources list changed notifications when resources.listChanged is false', async () => {
397+
const notifications: Notification[] = [];
398+
const mcpServer = new McpServer(
399+
{
400+
name: 'test server',
401+
version: '1.0'
402+
},
403+
{ capabilities: { resources: { listChanged: false } } }
404+
);
405+
const client = new Client({
406+
name: 'test client',
407+
version: '1.0'
408+
});
409+
client.fallbackNotificationHandler = async notification => {
410+
notifications.push(notification);
411+
};
412+
413+
mcpServer.registerResource('test-resource', 'test://resource', {}, async () => ({
414+
contents: [{ uri: 'test://resource', text: 'Test' }]
415+
}));
416+
417+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
418+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
419+
420+
mcpServer.registerResource('test-resource-2', 'test://resource-2', {}, async () => ({
421+
contents: [{ uri: 'test://resource-2', text: 'Test 2' }]
422+
}));
423+
424+
await new Promise(resolve => setTimeout(resolve, 50));
425+
426+
expect(notifications.filter(notification => notification.method === 'notifications/resources/list_changed')).toHaveLength(0);
427+
});
428+
429+
/***
430+
* Test: explicit false should suppress prompt list changed notifications
431+
*/
432+
test('should not send prompts list changed notifications when prompts.listChanged is false', async () => {
433+
const notifications: Notification[] = [];
434+
const mcpServer = new McpServer({ name: 'test server', version: '1.0' }, { capabilities: { prompts: { listChanged: false } } });
435+
const client = new Client({
436+
name: 'test client',
437+
version: '1.0'
438+
});
439+
client.fallbackNotificationHandler = async notification => {
440+
notifications.push(notification);
441+
};
442+
443+
mcpServer.registerPrompt('test-prompt', {}, async () => ({
444+
messages: [{ role: 'assistant', content: { type: 'text', text: 'Test' } }]
445+
}));
446+
447+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
448+
await Promise.all([client.connect(clientTransport), mcpServer.connect(serverTransport)]);
449+
450+
mcpServer.registerPrompt('test-prompt-2', {}, async () => ({
451+
messages: [{ role: 'assistant', content: { type: 'text', text: 'Test 2' } }]
452+
}));
453+
454+
await new Promise(resolve => setTimeout(resolve, 50));
455+
456+
expect(notifications.filter(notification => notification.method === 'notifications/prompts/list_changed')).toHaveLength(0);
457+
});
255458
});
256459

257460
describe('ResourceTemplate', () => {

0 commit comments

Comments
 (0)