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 }} diff --git a/README.md b/README.md index 08053225..a6ec645c 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: {} - } } ); @@ -471,15 +464,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({ diff --git a/package-lock.json b/package-lock.json index 73f1cbba..1165b751 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.7.0", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.7.0", + "version": "1.9.0", "license": "MIT", "dependencies": { "content-type": "^1.0.5", @@ -15,7 +15,7 @@ "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", - "pkce-challenge": "^4.1.0", + "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" @@ -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" @@ -5416,9 +5421,9 @@ } }, "node_modules/pkce-challenge": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", - "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", "license": "MIT", "engines": { "node": ">=16.20.0" diff --git a/package.json b/package.json index e2d8b3d7..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)", @@ -52,7 +52,7 @@ "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", - "pkce-challenge": "^4.1.0", + "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" 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..64ea62c4 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) { @@ -237,9 +238,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; diff --git a/src/client/sse.test.ts b/src/client/sse.test.ts index 2b166530..77b28508 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", }); @@ -56,6 +56,8 @@ describe("SSEClientTransport", () => { baseUrl = new URL(`http://127.0.0.1:${addr.port}`); done(); }); + + jest.spyOn(console, 'error').mockImplementation(() => {}); }); afterEach(async () => { @@ -183,7 +185,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 +399,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 +526,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 +612,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/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 () => { diff --git a/src/server/sse.ts b/src/server/sse.ts index e990fafd..848043ba 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -45,7 +45,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", }); 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); } 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; 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. */ diff --git a/tsconfig.json b/tsconfig.json index dc96ff04..cedbaaae 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,11 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, - "skipLibCheck": true + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "pkce-challenge": ["node_modules/pkce-challenge/dist/index.node"] + } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]