Handle capabilities checks at the request() level, make non-strict by default

This commit is contained in:
Justin Spahr-Summers 2024-11-12 10:57:59 +00:00
commit dc45f5d148
No known key found for this signature in database
2 changed files with 254 additions and 83 deletions

View file

@ -46,10 +46,12 @@ test("should accept latest protocol version", async () => {
version: "1.0",
},
{
prompts: {},
resources: {},
tools: {},
logging: {},
capabilities: {
prompts: {},
resources: {},
tools: {},
logging: {},
},
},
);
@ -105,10 +107,12 @@ test("should accept supported older protocol version", async () => {
version: "1.0",
},
{
prompts: {},
resources: {},
tools: {},
logging: {},
capabilities: {
prompts: {},
resources: {},
tools: {},
logging: {},
},
},
);
@ -163,10 +167,12 @@ test("should handle unsupported protocol version", async () => {
version: "1.0",
},
{
prompts: {},
resources: {},
tools: {},
logging: {},
capabilities: {
prompts: {},
resources: {},
tools: {},
logging: {},
},
},
);
@ -197,19 +203,25 @@ test("should respect client capabilities", async () => {
version: "1.0",
},
{
prompts: {},
resources: {},
tools: {},
logging: {},
capabilities: {
prompts: {},
resources: {},
tools: {},
logging: {},
},
enforceStrictCapabilities: true,
},
);
const client = new Client(
{
name: "test client",
version: "1.0",
},
{
sampling: {},
capabilities: {
sampling: {},
},
},
);
@ -245,9 +257,7 @@ test("should respect client capabilities", async () => {
).resolves.not.toThrow();
// This should still throw because roots are not supported by the client
await expect(server.listRoots()).rejects.toThrow(
"Client does not support roots",
);
await expect(server.listRoots()).rejects.toThrow(/^Client does not support/);
});
/*
@ -301,10 +311,12 @@ test("should typecheck", () => {
version: "1.0.0",
},
{
prompts: {},
resources: {},
tools: {},
logging: {},
capabilities: {
prompts: {},
resources: {},
tools: {},
logging: {},
},
},
);

View file

@ -1,4 +1,8 @@
import { ProgressCallback, Protocol } from "../shared/protocol.js";
import {
ProgressCallback,
Protocol,
ProtocolOptions,
} from "../shared/protocol.js";
import {
ClientCapabilities,
CreateMessageRequest,
@ -24,6 +28,13 @@ import {
SUPPORTED_PROTOCOL_VERSIONS,
} from "../types.js";
export type ServerOptions = ProtocolOptions & {
/**
* Capabilities to advertise as being supported by this server.
*/
capabilities: ClientCapabilities;
};
/**
* An MCP server on top of a pluggable transport.
*
@ -60,6 +71,7 @@ export class Server<
> {
private _clientCapabilities?: ClientCapabilities;
private _clientVersion?: Implementation;
private _capabilities: ServerCapabilities;
/**
* Callback for when initialization has fully completed (i.e., the client has sent an `initialized` notification).
@ -71,9 +83,10 @@ export class Server<
*/
constructor(
private _serverInfo: Implementation,
private _capabilities: ServerCapabilities,
options: ServerOptions,
) {
super();
super(options);
this._capabilities = options.capabilities;
this.setRequestHandler(InitializeRequestSchema, (request) =>
this._oninitialize(request),
@ -83,6 +96,30 @@ export class Server<
);
}
protected assertCapabilityForMethod(method: RequestT["method"]): void {
switch (method as ServerRequest["method"]) {
case "sampling/createMessage":
if (!this._clientCapabilities?.sampling) {
throw new Error(
`Client does not support sampling (required for ${method})`,
);
}
break;
case "roots/list":
if (!this._clientCapabilities?.roots) {
throw new Error(
`Client does not support listing roots (required for ${method})`,
);
}
break;
case "ping":
// No specific capability required for ping
break;
}
}
private async _oninitialize(
request: InitializeRequest,
): Promise<InitializeResult> {
@ -137,7 +174,6 @@ export class Server<
params: CreateMessageRequest["params"],
onprogress?: ProgressCallback,
) {
this.assertCapability("sampling", "sampling/createMessage");
return this.request(
{ method: "sampling/createMessage", params },
CreateMessageResultSchema,
@ -149,7 +185,6 @@ export class Server<
params?: ListRootsRequest["params"],
onprogress?: ProgressCallback,
) {
this.assertCapability("roots", "roots/list");
return this.request(
{ method: "roots/list", params },
ListRootsResultSchema,