From 6bfa975eb251bb48513d96b002bd712bbb84086b Mon Sep 17 00:00:00 2001 From: SToneX Date: Tue, 11 Mar 2025 15:34:30 +0800 Subject: [PATCH 01/12] docs: Update README for client method usage --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fe4caa3f..449dafa4 100644 --- a/README.md +++ b/README.md @@ -459,15 +459,20 @@ await client.connect(transport); const prompts = await client.listPrompts(); // Get a prompt -const prompt = await client.getPrompt("example-prompt", { - arg1: "value" +const prompt = await client.getPrompt({ + name: "example-prompt", + arguments: { + arg1: "value" + } }); // List resources const resources = await client.listResources(); // Read a resource -const resource = await client.readResource("file:///example.txt"); +const resource = await client.readResource({ + uri: "file:///example.txt" +}); // Call a tool const result = await client.callTool({ From 670d98863dcb63100e45fbee9ef5f88a398f3930 Mon Sep 17 00:00:00 2001 From: Yechao Li Date: Thu, 13 Mar 2025 15:42:20 +0800 Subject: [PATCH 02/12] feat: add `variableNames` for UriTemplate --- package-lock.json | 4 ++-- src/shared/uriTemplate.test.ts | 17 +++++++++++++++++ src/shared/uriTemplate.ts | 4 ++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfd8a622..33d551aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.5.0", + "version": "1.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.5.0", + "version": "1.7.0", "license": "MIT", "dependencies": { "content-type": "^1.0.5", diff --git a/src/shared/uriTemplate.test.ts b/src/shared/uriTemplate.test.ts index 7941d447..8ec4fb73 100644 --- a/src/shared/uriTemplate.test.ts +++ b/src/shared/uriTemplate.test.ts @@ -24,11 +24,13 @@ describe("UriTemplate", () => { expect(template.expand({ username: "fred" })).toBe( "http://example.com/users/fred", ); + expect(template.variableNames).toEqual(['username']) }); it("should handle multiple variables", () => { const template = new UriTemplate("{x,y}"); expect(template.expand({ x: "1024", y: "768" })).toBe("1024,768"); + expect(template.variableNames).toEqual(['x', 'y']) }); it("should encode reserved characters", () => { @@ -43,6 +45,7 @@ describe("UriTemplate", () => { it("should not encode reserved characters with + operator", () => { const template = new UriTemplate("{+path}/here"); expect(template.expand({ path: "/foo/bar" })).toBe("/foo/bar/here"); + expect(template.variableNames).toEqual(['path']) }); }); @@ -50,6 +53,7 @@ describe("UriTemplate", () => { it("should add # prefix and not encode reserved chars", () => { const template = new UriTemplate("X{#var}"); expect(template.expand({ var: "/test" })).toBe("X#/test"); + expect(template.variableNames).toEqual(['var']) }); }); @@ -57,6 +61,7 @@ describe("UriTemplate", () => { it("should add . prefix", () => { const template = new UriTemplate("X{.var}"); expect(template.expand({ var: "test" })).toBe("X.test"); + expect(template.variableNames).toEqual(['var']) }); }); @@ -64,6 +69,7 @@ describe("UriTemplate", () => { it("should add / prefix", () => { const template = new UriTemplate("X{/var}"); expect(template.expand({ var: "test" })).toBe("X/test"); + expect(template.variableNames).toEqual(['var']) }); }); @@ -71,6 +77,7 @@ describe("UriTemplate", () => { it("should add ? prefix and name=value format", () => { const template = new UriTemplate("X{?var}"); expect(template.expand({ var: "test" })).toBe("X?var=test"); + expect(template.variableNames).toEqual(['var']) }); }); @@ -78,6 +85,7 @@ describe("UriTemplate", () => { it("should add & prefix and name=value format", () => { const template = new UriTemplate("X{&var}"); expect(template.expand({ var: "test" })).toBe("X&var=test"); + expect(template.variableNames).toEqual(['var']) }); }); @@ -133,6 +141,7 @@ describe("UriTemplate", () => { resource: "users", id: "123" })).toBe("/api/v1/users/123"); + expect(template.variableNames).toEqual(['version', 'resource', 'id']) }); it("should handle query parameters with arrays", () => { @@ -140,6 +149,7 @@ describe("UriTemplate", () => { expect(template.expand({ tags: ["nodejs", "typescript", "testing"] })).toBe("/search?tags=nodejs,typescript,testing"); + expect(template.variableNames).toEqual(['tags']) }); it("should handle multiple query parameters", () => { @@ -149,6 +159,7 @@ describe("UriTemplate", () => { page: "1", limit: "10" })).toBe("/search?q=test&page=1&limit=10"); + expect(template.variableNames).toEqual(['q', 'page', 'limit']) }); }); @@ -161,18 +172,21 @@ describe("UriTemplate", () => { resource: "users", id: "123" }); + expect(template.variableNames).toEqual(['version', 'resource', 'id']) }); it("should match query parameters", () => { const template = new UriTemplate("/search{?q}"); const match = template.match("/search?q=test"); expect(match).toEqual({ q: "test" }); + expect(template.variableNames).toEqual(['q']) }); it("should match multiple query parameters", () => { const template = new UriTemplate("/search{?q,page}"); const match = template.match("/search?q=test&page=1"); expect(match).toEqual({ q: "test", page: "1" }); + expect(template.variableNames).toEqual(['q', 'page']) }); it("should handle partial matches correctly", () => { @@ -229,17 +243,20 @@ describe("UriTemplate", () => { it("should handle repeated operators", () => { const template = new UriTemplate("{?a}{?b}{?c}"); expect(template.expand({ a: "1", b: "2", c: "3" })).toBe("?a=1&b=2&c=3"); + expect(template.variableNames).toEqual(['a', 'b', 'c']) }); it("should handle overlapping variable names", () => { const template = new UriTemplate("{var}{vara}"); expect(template.expand({ var: "1", vara: "2" })).toBe("12"); + expect(template.variableNames).toEqual(['var', 'vara']) }); it("should handle empty segments", () => { const template = new UriTemplate("///{a}////{b}////"); expect(template.expand({ a: "1", b: "2" })).toBe("///1////2////"); expect(template.match("///1////2////")).toEqual({ a: "1", b: "2" }); + expect(template.variableNames).toEqual(['a', 'b']) }); it("should handle maximum template expression limit", () => { diff --git a/src/shared/uriTemplate.ts b/src/shared/uriTemplate.ts index bb177325..982589ac 100644 --- a/src/shared/uriTemplate.ts +++ b/src/shared/uriTemplate.ts @@ -36,6 +36,10 @@ export class UriTemplate { | { name: string; operator: string; names: string[]; exploded: boolean } >; + get variableNames(): string[] { + return this.parts.flatMap((part) => typeof part === 'string' ? [] : part.names); + } + constructor(template: string) { UriTemplate.validateLength(template, MAX_TEMPLATE_LENGTH, "Template"); this.template = template; From 2729a74ab1e04415d339d83e6e08cebe687c3238 Mon Sep 17 00:00:00 2001 From: Dmitry Volokh Date: Thu, 13 Mar 2025 11:24:44 +0100 Subject: [PATCH 03/12] #89 NPM provenance added NPM Provenance added https://docs.npmjs.com/generating-provenance-statements --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70b04b6f..04ba17c9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,7 +46,6 @@ jobs: - run: npm ci - # TODO: Add --provenance once the repo is public - - run: npm publish --access public + - run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} From fee4bbec58e207778483a955733e96524ed498d7 Mon Sep 17 00:00:00 2001 From: Dmitry Volokh Date: Fri, 14 Mar 2025 19:54:03 +0100 Subject: [PATCH 04/12] console.error pollution removed in tests --- src/client/sse.test.ts | 2 ++ src/server/auth/middleware/bearerAuth.test.ts | 4 ++++ src/server/auth/router.test.ts | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/src/client/sse.test.ts b/src/client/sse.test.ts index 2b166530..b77938ca 100644 --- a/src/client/sse.test.ts +++ b/src/client/sse.test.ts @@ -56,6 +56,8 @@ describe("SSEClientTransport", () => { baseUrl = new URL(`http://127.0.0.1:${addr.port}`); done(); }); + + jest.spyOn(console, 'error').mockImplementation(() => {}); }); afterEach(async () => { diff --git a/src/server/auth/middleware/bearerAuth.test.ts b/src/server/auth/middleware/bearerAuth.test.ts index da2f5838..43cbfa0a 100644 --- a/src/server/auth/middleware/bearerAuth.test.ts +++ b/src/server/auth/middleware/bearerAuth.test.ts @@ -31,6 +31,10 @@ describe("requireBearerAuth middleware", () => { set: jest.fn().mockReturnThis(), }; nextFunction = jest.fn(); + jest.spyOn(console, 'error').mockImplementation(() => {}); + }) + + afterEach(() => { jest.clearAllMocks(); }); diff --git a/src/server/auth/router.test.ts b/src/server/auth/router.test.ts index 38817cc1..86eda221 100644 --- a/src/server/auth/router.test.ts +++ b/src/server/auth/router.test.ts @@ -259,6 +259,11 @@ describe('MCP Auth Router', () => { issuerUrl: new URL('https://auth.example.com') }; app.use(mcpAuthRouter(options)); + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); }); it('routes to authorization endpoint', async () => { From b2655cdd462a3ea4306604eb851f3d01f5c4920c Mon Sep 17 00:00:00 2001 From: Vlad Svoka Date: Fri, 21 Mar 2025 17:08:18 -0700 Subject: [PATCH 05/12] Fixing for windows terminal forcing \r\n --- src/shared/stdio.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/stdio.ts b/src/shared/stdio.ts index a30e90bc..52bde646 100644 --- a/src/shared/stdio.ts +++ b/src/shared/stdio.ts @@ -20,7 +20,7 @@ export class ReadBuffer { return null; } - const line = this._buffer.toString("utf8", 0, index); + const line = this._buffer.toString("utf8", 0, index).replace(/\r$/, ''); this._buffer = this._buffer.subarray(index + 1); return deserializeMessage(line); } From b6749998f5db56ef24d0c60d92c1524ba06588ba Mon Sep 17 00:00:00 2001 From: Ironben Date: Wed, 26 Mar 2025 11:54:48 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E2=9C=A8=20(client/index.ts):=20add=20op?= =?UTF-8?q?tional=20RequestOptions=20parameter=20to=20connect=20method=20t?= =?UTF-8?q?o=20allow=20additional=20configuration=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/index.ts b/src/client/index.ts index bcad952c..700800c5 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -124,7 +124,7 @@ export class Client< } } - override async connect(transport: Transport): Promise { + override async connect(transport: Transport, options?: RequestOptions): Promise { await super.connect(transport); try { @@ -138,6 +138,7 @@ export class Client< }, }, InitializeResultSchema, + options ); if (result === undefined) { From 6c15dcb65f8435b863b5b914b4e163af43632a6a Mon Sep 17 00:00:00 2001 From: ExlauChen <285874446@qq.com> Date: Fri, 28 Mar 2025 14:50:59 +0800 Subject: [PATCH 07/12] =?UTF-8?q?fix:=20=F0=9F=90=9B=20can't=20establish?= =?UTF-8?q?=20sse=20when=20server=20side=20enable=20compress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/sse.test.ts | 10 +++++----- src/server/sse.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/sse.test.ts b/src/client/sse.test.ts index 2b166530..d2bf5911 100644 --- a/src/client/sse.test.ts +++ b/src/client/sse.test.ts @@ -24,7 +24,7 @@ describe("SSEClientTransport", () => { // Send SSE headers res.writeHead(200, { "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", + "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }); @@ -183,7 +183,7 @@ describe("SSEClientTransport", () => { if (req.method === "GET") { res.writeHead(200, { "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", + "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }); res.write("event: endpoint\n"); @@ -397,7 +397,7 @@ describe("SSEClientTransport", () => { res.writeHead(200, { "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", + "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }); res.write("event: endpoint\n"); @@ -524,7 +524,7 @@ describe("SSEClientTransport", () => { if (auth === "Bearer new-token") { res.writeHead(200, { "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", + "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }); res.write("event: endpoint\n"); @@ -610,7 +610,7 @@ describe("SSEClientTransport", () => { res.writeHead(200, { "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", + "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }); res.write("event: endpoint\n"); diff --git a/src/server/sse.ts b/src/server/sse.ts index 84c1cbb9..e6cddc4e 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -44,7 +44,7 @@ export class SSEServerTransport implements Transport { this.res.writeHead(200, { "Content-Type": "text/event-stream", - "Cache-Control": "no-cache", + "Cache-Control": "no-cache, no-transform", Connection: "keep-alive", }); From 11c77cdeb7ca2ac3c22a4bf076c9b6aefe949d15 Mon Sep 17 00:00:00 2001 From: Shinya Fujino Date: Sun, 6 Apr 2025 01:52:14 +0900 Subject: [PATCH 08/12] docs: Update README to not specify invalid capabilities --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 08053225..129f9298 100644 --- a/README.md +++ b/README.md @@ -455,13 +455,6 @@ const client = new Client( { name: "example-client", version: "1.0.0" - }, - { - capabilities: { - prompts: {}, - resources: {}, - tools: {} - } } ); From 87dc5eb4a23c63a4302a078eac02e05fc0dbdb69 Mon Sep 17 00:00:00 2001 From: Shinya Fujino Date: Sun, 6 Apr 2025 11:42:18 +0900 Subject: [PATCH 09/12] fix: Ensure the correct capability is checked for `completion/complete` --- src/client/index.test.ts | 11 ++++++++++- src/client/index.ts | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/client/index.test.ts b/src/client/index.test.ts index 1209b60c..a1b43b2b 100644 --- a/src/client/index.test.ts +++ b/src/client/index.test.ts @@ -227,10 +227,19 @@ test("should respect server capabilities", async () => { await expect(client.listResources()).resolves.not.toThrow(); await expect(client.listTools()).resolves.not.toThrow(); - // This should throw because prompts are not supported + // These should throw because prompts, logging, and completions are not supported await expect(client.listPrompts()).rejects.toThrow( "Server does not support prompts", ); + await expect(client.setLoggingLevel("error")).rejects.toThrow( + "Server does not support logging", + ); + await expect( + client.complete({ + ref: { type: "ref/prompt", name: "test" }, + argument: { name: "test", value: "test" }, + }), + ).rejects.toThrow("Server does not support completions"); }); test("should respect client notification capabilities", async () => { diff --git a/src/client/index.ts b/src/client/index.ts index bcad952c..d6b2ed33 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -237,9 +237,9 @@ export class Client< break; case "completion/complete": - if (!this._serverCapabilities?.prompts) { + if (!this._serverCapabilities?.completions) { throw new Error( - `Server does not support prompts (required for ${method})`, + `Server does not support completions (required for ${method})`, ); } break; From 1c1178143c19c60370540cedb0d1280b086804be Mon Sep 17 00:00:00 2001 From: Shinya Fujino Date: Sun, 6 Apr 2025 22:46:02 +0900 Subject: [PATCH 10/12] feat: add completions capability in ServerCapabilitiesSchema --- src/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/types.ts b/src/types.ts index b9c0ab37..da4afd2a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -247,6 +247,10 @@ export const ServerCapabilitiesSchema = z * Present if the server supports sending log messages to the client. */ logging: z.optional(z.object({}).passthrough()), + /** + * Present if the server supports sending completions to the client. + */ + completions: z.optional(z.object({}).passthrough()), /** * Present if the server offers any prompt templates. */ From 8027ae15938fde09e67d342c29cceed3ead1df52 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Mon, 7 Apr 2025 10:47:37 +0100 Subject: [PATCH 11/12] Revert "feat: support extending McpServer with authorization" --- .prettierrc | 8 - jest.config.js | 1 - package.json | 1 - src/inMemory.ts | 1 - src/server/mcp.test.ts | 404 +++------------------------------------- src/server/mcp.ts | 29 +-- src/server/sse.ts | 1 - src/server/stdio.ts | 3 +- src/shared/protocol.ts | 8 +- src/shared/transport.ts | 5 - 10 files changed, 42 insertions(+), 419 deletions(-) delete mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 4379c748..00000000 --- a/.prettierrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 80, - "tabWidth": 2, - "trailingComma": "all", - "jsxBracketSameLine": true, - "semi": true, - "singleQuote": false -} diff --git a/jest.config.js b/jest.config.js index a0021104..f8f621c8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -12,6 +12,5 @@ export default { transformIgnorePatterns: [ "/node_modules/(?!eventsource)/" ], - collectCoverageFrom: ["src/**/*.ts"], testPathIgnorePatterns: ["/node_modules/", "/dist/"], }; diff --git a/package.json b/package.json index dfaa10cd..86fd9d6d 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "prepack": "npm run build:esm && npm run build:cjs", "lint": "eslint src/", "test": "jest", - "coverage": "jest --coverage", "start": "npm run server", "server": "tsx watch --clear-screen=false src/cli.ts server", "client": "tsx src/cli.ts client" diff --git a/src/inMemory.ts b/src/inMemory.ts index 65915baa..106a9e7e 100644 --- a/src/inMemory.ts +++ b/src/inMemory.ts @@ -12,7 +12,6 @@ export class InMemoryTransport implements Transport { onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage) => void; sessionId?: string; - user?: unknown; /** * Creates a pair of linked in-memory transports that can communicate with each other. One should be passed to a Client and one to a Server. diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index 08518b20..2e91a568 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1,8 +1,7 @@ -import { McpServer, ToolCallback } from "./mcp.js"; +import { McpServer } from "./mcp.js"; import { Client } from "../client/index.js"; import { InMemoryTransport } from "../inMemory.js"; -import { z, ZodRawShape } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; +import { z } from "zod"; import { ListToolsResultSchema, CallToolResultSchema, @@ -12,16 +11,10 @@ import { ListPromptsResultSchema, GetPromptResultSchema, CompleteResultSchema, - CallToolRequestSchema, - CallToolRequest, - ListToolsRequestSchema, - ListToolsResult, - Tool, } from "../types.js"; import { ResourceTemplate } from "./mcp.js"; import { completable } from "./completable.js"; import { UriTemplate } from "../shared/uriTemplate.js"; -import { RequestHandlerExtra } from "../shared/protocol.js"; describe("McpServer", () => { test("should expose underlying Server instance", () => { @@ -325,7 +318,7 @@ describe("tool()", () => { // This should succeed mcpServer.tool("tool1", () => ({ content: [] })); - + // This should also succeed and not throw about request handlers mcpServer.tool("tool2", () => ({ content: [] })); }); @@ -361,8 +354,7 @@ describe("tool()", () => { }; }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); // Set a test sessionId on the server transport serverTransport.sessionId = "test-session-123"; @@ -823,7 +815,7 @@ describe("resource()", () => { }, ], })); - + // This should also succeed and not throw about request handlers mcpServer.resource("resource2", "test://resource2", async () => ({ contents: [ @@ -1329,7 +1321,7 @@ describe("prompt()", () => { }, ], })); - + // This should also succeed and not throw about request handlers mcpServer.prompt("prompt2", async () => ({ messages: [ @@ -1351,17 +1343,19 @@ describe("prompt()", () => { }); // This should succeed - mcpServer.prompt("echo", { message: z.string() }, ({ message }) => ({ - messages: [ - { + mcpServer.prompt( + "echo", + { message: z.string() }, + ({ message }) => ({ + messages: [{ role: "user", content: { type: "text", - text: `Please process this message: ${message}`, - }, - }, - ], - })); + text: `Please process this message: ${message}` + } + }] + }) + ); }); test("should allow registering both resources and prompts with completion handlers", () => { @@ -1394,16 +1388,14 @@ describe("prompt()", () => { "echo", { message: completable(z.string(), () => ["hello", "world"]) }, ({ message }) => ({ - messages: [ - { - role: "user", - content: { - type: "text", - text: `Please process this message: ${message}`, - }, - }, - ], - }), + messages: [{ + role: "user", + content: { + type: "text", + text: `Please process this message: ${message}` + } + }] + }) ); }); @@ -1590,351 +1582,3 @@ describe("prompt()", () => { expect(result.completion.total).toBe(1); }); }); - -describe("McpServer with Auth Extension", () => { - type SessionUser = { - role: string; - [key: string]: unknown; - }; - - type AccessPolicy = { - allow?: { - roles?: string[]; - }; - deny?: { - roles?: string[]; - }; - }; - - type RegisteredToolWithAuth = { - description?: string; - inputSchema?: z.ZodObject; - callback: ToolCallback; - accessPolicy?: AccessPolicy; - }; - - // Just a simple extension to McpServer that adds support for access policies on tools - class McpServerWithAuth extends McpServer { - protected override _registeredTools: { - [name: string]: RegisteredToolWithAuth; - } = {}; - checkPermissions(user?: SessionUser, policy?: AccessPolicy): boolean { - if (!policy) { - return true; - } - - if (!user) { - return false; - } - - // Check deny rules first - if (policy.deny) { - // Check denied roles - if (policy.deny.roles?.includes(user.role)) { - return false; - } - } - - // Check allow rules - if (policy.allow) { - let isAllowed = false; - - // If no allow rules are specified, default to allowed - if (!policy.allow.roles) { - isAllowed = true; - } else { - // Check allowed roles - if (policy.allow.roles?.includes(user.role)) { - isAllowed = true; - } - } - - return isAllowed; - } - - // If no rules specified, default to allowed - return true; - } - - override tool( - name: string, - cb: ToolCallback, - accessPolicy?: AccessPolicy, - ): void; - override tool( - name: string, - description: string, - cb: ToolCallback, - accessPolicy?: AccessPolicy, - ): void; - override tool( - name: string, - paramsSchema: Args, - cb: ToolCallback, - accessPolicy?: AccessPolicy, - ): void; - override tool( - name: string, - description: string, - paramsSchema: Args, - cb: ToolCallback, - accessPolicy?: AccessPolicy, - ): void; - override tool(name: string, ...rest: unknown[]): void { - let description: string | undefined; - let paramsSchema: ZodRawShape | undefined; - let accessPolicy: AccessPolicy | undefined; - let cb: ToolCallback; - - // Parse arguments based on their types - if (typeof rest[0] === "function") { - // Case: tool(name, cb, accessPolicy?) - cb = rest[0] as ToolCallback; - accessPolicy = rest[1] as AccessPolicy | undefined; - } else if (typeof rest[0] === "string") { - // Cases with description - description = rest[0]; - if (typeof rest[1] === "function") { - // Case: tool(name, description, cb, accessPolicy?) - cb = rest[1] as ToolCallback; - accessPolicy = rest[2] as AccessPolicy | undefined; - } else { - // Case: tool(name, description, paramsSchema, cb, accessPolicy?) - paramsSchema = rest[1] as ZodRawShape; - cb = rest[2] as ToolCallback; - accessPolicy = rest[3] as AccessPolicy | undefined; - } - } else { - // Case: tool(name, paramsSchema, cb, accessPolicy?) - paramsSchema = rest[0] as ZodRawShape; - cb = rest[1] as ToolCallback; - accessPolicy = rest[2] as AccessPolicy | undefined; - } - - // Register with base class - const args: unknown[] = [name]; - if (description) args.push(description); - if (paramsSchema) args.push(paramsSchema); - args.push(cb); - - // Set up request handlers if not already initialized - if (!this._toolHandlersInitialized) { - this.server.assertCanSetRequestHandler( - CallToolRequestSchema.shape.method.value, - ); - this.server.assertCanSetRequestHandler( - ListToolsRequestSchema.shape.method.value, - ); - this.server.registerCapabilities({ tools: {} }); - - // Add ListToolsRequestSchema handler - this.server.setRequestHandler( - ListToolsRequestSchema, - (request, extra): ListToolsResult => { - const user = extra.user as SessionUser | undefined; - - // Filter tools based on permissions - const accessibleTools = Object.entries(this._registeredTools) - .filter(([_, tool]) => - this.checkPermissions(user, tool.accessPolicy), - ) - .map( - ([name, tool]): Tool => ({ - name, - description: tool.description, - inputSchema: tool.inputSchema - ? (zodToJsonSchema(tool.inputSchema, { - strictUnions: true, - }) as Tool["inputSchema"]) - : { type: "object" }, - }), - ); - - return { tools: accessibleTools }; - }, - ); - - this.server.setRequestHandler( - CallToolRequestSchema, - async (request: CallToolRequest, extra: RequestHandlerExtra) => { - const tool = this._registeredTools[request.params.name]; - if (!tool) { - throw new Error(`Tool ${request.params.name} not found`); - } - - if ( - !this.checkPermissions( - extra.user as SessionUser, - tool.accessPolicy, - ) - ) { - throw new Error(`Access denied for tool: ${request.params.name}`); - } - - if (tool.inputSchema) { - const parseResult = await tool.inputSchema.safeParseAsync( - request.params.arguments, - ); - if (!parseResult.success) { - throw new Error( - `Invalid arguments for tool ${request.params.name}: ${parseResult.error.message}`, - ); - } - - const args = parseResult.data; - const cb = tool.callback as ToolCallback; - return await Promise.resolve(cb(args, extra)); - } else { - const cb = tool.callback as ToolCallback; - return await Promise.resolve(cb(extra)); - } - }, - ); - this._toolHandlersInitialized = true; - } - - McpServer.prototype.tool.apply( - this, - args as Parameters, - ); - this._registeredTools[name].accessPolicy = accessPolicy; - } - } - - const mcpServer = new McpServerWithAuth({ - name: "test server with auth", - version: "1.0", - }); - const client = new Client({ - name: "test client", - version: "1.0", - }); - - mcpServer.tool("public-tool", async () => ({ - content: [ - { - type: "text", - text: "Public tool response", - }, - ], - })); - - mcpServer.tool( - "protected-tool", - async () => ({ - content: [ - { - type: "text", - text: "Protected tool response", - }, - ], - }), - { - allow: { - roles: ["admin"], - }, - }, - ); - - test("should public tools work with list and call when unauthenticated", async () => { - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - await Promise.all([ - client.connect(clientTransport), - mcpServer.server.connect(serverTransport), - ]); - - // Public tool should be accessible - const result = await client.request( - { method: "tools/list" }, - ListToolsResultSchema, - ); - expect(result.tools).toHaveLength(1); - expect(result.tools[0].name).toBe("public-tool"); - const response = await client.request( - { - method: "tools/call", - params: { - name: "public-tool", - arguments: {}, - }, - }, - CallToolResultSchema, - ); - expect(response.content).toEqual([ - { - type: "text", - text: "Public tool response", - }, - ]); - }); - - test("should public tools work with list and call when unauthorized", async () => { - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - await Promise.all([ - client.connect(clientTransport), - mcpServer.server.connect(serverTransport), - ]); - - // Protected tool should be inaccessible when authenticated as a non-admin user - serverTransport.user = { role: "member" }; - const result = await client.request( - { method: "tools/list" }, - ListToolsResultSchema, - ); - expect(result.tools).toHaveLength(1); - expect(result.tools[0].name).toBe("public-tool"); - const response = await client.request( - { - method: "tools/call", - params: { - name: "public-tool", - arguments: {}, - }, - }, - CallToolResultSchema, - ); - expect(response.content).toEqual([ - { - type: "text", - text: "Public tool response", - }, - ]); - }); - - test("should protected tools work with list and call when authorized", async () => { - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - await Promise.all([ - client.connect(clientTransport), - mcpServer.server.connect(serverTransport), - ]); - - serverTransport.user = { role: "admin" }; - const result = await client.request( - { method: "tools/list" }, - ListToolsResultSchema, - ); - expect(result.tools).toHaveLength(2); - expect(result.tools[0].name).toBe("public-tool"); - expect(result.tools[1].name).toBe("protected-tool"); - - const response = await client.request( - { - method: "tools/call", - params: { - name: "protected-tool", - arguments: {}, - }, - }, - CallToolResultSchema, - ); - expect(response.content).toEqual([ - { - type: "text", - text: "Protected tool response", - }, - ]); - }); -}); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index ed93256f..8f4a909c 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -54,17 +54,12 @@ export class McpServer { */ public readonly server: Server; - protected _registeredResources: { [uri: string]: RegisteredResource } = {}; - protected _registeredResourceTemplates: { + private _registeredResources: { [uri: string]: RegisteredResource } = {}; + private _registeredResourceTemplates: { [name: string]: RegisteredResourceTemplate; } = {}; - protected _registeredTools: { [name: string]: RegisteredTool } = {}; - protected _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; - - protected _toolHandlersInitialized = false; - protected _completionHandlerInitialized = false; - protected _resourceHandlersInitialized = false; - protected _promptHandlersInitialized = false; + private _registeredTools: { [name: string]: RegisteredTool } = {}; + private _registeredPrompts: { [name: string]: RegisteredPrompt } = {}; constructor(serverInfo: Implementation, options?: ServerOptions) { this.server = new Server(serverInfo, options); @@ -86,11 +81,13 @@ export class McpServer { await this.server.close(); } + private _toolHandlersInitialized = false; + private setToolRequestHandlers() { if (this._toolHandlersInitialized) { return; } - + this.server.assertCanSetRequestHandler( ListToolsRequestSchema.shape.method.value, ); @@ -180,6 +177,8 @@ export class McpServer { this._toolHandlersInitialized = true; } + private _completionHandlerInitialized = false; + private setCompletionRequestHandler() { if (this._completionHandlerInitialized) { return; @@ -268,6 +267,8 @@ export class McpServer { return createCompletionResult(suggestions); } + private _resourceHandlersInitialized = false; + private setResourceRequestHandlers() { if (this._resourceHandlersInitialized) { return; @@ -365,10 +366,12 @@ export class McpServer { ); this.setCompletionRequestHandler(); - + this._resourceHandlersInitialized = true; } + private _promptHandlersInitialized = false; + private setPromptRequestHandlers() { if (this._promptHandlersInitialized) { return; @@ -435,7 +438,7 @@ export class McpServer { ); this.setCompletionRequestHandler(); - + this._promptHandlersInitialized = true; } @@ -767,7 +770,7 @@ type RegisteredPrompt = { callback: PromptCallback; }; -export function promptArgumentsFromSchema( +function promptArgumentsFromSchema( schema: ZodObject, ): PromptArgument[] { return Object.entries(schema.shape).map( diff --git a/src/server/sse.ts b/src/server/sse.ts index 73603021..84c1cbb9 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -19,7 +19,6 @@ export class SSEServerTransport implements Transport { onclose?: () => void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage) => void; - user?: unknown; /** * Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL identified by `_endpoint`. diff --git a/src/server/stdio.ts b/src/server/stdio.ts index 7c3b21c6..30c80012 100644 --- a/src/server/stdio.ts +++ b/src/server/stdio.ts @@ -21,7 +21,6 @@ export class StdioServerTransport implements Transport { onclose?: () => void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage) => void; - user?: unknown; // Arrow functions to bind `this` properly, while maintaining function identity. _ondata = (chunk: Buffer) => { @@ -74,7 +73,7 @@ export class StdioServerTransport implements Transport { // This prevents interfering with other parts of the application that might be using stdin this._stdin.pause(); } - + // Clear the buffer and notify closure this._readBuffer.clear(); this.onclose?.(); diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 5540d4fe..a6e47184 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -93,11 +93,6 @@ export type RequestHandlerExtra = { * The session ID from the transport, if available. */ sessionId?: string; - - /** - * The authenticated user, if available. - */ - user?: unknown; }; /** @@ -324,7 +319,6 @@ export abstract class Protocol< const extra: RequestHandlerExtra = { signal: abortController.signal, sessionId: this._transport?.sessionId, - user: this._transport?.user, }; // Starting with Promise.resolve() puts any synchronous errors into the monad as well. @@ -370,7 +364,7 @@ export abstract class Protocol< private _onprogress(notification: ProgressNotification): void { const { progressToken, ...params } = notification.params; const messageId = Number(progressToken); - + const handler = this._progressHandlers.get(messageId); if (!handler) { this._onerror(new Error(`Received a progress notification for an unknown token: ${JSON.stringify(notification)}`)); diff --git a/src/shared/transport.ts b/src/shared/transport.ts index 88cea7f2..b80e2a51 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -46,9 +46,4 @@ export interface Transport { * The session ID generated for this connection. */ sessionId?: string; - - /** - * The authenticated user for this transport session. - */ - user?: unknown; } From 0d0af5402d04461b3464f415055988d17f29f7f6 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Mon, 7 Apr 2025 11:36:52 +0100 Subject: [PATCH 12/12] Bump version, `npm audit fix` --- package-lock.json | 51 ++++++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index dd8976e1..1165b751 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.8.0", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.8.0", + "version": "1.9.0", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -60,10 +60,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", - "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", @@ -211,25 +212,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", - "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -461,14 +464,15 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" @@ -502,10 +506,11 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" diff --git a/package.json b/package.json index 86fd9d6d..1b9fd44d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.8.0", + "version": "1.9.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)",