fix(server,client): non-SEP draft spec conformance (eager list handlers, pagination docs, path-sanitization note) (#2269)

Co-authored-by: Felix Weinberger <3823880+felixweinberger@users.noreply.github.com>
This commit is contained in:
Matt 2026-06-15 13:25:35 +01:00 • committed by GitHub
commit e84c3e9ad0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 201 additions and 6 deletions

View file

@ -143,11 +143,12 @@ async function Client_listTools_pagination(client: Client) {
//#region Client_listTools_pagination
const allTools: Tool[] = [];
let cursor: string | undefined;
// Note: an empty-string cursor is valid and does not signal the end of results.
do {
const { tools, nextCursor } = await client.listTools({ cursor });
allTools.push(...tools);
cursor = nextCursor;
} while (cursor);
} while (cursor !== undefined);
console.log(
'Available tools:',
allTools.map(t => t.name)
@ -162,11 +163,12 @@ async function Client_listPrompts_pagination(client: Client) {
//#region Client_listPrompts_pagination
const allPrompts: Prompt[] = [];
let cursor: string | undefined;
// Note: an empty-string cursor is valid and does not signal the end of results.
do {
const { prompts, nextCursor } = await client.listPrompts({ cursor });
allPrompts.push(...prompts);
cursor = nextCursor;
} while (cursor);
} while (cursor !== undefined);
console.log(
'Available prompts:',
allPrompts.map(p => p.name)
@ -181,11 +183,12 @@ async function Client_listResources_pagination(client: Client) {
//#region Client_listResources_pagination
const allResources: Resource[] = [];
let cursor: string | undefined;
// Note: an empty-string cursor is valid and does not signal the end of results.
do {
const { resources, nextCursor } = await client.listResources({ cursor });
allResources.push(...resources);
cursor = nextCursor;
} while (cursor);
} while (cursor !== undefined);
console.log(
'Available resources:',
allResources.map(r => r.name)

View file

@ -677,11 +677,12 @@ export class Client extends Protocol<ClientContext> {
* ```ts source="./client.examples.ts#Client_listPrompts_pagination"
* const allPrompts: Prompt[] = [];
* let cursor: string | undefined;
* // Note: an empty-string cursor is valid and does not signal the end of results.
* do {
* const { prompts, nextCursor } = await client.listPrompts({ cursor });
* allPrompts.push(...prompts);
* cursor = nextCursor;
* } while (cursor);
* } while (cursor !== undefined);
* console.log(
* 'Available prompts:',
* allPrompts.map(p => p.name)
@ -707,11 +708,12 @@ export class Client extends Protocol<ClientContext> {
* ```ts source="./client.examples.ts#Client_listResources_pagination"
* const allResources: Resource[] = [];
* let cursor: string | undefined;
* // Note: an empty-string cursor is valid and does not signal the end of results.
* do {
* const { resources, nextCursor } = await client.listResources({ cursor });
* allResources.push(...resources);
* cursor = nextCursor;
* } while (cursor);
* } while (cursor !== undefined);
* console.log(
* 'Available resources:',
* allResources.map(r => r.name)
@ -870,11 +872,12 @@ export class Client extends Protocol<ClientContext> {
* ```ts source="./client.examples.ts#Client_listTools_pagination"
* const allTools: Tool[] = [];
* let cursor: string | undefined;
* // Note: an empty-string cursor is valid and does not signal the end of results.
* do {
* const { tools, nextCursor } = await client.listTools({ cursor });
* allTools.push(...tools);
* cursor = nextCursor;
* } while (cursor);
* } while (cursor !== undefined);
* console.log(
* 'Available tools:',
* allTools.map(t => t.name)

View file

@ -70,6 +70,21 @@ export class McpServer {
constructor(serverInfo: Implementation, options?: ServerOptions) {
this.server = new Server(serverInfo, options);
// Per the MCP spec, a server that declares a primitive capability MUST respond to its
// list method (potentially with an empty result) rather than "Method not found" — even
// if nothing has been registered yet. Handlers are normally installed lazily on first
// registration, so eagerly install them here for any capability declared up front.
// (Users of the low-level `Server` class remain responsible for their own handlers.)
if (options?.capabilities?.tools) {
this.setToolRequestHandlers();
}
if (options?.capabilities?.resources) {
this.setResourceRequestHandlers();
}
if (options?.capabilities?.prompts) {
this.setPromptRequestHandlers();
}
}
/**
@ -111,6 +126,9 @@ export class McpServer {
}
});
// Note: tools are listed in registration (insertion) order, which keeps the ordering
// deterministic across requests when the underlying tool set has not changed, as
// recommended by the spec.
this.server.setRequestHandler(
'tools/list',
(): ListToolsResult => ({

View file

@ -54,6 +54,13 @@ import { DefaultJsonSchemaValidator } from '@modelcontextprotocol/server/_shims'
export type ServerOptions = ProtocolOptions & {
/**
* Capabilities to advertise as being supported by this server.
*
* Note: per the MCP spec, a server that declares a capability MUST respond to that
* capability's requests (e.g. `tools/list` for `tools`) — potentially with an empty
* result — rather than with a "Method not found" error. {@linkcode server/mcp.McpServer | McpServer}
* handles this automatically for capabilities declared here; when using the low-level
* {@linkcode Server} directly, you are responsible for registering a request handler for
* every capability you declare.
*/
capabilities?: ServerCapabilities;