From e5167cbe2004e12cedbcaa3d2ba444f13ef80273 Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Tue, 1 Jul 2025 13:20:28 -0400 Subject: [PATCH 01/13] Update streamableHttp custom fetch test --- src/client/sse.test.ts | 30 ++++++++++++++++++++++++++++++ src/client/sse.ts | 13 +++++++++++-- src/client/streamableHttp.test.ts | 29 +++++++++++++++++++++++++++++ src/client/streamableHttp.ts | 15 ++++++++++++--- 4 files changed, 82 insertions(+), 5 deletions(-) diff --git a/src/client/sse.test.ts b/src/client/sse.test.ts index 3cb4e8a3..c446b9e9 100644 --- a/src/client/sse.test.ts +++ b/src/client/sse.test.ts @@ -262,6 +262,36 @@ describe("SSEClientTransport", () => { expect(lastServerRequest.headers.authorization).toBe(authToken); }); + it("uses custom fetch implementation from options", async () => { + const authToken = "Bearer custom-token"; + + const fetchWithAuth = jest.fn((url: string | URL, init?: RequestInit) => { + const headers = new Headers(init?.headers); + headers.set("Authorization", authToken); + return fetch(url.toString(), { ...init, headers }); + }); + + transport = new SSEClientTransport(resourceBaseUrl, { + fetch: fetchWithAuth, + }); + + await transport.start(); + + expect(lastServerRequest.headers.authorization).toBe(authToken); + + // Send a message to verify fetchWithAuth used for POST as well + const message: JSONRPCMessage = { + jsonrpc: "2.0", + id: "1", + method: "test", + params: {}, + }; + + await transport.send(message); + + expect(fetchWithAuth).toHaveBeenCalled(); + }); + it("passes custom headers to fetch requests", async () => { const customHeaders = { Authorization: "Bearer test-token", diff --git a/src/client/sse.ts b/src/client/sse.ts index 2546d508..5ca724b1 100644 --- a/src/client/sse.ts +++ b/src/client/sse.ts @@ -3,6 +3,8 @@ import { Transport } from "../shared/transport.js"; import { JSONRPCMessage, JSONRPCMessageSchema } from "../types.js"; import { auth, AuthResult, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js"; +export type FetchLike = (url: string | URL, init?: RequestInit) => Promise; + export class SseError extends Error { constructor( public readonly code: number | undefined, @@ -47,6 +49,11 @@ export type SSEClientTransportOptions = { * Customizes recurring POST requests to the server. */ requestInit?: RequestInit; + + /** + * Custom fetch implementation used for all network requests. + */ + fetch?: FetchLike; }; /** @@ -62,6 +69,7 @@ export class SSEClientTransport implements Transport { private _eventSourceInit?: EventSourceInit; private _requestInit?: RequestInit; private _authProvider?: OAuthClientProvider; + private _fetch?: FetchLike; private _protocolVersion?: string; onclose?: () => void; @@ -77,6 +85,7 @@ export class SSEClientTransport implements Transport { this._eventSourceInit = opts?.eventSourceInit; this._requestInit = opts?.requestInit; this._authProvider = opts?.authProvider; + this._fetch = opts?.fetch; } private async _authThenStart(): Promise { @@ -117,7 +126,7 @@ export class SSEClientTransport implements Transport { } private _startOrAuth(): Promise { - const fetchImpl = (this?._eventSourceInit?.fetch || fetch) as typeof fetch + const fetchImpl = (this?._eventSourceInit?.fetch || this._fetch || fetch) as typeof fetch return new Promise((resolve, reject) => { this._eventSource = new EventSource( this._url.href, @@ -242,7 +251,7 @@ export class SSEClientTransport implements Transport { signal: this._abortController?.signal, }; - const response = await fetch(this._endpoint, init); + const response = await (this._fetch ?? fetch)(this._endpoint, init); if (!response.ok) { if (response.status === 401 && this._authProvider) { diff --git a/src/client/streamableHttp.test.ts b/src/client/streamableHttp.test.ts index 11dfe7d4..be70b8b4 100644 --- a/src/client/streamableHttp.test.ts +++ b/src/client/streamableHttp.test.ts @@ -443,6 +443,35 @@ describe("StreamableHTTPClientTransport", () => { expect(errorSpy).toHaveBeenCalled(); }); + it("uses custom fetch implementation", async () => { + const authToken = "Bearer custom-token"; + + const fetchWithAuth = jest.fn((url: string | URL, init?: RequestInit) => { + const headers = new Headers(init?.headers); + headers.set("Authorization", authToken); + return (global.fetch as jest.Mock)(url, { ...init, headers }); + }); + + (global.fetch as jest.Mock) + .mockResolvedValueOnce( + new Response(null, { status: 200, headers: { "content-type": "text/event-stream" } }) + ) + .mockResolvedValueOnce(new Response(null, { status: 202 })); + + transport = new StreamableHTTPClientTransport(new URL("http://localhost:1234/mcp"), { fetch: fetchWithAuth }); + + await transport.start(); + await (transport as unknown as { _startOrAuthSse: (opts: any) => Promise })._startOrAuthSse({}); + + await transport.send({ jsonrpc: "2.0", method: "test", params: {}, id: "1" } as JSONRPCMessage); + + expect(fetchWithAuth).toHaveBeenCalled(); + for (const call of (global.fetch as jest.Mock).mock.calls) { + const headers = call[1].headers as Headers; + expect(headers.get("Authorization")).toBe(authToken); + } + }); + it("should always send specified custom headers", async () => { const requestInit = { diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts index 73078442..b73f67b9 100644 --- a/src/client/streamableHttp.ts +++ b/src/client/streamableHttp.ts @@ -3,6 +3,8 @@ import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResponse, JSONRPC import { auth, AuthResult, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js"; import { EventSourceParserStream } from "eventsource-parser/stream"; +export type FetchLike = (url: string | URL, init?: RequestInit) => Promise; + // Default reconnection options for StreamableHTTP connections const DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS: StreamableHTTPReconnectionOptions = { initialReconnectionDelay: 1000, @@ -99,6 +101,11 @@ export type StreamableHTTPClientTransportOptions = { */ requestInit?: RequestInit; + /** + * Custom fetch implementation used for all network requests. + */ + fetch?: FetchLike; + /** * Options to configure the reconnection behavior. */ @@ -122,6 +129,7 @@ export class StreamableHTTPClientTransport implements Transport { private _resourceMetadataUrl?: URL; private _requestInit?: RequestInit; private _authProvider?: OAuthClientProvider; + private _fetch?: FetchLike; private _sessionId?: string; private _reconnectionOptions: StreamableHTTPReconnectionOptions; private _protocolVersion?: string; @@ -138,6 +146,7 @@ export class StreamableHTTPClientTransport implements Transport { this._resourceMetadataUrl = undefined; this._requestInit = opts?.requestInit; this._authProvider = opts?.authProvider; + this._fetch = opts?.fetch; this._sessionId = opts?.sessionId; this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS; } @@ -200,7 +209,7 @@ export class StreamableHTTPClientTransport implements Transport { headers.set("last-event-id", resumptionToken); } - const response = await fetch(this._url, { + const response = await (this._fetch ?? fetch)(this._url, { method: "GET", headers, signal: this._abortController?.signal, @@ -414,7 +423,7 @@ export class StreamableHTTPClientTransport implements Transport { signal: this._abortController?.signal, }; - const response = await fetch(this._url, init); + const response = await (this._fetch ?? fetch)(this._url, init); // Handle session ID received during initialization const sessionId = response.headers.get("mcp-session-id"); @@ -520,7 +529,7 @@ export class StreamableHTTPClientTransport implements Transport { signal: this._abortController?.signal, }; - const response = await fetch(this._url, init); + const response = await (this._fetch ?? fetch)(this._url, init); // We specifically handle 405 as a valid response according to the spec, // meaning the server does not support explicit session termination From 3300a43742baebc60f4b715628f49b298e33af69 Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Tue, 1 Jul 2025 13:33:31 -0400 Subject: [PATCH 02/13] Update src/client/sse.ts nullish coalescing operator ?? instead of the logical OR operator || to handle the case where this?._eventSourceInit?.fetch or this._fetch might be null or undefined but not falsy Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/client/sse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/sse.ts b/src/client/sse.ts index 5ca724b1..808aa24b 100644 --- a/src/client/sse.ts +++ b/src/client/sse.ts @@ -126,7 +126,7 @@ export class SSEClientTransport implements Transport { } private _startOrAuth(): Promise { - const fetchImpl = (this?._eventSourceInit?.fetch || this._fetch || fetch) as typeof fetch +const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch) as typeof fetch return new Promise((resolve, reject) => { this._eventSource = new EventSource( this._url.href, From a3c4e28138753696498647e9a8ed51b7b9e0045f Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Tue, 1 Jul 2025 13:33:39 -0400 Subject: [PATCH 03/13] Update src/client/streamableHttp.ts nullish coalescing operator ?? instead of the logical OR operator || to handle the case where this?._eventSourceInit?.fetch or this._fetch might be null or undefined but not falsy Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/client/streamableHttp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts index b73f67b9..af884b26 100644 --- a/src/client/streamableHttp.ts +++ b/src/client/streamableHttp.ts @@ -209,7 +209,7 @@ export class StreamableHTTPClientTransport implements Transport { headers.set("last-event-id", resumptionToken); } - const response = await (this._fetch ?? fetch)(this._url, { +const response = await (this._fetch ?? fetch)(this._url, { method: "GET", headers, signal: this._abortController?.signal, From 8e02df150e88098cf5bd63b82024933ac8ccd98f Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Tue, 1 Jul 2025 13:33:48 -0400 Subject: [PATCH 04/13] Update src/client/sse.ts nullish coalescing operator ?? instead of the logical OR operator || to handle the case where this?._eventSourceInit?.fetch or this._fetch might be null or undefined but not falsy Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/client/sse.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/sse.ts b/src/client/sse.ts index 808aa24b..af04f274 100644 --- a/src/client/sse.ts +++ b/src/client/sse.ts @@ -251,7 +251,7 @@ const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch) as typ signal: this._abortController?.signal, }; - const response = await (this._fetch ?? fetch)(this._endpoint, init); +const response = await (this._fetch ?? fetch)(this._endpoint, init); if (!response.ok) { if (response.status === 401 && this._authProvider) { From 7d8c0610283f5f1c6a7132a8ed50fc6c4eafd3e6 Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Tue, 1 Jul 2025 13:33:56 -0400 Subject: [PATCH 05/13] Update src/client/streamableHttp.ts nullish coalescing operator ?? instead of the logical OR operator || to handle the case where this?._eventSourceInit?.fetch or this._fetch might be null or undefined but not falsy Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/client/streamableHttp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts index af884b26..d28eae86 100644 --- a/src/client/streamableHttp.ts +++ b/src/client/streamableHttp.ts @@ -423,7 +423,7 @@ const response = await (this._fetch ?? fetch)(this._url, { signal: this._abortController?.signal, }; - const response = await (this._fetch ?? fetch)(this._url, init); +const response = await (this._fetch ?? fetch)(this._url, init); // Handle session ID received during initialization const sessionId = response.headers.get("mcp-session-id"); From 242c8248f73078c62883e49b1dcdbc0727e95319 Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Tue, 1 Jul 2025 13:34:04 -0400 Subject: [PATCH 06/13] Update src/client/streamableHttp.ts nullish coalescing operator ?? instead of the logical OR operator || to handle the case where this?._eventSourceInit?.fetch or this._fetch might be null or undefined but not falsy Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/client/streamableHttp.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts index d28eae86..ceaecd83 100644 --- a/src/client/streamableHttp.ts +++ b/src/client/streamableHttp.ts @@ -529,7 +529,7 @@ const response = await (this._fetch ?? fetch)(this._url, init); signal: this._abortController?.signal, }; - const response = await (this._fetch ?? fetch)(this._url, init); +const response = await (this._fetch ?? fetch)(this._url, init); // We specifically handle 405 as a valid response according to the spec, // meaning the server does not support explicit session termination From f383a9cb8cbcebd9057e0eb2ec1dc22b250c8970 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Tue, 1 Jul 2025 14:00:56 -0400 Subject: [PATCH 07/13] * In sse.test.ts - More specific expectations in test "uses custom fetch implementation from options" * In sse.ts - Import FetchLike from transport.ts * In steramableHttp.ts - Import FetchLike from transport.ts - Export StartSSEOptions interface for testing * In streamableHttp.test.ts - import StartSSEOptions from streamableHttp.ts - use StartSSEOptions instead of any in test "uses custom fetch implementation" * In transport.ts - Add FetchLike function type --- src/client/sse.test.ts | 4 +++- src/client/sse.ts | 4 +--- src/client/streamableHttp.test.ts | 6 +++--- src/client/streamableHttp.ts | 12 +++++------- src/shared/transport.ts | 10 ++++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/client/sse.test.ts b/src/client/sse.test.ts index c446b9e9..2d116344 100644 --- a/src/client/sse.test.ts +++ b/src/client/sse.test.ts @@ -289,7 +289,9 @@ describe("SSEClientTransport", () => { await transport.send(message); - expect(fetchWithAuth).toHaveBeenCalled(); + expect(fetchWithAuth).toHaveBeenCalledTimes(2); + expect(lastServerRequest.method).toBe("POST"); + expect(lastServerRequest.headers.authorization).toBe(authToken); }); it("passes custom headers to fetch requests", async () => { diff --git a/src/client/sse.ts b/src/client/sse.ts index af04f274..faffecc4 100644 --- a/src/client/sse.ts +++ b/src/client/sse.ts @@ -1,10 +1,8 @@ import { EventSource, type ErrorEvent, type EventSourceInit } from "eventsource"; -import { Transport } from "../shared/transport.js"; +import { Transport, FetchLike } from "../shared/transport.js"; import { JSONRPCMessage, JSONRPCMessageSchema } from "../types.js"; import { auth, AuthResult, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js"; -export type FetchLike = (url: string | URL, init?: RequestInit) => Promise; - export class SseError extends Error { constructor( public readonly code: number | undefined, diff --git a/src/client/streamableHttp.test.ts b/src/client/streamableHttp.test.ts index be70b8b4..dcd76528 100644 --- a/src/client/streamableHttp.test.ts +++ b/src/client/streamableHttp.test.ts @@ -1,4 +1,4 @@ -import { StreamableHTTPClientTransport, StreamableHTTPReconnectionOptions } from "./streamableHttp.js"; +import { StreamableHTTPClientTransport, StreamableHTTPReconnectionOptions, StartSSEOptions } from "./streamableHttp.js"; import { OAuthClientProvider, UnauthorizedError } from "./auth.js"; import { JSONRPCMessage } from "../types.js"; @@ -461,7 +461,7 @@ describe("StreamableHTTPClientTransport", () => { transport = new StreamableHTTPClientTransport(new URL("http://localhost:1234/mcp"), { fetch: fetchWithAuth }); await transport.start(); - await (transport as unknown as { _startOrAuthSse: (opts: any) => Promise })._startOrAuthSse({}); + await (transport as unknown as { _startOrAuthSse: (opts: StartSSEOptions) => Promise })._startOrAuthSse({}); await transport.send({ jsonrpc: "2.0", method: "test", params: {}, id: "1" } as JSONRPCMessage); @@ -559,7 +559,7 @@ describe("StreamableHTTPClientTransport", () => { // Second retry - should double (2^1 * 100 = 200) expect(getDelay(1)).toBe(200); - // Third retry - should double again (2^2 * 100 = 400) + // Third retry - should double again (2^2 * 100 = 400) expect(getDelay(2)).toBe(400); // Fourth retry - should double again (2^3 * 100 = 800) diff --git a/src/client/streamableHttp.ts b/src/client/streamableHttp.ts index ceaecd83..b81f1a5d 100644 --- a/src/client/streamableHttp.ts +++ b/src/client/streamableHttp.ts @@ -1,10 +1,8 @@ -import { Transport } from "../shared/transport.js"; +import { Transport, FetchLike } from "../shared/transport.js"; import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema } from "../types.js"; import { auth, AuthResult, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js"; import { EventSourceParserStream } from "eventsource-parser/stream"; -export type FetchLike = (url: string | URL, init?: RequestInit) => Promise; - // Default reconnection options for StreamableHTTP connections const DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS: StreamableHTTPReconnectionOptions = { initialReconnectionDelay: 1000, @@ -25,7 +23,7 @@ export class StreamableHTTPError extends Error { /** * Options for starting or authenticating an SSE connection */ -interface StartSSEOptions { +export interface StartSSEOptions { /** * The resumption token used to continue long-running requests that were interrupted. * @@ -260,15 +258,15 @@ const response = await (this._fetch ?? fetch)(this._url, { private _normalizeHeaders(headers: HeadersInit | undefined): Record { if (!headers) return {}; - + if (headers instanceof Headers) { return Object.fromEntries(headers.entries()); } - + if (Array.isArray(headers)) { return Object.fromEntries(headers); } - + return { ...headers as Record }; } diff --git a/src/shared/transport.ts b/src/shared/transport.ts index 96b291fa..386b6bae 100644 --- a/src/shared/transport.ts +++ b/src/shared/transport.ts @@ -1,10 +1,12 @@ import { JSONRPCMessage, MessageExtraInfo, RequestId } from "../types.js"; +export type FetchLike = (url: string | URL, init?: RequestInit) => Promise; + /** * Options for sending a JSON-RPC message. */ export type TransportSendOptions = { - /** + /** * If present, `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. */ relatedRequestId?: RequestId; @@ -38,7 +40,7 @@ export interface Transport { /** * Sends a JSON-RPC message (request or response). - * + * * If present, `relatedRequestId` is used to indicate to the transport which incoming request to associate this outgoing message with. */ send(message: JSONRPCMessage, options?: TransportSendOptions): Promise; @@ -64,9 +66,9 @@ export interface Transport { /** * Callback for when a message (request or response) is received over the connection. - * + * * Includes the requestInfo and authInfo if the transport is authenticated. - * + * * The requestInfo can be used to get the original request information (headers, etc.) */ onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void; From a45e4febd94b99b4bccac23a737c1ca39610e2d4 Mon Sep 17 00:00:00 2001 From: Kentaro Suzuki <71284054+sushichan044@users.noreply.github.com> Date: Thu, 3 Jul 2025 22:13:24 +0900 Subject: [PATCH 08/13] Revert "fix: add type safety for tool output schemas in ToolCallback" --- src/examples/server/mcpServerOutputSchema.ts | 11 +--- src/server/mcp.test.ts | 2 +- src/server/mcp.ts | 68 +++++++------------- 3 files changed, 27 insertions(+), 54 deletions(-) diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index de3b363e..75bfe690 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -43,14 +43,7 @@ server.registerTool( void country; // Simulate weather API call const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10; - const conditionCandidates = [ - "sunny", - "cloudy", - "rainy", - "stormy", - "snowy", - ] as const; - const conditions = conditionCandidates[Math.floor(Math.random() * conditionCandidates.length)]; + const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)]; const structuredContent = { temperature: { @@ -84,4 +77,4 @@ async function main() { main().catch((error) => { console.error("Server error:", error); process.exit(1); -}); +}); \ No newline at end of file diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index dc96a1b0..10e550df 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -1312,7 +1312,7 @@ describe("tool()", () => { resultType: "structured", // Missing required 'timestamp' field someExtraField: "unexpected" // Extra field not in schema - } as unknown as { processedInput: string; resultType: string; timestamp: string }, // Type assertion to bypass TypeScript validation for testing purposes + }, }) ); diff --git a/src/server/mcp.ts b/src/server/mcp.ts index a5624e15..791facef 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -169,7 +169,7 @@ export class McpServer { } const args = parseResult.data; - const cb = tool.callback as ToolCallback; + const cb = tool.callback as ToolCallback; try { result = await Promise.resolve(cb(args, extra)); } catch (error) { @@ -184,7 +184,7 @@ export class McpServer { }; } } else { - const cb = tool.callback as ToolCallback; + const cb = tool.callback as ToolCallback; try { result = await Promise.resolve(cb(extra)); } catch (error) { @@ -772,7 +772,7 @@ export class McpServer { inputSchema: ZodRawShape | undefined, outputSchema: ZodRawShape | undefined, annotations: ToolAnnotations | undefined, - callback: ToolCallback + callback: ToolCallback ): RegisteredTool { const registeredTool: RegisteredTool = { title, @@ -929,7 +929,7 @@ export class McpServer { outputSchema?: OutputArgs; annotations?: ToolAnnotations; }, - cb: ToolCallback + cb: ToolCallback ): RegisteredTool { if (this._registeredTools[name]) { throw new Error(`Tool ${name} is already registered`); @@ -944,7 +944,7 @@ export class McpServer { inputSchema, outputSchema, annotations, - cb as ToolCallback + cb as ToolCallback ); } @@ -1138,16 +1138,6 @@ export class ResourceTemplate { } } -/** - * Type helper to create a strongly-typed CallToolResult with structuredContent - */ -type TypedCallToolResult = - OutputArgs extends ZodRawShape - ? CallToolResult & { - structuredContent?: z.objectOutputType; - } - : CallToolResult; - /** * Callback for a tool handler registered with Server.tool(). * @@ -1158,21 +1148,13 @@ type TypedCallToolResult = * - `content` if the tool does not have an outputSchema * - Both fields are optional but typically one should be provided */ -export type ToolCallback< - InputArgs extends undefined | ZodRawShape = undefined, - OutputArgs extends undefined | ZodRawShape = undefined -> = InputArgs extends ZodRawShape +export type ToolCallback = + Args extends ZodRawShape ? ( - args: z.objectOutputType, - extra: RequestHandlerExtra - ) => - | TypedCallToolResult - | Promise> - : ( - extra: RequestHandlerExtra - ) => - | TypedCallToolResult - | Promise>; + args: z.objectOutputType, + extra: RequestHandlerExtra, + ) => CallToolResult | Promise + : (extra: RequestHandlerExtra) => CallToolResult | Promise; export type RegisteredTool = { title?: string; @@ -1180,24 +1162,22 @@ export type RegisteredTool = { inputSchema?: AnyZodObject; outputSchema?: AnyZodObject; annotations?: ToolAnnotations; - callback: ToolCallback; + callback: ToolCallback; enabled: boolean; enable(): void; disable(): void; - update< - InputArgs extends ZodRawShape, - OutputArgs extends ZodRawShape - >(updates: { - name?: string | null; - title?: string; - description?: string; - paramsSchema?: InputArgs; - outputSchema?: OutputArgs; - annotations?: ToolAnnotations; - callback?: ToolCallback - enabled?: boolean - }): void; - remove(): void; + update( + updates: { + name?: string | null, + title?: string, + description?: string, + paramsSchema?: InputArgs, + outputSchema?: OutputArgs, + annotations?: ToolAnnotations, + callback?: ToolCallback, + enabled?: boolean + }): void + remove(): void }; const EMPTY_OBJECT_JSON_SCHEMA = { From 25048b9eaf8410a97833807dba7039f045ba7414 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Thu, 3 Jul 2025 15:51:16 +0100 Subject: [PATCH 09/13] rename reject to decline --- README.md | 2 +- src/examples/client/simpleStreamableHttp.ts | 8 ++++---- src/examples/server/simpleStreamableHttp.ts | 8 ++++---- src/server/index.test.ts | 4 ++-- src/types.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 5cc1f4b2..ac10e8cb 100644 --- a/README.md +++ b/README.md @@ -1041,7 +1041,7 @@ Client-side: Handle elicitation requests ```typescript // This is a placeholder - implement based on your UI framework async function getInputFromUser(message: string, schema: any): Promise<{ - action: "accept" | "reject" | "cancel"; + action: "accept" | "decline" | "cancel"; data?: Record; }> { // This should be implemented depending on the app diff --git a/src/examples/client/simpleStreamableHttp.ts b/src/examples/client/simpleStreamableHttp.ts index 02db131e..ddb27419 100644 --- a/src/examples/client/simpleStreamableHttp.ts +++ b/src/examples/client/simpleStreamableHttp.ts @@ -363,7 +363,7 @@ async function connect(url?: string): Promise { continue; } else { console.log('Maximum attempts reached. Declining request.'); - return { action: 'reject' }; + return { action: 'decline' }; } } @@ -381,7 +381,7 @@ async function connect(url?: string): Promise { continue; } else { console.log('Maximum attempts reached. Declining request.'); - return { action: 'reject' }; + return { action: 'decline' }; } } @@ -408,13 +408,13 @@ async function connect(url?: string): Promise { console.log('Please re-enter the information...'); continue; } else { - return { action: 'reject' }; + return { action: 'decline' }; } } } console.log('Maximum attempts reached. Declining request.'); - return { action: 'reject' }; + return { action: 'decline' }; }); transport = new StreamableHTTPClientTransport( diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index fea0eec0..7adab7db 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -206,12 +206,12 @@ const getServer = () => { }, ], }; - } else if (result.action === 'reject') { + } else if (result.action === 'decline') { return { content: [ { type: 'text', - text: `No information was collected. User rejected ${infoType} information request.`, + text: `No information was collected. User declined ${infoType} information request.`, }, ], }; @@ -433,7 +433,7 @@ if (useOAuth) { const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`); const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`); - const oauthMetadata: OAuthMetadata = setupAuthServer({authServerUrl, mcpServerUrl, strictResource: strictOAuth}); + const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth }); const tokenVerifier = { verifyAccessToken: async (token: string) => { @@ -499,7 +499,7 @@ const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; // MCP POST endpoint with optional auth const mcpPostHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - console.log(sessionId? `Received MCP request for session: ${sessionId}`: 'Received MCP request:', req.body); + console.log(sessionId ? `Received MCP request for session: ${sessionId}` : 'Received MCP request:', req.body); if (useOAuth && req.auth) { console.log('Authenticated user:', req.auth); } diff --git a/src/server/index.test.ts b/src/server/index.test.ts index d91b90a9..46205d72 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -538,7 +538,7 @@ test("should allow elicitation reject and cancel without validation", async () = client.setRequestHandler(ElicitRequestSchema, (request) => { requestCount++; if (requestCount === 1) { - return { action: "reject" }; + return { action: "decline" }; } else { return { action: "cancel" }; } @@ -566,7 +566,7 @@ test("should allow elicitation reject and cancel without validation", async () = requestedSchema: schema, }), ).resolves.toEqual({ - action: "reject", + action: "decline", }); // Test cancel - should not validate diff --git a/src/types.ts b/src/types.ts index f66d2c4b..b96ab050 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1238,7 +1238,7 @@ export const ElicitResultSchema = ResultSchema.extend({ /** * The user's response action. */ - action: z.enum(["accept", "reject", "cancel"]), + action: z.enum(["accept", "decline", "cancel"]), /** * The collected user input content (only present if action is "accept"). */ From d868c4a1470bdf29012a5e6abed73a9f689a6369 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Thu, 3 Jul 2025 16:01:14 +0100 Subject: [PATCH 10/13] build --- src/examples/server/simpleStreamableHttp.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/examples/server/simpleStreamableHttp.ts b/src/examples/server/simpleStreamableHttp.ts index 7adab7db..3d523543 100644 --- a/src/examples/server/simpleStreamableHttp.ts +++ b/src/examples/server/simpleStreamableHttp.ts @@ -499,7 +499,12 @@ const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; // MCP POST endpoint with optional auth const mcpPostHandler = async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; - console.log(sessionId ? `Received MCP request for session: ${sessionId}` : 'Received MCP request:', req.body); + if (sessionId) { + console.log(`Received MCP request for session: ${sessionId}`); + } else { + console.log('Request body:', req.body); + } + if (useOAuth && req.auth) { console.log('Authenticated user:', req.auth); } From fc197e01f837971f997f1f57d853e127b7611ac2 Mon Sep 17 00:00:00 2001 From: ihrpr Date: Thu, 3 Jul 2025 16:09:00 +0100 Subject: [PATCH 11/13] 1.14.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16b90a3b..f2c8cbfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.13.3", + "version": "1.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.13.3", + "version": "1.14.0", "license": "MIT", "dependencies": { "ajv": "^6.12.6", diff --git a/package.json b/package.json index e5061966..15b7753b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.13.3", + "version": "1.14.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 42d4abf383a7eb710d5e12322c5ae5d09ea4a817 Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Thu, 3 Jul 2025 18:06:29 -0400 Subject: [PATCH 12/13] bump version to 1.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15b7753b..e381dd12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.14.0", + "version": "1.15.0", "description": "Model Context Protocol implementation for TypeScript", "license": "MIT", "author": "Anthropic, PBC (https://anthropic.com)", From 979700a7b76746e5aeb92e1b20e5968f30a8286d Mon Sep 17 00:00:00 2001 From: Basil Hosmer Date: Thu, 3 Jul 2025 18:13:48 -0400 Subject: [PATCH 13/13] npm install --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2c8cbfa..01bc0953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/sdk", - "version": "1.14.0", + "version": "1.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/sdk", - "version": "1.14.0", + "version": "1.15.0", "license": "MIT", "dependencies": { "ajv": "^6.12.6",