From a7dcb88fad808aeb886e4cac3a77a89ed2a1f365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asbj=C3=B8rn=20Enge?= Date: Tue, 20 May 2025 09:59:21 +0200 Subject: [PATCH 1/2] First pass --- src/server/mcp.test.ts | 147 +++++++++++++++++++++++++++++++++++++++++ src/server/mcp.ts | 53 +++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 1b2f4d4b..6754a44f 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1601,6 +1601,67 @@ describe("resource()", () => { expect(result.resources[0].name).toBe("test"); expect(result.resources[0].uri).toBe("test://resource"); }); + + /*** + * Test: Resource Registration with registerResource + */ + test("should register resource with registerResource method", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + const client = new Client({ + name: "test client", + version: "1.0", + }); + + mcpServer.registerResource("test-resource", { + uri: "test://registered-resource", + description: "A test resource registered with registerResource" + }, async () => ({ + contents: [ + { + uri: "test://registered-resource", + text: "Content from registered resource", + }, + ], + })); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + // Check if resource is listed correctly + const listResult = await client.request( + { + method: "resources/list", + }, + ListResourcesResultSchema, + ); + + expect(listResult.resources).toHaveLength(1); + expect(listResult.resources[0].name).toBe("test-resource"); + expect(listResult.resources[0].uri).toBe("test://registered-resource"); + expect(listResult.resources[0].description).toBe("A test resource registered with registerResource"); + + // Check if resource can be read + const readResult = await client.request( + { + method: "resources/read", + params: { + uri: "test://registered-resource", + }, + }, + ReadResourceResultSchema, + ); + + expect(readResult.contents).toHaveLength(1); + expect(readResult.contents[0].text).toBe("Content from registered resource"); + }); /*** * Test: Update Resource with URI @@ -2160,6 +2221,92 @@ describe("resource()", () => { })); }).toThrow(/already registered/); }); + + /*** + * Test: Preventing Duplicate Resource Registration with registerResource + */ + test("should prevent duplicate resource registration with registerResource", () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + + mcpServer.resource("test", "test://resource", async () => ({ + contents: [ + { + uri: "test://resource", + text: "Test content", + }, + ], + })); + + expect(() => { + mcpServer.registerResource("test2", { + uri: "test://resource", + description: "Duplicate resource" + }, async () => ({ + contents: [ + { + uri: "test://resource", + text: "Test content 2", + }, + ], + })); + }).toThrow(/already registered/); + }); + + /*** + * Test: Resource Registration with registerResource and Metadata + */ + test("should register resource with registerResource and metadata", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + const client = new Client({ + name: "test client", + version: "1.0", + }); + + mcpServer.registerResource("metadata-resource", { + uri: "test://metadata-resource", + description: "Resource with metadata", + metadata: { + mimeType: "application/json", + custom: "custom-value" + } + }, async () => ({ + contents: [ + { + uri: "test://metadata-resource", + text: "{\"test\": \"data\"}", + mimeType: "application/json" + }, + ], + })); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + const result = await client.request( + { + method: "resources/list", + }, + ListResourcesResultSchema, + ); + + expect(result.resources).toHaveLength(1); + expect(result.resources[0].name).toBe("metadata-resource"); + expect(result.resources[0].uri).toBe("test://metadata-resource"); + expect(result.resources[0].description).toBe("Resource with metadata"); + expect(result.resources[0].mimeType).toBe("application/json"); + expect(result.resources[0].custom).toBe("custom-value"); + }); /*** * Test: Multiple Resource Registration diff --git a/src/server/mcp.ts b/src/server/mcp.ts index a5f2d0f1..1be89f14 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -825,6 +825,59 @@ export class McpServer { ) } + /** + * Registers a resource with a config object and callback. + * Similar to the resource() method but with a more structured configuration. + */ + registerResource( + name: string, + config: { + uri: string; + description?: string; + metadata?: ResourceMetadata; + }, + readCallback: ReadResourceCallback + ): RegisteredResource { + if (this._registeredResources[config.uri]) { + throw new Error(`Resource ${config.uri} is already registered`); + } + + const { uri, description, metadata } = config; + const resourceMetadata = { ...metadata }; + + // Add description to metadata if provided + if (description) { + resourceMetadata.description = description; + } + + const registeredResource: RegisteredResource = { + name, + metadata: resourceMetadata, + readCallback, + enabled: true, + disable: () => registeredResource.update({ enabled: false }), + enable: () => registeredResource.update({ enabled: true }), + remove: () => registeredResource.update({ uri: null }), + update: (updates) => { + if (typeof updates.uri !== "undefined" && updates.uri !== uri) { + delete this._registeredResources[uri] + if (updates.uri) this._registeredResources[updates.uri] = registeredResource + } + if (typeof updates.name !== "undefined") registeredResource.name = updates.name + if (typeof updates.metadata !== "undefined") registeredResource.metadata = updates.metadata + if (typeof updates.callback !== "undefined") registeredResource.readCallback = updates.callback + if (typeof updates.enabled !== "undefined") registeredResource.enabled = updates.enabled + this.sendResourceListChanged() + }, + }; + + this._registeredResources[uri] = registeredResource; + this.setResourceRequestHandlers(); + this.sendResourceListChanged(); + + return registeredResource; + } + /** * Registers a zero-argument prompt `name`, which will run the given function when the client calls it. */ From f67b9c01a92e3daff894931e4f51c2c888735ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asbj=C3=B8rn=20Enge?= Date: Tue, 20 May 2025 11:15:18 +0200 Subject: [PATCH 2/2] Added some docs for registerTool and registerResource --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index f468969b..9397b9a1 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,26 @@ server.resource( }] }) ); + +// Structured resource registration with config object +server.registerResource( + "documentation", + { + uri: "docs://api/reference", + description: "API Reference Documentation", + metadata: { + mimeType: "text/markdown", + tags: ["reference", "api"] + } + }, + async (uri) => ({ + contents: [{ + uri: uri.href, + text: "# API Reference\n\nThis is the API reference documentation.", + mimeType: "text/markdown" + }] + }) +); ``` ### Tools @@ -167,6 +187,32 @@ server.tool( }; } ); + +// Structured tool registration with config object +server.registerTool( + "structured-tool", + { + description: "A tool registered with the structured approach", + inputSchema: { + query: z.string().min(3), + limit: z.number().optional() + }, + outputSchema: { + results: z.array(z.string()), + count: z.number() + }, + annotations: { + readOnlyHint: true, + title: "Search Tool" + } + }, + async ({ query, limit = 10 }) => ({ + structuredContent: { + results: [`Result for: ${query}`], + count: 1 + } + }) +); ``` ### Prompts