@modelcontextprotocol/server 2.0.0-alpha.2 → 2.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +10 -3
  2. package/dist/{src-IKPjmxu7.mjs → ajvProvider-BQMcjynJ.mjs} +4147 -6749
  3. package/dist/ajvProvider-BQMcjynJ.mjs.map +1 -0
  4. package/dist/ajvProvider-Dzgk80kq.d.mts +1030 -0
  5. package/dist/ajvProvider-Dzgk80kq.d.mts.map +1 -0
  6. package/dist/cfWorkerProvider-BDC2rVl3.mjs +970 -0
  7. package/dist/cfWorkerProvider-BDC2rVl3.mjs.map +1 -0
  8. package/dist/cfWorkerProvider-DmvjVsvQ.d.mts +59 -0
  9. package/dist/cfWorkerProvider-DmvjVsvQ.d.mts.map +1 -0
  10. package/dist/chunk-BRhqBsOc.mjs +42 -0
  11. package/dist/{index-Bhfkexnj.d.mts → createMcpHandler-Du3hjXvf.d.mts} +4702 -2249
  12. package/dist/createMcpHandler-Du3hjXvf.d.mts.map +1 -0
  13. package/dist/index.d.mts +177 -833
  14. package/dist/index.d.mts.map +1 -1
  15. package/dist/index.mjs +1349 -2067
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/mcp-JttQJlI9.mjs +9998 -0
  18. package/dist/mcp-JttQJlI9.mjs.map +1 -0
  19. package/dist/shimsNode.d.mts +1 -1
  20. package/dist/shimsNode.mjs +1 -1
  21. package/dist/shimsWorkerd.d.mts +1 -1
  22. package/dist/shimsWorkerd.mjs +1 -1
  23. package/dist/shimsWorkerd.mjs.map +1 -1
  24. package/dist/stdio.d.mts +107 -0
  25. package/dist/stdio.d.mts.map +1 -0
  26. package/dist/stdio.mjs +561 -0
  27. package/dist/stdio.mjs.map +1 -0
  28. package/dist/types-DBYdVs-n.d.mts +1099 -0
  29. package/dist/types-DBYdVs-n.d.mts.map +1 -0
  30. package/dist/validators/ajv.d.mts +2 -0
  31. package/dist/validators/ajv.mjs +4 -0
  32. package/dist/validators/cfWorker.d.mts +2 -0
  33. package/dist/validators/cfWorker.mjs +3 -0
  34. package/package.json +33 -17
  35. package/dist/index-Bhfkexnj.d.mts.map +0 -1
  36. package/dist/src-IKPjmxu7.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,1554 +1,1306 @@
1
- import { $ as PARSE_ERROR, A as parseJSONRPCMessage, B as ListRootsResultSchema, C as isInitializeRequest, D as isJSONRPCRequest, E as isJSONRPCNotification, F as CreateTaskResultSchema, G as ProtocolErrorCode, H as getResultSchema, I as ElicitResultSchema, J as INVALID_PARAMS, K as DEFAULT_NEGOTIATED_PROTOCOL_VERSION, L as EmptyResultSchema, M as CallToolResultSchema, N as CreateMessageResultSchema, O as isJSONRPCResultResponse, P as CreateMessageResultWithToolsSchema, Q as METHOD_NOT_FOUND, R as GetTaskPayloadResultSchema, S as assertCompleteRequestResourceTemplate, T as isJSONRPCErrorResponse, U as ProtocolError, V as LoggingLevelSchema, W as UrlElicitationRequiredError, X as JSONRPC_VERSION, Y as INVALID_REQUEST, Z as LATEST_PROTOCOL_VERSION, _ as mergeCapabilities, a as standardSchemaToJsonSchema, at as SdkError, b as parseSchema, c as createFetchWithInit, ct as OAuthErrorCode, d as deserializeMessage, et as RELATED_TASK_META_KEY, f as serializeMessage, g as Protocol, h as DEFAULT_REQUEST_TIMEOUT_MSEC, i as promptArgumentsFromStandardSchema, it as resourceUrlFromServerUrl, j as CallToolRequestSchema, k as isTaskAugmentedRequestParams, l as validateAndWarnToolName, m as toArrayAsync, n as CfWorkerJsonSchemaValidator, nt as getDisplayName, o as validateStandardSchema, ot as SdkErrorCode, p as takeResult, q as INTERNAL_ERROR, r as AjvJsonSchemaValidator, rt as checkResourceAllowed, s as UriTemplate, st as OAuthError, t as fromJsonSchema$1, tt as SUPPORTED_PROTOCOL_VERSIONS, u as ReadBuffer, v as extractTaskManagerOptions, w as isInitializedNotification, x as assertCompleteRequestPrompt, y as isTerminal, z as JSONRPCMessageSchema } from "./src-IKPjmxu7.mjs";
2
- import { DefaultJsonSchemaValidator, process } from "@modelcontextprotocol/server/_shims";
1
+ import { A as classifyInboundRequest, At as SdkErrorCode, B as isInitializedNotification, C as isSpecType, Ct as TRACEPARENT_META_KEY, D as inputResponse, Dt as checkResourceAllowed, E as inputRequired, Et as requiredClientCapabilitiesForRequest, F as validateMcpParamHeaders, Ft as isCompletable, G as isJSONRPCResponse, H as isJSONRPCErrorResponse, I as assertCompleteRequestPrompt, J as parseJSONRPCMessage, K as isJSONRPCResultResponse, L as assertCompleteRequestResourceTemplate, M as modernOnlyStrictRejection, Mt as OAuthError, N as validateStandardRequestHeaders, Nt as OAuthErrorCode, O as LADDER_ERROR_HTTP_STATUS, Ot as resourceUrlFromServerUrl, P as scanXMcpHeaderDeclarations, Pt as completable, Q as requestMetaOf, R as isCallToolResult, S as setNegotiatedProtocolVersion, St as SUPPORTED_PROTOCOL_VERSIONS, T as acceptedContent, Tt as missingClientCapabilities, U as isJSONRPCNotification, V as isInputRequiredResult, W as isJSONRPCRequest, Y as JSONRPCMessageSchema, _ as STDIO_DEFAULT_MAX_BUFFER_SIZE, _t as METHOD_NOT_FOUND, a as seedClientIdentityFromEnvelope, at as ProtocolErrorCode, b as getDisplayName, bt as RELATED_TASK_META_KEY, ct as CLIENT_CAPABILITIES_META_KEY, d as createServerNotifier, dt as INTERNAL_ERROR, et as MissingRequiredClientCapabilityError, f as fromJsonSchema$1, ft as INVALID_PARAMS, g as ReadBuffer, gt as LOG_LEVEL_META_KEY, h as createFetchWithInit, ht as LATEST_PROTOCOL_VERSION, i as installModernOnlyHandlers, it as UrlElicitationRequiredError, j as httpStatusForErrorCode, jt as SdkHttpError, kt as SdkError, l as createListenRouter, lt as CLIENT_INFO_META_KEY, m as UriTemplate, mt as JSONRPC_VERSION, n as ResourceTemplate, nt as ResourceNotFoundError, o as DEFAULT_LISTEN_KEEPALIVE_MS, ot as SUPPORTED_MODERN_PROTOCOL_VERSIONS, p as InMemoryTransport, pt as INVALID_REQUEST, q as isTaskAugmentedRequestParams, r as Server, rt as UnsupportedProtocolVersionError, s as DEFAULT_MAX_SUBSCRIPTIONS, st as BAGGAGE_META_KEY, t as McpServer, tt as ProtocolError, u as InMemoryServerEventBus, ut as DEFAULT_NEGOTIATED_PROTOCOL_VERSION, v as deserializeMessage, vt as PARSE_ERROR, w as specTypeSchemas, wt as TRACESTATE_META_KEY, x as DEFAULT_REQUEST_TIMEOUT_MSEC, xt as SUBSCRIPTION_ID_META_KEY, y as serializeMessage, yt as PROTOCOL_VERSION_META_KEY, z as isInitializeRequest } from "./mcp-JttQJlI9.mjs";
2
+ import { DefaultJsonSchemaValidator } from "@modelcontextprotocol/server/_shims";
3
3
 
4
- //#region src/server/completable.ts
5
- const COMPLETABLE_SYMBOL = Symbol.for("mcp.completable");
4
+ //#region src/server/perRequestTransport.ts
6
5
  /**
7
- * Wraps a schema to provide autocompletion capabilities. Useful for, e.g., prompt arguments in MCP.
8
- *
9
- * @example
10
- * ```ts source="./completable.examples.ts#completable_basicUsage"
11
- * server.registerPrompt(
12
- * 'review-code',
13
- * {
14
- * title: 'Code Review',
15
- * argsSchema: z.object({
16
- * language: completable(z.string().describe('Programming language'), value =>
17
- * ['typescript', 'javascript', 'python', 'rust', 'go'].filter(lang => lang.startsWith(value))
18
- * )
19
- * })
20
- * },
21
- * ({ language }) => ({
22
- * messages: [
23
- * {
24
- * role: 'user' as const,
25
- * content: {
26
- * type: 'text' as const,
27
- * text: `Review this ${language} code.`
28
- * }
29
- * }
30
- * ]
31
- * })
32
- * );
33
- * ```
34
- *
35
- * @see {@linkcode server/mcp.McpServer.registerPrompt | McpServer.registerPrompt} for using completable schemas in prompt argument definitions
36
- */
37
- function completable(schema, complete) {
38
- Object.defineProperty(schema, COMPLETABLE_SYMBOL, {
39
- value: { complete },
40
- enumerable: false,
41
- writable: false,
42
- configurable: false
43
- });
44
- return schema;
45
- }
46
- /**
47
- * Checks if a schema is completable (has completion metadata).
48
- */
49
- function isCompletable(schema) {
50
- return !!schema && typeof schema === "object" && COMPLETABLE_SYMBOL in schema;
51
- }
52
- /**
53
- * Gets the completer callback from a completable schema, if it exists.
54
- */
55
- function getCompleter(schema) {
56
- return schema[COMPLETABLE_SYMBOL]?.complete;
57
- }
58
-
59
- //#endregion
60
- //#region ../core/src/experimental/tasks/helpers.ts
61
- /**
62
- * Experimental task capability assertion helpers.
63
- * WARNING: These APIs are experimental and may change without notice.
64
- *
65
- * @experimental
66
- */
67
- /**
68
- * Asserts that task creation is supported for `tools/call`.
69
- * Used to implement the `assertTaskCapability` or `assertTaskHandlerCapability` abstract methods on Protocol.
70
- *
71
- * @param requests - The task requests capability object
72
- * @param method - The method being checked
73
- * @param entityName - `'Server'` or `'Client'` for error messages
74
- * @throws {@linkcode SdkError} with {@linkcode SdkErrorCode.CapabilityNotSupported} if the capability is not supported
75
- *
76
- * @experimental
77
- */
78
- function assertToolsCallTaskCapability(requests, method, entityName) {
79
- if (!requests) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `${entityName} does not support task creation (required for ${method})`);
80
- switch (method) {
81
- case "tools/call":
82
- if (!requests.tools?.call) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `${entityName} does not support task creation for tools/call (required for ${method})`);
83
- break;
84
- default: break;
85
- }
86
- }
87
- /**
88
- * Asserts that task creation is supported for `sampling/createMessage` or `elicitation/create`.
89
- * Used to implement the `assertTaskCapability` or `assertTaskHandlerCapability` abstract methods on Protocol.
90
- *
91
- * @param requests - The task requests capability object
92
- * @param method - The method being checked
93
- * @param entityName - `'Server'` or `'Client'` for error messages
94
- * @throws {@linkcode SdkError} with {@linkcode SdkErrorCode.CapabilityNotSupported} if the capability is not supported
95
- *
96
- * @experimental
97
- */
98
- function assertClientRequestTaskCapability(requests, method, entityName) {
99
- if (!requests) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `${entityName} does not support task creation (required for ${method})`);
100
- switch (method) {
101
- case "sampling/createMessage":
102
- if (!requests.sampling?.createMessage) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `${entityName} does not support task creation for sampling/createMessage (required for ${method})`);
103
- break;
104
- case "elicitation/create":
105
- if (!requests.elicitation?.create) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `${entityName} does not support task creation for elicitation/create (required for ${method})`);
106
- break;
107
- default: break;
108
- }
109
- }
110
-
111
- //#endregion
112
- //#region ../core/src/experimental/tasks/stores/inMemory.ts
113
- /**
114
- * In-memory {@linkcode TaskStore} implementation for development and testing.
115
- * For production, use a database or distributed cache.
116
- * @experimental
6
+ * The per-request micro-transport: a real, connected `Transport` whose whole
7
+ * lifetime is one HTTP exchange. See the module documentation for the
8
+ * response shapes it produces.
117
9
  */
118
- var InMemoryTaskStore = class {
119
- tasks = /* @__PURE__ */ new Map();
120
- cleanupTimers = /* @__PURE__ */ new Map();
10
+ var PerRequestHTTPServerTransport = class {
11
+ onclose;
12
+ onerror;
13
+ onmessage;
14
+ _classification;
15
+ _responseMode;
16
+ _started = false;
17
+ _used = false;
18
+ _closed = false;
19
+ _terminalDelivered = false;
121
20
  /**
122
- * Generates a unique task ID using Web Crypto API.
21
+ * `true` only while the inbound message is being delivered synchronously
22
+ * to the connected protocol layer. The pre-handler gates (the era
23
+ * registry gate, the edge→instance handoff check, the missing-handler
24
+ * rejection) answer inside this window; request handlers always run
25
+ * after it (the protocol layer defers them to a microtask). An error
26
+ * sent inside the window is therefore ladder-originated, and an error
27
+ * sent after it is handler-produced.
123
28
  */
124
- generateTaskId() {
125
- return crypto.randomUUID().replaceAll("-", "");
29
+ _dispatchWindowOpen = false;
30
+ _requestId;
31
+ _deferredResponse;
32
+ _sse;
33
+ _abortCleanup;
34
+ constructor(options) {
35
+ this._classification = options.classification;
36
+ this._responseMode = options.responseMode ?? "auto";
126
37
  }
127
- /** {@inheritDoc TaskStore.createTask} */
128
- async createTask(taskParams, requestId, request, sessionId) {
129
- const taskId = this.generateTaskId();
130
- if (this.tasks.has(taskId)) throw new Error(`Task with ID ${taskId} already exists`);
131
- const actualTtl = taskParams.ttl ?? null;
132
- const createdAt = (/* @__PURE__ */ new Date()).toISOString();
133
- const task = {
134
- taskId,
135
- status: "working",
136
- ttl: actualTtl,
137
- createdAt,
138
- lastUpdatedAt: createdAt,
139
- pollInterval: taskParams.pollInterval ?? 1e3
140
- };
141
- this.tasks.set(taskId, {
142
- task,
143
- request,
144
- requestId,
145
- sessionId
146
- });
147
- if (actualTtl) {
148
- const timer = setTimeout(() => {
149
- this.tasks.delete(taskId);
150
- this.cleanupTimers.delete(taskId);
151
- }, actualTtl);
152
- this.cleanupTimers.set(taskId, timer);
153
- }
154
- return task;
38
+ async start() {
39
+ if (this._started) throw new Error("PerRequestHTTPServerTransport is already started");
40
+ this._started = true;
155
41
  }
156
42
  /**
157
- * Retrieves a stored task, enforcing session ownership when a sessionId is provided.
158
- * Returns undefined if the task does not exist or belongs to a different session.
43
+ * Serves the single exchange: delivers the classified message to the
44
+ * connected server instance and resolves with the HTTP response.
45
+ *
46
+ * Throws when called a second time (the transport is strictly
47
+ * single-use), or before a server has been connected to the transport.
48
+ * The returned promise rejects with a connection-closed error when the
49
+ * transport is closed before a response was produced (for example because
50
+ * the client disconnected).
159
51
  */
160
- getStoredTask(taskId, sessionId) {
161
- const stored = this.tasks.get(taskId);
162
- if (!stored) return;
163
- if (sessionId !== void 0 && stored.sessionId !== void 0 && stored.sessionId !== sessionId) return;
164
- return stored;
165
- }
166
- async getTask(taskId, sessionId) {
167
- const stored = this.getStoredTask(taskId, sessionId);
168
- return stored ? { ...stored.task } : null;
169
- }
170
- /** {@inheritDoc TaskStore.storeTaskResult} */
171
- async storeTaskResult(taskId, status, result, sessionId) {
172
- const stored = this.getStoredTask(taskId, sessionId);
173
- if (!stored) throw new Error(`Task with ID ${taskId} not found`);
174
- if (isTerminal(stored.task.status)) throw new Error(`Cannot store result for task ${taskId} in terminal status '${stored.task.status}'. Task results can only be stored once.`);
175
- stored.result = result;
176
- stored.task.status = status;
177
- stored.task.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
178
- if (stored.task.ttl) {
179
- const existingTimer = this.cleanupTimers.get(taskId);
180
- if (existingTimer) clearTimeout(existingTimer);
181
- const timer = setTimeout(() => {
182
- this.tasks.delete(taskId);
183
- this.cleanupTimers.delete(taskId);
184
- }, stored.task.ttl);
185
- this.cleanupTimers.set(taskId, timer);
52
+ async handleMessage(message, extra) {
53
+ if (this._used) throw new Error("PerRequestHTTPServerTransport serves exactly one exchange; construct a new transport per request");
54
+ if (!this._started || this.onmessage === void 0) throw new Error("PerRequestHTTPServerTransport is not connected: connect a server to this transport before handling a message");
55
+ if (this._closed) throw new Error("PerRequestHTTPServerTransport is closed");
56
+ this._used = true;
57
+ const signal = extra?.request?.signal;
58
+ if (signal?.aborted) {
59
+ await this.close();
60
+ throw new SdkError(SdkErrorCode.ConnectionClosed, "The request was aborted before it could be handled");
186
61
  }
187
- }
188
- /** {@inheritDoc TaskStore.getTaskResult} */
189
- async getTaskResult(taskId, sessionId) {
190
- const stored = this.getStoredTask(taskId, sessionId);
191
- if (!stored) throw new Error(`Task with ID ${taskId} not found`);
192
- if (!stored.result) throw new Error(`Task ${taskId} has no result stored`);
193
- return stored.result;
194
- }
195
- /** {@inheritDoc TaskStore.updateTaskStatus} */
196
- async updateTaskStatus(taskId, status, statusMessage, sessionId) {
197
- const stored = this.getStoredTask(taskId, sessionId);
198
- if (!stored) throw new Error(`Task with ID ${taskId} not found`);
199
- if (isTerminal(stored.task.status)) throw new Error(`Cannot update task ${taskId} from terminal status '${stored.task.status}' to '${status}'. Terminal states (completed, failed, cancelled) cannot transition to other states.`);
200
- stored.task.status = status;
201
- if (statusMessage) stored.task.statusMessage = statusMessage;
202
- stored.task.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
203
- if (isTerminal(status) && stored.task.ttl) {
204
- const existingTimer = this.cleanupTimers.get(taskId);
205
- if (existingTimer) clearTimeout(existingTimer);
206
- const timer = setTimeout(() => {
207
- this.tasks.delete(taskId);
208
- this.cleanupTimers.delete(taskId);
209
- }, stored.task.ttl);
210
- this.cleanupTimers.set(taskId, timer);
62
+ const messageExtra = {
63
+ classification: this._classification,
64
+ ...extra?.request !== void 0 && { request: extra.request },
65
+ ...extra?.authInfo !== void 0 && { authInfo: extra.authInfo }
66
+ };
67
+ if (isJSONRPCRequest(message)) {
68
+ this._requestId = message.id;
69
+ let resolve;
70
+ let reject;
71
+ const promise = new Promise((promiseResolve, promiseReject) => {
72
+ resolve = promiseResolve;
73
+ reject = promiseReject;
74
+ });
75
+ this._deferredResponse = {
76
+ promise,
77
+ resolve,
78
+ reject,
79
+ settled: false
80
+ };
81
+ if (signal !== void 0) {
82
+ const onAbort = () => void this.close();
83
+ signal.addEventListener("abort", onAbort, { once: true });
84
+ this._abortCleanup = () => signal.removeEventListener("abort", onAbort);
85
+ }
86
+ this._dispatchWindowOpen = true;
87
+ try {
88
+ this.onmessage(message, messageExtra);
89
+ } finally {
90
+ this._dispatchWindowOpen = false;
91
+ }
92
+ if (this._responseMode === "sse" && !this._closed && !this._deferredResponse.settled) this.upgradeToSse();
93
+ return promise;
211
94
  }
95
+ this.onmessage(message, messageExtra);
96
+ return new Response(null, { status: 202 });
212
97
  }
213
- /** {@inheritDoc TaskStore.listTasks} */
214
- async listTasks(cursor, sessionId) {
215
- const PAGE_SIZE = 10;
216
- const filteredTaskIds = [...this.tasks.entries()].filter(([, stored]) => {
217
- if (sessionId === void 0 || stored.sessionId === void 0) return true;
218
- return stored.sessionId === sessionId;
219
- }).map(([taskId]) => taskId);
220
- let startIndex = 0;
221
- if (cursor) {
222
- const cursorIndex = filteredTaskIds.indexOf(cursor);
223
- if (cursorIndex === -1) throw new Error(`Invalid cursor: ${cursor}`);
224
- else startIndex = cursorIndex + 1;
98
+ async send(message, options) {
99
+ if (this._closed) return;
100
+ const isResponse = isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message);
101
+ const relatedId = isResponse ? message.id : options?.relatedRequestId;
102
+ if (this._requestId === void 0 || relatedId === void 0 || relatedId !== this._requestId) {
103
+ if (isResponse) this.onerror?.(/* @__PURE__ */ new Error(`Received a response for an unknown request id: ${String(message.id)}`));
104
+ return;
225
105
  }
226
- const pageTaskIds = filteredTaskIds.slice(startIndex, startIndex + PAGE_SIZE);
227
- return {
228
- tasks: pageTaskIds.map((taskId) => {
229
- return { ...this.tasks.get(taskId).task };
230
- }),
231
- nextCursor: startIndex + PAGE_SIZE < filteredTaskIds.length ? pageTaskIds.at(-1) : void 0
232
- };
106
+ if (isResponse) {
107
+ if (this._terminalDelivered) return;
108
+ this._terminalDelivered = true;
109
+ const ladderStatus = this._dispatchWindowOpen && isJSONRPCErrorResponse(message) ? LADDER_ERROR_HTTP_STATUS[message.error.code] : void 0;
110
+ if (ladderStatus !== void 0 && this._sse === void 0) {
111
+ this.settleResponse(Response.json(message, {
112
+ status: ladderStatus,
113
+ headers: { "Content-Type": "application/json" }
114
+ }));
115
+ queueMicrotask(() => void this.close());
116
+ return;
117
+ }
118
+ if (this._sse !== void 0 || this._responseMode === "sse") {
119
+ if (this._sse === void 0) this.upgradeToSse();
120
+ this.writeMessageFrame(message);
121
+ this.finalizeStream();
122
+ return;
123
+ }
124
+ this.settleResponse(Response.json(message, {
125
+ status: 200,
126
+ headers: { "Content-Type": "application/json" }
127
+ }));
128
+ queueMicrotask(() => void this.close());
129
+ return;
130
+ }
131
+ if (this._responseMode === "json") return;
132
+ if (this._sse === void 0) this.upgradeToSse();
133
+ this.writeMessageFrame(message);
233
134
  }
234
135
  /**
235
- * Cleanup all timers (useful for testing or graceful shutdown)
136
+ * Writes an SSE comment frame (a keep-alive heartbeat). Dropped when the
137
+ * exchange is not currently streaming.
236
138
  */
237
- cleanup() {
238
- for (const timer of this.cleanupTimers.values()) clearTimeout(timer);
239
- this.cleanupTimers.clear();
240
- this.tasks.clear();
139
+ writeCommentFrame(comment) {
140
+ if (this._closed || this._sse === void 0 || this._sse.closed) return;
141
+ const frame = comment.split("\n").map((line) => `: ${line}`).join("\n");
142
+ this.writeFrame(`${frame}\n\n`);
241
143
  }
242
- /**
243
- * Get all tasks (useful for debugging)
244
- */
245
- getAllTasks() {
246
- return [...this.tasks.values()].map((stored) => ({ ...stored.task }));
144
+ async close() {
145
+ if (this._closed) return;
146
+ this._closed = true;
147
+ this._abortCleanup?.();
148
+ this._abortCleanup = void 0;
149
+ if (this._sse !== void 0 && !this._sse.closed) {
150
+ this._sse.closed = true;
151
+ try {
152
+ this._sse.controller.close();
153
+ } catch {}
154
+ }
155
+ if (this._deferredResponse !== void 0 && !this._deferredResponse.settled) {
156
+ this._deferredResponse.settled = true;
157
+ this._deferredResponse.reject(new SdkError(SdkErrorCode.ConnectionClosed, "Connection closed before a response was produced"));
158
+ }
159
+ this.onclose?.();
247
160
  }
248
- };
249
- /**
250
- * In-memory {@linkcode TaskMessageQueue} implementation for development and testing.
251
- * For production, use Redis or another distributed queue.
252
- * @experimental
253
- */
254
- var InMemoryTaskMessageQueue = class {
255
- queues = /* @__PURE__ */ new Map();
256
- /**
257
- * Generates a queue key from taskId.
258
- * SessionId is intentionally ignored because taskIds are globally unique
259
- * and tasks need to be accessible across HTTP requests/sessions.
260
- */
261
- getQueueKey(taskId, _sessionId) {
262
- return taskId;
161
+ settleResponse(response) {
162
+ if (this._deferredResponse === void 0 || this._deferredResponse.settled) return;
163
+ this._deferredResponse.settled = true;
164
+ this._deferredResponse.resolve(response);
263
165
  }
264
- /**
265
- * Gets or creates a queue for the given task and session.
266
- */
267
- getQueue(taskId, sessionId) {
268
- const key = this.getQueueKey(taskId, sessionId);
269
- let queue = this.queues.get(key);
270
- if (!queue) {
271
- queue = [];
272
- this.queues.set(key, queue);
273
- }
274
- return queue;
166
+ upgradeToSse() {
167
+ let controller;
168
+ const readable = new ReadableStream({
169
+ start: (streamController) => {
170
+ controller = streamController;
171
+ },
172
+ cancel: () => {
173
+ this.close();
174
+ }
175
+ });
176
+ this._sse = {
177
+ controller,
178
+ encoder: new TextEncoder(),
179
+ closed: false
180
+ };
181
+ this.settleResponse(new Response(readable, {
182
+ status: 200,
183
+ headers: {
184
+ "Content-Type": "text/event-stream",
185
+ "Cache-Control": "no-cache",
186
+ Connection: "keep-alive",
187
+ "X-Accel-Buffering": "no"
188
+ }
189
+ }));
275
190
  }
276
- /**
277
- * Adds a message to the end of the queue for a specific task.
278
- * Atomically checks queue size and throws if maxSize would be exceeded.
279
- * @param taskId The task identifier
280
- * @param message The message to enqueue
281
- * @param sessionId Optional session ID for binding the operation to a specific session
282
- * @param maxSize Optional maximum queue size - if specified and queue is full, throws an error
283
- * @throws Error if maxSize is specified and would be exceeded
284
- */
285
- async enqueue(taskId, message, sessionId, maxSize) {
286
- const queue = this.getQueue(taskId, sessionId);
287
- if (maxSize !== void 0 && queue.length >= maxSize) throw new Error(`Task message queue overflow: queue size (${queue.length}) exceeds maximum (${maxSize})`);
288
- queue.push(message);
191
+ finalizeStream() {
192
+ if (this._sse !== void 0 && !this._sse.closed) {
193
+ this._sse.closed = true;
194
+ try {
195
+ this._sse.controller.close();
196
+ } catch {}
197
+ }
198
+ queueMicrotask(() => void this.close());
289
199
  }
290
- /**
291
- * Removes and returns the first message from the queue for a specific task.
292
- * @param taskId The task identifier
293
- * @param sessionId Optional session ID for binding the query to a specific session
294
- * @returns The first message, or `undefined` if the queue is empty
295
- */
296
- async dequeue(taskId, sessionId) {
297
- return this.getQueue(taskId, sessionId).shift();
200
+ writeMessageFrame(message) {
201
+ this.writeFrame(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
298
202
  }
299
- /**
300
- * Removes and returns all messages from the queue for a specific task.
301
- * @param taskId The task identifier
302
- * @param sessionId Optional session ID for binding the query to a specific session
303
- * @returns Array of all messages that were in the queue
304
- */
305
- async dequeueAll(taskId, sessionId) {
306
- const key = this.getQueueKey(taskId, sessionId);
307
- const queue = this.queues.get(key) ?? [];
308
- this.queues.delete(key);
309
- return queue;
203
+ writeFrame(frame) {
204
+ if (this._sse === void 0 || this._sse.closed) return;
205
+ try {
206
+ this._sse.controller.enqueue(this._sse.encoder.encode(frame));
207
+ } catch (error) {
208
+ this.onerror?.(/* @__PURE__ */ new Error(`Failed to write to the response stream: ${error}`));
209
+ }
310
210
  }
311
211
  };
312
212
 
313
213
  //#endregion
314
- //#region src/experimental/tasks/mcpServer.ts
214
+ //#region src/server/invoke.ts
315
215
  /**
316
- * Experimental task features for {@linkcode McpServer}.
317
- *
318
- * Access via `server.experimental.tasks`:
319
- * ```typescript
320
- * server.experimental.tasks.registerToolTask('long-running', config, handler);
321
- * ```
216
+ * Serves one classified inbound message on the given server instance and
217
+ * returns the HTTP response for the exchange.
322
218
  *
323
- * @experimental
219
+ * The instance is connected to a fresh single-exchange transport, the message
220
+ * is injected through the normal transport message path, and whatever the
221
+ * dispatch layer produces (the handler result, a protocol-level rejection, or
222
+ * streamed related messages followed by the result) is captured as the
223
+ * returned `Response`. For request exchanges, teardown rides the transport's
224
+ * close chain once the terminal response has been delivered; notification
225
+ * exchanges resolve with the 202 response immediately and do NOT run the
226
+ * close chain — the transport stays connected until the caller closes it or
227
+ * drops the per-request instance, which is the caller's choice either way.
324
228
  */
325
- var ExperimentalMcpServerTasks = class {
326
- constructor(_mcpServer) {
327
- this._mcpServer = _mcpServer;
328
- }
329
- registerToolTask(name, config, handler) {
330
- const execution = {
331
- taskSupport: "required",
332
- ...config.execution
333
- };
334
- if (execution.taskSupport === "forbidden") throw new Error(`Cannot register task-based tool '${name}' with taskSupport 'forbidden'. Use registerTool() instead.`);
335
- return this._mcpServer._createRegisteredTool(name, config.title, config.description, config.inputSchema, config.outputSchema, config.annotations, execution, config._meta, handler);
336
- }
337
- };
229
+ async function invoke(server, message, ctx) {
230
+ const transport = new PerRequestHTTPServerTransport({
231
+ classification: ctx.classification,
232
+ ...ctx.responseMode !== void 0 && { responseMode: ctx.responseMode }
233
+ });
234
+ await server.connect(transport);
235
+ return transport.handleMessage(message, {
236
+ ...ctx.request !== void 0 && { request: ctx.request },
237
+ ...ctx.authInfo !== void 0 && { authInfo: ctx.authInfo }
238
+ });
239
+ }
338
240
 
339
241
  //#endregion
340
- //#region src/experimental/tasks/server.ts
242
+ //#region src/server/streamableHttp.ts
341
243
  /**
342
- * Experimental task features for low-level MCP servers.
244
+ * Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification
245
+ * using Web Standard APIs (`Request`, `Response`, `ReadableStream`).
246
+ *
247
+ * This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
248
+ *
249
+ * In stateful mode:
250
+ * - Session ID is generated and included in response headers
251
+ * - Session ID is always included in initialization responses
252
+ * - Requests with invalid session IDs are rejected with `404 Not Found`
253
+ * - Non-initialization requests without a session ID are rejected with `400 Bad Request`
254
+ * - State is maintained in-memory (connections, message history)
255
+ *
256
+ * In stateless mode:
257
+ * - No Session ID is included in any responses
258
+ * - No session validation is performed
343
259
  *
344
- * Access via `server.experimental.tasks`:
345
- * ```typescript
346
- * const stream = server.experimental.tasks.requestStream(request, options);
260
+ * @example Stateful setup
261
+ * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateful"
262
+ * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
263
+ *
264
+ * const transport = new WebStandardStreamableHTTPServerTransport({
265
+ * sessionIdGenerator: () => crypto.randomUUID()
266
+ * });
267
+ *
268
+ * await server.connect(transport);
269
+ * ```
270
+ *
271
+ * @example Stateless setup
272
+ * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateless"
273
+ * const transport = new WebStandardStreamableHTTPServerTransport({
274
+ * sessionIdGenerator: undefined
275
+ * });
347
276
  * ```
348
277
  *
349
- * For high-level server usage with task-based tools, use {@linkcode index.McpServer | McpServer}.experimental.tasks instead.
278
+ * @example Hono.js
279
+ * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_hono"
280
+ * app.all('/mcp', async c => {
281
+ * return transport.handleRequest(c.req.raw);
282
+ * });
283
+ * ```
350
284
  *
351
- * @experimental
285
+ * @example Cloudflare Workers
286
+ * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_workers"
287
+ * const worker = {
288
+ * async fetch(request: Request): Promise<Response> {
289
+ * return transport.handleRequest(request);
290
+ * }
291
+ * };
292
+ * ```
352
293
  */
353
- var ExperimentalServerTasks = class {
354
- constructor(_server) {
355
- this._server = _server;
294
+ var WebStandardStreamableHTTPServerTransport = class {
295
+ sessionIdGenerator;
296
+ _started = false;
297
+ _closed = false;
298
+ _streamMapping = /* @__PURE__ */ new Map();
299
+ _requestToStreamMapping = /* @__PURE__ */ new Map();
300
+ _requestResponseMap = /* @__PURE__ */ new Map();
301
+ _initialized = false;
302
+ _enableJsonResponse = false;
303
+ _standaloneSseStreamId = "_GET_stream";
304
+ _eventStore;
305
+ _onsessioninitialized;
306
+ _onsessionclosed;
307
+ _allowedHosts;
308
+ _allowedOrigins;
309
+ _enableDnsRebindingProtection;
310
+ _retryInterval;
311
+ _supportedProtocolVersions;
312
+ sessionId;
313
+ onclose;
314
+ onerror;
315
+ onmessage;
316
+ constructor(options = {}) {
317
+ this.sessionIdGenerator = options.sessionIdGenerator;
318
+ this._enableJsonResponse = options.enableJsonResponse ?? false;
319
+ this._eventStore = options.eventStore;
320
+ this._onsessioninitialized = options.onsessioninitialized;
321
+ this._onsessionclosed = options.onsessionclosed;
322
+ this._allowedHosts = options.allowedHosts;
323
+ this._allowedOrigins = options.allowedOrigins;
324
+ this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
325
+ this._retryInterval = options.retryInterval;
326
+ this._supportedProtocolVersions = options.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS;
356
327
  }
357
- get _module() {
358
- return this._server.taskManager;
328
+ /**
329
+ * Starts the transport. This is required by the {@linkcode Transport} interface but is a no-op
330
+ * for the Streamable HTTP transport as connections are managed per-request.
331
+ */
332
+ async start() {
333
+ if (this._started) throw new Error("Transport already started");
334
+ this._started = true;
359
335
  }
360
336
  /**
361
- * Sends a request and returns an AsyncGenerator that yields response messages.
362
- * The generator is guaranteed to end with either a `'result'` or `'error'` message.
363
- *
364
- * This method provides streaming access to request processing, allowing you to
365
- * observe intermediate task status updates for task-augmented requests.
366
- *
367
- * @param request - The request to send (method name determines the result schema)
368
- * @param options - Optional request options (timeout, signal, task creation params, etc.)
369
- * @returns AsyncGenerator that yields {@linkcode ResponseMessage} objects
370
- *
371
- * @experimental
337
+ * Sets the supported protocol versions for header validation.
338
+ * Called by the server during {@linkcode server/server.Server.connect | connect()} to pass its supported versions.
372
339
  */
373
- requestStream(request, options) {
374
- const resultSchema = getResultSchema(request.method);
375
- return this._module.requestStream(request, resultSchema, options);
340
+ setSupportedProtocolVersions(versions) {
341
+ this._supportedProtocolVersions = versions;
376
342
  }
377
343
  /**
378
- * Sends a sampling request and returns an AsyncGenerator that yields response messages.
379
- * The generator is guaranteed to end with either a 'result' or 'error' message.
380
- *
381
- * For task-augmented requests, yields 'taskCreated' and 'taskStatus' messages
382
- * before the final result.
383
- *
384
- * @example
385
- * ```typescript
386
- * const stream = server.experimental.tasks.createMessageStream({
387
- * messages: [{ role: 'user', content: { type: 'text', text: 'Hello' } }],
388
- * maxTokens: 100
389
- * }, {
390
- * onprogress: (progress) => {
391
- * // Handle streaming tokens via progress notifications
392
- * console.log('Progress:', progress.message);
393
- * }
394
- * });
395
- *
396
- * for await (const message of stream) {
397
- * switch (message.type) {
398
- * case 'taskCreated':
399
- * console.log('Task created:', message.task.taskId);
400
- * break;
401
- * case 'taskStatus':
402
- * console.log('Task status:', message.task.status);
403
- * break;
404
- * case 'result':
405
- * console.log('Final result:', message.result);
406
- * break;
407
- * case 'error':
408
- * console.error('Error:', message.error);
409
- * break;
410
- * }
411
- * }
412
- * ```
413
- *
414
- * @param params - The sampling request parameters
415
- * @param options - Optional request options (timeout, signal, task creation params, onprogress, etc.)
416
- * @returns AsyncGenerator that yields ResponseMessage objects
417
- *
418
- * @experimental
344
+ * Helper to create a JSON error response
419
345
  */
420
- createMessageStream(params, options) {
421
- const clientCapabilities = this._server.getClientCapabilities();
422
- if ((params.tools || params.toolChoice) && !clientCapabilities?.sampling?.tools) throw new SdkError(SdkErrorCode.CapabilityNotSupported, "Client does not support sampling tools capability.");
423
- if (params.messages.length > 0) {
424
- const lastMessage = params.messages.at(-1);
425
- const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
426
- const hasToolResults = lastContent.some((c) => c.type === "tool_result");
427
- const previousMessage = params.messages.length > 1 ? params.messages.at(-2) : void 0;
428
- const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
429
- const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
430
- if (hasToolResults) {
431
- if (lastContent.some((c) => c.type !== "tool_result")) throw new Error("The last message must contain only tool_result content if any is present");
432
- if (!hasPreviousToolUse) throw new Error("tool_result blocks are not matching any tool_use from the previous message");
433
- }
434
- if (hasPreviousToolUse) {
435
- const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
436
- const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
437
- if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) throw new Error("ids of tool_result blocks and tool_use blocks from previous message do not match");
346
+ createJsonErrorResponse(status, code, message, options) {
347
+ const error = {
348
+ code,
349
+ message
350
+ };
351
+ if (options?.data !== void 0) error.data = options.data;
352
+ return Response.json({
353
+ jsonrpc: "2.0",
354
+ error,
355
+ id: null
356
+ }, {
357
+ status,
358
+ headers: {
359
+ "Content-Type": "application/json",
360
+ ...options?.headers
438
361
  }
439
- }
440
- return this.requestStream({
441
- method: "sampling/createMessage",
442
- params
443
- }, options);
362
+ });
444
363
  }
445
364
  /**
446
- * Sends an elicitation request and returns an AsyncGenerator that yields response messages.
447
- * The generator is guaranteed to end with either a 'result' or 'error' message.
448
- *
449
- * For task-augmented requests (especially URL-based elicitation), yields 'taskCreated'
450
- * and 'taskStatus' messages before the final result.
451
- *
452
- * @example
453
- * ```typescript
454
- * const stream = server.experimental.tasks.elicitInputStream({
455
- * mode: 'url',
456
- * message: 'Please authenticate',
457
- * elicitationId: 'auth-123',
458
- * url: 'https://example.com/auth'
459
- * }, {
460
- * task: { ttl: 300000 } // Task-augmented for long-running auth flow
461
- * });
462
- *
463
- * for await (const message of stream) {
464
- * switch (message.type) {
465
- * case 'taskCreated':
466
- * console.log('Task created:', message.task.taskId);
467
- * break;
468
- * case 'taskStatus':
469
- * console.log('Task status:', message.task.status);
470
- * break;
471
- * case 'result':
472
- * console.log('User action:', message.result.action);
473
- * break;
474
- * case 'error':
475
- * console.error('Error:', message.error);
476
- * break;
477
- * }
478
- * }
479
- * ```
480
- *
481
- * @param params - The elicitation request parameters
482
- * @param options - Optional request options (timeout, signal, task creation params, etc.)
483
- * @returns AsyncGenerator that yields ResponseMessage objects
484
- *
485
- * @experimental
365
+ * Validates request headers for DNS rebinding protection.
366
+ * @returns Error response if validation fails, `undefined` if validation passes.
486
367
  */
487
- elicitInputStream(params, options) {
488
- const clientCapabilities = this._server.getClientCapabilities();
489
- const mode = params.mode ?? "form";
490
- switch (mode) {
491
- case "url":
492
- if (!clientCapabilities?.elicitation?.url) throw new SdkError(SdkErrorCode.CapabilityNotSupported, "Client does not support url elicitation.");
493
- break;
494
- case "form":
495
- if (!clientCapabilities?.elicitation?.form) throw new SdkError(SdkErrorCode.CapabilityNotSupported, "Client does not support form elicitation.");
496
- break;
368
+ validateRequestHeaders(req) {
369
+ if (!this._enableDnsRebindingProtection) return;
370
+ if (this._allowedHosts && this._allowedHosts.length > 0) {
371
+ const hostHeader = req.headers.get("host");
372
+ if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
373
+ const error = `Invalid Host header: ${hostHeader}`;
374
+ this.onerror?.(new Error(error));
375
+ return this.createJsonErrorResponse(403, -32e3, error);
376
+ }
377
+ }
378
+ if (this._allowedOrigins && this._allowedOrigins.length > 0) {
379
+ const originHeader = req.headers.get("origin");
380
+ if (originHeader && !this._allowedOrigins.includes(originHeader)) {
381
+ const error = `Invalid Origin header: ${originHeader}`;
382
+ this.onerror?.(new Error(error));
383
+ return this.createJsonErrorResponse(403, -32e3, error);
384
+ }
497
385
  }
498
- const normalizedParams = mode === "form" && params.mode !== "form" ? {
499
- ...params,
500
- mode: "form"
501
- } : params;
502
- return this.requestStream({
503
- method: "elicitation/create",
504
- params: normalizedParams
505
- }, options);
506
386
  }
507
387
  /**
508
- * Gets the current status of a task.
509
- *
510
- * @param taskId - The task identifier
511
- * @param options - Optional request options
512
- * @returns The task status
513
- *
514
- * @experimental
515
- */
516
- async getTask(taskId, options) {
517
- return this._module.getTask({ taskId }, options);
518
- }
519
- /**
520
- * Retrieves the result of a completed task.
521
- *
522
- * @param taskId - The task identifier
523
- * @param options - Optional request options
524
- * @returns The task result. The payload structure matches the result type of the
525
- * original request (e.g., a `tools/call` task returns a `CallToolResult`).
526
- *
527
- * @experimental
528
- */
529
- async getTaskResult(taskId, options) {
530
- return this._module.getTaskResult({ taskId }, GetTaskPayloadResultSchema, options);
531
- }
532
- /**
533
- * Lists tasks with optional pagination.
534
- *
535
- * @param cursor - Optional pagination cursor
536
- * @param options - Optional request options
537
- * @returns List of tasks with optional next cursor
538
- *
539
- * @experimental
540
- */
541
- async listTasks(cursor, options) {
542
- return this._module.listTasks(cursor ? { cursor } : void 0, options);
543
- }
544
- /**
545
- * Cancels a running task.
546
- *
547
- * @param taskId - The task identifier
548
- * @param options - Optional request options
549
- *
550
- * @experimental
551
- */
552
- async cancelTask(taskId, options) {
553
- return this._module.cancelTask({ taskId }, options);
554
- }
555
- };
556
-
557
- //#endregion
558
- //#region src/server/server.ts
559
- /**
560
- * An MCP server on top of a pluggable transport.
561
- *
562
- * This server will automatically respond to the initialization flow as initiated from the client.
563
- *
564
- * @deprecated Use {@linkcode server/mcp.McpServer | McpServer} instead for the high-level API. Only use `Server` for advanced use cases.
565
- */
566
- var Server = class extends Protocol {
567
- _clientCapabilities;
568
- _clientVersion;
569
- _capabilities;
570
- _instructions;
571
- _jsonSchemaValidator;
572
- _experimental;
573
- /**
574
- * Callback for when initialization has fully completed (i.e., the client has sent an `notifications/initialized` notification).
575
- */
576
- oninitialized;
577
- /**
578
- * Initializes this server with the given name and version information.
388
+ * Handles an incoming HTTP request, whether `GET`, `POST`, or `DELETE`
389
+ * Returns a `Response` object (Web Standard)
579
390
  */
580
- constructor(_serverInfo, options) {
581
- super({
582
- ...options,
583
- tasks: extractTaskManagerOptions(options?.capabilities?.tasks)
584
- });
585
- this._serverInfo = _serverInfo;
586
- this._capabilities = options?.capabilities ? { ...options.capabilities } : {};
587
- this._instructions = options?.instructions;
588
- this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new DefaultJsonSchemaValidator();
589
- if (options?.capabilities?.tasks) {
590
- const { taskStore, taskMessageQueue, defaultTaskPollInterval, maxTaskQueueSize, ...wireCapabilities } = options.capabilities.tasks;
591
- this._capabilities.tasks = wireCapabilities;
391
+ async handleRequest(req, options) {
392
+ const validationError = this.validateRequestHeaders(req);
393
+ if (validationError) return validationError;
394
+ switch (req.method) {
395
+ case "POST": return this.handlePostRequest(req, options);
396
+ case "GET": return this.handleGetRequest(req);
397
+ case "DELETE": return this.handleDeleteRequest(req);
398
+ default: return this.handleUnsupportedRequest();
592
399
  }
593
- this.setRequestHandler("initialize", (request) => this._oninitialize(request));
594
- this.setNotificationHandler("notifications/initialized", () => this.oninitialized?.());
595
- if (this._capabilities.logging) this._registerLoggingHandler();
596
- }
597
- _registerLoggingHandler() {
598
- this.setRequestHandler("logging/setLevel", async (request, ctx) => {
599
- const transportSessionId = ctx.sessionId || ctx.http?.req?.headers.get("mcp-session-id") || void 0;
600
- const { level } = request.params;
601
- const parseResult = parseSchema(LoggingLevelSchema, level);
602
- if (parseResult.success) this._loggingLevels.set(transportSessionId, parseResult.data);
603
- return {};
604
- });
605
- }
606
- buildContext(ctx, transportInfo) {
607
- const hasHttpInfo = ctx.http || transportInfo?.request || transportInfo?.closeSSEStream || transportInfo?.closeStandaloneSSEStream;
608
- return {
609
- ...ctx,
610
- mcpReq: {
611
- ...ctx.mcpReq,
612
- log: (level, data, logger) => this.sendLoggingMessage({
613
- level,
614
- data,
615
- logger
616
- }),
617
- elicitInput: (params, options) => this.elicitInput(params, options),
618
- requestSampling: (params, options) => this.createMessage(params, options)
619
- },
620
- http: hasHttpInfo ? {
621
- ...ctx.http,
622
- req: transportInfo?.request,
623
- closeSSE: transportInfo?.closeSSEStream,
624
- closeStandaloneSSE: transportInfo?.closeStandaloneSSEStream
625
- } : void 0
626
- };
627
400
  }
628
401
  /**
629
- * Access experimental features.
630
- *
631
- * WARNING: These APIs are experimental and may change without notice.
402
+ * Returns true if the client's protocol version supports empty SSE data in
403
+ * priming events (the fix shipped with protocol version `2025-11-25`).
632
404
  *
633
- * @experimental
405
+ * The version is checked for membership in this transport instance's
406
+ * supported protocol versions rather than with an open-ended
407
+ * `>= '2025-11-25'` comparison: the value may come from an `initialize`
408
+ * request body, which (unlike the `MCP-Protocol-Version` header) is not
409
+ * validated against `supportedProtocolVersions` before reaching this
410
+ * check. An unknown future version string must not silently enable
411
+ * behavior reserved for versions this transport actually supports.
634
412
  */
635
- get experimental() {
636
- if (!this._experimental) this._experimental = { tasks: new ExperimentalServerTasks(this) };
637
- return this._experimental;
413
+ supportsEmptySSEData(protocolVersion) {
414
+ return this._supportedProtocolVersions.includes(protocolVersion) && protocolVersion >= "2025-11-25";
638
415
  }
639
- _loggingLevels = /* @__PURE__ */ new Map();
640
- LOG_LEVEL_SEVERITY = new Map(LoggingLevelSchema.options.map((level, index) => [level, index]));
641
- isMessageIgnored = (level, sessionId) => {
642
- const currentLevel = this._loggingLevels.get(sessionId);
643
- return currentLevel ? this.LOG_LEVEL_SEVERITY.get(level) < this.LOG_LEVEL_SEVERITY.get(currentLevel) : false;
644
- };
645
416
  /**
646
- * Registers new capabilities. This can only be called before connecting to a transport.
647
- *
648
- * The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization).
417
+ * Writes a priming event to establish resumption capability.
418
+ * Only sends if `eventStore` is configured (opt-in for resumability) and
419
+ * the client's protocol version supports empty SSE data (a supported
420
+ * version that is >= `2025-11-25`).
649
421
  */
650
- registerCapabilities(capabilities) {
651
- if (this.transport) throw new SdkError(SdkErrorCode.AlreadyConnected, "Cannot register capabilities after connecting to transport");
652
- const hadLogging = !!this._capabilities.logging;
653
- this._capabilities = mergeCapabilities(this._capabilities, capabilities);
654
- if (!hadLogging && this._capabilities.logging) this._registerLoggingHandler();
422
+ async writePrimingEvent(controller, encoder, streamId, protocolVersion) {
423
+ if (!this._eventStore) return;
424
+ if (!this.supportsEmptySSEData(protocolVersion)) return;
425
+ const primingEventId = await this._eventStore.storeEvent(streamId, {});
426
+ let primingEvent = `id: ${primingEventId}\ndata: \n\n`;
427
+ if (this._retryInterval !== void 0) primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`;
428
+ controller.enqueue(encoder.encode(primingEvent));
655
429
  }
656
430
  /**
657
- * Override request handler registration to enforce server-side validation for `tools/call`.
431
+ * Handles `GET` requests for SSE stream
658
432
  */
659
- setRequestHandler(method, handler) {
660
- if (method === "tools/call") {
661
- const wrappedHandler = async (request, ctx) => {
662
- const validatedRequest = parseSchema(CallToolRequestSchema, request);
663
- if (!validatedRequest.success) {
664
- const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
665
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid tools/call request: ${errorMessage}`);
666
- }
667
- const { params } = validatedRequest.data;
668
- const result = await Promise.resolve(handler(request, ctx));
669
- if (params.task) {
670
- const taskValidationResult = parseSchema(CreateTaskResultSchema, result);
671
- if (!taskValidationResult.success) {
672
- const errorMessage = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
673
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
674
- }
675
- return taskValidationResult.data;
676
- }
677
- const validationResult = parseSchema(CallToolResultSchema, result);
678
- if (!validationResult.success) {
679
- const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
680
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid tools/call result: ${errorMessage}`);
681
- }
682
- return validationResult.data;
683
- };
684
- return super.setRequestHandler(method, wrappedHandler);
685
- }
686
- return super.setRequestHandler(method, handler);
687
- }
688
- assertCapabilityForMethod(method) {
689
- switch (method) {
690
- case "sampling/createMessage":
691
- if (!this._clientCapabilities?.sampling) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support sampling (required for ${method})`);
692
- break;
693
- case "elicitation/create":
694
- if (!this._clientCapabilities?.elicitation) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support elicitation (required for ${method})`);
695
- break;
696
- case "roots/list":
697
- if (!this._clientCapabilities?.roots) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support listing roots (required for ${method})`);
698
- break;
699
- case "ping": break;
433
+ async handleGetRequest(req) {
434
+ if (!req.headers.get("accept")?.includes("text/event-stream")) {
435
+ this.onerror?.(/* @__PURE__ */ new Error("Not Acceptable: Client must accept text/event-stream"));
436
+ return this.createJsonErrorResponse(406, -32e3, "Not Acceptable: Client must accept text/event-stream");
700
437
  }
701
- }
702
- assertNotificationCapability(method) {
703
- switch (method) {
704
- case "notifications/message":
705
- if (!this._capabilities.logging) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support logging (required for ${method})`);
706
- break;
707
- case "notifications/resources/updated":
708
- case "notifications/resources/list_changed":
709
- if (!this._capabilities.resources) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support notifying about resources (required for ${method})`);
710
- break;
711
- case "notifications/tools/list_changed":
712
- if (!this._capabilities.tools) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support notifying of tool list changes (required for ${method})`);
713
- break;
714
- case "notifications/prompts/list_changed":
715
- if (!this._capabilities.prompts) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support notifying of prompt list changes (required for ${method})`);
716
- break;
717
- case "notifications/elicitation/complete":
718
- if (!this._clientCapabilities?.elicitation?.url) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Client does not support URL elicitation (required for ${method})`);
719
- break;
720
- case "notifications/cancelled": break;
721
- case "notifications/progress": break;
438
+ const sessionError = this.validateSession(req);
439
+ if (sessionError) return sessionError;
440
+ const protocolError = this.validateProtocolVersion(req);
441
+ if (protocolError) return protocolError;
442
+ if (this._eventStore) {
443
+ const lastEventId = req.headers.get("last-event-id");
444
+ if (lastEventId) return this.replayEvents(lastEventId);
722
445
  }
723
- }
724
- assertRequestHandlerCapability(method) {
725
- switch (method) {
726
- case "completion/complete":
727
- if (!this._capabilities.completions) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support completions (required for ${method})`);
728
- break;
729
- case "logging/setLevel":
730
- if (!this._capabilities.logging) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support logging (required for ${method})`);
731
- break;
732
- case "prompts/get":
733
- case "prompts/list":
734
- if (!this._capabilities.prompts) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support prompts (required for ${method})`);
735
- break;
736
- case "resources/list":
737
- case "resources/templates/list":
738
- case "resources/read":
739
- if (!this._capabilities.resources) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support resources (required for ${method})`);
740
- break;
741
- case "tools/call":
742
- case "tools/list":
743
- if (!this._capabilities.tools) throw new SdkError(SdkErrorCode.CapabilityNotSupported, `Server does not support tools (required for ${method})`);
744
- break;
745
- case "ping":
746
- case "initialize": break;
446
+ if (this._streamMapping.get(this._standaloneSseStreamId) !== void 0) {
447
+ this.onerror?.(/* @__PURE__ */ new Error("Conflict: Only one SSE stream is allowed per session"));
448
+ return this.createJsonErrorResponse(409, -32e3, "Conflict: Only one SSE stream is allowed per session");
747
449
  }
748
- }
749
- assertTaskCapability(method) {
750
- assertClientRequestTaskCapability(this._clientCapabilities?.tasks?.requests, method, "Client");
751
- }
752
- assertTaskHandlerCapability(method) {
753
- assertToolsCallTaskCapability(this._capabilities?.tasks?.requests, method, "Server");
754
- }
755
- async _oninitialize(request) {
756
- const requestedVersion = request.params.protocolVersion;
757
- this._clientCapabilities = request.params.capabilities;
758
- this._clientVersion = request.params.clientInfo;
759
- const protocolVersion = this._supportedProtocolVersions.includes(requestedVersion) ? requestedVersion : this._supportedProtocolVersions[0] ?? LATEST_PROTOCOL_VERSION;
760
- this.transport?.setProtocolVersion?.(protocolVersion);
761
- return {
762
- protocolVersion,
763
- capabilities: this.getCapabilities(),
764
- serverInfo: this._serverInfo,
765
- ...this._instructions && { instructions: this._instructions }
766
- };
767
- }
768
- /**
769
- * After initialization has completed, this will be populated with the client's reported capabilities.
770
- */
771
- getClientCapabilities() {
772
- return this._clientCapabilities;
773
- }
774
- /**
775
- * After initialization has completed, this will be populated with information about the client's name and version.
776
- */
777
- getClientVersion() {
778
- return this._clientVersion;
779
- }
780
- /**
781
- * Returns the current server capabilities.
782
- */
783
- getCapabilities() {
784
- return this._capabilities;
785
- }
786
- async ping() {
787
- return this._requestWithSchema({ method: "ping" }, EmptyResultSchema);
788
- }
789
- async createMessage(params, options) {
790
- if ((params.tools || params.toolChoice) && !this._clientCapabilities?.sampling?.tools) throw new SdkError(SdkErrorCode.CapabilityNotSupported, "Client does not support sampling tools capability.");
791
- if (params.messages.length > 0) {
792
- const lastMessage = params.messages.at(-1);
793
- const lastContent = Array.isArray(lastMessage.content) ? lastMessage.content : [lastMessage.content];
794
- const hasToolResults = lastContent.some((c) => c.type === "tool_result");
795
- const previousMessage = params.messages.length > 1 ? params.messages.at(-2) : void 0;
796
- const previousContent = previousMessage ? Array.isArray(previousMessage.content) ? previousMessage.content : [previousMessage.content] : [];
797
- const hasPreviousToolUse = previousContent.some((c) => c.type === "tool_use");
798
- if (hasToolResults) {
799
- if (lastContent.some((c) => c.type !== "tool_result")) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "The last message must contain only tool_result content if any is present");
800
- if (!hasPreviousToolUse) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "tool_result blocks are not matching any tool_use from the previous message");
450
+ const encoder = new TextEncoder();
451
+ let streamController;
452
+ const readable = new ReadableStream({
453
+ start: (controller) => {
454
+ streamController = controller;
455
+ },
456
+ cancel: () => {
457
+ if (this._streamMapping.get(this._standaloneSseStreamId)?.controller === streamController) this._streamMapping.delete(this._standaloneSseStreamId);
801
458
  }
802
- if (hasPreviousToolUse) {
803
- const toolUseIds = new Set(previousContent.filter((c) => c.type === "tool_use").map((c) => c.id));
804
- const toolResultIds = new Set(lastContent.filter((c) => c.type === "tool_result").map((c) => c.toolUseId));
805
- if (toolUseIds.size !== toolResultIds.size || ![...toolUseIds].every((id) => toolResultIds.has(id))) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "ids of tool_result blocks and tool_use blocks from previous message do not match");
459
+ });
460
+ const headers = {
461
+ "Content-Type": "text/event-stream",
462
+ "Cache-Control": "no-cache, no-transform",
463
+ Connection: "keep-alive"
464
+ };
465
+ if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
466
+ this._streamMapping.set(this._standaloneSseStreamId, {
467
+ controller: streamController,
468
+ encoder,
469
+ cleanup: () => {
470
+ this._streamMapping.delete(this._standaloneSseStreamId);
471
+ try {
472
+ streamController.close();
473
+ } catch {}
806
474
  }
807
- }
808
- if (params.tools) return this._requestWithSchema({
809
- method: "sampling/createMessage",
810
- params
811
- }, CreateMessageResultWithToolsSchema, options);
812
- return this._requestWithSchema({
813
- method: "sampling/createMessage",
814
- params
815
- }, CreateMessageResultSchema, options);
475
+ });
476
+ return new Response(readable, { headers });
816
477
  }
817
478
  /**
818
- * Creates an elicitation request for the given parameters.
819
- * For backwards compatibility, `mode` may be omitted for form requests and will default to `"form"`.
820
- * @param params The parameters for the elicitation request.
821
- * @param options Optional request options.
822
- * @returns The result of the elicitation request.
479
+ * Replays events that would have been sent after the specified event ID
480
+ * Only used when resumability is enabled
823
481
  */
824
- async elicitInput(params, options) {
825
- switch (params.mode ?? "form") {
826
- case "url": {
827
- if (!this._clientCapabilities?.elicitation?.url) throw new SdkError(SdkErrorCode.CapabilityNotSupported, "Client does not support url elicitation.");
828
- const urlParams = params;
829
- return this._requestWithSchema({
830
- method: "elicitation/create",
831
- params: urlParams
832
- }, ElicitResultSchema, options);
482
+ async replayEvents(lastEventId) {
483
+ if (!this._eventStore) {
484
+ this.onerror?.(/* @__PURE__ */ new Error("Event store not configured"));
485
+ return this.createJsonErrorResponse(400, -32e3, "Event store not configured");
486
+ }
487
+ try {
488
+ let streamId;
489
+ if (this._eventStore.getStreamIdForEventId) {
490
+ streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
491
+ if (!streamId) {
492
+ this.onerror?.(/* @__PURE__ */ new Error("Invalid event ID format"));
493
+ return this.createJsonErrorResponse(400, -32e3, "Invalid event ID format");
494
+ }
495
+ if (this._streamMapping.get(streamId) !== void 0) {
496
+ this.onerror?.(/* @__PURE__ */ new Error("Conflict: Stream already has an active connection"));
497
+ return this.createJsonErrorResponse(409, -32e3, "Conflict: Stream already has an active connection");
498
+ }
833
499
  }
834
- case "form": {
835
- if (!this._clientCapabilities?.elicitation?.form) throw new SdkError(SdkErrorCode.CapabilityNotSupported, "Client does not support form elicitation.");
836
- const formParams = params.mode === "form" ? params : {
837
- ...params,
838
- mode: "form"
839
- };
840
- const result = await this._requestWithSchema({
841
- method: "elicitation/create",
842
- params: formParams
843
- }, ElicitResultSchema, options);
844
- if (result.action === "accept" && result.content && formParams.requestedSchema) try {
845
- const validationResult = this._jsonSchemaValidator.getValidator(formParams.requestedSchema)(result.content);
846
- if (!validationResult.valid) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Elicitation response content does not match requested schema: ${validationResult.errorMessage}`);
847
- } catch (error) {
848
- if (error instanceof ProtocolError) throw error;
849
- throw new ProtocolError(ProtocolErrorCode.InternalError, `Error validating elicitation response: ${error instanceof Error ? error.message : String(error)}`);
500
+ const headers = {
501
+ "Content-Type": "text/event-stream",
502
+ "Cache-Control": "no-cache, no-transform",
503
+ Connection: "keep-alive"
504
+ };
505
+ if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
506
+ const encoder = new TextEncoder();
507
+ let streamController;
508
+ let replayedStreamId;
509
+ const readable = new ReadableStream({
510
+ start: (controller) => {
511
+ streamController = controller;
512
+ },
513
+ cancel: () => {
514
+ if (replayedStreamId !== void 0 && this._streamMapping.get(replayedStreamId)?.controller === streamController) this._streamMapping.delete(replayedStreamId);
515
+ }
516
+ });
517
+ const replayedEventIds = /* @__PURE__ */ new Set();
518
+ replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, { send: async (eventId, message) => {
519
+ replayedEventIds.add(eventId);
520
+ if (!this.writeSSEEvent(streamController, encoder, message, eventId)) try {
521
+ streamController.close();
522
+ } catch {}
523
+ } });
524
+ this._streamMapping.set(replayedStreamId, {
525
+ controller: streamController,
526
+ encoder,
527
+ replayedEventIds,
528
+ cleanup: () => {
529
+ this._streamMapping.delete(replayedStreamId);
530
+ try {
531
+ streamController.close();
532
+ } catch {}
533
+ }
534
+ });
535
+ if (replayedStreamId !== this._standaloneSseStreamId) {
536
+ if (![...this._requestToStreamMapping.values()].includes(replayedStreamId)) {
537
+ this._streamMapping.delete(replayedStreamId);
538
+ try {
539
+ streamController.close();
540
+ } catch {}
850
541
  }
851
- return result;
852
542
  }
543
+ return new Response(readable, { headers });
544
+ } catch (error) {
545
+ this.onerror?.(error);
546
+ return this.createJsonErrorResponse(500, -32e3, "Error replaying events");
853
547
  }
854
548
  }
855
549
  /**
856
- * Creates a reusable callback that, when invoked, will send a `notifications/elicitation/complete`
857
- * notification for the specified elicitation ID.
858
- *
859
- * @param elicitationId The ID of the elicitation to mark as complete.
860
- * @param options Optional notification options. Useful when the completion notification should be related to a prior request.
861
- * @returns A function that emits the completion notification when awaited.
550
+ * Writes an event to an SSE stream via controller with proper formatting
862
551
  */
863
- createElicitationCompletionNotifier(elicitationId, options) {
864
- if (!this._clientCapabilities?.elicitation?.url) throw new SdkError(SdkErrorCode.CapabilityNotSupported, "Client does not support URL elicitation (required for notifications/elicitation/complete)");
865
- return () => this.notification({
866
- method: "notifications/elicitation/complete",
867
- params: { elicitationId }
868
- }, options);
869
- }
870
- async listRoots(params, options) {
871
- return this._requestWithSchema({
872
- method: "roots/list",
873
- params
874
- }, ListRootsResultSchema, options);
552
+ writeSSEEvent(controller, encoder, message, eventId) {
553
+ try {
554
+ let eventData = `event: message\n`;
555
+ if (eventId) eventData += `id: ${eventId}\n`;
556
+ eventData += `data: ${JSON.stringify(message)}\n\n`;
557
+ controller.enqueue(encoder.encode(eventData));
558
+ return true;
559
+ } catch (error) {
560
+ this.onerror?.(error);
561
+ return false;
562
+ }
875
563
  }
876
564
  /**
877
- * Sends a logging message to the client, if connected.
878
- * Note: You only need to send the parameters object, not the entire JSON-RPC message.
879
- * @see {@linkcode LoggingMessageNotification}
880
- * @param params
881
- * @param sessionId Optional for stateless transports and backward compatibility.
565
+ * Handles unsupported requests (`PUT`, `PATCH`, etc.)
882
566
  */
883
- async sendLoggingMessage(params, sessionId) {
884
- if (this._capabilities.logging && !this.isMessageIgnored(params.level, sessionId)) return this.notification({
885
- method: "notifications/message",
886
- params
887
- });
888
- }
889
- async sendResourceUpdated(params) {
890
- return this.notification({
891
- method: "notifications/resources/updated",
892
- params
567
+ handleUnsupportedRequest() {
568
+ this.onerror?.(/* @__PURE__ */ new Error("Method not allowed."));
569
+ return Response.json({
570
+ jsonrpc: "2.0",
571
+ error: {
572
+ code: -32e3,
573
+ message: "Method not allowed."
574
+ },
575
+ id: null
576
+ }, {
577
+ status: 405,
578
+ headers: {
579
+ Allow: "GET, POST, DELETE",
580
+ "Content-Type": "application/json"
581
+ }
893
582
  });
894
583
  }
895
- async sendResourceListChanged() {
896
- return this.notification({ method: "notifications/resources/list_changed" });
897
- }
898
- async sendToolListChanged() {
899
- return this.notification({ method: "notifications/tools/list_changed" });
900
- }
901
- async sendPromptListChanged() {
902
- return this.notification({ method: "notifications/prompts/list_changed" });
903
- }
904
- };
905
-
906
- //#endregion
907
- //#region src/server/mcp.ts
908
- /**
909
- * High-level MCP server that provides a simpler API for working with resources, tools, and prompts.
910
- * For advanced usage (like sending notifications or setting custom request handlers), use the underlying
911
- * {@linkcode Server} instance available via the {@linkcode McpServer.server | server} property.
912
- *
913
- * @example
914
- * ```ts source="./mcp.examples.ts#McpServer_basicUsage"
915
- * const server = new McpServer({
916
- * name: 'my-server',
917
- * version: '1.0.0'
918
- * });
919
- * ```
920
- */
921
- var McpServer = class {
922
- /**
923
- * The underlying {@linkcode Server} instance, useful for advanced operations like sending notifications.
924
- */
925
- server;
926
- _registeredResources = {};
927
- _registeredResourceTemplates = {};
928
- _registeredTools = {};
929
- _registeredPrompts = {};
930
- _experimental;
931
- constructor(serverInfo, options) {
932
- this.server = new Server(serverInfo, options);
933
- }
934
- /**
935
- * Access experimental features.
936
- *
937
- * WARNING: These APIs are experimental and may change without notice.
938
- *
939
- * @experimental
940
- */
941
- get experimental() {
942
- if (!this._experimental) this._experimental = { tasks: new ExperimentalMcpServerTasks(this) };
943
- return this._experimental;
944
- }
945
- /**
946
- * Attaches to the given transport, starts it, and starts listening for messages.
947
- *
948
- * The `server` object assumes ownership of the {@linkcode Transport}, replacing any callbacks that have already been set, and expects that it is the only user of the {@linkcode Transport} instance going forward.
949
- *
950
- * @example
951
- * ```ts source="./mcp.examples.ts#McpServer_connect_stdio"
952
- * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
953
- * const transport = new StdioServerTransport();
954
- * await server.connect(transport);
955
- * ```
956
- */
957
- async connect(transport) {
958
- return await this.server.connect(transport);
959
- }
960
584
  /**
961
- * Closes the connection.
585
+ * Handles `POST` requests containing JSON-RPC messages
962
586
  */
963
- async close() {
964
- await this.server.close();
965
- }
966
- _toolHandlersInitialized = false;
967
- setToolRequestHandlers() {
968
- if (this._toolHandlersInitialized) return;
969
- this.server.assertCanSetRequestHandler("tools/list");
970
- this.server.assertCanSetRequestHandler("tools/call");
971
- this.server.registerCapabilities({ tools: { listChanged: this.server.getCapabilities().tools?.listChanged ?? true } });
972
- this.server.setRequestHandler("tools/list", () => ({ tools: Object.entries(this._registeredTools).filter(([, tool]) => tool.enabled).map(([name, tool]) => {
973
- const toolDefinition = {
974
- name,
975
- title: tool.title,
976
- description: tool.description,
977
- inputSchema: tool.inputSchema ? standardSchemaToJsonSchema(tool.inputSchema, "input") : EMPTY_OBJECT_JSON_SCHEMA,
978
- annotations: tool.annotations,
979
- execution: tool.execution,
980
- _meta: tool._meta
981
- };
982
- if (tool.outputSchema) toolDefinition.outputSchema = standardSchemaToJsonSchema(tool.outputSchema, "output");
983
- return toolDefinition;
984
- }) }));
985
- this.server.setRequestHandler("tools/call", async (request, ctx) => {
986
- const tool = this._registeredTools[request.params.name];
987
- if (!tool) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`);
988
- if (!tool.enabled) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} disabled`);
989
- try {
990
- const isTaskRequest = !!request.params.task;
991
- const taskSupport = tool.execution?.taskSupport;
992
- const isTaskHandler = "createTask" in tool.handler;
993
- if ((taskSupport === "required" || taskSupport === "optional") && !isTaskHandler) throw new ProtocolError(ProtocolErrorCode.InternalError, `Tool ${request.params.name} has taskSupport '${taskSupport}' but was not registered with registerToolTask`);
994
- if (taskSupport === "required" && !isTaskRequest) throw new ProtocolError(ProtocolErrorCode.MethodNotFound, `Tool ${request.params.name} requires task augmentation (taskSupport: 'required')`);
995
- if (taskSupport === "optional" && !isTaskRequest && isTaskHandler) return await this.handleAutomaticTaskPolling(tool, request, ctx);
996
- const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
997
- const result = await this.executeToolHandler(tool, args, ctx);
998
- if (isTaskRequest) return result;
999
- await this.validateToolOutput(tool, result, request.params.name);
1000
- return result;
587
+ async handlePostRequest(req, options) {
588
+ try {
589
+ const acceptHeader = req.headers.get("accept");
590
+ if (!acceptHeader?.includes("application/json") || !acceptHeader.includes("text/event-stream")) {
591
+ this.onerror?.(/* @__PURE__ */ new Error("Not Acceptable: Client must accept both application/json and text/event-stream"));
592
+ return this.createJsonErrorResponse(406, -32e3, "Not Acceptable: Client must accept both application/json and text/event-stream");
593
+ }
594
+ const ct = req.headers.get("content-type");
595
+ if (!ct || !ct.includes("application/json")) {
596
+ this.onerror?.(/* @__PURE__ */ new Error("Unsupported Media Type: Content-Type must be application/json"));
597
+ return this.createJsonErrorResponse(415, -32e3, "Unsupported Media Type: Content-Type must be application/json");
598
+ }
599
+ const request = req;
600
+ let rawMessage;
601
+ if (options?.parsedBody === void 0) try {
602
+ rawMessage = await req.json();
1001
603
  } catch (error) {
1002
- if (error instanceof ProtocolError && error.code === ProtocolErrorCode.UrlElicitationRequired) throw error;
1003
- return this.createToolError(error instanceof Error ? error.message : String(error));
604
+ this.onerror?.(error);
605
+ return this.createJsonErrorResponse(400, -32700, "Parse error: Invalid JSON");
1004
606
  }
1005
- });
1006
- this._toolHandlersInitialized = true;
1007
- }
1008
- /**
1009
- * Creates a tool error result.
1010
- *
1011
- * @param errorMessage - The error message.
1012
- * @returns The tool error result.
1013
- */
1014
- createToolError(errorMessage) {
1015
- return {
1016
- content: [{
1017
- type: "text",
1018
- text: errorMessage
1019
- }],
1020
- isError: true
1021
- };
1022
- }
1023
- /**
1024
- * Validates tool input arguments against the tool's input schema.
1025
- */
1026
- async validateToolInput(tool, args, toolName) {
1027
- if (!tool.inputSchema) return;
1028
- const parseResult = await validateStandardSchema(tool.inputSchema, args ?? {});
1029
- if (!parseResult.success) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${parseResult.error}`);
1030
- return parseResult.data;
1031
- }
1032
- /**
1033
- * Validates tool output against the tool's output schema.
1034
- */
1035
- async validateToolOutput(tool, result, toolName) {
1036
- if (!tool.outputSchema) return;
1037
- if (!("content" in result)) return;
1038
- if (result.isError) return;
1039
- if (!result.structuredContent) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Output validation error: Tool ${toolName} has an output schema but no structured content was provided`);
1040
- const parseResult = await validateStandardSchema(tool.outputSchema, result.structuredContent);
1041
- if (!parseResult.success) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Output validation error: Invalid structured content for tool ${toolName}: ${parseResult.error}`);
1042
- }
1043
- /**
1044
- * Executes a tool handler (either regular or task-based).
1045
- */
1046
- async executeToolHandler(tool, args, ctx) {
1047
- return tool.executor(args, ctx);
1048
- }
1049
- /**
1050
- * Handles automatic task polling for tools with `taskSupport` `'optional'`.
1051
- */
1052
- async handleAutomaticTaskPolling(tool, request, ctx) {
1053
- if (!ctx.task?.store) throw new Error("No task store provided for task-capable tool.");
1054
- const args = await this.validateToolInput(tool, request.params.arguments, request.params.name);
1055
- const createTaskResult = await tool.executor(args, ctx);
1056
- const taskId = createTaskResult.task.taskId;
1057
- let task = createTaskResult.task;
1058
- const pollInterval = task.pollInterval ?? 5e3;
1059
- while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
1060
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
1061
- const updatedTask = await ctx.task.store.getTask(taskId);
1062
- if (!updatedTask) throw new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} not found during polling`);
1063
- task = updatedTask;
1064
- }
1065
- return await ctx.task.store.getTaskResult(taskId);
1066
- }
1067
- _completionHandlerInitialized = false;
1068
- setCompletionRequestHandler() {
1069
- if (this._completionHandlerInitialized) return;
1070
- this.server.assertCanSetRequestHandler("completion/complete");
1071
- this.server.registerCapabilities({ completions: {} });
1072
- this.server.setRequestHandler("completion/complete", async (request) => {
1073
- switch (request.params.ref.type) {
1074
- case "ref/prompt":
1075
- assertCompleteRequestPrompt(request);
1076
- return this.handlePromptCompletion(request, request.params.ref);
1077
- case "ref/resource":
1078
- assertCompleteRequestResourceTemplate(request);
1079
- return this.handleResourceCompletion(request, request.params.ref);
1080
- default: throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid completion reference: ${request.params.ref}`);
1081
- }
1082
- });
1083
- this._completionHandlerInitialized = true;
1084
- }
1085
- async handlePromptCompletion(request, ref) {
1086
- const prompt = this._registeredPrompts[ref.name];
1087
- if (!prompt) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${ref.name} not found`);
1088
- if (!prompt.enabled) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${ref.name} disabled`);
1089
- if (!prompt.argsSchema) return EMPTY_COMPLETION_RESULT;
1090
- const field = unwrapOptionalSchema(getSchemaShape(prompt.argsSchema)?.[request.params.argument.name]);
1091
- if (!isCompletable(field)) return EMPTY_COMPLETION_RESULT;
1092
- const completer = getCompleter(field);
1093
- if (!completer) return EMPTY_COMPLETION_RESULT;
1094
- return createCompletionResult(await completer(request.params.argument.value, request.params.context));
1095
- }
1096
- async handleResourceCompletion(request, ref) {
1097
- const template = Object.values(this._registeredResourceTemplates).find((t) => t.resourceTemplate.uriTemplate.toString() === ref.uri);
1098
- if (!template) {
1099
- if (this._registeredResources[ref.uri]) return EMPTY_COMPLETION_RESULT;
1100
- throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource template ${request.params.ref.uri} not found`);
1101
- }
1102
- const completer = template.resourceTemplate.completeCallback(request.params.argument.name);
1103
- if (!completer) return EMPTY_COMPLETION_RESULT;
1104
- return createCompletionResult(await completer(request.params.argument.value, request.params.context));
1105
- }
1106
- _resourceHandlersInitialized = false;
1107
- setResourceRequestHandlers() {
1108
- if (this._resourceHandlersInitialized) return;
1109
- this.server.assertCanSetRequestHandler("resources/list");
1110
- this.server.assertCanSetRequestHandler("resources/templates/list");
1111
- this.server.assertCanSetRequestHandler("resources/read");
1112
- this.server.registerCapabilities({ resources: { listChanged: this.server.getCapabilities().resources?.listChanged ?? true } });
1113
- this.server.setRequestHandler("resources/list", async (_request, ctx) => {
1114
- const resources = Object.entries(this._registeredResources).filter(([_, resource]) => resource.enabled).map(([uri, resource]) => ({
1115
- uri,
1116
- name: resource.name,
1117
- ...resource.metadata
1118
- }));
1119
- const templateResources = [];
1120
- for (const template of Object.values(this._registeredResourceTemplates)) {
1121
- if (!template.resourceTemplate.listCallback) continue;
1122
- const result = await template.resourceTemplate.listCallback(ctx);
1123
- for (const resource of result.resources) templateResources.push({
1124
- ...template.metadata,
1125
- ...resource
1126
- });
1127
- }
1128
- return { resources: [...resources, ...templateResources] };
1129
- });
1130
- this.server.setRequestHandler("resources/templates/list", async () => {
1131
- return { resourceTemplates: Object.entries(this._registeredResourceTemplates).map(([name, template]) => ({
1132
- name,
1133
- uriTemplate: template.resourceTemplate.uriTemplate.toString(),
1134
- ...template.metadata
1135
- })) };
1136
- });
1137
- this.server.setRequestHandler("resources/read", async (request, ctx) => {
1138
- const uri = new URL(request.params.uri);
1139
- const resource = this._registeredResources[uri.toString()];
1140
- if (resource) {
1141
- if (!resource.enabled) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource ${uri} disabled`);
1142
- return resource.readCallback(uri, ctx);
1143
- }
1144
- for (const template of Object.values(this._registeredResourceTemplates)) {
1145
- const variables = template.resourceTemplate.uriTemplate.match(uri.toString());
1146
- if (variables) return template.readCallback(uri, variables, ctx);
607
+ else rawMessage = options.parsedBody;
608
+ let messages;
609
+ try {
610
+ messages = Array.isArray(rawMessage) ? rawMessage.map((msg) => JSONRPCMessageSchema.parse(msg)) : [JSONRPCMessageSchema.parse(rawMessage)];
611
+ } catch (error) {
612
+ this.onerror?.(error);
613
+ return this.createJsonErrorResponse(400, -32700, "Parse error: Invalid JSON-RPC message");
1147
614
  }
1148
- throw new ProtocolError(ProtocolErrorCode.ResourceNotFound, `Resource ${uri} not found`);
1149
- });
1150
- this._resourceHandlersInitialized = true;
1151
- }
1152
- _promptHandlersInitialized = false;
1153
- setPromptRequestHandlers() {
1154
- if (this._promptHandlersInitialized) return;
1155
- this.server.assertCanSetRequestHandler("prompts/list");
1156
- this.server.assertCanSetRequestHandler("prompts/get");
1157
- this.server.registerCapabilities({ prompts: { listChanged: this.server.getCapabilities().prompts?.listChanged ?? true } });
1158
- this.server.setRequestHandler("prompts/list", () => ({ prompts: Object.entries(this._registeredPrompts).filter(([, prompt]) => prompt.enabled).map(([name, prompt]) => {
1159
- return {
1160
- name,
1161
- title: prompt.title,
1162
- description: prompt.description,
1163
- arguments: prompt.argsSchema ? promptArgumentsFromStandardSchema(prompt.argsSchema) : void 0,
1164
- _meta: prompt._meta
1165
- };
1166
- }) }));
1167
- this.server.setRequestHandler("prompts/get", async (request, ctx) => {
1168
- const prompt = this._registeredPrompts[request.params.name];
1169
- if (!prompt) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${request.params.name} not found`);
1170
- if (!prompt.enabled) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Prompt ${request.params.name} disabled`);
1171
- return prompt.handler(request.params.arguments, ctx);
1172
- });
1173
- this._promptHandlersInitialized = true;
1174
- }
1175
- registerResource(name, uriOrTemplate, config, readCallback) {
1176
- if (typeof uriOrTemplate === "string") {
1177
- if (this._registeredResources[uriOrTemplate]) throw new Error(`Resource ${uriOrTemplate} is already registered`);
1178
- const registeredResource = this._createRegisteredResource(name, config.title, uriOrTemplate, config, readCallback);
1179
- this.setResourceRequestHandlers();
1180
- this.sendResourceListChanged();
1181
- return registeredResource;
1182
- } else {
1183
- if (this._registeredResourceTemplates[name]) throw new Error(`Resource template ${name} is already registered`);
1184
- const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config.title, uriOrTemplate, config, readCallback);
1185
- this.setResourceRequestHandlers();
1186
- this.sendResourceListChanged();
1187
- return registeredResourceTemplate;
1188
- }
1189
- }
1190
- _createRegisteredResource(name, title, uri, metadata, readCallback) {
1191
- const registeredResource = {
1192
- name,
1193
- title,
1194
- metadata,
1195
- readCallback,
1196
- enabled: true,
1197
- disable: () => registeredResource.update({ enabled: false }),
1198
- enable: () => registeredResource.update({ enabled: true }),
1199
- remove: () => registeredResource.update({ uri: null }),
1200
- update: (updates) => {
1201
- if (updates.uri !== void 0 && updates.uri !== uri) {
1202
- delete this._registeredResources[uri];
1203
- if (updates.uri) this._registeredResources[updates.uri] = registeredResource;
615
+ const isInitializationRequest = messages.some((element) => isInitializeRequest(element));
616
+ if (isInitializationRequest) {
617
+ if (this._initialized && this.sessionId !== void 0) {
618
+ this.onerror?.(/* @__PURE__ */ new Error("Invalid Request: Server already initialized"));
619
+ return this.createJsonErrorResponse(400, -32600, "Invalid Request: Server already initialized");
1204
620
  }
1205
- if (updates.name !== void 0) registeredResource.name = updates.name;
1206
- if (updates.title !== void 0) registeredResource.title = updates.title;
1207
- if (updates.metadata !== void 0) registeredResource.metadata = updates.metadata;
1208
- if (updates.callback !== void 0) registeredResource.readCallback = updates.callback;
1209
- if (updates.enabled !== void 0) registeredResource.enabled = updates.enabled;
1210
- this.sendResourceListChanged();
1211
- }
1212
- };
1213
- this._registeredResources[uri] = registeredResource;
1214
- return registeredResource;
1215
- }
1216
- _createRegisteredResourceTemplate(name, title, template, metadata, readCallback) {
1217
- const registeredResourceTemplate = {
1218
- resourceTemplate: template,
1219
- title,
1220
- metadata,
1221
- readCallback,
1222
- enabled: true,
1223
- disable: () => registeredResourceTemplate.update({ enabled: false }),
1224
- enable: () => registeredResourceTemplate.update({ enabled: true }),
1225
- remove: () => registeredResourceTemplate.update({ name: null }),
1226
- update: (updates) => {
1227
- if (updates.name !== void 0 && updates.name !== name) {
1228
- delete this._registeredResourceTemplates[name];
1229
- if (updates.name) this._registeredResourceTemplates[updates.name] = registeredResourceTemplate;
621
+ if (messages.length > 1) {
622
+ this.onerror?.(/* @__PURE__ */ new Error("Invalid Request: Only one initialization request is allowed"));
623
+ return this.createJsonErrorResponse(400, -32600, "Invalid Request: Only one initialization request is allowed");
1230
624
  }
1231
- if (updates.title !== void 0) registeredResourceTemplate.title = updates.title;
1232
- if (updates.template !== void 0) registeredResourceTemplate.resourceTemplate = updates.template;
1233
- if (updates.metadata !== void 0) registeredResourceTemplate.metadata = updates.metadata;
1234
- if (updates.callback !== void 0) registeredResourceTemplate.readCallback = updates.callback;
1235
- if (updates.enabled !== void 0) registeredResourceTemplate.enabled = updates.enabled;
1236
- this.sendResourceListChanged();
625
+ this.sessionId = this.sessionIdGenerator?.();
626
+ this._initialized = true;
627
+ if (this.sessionId && this._onsessioninitialized) await Promise.resolve(this._onsessioninitialized(this.sessionId));
1237
628
  }
1238
- };
1239
- this._registeredResourceTemplates[name] = registeredResourceTemplate;
1240
- const variableNames = template.uriTemplate.variableNames;
1241
- if (Array.isArray(variableNames) && variableNames.some((v) => !!template.completeCallback(v))) this.setCompletionRequestHandler();
1242
- return registeredResourceTemplate;
1243
- }
1244
- _createRegisteredPrompt(name, title, description, argsSchema, callback, _meta) {
1245
- let currentArgsSchema = argsSchema;
1246
- let currentCallback = callback;
1247
- const registeredPrompt = {
1248
- title,
1249
- description,
1250
- argsSchema,
1251
- _meta,
1252
- handler: createPromptHandler(name, argsSchema, callback),
1253
- enabled: true,
1254
- disable: () => registeredPrompt.update({ enabled: false }),
1255
- enable: () => registeredPrompt.update({ enabled: true }),
1256
- remove: () => registeredPrompt.update({ name: null }),
1257
- update: (updates) => {
1258
- if (updates.name !== void 0 && updates.name !== name) {
1259
- delete this._registeredPrompts[name];
1260
- if (updates.name) this._registeredPrompts[updates.name] = registeredPrompt;
1261
- }
1262
- if (updates.title !== void 0) registeredPrompt.title = updates.title;
1263
- if (updates.description !== void 0) registeredPrompt.description = updates.description;
1264
- if (updates._meta !== void 0) registeredPrompt._meta = updates._meta;
1265
- let needsHandlerRegen = false;
1266
- if (updates.argsSchema !== void 0) {
1267
- registeredPrompt.argsSchema = updates.argsSchema;
1268
- currentArgsSchema = updates.argsSchema;
1269
- needsHandlerRegen = true;
1270
- }
1271
- if (updates.callback !== void 0) {
1272
- currentCallback = updates.callback;
1273
- needsHandlerRegen = true;
1274
- }
1275
- if (needsHandlerRegen) registeredPrompt.handler = createPromptHandler(name, currentArgsSchema, currentCallback);
1276
- if (updates.enabled !== void 0) registeredPrompt.enabled = updates.enabled;
1277
- this.sendPromptListChanged();
629
+ if (!isInitializationRequest) {
630
+ const sessionError = this.validateSession(req);
631
+ if (sessionError) return sessionError;
632
+ const protocolError = this.validateProtocolVersion(req);
633
+ if (protocolError) return protocolError;
1278
634
  }
1279
- };
1280
- this._registeredPrompts[name] = registeredPrompt;
1281
- if (argsSchema) {
1282
- const shape = getSchemaShape(argsSchema);
1283
- if (shape) {
1284
- if (Object.values(shape).some((field) => {
1285
- return isCompletable(unwrapOptionalSchema(field));
1286
- })) this.setCompletionRequestHandler();
635
+ if (!messages.some((element) => isJSONRPCRequest(element))) {
636
+ for (const message of messages) this.onmessage?.(message, {
637
+ authInfo: options?.authInfo,
638
+ request
639
+ });
640
+ return new Response(null, { status: 202 });
1287
641
  }
1288
- }
1289
- return registeredPrompt;
1290
- }
1291
- _createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, execution, _meta, handler) {
1292
- validateAndWarnToolName(name);
1293
- let currentHandler = handler;
1294
- const registeredTool = {
1295
- title,
1296
- description,
1297
- inputSchema,
1298
- outputSchema,
1299
- annotations,
1300
- execution,
1301
- _meta,
1302
- handler,
1303
- executor: createToolExecutor(inputSchema, handler),
1304
- enabled: true,
1305
- disable: () => registeredTool.update({ enabled: false }),
1306
- enable: () => registeredTool.update({ enabled: true }),
1307
- remove: () => registeredTool.update({ name: null }),
1308
- update: (updates) => {
1309
- if (updates.name !== void 0 && updates.name !== name) {
1310
- if (typeof updates.name === "string") validateAndWarnToolName(updates.name);
1311
- delete this._registeredTools[name];
1312
- if (updates.name) this._registeredTools[updates.name] = registeredTool;
1313
- }
1314
- if (updates.title !== void 0) registeredTool.title = updates.title;
1315
- if (updates.description !== void 0) registeredTool.description = updates.description;
1316
- let needsExecutorRegen = false;
1317
- if (updates.paramsSchema !== void 0) {
1318
- registeredTool.inputSchema = updates.paramsSchema;
1319
- needsExecutorRegen = true;
642
+ const streamId = crypto.randomUUID();
643
+ const initRequest = messages.find((m) => isInitializeRequest(m));
644
+ const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
645
+ if (this._enableJsonResponse) return new Promise((resolve) => {
646
+ this._streamMapping.set(streamId, {
647
+ resolveJson: resolve,
648
+ cleanup: () => {
649
+ this._streamMapping.delete(streamId);
650
+ }
651
+ });
652
+ for (const message of messages) if (isJSONRPCRequest(message)) this._requestToStreamMapping.set(message.id, streamId);
653
+ for (const message of messages) this.onmessage?.(message, {
654
+ authInfo: options?.authInfo,
655
+ request
656
+ });
657
+ });
658
+ const encoder = new TextEncoder();
659
+ let streamController;
660
+ const readable = new ReadableStream({
661
+ start: (controller) => {
662
+ streamController = controller;
663
+ },
664
+ cancel: () => {
665
+ if (this._streamMapping.get(streamId)?.controller === streamController) this._streamMapping.delete(streamId);
1320
666
  }
1321
- if (updates.callback !== void 0) {
1322
- registeredTool.handler = updates.callback;
1323
- currentHandler = updates.callback;
1324
- needsExecutorRegen = true;
667
+ });
668
+ const headers = {
669
+ "Content-Type": "text/event-stream",
670
+ "Cache-Control": "no-cache",
671
+ Connection: "keep-alive"
672
+ };
673
+ if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
674
+ for (const message of messages) if (isJSONRPCRequest(message)) {
675
+ this._streamMapping.set(streamId, {
676
+ controller: streamController,
677
+ encoder,
678
+ cleanup: () => {
679
+ this._streamMapping.delete(streamId);
680
+ try {
681
+ streamController.close();
682
+ } catch {}
683
+ }
684
+ });
685
+ this._requestToStreamMapping.set(message.id, streamId);
686
+ }
687
+ await this.writePrimingEvent(streamController, encoder, streamId, clientProtocolVersion);
688
+ for (const message of messages) {
689
+ let closeSSEStream;
690
+ let closeStandaloneSSEStream;
691
+ if (isJSONRPCRequest(message) && this._eventStore && this.supportsEmptySSEData(clientProtocolVersion)) {
692
+ closeSSEStream = () => {
693
+ this.closeSSEStream(message.id);
694
+ };
695
+ closeStandaloneSSEStream = () => {
696
+ this.closeStandaloneSSEStream();
697
+ };
1325
698
  }
1326
- if (needsExecutorRegen) registeredTool.executor = createToolExecutor(registeredTool.inputSchema, currentHandler);
1327
- if (updates.outputSchema !== void 0) registeredTool.outputSchema = updates.outputSchema;
1328
- if (updates.annotations !== void 0) registeredTool.annotations = updates.annotations;
1329
- if (updates._meta !== void 0) registeredTool._meta = updates._meta;
1330
- if (updates.enabled !== void 0) registeredTool.enabled = updates.enabled;
1331
- this.sendToolListChanged();
699
+ this.onmessage?.(message, {
700
+ authInfo: options?.authInfo,
701
+ request,
702
+ closeSSEStream,
703
+ closeStandaloneSSEStream
704
+ });
1332
705
  }
1333
- };
1334
- this._registeredTools[name] = registeredTool;
1335
- this.setToolRequestHandlers();
1336
- this.sendToolListChanged();
1337
- return registeredTool;
1338
- }
1339
- /**
1340
- * Registers a tool with a config object and callback.
1341
- *
1342
- * @example
1343
- * ```ts source="./mcp.examples.ts#McpServer_registerTool_basic"
1344
- * server.registerTool(
1345
- * 'calculate-bmi',
1346
- * {
1347
- * title: 'BMI Calculator',
1348
- * description: 'Calculate Body Mass Index',
1349
- * inputSchema: z.object({
1350
- * weightKg: z.number(),
1351
- * heightM: z.number()
1352
- * }),
1353
- * outputSchema: z.object({ bmi: z.number() })
1354
- * },
1355
- * async ({ weightKg, heightM }) => {
1356
- * const output = { bmi: weightKg / (heightM * heightM) };
1357
- * return {
1358
- * content: [{ type: 'text', text: JSON.stringify(output) }],
1359
- * structuredContent: output
1360
- * };
1361
- * }
1362
- * );
1363
- * ```
1364
- */
1365
- registerTool(name, config, cb) {
1366
- if (this._registeredTools[name]) throw new Error(`Tool ${name} is already registered`);
1367
- const { title, description, inputSchema, outputSchema, annotations, _meta } = config;
1368
- return this._createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, { taskSupport: "forbidden" }, _meta, cb);
706
+ return new Response(readable, {
707
+ status: 200,
708
+ headers
709
+ });
710
+ } catch (error) {
711
+ this.onerror?.(error);
712
+ return this.createJsonErrorResponse(400, -32700, "Parse error", { data: String(error) });
713
+ }
1369
714
  }
1370
715
  /**
1371
- * Registers a prompt with a config object and callback.
1372
- *
1373
- * @example
1374
- * ```ts source="./mcp.examples.ts#McpServer_registerPrompt_basic"
1375
- * server.registerPrompt(
1376
- * 'review-code',
1377
- * {
1378
- * title: 'Code Review',
1379
- * description: 'Review code for best practices',
1380
- * argsSchema: z.object({ code: z.string() })
1381
- * },
1382
- * ({ code }) => ({
1383
- * messages: [
1384
- * {
1385
- * role: 'user' as const,
1386
- * content: {
1387
- * type: 'text' as const,
1388
- * text: `Please review this code:\n\n${code}`
1389
- * }
1390
- * }
1391
- * ]
1392
- * })
1393
- * );
1394
- * ```
716
+ * Handles `DELETE` requests to terminate sessions
1395
717
  */
1396
- registerPrompt(name, config, cb) {
1397
- if (this._registeredPrompts[name]) throw new Error(`Prompt ${name} is already registered`);
1398
- const { title, description, argsSchema, _meta } = config;
1399
- const registeredPrompt = this._createRegisteredPrompt(name, title, description, argsSchema, cb, _meta);
1400
- this.setPromptRequestHandlers();
1401
- this.sendPromptListChanged();
1402
- return registeredPrompt;
718
+ async handleDeleteRequest(req) {
719
+ const sessionError = this.validateSession(req);
720
+ if (sessionError) return sessionError;
721
+ const protocolError = this.validateProtocolVersion(req);
722
+ if (protocolError) return protocolError;
723
+ await Promise.resolve(this._onsessionclosed?.(this.sessionId));
724
+ await this.close();
725
+ return new Response(null, { status: 200 });
1403
726
  }
1404
727
  /**
1405
- * Checks if the server is connected to a transport.
1406
- * @returns `true` if the server is connected
728
+ * Validates session ID for non-initialization requests.
729
+ * Returns `Response` error if invalid, `undefined` otherwise
1407
730
  */
1408
- isConnected() {
1409
- return this.server.transport !== void 0;
731
+ validateSession(req) {
732
+ if (this.sessionIdGenerator === void 0) return;
733
+ if (!this._initialized) {
734
+ this.onerror?.(/* @__PURE__ */ new Error("Bad Request: Server not initialized"));
735
+ return this.createJsonErrorResponse(400, -32e3, "Bad Request: Server not initialized");
736
+ }
737
+ const sessionId = req.headers.get("mcp-session-id");
738
+ if (!sessionId) {
739
+ this.onerror?.(/* @__PURE__ */ new Error("Bad Request: Mcp-Session-Id header is required"));
740
+ return this.createJsonErrorResponse(400, -32e3, "Bad Request: Mcp-Session-Id header is required");
741
+ }
742
+ if (sessionId !== this.sessionId) {
743
+ this.onerror?.(/* @__PURE__ */ new Error("Session not found"));
744
+ return this.createJsonErrorResponse(404, -32001, "Session not found");
745
+ }
1410
746
  }
1411
747
  /**
1412
- * Sends a logging message to the client, if connected.
1413
- * Note: You only need to send the parameters object, not the entire JSON-RPC message.
1414
- * @see {@linkcode LoggingMessageNotification}
1415
- * @param params
1416
- * @param sessionId Optional for stateless transports and backward compatibility.
748
+ * Validates the `MCP-Protocol-Version` header on incoming requests.
1417
749
  *
1418
- * @example
1419
- * ```ts source="./mcp.examples.ts#McpServer_sendLoggingMessage_basic"
1420
- * await server.sendLoggingMessage({
1421
- * level: 'info',
1422
- * data: 'Processing complete'
1423
- * });
1424
- * ```
1425
- */
1426
- async sendLoggingMessage(params, sessionId) {
1427
- return this.server.sendLoggingMessage(params, sessionId);
1428
- }
1429
- /**
1430
- * Sends a resource list changed event to the client, if connected.
1431
- */
1432
- sendResourceListChanged() {
1433
- if (this.isConnected()) this.server.sendResourceListChanged();
1434
- }
1435
- /**
1436
- * Sends a tool list changed event to the client, if connected.
1437
- */
1438
- sendToolListChanged() {
1439
- if (this.isConnected()) this.server.sendToolListChanged();
1440
- }
1441
- /**
1442
- * Sends a prompt list changed event to the client, if connected.
750
+ * For initialization: Version negotiation handles unknown versions gracefully
751
+ * (server responds with its supported version).
752
+ *
753
+ * For subsequent requests with `MCP-Protocol-Version` header:
754
+ * - Accept if in supported list
755
+ * - 400 if unsupported
756
+ *
757
+ * For HTTP requests without the `MCP-Protocol-Version` header:
758
+ * - Accept and default to the version negotiated at initialization
1443
759
  */
1444
- sendPromptListChanged() {
1445
- if (this.isConnected()) this.server.sendPromptListChanged();
1446
- }
1447
- };
1448
- /**
1449
- * A resource template combines a URI pattern with optional functionality to enumerate
1450
- * all resources matching that pattern.
1451
- */
1452
- var ResourceTemplate = class {
1453
- _uriTemplate;
1454
- constructor(uriTemplate, _callbacks) {
1455
- this._callbacks = _callbacks;
1456
- this._uriTemplate = typeof uriTemplate === "string" ? new UriTemplate(uriTemplate) : uriTemplate;
760
+ validateProtocolVersion(req) {
761
+ const protocolVersion = req.headers.get("mcp-protocol-version");
762
+ if (protocolVersion !== null && !this._supportedProtocolVersions.includes(protocolVersion)) {
763
+ const error = `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${this._supportedProtocolVersions.join(", ")})`;
764
+ this.onerror?.(new Error(error));
765
+ return this.createJsonErrorResponse(400, -32e3, error);
766
+ }
1457
767
  }
1458
- /**
1459
- * Gets the URI template pattern.
1460
- */
1461
- get uriTemplate() {
1462
- return this._uriTemplate;
768
+ async close() {
769
+ if (this._closed) return;
770
+ this._closed = true;
771
+ for (const { cleanup } of this._streamMapping.values()) cleanup();
772
+ this._streamMapping.clear();
773
+ this._requestResponseMap.clear();
774
+ this.onclose?.();
1463
775
  }
1464
776
  /**
1465
- * Gets the list callback, if one was provided.
777
+ * Close an SSE stream for a specific request, triggering client reconnection.
778
+ * Use this to implement polling behavior during long-running operations -
779
+ * client will reconnect after the retry interval specified in the priming event.
1466
780
  */
1467
- get listCallback() {
1468
- return this._callbacks.list;
781
+ closeSSEStream(requestId) {
782
+ const streamId = this._requestToStreamMapping.get(requestId);
783
+ if (!streamId) return;
784
+ const stream = this._streamMapping.get(streamId);
785
+ if (stream) stream.cleanup();
1469
786
  }
1470
787
  /**
1471
- * Gets the callback for completing a specific URI template variable, if one was provided.
788
+ * Close the standalone `GET` SSE stream, triggering client reconnection.
789
+ * Use this to implement polling behavior for server-initiated notifications.
1472
790
  */
1473
- completeCallback(variable) {
1474
- return this._callbacks.complete?.[variable];
791
+ closeStandaloneSSEStream() {
792
+ const stream = this._streamMapping.get(this._standaloneSseStreamId);
793
+ if (stream) stream.cleanup();
1475
794
  }
1476
- };
1477
- /**
1478
- * Creates an executor that invokes the handler with the appropriate arguments.
1479
- * When `inputSchema` is defined, the handler is called with `(args, ctx)`.
1480
- * When `inputSchema` is undefined, the handler is called with just `(ctx)`.
1481
- */
1482
- function createToolExecutor(inputSchema, handler) {
1483
- if ("createTask" in handler) {
1484
- const taskHandler = handler;
1485
- return async (args, ctx) => {
1486
- if (!ctx.task?.store) throw new Error("No task store provided.");
1487
- const taskCtx = {
1488
- ...ctx,
1489
- task: {
1490
- store: ctx.task.store,
1491
- requestedTtl: ctx.task?.requestedTtl
795
+ async send(message, options) {
796
+ let requestId = options?.relatedRequestId;
797
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) requestId = message.id;
798
+ if (requestId === void 0) {
799
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");
800
+ let eventId;
801
+ if (this._eventStore) eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message);
802
+ const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId);
803
+ if (standaloneSse === void 0) return;
804
+ if (standaloneSse.controller && standaloneSse.encoder && (eventId === void 0 || !standaloneSse.replayedEventIds?.has(eventId))) this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId);
805
+ return;
806
+ }
807
+ const streamId = this._requestToStreamMapping.get(requestId);
808
+ if (!streamId) throw new Error(`No connection established for request ID: ${String(requestId)}`);
809
+ let stream = this._streamMapping.get(streamId);
810
+ if (!this._enableJsonResponse) {
811
+ let eventId;
812
+ if (this._eventStore) {
813
+ eventId = await this._eventStore.storeEvent(streamId, message);
814
+ stream = this._streamMapping.get(streamId);
815
+ }
816
+ if (stream?.controller && stream?.encoder && (eventId === void 0 || !stream.replayedEventIds?.has(eventId))) this.writeSSEEvent(stream.controller, stream.encoder, message, eventId);
817
+ }
818
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
819
+ this._requestResponseMap.set(requestId, message);
820
+ const relatedIds = [...this._requestToStreamMapping.entries()].filter(([_, sid]) => sid === streamId).map(([id]) => id);
821
+ if (relatedIds.every((id) => this._requestResponseMap.has(id))) {
822
+ if (!stream) {
823
+ if (this._enableJsonResponse) throw new Error(`No connection established for request ID: ${String(requestId)}`);
824
+ if (!this._eventStore) {
825
+ this.onerror?.(/* @__PURE__ */ new Error(`Response for request ID ${String(requestId)} is undeliverable: per-request stream is disconnected and no eventStore is configured`));
826
+ for (const id of relatedIds) {
827
+ this._requestResponseMap.delete(id);
828
+ this._requestToStreamMapping.delete(id);
829
+ }
830
+ return;
831
+ }
832
+ for (const id of relatedIds) {
833
+ this._requestResponseMap.delete(id);
834
+ this._requestToStreamMapping.delete(id);
835
+ }
836
+ return;
1492
837
  }
1493
- };
1494
- if (inputSchema) return taskHandler.createTask(args, taskCtx);
1495
- return taskHandler.createTask(taskCtx);
1496
- };
1497
- }
1498
- if (inputSchema) {
1499
- const callback$1 = handler;
1500
- return async (args, ctx) => callback$1(args, ctx);
838
+ if (this._enableJsonResponse && stream.resolveJson) {
839
+ const headers = { "Content-Type": "application/json" };
840
+ if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
841
+ const responses = relatedIds.map((id) => this._requestResponseMap.get(id));
842
+ if (responses.length === 1) stream.resolveJson(Response.json(responses[0], {
843
+ status: 200,
844
+ headers
845
+ }));
846
+ else stream.resolveJson(Response.json(responses, {
847
+ status: 200,
848
+ headers
849
+ }));
850
+ stream.cleanup();
851
+ } else stream.cleanup();
852
+ for (const id of relatedIds) {
853
+ this._requestResponseMap.delete(id);
854
+ this._requestToStreamMapping.delete(id);
855
+ }
856
+ }
857
+ }
1501
858
  }
1502
- const callback = handler;
1503
- return async (_args, ctx) => callback(ctx);
1504
- }
1505
- const EMPTY_OBJECT_JSON_SCHEMA = {
1506
- type: "object",
1507
- properties: {}
1508
859
  };
860
+
861
+ //#endregion
862
+ //#region src/server/createMcpHandler.ts
1509
863
  /**
1510
- * Creates a type-safe prompt handler that captures the schema and callback in a closure.
1511
- * This eliminates the need for type assertions at the call site.
864
+ * The JSON-RPC id to echo on an entry-built error response: the body's `id`
865
+ * when the body is a single JSON-RPC request whose id is a string or number,
866
+ * `null` otherwise. Error responses must carry the id of the request they
867
+ * correspond to whenever it could be read; `null` is reserved for the cases
868
+ * where no single request id is determinable — unparseable bodies, body-less
869
+ * methods, notifications, posted responses and batch arrays.
1512
870
  */
1513
- function createPromptHandler(name, argsSchema, callback) {
1514
- if (argsSchema) {
1515
- const typedCallback = callback;
1516
- return async (args, ctx) => {
1517
- const parseResult = await validateStandardSchema(argsSchema, args);
1518
- if (!parseResult.success) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for prompt ${name}: ${parseResult.error}`);
1519
- return typedCallback(parseResult.data, ctx);
1520
- };
1521
- } else {
1522
- const typedCallback = callback;
1523
- return async (_args, ctx) => {
1524
- return typedCallback(ctx);
1525
- };
1526
- }
871
+ function echoableRequestId(body) {
872
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
873
+ const { method, id } = body;
874
+ if (typeof method !== "string") return null;
875
+ return typeof id === "string" || typeof id === "number" ? id : null;
876
+ }
877
+ function jsonRpcErrorResponse(httpStatus, code, message, data, id = null) {
878
+ return Response.json({
879
+ jsonrpc: "2.0",
880
+ error: {
881
+ code,
882
+ message,
883
+ ...data !== void 0 && { data }
884
+ },
885
+ id
886
+ }, { status: httpStatus });
887
+ }
888
+ function rejectionResponse(rejection, id = null) {
889
+ return jsonRpcErrorResponse(rejection.httpStatus, rejection.code, rejection.message, rejection.data, id);
890
+ }
891
+ function toError(value) {
892
+ return value instanceof Error ? value : new Error(String(value));
1527
893
  }
1528
- function createCompletionResult(suggestions) {
1529
- return { completion: {
1530
- values: suggestions.map(String).slice(0, 100),
1531
- total: suggestions.length,
1532
- hasMore: suggestions.length > 100
1533
- } };
894
+ function internalServerErrorResponse(id = null) {
895
+ return jsonRpcErrorResponse(500, -32603, "Internal server error", void 0, id);
896
+ }
897
+ /**
898
+ * The entry's default legacy serving (`legacy: 'stateless'`): per-request
899
+ * stateless serving of 2025-era traffic using the same factory as the modern
900
+ * path. Exported as a standalone building block for hand-wired compositions
901
+ * (for example mounting legacy stateless serving on its own route next to a
902
+ * strict modern endpoint).
903
+ *
904
+ * Each POST is served by a fresh instance from the factory connected to a
905
+ * fresh streamable HTTP transport constructed with only
906
+ * `sessionIdGenerator: undefined` — the established stateless idiom, unchanged.
907
+ * Because serving is per-request and stateless, GET and DELETE (2025 session
908
+ * operations) are answered with `405` / `Method not allowed.`, exactly like the
909
+ * canonical stateless example.
910
+ *
911
+ * The optional `onerror` callback receives factory and serving failures on
912
+ * this leg (reporting only — the response stays the 500 internal-error body).
913
+ * The entry passes its own `onerror` here when expanding the default, so
914
+ * legacy-leg failures are never silently swallowed.
915
+ */
916
+ function legacyStatelessFallback(factory, onerror) {
917
+ return async (request, options) => {
918
+ if (request.method.toUpperCase() !== "POST") return jsonRpcErrorResponse(405, -32e3, "Method not allowed.");
919
+ try {
920
+ const product = await factory({
921
+ era: "legacy",
922
+ ...options?.authInfo !== void 0 && { authInfo: options.authInfo },
923
+ requestInfo: request
924
+ });
925
+ const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
926
+ await product.connect(transport);
927
+ const teardown = () => {
928
+ transport.close().catch(() => {});
929
+ product.close().catch(() => {});
930
+ };
931
+ request.signal?.addEventListener("abort", teardown, { once: true });
932
+ const response = await transport.handleRequest(request, {
933
+ ...options?.authInfo !== void 0 && { authInfo: options.authInfo },
934
+ ...options?.parsedBody !== void 0 && { parsedBody: options.parsedBody }
935
+ });
936
+ if (response.body === null || !(response.headers.get("content-type") ?? "").includes("text/event-stream")) {
937
+ teardown();
938
+ return response;
939
+ }
940
+ const reader = response.body.getReader();
941
+ let toreDown = false;
942
+ const completeExchange = () => {
943
+ if (!toreDown) {
944
+ toreDown = true;
945
+ teardown();
946
+ }
947
+ };
948
+ const monitoredBody = new ReadableStream({
949
+ pull: async (controller) => {
950
+ try {
951
+ const { done, value } = await reader.read();
952
+ if (done) {
953
+ completeExchange();
954
+ controller.close();
955
+ return;
956
+ }
957
+ if (value !== void 0) controller.enqueue(value);
958
+ } catch (error) {
959
+ completeExchange();
960
+ controller.error(error);
961
+ }
962
+ },
963
+ cancel: (reason) => {
964
+ completeExchange();
965
+ return reader.cancel(reason).catch(() => {});
966
+ }
967
+ });
968
+ return new Response(monitoredBody, {
969
+ status: response.status,
970
+ statusText: response.statusText,
971
+ headers: response.headers
972
+ });
973
+ } catch (error) {
974
+ try {
975
+ onerror?.(toError(error));
976
+ } catch {}
977
+ return internalServerErrorResponse(echoableRequestId(options?.parsedBody));
978
+ }
979
+ };
1534
980
  }
1535
- const EMPTY_COMPLETION_RESULT = { completion: {
1536
- values: [],
1537
- hasMore: false
1538
- } };
1539
- /** @internal Gets the shape of a Zod object schema */
1540
- function getSchemaShape(schema) {
1541
- const candidate = schema;
1542
- if (candidate.shape && typeof candidate.shape === "object") return candidate.shape;
981
+ /**
982
+ * The entry's classification step: read the request body exactly once (unless
983
+ * a pre-parsed body is supplied) and classify the request with
984
+ * {@linkcode classifyInboundRequest}. This is the single code path behind both
985
+ * {@linkcode createMcpHandler}'s routing and the exported
986
+ * {@linkcode isLegacyRequest} predicate, so the two can never disagree.
987
+ *
988
+ * Pass `needsForward: false` when the caller never reads `forwardRequest` —
989
+ * the body-preserving clone is then skipped and `forwardRequest` is the
990
+ * (consumed) input request.
991
+ */
992
+ async function classifyEntryRequest(request, providedParsedBody, needsForward = true) {
993
+ const httpMethod = request.method.toUpperCase();
994
+ let body;
995
+ let parsedBody = providedParsedBody;
996
+ let forwardRequest = request;
997
+ let unparseable = false;
998
+ if (httpMethod === "POST") {
999
+ if (parsedBody === void 0) {
1000
+ if (needsForward) forwardRequest = request.clone();
1001
+ let bodyText;
1002
+ try {
1003
+ bodyText = await request.text();
1004
+ } catch {
1005
+ return { step: "unreadable-body" };
1006
+ }
1007
+ try {
1008
+ body = bodyText.length === 0 ? void 0 : JSON.parse(bodyText);
1009
+ } catch {
1010
+ unparseable = true;
1011
+ }
1012
+ if (!unparseable && body !== void 0) parsedBody = body;
1013
+ } else body = parsedBody;
1014
+ if (unparseable || body === void 0) return {
1015
+ step: "no-json-body",
1016
+ forwardRequest
1017
+ };
1018
+ }
1019
+ return {
1020
+ step: "classified",
1021
+ outcome: classifyInboundRequest({
1022
+ httpMethod,
1023
+ protocolVersionHeader: request.headers.get("mcp-protocol-version") ?? void 0,
1024
+ mcpMethodHeader: request.headers.get("mcp-method") ?? void 0,
1025
+ mcpNameHeader: request.headers.get("mcp-name") ?? void 0,
1026
+ ...body !== void 0 && { body }
1027
+ }),
1028
+ body,
1029
+ parsedBody,
1030
+ forwardRequest
1031
+ };
1543
1032
  }
1544
- /** @internal Checks if a Zod schema is optional */
1545
- function isOptionalSchema(schema) {
1546
- return schema?.type === "optional";
1033
+ /**
1034
+ * Whether {@linkcode createMcpHandler} would route this request to its legacy
1035
+ * (2025-era) serving rather than the modern (2026-07-28) path.
1036
+ *
1037
+ * Call it with just the request: `await isLegacyRequest(request)`. For a
1038
+ * `POST` the body is read from an internal clone, so the request you pass
1039
+ * stays fully readable for whichever handler you route it to — no second
1040
+ * argument is needed. (In a Node `(req, res)` handler, build that `Request`
1041
+ * with `toWebRequest(req)` from `@modelcontextprotocol/node`; behind a body
1042
+ * parser, which has already drained the Node stream, build it as
1043
+ * `toWebRequest(req, req.body)` so the bytes come from the parsed body —
1044
+ * either way the predicate still takes just the request.) The optional
1045
+ * `parsedBody` is a perf escape hatch for a body you already hold parsed:
1046
+ * pass it and the predicate classifies from the value directly, reading and
1047
+ * cloning nothing. It is needed, not just faster, when the request's own
1048
+ * body was already read — the internal clone is then impossible (cloning a
1049
+ * used body throws a `TypeError`), so such a single-argument call rejects
1050
+ * instead of guessing.
1051
+ *
1052
+ * This is the entry's own classification step exported as a predicate — it
1053
+ * runs exactly the code `createMcpHandler` runs to make the routing decision,
1054
+ * not a re-implementation — so a hand-wired composition that branches on it
1055
+ * can never disagree with the entry. Use it to keep an existing legacy
1056
+ * deployment (for example a sessionful streamable HTTP wiring) serving 2025
1057
+ * traffic next to a strict modern endpoint, now that the entry has no
1058
+ * handler-valued `legacy` option:
1059
+ *
1060
+ * ```ts
1061
+ * import { createMcpHandler, isLegacyRequest } from '@modelcontextprotocol/server';
1062
+ *
1063
+ * const modern = createMcpHandler(factory, { legacy: 'reject' });
1064
+ *
1065
+ * export default {
1066
+ * async fetch(request: Request): Promise<Response> {
1067
+ * if (await isLegacyRequest(request)) {
1068
+ * // e.g. an existing sessionful WebStandardStreamableHTTPServerTransport wiring
1069
+ * return myExistingLegacyHandler(request);
1070
+ * }
1071
+ * return modern.fetch(request);
1072
+ * }
1073
+ * };
1074
+ * ```
1075
+ *
1076
+ * Semantics (identical to the entry's routing):
1077
+ *
1078
+ * - Returns `true` only for requests with no per-request `_meta` envelope
1079
+ * claim: claim-less POSTs (including the `initialize` handshake and 2025-era
1080
+ * notification POSTs without a modern protocol-version header), body-less
1081
+ * GET/DELETE session operations, all-legacy JSON-RPC batch arrays, posted
1082
+ * JSON-RPC responses, and POSTs whose body is empty or not valid JSON.
1083
+ * - Returns `false` for everything the modern path answers, including its
1084
+ * validation-ladder rejections: a request carrying the envelope claim (even
1085
+ * one naming a revision the endpoint does not serve — the modern path
1086
+ * answers it with the unsupported-protocol-version error), a malformed
1087
+ * envelope behind a present claim (answered `-32602`), a request whose
1088
+ * `MCP-Protocol-Version` header names a modern revision but that lacks the
1089
+ * envelope (`-32602`), and header/body mismatches (`-32020`). Consumers
1090
+ * routing on the predicate must send `false` traffic to the modern handler,
1091
+ * never to a legacy handler — the modern path owns those error answers.
1092
+ * - `server/discover` probes sent by negotiating clients always carry the
1093
+ * envelope claim, so they are never legacy; a hand-built claim-less POST to
1094
+ * a method named `server/discover` has no claim and classifies legacy,
1095
+ * exactly as the entry itself routes it.
1096
+ */
1097
+ async function isLegacyRequest(request, parsedBody) {
1098
+ const classified = await classifyEntryRequest(parsedBody === void 0 && request.method.toUpperCase() === "POST" ? request.clone() : request, parsedBody, false);
1099
+ return classified.step === "no-json-body" || classified.step === "classified" && classified.outcome.kind === "legacy";
1547
1100
  }
1548
- /** @internal Unwraps an optional Zod schema */
1549
- function unwrapOptionalSchema(schema) {
1550
- if (!isOptionalSchema(schema)) return schema;
1551
- return schema.def?.innerType ?? schema;
1101
+ /**
1102
+ * Creates an HTTP handler that serves the 2026-07-28 protocol revision from a
1103
+ * per-request server factory and, by default, falls back to old-school
1104
+ * stateless serving for 2025-era traffic. Pass `legacy: 'reject'` for a
1105
+ * modern-only strict endpoint.
1106
+ *
1107
+ * Mounting: `handler.fetch` is the web-standard face (Cloudflare Workers,
1108
+ * Deno, Bun, Hono's `c.req.raw`); for Express/Fastify/plain `node:http`, wrap
1109
+ * the handler once with `toNodeHandler(handler)` from
1110
+ * `@modelcontextprotocol/node`. When mounting bare on a fetch-native runtime,
1111
+ * put Origin/Host validation in front of the handler — the entry itself is
1112
+ * deliberately validation-free:
1113
+ *
1114
+ * ```ts
1115
+ * import { hostHeaderValidationResponse, originValidationResponse, localhostAllowedHostnames, localhostAllowedOrigins } from '@modelcontextprotocol/server';
1116
+ *
1117
+ * export default {
1118
+ * async fetch(request: Request): Promise<Response> {
1119
+ * const rejected =
1120
+ * hostHeaderValidationResponse(request, localhostAllowedHostnames()) ??
1121
+ * originValidationResponse(request, localhostAllowedOrigins());
1122
+ * return rejected ?? handler.fetch(request);
1123
+ * }
1124
+ * };
1125
+ * ```
1126
+ *
1127
+ * Use ONE factory for both legs: the same tools/resources/prompts definition
1128
+ * backs the modern path and the stateless legacy fallback, so the two eras can
1129
+ * never drift apart. To keep an existing legacy deployment (for example a
1130
+ * sessionful streamable HTTP wiring) serving 2025 traffic instead of the
1131
+ * stateless fallback, route in user land with {@linkcode isLegacyRequest} in
1132
+ * front of a strict handler — see that predicate's documentation for the
1133
+ * pattern. Power users composing transport-neutral routing can also use the
1134
+ * exported building blocks directly: {@linkcode classifyInboundRequest} for
1135
+ * the era decision and `PerRequestHTTPServerTransport` for single-exchange
1136
+ * serving.
1137
+ *
1138
+ * The entry performs no token verification: `authInfo` given to `fetch` is
1139
+ * passed through to handlers and the factory as-is and is never derived from
1140
+ * request headers.
1141
+ */
1142
+ function createMcpHandler(factory, options = {}) {
1143
+ const { legacy, onerror, responseMode } = options;
1144
+ if (typeof legacy === "function") throw new TypeError("The 'legacy' option only accepts 'stateless' or 'reject', not a handler function. To serve 2025-era traffic with your own handler, route in user land with the exported isLegacyRequest(request) predicate in front of a strict (legacy: 'reject') handler.");
1145
+ /** Modern per-request instances with an exchange still in flight (close() tears these down). */
1146
+ const inflight = /* @__PURE__ */ new Set();
1147
+ let closed = false;
1148
+ const reportError = (error) => {
1149
+ try {
1150
+ onerror?.(error);
1151
+ } catch {}
1152
+ };
1153
+ const bus = options.bus ?? new InMemoryServerEventBus(reportError);
1154
+ const notify = createServerNotifier(bus);
1155
+ const listenRouter = createListenRouter({
1156
+ bus,
1157
+ maxSubscriptions: options.maxSubscriptions ?? DEFAULT_MAX_SUBSCRIPTIONS,
1158
+ keepAliveMs: options.keepAliveMs ?? DEFAULT_LISTEN_KEEPALIVE_MS,
1159
+ onerror: reportError
1160
+ });
1161
+ if (responseMode === "json") console.warn("responseMode: 'json' drops mid-call notifications. subscriptions/listen streams are always served over SSE regardless; other notifications emitted before a result are dropped.");
1162
+ const legacyHandler = legacy === "reject" ? void 0 : legacyStatelessFallback(factory, reportError);
1163
+ async function serveModern(route, request, authInfo) {
1164
+ const claimedRevision = route.classification.revision;
1165
+ if (claimedRevision === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedRevision)) {
1166
+ const error = new UnsupportedProtocolVersionError({
1167
+ supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
1168
+ requested: claimedRevision ?? "unknown"
1169
+ });
1170
+ reportError(error);
1171
+ return jsonRpcErrorResponse(400, error.code, error.message, error.data, echoableRequestId(route.message));
1172
+ }
1173
+ const stdHeaderRejection = validateStandardRequestHeaders({
1174
+ httpMethod: request.method,
1175
+ mcpMethodHeader: request.headers.get("mcp-method") ?? void 0,
1176
+ mcpNameHeader: request.headers.get("mcp-name") ?? void 0
1177
+ }, route);
1178
+ if (stdHeaderRejection !== void 0) {
1179
+ reportError(/* @__PURE__ */ new Error(`Rejected inbound request (${stdHeaderRejection.cell}): ${stdHeaderRejection.message}`));
1180
+ return rejectionResponse(stdHeaderRejection, echoableRequestId(route.message));
1181
+ }
1182
+ const meta = route.messageKind === "request" ? requestMetaOf(route.message.params) : void 0;
1183
+ const declaredClientCapabilities = meta?.[CLIENT_CAPABILITIES_META_KEY];
1184
+ if (route.messageKind === "request") {
1185
+ const required = requiredClientCapabilitiesForRequest(route.message.method);
1186
+ if (required !== void 0) {
1187
+ const missing = missingClientCapabilities(required, declaredClientCapabilities);
1188
+ if (missing !== void 0) {
1189
+ const error = new MissingRequiredClientCapabilityError({ requiredCapabilities: missing });
1190
+ reportError(error);
1191
+ return jsonRpcErrorResponse(httpStatusForErrorCode(error.code, "ladder"), error.code, error.message, error.data, route.message.id);
1192
+ }
1193
+ }
1194
+ }
1195
+ const product = await factory({
1196
+ era: "modern",
1197
+ ...authInfo !== void 0 && { authInfo },
1198
+ requestInfo: request
1199
+ });
1200
+ const server = product instanceof McpServer ? product.server : product;
1201
+ if (route.messageKind === "request" && route.message.method === "subscriptions/listen") {
1202
+ const capabilities = server.getCapabilities();
1203
+ product.close().catch(reportError);
1204
+ return listenRouter.serve(route.message, request.signal, capabilities);
1205
+ }
1206
+ if (route.messageKind === "request" && route.message.method === "tools/call" && product instanceof McpServer) {
1207
+ const callParams = route.message.params;
1208
+ const toolName = typeof callParams?.name === "string" ? callParams.name : void 0;
1209
+ const inputSchema = toolName === void 0 ? void 0 : product.toolInputSchemaJson(toolName);
1210
+ if (inputSchema !== void 0) {
1211
+ const scan = scanXMcpHeaderDeclarations(inputSchema);
1212
+ if (scan.valid && scan.declarations.length > 0) {
1213
+ const rejection = validateMcpParamHeaders(scan.declarations, callParams?.arguments, request.headers);
1214
+ if (rejection !== void 0) {
1215
+ product.close().catch(reportError);
1216
+ reportError(/* @__PURE__ */ new Error(`Rejected inbound request (${rejection.cell}): ${rejection.message}`));
1217
+ return rejectionResponse(rejection, route.message.id);
1218
+ }
1219
+ }
1220
+ }
1221
+ }
1222
+ setNegotiatedProtocolVersion(server, claimedRevision);
1223
+ installModernOnlyHandlers(server, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
1224
+ if (meta !== void 0) seedClientIdentityFromEnvelope(server, {
1225
+ clientInfo: meta[CLIENT_INFO_META_KEY],
1226
+ clientCapabilities: declaredClientCapabilities
1227
+ });
1228
+ const previousOnClose = server.onclose;
1229
+ inflight.add(server);
1230
+ server.onclose = () => {
1231
+ inflight.delete(server);
1232
+ previousOnClose?.();
1233
+ };
1234
+ try {
1235
+ const response = await invoke(product, route.message, {
1236
+ classification: route.classification,
1237
+ request,
1238
+ ...authInfo !== void 0 && { authInfo },
1239
+ ...responseMode !== void 0 && { responseMode }
1240
+ });
1241
+ if (route.messageKind === "notification") queueMicrotask(() => void server.close().catch(() => {}));
1242
+ return response;
1243
+ } catch (error) {
1244
+ if (error instanceof SdkError && error.code === SdkErrorCode.ConnectionClosed) return new Response(null, { status: 499 });
1245
+ await server.close().catch(() => {});
1246
+ inflight.delete(server);
1247
+ reportError(toError(error));
1248
+ return internalServerErrorResponse(echoableRequestId(route.message));
1249
+ }
1250
+ }
1251
+ async function serveLegacyRoute(route, forwardRequest, authInfo, parsedBody) {
1252
+ if (legacyHandler !== void 0) return legacyHandler(forwardRequest, {
1253
+ ...authInfo !== void 0 && { authInfo },
1254
+ ...parsedBody !== void 0 && { parsedBody }
1255
+ });
1256
+ const strict = modernOnlyStrictRejection(route, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
1257
+ if (strict === void 0) return new Response(null, { status: 202 });
1258
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only endpoint (${strict.cell}): ${strict.message}`));
1259
+ return rejectionResponse(strict, echoableRequestId(parsedBody));
1260
+ }
1261
+ async function handle(request, requestOptions) {
1262
+ const authInfo = requestOptions?.authInfo;
1263
+ const classified = await classifyEntryRequest(request, requestOptions?.parsedBody);
1264
+ if (classified.step === "unreadable-body") return jsonRpcErrorResponse(400, -32700, "Parse error: the request body could not be read");
1265
+ if (classified.step === "no-json-body") {
1266
+ if (legacyHandler !== void 0) return legacyHandler(classified.forwardRequest, { ...authInfo !== void 0 && { authInfo } });
1267
+ return jsonRpcErrorResponse(400, -32700, "Parse error: the request body is not valid JSON");
1268
+ }
1269
+ const { outcome, body, parsedBody, forwardRequest } = classified;
1270
+ try {
1271
+ switch (outcome.kind) {
1272
+ case "reject":
1273
+ reportError(/* @__PURE__ */ new Error(`Rejected inbound request (${outcome.cell}): ${outcome.message}`));
1274
+ return rejectionResponse(outcome, echoableRequestId(body));
1275
+ case "modern": return await serveModern(outcome, request, authInfo);
1276
+ case "legacy": return await serveLegacyRoute(outcome, forwardRequest, authInfo, parsedBody);
1277
+ }
1278
+ } catch (error) {
1279
+ reportError(toError(error));
1280
+ return internalServerErrorResponse(echoableRequestId(body));
1281
+ }
1282
+ }
1283
+ const fetchFace = async (request, requestOptions) => {
1284
+ if (closed) throw new Error("This MCP handler has been closed");
1285
+ try {
1286
+ return await handle(request, requestOptions);
1287
+ } catch (error) {
1288
+ reportError(toError(error));
1289
+ return internalServerErrorResponse(echoableRequestId(requestOptions?.parsedBody));
1290
+ }
1291
+ };
1292
+ return {
1293
+ fetch: fetchFace,
1294
+ notify,
1295
+ bus,
1296
+ close: async () => {
1297
+ closed = true;
1298
+ listenRouter.closeAll();
1299
+ const closing = [...inflight].map((server) => server.close().catch(() => {}));
1300
+ inflight.clear();
1301
+ await Promise.all(closing);
1302
+ }
1303
+ };
1552
1304
  }
1553
1305
 
1554
1306
  //#endregion
@@ -1622,667 +1374,197 @@ function hostHeaderValidationResponse(req, allowedHostnames) {
1622
1374
  }
1623
1375
 
1624
1376
  //#endregion
1625
- //#region src/server/stdio.ts
1377
+ //#region src/server/middleware/originValidation.ts
1626
1378
  /**
1627
- * Server transport for stdio: this communicates with an MCP client by reading from the current process' `stdin` and writing to `stdout`.
1628
- *
1629
- * This transport is only available in Node.js environments.
1379
+ * Validate an `Origin` header against an allowlist of hostnames (port-agnostic).
1630
1380
  *
1631
- * @example
1632
- * ```ts source="./stdio.examples.ts#StdioServerTransport_basicUsage"
1633
- * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
1634
- * const transport = new StdioServerTransport();
1635
- * await server.connect(transport);
1636
- * ```
1381
+ * - A missing/empty `Origin` header passes: non-browser clients do not send one,
1382
+ * and only browser-originated requests carry the header this check defends against.
1383
+ * - Allowlist items are hostnames only (no scheme, no port), the same convention as
1384
+ * `validateHostHeader`. For IPv6, include brackets (e.g. `[::1]`).
1385
+ * - Any present value that cannot be parsed as an origin URL — including the literal
1386
+ * `null` origin browsers send for opaque contexts — is rejected (deny on failure).
1637
1387
  */
1638
- var StdioServerTransport = class {
1639
- _readBuffer = new ReadBuffer();
1640
- _started = false;
1641
- _closed = false;
1642
- constructor(_stdin = process.stdin, _stdout = process.stdout) {
1643
- this._stdin = _stdin;
1644
- this._stdout = _stdout;
1388
+ function validateOriginHeader(originHeader, allowedOriginHostnames) {
1389
+ if (originHeader === null || originHeader === void 0 || originHeader === "") return { ok: true };
1390
+ let hostname;
1391
+ try {
1392
+ hostname = new URL(originHeader).hostname;
1393
+ } catch {
1394
+ return {
1395
+ ok: false,
1396
+ errorCode: "invalid_origin_header",
1397
+ message: `Invalid Origin header: ${originHeader}`,
1398
+ originHeader
1399
+ };
1645
1400
  }
1646
- onclose;
1647
- onerror;
1648
- onmessage;
1649
- _ondata = (chunk) => {
1650
- this._readBuffer.append(chunk);
1651
- this.processReadBuffer();
1401
+ if (hostname === "") return {
1402
+ ok: false,
1403
+ errorCode: "invalid_origin_header",
1404
+ message: `Invalid Origin header: ${originHeader}`,
1405
+ originHeader
1652
1406
  };
1653
- _onerror = (error) => {
1654
- this.onerror?.(error);
1407
+ if (!allowedOriginHostnames.includes(hostname)) return {
1408
+ ok: false,
1409
+ errorCode: "invalid_origin",
1410
+ message: `Invalid Origin: ${hostname}`,
1411
+ originHeader,
1412
+ hostname
1655
1413
  };
1656
- _onstdouterror = (error) => {
1657
- this.onerror?.(error);
1658
- this.close().catch(() => {});
1414
+ return {
1415
+ ok: true,
1416
+ origin: originHeader,
1417
+ hostname
1659
1418
  };
1660
- /**
1661
- * Starts listening for messages on `stdin`.
1662
- */
1663
- async start() {
1664
- if (this._started) throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
1665
- this._started = true;
1666
- this._stdin.on("data", this._ondata);
1667
- this._stdin.on("error", this._onerror);
1668
- this._stdout.on("error", this._onstdouterror);
1669
- }
1670
- processReadBuffer() {
1671
- while (true) try {
1672
- const message = this._readBuffer.readMessage();
1673
- if (message === null) break;
1674
- this.onmessage?.(message);
1675
- } catch (error) {
1676
- this.onerror?.(error);
1677
- }
1678
- }
1679
- async close() {
1680
- if (this._closed) return;
1681
- this._closed = true;
1682
- this._stdin.off("data", this._ondata);
1683
- this._stdin.off("error", this._onerror);
1684
- this._stdout.off("error", this._onstdouterror);
1685
- if (this._stdin.listenerCount("data") === 0) this._stdin.pause();
1686
- this._readBuffer.clear();
1687
- this.onclose?.();
1688
- }
1689
- send(message) {
1690
- if (this._closed) return Promise.reject(/* @__PURE__ */ new Error("StdioServerTransport is closed"));
1691
- return new Promise((resolve, reject) => {
1692
- const json = serializeMessage(message);
1693
- let settled = false;
1694
- const onError = (error) => {
1695
- if (settled) return;
1696
- settled = true;
1697
- this._stdout.off("error", onError);
1698
- this._stdout.off("drain", onDrain);
1699
- reject(error);
1700
- };
1701
- const onDrain = () => {
1702
- if (settled) return;
1703
- settled = true;
1704
- this._stdout.off("error", onError);
1705
- this._stdout.off("drain", onDrain);
1706
- resolve();
1707
- };
1708
- this._stdout.once("error", onError);
1709
- if (this._stdout.write(json)) {
1710
- if (settled) return;
1711
- settled = true;
1712
- this._stdout.off("error", onError);
1713
- resolve();
1714
- } else if (!settled) this._stdout.once("drain", onDrain);
1715
- });
1716
- }
1717
- };
1419
+ }
1420
+ /**
1421
+ * Convenience allowlist of localhost-class origin hostnames, mirroring
1422
+ * `localhostAllowedHostnames`.
1423
+ */
1424
+ function localhostAllowedOrigins() {
1425
+ return [
1426
+ "localhost",
1427
+ "127.0.0.1",
1428
+ "[::1]"
1429
+ ];
1430
+ }
1431
+ /**
1432
+ * Web-standard `Request` helper for Origin validation: returns a `403` JSON-RPC
1433
+ * error response when the request's `Origin` header is not allowed, and
1434
+ * `undefined` when the request may proceed.
1435
+ *
1436
+ * ```ts
1437
+ * const rejected = originValidationResponse(request, localhostAllowedOrigins());
1438
+ * if (rejected) return rejected;
1439
+ * ```
1440
+ */
1441
+ function originValidationResponse(req, allowedOriginHostnames) {
1442
+ const result = validateOriginHeader(req.headers.get("origin"), allowedOriginHostnames);
1443
+ if (result.ok) return void 0;
1444
+ return Response.json({
1445
+ jsonrpc: "2.0",
1446
+ error: {
1447
+ code: -32e3,
1448
+ message: result.message
1449
+ },
1450
+ id: null
1451
+ }, {
1452
+ status: 403,
1453
+ headers: { "Content-Type": "application/json" }
1454
+ });
1455
+ }
1718
1456
 
1719
1457
  //#endregion
1720
- //#region src/server/streamableHttp.ts
1458
+ //#region src/server/requestStateCodec.ts
1459
+ const PREFIX = "v1.";
1460
+ function bytesToBase64Url(bytes) {
1461
+ let bin = "";
1462
+ for (const b of bytes) bin += String.fromCodePoint(b);
1463
+ return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
1464
+ }
1465
+ function constantTimeTagEqual(a, b) {
1466
+ if (a.length !== b.length) return false;
1467
+ let r = 0;
1468
+ for (let i = 0; i < a.length; i++) r |= a.codePointAt(i) ^ b.codePointAt(i);
1469
+ return r === 0;
1470
+ }
1471
+ function base64UrlToBytes(s) {
1472
+ const b64 = s.replaceAll("-", "+").replaceAll("_", "/");
1473
+ const bin = atob(b64);
1474
+ const bytes = new Uint8Array(bin.length);
1475
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.codePointAt(i);
1476
+ return bytes;
1477
+ }
1721
1478
  /**
1722
- * Server transport for Web Standards Streamable HTTP: this implements the MCP Streamable HTTP transport specification
1723
- * using Web Standard APIs (`Request`, `Response`, `ReadableStream`).
1724
- *
1725
- * This transport works on any runtime that supports Web Standards: Node.js 18+, Cloudflare Workers, Deno, Bun, etc.
1726
- *
1727
- * In stateful mode:
1728
- * - Session ID is generated and included in response headers
1729
- * - Session ID is always included in initialization responses
1730
- * - Requests with invalid session IDs are rejected with `404 Not Found`
1731
- * - Non-initialization requests without a session ID are rejected with `400 Bad Request`
1732
- * - State is maintained in-memory (connections, message history)
1733
- *
1734
- * In stateless mode:
1735
- * - No Session ID is included in any responses
1736
- * - No session validation is performed
1479
+ * Create an opt-in HMAC-SHA256 codec for the multi-round-trip `requestState`
1480
+ * (protocol revision 2026-07-28).
1737
1481
  *
1738
- * @example Stateful setup
1739
- * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateful"
1740
- * const server = new McpServer({ name: 'my-server', version: '1.0.0' });
1482
+ * `requestState` round-trips through the client and is attacker-controlled
1483
+ * input on re-entry. The SDK applies no protection of its own; this helper is
1484
+ * the convenience implementation of the spec's integrity MUST so authors don't
1485
+ * hand-roll HMAC. Wire shape:
1741
1486
  *
1742
- * const transport = new WebStandardStreamableHTTPServerTransport({
1743
- * sessionIdGenerator: () => crypto.randomUUID()
1744
- * });
1487
+ * "v1." b64url({"p":<payload>,"exp":<unixSeconds>,"b":<bindTag>?}) "." b64url(mac)
1745
1488
  *
1746
- * await server.connect(transport);
1747
- * ```
1489
+ * where `bindTag` is `b64url(HMAC(key, "mcp.requestState.bind:" + bind(ctx))[:16])`
1490
+ * — the binding value is never embedded raw.
1748
1491
  *
1749
- * @example Stateless setup
1750
- * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_stateless"
1751
- * const transport = new WebStandardStreamableHTTPServerTransport({
1752
- * sessionIdGenerator: undefined
1753
- * });
1754
- * ```
1492
+ * The codec is **signed, not encrypted**: the body is integrity-protected but
1493
+ * the client can base64url-decode it and read the payload (`p`) in clear. Do
1494
+ * not put secrets in the payload; use an AEAD construction if confidentiality
1495
+ * is required. The handler reads its payload back via the typed
1496
+ * `ctx.mcpReq.requestState<T>()` accessor — the seam has already run `verify`
1497
+ * (integrity proven, payload decoded) by the time the handler is entered.
1755
1498
  *
1756
- * @example Hono.js
1757
- * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_hono"
1758
- * app.all('/mcp', async c => {
1759
- * return transport.handleRequest(c.req.raw);
1760
- * });
1761
- * ```
1499
+ * Verification is fail-closed and constant-time (WebCrypto `subtle.verify` for
1500
+ * the body MAC; a fixed-length XOR-accumulator compare for the bind tag).
1501
+ * See `examples/mrtr/server.ts` for a worked end-to-end example.
1762
1502
  *
1763
- * @example Cloudflare Workers
1764
- * ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_workers"
1765
- * const worker = {
1766
- * async fetch(request: Request): Promise<Response> {
1767
- * return transport.handleRequest(request);
1768
- * }
1769
- * };
1770
- * ```
1503
+ * Design comparison (mcp.d `secureRequestState`, the peer SDK's reference
1504
+ * implementation): mcp.d additionally offers an AES-256-GCM encrypted mode and
1505
+ * derives independent cipher / bind-HMAC sub-keys from the operator secret via
1506
+ * HKDF-SHA256, with an auto-generated per-process ephemeral key when none is
1507
+ * supplied. This codec deliberately ships only the signed mode and a single
1508
+ * keyed HMAC (domain-separated by input prefix) — HKDF sub-key derivation and
1509
+ * an encrypted mode are intentionally out of scope for the initial release.
1771
1510
  */
1772
- var WebStandardStreamableHTTPServerTransport = class {
1773
- sessionIdGenerator;
1774
- _started = false;
1775
- _streamMapping = /* @__PURE__ */ new Map();
1776
- _requestToStreamMapping = /* @__PURE__ */ new Map();
1777
- _requestResponseMap = /* @__PURE__ */ new Map();
1778
- _initialized = false;
1779
- _enableJsonResponse = false;
1780
- _standaloneSseStreamId = "_GET_stream";
1781
- _eventStore;
1782
- _onsessioninitialized;
1783
- _onsessionclosed;
1784
- _allowedHosts;
1785
- _allowedOrigins;
1786
- _enableDnsRebindingProtection;
1787
- _retryInterval;
1788
- _supportedProtocolVersions;
1789
- sessionId;
1790
- onclose;
1791
- onerror;
1792
- onmessage;
1793
- constructor(options = {}) {
1794
- this.sessionIdGenerator = options.sessionIdGenerator;
1795
- this._enableJsonResponse = options.enableJsonResponse ?? false;
1796
- this._eventStore = options.eventStore;
1797
- this._onsessioninitialized = options.onsessioninitialized;
1798
- this._onsessionclosed = options.onsessionclosed;
1799
- this._allowedHosts = options.allowedHosts;
1800
- this._allowedOrigins = options.allowedOrigins;
1801
- this._enableDnsRebindingProtection = options.enableDnsRebindingProtection ?? false;
1802
- this._retryInterval = options.retryInterval;
1803
- this._supportedProtocolVersions = options.supportedProtocolVersions ?? SUPPORTED_PROTOCOL_VERSIONS;
1804
- }
1805
- /**
1806
- * Starts the transport. This is required by the {@linkcode Transport} interface but is a no-op
1807
- * for the Streamable HTTP transport as connections are managed per-request.
1808
- */
1809
- async start() {
1810
- if (this._started) throw new Error("Transport already started");
1811
- this._started = true;
1812
- }
1813
- /**
1814
- * Sets the supported protocol versions for header validation.
1815
- * Called by the server during {@linkcode server/server.Server.connect | connect()} to pass its supported versions.
1816
- */
1817
- setSupportedProtocolVersions(versions) {
1818
- this._supportedProtocolVersions = versions;
1819
- }
1820
- /**
1821
- * Helper to create a JSON error response
1822
- */
1823
- createJsonErrorResponse(status, code, message, options) {
1824
- const error = {
1825
- code,
1826
- message
1827
- };
1828
- if (options?.data !== void 0) error.data = options.data;
1829
- return Response.json({
1830
- jsonrpc: "2.0",
1831
- error,
1832
- id: null
1833
- }, {
1834
- status,
1835
- headers: {
1836
- "Content-Type": "application/json",
1837
- ...options?.headers
1838
- }
1839
- });
1840
- }
1841
- /**
1842
- * Validates request headers for DNS rebinding protection.
1843
- * @returns Error response if validation fails, `undefined` if validation passes.
1844
- */
1845
- validateRequestHeaders(req) {
1846
- if (!this._enableDnsRebindingProtection) return;
1847
- if (this._allowedHosts && this._allowedHosts.length > 0) {
1848
- const hostHeader = req.headers.get("host");
1849
- if (!hostHeader || !this._allowedHosts.includes(hostHeader)) {
1850
- const error = `Invalid Host header: ${hostHeader}`;
1851
- this.onerror?.(new Error(error));
1852
- return this.createJsonErrorResponse(403, -32e3, error);
1853
- }
1854
- }
1855
- if (this._allowedOrigins && this._allowedOrigins.length > 0) {
1856
- const originHeader = req.headers.get("origin");
1857
- if (originHeader && !this._allowedOrigins.includes(originHeader)) {
1858
- const error = `Invalid Origin header: ${originHeader}`;
1859
- this.onerror?.(new Error(error));
1860
- return this.createJsonErrorResponse(403, -32e3, error);
1861
- }
1862
- }
1863
- }
1864
- /**
1865
- * Handles an incoming HTTP request, whether `GET`, `POST`, or `DELETE`
1866
- * Returns a `Response` object (Web Standard)
1867
- */
1868
- async handleRequest(req, options) {
1869
- const validationError = this.validateRequestHeaders(req);
1870
- if (validationError) return validationError;
1871
- switch (req.method) {
1872
- case "POST": return this.handlePostRequest(req, options);
1873
- case "GET": return this.handleGetRequest(req);
1874
- case "DELETE": return this.handleDeleteRequest(req);
1875
- default: return this.handleUnsupportedRequest();
1876
- }
1877
- }
1878
- /**
1879
- * Writes a priming event to establish resumption capability.
1880
- * Only sends if `eventStore` is configured (opt-in for resumability) and
1881
- * the client's protocol version supports empty SSE data (>= `2025-11-25`).
1882
- */
1883
- async writePrimingEvent(controller, encoder, streamId, protocolVersion) {
1884
- if (!this._eventStore) return;
1885
- if (protocolVersion < "2025-11-25") return;
1886
- const primingEventId = await this._eventStore.storeEvent(streamId, {});
1887
- let primingEvent = `id: ${primingEventId}\ndata: \n\n`;
1888
- if (this._retryInterval !== void 0) primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`;
1889
- controller.enqueue(encoder.encode(primingEvent));
1890
- }
1891
- /**
1892
- * Handles `GET` requests for SSE stream
1893
- */
1894
- async handleGetRequest(req) {
1895
- if (!req.headers.get("accept")?.includes("text/event-stream")) {
1896
- this.onerror?.(/* @__PURE__ */ new Error("Not Acceptable: Client must accept text/event-stream"));
1897
- return this.createJsonErrorResponse(406, -32e3, "Not Acceptable: Client must accept text/event-stream");
1898
- }
1899
- const sessionError = this.validateSession(req);
1900
- if (sessionError) return sessionError;
1901
- const protocolError = this.validateProtocolVersion(req);
1902
- if (protocolError) return protocolError;
1903
- if (this._eventStore) {
1904
- const lastEventId = req.headers.get("last-event-id");
1905
- if (lastEventId) return this.replayEvents(lastEventId);
1906
- }
1907
- if (this._streamMapping.get(this._standaloneSseStreamId) !== void 0) {
1908
- this.onerror?.(/* @__PURE__ */ new Error("Conflict: Only one SSE stream is allowed per session"));
1909
- return this.createJsonErrorResponse(409, -32e3, "Conflict: Only one SSE stream is allowed per session");
1910
- }
1911
- const encoder = new TextEncoder();
1912
- let streamController;
1913
- const readable = new ReadableStream({
1914
- start: (controller) => {
1915
- streamController = controller;
1916
- },
1917
- cancel: () => {
1918
- this._streamMapping.delete(this._standaloneSseStreamId);
1919
- }
1920
- });
1921
- const headers = {
1922
- "Content-Type": "text/event-stream",
1923
- "Cache-Control": "no-cache, no-transform",
1924
- Connection: "keep-alive"
1925
- };
1926
- if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
1927
- this._streamMapping.set(this._standaloneSseStreamId, {
1928
- controller: streamController,
1929
- encoder,
1930
- cleanup: () => {
1931
- this._streamMapping.delete(this._standaloneSseStreamId);
1932
- try {
1933
- streamController.close();
1934
- } catch {}
1935
- }
1936
- });
1937
- return new Response(readable, { headers });
1938
- }
1939
- /**
1940
- * Replays events that would have been sent after the specified event ID
1941
- * Only used when resumability is enabled
1942
- */
1943
- async replayEvents(lastEventId) {
1944
- if (!this._eventStore) {
1945
- this.onerror?.(/* @__PURE__ */ new Error("Event store not configured"));
1946
- return this.createJsonErrorResponse(400, -32e3, "Event store not configured");
1947
- }
1948
- try {
1949
- let streamId;
1950
- if (this._eventStore.getStreamIdForEventId) {
1951
- streamId = await this._eventStore.getStreamIdForEventId(lastEventId);
1952
- if (!streamId) {
1953
- this.onerror?.(/* @__PURE__ */ new Error("Invalid event ID format"));
1954
- return this.createJsonErrorResponse(400, -32e3, "Invalid event ID format");
1955
- }
1956
- if (this._streamMapping.get(streamId) !== void 0) {
1957
- this.onerror?.(/* @__PURE__ */ new Error("Conflict: Stream already has an active connection"));
1958
- return this.createJsonErrorResponse(409, -32e3, "Conflict: Stream already has an active connection");
1959
- }
1960
- }
1961
- const headers = {
1962
- "Content-Type": "text/event-stream",
1963
- "Cache-Control": "no-cache, no-transform",
1964
- Connection: "keep-alive"
1511
+ function createRequestStateCodec(options) {
1512
+ const subtle = globalThis.crypto?.subtle;
1513
+ if (subtle === void 0) throw new TypeError("createRequestStateCodec requires the Web Crypto API (globalThis.crypto.subtle); see https://ts.sdk.modelcontextprotocol.io/v2/troubleshooting for the Node.js polyfill instructions");
1514
+ const keyBytes = typeof options.key === "string" ? new TextEncoder().encode(options.key) : Uint8Array.from(options.key);
1515
+ if (keyBytes.byteLength < 32) throw new RangeError(`createRequestStateCodec: key must be at least 32 bytes (got ${keyBytes.byteLength})`);
1516
+ const ttlSeconds = options.ttlSeconds ?? 600;
1517
+ if (!Number.isFinite(ttlSeconds)) throw new RangeError("createRequestStateCodec: ttlSeconds must be a finite number");
1518
+ const bind = options.bind;
1519
+ let cryptoKey;
1520
+ const importedKey = () => cryptoKey ??= subtle.importKey("raw", keyBytes, {
1521
+ name: "HMAC",
1522
+ hash: "SHA-256"
1523
+ }, false, ["sign", "verify"]);
1524
+ const utf8 = new TextEncoder();
1525
+ const BIND_LABEL = "mcp.requestState.bind:";
1526
+ const bindTag = async (value) => {
1527
+ return bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(BIND_LABEL + value))).slice(0, 16));
1528
+ };
1529
+ return {
1530
+ async mint(payload, ctx) {
1531
+ const envelope = {
1532
+ p: payload,
1533
+ exp: Math.floor(Date.now() / 1e3) + ttlSeconds
1965
1534
  };
1966
- if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
1967
- const encoder = new TextEncoder();
1968
- let streamController;
1969
- const readable = new ReadableStream({
1970
- start: (controller) => {
1971
- streamController = controller;
1972
- },
1973
- cancel: () => {}
1974
- });
1975
- const replayedStreamId = await this._eventStore.replayEventsAfter(lastEventId, { send: async (eventId, message) => {
1976
- if (!this.writeSSEEvent(streamController, encoder, message, eventId)) try {
1977
- streamController.close();
1978
- } catch {}
1979
- } });
1980
- this._streamMapping.set(replayedStreamId, {
1981
- controller: streamController,
1982
- encoder,
1983
- cleanup: () => {
1984
- this._streamMapping.delete(replayedStreamId);
1985
- try {
1986
- streamController.close();
1987
- } catch {}
1988
- }
1989
- });
1990
- return new Response(readable, { headers });
1991
- } catch (error) {
1992
- this.onerror?.(error);
1993
- return this.createJsonErrorResponse(500, -32e3, "Error replaying events");
1994
- }
1995
- }
1996
- /**
1997
- * Writes an event to an SSE stream via controller with proper formatting
1998
- */
1999
- writeSSEEvent(controller, encoder, message, eventId) {
2000
- try {
2001
- let eventData = `event: message\n`;
2002
- if (eventId) eventData += `id: ${eventId}\n`;
2003
- eventData += `data: ${JSON.stringify(message)}\n\n`;
2004
- controller.enqueue(encoder.encode(eventData));
2005
- return true;
2006
- } catch (error) {
2007
- this.onerror?.(error);
2008
- return false;
2009
- }
2010
- }
2011
- /**
2012
- * Handles unsupported requests (`PUT`, `PATCH`, etc.)
2013
- */
2014
- handleUnsupportedRequest() {
2015
- this.onerror?.(/* @__PURE__ */ new Error("Method not allowed."));
2016
- return Response.json({
2017
- jsonrpc: "2.0",
2018
- error: {
2019
- code: -32e3,
2020
- message: "Method not allowed."
2021
- },
2022
- id: null
2023
- }, {
2024
- status: 405,
2025
- headers: {
2026
- Allow: "GET, POST, DELETE",
2027
- "Content-Type": "application/json"
2028
- }
2029
- });
2030
- }
2031
- /**
2032
- * Handles `POST` requests containing JSON-RPC messages
2033
- */
2034
- async handlePostRequest(req, options) {
2035
- try {
2036
- const acceptHeader = req.headers.get("accept");
2037
- if (!acceptHeader?.includes("application/json") || !acceptHeader.includes("text/event-stream")) {
2038
- this.onerror?.(/* @__PURE__ */ new Error("Not Acceptable: Client must accept both application/json and text/event-stream"));
2039
- return this.createJsonErrorResponse(406, -32e3, "Not Acceptable: Client must accept both application/json and text/event-stream");
2040
- }
2041
- const ct = req.headers.get("content-type");
2042
- if (!ct || !ct.includes("application/json")) {
2043
- this.onerror?.(/* @__PURE__ */ new Error("Unsupported Media Type: Content-Type must be application/json"));
2044
- return this.createJsonErrorResponse(415, -32e3, "Unsupported Media Type: Content-Type must be application/json");
1535
+ if (bind !== void 0) {
1536
+ if (ctx === void 0) throw new TypeError("createRequestStateCodec: mint() requires ctx when a bind callback is configured");
1537
+ envelope.b = await bindTag(bind(ctx));
2045
1538
  }
2046
- const request = req;
2047
- let rawMessage;
2048
- if (options?.parsedBody === void 0) try {
2049
- rawMessage = await req.json();
2050
- } catch (error) {
2051
- this.onerror?.(error);
2052
- return this.createJsonErrorResponse(400, -32700, "Parse error: Invalid JSON");
2053
- }
2054
- else rawMessage = options.parsedBody;
2055
- let messages;
1539
+ const body = bytesToBase64Url(utf8.encode(JSON.stringify(envelope)));
1540
+ return `${PREFIX}${body}.${bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(PREFIX + body))))}`;
1541
+ },
1542
+ async verify(state, ctx) {
1543
+ const dot = state.lastIndexOf(".");
1544
+ if (!state.startsWith(PREFIX) || dot <= 3) throw new Error("malformed");
1545
+ const body = state.slice(3, dot);
1546
+ let macBytes;
2056
1547
  try {
2057
- messages = Array.isArray(rawMessage) ? rawMessage.map((msg) => JSONRPCMessageSchema.parse(msg)) : [JSONRPCMessageSchema.parse(rawMessage)];
2058
- } catch (error) {
2059
- this.onerror?.(error);
2060
- return this.createJsonErrorResponse(400, -32700, "Parse error: Invalid JSON-RPC message");
2061
- }
2062
- const isInitializationRequest = messages.some((element) => isInitializeRequest(element));
2063
- if (isInitializationRequest) {
2064
- if (this._initialized && this.sessionId !== void 0) {
2065
- this.onerror?.(/* @__PURE__ */ new Error("Invalid Request: Server already initialized"));
2066
- return this.createJsonErrorResponse(400, -32600, "Invalid Request: Server already initialized");
2067
- }
2068
- if (messages.length > 1) {
2069
- this.onerror?.(/* @__PURE__ */ new Error("Invalid Request: Only one initialization request is allowed"));
2070
- return this.createJsonErrorResponse(400, -32600, "Invalid Request: Only one initialization request is allowed");
2071
- }
2072
- this.sessionId = this.sessionIdGenerator?.();
2073
- this._initialized = true;
2074
- if (this.sessionId && this._onsessioninitialized) await Promise.resolve(this._onsessioninitialized(this.sessionId));
2075
- }
2076
- if (!isInitializationRequest) {
2077
- const sessionError = this.validateSession(req);
2078
- if (sessionError) return sessionError;
2079
- const protocolError = this.validateProtocolVersion(req);
2080
- if (protocolError) return protocolError;
1548
+ macBytes = base64UrlToBytes(state.slice(dot + 1));
1549
+ } catch {
1550
+ throw new Error("malformed");
2081
1551
  }
2082
- if (!messages.some((element) => isJSONRPCRequest(element))) {
2083
- for (const message of messages) this.onmessage?.(message, {
2084
- authInfo: options?.authInfo,
2085
- request
2086
- });
2087
- return new Response(null, { status: 202 });
2088
- }
2089
- const streamId = crypto.randomUUID();
2090
- const initRequest = messages.find((m) => isInitializeRequest(m));
2091
- const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
2092
- if (this._enableJsonResponse) return new Promise((resolve) => {
2093
- this._streamMapping.set(streamId, {
2094
- resolveJson: resolve,
2095
- cleanup: () => {
2096
- this._streamMapping.delete(streamId);
2097
- }
2098
- });
2099
- for (const message of messages) if (isJSONRPCRequest(message)) this._requestToStreamMapping.set(message.id, streamId);
2100
- for (const message of messages) this.onmessage?.(message, {
2101
- authInfo: options?.authInfo,
2102
- request
2103
- });
2104
- });
2105
- const encoder = new TextEncoder();
2106
- let streamController;
2107
- const readable = new ReadableStream({
2108
- start: (controller) => {
2109
- streamController = controller;
2110
- },
2111
- cancel: () => {
2112
- this._streamMapping.delete(streamId);
2113
- }
2114
- });
2115
- const headers = {
2116
- "Content-Type": "text/event-stream",
2117
- "Cache-Control": "no-cache",
2118
- Connection: "keep-alive"
2119
- };
2120
- if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
2121
- for (const message of messages) if (isJSONRPCRequest(message)) {
2122
- this._streamMapping.set(streamId, {
2123
- controller: streamController,
2124
- encoder,
2125
- cleanup: () => {
2126
- this._streamMapping.delete(streamId);
2127
- try {
2128
- streamController.close();
2129
- } catch {}
2130
- }
2131
- });
2132
- this._requestToStreamMapping.set(message.id, streamId);
2133
- }
2134
- await this.writePrimingEvent(streamController, encoder, streamId, clientProtocolVersion);
2135
- for (const message of messages) {
2136
- let closeSSEStream;
2137
- let closeStandaloneSSEStream;
2138
- if (isJSONRPCRequest(message) && this._eventStore && clientProtocolVersion >= "2025-11-25") {
2139
- closeSSEStream = () => {
2140
- this.closeSSEStream(message.id);
2141
- };
2142
- closeStandaloneSSEStream = () => {
2143
- this.closeStandaloneSSEStream();
2144
- };
2145
- }
2146
- this.onmessage?.(message, {
2147
- authInfo: options?.authInfo,
2148
- request,
2149
- closeSSEStream,
2150
- closeStandaloneSSEStream
2151
- });
2152
- }
2153
- return new Response(readable, {
2154
- status: 200,
2155
- headers
2156
- });
2157
- } catch (error) {
2158
- this.onerror?.(error);
2159
- return this.createJsonErrorResponse(400, -32700, "Parse error", { data: String(error) });
2160
- }
2161
- }
2162
- /**
2163
- * Handles `DELETE` requests to terminate sessions
2164
- */
2165
- async handleDeleteRequest(req) {
2166
- const sessionError = this.validateSession(req);
2167
- if (sessionError) return sessionError;
2168
- const protocolError = this.validateProtocolVersion(req);
2169
- if (protocolError) return protocolError;
2170
- await Promise.resolve(this._onsessionclosed?.(this.sessionId));
2171
- await this.close();
2172
- return new Response(null, { status: 200 });
2173
- }
2174
- /**
2175
- * Validates session ID for non-initialization requests.
2176
- * Returns `Response` error if invalid, `undefined` otherwise
2177
- */
2178
- validateSession(req) {
2179
- if (this.sessionIdGenerator === void 0) return;
2180
- if (!this._initialized) {
2181
- this.onerror?.(/* @__PURE__ */ new Error("Bad Request: Server not initialized"));
2182
- return this.createJsonErrorResponse(400, -32e3, "Bad Request: Server not initialized");
2183
- }
2184
- const sessionId = req.headers.get("mcp-session-id");
2185
- if (!sessionId) {
2186
- this.onerror?.(/* @__PURE__ */ new Error("Bad Request: Mcp-Session-Id header is required"));
2187
- return this.createJsonErrorResponse(400, -32e3, "Bad Request: Mcp-Session-Id header is required");
2188
- }
2189
- if (sessionId !== this.sessionId) {
2190
- this.onerror?.(/* @__PURE__ */ new Error("Session not found"));
2191
- return this.createJsonErrorResponse(404, -32001, "Session not found");
2192
- }
2193
- }
2194
- /**
2195
- * Validates the `MCP-Protocol-Version` header on incoming requests.
2196
- *
2197
- * For initialization: Version negotiation handles unknown versions gracefully
2198
- * (server responds with its supported version).
2199
- *
2200
- * For subsequent requests with `MCP-Protocol-Version` header:
2201
- * - Accept if in supported list
2202
- * - 400 if unsupported
2203
- *
2204
- * For HTTP requests without the `MCP-Protocol-Version` header:
2205
- * - Accept and default to the version negotiated at initialization
2206
- */
2207
- validateProtocolVersion(req) {
2208
- const protocolVersion = req.headers.get("mcp-protocol-version");
2209
- if (protocolVersion !== null && !this._supportedProtocolVersions.includes(protocolVersion)) {
2210
- const error = `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${this._supportedProtocolVersions.join(", ")})`;
2211
- this.onerror?.(new Error(error));
2212
- return this.createJsonErrorResponse(400, -32e3, error);
2213
- }
2214
- }
2215
- async close() {
2216
- for (const { cleanup } of this._streamMapping.values()) cleanup();
2217
- this._streamMapping.clear();
2218
- this._requestResponseMap.clear();
2219
- this.onclose?.();
2220
- }
2221
- /**
2222
- * Close an SSE stream for a specific request, triggering client reconnection.
2223
- * Use this to implement polling behavior during long-running operations -
2224
- * client will reconnect after the retry interval specified in the priming event.
2225
- */
2226
- closeSSEStream(requestId) {
2227
- const streamId = this._requestToStreamMapping.get(requestId);
2228
- if (!streamId) return;
2229
- const stream = this._streamMapping.get(streamId);
2230
- if (stream) stream.cleanup();
2231
- }
2232
- /**
2233
- * Close the standalone `GET` SSE stream, triggering client reconnection.
2234
- * Use this to implement polling behavior for server-initiated notifications.
2235
- */
2236
- closeStandaloneSSEStream() {
2237
- const stream = this._streamMapping.get(this._standaloneSseStreamId);
2238
- if (stream) stream.cleanup();
2239
- }
2240
- async send(message, options) {
2241
- let requestId = options?.relatedRequestId;
2242
- if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) requestId = message.id;
2243
- if (requestId === void 0) {
2244
- if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");
2245
- let eventId;
2246
- if (this._eventStore) eventId = await this._eventStore.storeEvent(this._standaloneSseStreamId, message);
2247
- const standaloneSse = this._streamMapping.get(this._standaloneSseStreamId);
2248
- if (standaloneSse === void 0) return;
2249
- if (standaloneSse.controller && standaloneSse.encoder) this.writeSSEEvent(standaloneSse.controller, standaloneSse.encoder, message, eventId);
2250
- return;
2251
- }
2252
- const streamId = this._requestToStreamMapping.get(requestId);
2253
- if (!streamId) throw new Error(`No connection established for request ID: ${String(requestId)}`);
2254
- const stream = this._streamMapping.get(streamId);
2255
- if (!this._enableJsonResponse && stream?.controller && stream?.encoder) {
2256
- let eventId;
2257
- if (this._eventStore) eventId = await this._eventStore.storeEvent(streamId, message);
2258
- this.writeSSEEvent(stream.controller, stream.encoder, message, eventId);
2259
- }
2260
- if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
2261
- this._requestResponseMap.set(requestId, message);
2262
- const relatedIds = [...this._requestToStreamMapping.entries()].filter(([_, sid]) => sid === streamId).map(([id]) => id);
2263
- if (relatedIds.every((id) => this._requestResponseMap.has(id))) {
2264
- if (!stream) throw new Error(`No connection established for request ID: ${String(requestId)}`);
2265
- if (this._enableJsonResponse && stream.resolveJson) {
2266
- const headers = { "Content-Type": "application/json" };
2267
- if (this.sessionId !== void 0) headers["mcp-session-id"] = this.sessionId;
2268
- const responses = relatedIds.map((id) => this._requestResponseMap.get(id));
2269
- if (responses.length === 1) stream.resolveJson(Response.json(responses[0], {
2270
- status: 200,
2271
- headers
2272
- }));
2273
- else stream.resolveJson(Response.json(responses, {
2274
- status: 200,
2275
- headers
2276
- }));
2277
- } else stream.cleanup();
2278
- for (const id of relatedIds) {
2279
- this._requestResponseMap.delete(id);
2280
- this._requestToStreamMapping.delete(id);
2281
- }
1552
+ if (!await subtle.verify("HMAC", await importedKey(), macBytes, utf8.encode(PREFIX + body))) throw new Error("mac");
1553
+ let envelope;
1554
+ try {
1555
+ envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(base64UrlToBytes(body)));
1556
+ } catch {
1557
+ throw new Error("malformed");
2282
1558
  }
1559
+ if (typeof envelope.exp !== "number" || envelope.exp < Math.floor(Date.now() / 1e3)) throw new Error("expired");
1560
+ if (bind !== void 0) {
1561
+ const expected = await bindTag(bind(ctx));
1562
+ if (envelope.b === void 0 || !constantTimeTagEqual(envelope.b, expected)) throw new Error("bind");
1563
+ } else if (envelope.b !== void 0) throw new Error("bind");
1564
+ return envelope.p;
2283
1565
  }
2284
- }
2285
- };
1566
+ };
1567
+ }
2286
1568
 
2287
1569
  //#endregion
2288
1570
  //#region src/fromJsonSchema.ts
@@ -2292,5 +1574,5 @@ function fromJsonSchema(schema, validator) {
2292
1574
  }
2293
1575
 
2294
1576
  //#endregion
2295
- export { AjvJsonSchemaValidator, CfWorkerJsonSchemaValidator, DEFAULT_NEGOTIATED_PROTOCOL_VERSION, DEFAULT_REQUEST_TIMEOUT_MSEC, ExperimentalMcpServerTasks, ExperimentalServerTasks, INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, InMemoryTaskMessageQueue, InMemoryTaskStore, JSONRPC_VERSION, LATEST_PROTOCOL_VERSION, METHOD_NOT_FOUND, McpServer, OAuthError, OAuthErrorCode, PARSE_ERROR, ProtocolError, ProtocolErrorCode, RELATED_TASK_META_KEY, ReadBuffer, ResourceTemplate, SUPPORTED_PROTOCOL_VERSIONS, SdkError, SdkErrorCode, Server, StdioServerTransport, UriTemplate, UrlElicitationRequiredError, WebStandardStreamableHTTPServerTransport, assertClientRequestTaskCapability, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, assertToolsCallTaskCapability, checkResourceAllowed, completable, createFetchWithInit, deserializeMessage, fromJsonSchema, getDisplayName, hostHeaderValidationResponse, isCompletable, isInitializeRequest, isInitializedNotification, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResultResponse, isTaskAugmentedRequestParams, isTerminal, localhostAllowedHostnames, parseJSONRPCMessage, resourceUrlFromServerUrl, serializeMessage, takeResult, toArrayAsync, validateHostHeader };
1577
+ export { BAGGAGE_META_KEY, CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, DEFAULT_NEGOTIATED_PROTOCOL_VERSION, DEFAULT_REQUEST_TIMEOUT_MSEC, INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, InMemoryServerEventBus, InMemoryTransport, JSONRPC_VERSION, LATEST_PROTOCOL_VERSION, LOG_LEVEL_META_KEY, METHOD_NOT_FOUND, McpServer, MissingRequiredClientCapabilityError, OAuthError, OAuthErrorCode, PARSE_ERROR, PROTOCOL_VERSION_META_KEY, PerRequestHTTPServerTransport, ProtocolError, ProtocolErrorCode, RELATED_TASK_META_KEY, ReadBuffer, ResourceNotFoundError, ResourceTemplate, STDIO_DEFAULT_MAX_BUFFER_SIZE, SUBSCRIPTION_ID_META_KEY, SUPPORTED_PROTOCOL_VERSIONS, SdkError, SdkErrorCode, SdkHttpError, Server, TRACEPARENT_META_KEY, TRACESTATE_META_KEY, UnsupportedProtocolVersionError, UriTemplate, UrlElicitationRequiredError, WebStandardStreamableHTTPServerTransport, acceptedContent, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate, checkResourceAllowed, classifyInboundRequest, completable, createFetchWithInit, createMcpHandler, createRequestStateCodec, deserializeMessage, fromJsonSchema, getDisplayName, hostHeaderValidationResponse, inputRequired, inputResponse, isCallToolResult, isCompletable, isInitializeRequest, isInitializedNotification, isInputRequiredResult, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResponse, isJSONRPCResultResponse, isLegacyRequest, isSpecType, isTaskAugmentedRequestParams, legacyStatelessFallback, localhostAllowedHostnames, localhostAllowedOrigins, originValidationResponse, parseJSONRPCMessage, resourceUrlFromServerUrl, serializeMessage, specTypeSchemas, validateHostHeader, validateOriginHeader };
2296
1578
  //# sourceMappingURL=index.mjs.map