From 1b8e94ca4091c2db442961c033ca512469aa9f6b Mon Sep 17 00:00:00 2001 From: Jerome Date: Sat, 22 Feb 2025 16:20:41 +0000 Subject: [PATCH 01/23] During metadata fetching, try with the MCP header, then without, then return undefined if both fail --- src/client/auth.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/client/auth.ts b/src/client/auth.ts index c0bac9e8..5f5c860e 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -163,11 +163,20 @@ export async function discoverOAuthMetadata( opts?: { protocolVersion?: string }, ): Promise { const url = new URL("/.well-known/oauth-authorization-server", serverUrl); - const response = await fetch(url, { - headers: { - "MCP-Protocol-Version": opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION + let response: Response; + try { + response = await fetch(url, { + headers: { + "MCP-Protocol-Version": opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION + } + }); + } catch { + try { + response = await fetch(url); + } catch { + return undefined; } - }); + } if (response.status === 404) { return undefined; From 2dc7fa5df8a041ec374dff197d37ed5253eef730 Mon Sep 17 00:00:00 2001 From: Jerome Date: Sun, 23 Feb 2025 10:57:26 +0000 Subject: [PATCH 02/23] Check token expiry in bearer auth --- src/server/auth/middleware/bearerAuth.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/server/auth/middleware/bearerAuth.ts b/src/server/auth/middleware/bearerAuth.ts index 14109e17..cd1b314a 100644 --- a/src/server/auth/middleware/bearerAuth.ts +++ b/src/server/auth/middleware/bearerAuth.ts @@ -55,6 +55,11 @@ export function requireBearerAuth({ provider, requiredScopes = [] }: BearerAuthM } } + // Check if the token is expired + if (!!authInfo.expiresAt && authInfo.expiresAt < Date.now() / 1000) { + throw new InvalidTokenError("Token has expired"); + } + req.auth = authInfo; next(); } catch (error) { From 8a222e8b385ec027ac013ab927bb71ad3456004e Mon Sep 17 00:00:00 2001 From: Jerome Date: Sun, 23 Feb 2025 10:59:05 +0000 Subject: [PATCH 03/23] Don't set client secret expiry if public client --- src/server/auth/handlers/register.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/server/auth/handlers/register.ts b/src/server/auth/handlers/register.ts index 675e8733..7eca5b17 100644 --- a/src/server/auth/handlers/register.ts +++ b/src/server/auth/handlers/register.ts @@ -75,10 +75,11 @@ export function clientRegistrationHandler({ } const clientMetadata = parseResult.data; + const isPublicClient = clientMetadata.token_endpoint_auth_method !== 'none' // Generate client credentials const clientId = crypto.randomUUID(); - const clientSecret = clientMetadata.token_endpoint_auth_method !== 'none' + const clientSecret = isPublicClient ? crypto.randomBytes(32).toString('hex') : undefined; const clientIdIssuedAt = Math.floor(Date.now() / 1000); @@ -88,7 +89,11 @@ export function clientRegistrationHandler({ client_id: clientId, client_secret: clientSecret, client_id_issued_at: clientIdIssuedAt, - client_secret_expires_at: clientSecretExpirySeconds > 0 ? clientIdIssuedAt + clientSecretExpirySeconds : 0 + client_secret_expires_at: isPublicClient + ? clientSecretExpirySeconds > 0 + ? clientIdIssuedAt + clientSecretExpirySeconds + : 0 + : undefined, }; clientInfo = await clientsStore.registerClient!(clientInfo); From 3b12961e2e0082b6cb240c17ae95d11114e1e973 Mon Sep 17 00:00:00 2001 From: Jerome Date: Sun, 23 Feb 2025 14:55:30 +0000 Subject: [PATCH 04/23] Added tests --- src/client/auth.test.ts | 30 +++++++++++ src/server/auth/handlers/register.test.ts | 31 +++++++++++ src/server/auth/middleware/bearerAuth.test.ts | 51 +++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 97c9e14a..1a6ef5af 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -43,6 +43,36 @@ describe("OAuth Authorization", () => { }); }); + it("returns metadata when first fetch fails but second without MCP header succeeds", async () => { + // First request with MCP header fails + mockFetch.mockRejectedValueOnce(new Error("Network error")); + + // Second request without header succeeds + mockFetch.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => validMetadata, + }); + + const metadata = await discoverOAuthMetadata("https://auth.example.com"); + expect(metadata).toEqual(validMetadata); + + // Verify second call was made without header + expect(mockFetch).toHaveBeenCalledTimes(2); + const secondCallOptions = mockFetch.mock.calls[1][1]; + expect(secondCallOptions).toBeUndefined(); // No options means no headers + }); + + it("returns undefined when all fetch attempts fail", async () => { + // Both requests fail + mockFetch.mockRejectedValueOnce(new Error("Network error")); + mockFetch.mockRejectedValueOnce(new Error("Network error")); + + const metadata = await discoverOAuthMetadata("https://auth.example.com"); + expect(metadata).toBeUndefined(); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + it("returns undefined when discovery endpoint returns 404", async () => { mockFetch.mockResolvedValueOnce({ ok: false, diff --git a/src/server/auth/handlers/register.test.ts b/src/server/auth/handlers/register.test.ts index 5faf1a4a..a961f654 100644 --- a/src/server/auth/handlers/register.test.ts +++ b/src/server/auth/handlers/register.test.ts @@ -141,6 +141,37 @@ describe('Client Registration Handler', () => { expect(response.status).toBe(201); expect(response.body.client_secret).toBeUndefined(); + expect(response.body.client_secret_expires_at).toBeUndefined(); + }); + + it('sets client_secret_expires_at for public clients only', async () => { + // Test for public client (token_endpoint_auth_method not 'none') + const publicClientMetadata: OAuthClientMetadata = { + redirect_uris: ['https://example.com/callback'], + token_endpoint_auth_method: 'client_secret_basic' + }; + + const publicResponse = await supertest(app) + .post('/register') + .send(publicClientMetadata); + + expect(publicResponse.status).toBe(201); + expect(publicResponse.body.client_secret).toBeDefined(); + expect(publicResponse.body.client_secret_expires_at).toBeDefined(); + + // Test for non-public client (token_endpoint_auth_method is 'none') + const nonPublicClientMetadata: OAuthClientMetadata = { + redirect_uris: ['https://example.com/callback'], + token_endpoint_auth_method: 'none' + }; + + const nonPublicResponse = await supertest(app) + .post('/register') + .send(nonPublicClientMetadata); + + expect(nonPublicResponse.status).toBe(201); + expect(nonPublicResponse.body.client_secret).toBeUndefined(); + expect(nonPublicResponse.body.client_secret_expires_at).toBeUndefined(); }); it('sets expiry based on clientSecretExpirySeconds', async () => { diff --git a/src/server/auth/middleware/bearerAuth.test.ts b/src/server/auth/middleware/bearerAuth.test.ts index 8c0b595e..da2f5838 100644 --- a/src/server/auth/middleware/bearerAuth.test.ts +++ b/src/server/auth/middleware/bearerAuth.test.ts @@ -55,6 +55,57 @@ describe("requireBearerAuth middleware", () => { expect(mockResponse.status).not.toHaveBeenCalled(); expect(mockResponse.json).not.toHaveBeenCalled(); }); + + it("should reject expired tokens", async () => { + const expiredAuthInfo: AuthInfo = { + token: "expired-token", + clientId: "client-123", + scopes: ["read", "write"], + expiresAt: Math.floor(Date.now() / 1000) - 100, // Token expired 100 seconds ago + }; + mockVerifyAccessToken.mockResolvedValue(expiredAuthInfo); + + mockRequest.headers = { + authorization: "Bearer expired-token", + }; + + const middleware = requireBearerAuth({ provider: mockProvider }); + await middleware(mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockVerifyAccessToken).toHaveBeenCalledWith("expired-token"); + expect(mockResponse.status).toHaveBeenCalledWith(401); + expect(mockResponse.set).toHaveBeenCalledWith( + "WWW-Authenticate", + expect.stringContaining('Bearer error="invalid_token"') + ); + expect(mockResponse.json).toHaveBeenCalledWith( + expect.objectContaining({ error: "invalid_token", error_description: "Token has expired" }) + ); + expect(nextFunction).not.toHaveBeenCalled(); + }); + + it("should accept non-expired tokens", async () => { + const nonExpiredAuthInfo: AuthInfo = { + token: "valid-token", + clientId: "client-123", + scopes: ["read", "write"], + expiresAt: Math.floor(Date.now() / 1000) + 3600, // Token expires in an hour + }; + mockVerifyAccessToken.mockResolvedValue(nonExpiredAuthInfo); + + mockRequest.headers = { + authorization: "Bearer valid-token", + }; + + const middleware = requireBearerAuth({ provider: mockProvider }); + await middleware(mockRequest as Request, mockResponse as Response, nextFunction); + + expect(mockVerifyAccessToken).toHaveBeenCalledWith("valid-token"); + expect(mockRequest.auth).toEqual(nonExpiredAuthInfo); + expect(nextFunction).toHaveBeenCalled(); + expect(mockResponse.status).not.toHaveBeenCalled(); + expect(mockResponse.json).not.toHaveBeenCalled(); + }); it("should require specific scopes when configured", async () => { const authInfo: AuthInfo = { From 44c9bffa8511372b760dc5edb78650563053b18f Mon Sep 17 00:00:00 2001 From: Jason Victor Date: Sun, 23 Feb 2025 12:39:15 -0500 Subject: [PATCH 05/23] added session id to request handler extra data --- src/server/sse.ts | 2 -- src/shared/protocol.ts | 13 ++++++++++++- src/shared/transport.ts | 2 ++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/server/sse.ts b/src/server/sse.ts index 84c1cbb9..1bc304a8 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -136,8 +136,6 @@ export class SSEServerTransport implements Transport { /** * Returns the session ID for this transport. - * - * This can be used to route incoming POST requests. */ get sessionId(): string { return this._sessionId; diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index a4f211c6..ef303a39 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -74,6 +74,11 @@ export type RequestHandlerExtra = { * An abort signal used to communicate if the request was cancelled from the sender's side. */ signal: AbortSignal; + + /** + * The session ID from the transport, if available. + */ + sessionId?: string; }; /** @@ -239,9 +244,15 @@ export abstract class Protocol< const abortController = new AbortController(); this._requestHandlerAbortControllers.set(request.id, abortController); + // Create extra object with both abort signal and sessionId from transport + const extra: RequestHandlerExtra = { + signal: abortController.signal, + sessionId: this._transport?.sessionId, + }; + // Starting with Promise.resolve() puts any synchronous errors into the monad as well. Promise.resolve() - .then(() => handler(request, { signal: abortController.signal })) + .then(() => handler(request, extra)) .then( (result) => { if (abortController.signal.aborted) { diff --git a/src/shared/transport.ts b/src/shared/transport.ts index 5843cf00..bcadcf4d 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -41,4 +41,6 @@ export interface Transport { * Callback for when a message (request or response) is received over the connection. */ onmessage?: (message: JSONRPCMessage) => void; + + sessionId?: string; } From aad2badb2fd0ff293ba319eece74d77992001c7c Mon Sep 17 00:00:00 2001 From: Jerome Date: Mon, 24 Feb 2025 10:26:59 +0000 Subject: [PATCH 06/23] Removed nested ternaries --- src/server/auth/handlers/register.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/server/auth/handlers/register.ts b/src/server/auth/handlers/register.ts index 7eca5b17..2a74b4a4 100644 --- a/src/server/auth/handlers/register.ts +++ b/src/server/auth/handlers/register.ts @@ -84,16 +84,17 @@ export function clientRegistrationHandler({ : undefined; const clientIdIssuedAt = Math.floor(Date.now() / 1000); + // Calculate client secret expiry time + const clientsDoExpire = clientSecretExpirySeconds > 0 + const secretExpiryTime = clientsDoExpire ? clientIdIssuedAt + clientSecretExpirySeconds : 0 + const clientSecretExpiresAt = isPublicClient ? undefined : secretExpiryTime + let clientInfo: OAuthClientInformationFull = { ...clientMetadata, client_id: clientId, client_secret: clientSecret, client_id_issued_at: clientIdIssuedAt, - client_secret_expires_at: isPublicClient - ? clientSecretExpirySeconds > 0 - ? clientIdIssuedAt + clientSecretExpirySeconds - : 0 - : undefined, + client_secret_expires_at: clientSecretExpiresAt, }; clientInfo = await clientsStore.registerClient!(clientInfo); From 26b47438c60a673bf500728e7236d0dcd5173438 Mon Sep 17 00:00:00 2001 From: Jerome Date: Mon, 24 Feb 2025 10:39:38 +0000 Subject: [PATCH 07/23] Tidy up CORS error catching --- src/client/auth.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/client/auth.ts b/src/client/auth.ts index 5f5c860e..c7799429 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -170,11 +170,12 @@ export async function discoverOAuthMetadata( "MCP-Protocol-Version": opts?.protocolVersion ?? LATEST_PROTOCOL_VERSION } }); - } catch { - try { + } catch (error) { + // CORS errors come back as TypeError + if (error instanceof TypeError) { response = await fetch(url); - } catch { - return undefined; + } else { + throw error; } } From 885590c09973c736073b0d6af7df01f14c626c07 Mon Sep 17 00:00:00 2001 From: Jerome Date: Mon, 24 Feb 2025 13:39:58 +0000 Subject: [PATCH 08/23] Fixing tests --- src/client/auth.test.ts | 10 ++++++---- src/server/auth/handlers/register.ts | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 1a6ef5af..daa98de5 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -60,16 +60,18 @@ describe("OAuth Authorization", () => { // Verify second call was made without header expect(mockFetch).toHaveBeenCalledTimes(2); const secondCallOptions = mockFetch.mock.calls[1][1]; - expect(secondCallOptions).toBeUndefined(); // No options means no headers + // The second call still has options but doesn't include MCP-Protocol-Version header + expect(secondCallOptions).toBeDefined(); + expect(secondCallOptions?.headers).toBeUndefined(); }); - it("returns undefined when all fetch attempts fail", async () => { + it("throws an error when all fetch attempts fail", async () => { // Both requests fail mockFetch.mockRejectedValueOnce(new Error("Network error")); mockFetch.mockRejectedValueOnce(new Error("Network error")); - const metadata = await discoverOAuthMetadata("https://auth.example.com"); - expect(metadata).toBeUndefined(); + await expect(discoverOAuthMetadata("https://auth.example.com")) + .rejects.toThrow("Network error"); expect(mockFetch).toHaveBeenCalledTimes(2); }); diff --git a/src/server/auth/handlers/register.ts b/src/server/auth/handlers/register.ts index 2a74b4a4..30b7cdf8 100644 --- a/src/server/auth/handlers/register.ts +++ b/src/server/auth/handlers/register.ts @@ -75,13 +75,13 @@ export function clientRegistrationHandler({ } const clientMetadata = parseResult.data; - const isPublicClient = clientMetadata.token_endpoint_auth_method !== 'none' + const isPublicClient = clientMetadata.token_endpoint_auth_method === 'none' // Generate client credentials const clientId = crypto.randomUUID(); const clientSecret = isPublicClient - ? crypto.randomBytes(32).toString('hex') - : undefined; + ? undefined + : crypto.randomBytes(32).toString('hex'); const clientIdIssuedAt = Math.floor(Date.now() / 1000); // Calculate client secret expiry time From 8dfa64338e072178b6cab2f56a9cb11bec4608c5 Mon Sep 17 00:00:00 2001 From: Jerome Date: Mon, 24 Feb 2025 13:48:24 +0000 Subject: [PATCH 09/23] Fixing up tests --- src/client/auth.test.ts | 58 +++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index daa98de5..1e892308 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -44,34 +44,60 @@ describe("OAuth Authorization", () => { }); it("returns metadata when first fetch fails but second without MCP header succeeds", async () => { - // First request with MCP header fails - mockFetch.mockRejectedValueOnce(new Error("Network error")); + // Set up a counter to control behavior + let callCount = 0; - // Second request without header succeeds - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => validMetadata, + // Mock implementation that changes behavior based on call count + mockFetch.mockImplementation((url, options) => { + callCount++; + + if (callCount === 1) { + // First call with MCP header - fail with TypeError (simulating CORS error) + // We need to use TypeError specifically because that's what the implementation checks for + return Promise.reject(new TypeError("Network error")); + } else { + // Second call without header - succeed + return Promise.resolve({ + ok: true, + status: 200, + json: async () => validMetadata + }); + } }); + // Should succeed with the second call const metadata = await discoverOAuthMetadata("https://auth.example.com"); expect(metadata).toEqual(validMetadata); - // Verify second call was made without header + // Verify both calls were made expect(mockFetch).toHaveBeenCalledTimes(2); - const secondCallOptions = mockFetch.mock.calls[1][1]; - // The second call still has options but doesn't include MCP-Protocol-Version header - expect(secondCallOptions).toBeDefined(); - expect(secondCallOptions?.headers).toBeUndefined(); + + // Verify first call had MCP header + expect(mockFetch.mock.calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version"); }); it("throws an error when all fetch attempts fail", async () => { - // Both requests fail - mockFetch.mockRejectedValueOnce(new Error("Network error")); - mockFetch.mockRejectedValueOnce(new Error("Network error")); + // Set up a counter to control behavior + let callCount = 0; + + // Mock implementation that changes behavior based on call count + mockFetch.mockImplementation((url, options) => { + callCount++; + + if (callCount === 1) { + // First call - fail with TypeError + return Promise.reject(new TypeError("First failure")); + } else { + // Second call - fail with different error + return Promise.reject(new Error("Second failure")); + } + }); + // Should fail with the second error await expect(discoverOAuthMetadata("https://auth.example.com")) - .rejects.toThrow("Network error"); + .rejects.toThrow("Second failure"); + + // Verify both calls were made expect(mockFetch).toHaveBeenCalledTimes(2); }); From 0408fdbb6284d4609703fcbc374082f653200ae7 Mon Sep 17 00:00:00 2001 From: Jason Victor Date: Mon, 24 Feb 2025 11:06:34 -0500 Subject: [PATCH 10/23] put missing comment back --- src/server/sse.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server/sse.ts b/src/server/sse.ts index 1bc304a8..84c1cbb9 100644 --- a/src/server/sse.ts +++ b/src/server/sse.ts @@ -136,6 +136,8 @@ export class SSEServerTransport implements Transport { /** * Returns the session ID for this transport. + * + * This can be used to route incoming POST requests. */ get sessionId(): string { return this._sessionId; From c521710b86006826d49924213a2a433c8a5dfc3a Mon Sep 17 00:00:00 2001 From: Jerome Date: Tue, 25 Feb 2025 09:51:17 +0000 Subject: [PATCH 11/23] Lint --- src/client/auth.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 1e892308..02026c4f 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -48,7 +48,7 @@ describe("OAuth Authorization", () => { let callCount = 0; // Mock implementation that changes behavior based on call count - mockFetch.mockImplementation((url, options) => { + mockFetch.mockImplementation((_url, _options) => { callCount++; if (callCount === 1) { @@ -81,7 +81,7 @@ describe("OAuth Authorization", () => { let callCount = 0; // Mock implementation that changes behavior based on call count - mockFetch.mockImplementation((url, options) => { + mockFetch.mockImplementation((_url, _options) => { callCount++; if (callCount === 1) { From 57f3b07bb9a827ab97089bcbeeca89f56b0c009c Mon Sep 17 00:00:00 2001 From: Jason Victor Date: Tue, 25 Feb 2025 17:48:14 -0500 Subject: [PATCH 12/23] docstring --- src/shared/transport.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/shared/transport.ts b/src/shared/transport.ts index bcadcf4d..b80e2a51 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -42,5 +42,8 @@ export interface Transport { */ onmessage?: (message: JSONRPCMessage) => void; + /** + * The session ID generated for this connection. + */ sessionId?: string; } From c396b4af1daea1afd144514348d75d76c93a4d86 Mon Sep 17 00:00:00 2001 From: Jason Victor Date: Tue, 25 Feb 2025 17:57:45 -0500 Subject: [PATCH 13/23] added test for session id using in memory transport --- src/inMemory.ts | 1 + src/server/mcp.test.ts | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/inMemory.ts b/src/inMemory.ts index 2763f38c..106a9e7e 100644 --- a/src/inMemory.ts +++ b/src/inMemory.ts @@ -11,6 +11,7 @@ export class InMemoryTransport implements Transport { onclose?: () => void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage) => void; + sessionId?: string; /** * 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 8f9bfa77..2e91a568 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -323,6 +323,59 @@ describe("tool()", () => { mcpServer.tool("tool2", () => ({ content: [] })); }); + test("should pass sessionId to tool callback via RequestHandlerExtra", async () => { + const mcpServer = new McpServer({ + name: "test server", + version: "1.0", + }); + + const client = new Client( + { + name: "test client", + version: "1.0", + }, + { + capabilities: { + tools: {}, + }, + }, + ); + + let receivedSessionId: string | undefined; + mcpServer.tool("test-tool", async (extra) => { + receivedSessionId = extra.sessionId; + return { + content: [ + { + type: "text", + text: "Test response", + }, + ], + }; + }); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + // Set a test sessionId on the server transport + serverTransport.sessionId = "test-session-123"; + + await Promise.all([ + client.connect(clientTransport), + mcpServer.server.connect(serverTransport), + ]); + + await client.request( + { + method: "tools/call", + params: { + name: "test-tool", + }, + }, + CallToolResultSchema, + ); + + expect(receivedSessionId).toBe("test-session-123"); + }); + test("should allow client to call server tools", async () => { const mcpServer = new McpServer({ name: "test server", From ac729189517d9eed026697e78cdd8766adc67dbc Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Wed, 26 Feb 2025 16:10:31 -0800 Subject: [PATCH 14/23] Add CLAUDE.md with SDK development guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://docs.anthropic.com/s/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..8e084134 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,25 @@ +# MCP TypeScript SDK Guide + +## Build & Test Commands +``` +npm run build # Build ESM and CJS versions +npm run lint # Run ESLint +npm test # Run all tests +npx jest path/to/file.test.ts # Run specific test file +npx jest -t "test name" # Run tests matching pattern +``` + +## Code Style Guidelines +- **TypeScript**: Strict type checking, ES modules, explicit return types +- **Naming**: PascalCase for classes/types, camelCase for functions/variables +- **Files**: Lowercase with hyphens, test files with `.test.ts` suffix +- **Imports**: ES module style, include `.js` extension, group imports logically +- **Error Handling**: Use TypeScript's strict mode, explicit error checking in tests +- **Formatting**: 2-space indentation, semicolons required, single quotes preferred +- **Testing**: Co-locate tests with source files, use descriptive test names +- **Comments**: JSDoc for public APIs, inline comments for complex logic + +## Project Structure +- `/src`: Source code with client, server, and shared modules +- Tests alongside source files with `.test.ts` suffix +- Node.js >= 18 required \ No newline at end of file From 66e1508162d37c0b83b0637ebcd7f07946e3d210 Mon Sep 17 00:00:00 2001 From: Jerome Date: Fri, 28 Feb 2025 11:55:10 +0000 Subject: [PATCH 15/23] Bumping version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 21ed2505..81104cb3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.6.0", + "version": "1.6.1", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 0fa2397174eba309b54575294d56754c52b13a65 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Tue, 11 Mar 2025 10:54:21 +0000 Subject: [PATCH 16/23] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81104cb3..b7641c07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.6.1", + "version": "1.7.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 3c0081f2ecd8bb9b117330783ad25c2ec127ef60 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 15 Mar 2025 13:22:15 +0000 Subject: [PATCH 17/23] add improve transport in SSE server example --- README.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index fe4caa3f..a959ae3d 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ await server.connect(transport); For remote servers, start a web server with a Server-Sent Events (SSE) endpoint, and a separate endpoint for the client to send its messages to: ```typescript -import express from "express"; +import express, { Request, Response } from "express"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; @@ -224,16 +224,27 @@ const server = new McpServer({ const app = express(); -app.get("/sse", async (req, res) => { - const transport = new SSEServerTransport("/messages", res); +// to support multiple simultaneous connections we have a lookup object from +// sessionId to transport +const transports: {[sessionId: string]: SSEServerTransport} = {}; + +app.get("/sse", async (_: Request, res: Response) => { + const transport = new SSEServerTransport('/messages', res); + transports[transport.sessionId] = transport; + res.on("close", () => { + delete transports[transport.sessionId]; + }); await server.connect(transport); }); -app.post("/messages", async (req, res) => { - // Note: to support multiple simultaneous connections, these messages will - // need to be routed to a specific matching transport. (This logic isn't - // implemented here, for simplicity.) - await transport.handlePostMessage(req, res); +app.post("/messages", async (req: Request, res: Response) => { + const sessionId = req.query.sessionId as string; + const transport = transports[sessionId]; + if (transport) { + await transport.handlePostMessage(req, res); + } else { + res.status(400).send('No transport found for sessionId'); + } }); app.listen(3001); From d4bf73b06e404d75c974d5d526cb0acf4e9a56a4 Mon Sep 17 00:00:00 2001 From: zhibisora Date: Sun, 16 Mar 2025 05:00:36 +0800 Subject: [PATCH 18/23] Replace native spawn with cross-spawn for improved cross-platform compatibility --- package.json | 1 + src/client/cross-spawn.test.ts | 112 +++++++++++++++++++++++++++++++++ src/client/stdio.ts | 3 +- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/client/cross-spawn.test.ts diff --git a/package.json b/package.json index b7641c07..51c4e0cd 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", + "cross-spawn": "^7.0.3", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", diff --git a/src/client/cross-spawn.test.ts b/src/client/cross-spawn.test.ts new file mode 100644 index 00000000..d898aeb6 --- /dev/null +++ b/src/client/cross-spawn.test.ts @@ -0,0 +1,112 @@ +import { StdioClientTransport } from "./stdio.js"; +import spawn from "cross-spawn"; + +// mock cross-spawn +jest.mock("cross-spawn"); +const mockSpawn = spawn as jest.MockedFunction; + +describe("StdioClientTransport using cross-spawn", () => { + beforeEach(() => { + // mock cross-spawn's return value + mockSpawn.mockImplementation(() => { + const mockProcess = { + on: jest.fn((event: string, callback: Function) => { + if (event === "spawn") { + callback(); + } + return mockProcess; + }), + stdin: { + on: jest.fn(), + write: jest.fn().mockReturnValue(true) + }, + stdout: { + on: jest.fn() + }, + stderr: null + }; + return mockProcess as any; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test("should call cross-spawn correctly", async () => { + const transport = new StdioClientTransport({ + command: "test-command", + args: ["arg1", "arg2"] + }); + + await transport.start(); + + // verify spawn is called correctly + expect(mockSpawn).toHaveBeenCalledWith( + "test-command", + ["arg1", "arg2"], + expect.objectContaining({ + shell: false + }) + ); + }); + + test("should pass environment variables correctly", async () => { + const customEnv = { TEST_VAR: "test-value" }; + const transport = new StdioClientTransport({ + command: "test-command", + env: customEnv + }); + + await transport.start(); + + // verify environment variables are passed correctly + expect(mockSpawn).toHaveBeenCalledWith( + "test-command", + [], + expect.objectContaining({ + env: customEnv + }) + ); + }); + + test("should send messages correctly", async () => { + const transport = new StdioClientTransport({ + command: "test-command" + }); + + // get the mock process object + const mockProcess = { + on: jest.fn((event, callback) => { + if (event === "spawn") { + callback(); + } + return mockProcess; + }), + stdin: { + on: jest.fn(), + write: jest.fn().mockReturnValue(true), + once: jest.fn() + }, + stdout: { + on: jest.fn() + }, + stderr: null + }; + + mockSpawn.mockReturnValue(mockProcess as any); + + await transport.start(); + + const message = { + jsonrpc: "2.0", + id: "test-id", + method: "test-method" + }; + + await transport.send(message); + + // verify message is sent correctly + expect(mockProcess.stdin.write).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/src/client/stdio.ts b/src/client/stdio.ts index 07e9a196..b83bf27c 100644 --- a/src/client/stdio.ts +++ b/src/client/stdio.ts @@ -1,4 +1,5 @@ -import { ChildProcess, IOType, spawn } from "node:child_process"; +import { ChildProcess, IOType } from "node:child_process"; +import spawn from "cross-spawn"; import process from "node:process"; import { Stream } from "node:stream"; import { ReadBuffer, serializeMessage } from "../shared/stdio.js"; From 46ae2a17af4d331d3076071ed0033bfac0fbb9ef Mon Sep 17 00:00:00 2001 From: zhibisora Date: Sun, 16 Mar 2025 05:15:50 +0800 Subject: [PATCH 19/23] Fix test for cross-spawn --- package-lock.json | 24 +++++++++++++++--------- package.json | 1 + src/client/cross-spawn.test.ts | 28 +++++++++++++++++++++++----- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index cfd8a622..73f1cbba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "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", "cors": "^2.8.5", + "cross-spawn": "^7.0.3", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", @@ -24,6 +25,7 @@ "@jest-mock/express": "^3.0.0", "@types/content-type": "^1.1.8", "@types/cors": "^2.8.17", + "@types/cross-spawn": "^6.0.6", "@types/eslint__js": "^8.42.3", "@types/eventsource": "^1.1.15", "@types/express": "^5.0.0", @@ -1693,6 +1695,16 @@ "@types/node": "*" } }, + "node_modules/@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -2809,7 +2821,6 @@ "version": "7.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4156,8 +4167,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -5359,7 +5369,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5871,7 +5880,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5883,7 +5891,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -6480,7 +6487,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index 51c4e0cd..7302af05 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@jest-mock/express": "^3.0.0", "@types/content-type": "^1.1.8", "@types/cors": "^2.8.17", + "@types/cross-spawn": "^6.0.6", "@types/eslint__js": "^8.42.3", "@types/eventsource": "^1.1.15", "@types/express": "^5.0.0", diff --git a/src/client/cross-spawn.test.ts b/src/client/cross-spawn.test.ts index d898aeb6..ae0a4fd6 100644 --- a/src/client/cross-spawn.test.ts +++ b/src/client/cross-spawn.test.ts @@ -1,5 +1,6 @@ import { StdioClientTransport } from "./stdio.js"; import spawn from "cross-spawn"; +import { JSONRPCMessage } from "../types.js"; // mock cross-spawn jest.mock("cross-spawn"); @@ -9,8 +10,13 @@ describe("StdioClientTransport using cross-spawn", () => { beforeEach(() => { // mock cross-spawn's return value mockSpawn.mockImplementation(() => { - const mockProcess = { - on: jest.fn((event: string, callback: Function) => { + const mockProcess: { + on: jest.Mock; + stdin?: { on: jest.Mock; write: jest.Mock }; + stdout?: { on: jest.Mock }; + stderr?: null; + } = { + on: jest.fn((event: string, callback: () => void) => { if (event === "spawn") { callback(); } @@ -76,8 +82,19 @@ describe("StdioClientTransport using cross-spawn", () => { }); // get the mock process object - const mockProcess = { - on: jest.fn((event, callback) => { + const mockProcess: { + on: jest.Mock; + stdin: { + on: jest.Mock; + write: jest.Mock; + once: jest.Mock; + }; + stdout: { + on: jest.Mock; + }; + stderr: null; + } = { + on: jest.fn((event: string, callback: () => void) => { if (event === "spawn") { callback(); } @@ -98,7 +115,8 @@ describe("StdioClientTransport using cross-spawn", () => { await transport.start(); - const message = { + // 关键修复:确保 jsonrpc 是字面量 "2.0" + const message: JSONRPCMessage = { jsonrpc: "2.0", id: "test-id", method: "test-method" From 2a2392be2608a50a8773ca0ffea202fadfa6669e Mon Sep 17 00:00:00 2001 From: Peter Raboud Date: Tue, 25 Mar 2025 13:26:29 -0700 Subject: [PATCH 20/23] Correctly pass redirect_uri to tokens call --- src/client/auth.test.ts | 18 +++++++++++------- src/client/auth.ts | 4 ++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/client/auth.test.ts b/src/client/auth.test.ts index 02026c4f..cc4717ec 100644 --- a/src/client/auth.test.ts +++ b/src/client/auth.test.ts @@ -46,11 +46,11 @@ describe("OAuth Authorization", () => { it("returns metadata when first fetch fails but second without MCP header succeeds", async () => { // Set up a counter to control behavior let callCount = 0; - + // Mock implementation that changes behavior based on call count mockFetch.mockImplementation((_url, _options) => { callCount++; - + if (callCount === 1) { // First call with MCP header - fail with TypeError (simulating CORS error) // We need to use TypeError specifically because that's what the implementation checks for @@ -68,10 +68,10 @@ describe("OAuth Authorization", () => { // Should succeed with the second call const metadata = await discoverOAuthMetadata("https://auth.example.com"); expect(metadata).toEqual(validMetadata); - + // Verify both calls were made expect(mockFetch).toHaveBeenCalledTimes(2); - + // Verify first call had MCP header expect(mockFetch.mock.calls[0][1]?.headers).toHaveProperty("MCP-Protocol-Version"); }); @@ -79,11 +79,11 @@ describe("OAuth Authorization", () => { it("throws an error when all fetch attempts fail", async () => { // Set up a counter to control behavior let callCount = 0; - + // Mock implementation that changes behavior based on call count mockFetch.mockImplementation((_url, _options) => { callCount++; - + if (callCount === 1) { // First call - fail with TypeError return Promise.reject(new TypeError("First failure")); @@ -96,7 +96,7 @@ describe("OAuth Authorization", () => { // Should fail with the second error await expect(discoverOAuthMetadata("https://auth.example.com")) .rejects.toThrow("Second failure"); - + // Verify both calls were made expect(mockFetch).toHaveBeenCalledTimes(2); }); @@ -250,6 +250,7 @@ describe("OAuth Authorization", () => { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", + redirectUri: "http://localhost:3000/callback", }); expect(tokens).toEqual(validTokens); @@ -271,6 +272,7 @@ describe("OAuth Authorization", () => { expect(body.get("code_verifier")).toBe("verifier123"); expect(body.get("client_id")).toBe("client123"); expect(body.get("client_secret")).toBe("secret123"); + expect(body.get("redirect_uri")).toBe("http://localhost:3000/callback"); }); it("validates token response schema", async () => { @@ -288,6 +290,7 @@ describe("OAuth Authorization", () => { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", + redirectUri: "http://localhost:3000/callback", }) ).rejects.toThrow(); }); @@ -303,6 +306,7 @@ describe("OAuth Authorization", () => { clientInformation: validClientInfo, authorizationCode: "code123", codeVerifier: "verifier123", + redirectUri: "http://localhost:3000/callback", }) ).rejects.toThrow("Token exchange failed"); }); diff --git a/src/client/auth.ts b/src/client/auth.ts index c7799429..a279121b 100644 --- a/src/client/auth.ts +++ b/src/client/auth.ts @@ -115,6 +115,7 @@ export async function auth( clientInformation, authorizationCode, codeVerifier, + redirectUri: provider.redirectUrl, }); await provider.saveTokens(tokens); @@ -259,11 +260,13 @@ export async function exchangeAuthorization( clientInformation, authorizationCode, codeVerifier, + redirectUri, }: { metadata?: OAuthMetadata; clientInformation: OAuthClientInformation; authorizationCode: string; codeVerifier: string; + redirectUri: string | URL; }, ): Promise { const grantType = "authorization_code"; @@ -290,6 +293,7 @@ export async function exchangeAuthorization( client_id: clientInformation.client_id, code: authorizationCode, code_verifier: codeVerifier, + redirect_uri: String(redirectUri), }); if (clientInformation.client_secret) { From c6635d71d6685a92617ae1b7d7520e56868b2e72 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Wed, 26 Mar 2025 10:12:19 +0000 Subject: [PATCH 21/23] Fix lints --- src/client/cross-spawn.test.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/client/cross-spawn.test.ts b/src/client/cross-spawn.test.ts index ae0a4fd6..11e81bf6 100644 --- a/src/client/cross-spawn.test.ts +++ b/src/client/cross-spawn.test.ts @@ -1,6 +1,7 @@ import { StdioClientTransport } from "./stdio.js"; import spawn from "cross-spawn"; import { JSONRPCMessage } from "../types.js"; +import { ChildProcess } from "node:child_process"; // mock cross-spawn jest.mock("cross-spawn"); @@ -31,7 +32,7 @@ describe("StdioClientTransport using cross-spawn", () => { }, stderr: null }; - return mockProcess as any; + return mockProcess as unknown as ChildProcess; }); }); @@ -44,9 +45,9 @@ describe("StdioClientTransport using cross-spawn", () => { command: "test-command", args: ["arg1", "arg2"] }); - + await transport.start(); - + // verify spawn is called correctly expect(mockSpawn).toHaveBeenCalledWith( "test-command", @@ -63,9 +64,9 @@ describe("StdioClientTransport using cross-spawn", () => { command: "test-command", env: customEnv }); - + await transport.start(); - + // verify environment variables are passed correctly expect(mockSpawn).toHaveBeenCalledWith( "test-command", @@ -80,7 +81,7 @@ describe("StdioClientTransport using cross-spawn", () => { const transport = new StdioClientTransport({ command: "test-command" }); - + // get the mock process object const mockProcess: { on: jest.Mock; @@ -110,20 +111,20 @@ describe("StdioClientTransport using cross-spawn", () => { }, stderr: null }; - - mockSpawn.mockReturnValue(mockProcess as any); - + + mockSpawn.mockReturnValue(mockProcess as unknown as ChildProcess); + await transport.start(); - + // 关键修复:确保 jsonrpc 是字面量 "2.0" const message: JSONRPCMessage = { jsonrpc: "2.0", id: "test-id", method: "test-method" }; - + await transport.send(message); - + // verify message is sent correctly expect(mockProcess.stdin.write).toHaveBeenCalled(); }); From 784012946dbc28f69708061863da6733f91c6696 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Wed, 26 Mar 2025 10:15:46 +0000 Subject: [PATCH 22/23] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7302af05..e2d8b3d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.7.0", + "version": "1.8.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 9f5b4bf00d506eb5914e001773143e5408a35133 Mon Sep 17 00:00:00 2001 From: Nathan Arseneau Date: Tue, 1 Apr 2025 19:01:22 -0400 Subject: [PATCH 23/23] Add audio content --- src/types.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index d051d8f7..b9c0ab37 100644 --- a/src/types.ts +++ b/src/types.ts @@ -666,6 +666,23 @@ export const ImageContentSchema = z }) .passthrough(); +/** + * An Audio provided to or from an LLM. + */ +export const AudioContentSchema = z + .object({ + type: z.literal("audio"), + /** + * The base64-encoded audio data. + */ + data: z.string().base64(), + /** + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: z.string(), + }) + .passthrough(); + /** * The contents of a resource, embedded into a prompt or tool call result. */ @@ -685,6 +702,7 @@ export const PromptMessageSchema = z content: z.union([ TextContentSchema, ImageContentSchema, + AudioContentSchema, EmbeddedResourceSchema, ]), }) @@ -753,7 +771,7 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({ */ export const CallToolResultSchema = ResultSchema.extend({ content: z.array( - z.union([TextContentSchema, ImageContentSchema, EmbeddedResourceSchema]), + z.union([TextContentSchema, ImageContentSchema, AudioContentSchema, EmbeddedResourceSchema]), ), isError: z.boolean().default(false).optional(), }); @@ -877,7 +895,7 @@ export const ModelPreferencesSchema = z export const SamplingMessageSchema = z .object({ role: z.enum(["user", "assistant"]), - content: z.union([TextContentSchema, ImageContentSchema]), + content: z.union([TextContentSchema, ImageContentSchema, AudioContentSchema]), }) .passthrough(); @@ -931,6 +949,7 @@ export const CreateMessageResultSchema = ResultSchema.extend({ content: z.discriminatedUnion("type", [ TextContentSchema, ImageContentSchema, + AudioContentSchema ]), }); @@ -1195,6 +1214,7 @@ export type ListPromptsResult = Infer; export type GetPromptRequest = Infer; export type TextContent = Infer; export type ImageContent = Infer; +export type AudioContent = Infer; export type EmbeddedResource = Infer; export type PromptMessage = Infer; export type GetPromptResult = Infer;