Skip to content

Commit 8b307a5

Browse files
committed
Check capabilities when request handlers are set
1 parent 64c2862 commit 8b307a5

File tree

5 files changed

+155
-1
lines changed

5 files changed

+155
-1
lines changed

src/client/index.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
InitializeRequestSchema,
1313
ListResourcesRequestSchema,
1414
ListToolsRequestSchema,
15+
CreateMessageRequestSchema,
16+
ListRootsRequestSchema,
1517
} from "../types.js";
1618
import { Transport } from "../shared/transport.js";
1719
import { Server } from "../server/index.js";
@@ -325,6 +327,37 @@ test("should respect server notification capabilities", async () => {
325327
);
326328
});
327329

330+
test("should only allow setRequestHandler for declared capabilities", () => {
331+
const client = new Client(
332+
{
333+
name: "test client",
334+
version: "1.0",
335+
},
336+
{
337+
capabilities: {
338+
sampling: {},
339+
},
340+
},
341+
);
342+
343+
// This should work because sampling is a declared capability
344+
expect(() => {
345+
client.setRequestHandler(CreateMessageRequestSchema, () => ({
346+
model: "test-model",
347+
role: "assistant",
348+
content: {
349+
type: "text",
350+
text: "Test response",
351+
},
352+
}));
353+
}).not.toThrow();
354+
355+
// This should throw because roots listing is not a declared capability
356+
expect(() => {
357+
client.setRequestHandler(ListRootsRequestSchema, () => ({}));
358+
}).toThrow("Client does not support roots capability");
359+
});
360+
328361
/*
329362
Test that custom request/notification/result schemas can be used with the Client class.
330363
*/

src/client/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,26 @@ export class Client<
248248
}
249249
}
250250

251+
protected assertRequestHandlerCapability(method: string): void {
252+
switch (method) {
253+
case "sampling/createMessage":
254+
if (!this._capabilities.sampling) {
255+
throw new Error("Client does not support sampling capability");
256+
}
257+
break;
258+
259+
case "roots/list":
260+
if (!this._capabilities.roots) {
261+
throw new Error("Client does not support roots capability");
262+
}
263+
break;
264+
265+
case "ping":
266+
// No specific capability required for ping
267+
break;
268+
}
269+
}
270+
251271
async ping() {
252272
return this.request({ method: "ping" }, EmptyResultSchema);
253273
}

src/server/index.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import {
1010
LATEST_PROTOCOL_VERSION,
1111
SUPPORTED_PROTOCOL_VERSIONS,
1212
CreateMessageRequestSchema,
13+
ListPromptsRequestSchema,
14+
ListResourcesRequestSchema,
15+
ListToolsRequestSchema,
16+
SetLevelRequestSchema,
1317
} from "../types.js";
1418
import { Transport } from "../shared/transport.js";
1519
import { InMemoryTransport } from "../inMemory.js";
@@ -293,6 +297,41 @@ test("should respect server notification capabilities", async () => {
293297
).rejects.toThrow(/^Server does not support/);
294298
});
295299

300+
test("should only allow setRequestHandler for declared capabilities", () => {
301+
const server = new Server(
302+
{
303+
name: "test server",
304+
version: "1.0",
305+
},
306+
{
307+
capabilities: {
308+
prompts: {},
309+
resources: {},
310+
},
311+
},
312+
);
313+
314+
// These should work because the capabilities are declared
315+
expect(() => {
316+
server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: [] }));
317+
}).not.toThrow();
318+
319+
expect(() => {
320+
server.setRequestHandler(ListResourcesRequestSchema, () => ({
321+
resources: [],
322+
}));
323+
}).not.toThrow();
324+
325+
// These should throw because the capabilities are not declared
326+
expect(() => {
327+
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [] }));
328+
}).toThrow(/^Server does not support tools/);
329+
330+
expect(() => {
331+
server.setRequestHandler(SetLevelRequestSchema, () => ({}));
332+
}).toThrow(/^Server does not support logging/);
333+
});
334+
296335
/*
297336
Test that custom request/notification/result schemas can be used with the Server class.
298337
*/

src/server/index.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,59 @@ export class Server<
163163
}
164164
}
165165

166+
protected assertRequestHandlerCapability(method: string): void {
167+
switch (method) {
168+
case "sampling/createMessage":
169+
if (!this._capabilities.sampling) {
170+
throw new Error(
171+
`Server does not support sampling (required for ${method})`,
172+
);
173+
}
174+
break;
175+
176+
case "logging/setLevel":
177+
if (!this._capabilities.logging) {
178+
throw new Error(
179+
`Server does not support logging (required for ${method})`,
180+
);
181+
}
182+
break;
183+
184+
case "prompts/get":
185+
case "prompts/list":
186+
if (!this._capabilities.prompts) {
187+
throw new Error(
188+
`Server does not support prompts (required for ${method})`,
189+
);
190+
}
191+
break;
192+
193+
case "resources/list":
194+
case "resources/templates/list":
195+
case "resources/read":
196+
if (!this._capabilities.resources) {
197+
throw new Error(
198+
`Server does not support resources (required for ${method})`,
199+
);
200+
}
201+
break;
202+
203+
case "tools/call":
204+
case "tools/list":
205+
if (!this._capabilities.tools) {
206+
throw new Error(
207+
`Server does not support tools (required for ${method})`,
208+
);
209+
}
210+
break;
211+
212+
case "ping":
213+
case "initialize":
214+
// No specific capability required for these methods
215+
break;
216+
}
217+
}
218+
166219
private async _oninitialize(
167220
request: InitializeRequest,
168221
): Promise<InitializeResult> {

src/shared/protocol.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,13 @@ export abstract class Protocol<
277277
method: SendNotificationT["method"],
278278
): void;
279279

280+
/**
281+
* A method to check if a request handler is supported by the local side, for the given method to be handled.
282+
*
283+
* This should be implemented by subclasses.
284+
*/
285+
protected abstract assertRequestHandlerCapability(method: string): void;
286+
280287
/**
281288
* Sends a request and wait for a response, with optional progress notifications in the meantime (if supported by the server).
282289
*
@@ -360,7 +367,9 @@ export abstract class Protocol<
360367
requestSchema: T,
361368
handler: (request: z.infer<T>) => SendResultT | Promise<SendResultT>,
362369
): void {
363-
this._requestHandlers.set(requestSchema.shape.method.value, (request) =>
370+
const method = requestSchema.shape.method.value;
371+
this.assertRequestHandlerCapability(method);
372+
this._requestHandlers.set(method, (request) =>
364373
Promise.resolve(handler(requestSchema.parse(request))),
365374
);
366375
}

0 commit comments

Comments
 (0)