From 8356cb900c3843b2f1fc2b3d0a598bef6060b8c7 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Wed, 8 Jan 2025 13:44:12 +0000 Subject: [PATCH] Create `ResourceTemplate` class and move `listCallback` into it --- src/server/index.test.ts | 42 +++++++++++++--- src/server/index.ts | 101 ++++++++++++++++++++------------------- 2 files changed, 88 insertions(+), 55 deletions(-) diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 99a60e7..4ec0f30 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -25,6 +25,7 @@ import { Transport } from "../shared/transport.js"; import { InMemoryTransport } from "../inMemory.js"; import { Client } from "../client/index.js"; import { UriTemplate } from "../shared/uriTemplate.js"; +import { ResourceTemplate } from "./index.js"; test("should accept latest protocol version", async () => { let sendPromiseResolve: (value: unknown) => void; @@ -553,6 +554,34 @@ test("should handle request timeout", async () => { }); }); +describe("ResourceTemplate", () => { + test("should create ResourceTemplate with string pattern", () => { + const template = new ResourceTemplate("test://{category}/{id}", undefined); + expect(template.uriTemplate.toString()).toBe("test://{category}/{id}"); + expect(template.listCallback).toBeUndefined(); + }); + + test("should create ResourceTemplate with UriTemplate", () => { + const uriTemplate = new UriTemplate("test://{category}/{id}"); + const template = new ResourceTemplate(uriTemplate, undefined); + expect(template.uriTemplate).toBe(uriTemplate); + expect(template.listCallback).toBeUndefined(); + }); + + test("should create ResourceTemplate with list callback", async () => { + const listCallback = jest.fn().mockResolvedValue({ + resources: [{ name: "Test", uri: "test://example" }], + }); + + const template = new ResourceTemplate("test://{id}", listCallback); + expect(template.listCallback).toBe(listCallback); + + const result = await template.listCallback?.(); + expect(result?.resources).toHaveLength(1); + expect(listCallback).toHaveBeenCalled(); + }); +}); + describe("Server.tool", () => { test("should register zero-argument tool", async () => { const server = new Server({ @@ -1032,7 +1061,7 @@ describe("Server.resource", () => { server.resource( "test", - new UriTemplate("test://resource/{id}"), + new ResourceTemplate("test://resource/{id}", undefined), async () => ({ contents: [ { @@ -1077,8 +1106,7 @@ describe("Server.resource", () => { server.resource( "test", - new UriTemplate("test://resource/{id}"), - async () => ({ + new ResourceTemplate("test://resource/{id}", async () => ({ resources: [ { name: "Resource 1", @@ -1089,7 +1117,7 @@ describe("Server.resource", () => { uri: "test://resource/2", }, ], - }), + })), async (uri) => ({ contents: [ { @@ -1134,7 +1162,7 @@ describe("Server.resource", () => { server.resource( "test", - new UriTemplate("test://resource/{category}/{id}"), + new ResourceTemplate("test://resource/{category}/{id}", undefined), async (uri, { category, id }) => ({ contents: [ { @@ -1201,7 +1229,7 @@ describe("Server.resource", () => { server.resource( "test", - new UriTemplate("test://resource/{id}"), + new ResourceTemplate("test://resource/{id}", undefined), async () => ({ contents: [ { @@ -1215,7 +1243,7 @@ describe("Server.resource", () => { expect(() => { server.resource( "test", - new UriTemplate("test://resource/{id}"), + new ResourceTemplate("test://resource/{id}", undefined), async () => ({ contents: [ { diff --git a/src/server/index.ts b/src/server/index.ts index a0cc2ed..9f51f91 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -98,9 +98,8 @@ export type ReadResourceTemplateCallback = ( ) => ReadResourceResult | Promise; type RegisteredResourceTemplate = { - uriTemplate: UriTemplate; + resourceTemplate: ResourceTemplate; metadata?: ResourceMetadata; - listCallback?: ListResourcesCallback; readCallback: ReadResourceTemplateCallback; }; @@ -558,11 +557,11 @@ export class Server< const templateResources: Resource[] = []; for (const template of Object.values(this._registeredResourceTemplates)) { - if (!template.listCallback) { + if (!template.resourceTemplate.listCallback) { continue; } - const result = await template.listCallback(); + const result = await template.resourceTemplate.listCallback(); for (const resource of result.resources) { templateResources.push({ ...resource, @@ -579,7 +578,7 @@ export class Server< this._registeredResourceTemplates, ).map(([name, template]) => ({ name, - uriTemplate: template.uriTemplate.toString(), + uriTemplate: template.resourceTemplate.uriTemplate.toString(), ...template.metadata, })); @@ -597,7 +596,9 @@ export class Server< // Then check templates for (const template of Object.values(this._registeredResourceTemplates)) { - const variables = template.uriTemplate.match(uri.toString()); + const variables = template.resourceTemplate.uriTemplate.match( + uri.toString(), + ); if (variables) { return template.readCallback(uri, variables); } @@ -623,48 +624,27 @@ export class Server< ): void; /** - * Registers a resource `name` with a URI template pattern, which will use the given callback to respond to read requests. + * Registers a resource `name` with a template pattern, which will use the given callback to respond to read requests. */ resource( name: string, - uriTemplate: UriTemplate, + template: ResourceTemplate, readCallback: ReadResourceTemplateCallback, ): void; /** - * Registers a resource `name` with a URI template pattern and metadata, which will use the given callback to respond to read requests. + * Registers a resource `name` with a template pattern and metadata, which will use the given callback to respond to read requests. */ resource( name: string, - uriTemplate: UriTemplate, + template: ResourceTemplate, metadata: ResourceMetadata, readCallback: ReadResourceTemplateCallback, ): void; - /** - * Registers a resource `name` with a URI template pattern, which will use the list callback to enumerate matching resources and read callback to respond to read requests. - */ - resource( - name: string, - uriTemplate: UriTemplate, - listCallback: ListResourcesCallback, - readCallback: ReadResourceTemplateCallback, - ): void; - - /** - * Registers a resource `name` with a URI template pattern and metadata, which will use the list callback to enumerate matching resources and read callback to respond to read requests. - */ resource( name: string, - uriTemplate: UriTemplate, - metadata: ResourceMetadata, - listCallback: ListResourcesCallback, - readCallback: ReadResourceTemplateCallback, - ): void; - - resource( - name: string, - uriOrTemplate: string | UriTemplate, + uriOrTemplate: string | ResourceTemplate, ...rest: unknown[] ): void { let metadata: ResourceMetadata | undefined; @@ -672,27 +652,23 @@ export class Server< metadata = rest.shift() as ResourceMetadata; } - let listCallback: ListResourcesCallback | undefined; - if (rest.length > 1) { - listCallback = rest.shift() as ListResourcesCallback; - } + const readCallback = rest[0] as + | ReadResourceCallback + | ReadResourceTemplateCallback; if (typeof uriOrTemplate === "string") { - const readCallback = rest[0] as ReadResourceCallback; this.registerResource({ name, uri: uriOrTemplate, metadata, - readCallback, + readCallback: readCallback as ReadResourceCallback, }); } else { - const readCallback = rest[0] as ReadResourceTemplateCallback; this.registerResourceTemplate({ name, - uriTemplate: uriOrTemplate, + resourceTemplate: uriOrTemplate, metadata, - listCallback, - readCallback, + readCallback: readCallback as ReadResourceTemplateCallback, }); } } @@ -723,15 +699,13 @@ export class Server< private registerResourceTemplate({ name, - uriTemplate, + resourceTemplate, metadata, - listCallback, readCallback, }: { name: string; - uriTemplate: UriTemplate; + resourceTemplate: ResourceTemplate; metadata?: ResourceMetadata; - listCallback?: ListResourcesCallback; readCallback: ReadResourceTemplateCallback; }): void { if (this._registeredResourceTemplates[name]) { @@ -739,12 +713,43 @@ export class Server< } this._registeredResourceTemplates[name] = { - uriTemplate, + resourceTemplate, metadata, - listCallback, readCallback, }; this.setResourceRequestHandlers(); } } + +/** + * A resource template combines a URI pattern with optional functionality to enumerate + * all resources matching that pattern. + */ +export class ResourceTemplate { + private _uriTemplate: UriTemplate; + + constructor( + uriTemplate: string | UriTemplate, + private _listCallback: ListResourcesCallback | undefined, + ) { + this._uriTemplate = + typeof uriTemplate === "string" + ? new UriTemplate(uriTemplate) + : uriTemplate; + } + + /** + * Gets the URI template pattern. + */ + get uriTemplate(): UriTemplate { + return this._uriTemplate; + } + + /** + * Gets the list callback, if one was provided. + */ + get listCallback(): ListResourcesCallback | undefined { + return this._listCallback; + } +}