Skip to content
29 changes: 16 additions & 13 deletions packages/client/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ import {
ResourceListChangedNotificationSchema,
safeParse,
SUPPORTED_PROTOCOL_VERSIONS,
ToolListChangedNotificationSchema
ToolListChangedNotificationSchema,
UnsupportedCapabilityError
} from '@modelcontextprotocol/core';

import { ExperimentalClientTasks } from '../experimental/tasks/client.js';
Expand Down Expand Up @@ -479,7 +480,7 @@ export class Client<

protected assertCapability(capability: keyof ServerCapabilities, method: string): void {
if (!this._serverCapabilities?.[capability]) {
throw new Error(`Server does not support ${capability} (required for ${method})`);
throw new UnsupportedCapabilityError(`Server does not support ${capability} (required for ${method})`);
}
}

Expand Down Expand Up @@ -562,14 +563,14 @@ export class Client<
switch (method as ClientRequest['method']) {
case 'logging/setLevel':
if (!this._serverCapabilities?.logging) {
throw new Error(`Server does not support logging (required for ${method})`);
throw new UnsupportedCapabilityError(`Server does not support logging (required for ${method})`);
}
break;

case 'prompts/get':
case 'prompts/list':
if (!this._serverCapabilities?.prompts) {
throw new Error(`Server does not support prompts (required for ${method})`);
throw new UnsupportedCapabilityError(`Server does not support prompts (required for ${method})`);
}
break;

Expand All @@ -579,25 +580,25 @@ export class Client<
case 'resources/subscribe':
case 'resources/unsubscribe':
if (!this._serverCapabilities?.resources) {
throw new Error(`Server does not support resources (required for ${method})`);
throw new UnsupportedCapabilityError(`Server does not support resources (required for ${method})`);
}

if (method === 'resources/subscribe' && !this._serverCapabilities.resources.subscribe) {
throw new Error(`Server does not support resource subscriptions (required for ${method})`);
throw new UnsupportedCapabilityError(`Server does not support resource subscriptions (required for ${method})`);
}

break;

case 'tools/call':
case 'tools/list':
if (!this._serverCapabilities?.tools) {
throw new Error(`Server does not support tools (required for ${method})`);
throw new UnsupportedCapabilityError(`Server does not support tools (required for ${method})`);
}
break;

case 'completion/complete':
if (!this._serverCapabilities?.completions) {
throw new Error(`Server does not support completions (required for ${method})`);
throw new UnsupportedCapabilityError(`Server does not support completions (required for ${method})`);
}
break;

Expand All @@ -615,7 +616,9 @@ export class Client<
switch (method as ClientNotification['method']) {
case 'notifications/roots/list_changed':
if (!this._capabilities.roots?.listChanged) {
throw new Error(`Client does not support roots list changed notifications (required for ${method})`);
throw new UnsupportedCapabilityError(
`Client does not support roots list changed notifications (required for ${method})`
);
}
break;

Expand Down Expand Up @@ -643,19 +646,19 @@ export class Client<
switch (method) {
case 'sampling/createMessage':
if (!this._capabilities.sampling) {
throw new Error(`Client does not support sampling capability (required for ${method})`);
throw new UnsupportedCapabilityError(`Client does not support sampling capability (required for ${method})`);
}
break;

case 'elicitation/create':
if (!this._capabilities.elicitation) {
throw new Error(`Client does not support elicitation capability (required for ${method})`);
throw new UnsupportedCapabilityError(`Client does not support elicitation capability (required for ${method})`);
}
break;

case 'roots/list':
if (!this._capabilities.roots) {
throw new Error(`Client does not support roots capability (required for ${method})`);
throw new UnsupportedCapabilityError(`Client does not support roots capability (required for ${method})`);
}
break;

Expand All @@ -664,7 +667,7 @@ export class Client<
case 'tasks/result':
case 'tasks/cancel':
if (!this._capabilities.tasks) {
throw new Error(`Client does not support tasks capability (required for ${method})`);
throw new UnsupportedCapabilityError(`Client does not support tasks capability (required for ${method})`);
}
break;

Expand Down
22 changes: 15 additions & 7 deletions packages/core/src/experimental/tasks/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* @experimental
*/

import { UnsupportedCapabilityError } from '../../types/types.js';

/**
* Type representing the task requests capability structure.
* This is derived from ClientTasksCapability.requests and ServerTasksCapability.requests.
Expand All @@ -22,7 +24,7 @@ interface TaskRequestsCapability {
* @param requests - The task requests capability object
* @param method - The method being checked
* @param entityName - 'Server' or 'Client' for error messages
* @throws Error if the capability is not supported
* @throws UnsupportedCapabilityError if the capability is not supported
*
* @experimental
*/
Expand All @@ -32,13 +34,15 @@ export function assertToolsCallTaskCapability(
entityName: 'Server' | 'Client'
): void {
if (!requests) {
throw new Error(`${entityName} does not support task creation (required for ${method})`);
throw new UnsupportedCapabilityError(`${entityName} does not support task creation (required for ${method})`);
}

switch (method) {
case 'tools/call':
if (!requests.tools?.call) {
throw new Error(`${entityName} does not support task creation for tools/call (required for ${method})`);
throw new UnsupportedCapabilityError(
`${entityName} does not support task creation for tools/call (required for ${method})`
);
}
break;

Expand All @@ -55,7 +59,7 @@ export function assertToolsCallTaskCapability(
* @param requests - The task requests capability object
* @param method - The method being checked
* @param entityName - 'Server' or 'Client' for error messages
* @throws Error if the capability is not supported
* @throws UnsupportedCapabilityError if the capability is not supported
*
* @experimental
*/
Expand All @@ -65,19 +69,23 @@ export function assertClientRequestTaskCapability(
entityName: 'Server' | 'Client'
): void {
if (!requests) {
throw new Error(`${entityName} does not support task creation (required for ${method})`);
throw new UnsupportedCapabilityError(`${entityName} does not support task creation (required for ${method})`);
}

switch (method) {
case 'sampling/createMessage':
if (!requests.sampling?.createMessage) {
throw new Error(`${entityName} does not support task creation for sampling/createMessage (required for ${method})`);
throw new UnsupportedCapabilityError(
`${entityName} does not support task creation for sampling/createMessage (required for ${method})`
);
}
break;

case 'elicitation/create':
if (!requests.elicitation?.create) {
throw new Error(`${entityName} does not support task creation for elicitation/create (required for ${method})`);
throw new UnsupportedCapabilityError(
`${entityName} does not support task creation for elicitation/create (required for ${method})`
);
}
break;

Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ export enum ErrorCode {
InternalError = -32603,

// MCP-specific error codes
UrlElicitationRequired = -32042
UrlElicitationRequired = -32042,
UnsupportedCapability = -32043
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per my other comment, seems like introducing a new code and a protocol-level error would be out of scope for this?

In reality, these errors would be thrown if:

  • enforceStrictCapabilities is true
  • the client implementation is incorrect and is trying to perform a request for which the server has not declared the capability for

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the protocol-level error code

}

/**
Expand Down Expand Up @@ -2369,6 +2370,12 @@ export class UrlElicitationRequiredError extends McpError {
}
}

export class UnsupportedCapabilityError extends McpError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

McpError is intended for protocol-level errors, while clients trying to do unsupported capabilities seems like an implementation error. Thus it makes sense to extend Error instead for the simple purpose of catching it/distinguishing it easily, as opposed to switching to a protocol-level error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes you're right, I misunderstood that part.
Thank you for the suggestion!

constructor(message: string) {
super(ErrorCode.UnsupportedCapability, message);
}
}

type Primitive = string | number | boolean | bigint | null | undefined;
type Flatten<T> = T extends Primitive
? T
Expand Down
2 changes: 1 addition & 1 deletion test/integration/test/client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ test('should respect client notification capabilities', async () => {
await clientWithoutCapability.connect(clientTransport);

// This should throw because the client doesn't have the roots.listChanged capability
await expect(clientWithoutCapability.sendRootsListChanged()).rejects.toThrow(/^Client does not support/);
await expect(clientWithoutCapability.sendRootsListChanged()).rejects.toThrow(/^MCP error -32043: Client does not support/);
});

/***
Expand Down
Loading