Skip to content

Commit 00249ce

Browse files
PederHPclaudeKKonstantinovfelixweinberger
authored
feat(client): return empty lists when server lacks capability (#1386)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com> Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
1 parent 4b5fdcb commit 00249ce

File tree

3 files changed

+113
-3
lines changed

3 files changed

+113
-3
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@modelcontextprotocol/client': patch
3+
---
4+
5+
Respect capability negotiation in list methods by returning empty lists when server lacks capability
6+
7+
The Client now returns empty lists instead of sending requests to servers that don't advertise the corresponding capability:
8+
- `listPrompts()` returns `{ prompts: [] }` if server lacks prompts capability
9+
- `listResources()` returns `{ resources: [] }` if server lacks resources capability
10+
- `listResourceTemplates()` returns `{ resourceTemplates: [] }` if server lacks resources capability
11+
- `listTools()` returns `{ tools: [] }` if server lacks tools capability
12+
13+
This respects the MCP spec requirement that "Both parties SHOULD respect capability negotiation" and avoids unnecessary server warnings and traffic. The existing `enforceStrictCapabilities` option continues to throw errors when set to `true`.

packages/client/src/client/client.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ export class Client<
253253
private _experimental?: { tasks: ExperimentalClientTasks<RequestT, NotificationT, ResultT> };
254254
private _listChangedDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
255255
private _pendingListChangedConfig?: ListChangedHandlers;
256+
private _enforceStrictCapabilities: boolean;
256257

257258
/**
258259
* Initializes this client with the given name and version information.
@@ -264,6 +265,7 @@ export class Client<
264265
super(options);
265266
this._capabilities = options?.capabilities ?? {};
266267
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();
268+
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
267269

268270
// Store list changed config for setup after connection (when we know server capabilities)
269271
if (options?.listChanged) {
@@ -728,14 +730,31 @@ export class Client<
728730
}
729731

730732
async listPrompts(params?: ListPromptsRequest['params'], options?: RequestOptions) {
733+
if (!this._serverCapabilities?.prompts && !this._enforceStrictCapabilities) {
734+
// Respect capability negotiation: server does not support prompts
735+
console.debug('Client.listPrompts() called but server does not advertise prompts capability - returning empty list');
736+
return { prompts: [] };
737+
}
731738
return this.request({ method: 'prompts/list', params }, ListPromptsResultSchema, options);
732739
}
733740

734741
async listResources(params?: ListResourcesRequest['params'], options?: RequestOptions) {
742+
if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) {
743+
// Respect capability negotiation: server does not support resources
744+
console.debug('Client.listResources() called but server does not advertise resources capability - returning empty list');
745+
return { resources: [] };
746+
}
735747
return this.request({ method: 'resources/list', params }, ListResourcesResultSchema, options);
736748
}
737749

738750
async listResourceTemplates(params?: ListResourceTemplatesRequest['params'], options?: RequestOptions) {
751+
if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) {
752+
// Respect capability negotiation: server does not support resources
753+
console.debug(
754+
'Client.listResourceTemplates() called but server does not advertise resources capability - returning empty list'
755+
);
756+
return { resourceTemplates: [] };
757+
}
739758
return this.request({ method: 'resources/templates/list', params }, ListResourceTemplatesResultSchema, options);
740759
}
741760

@@ -860,6 +879,11 @@ export class Client<
860879
}
861880

862881
async listTools(params?: ListToolsRequest['params'], options?: RequestOptions) {
882+
if (!this._serverCapabilities?.tools && !this._enforceStrictCapabilities) {
883+
// Respect capability negotiation: server does not support tools
884+
console.debug('Client.listTools() called but server does not advertise tools capability - returning empty list');
885+
return { tools: [] };
886+
}
863887
const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options);
864888

865889
// Cache the tools and their output schemas for future validation

test/integration/test/client/client.test.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,79 @@ test('should respect server capabilities', async () => {
595595
).rejects.toThrow('Server does not support completions');
596596
});
597597

598+
/***
599+
* Test: Return empty lists for missing capabilities (default behavior)
600+
* When enforceStrictCapabilities is not set (default), list methods should
601+
* return empty lists instead of sending requests to servers that don't
602+
* advertise those capabilities.
603+
*/
604+
test('should return empty lists for missing capabilities by default', async () => {
605+
const server = new Server(
606+
{
607+
name: 'test server',
608+
version: '1.0'
609+
},
610+
{
611+
capabilities: {
612+
// Server only supports tools - no prompts or resources
613+
tools: {}
614+
}
615+
}
616+
);
617+
618+
server.setRequestHandler(InitializeRequestSchema, _request => ({
619+
protocolVersion: LATEST_PROTOCOL_VERSION,
620+
capabilities: {
621+
tools: {}
622+
},
623+
serverInfo: {
624+
name: 'test',
625+
version: '1.0'
626+
}
627+
}));
628+
629+
server.setRequestHandler(ListToolsRequestSchema, () => ({
630+
tools: [{ name: 'test-tool', inputSchema: { type: 'object' } }]
631+
}));
632+
633+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
634+
635+
// Client with default settings (enforceStrictCapabilities not set)
636+
const client = new Client(
637+
{
638+
name: 'test client',
639+
version: '1.0'
640+
},
641+
{
642+
capabilities: {}
643+
}
644+
);
645+
646+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
647+
648+
// Server only supports tools
649+
expect(client.getServerCapabilities()).toEqual({
650+
tools: {}
651+
});
652+
653+
// listTools should work and return actual tools
654+
const toolsResult = await client.listTools();
655+
expect(toolsResult.tools).toHaveLength(1);
656+
expect(toolsResult.tools[0].name).toBe('test-tool');
657+
658+
// listPrompts should return empty list without sending request
659+
const promptsResult = await client.listPrompts();
660+
expect(promptsResult.prompts).toEqual([]);
661+
662+
// listResources should return empty list without sending request
663+
const resourcesResult = await client.listResources();
664+
expect(resourcesResult.resources).toEqual([]);
665+
666+
// listResourceTemplates should return empty list without sending request
667+
const templatesResult = await client.listResourceTemplates();
668+
expect(templatesResult.resourceTemplates).toEqual([]);
669+
});
670+
598671
/***
599672
* Test: Respect Client Notification Capabilities
600673
*/
@@ -1885,7 +1958,7 @@ describe('outputSchema validation', () => {
18851958
// Set up server handlers
18861959
server.setRequestHandler(InitializeRequestSchema, async request => ({
18871960
protocolVersion: request.params.protocolVersion,
1888-
capabilities: {},
1961+
capabilities: { tools: {} },
18891962
serverInfo: {
18901963
name: 'test-server',
18911964
version: '1.0.0'
@@ -1977,7 +2050,7 @@ describe('outputSchema validation', () => {
19772050
// Set up server handlers
19782051
server.setRequestHandler(InitializeRequestSchema, async request => ({
19792052
protocolVersion: request.params.protocolVersion,
1980-
capabilities: {},
2053+
capabilities: { tools: {} },
19812054
serverInfo: {
19822055
name: 'test-server',
19832056
version: '1.0.0'
@@ -2270,7 +2343,7 @@ describe('outputSchema validation', () => {
22702343
// Set up server handlers
22712344
server.setRequestHandler(InitializeRequestSchema, async request => ({
22722345
protocolVersion: request.params.protocolVersion,
2273-
capabilities: {},
2346+
capabilities: { tools: {} },
22742347
serverInfo: {
22752348
name: 'test-server',
22762349
version: '1.0.0'

0 commit comments

Comments
 (0)