From 90e91cdd6223381f4cdfe0ba5521645b99bcc136 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Mon, 11 Nov 2024 22:30:50 +0000 Subject: [PATCH] Tests and assertions for client/server capabilities Resolves #45. --- src/client/index.test.ts | 60 ++++++++++++++++++++++++++++++++++++++++ src/client/index.ts | 22 +++++++++++++++ src/server/index.test.ts | 31 +++++++++++++++++++-- src/server/index.ts | 19 +++++++++++-- 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 86b12ed7..6bcbf56f 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -9,8 +9,13 @@ import { ResultSchema, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, + InitializeRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, } from "../types.js"; import { Transport } from "../shared/transport.js"; +import { Server } from "../server/index.js"; +import { InMemoryTransport } from "../inMemory.js"; test("should initialize with matching protocol version", async () => { const clientTransport: Transport = { @@ -126,6 +131,61 @@ test("should reject unsupported protocol version", async () => { expect(clientTransport.close).toHaveBeenCalled(); }); +test("should respect server capabilities", async () => { + const server = new Server({ + name: "test server", + version: "1.0", + }); + + server.setRequestHandler(InitializeRequestSchema, (request) => ({ + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: { + resources: {}, + tools: {}, + }, + serverInfo: { + name: "test", + version: "1.0", + }, + })); + + server.setRequestHandler(ListResourcesRequestSchema, () => ({ + resources: [], + })); + + server.setRequestHandler(ListToolsRequestSchema, () => ({ + tools: [], + })); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + const client = new Client({ + name: "test client", + version: "1.0", + }); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + // Server supports resources and tools, but not prompts + expect(client.getServerCapabilities()).toEqual({ + resources: {}, + tools: {}, + }); + + // These should work + await expect(client.listResources()).resolves.not.toThrow(); + await expect(client.listTools()).resolves.not.toThrow(); + + // This should throw because prompts are not supported + await expect(client.listPrompts()).rejects.toThrow( + "Server does not support prompts", + ); +}); + /* Test that custom request/notification/result schemas can be used with the Client class. */ diff --git a/src/client/index.ts b/src/client/index.ts index 3b194f21..b2ead251 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -132,6 +132,17 @@ export class Client< return this._serverVersion; } + private assertCapability( + capability: keyof ServerCapabilities, + method: string, + ) { + if (!this._serverCapabilities?.[capability]) { + throw new Error( + `Server does not support ${capability} (required for ${method})`, + ); + } + } + async ping() { return this.request({ method: "ping" }, EmptyResultSchema); } @@ -140,6 +151,7 @@ export class Client< params: CompleteRequest["params"], onprogress?: ProgressCallback, ) { + this.assertCapability("prompts", "completion/complete"); return this.request( { method: "completion/complete", params }, CompleteResultSchema, @@ -148,6 +160,7 @@ export class Client< } async setLoggingLevel(level: LoggingLevel) { + this.assertCapability("logging", "logging/setLevel"); return this.request( { method: "logging/setLevel", params: { level } }, EmptyResultSchema, @@ -158,6 +171,7 @@ export class Client< params: GetPromptRequest["params"], onprogress?: ProgressCallback, ) { + this.assertCapability("prompts", "prompts/get"); return this.request( { method: "prompts/get", params }, GetPromptResultSchema, @@ -169,6 +183,7 @@ export class Client< params?: ListPromptsRequest["params"], onprogress?: ProgressCallback, ) { + this.assertCapability("prompts", "prompts/list"); return this.request( { method: "prompts/list", params }, ListPromptsResultSchema, @@ -180,6 +195,7 @@ export class Client< params?: ListResourcesRequest["params"], onprogress?: ProgressCallback, ) { + this.assertCapability("resources", "resources/list"); return this.request( { method: "resources/list", params }, ListResourcesResultSchema, @@ -191,6 +207,7 @@ export class Client< params?: ListResourceTemplatesRequest["params"], onprogress?: ProgressCallback, ) { + this.assertCapability("resources", "resources/templates/list"); return this.request( { method: "resources/templates/list", params }, ListResourceTemplatesResultSchema, @@ -202,6 +219,7 @@ export class Client< params: ReadResourceRequest["params"], onprogress?: ProgressCallback, ) { + this.assertCapability("resources", "resources/read"); return this.request( { method: "resources/read", params }, ReadResourceResultSchema, @@ -210,6 +228,7 @@ export class Client< } async subscribeResource(params: SubscribeRequest["params"]) { + this.assertCapability("resources", "resources/subscribe"); return this.request( { method: "resources/subscribe", params }, EmptyResultSchema, @@ -217,6 +236,7 @@ export class Client< } async unsubscribeResource(params: UnsubscribeRequest["params"]) { + this.assertCapability("resources", "resources/unsubscribe"); return this.request( { method: "resources/unsubscribe", params }, EmptyResultSchema, @@ -230,6 +250,7 @@ export class Client< | typeof CompatibilityCallToolResultSchema = CallToolResultSchema, onprogress?: ProgressCallback, ) { + this.assertCapability("tools", "tools/call"); return this.request( { method: "tools/call", params }, resultSchema, @@ -241,6 +262,7 @@ export class Client< params?: ListToolsRequest["params"], onprogress?: ProgressCallback, ) { + this.assertCapability("tools", "tools/list"); return this.request( { method: "tools/list", params }, ListToolsResultSchema, diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 5045e2a7..dd5c94a4 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -9,10 +9,10 @@ import { ResultSchema, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, - InitializeRequestSchema, - InitializeResultSchema, } from "../types.js"; import { Transport } from "../shared/transport.js"; +import { InMemoryTransport } from "../inMemory.js"; +import { Client } from "../client/index.js"; test("should accept latest protocol version", async () => { let sendPromiseResolve: (value: unknown) => void; @@ -165,6 +165,33 @@ test("should handle unsupported protocol version", async () => { await expect(sendPromise).resolves.toBeUndefined(); }); +test("should respect client capabilities", async () => { + const server = new Server({ + name: "test server", + version: "1.0", + }); + + const client = new Client({ + name: "test client", + version: "1.0", + }); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + + expect(server.getClientCapabilities()).toEqual({}); + + // This should throw because roots are not supported by the client + await expect(server.listRoots()).rejects.toThrow( + "Client does not support roots", + ); +}); + /* Test that custom request/notification/result schemas can be used with the Server class. */ diff --git a/src/server/index.ts b/src/server/index.ts index 8cf2a919..203d3bd6 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -25,7 +25,7 @@ import { ServerRequest, ServerResult, SetLevelRequestSchema, - SUPPORTED_PROTOCOL_VERSIONS + SUPPORTED_PROTOCOL_VERSIONS, } from "../types.js"; /** @@ -93,7 +93,9 @@ export class Server< this._clientVersion = request.params.clientInfo; return { - protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion : LATEST_PROTOCOL_VERSION, + protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) + ? requestedVersion + : LATEST_PROTOCOL_VERSION, capabilities: this.getCapabilities(), serverInfo: this._serverInfo, }; @@ -138,6 +140,17 @@ export class Server< }; } + private assertCapability( + capability: keyof ClientCapabilities, + method: string, + ) { + if (!this._clientCapabilities?.[capability]) { + throw new Error( + `Client does not support ${capability} (required for ${method})`, + ); + } + } + async ping() { return this.request({ method: "ping" }, EmptyResultSchema); } @@ -146,6 +159,7 @@ export class Server< params: CreateMessageRequest["params"], onprogress?: ProgressCallback, ) { + this.assertCapability("sampling", "sampling/createMessage"); return this.request( { method: "sampling/createMessage", params }, CreateMessageResultSchema, @@ -157,6 +171,7 @@ export class Server< params?: ListRootsRequest["params"], onprogress?: ProgressCallback, ) { + this.assertCapability("roots", "roots/list"); return this.request( { method: "roots/list", params }, ListRootsResultSchema,