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>
This commit is contained in:
parent
4b5fdcba02
commit
00249ce86d
3 changed files with 113 additions and 3 deletions
13
.changeset/respect-capability-negotiation.md
Normal file
13
.changeset/respect-capability-negotiation.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
'@modelcontextprotocol/client': patch
|
||||
---
|
||||
|
||||
Respect capability negotiation in list methods by returning empty lists when server lacks capability
|
||||
|
||||
The Client now returns empty lists instead of sending requests to servers that don't advertise the corresponding capability:
|
||||
- `listPrompts()` returns `{ prompts: [] }` if server lacks prompts capability
|
||||
- `listResources()` returns `{ resources: [] }` if server lacks resources capability
|
||||
- `listResourceTemplates()` returns `{ resourceTemplates: [] }` if server lacks resources capability
|
||||
- `listTools()` returns `{ tools: [] }` if server lacks tools capability
|
||||
|
||||
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`.
|
||||
|
|
@ -253,6 +253,7 @@ export class Client<
|
|||
private _experimental?: { tasks: ExperimentalClientTasks<RequestT, NotificationT, ResultT> };
|
||||
private _listChangedDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map();
|
||||
private _pendingListChangedConfig?: ListChangedHandlers;
|
||||
private _enforceStrictCapabilities: boolean;
|
||||
|
||||
/**
|
||||
* Initializes this client with the given name and version information.
|
||||
|
|
@ -264,6 +265,7 @@ export class Client<
|
|||
super(options);
|
||||
this._capabilities = options?.capabilities ?? {};
|
||||
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();
|
||||
this._enforceStrictCapabilities = options?.enforceStrictCapabilities ?? false;
|
||||
|
||||
// Store list changed config for setup after connection (when we know server capabilities)
|
||||
if (options?.listChanged) {
|
||||
|
|
@ -728,14 +730,31 @@ export class Client<
|
|||
}
|
||||
|
||||
async listPrompts(params?: ListPromptsRequest['params'], options?: RequestOptions) {
|
||||
if (!this._serverCapabilities?.prompts && !this._enforceStrictCapabilities) {
|
||||
// Respect capability negotiation: server does not support prompts
|
||||
console.debug('Client.listPrompts() called but server does not advertise prompts capability - returning empty list');
|
||||
return { prompts: [] };
|
||||
}
|
||||
return this.request({ method: 'prompts/list', params }, ListPromptsResultSchema, options);
|
||||
}
|
||||
|
||||
async listResources(params?: ListResourcesRequest['params'], options?: RequestOptions) {
|
||||
if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) {
|
||||
// Respect capability negotiation: server does not support resources
|
||||
console.debug('Client.listResources() called but server does not advertise resources capability - returning empty list');
|
||||
return { resources: [] };
|
||||
}
|
||||
return this.request({ method: 'resources/list', params }, ListResourcesResultSchema, options);
|
||||
}
|
||||
|
||||
async listResourceTemplates(params?: ListResourceTemplatesRequest['params'], options?: RequestOptions) {
|
||||
if (!this._serverCapabilities?.resources && !this._enforceStrictCapabilities) {
|
||||
// Respect capability negotiation: server does not support resources
|
||||
console.debug(
|
||||
'Client.listResourceTemplates() called but server does not advertise resources capability - returning empty list'
|
||||
);
|
||||
return { resourceTemplates: [] };
|
||||
}
|
||||
return this.request({ method: 'resources/templates/list', params }, ListResourceTemplatesResultSchema, options);
|
||||
}
|
||||
|
||||
|
|
@ -860,6 +879,11 @@ export class Client<
|
|||
}
|
||||
|
||||
async listTools(params?: ListToolsRequest['params'], options?: RequestOptions) {
|
||||
if (!this._serverCapabilities?.tools && !this._enforceStrictCapabilities) {
|
||||
// Respect capability negotiation: server does not support tools
|
||||
console.debug('Client.listTools() called but server does not advertise tools capability - returning empty list');
|
||||
return { tools: [] };
|
||||
}
|
||||
const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options);
|
||||
|
||||
// Cache the tools and their output schemas for future validation
|
||||
|
|
|
|||
|
|
@ -595,6 +595,79 @@ test('should respect server capabilities', async () => {
|
|||
).rejects.toThrow('Server does not support completions');
|
||||
});
|
||||
|
||||
/***
|
||||
* Test: Return empty lists for missing capabilities (default behavior)
|
||||
* When enforceStrictCapabilities is not set (default), list methods should
|
||||
* return empty lists instead of sending requests to servers that don't
|
||||
* advertise those capabilities.
|
||||
*/
|
||||
test('should return empty lists for missing capabilities by default', async () => {
|
||||
const server = new Server(
|
||||
{
|
||||
name: 'test server',
|
||||
version: '1.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
// Server only supports tools - no prompts or resources
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
server.setRequestHandler(InitializeRequestSchema, _request => ({
|
||||
protocolVersion: LATEST_PROTOCOL_VERSION,
|
||||
capabilities: {
|
||||
tools: {}
|
||||
},
|
||||
serverInfo: {
|
||||
name: 'test',
|
||||
version: '1.0'
|
||||
}
|
||||
}));
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
||||
tools: [{ name: 'test-tool', inputSchema: { type: 'object' } }]
|
||||
}));
|
||||
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
// Client with default settings (enforceStrictCapabilities not set)
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'test client',
|
||||
version: '1.0'
|
||||
},
|
||||
{
|
||||
capabilities: {}
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
|
||||
|
||||
// Server only supports tools
|
||||
expect(client.getServerCapabilities()).toEqual({
|
||||
tools: {}
|
||||
});
|
||||
|
||||
// listTools should work and return actual tools
|
||||
const toolsResult = await client.listTools();
|
||||
expect(toolsResult.tools).toHaveLength(1);
|
||||
expect(toolsResult.tools[0].name).toBe('test-tool');
|
||||
|
||||
// listPrompts should return empty list without sending request
|
||||
const promptsResult = await client.listPrompts();
|
||||
expect(promptsResult.prompts).toEqual([]);
|
||||
|
||||
// listResources should return empty list without sending request
|
||||
const resourcesResult = await client.listResources();
|
||||
expect(resourcesResult.resources).toEqual([]);
|
||||
|
||||
// listResourceTemplates should return empty list without sending request
|
||||
const templatesResult = await client.listResourceTemplates();
|
||||
expect(templatesResult.resourceTemplates).toEqual([]);
|
||||
});
|
||||
|
||||
/***
|
||||
* Test: Respect Client Notification Capabilities
|
||||
*/
|
||||
|
|
@ -1885,7 +1958,7 @@ describe('outputSchema validation', () => {
|
|||
// Set up server handlers
|
||||
server.setRequestHandler(InitializeRequestSchema, async request => ({
|
||||
protocolVersion: request.params.protocolVersion,
|
||||
capabilities: {},
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: {
|
||||
name: 'test-server',
|
||||
version: '1.0.0'
|
||||
|
|
@ -1977,7 +2050,7 @@ describe('outputSchema validation', () => {
|
|||
// Set up server handlers
|
||||
server.setRequestHandler(InitializeRequestSchema, async request => ({
|
||||
protocolVersion: request.params.protocolVersion,
|
||||
capabilities: {},
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: {
|
||||
name: 'test-server',
|
||||
version: '1.0.0'
|
||||
|
|
@ -2270,7 +2343,7 @@ describe('outputSchema validation', () => {
|
|||
// Set up server handlers
|
||||
server.setRequestHandler(InitializeRequestSchema, async request => ({
|
||||
protocolVersion: request.params.protocolVersion,
|
||||
capabilities: {},
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: {
|
||||
name: 'test-server',
|
||||
version: '1.0.0'
|
||||
|
|
|
|||
Loading…
Reference in a new issue