agents 0.14.5 → 0.15.0

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.
package/dist/mcp/index.js CHANGED
@@ -4,8 +4,9 @@ import { Agent, getAgentByName, getCurrentAgent } from "../index.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
6
6
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7
- import { ElicitRequestSchema, InitializeRequestSchema, JSONRPCMessageSchema, SUPPORTED_PROTOCOL_VERSIONS, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResultResponse } from "@modelcontextprotocol/sdk/types.js";
7
+ import { ElicitRequestSchema, InitializeRequestSchema, JSONRPCMessageSchema, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResultResponse } from "@modelcontextprotocol/sdk/types.js";
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
9
10
  //#region src/mcp/sse-keepalive.ts
10
11
  /**
11
12
  * Shared SSE keepalive utility for MCP transports.
@@ -1005,512 +1006,291 @@ var StreamableHTTPEdgeClientTransport = class extends StreamableHTTPClientTransp
1005
1006
  };
1006
1007
  //#endregion
1007
1008
  //#region src/mcp/worker-transport.ts
1008
- const MCP_PROTOCOL_VERSION_HEADER = "MCP-Protocol-Version";
1009
- const RESTORE_REQUEST_ID = "__restore__";
1010
- var WorkerTransport = class {
1011
- constructor(options) {
1012
- this.started = false;
1013
- this.initialized = false;
1014
- this.enableJsonResponse = false;
1015
- this.standaloneSseStreamId = "_GET_stream";
1016
- this.streamMapping = /* @__PURE__ */ new Map();
1017
- this.requestToStreamMapping = /* @__PURE__ */ new Map();
1018
- this.requestResponseMap = /* @__PURE__ */ new Map();
1019
- this.stateRestored = false;
1020
- this.sessionIdGenerator = options?.sessionIdGenerator;
1021
- this.enableJsonResponse = options?.enableJsonResponse ?? false;
1022
- this.onsessioninitialized = options?.onsessioninitialized;
1023
- this.onsessionclosed = options?.onsessionclosed;
1024
- this.corsOptions = options?.corsOptions;
1025
- this.storage = options?.storage;
1026
- this.eventStore = options?.eventStore;
1027
- this.retryInterval = options?.retryInterval;
1009
+ /**
1010
+ * WorkerTransport
1011
+ *
1012
+ * Thin Cloudflare-Workers wrapper around the official MCP SDK
1013
+ * `WebStandardStreamableHTTPServerTransport`. The wrapper layers a couple of
1014
+ * Workers-specific concerns on top of the SDK transport without forking it:
1015
+ *
1016
+ * 1. **CORS** — preflight handling and response-header injection,
1017
+ * configurable via `corsOptions`.
1018
+ * 2. **Persistent transport state** when a `storage` adapter
1019
+ * (`MCPStorageApi`) is supplied, the wrapper persists
1020
+ * `{sessionId, initialized, initializeParams}` so that an MCP session can
1021
+ * survive DO hibernation / eviction. On the first request after a cold
1022
+ * start, the saved initialize params are replayed through the `Server`
1023
+ * so client capabilities are re-established.
1024
+ * 3. **SSE keepalive** — SSE responses are wrapped in a TransformStream that
1025
+ * injects a `: keepalive\n\n` comment frame every 25s so the Cloudflare
1026
+ * edge ~5min idle-stream watchdog doesn't kill long-running tool calls.
1027
+ * Disabled on the standalone GET stream when an `eventStore` is
1028
+ * configured clients recover idle drops via `Last-Event-ID` instead.
1029
+ * POST response streams always keepalive (no resumption path during a
1030
+ * mid-flight tool call). See cloudflare/agents#1583.
1031
+ *
1032
+ * Everything else (session validation, SSE streaming, protocol-version
1033
+ * negotiation, event-store resumability, etc.) is delegated to the SDK
1034
+ * transport.
1035
+ */
1036
+ /** Sentinel id used when replaying the persisted initialize request. */
1037
+ const RESTORE_REQUEST_ID = "__worker_transport_restore__";
1038
+ const DEFAULT_CORS_OPTIONS = {
1039
+ origin: "*",
1040
+ headers: "Content-Type, Accept, Authorization, mcp-session-id, MCP-Protocol-Version",
1041
+ methods: "GET, POST, DELETE, OPTIONS",
1042
+ exposeHeaders: "mcp-session-id",
1043
+ maxAge: 86400
1044
+ };
1045
+ var WorkerTransport = class extends WebStandardStreamableHTTPServerTransport {
1046
+ constructor(options = {}) {
1047
+ const { corsOptions, storage, onsessioninitialized, ...sdkOptions } = options;
1048
+ super({
1049
+ ...sdkOptions,
1050
+ onsessioninitialized: void 0
1051
+ });
1052
+ this._stateRestored = false;
1053
+ this._bridgeInstalled = false;
1054
+ this._keepaliveCleanups = /* @__PURE__ */ new Map();
1055
+ this._closedRequestIds = /* @__PURE__ */ new Set();
1056
+ this._corsOptions = corsOptions;
1057
+ this._storage = storage;
1058
+ this._userOnSessionInitialized = onsessioninitialized;
1028
1059
  }
1029
1060
  /**
1030
- * Restore transport state from persistent storage.
1031
- * This is automatically called on start.
1061
+ * Backwards-compatible alias for the SDK's internal `_started` flag.
1062
+ * Several callers and tests check `transport.started` directly.
1032
1063
  */
1033
- async restoreState() {
1034
- if (!this.storage || this.stateRestored) return;
1035
- const state = await Promise.resolve(this.storage.get());
1036
- if (state) {
1037
- this.sessionId = state.sessionId;
1038
- this.initialized = state.initialized;
1039
- if (state.initializeParams && this.onmessage) this.onmessage({
1040
- jsonrpc: "2.0",
1041
- id: RESTORE_REQUEST_ID,
1042
- method: "initialize",
1043
- params: state.initializeParams
1044
- });
1045
- }
1046
- this.stateRestored = true;
1064
+ get started() {
1065
+ return this._started;
1047
1066
  }
1048
1067
  /**
1049
- * Persist current transport state to storage.
1068
+ * Top-level request entry point. Handles CORS preflight, restores any
1069
+ * persisted state on first invocation, then delegates to the SDK transport
1070
+ * and finally appends CORS headers + keepalive to whatever response comes
1071
+ * back.
1050
1072
  */
1051
- async saveState() {
1052
- if (!this.storage) return;
1053
- const state = {
1054
- sessionId: this.sessionId,
1055
- initialized: this.initialized,
1056
- initializeParams: this.initializeParams
1057
- };
1058
- await Promise.resolve(this.storage.set(state));
1073
+ async handleRequest(request, options) {
1074
+ if (request.method === "OPTIONS") return new Response(null, { headers: this.getCorsHeaders({ forPreflight: true }) });
1075
+ await this.restoreState();
1076
+ this.installOnSessionInitializedBridge();
1077
+ await this.captureInitializeParams(request, options);
1078
+ const requestIdForKeepalive = request.method === "GET" ? "_standalone" : this._pendingRequestId;
1079
+ const response = await super.handleRequest(request, options);
1080
+ return this.withCorsHeaders(this.withKeepalive(this.normalizeAllowHeader(response), requestIdForKeepalive));
1059
1081
  }
1060
- async start() {
1061
- if (this.started) throw new Error("Transport already started");
1062
- this.started = true;
1082
+ /**
1083
+ * The SDK's 405 responses advertise `Allow: GET, POST, DELETE` because
1084
+ * OPTIONS is handled outside the SDK. Since our wrapper *does* handle
1085
+ * OPTIONS, advertise it in `Allow` so clients can probe accurately.
1086
+ */
1087
+ normalizeAllowHeader(response) {
1088
+ if (response.status !== 405) return response;
1089
+ const allow = response.headers.get("Allow");
1090
+ if (!allow || allow.includes("OPTIONS")) return response;
1091
+ const headers = new Headers(response.headers);
1092
+ headers.set("Allow", `${allow}, OPTIONS`);
1093
+ return new Response(response.body, {
1094
+ status: response.status,
1095
+ statusText: response.statusText,
1096
+ headers
1097
+ });
1098
+ }
1099
+ closeSSEStream(requestId) {
1100
+ this._keepaliveCleanups.get(requestId)?.();
1101
+ this._keepaliveCleanups.delete(requestId);
1102
+ this._closedRequestIds.add(requestId);
1103
+ super.closeSSEStream(requestId);
1104
+ }
1105
+ closeStandaloneSSEStream() {
1106
+ this._keepaliveCleanups.get("_standalone")?.();
1107
+ this._keepaliveCleanups.delete("_standalone");
1108
+ super.closeStandaloneSSEStream();
1109
+ }
1110
+ async close() {
1111
+ for (const cleanup of Array.from(this._keepaliveCleanups.values())) cleanup();
1112
+ this._keepaliveCleanups.clear();
1113
+ this._closedRequestIds.clear();
1114
+ await super.close();
1063
1115
  }
1064
1116
  /**
1065
- * Validates the MCP-Protocol-Version header on incoming requests.
1117
+ * Swallow two classes of message that would otherwise surface as
1118
+ * unhandled rejections from the SDK transport's `send()`:
1066
1119
  *
1067
- * This performs a simple check: if a version header is present, it must be
1068
- * in the SUPPORTED_PROTOCOL_VERSIONS list. We do not track the negotiated
1069
- * version or enforce version consistency across requests - the SDK handles
1070
- * version negotiation during initialization, and we simply reject any
1071
- * explicitly unsupported versions.
1120
+ * 1. Replayed initialize responses (the `RESTORE_REQUEST_ID` sentinel)
1121
+ * we synthesise these in `restoreState()` to rebuild server
1122
+ * capabilities; there's no real client waiting for the response.
1123
+ * 2. Sends for a request id whose SSE stream has been deliberately
1124
+ * closed via `closeSSEStream`. The protocol layer's tool-handler
1125
+ * promise may settle after the close, and the SDK's `send()` throws
1126
+ * "No connection established" — a race the pre-refactor transport
1127
+ * silently swallowed.
1072
1128
  *
1073
- * - Header present and supported: Accept
1074
- * - Header present and unsupported: 400 Bad Request
1075
- * - Header missing: Accept (version validation is optional)
1129
+ * Everything else is delegated. We use `await super.send(...)` rather
1130
+ * than `return super.send(...)` so any rejection is observed inside this
1131
+ * async frame; without the await, the test runner's
1132
+ * unhandled-rejection tracker can fire before the caller's own `await`
1133
+ * observes it.
1076
1134
  */
1077
- validateProtocolVersion(request) {
1078
- const protocolVersion = request.headers.get(MCP_PROTOCOL_VERSION_HEADER);
1079
- if (protocolVersion !== null && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) return new Response(JSON.stringify({
1080
- jsonrpc: "2.0",
1081
- error: {
1082
- code: -32e3,
1083
- message: `Bad Request: Unsupported protocol version: ${protocolVersion} (supported versions: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")})`
1135
+ async send(message, options) {
1136
+ let requestId = options?.relatedRequestId;
1137
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) requestId = message.id;
1138
+ if (requestId === RESTORE_REQUEST_ID) return;
1139
+ if (requestId !== void 0 && this._closedRequestIds.has(requestId)) return;
1140
+ await super.send(message, options);
1141
+ }
1142
+ /**
1143
+ * If the response is an SSE stream, tee the body through a TransformStream
1144
+ * that injects a `: keepalive\n\n` comment frame every 25s. The interval
1145
+ * is cleared when the wrapped stream closes — which happens both when the
1146
+ * SDK ends the underlying stream naturally and when `closeSSEStream` is
1147
+ * called.
1148
+ *
1149
+ * Keepalive policy:
1150
+ * - POST response streams (`key` is a request id): always keepalive.
1151
+ * In-progress tool calls have no recovery path — if the stream drops
1152
+ * mid-execution the result is lost — so we keep it under the
1153
+ * Cloudflare edge ~5min idle watchdog.
1154
+ * - Standalone GET stream (`key === "_standalone"`): keepalive only
1155
+ * when no `eventStore` is configured. When resumability is enabled,
1156
+ * clients reconnect with `Last-Event-ID` after an idle drop, so we
1157
+ * skip the keepalive and let the DO hibernate.
1158
+ *
1159
+ * Uses the shared `sse-keepalive` constants so both this wrapper and
1160
+ * `McpAgent.serve()` write identical frames at the same cadence.
1161
+ * See cloudflare/agents#1583.
1162
+ */
1163
+ withKeepalive(response, key) {
1164
+ if (!(response.headers.get("Content-Type") ?? "").includes("text/event-stream") || !response.body) return response;
1165
+ if (key === "_standalone" && this.eventStoreConfigured()) return response;
1166
+ const encoder = new TextEncoder();
1167
+ let intervalId;
1168
+ let controllerRef;
1169
+ const clear = () => {
1170
+ if (intervalId !== void 0) {
1171
+ clearInterval(intervalId);
1172
+ intervalId = void 0;
1173
+ }
1174
+ if (key !== void 0) this._keepaliveCleanups.delete(key);
1175
+ };
1176
+ const transform = new TransformStream({
1177
+ start: (controller) => {
1178
+ controllerRef = controller;
1179
+ intervalId = setInterval(() => {
1180
+ try {
1181
+ controllerRef?.enqueue(encoder.encode(KEEPALIVE_FRAME));
1182
+ } catch {
1183
+ clear();
1184
+ }
1185
+ }, KEEPALIVE_INTERVAL_MS);
1186
+ if (key !== void 0) this._keepaliveCleanups.set(key, clear);
1084
1187
  },
1085
- id: null
1086
- }), {
1087
- status: 400,
1088
- headers: {
1089
- "Content-Type": "application/json",
1090
- ...this.getHeaders()
1188
+ transform(chunk, controller) {
1189
+ controller.enqueue(chunk);
1190
+ },
1191
+ flush() {
1192
+ clear();
1193
+ },
1194
+ cancel() {
1195
+ clear();
1091
1196
  }
1092
1197
  });
1198
+ const piped = response.body.pipeThrough(transform);
1199
+ return new Response(piped, {
1200
+ status: response.status,
1201
+ statusText: response.statusText,
1202
+ headers: response.headers
1203
+ });
1204
+ }
1205
+ /**
1206
+ * Does the SDK transport have an `eventStore`? Reaches into the SDK's
1207
+ * private field because the option isn't surfaced on the public API —
1208
+ * we only need a yes/no for keepalive policy.
1209
+ */
1210
+ eventStoreConfigured() {
1211
+ return this._eventStore !== void 0;
1093
1212
  }
1094
- getHeaders({ forPreflight } = {}) {
1095
- const options = {
1096
- origin: "*",
1097
- headers: "Content-Type, Accept, Authorization, mcp-session-id, MCP-Protocol-Version",
1098
- methods: "GET, POST, DELETE, OPTIONS",
1099
- exposeHeaders: "mcp-session-id",
1100
- maxAge: 86400,
1101
- ...this.corsOptions
1213
+ getCorsHeaders({ forPreflight } = {}) {
1214
+ const merged = {
1215
+ ...DEFAULT_CORS_OPTIONS,
1216
+ ...this._corsOptions
1102
1217
  };
1103
1218
  if (forPreflight) return {
1104
- "Access-Control-Allow-Origin": options.origin,
1105
- "Access-Control-Allow-Headers": options.headers,
1106
- "Access-Control-Allow-Methods": options.methods,
1107
- "Access-Control-Max-Age": options.maxAge.toString()
1219
+ "Access-Control-Allow-Origin": merged.origin,
1220
+ "Access-Control-Allow-Headers": merged.headers,
1221
+ "Access-Control-Allow-Methods": merged.methods,
1222
+ "Access-Control-Max-Age": String(merged.maxAge)
1108
1223
  };
1109
1224
  return {
1110
- "Access-Control-Allow-Origin": options.origin,
1111
- "Access-Control-Expose-Headers": options.exposeHeaders
1225
+ "Access-Control-Allow-Origin": merged.origin,
1226
+ "Access-Control-Expose-Headers": merged.exposeHeaders
1112
1227
  };
1113
1228
  }
1114
- async handleRequest(request, parsedBody) {
1115
- await this.restoreState();
1116
- switch (request.method) {
1117
- case "OPTIONS": return this.handleOptionsRequest(request);
1118
- case "GET": return this.handleGetRequest(request);
1119
- case "POST": return this.handlePostRequest(request, parsedBody);
1120
- case "DELETE": return this.handleDeleteRequest(request);
1121
- default: return this.handleUnsupportedRequest();
1122
- }
1123
- }
1124
- async handleGetRequest(request) {
1125
- if (!request.headers.get("Accept")?.includes("text/event-stream")) return new Response(JSON.stringify({
1126
- jsonrpc: "2.0",
1127
- error: {
1128
- code: -32e3,
1129
- message: "Not Acceptable: Client must accept text/event-stream"
1130
- },
1131
- id: null
1132
- }), {
1133
- status: 406,
1134
- headers: {
1135
- "Content-Type": "application/json",
1136
- ...this.getHeaders()
1137
- }
1229
+ withCorsHeaders(response) {
1230
+ const headers = new Headers(response.headers);
1231
+ for (const [k, v] of Object.entries(this.getCorsHeaders())) headers.set(k, v);
1232
+ return new Response(response.body, {
1233
+ status: response.status,
1234
+ statusText: response.statusText,
1235
+ headers
1138
1236
  });
1139
- const sessionError = this.validateSession(request);
1140
- if (sessionError) return sessionError;
1141
- const versionError = this.validateProtocolVersion(request);
1142
- if (versionError) return versionError;
1143
- let streamId = this.standaloneSseStreamId;
1144
- const lastEventId = request.headers.get("Last-Event-ID");
1145
- if (lastEventId && this.eventStore) {
1146
- const eventStreamId = await this.eventStore.getStreamIdForEventId?.(lastEventId);
1147
- if (eventStreamId) streamId = eventStreamId;
1148
- }
1149
- if (this.streamMapping.get(streamId) !== void 0) return new Response(JSON.stringify({
1150
- jsonrpc: "2.0",
1151
- error: {
1152
- code: -32e3,
1153
- message: "Conflict: Only one SSE stream is allowed per session"
1154
- },
1155
- id: null
1156
- }), {
1157
- status: 409,
1158
- headers: {
1159
- "Content-Type": "application/json",
1160
- ...this.getHeaders()
1161
- }
1162
- });
1163
- const { readable, writable } = new TransformStream();
1164
- const writer = writable.getWriter();
1165
- const encoder = new TextEncoder();
1166
- const headers = new Headers({
1167
- "Content-Type": "text/event-stream",
1168
- "Cache-Control": "no-cache",
1169
- Connection: "keep-alive",
1170
- ...this.getHeaders()
1171
- });
1172
- if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
1173
- const keepAlive = this.eventStore ? void 0 : startKeepalive(writer, encoder);
1174
- const cleanup = () => {
1175
- if (keepAlive !== void 0) clearInterval(keepAlive);
1176
- this.streamMapping.delete(streamId);
1177
- writer.close().catch(() => {});
1237
+ }
1238
+ installOnSessionInitializedBridge() {
1239
+ if (this._bridgeInstalled) return;
1240
+ const sdk = this;
1241
+ sdk._onsessioninitialized = async (sessionId) => {
1242
+ if (this._userOnSessionInitialized) await Promise.resolve(this._userOnSessionInitialized(sessionId));
1243
+ await this.saveState();
1178
1244
  };
1179
- this.streamMapping.set(streamId, {
1180
- writer,
1181
- encoder,
1182
- cleanup
1183
- });
1184
- if (this.retryInterval !== void 0) await writer.write(encoder.encode(`retry: ${this.retryInterval}\n\n`));
1185
- if (lastEventId && this.eventStore) {
1186
- const replayedStreamId = await this.eventStore.replayEventsAfter(lastEventId, { send: async (eventId, message) => {
1187
- const data = `id: ${eventId}\nevent: message\ndata: ${JSON.stringify(message)}\n\n`;
1188
- await writer.write(encoder.encode(data));
1189
- } });
1190
- if (replayedStreamId !== streamId) {
1191
- this.streamMapping.delete(streamId);
1192
- streamId = replayedStreamId;
1193
- this.streamMapping.set(streamId, {
1194
- writer,
1195
- encoder,
1196
- cleanup
1197
- });
1198
- }
1199
- }
1200
- return new Response(readable, { headers });
1245
+ this._bridgeInstalled = true;
1201
1246
  }
1202
- async handlePostRequest(request, parsedBody) {
1203
- const acceptHeader = request.headers.get("Accept");
1204
- if (!acceptHeader?.includes("application/json") || !acceptHeader?.includes("text/event-stream")) return new Response(JSON.stringify({
1205
- jsonrpc: "2.0",
1206
- error: {
1207
- code: -32e3,
1208
- message: "Not Acceptable: Client must accept both application/json and text/event-stream"
1209
- },
1210
- id: null
1211
- }), {
1212
- status: 406,
1213
- headers: {
1214
- "Content-Type": "application/json",
1215
- ...this.getHeaders()
1216
- }
1217
- });
1218
- if (!request.headers.get("Content-Type")?.includes("application/json")) return new Response(JSON.stringify({
1219
- jsonrpc: "2.0",
1220
- error: {
1221
- code: -32e3,
1222
- message: "Unsupported Media Type: Content-Type must be application/json"
1223
- },
1224
- id: null
1225
- }), {
1226
- status: 415,
1227
- headers: {
1228
- "Content-Type": "application/json",
1229
- ...this.getHeaders()
1230
- }
1231
- });
1232
- let rawMessage = parsedBody;
1233
- if (rawMessage === void 0) try {
1234
- rawMessage = await request.json();
1235
- } catch {
1236
- return new Response(JSON.stringify({
1237
- jsonrpc: "2.0",
1238
- error: {
1239
- code: -32700,
1240
- message: "Parse error: Invalid JSON"
1241
- },
1242
- id: null
1243
- }), {
1244
- status: 400,
1245
- headers: {
1246
- "Content-Type": "application/json",
1247
- ...this.getHeaders()
1248
- }
1249
- });
1250
- }
1251
- let messages;
1247
+ async captureInitializeParams(request, handleOptions) {
1248
+ this._pendingRequestId = void 0;
1249
+ if (request.method !== "POST") return;
1252
1250
  try {
1253
- if (Array.isArray(rawMessage)) messages = rawMessage.map((msg) => JSONRPCMessageSchema.parse(msg));
1254
- else messages = [JSONRPCMessageSchema.parse(rawMessage)];
1255
- } catch {
1256
- return new Response(JSON.stringify({
1257
- jsonrpc: "2.0",
1258
- error: {
1259
- code: -32700,
1260
- message: "Parse error: Invalid JSON-RPC message"
1261
- },
1262
- id: null
1263
- }), {
1264
- status: 400,
1265
- headers: {
1266
- "Content-Type": "application/json",
1267
- ...this.getHeaders()
1268
- }
1269
- });
1270
- }
1271
- const requestInfo = {
1272
- headers: Object.fromEntries(request.headers.entries()),
1273
- url: new URL(request.url)
1274
- };
1275
- const isInitializationRequest = messages.some(isInitializeRequest);
1276
- if (isInitializationRequest) {
1277
- if (this.initialized && this.sessionId !== void 0) return new Response(JSON.stringify({
1278
- jsonrpc: "2.0",
1279
- error: {
1280
- code: -32600,
1281
- message: "Invalid Request: Server already initialized"
1282
- },
1283
- id: null
1284
- }), {
1285
- status: 400,
1286
- headers: {
1287
- "Content-Type": "application/json",
1288
- ...this.getHeaders()
1289
- }
1290
- });
1291
- if (messages.length > 1) return new Response(JSON.stringify({
1292
- jsonrpc: "2.0",
1293
- error: {
1294
- code: -32600,
1295
- message: "Invalid Request: Only one initialization request is allowed"
1296
- },
1297
- id: null
1298
- }), {
1299
- status: 400,
1300
- headers: {
1301
- "Content-Type": "application/json",
1302
- ...this.getHeaders()
1303
- }
1304
- });
1305
- this.sessionId = this.sessionIdGenerator?.();
1306
- this.initialized = true;
1307
- const initMessage = messages.find(isInitializeRequest);
1308
- if (initMessage && isInitializeRequest(initMessage)) this.initializeParams = {
1309
- capabilities: initMessage.params.capabilities,
1310
- clientInfo: initMessage.params.clientInfo,
1311
- protocolVersion: initMessage.params.protocolVersion
1251
+ const parsed = handleOptions?.parsedBody ?? await request.clone().json();
1252
+ const messages = Array.isArray(parsed) ? parsed : [parsed];
1253
+ const init = messages.find((m) => typeof m === "object" && m !== null && isInitializeRequest(m));
1254
+ if (init && isInitializeRequest(init)) this._capturedInitializeParams = {
1255
+ capabilities: init.params.capabilities,
1256
+ clientInfo: init.params.clientInfo,
1257
+ protocolVersion: init.params.protocolVersion
1312
1258
  };
1313
- await this.saveState();
1314
- if (this.sessionId && this.onsessioninitialized) this.onsessioninitialized(this.sessionId);
1315
- }
1316
- if (!isInitializationRequest) {
1317
- const sessionError = this.validateSession(request);
1318
- if (sessionError) return sessionError;
1319
- const versionError = this.validateProtocolVersion(request);
1320
- if (versionError) return versionError;
1321
- }
1322
- if (!messages.some(isJSONRPCRequest)) {
1323
- for (const message of messages) this.onmessage?.(message, { requestInfo });
1324
- return new Response(null, {
1325
- status: 202,
1326
- headers: { ...this.getHeaders() }
1327
- });
1328
- }
1329
- const streamId = crypto.randomUUID();
1330
- if (this.enableJsonResponse) return new Promise((resolve) => {
1331
- this.streamMapping.set(streamId, {
1332
- resolveJson: resolve,
1333
- cleanup: () => {
1334
- this.streamMapping.delete(streamId);
1335
- }
1336
- });
1337
- for (const message of messages) if (isJSONRPCRequest(message)) this.requestToStreamMapping.set(message.id, streamId);
1338
- for (const message of messages) this.onmessage?.(message, { requestInfo });
1339
- });
1340
- const { readable, writable } = new TransformStream();
1341
- const writer = writable.getWriter();
1342
- const encoder = new TextEncoder();
1343
- const headers = new Headers({
1344
- "Content-Type": "text/event-stream",
1345
- "Cache-Control": "no-cache",
1346
- Connection: "keep-alive",
1347
- ...this.getHeaders()
1348
- });
1349
- if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
1350
- const keepAlive = startKeepalive(writer, encoder);
1351
- this.streamMapping.set(streamId, {
1352
- writer,
1353
- encoder,
1354
- cleanup: () => {
1355
- clearInterval(keepAlive);
1356
- this.streamMapping.delete(streamId);
1357
- writer.close().catch(() => {});
1358
- }
1359
- });
1360
- for (const message of messages) if (isJSONRPCRequest(message)) this.requestToStreamMapping.set(message.id, streamId);
1361
- for (const message of messages) this.onmessage?.(message, { requestInfo });
1362
- return new Response(readable, { headers });
1363
- }
1364
- async handleDeleteRequest(request) {
1365
- const sessionError = this.validateSession(request);
1366
- if (sessionError) return sessionError;
1367
- const versionError = this.validateProtocolVersion(request);
1368
- if (versionError) return versionError;
1369
- const closedSessionId = this.sessionId;
1370
- await this.close();
1371
- if (closedSessionId && this.onsessionclosed) this.onsessionclosed(closedSessionId);
1372
- return new Response(null, {
1373
- status: 200,
1374
- headers: { ...this.getHeaders() }
1375
- });
1376
- }
1377
- handleOptionsRequest(_request) {
1378
- return new Response(null, {
1379
- status: 200,
1380
- headers: { ...this.getHeaders({ forPreflight: true }) }
1381
- });
1259
+ const firstRequest = messages.find((m) => typeof m === "object" && m !== null && "id" in m && "method" in m);
1260
+ if (firstRequest) this._pendingRequestId = firstRequest.id;
1261
+ } catch {}
1382
1262
  }
1383
- handleUnsupportedRequest() {
1384
- return new Response(JSON.stringify({
1385
- jsonrpc: "2.0",
1386
- error: {
1387
- code: -32e3,
1388
- message: "Method not allowed."
1389
- },
1390
- id: null
1391
- }), {
1392
- status: 405,
1393
- headers: {
1394
- Allow: "GET, POST, DELETE, OPTIONS",
1395
- "Content-Type": "application/json"
1396
- }
1397
- });
1398
- }
1399
- validateSession(request) {
1400
- if (this.sessionIdGenerator === void 0) return;
1401
- if (!this.initialized) return new Response(JSON.stringify({
1402
- jsonrpc: "2.0",
1403
- error: {
1404
- code: -32e3,
1405
- message: "Bad Request: Server not initialized"
1406
- },
1407
- id: null
1408
- }), {
1409
- status: 400,
1410
- headers: {
1411
- "Content-Type": "application/json",
1412
- ...this.getHeaders()
1413
- }
1414
- });
1415
- const sessionId = request.headers.get("mcp-session-id");
1416
- if (!sessionId) return new Response(JSON.stringify({
1417
- jsonrpc: "2.0",
1418
- error: {
1419
- code: -32e3,
1420
- message: "Bad Request: Mcp-Session-Id header is required"
1421
- },
1422
- id: null
1423
- }), {
1424
- status: 400,
1425
- headers: {
1426
- "Content-Type": "application/json",
1427
- ...this.getHeaders()
1428
- }
1429
- });
1430
- if (sessionId !== this.sessionId) return new Response(JSON.stringify({
1263
+ async restoreState() {
1264
+ if (!this._storage || this._stateRestored) return;
1265
+ this._stateRestored = true;
1266
+ let state;
1267
+ try {
1268
+ state = await Promise.resolve(this._storage.get());
1269
+ } catch (error) {
1270
+ this._stateRestored = false;
1271
+ throw error;
1272
+ }
1273
+ if (!state) return;
1274
+ const sdk = this;
1275
+ sdk.sessionId = state.sessionId;
1276
+ sdk._initialized = state.initialized;
1277
+ this._capturedInitializeParams = state.initializeParams;
1278
+ if (state.initializeParams && this.onmessage) this.onmessage({
1431
1279
  jsonrpc: "2.0",
1432
- error: {
1433
- code: -32001,
1434
- message: "Session not found"
1435
- },
1436
- id: null
1437
- }), {
1438
- status: 404,
1439
- headers: {
1440
- "Content-Type": "application/json",
1441
- ...this.getHeaders()
1442
- }
1280
+ id: RESTORE_REQUEST_ID,
1281
+ method: "initialize",
1282
+ params: state.initializeParams
1443
1283
  });
1444
1284
  }
1445
- async close() {
1446
- for (const { cleanup } of this.streamMapping.values()) cleanup();
1447
- this.streamMapping.clear();
1448
- this.requestResponseMap.clear();
1449
- this.onclose?.();
1450
- }
1451
- /**
1452
- * Close an SSE stream for a specific request, triggering client reconnection.
1453
- * Use this to implement polling behavior during long-running operations -
1454
- * client will reconnect after the retry interval specified in the priming event.
1455
- */
1456
- closeSSEStream(requestId) {
1457
- const streamId = this.requestToStreamMapping.get(requestId);
1458
- if (!streamId) return;
1459
- const stream = this.streamMapping.get(streamId);
1460
- if (stream) stream.cleanup();
1461
- for (const [reqId, sid] of this.requestToStreamMapping.entries()) if (sid === streamId) {
1462
- this.requestToStreamMapping.delete(reqId);
1463
- this.requestResponseMap.delete(reqId);
1464
- }
1465
- }
1466
- async send(message, options) {
1467
- let requestId = options?.relatedRequestId;
1468
- if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) requestId = message.id;
1469
- if (requestId === RESTORE_REQUEST_ID) return;
1470
- if (requestId === void 0) {
1471
- if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");
1472
- const standaloneSse = this.streamMapping.get(this.standaloneSseStreamId);
1473
- if (standaloneSse === void 0) return;
1474
- if (standaloneSse.writer && standaloneSse.encoder) {
1475
- let eventId;
1476
- if (this.eventStore) eventId = await this.eventStore.storeEvent(this.standaloneSseStreamId, message);
1477
- const data = `${eventId ? `id: ${eventId}\n` : ""}event: message\ndata: ${JSON.stringify(message)}\n\n`;
1478
- await standaloneSse.writer.write(standaloneSse.encoder.encode(data));
1479
- }
1480
- return;
1481
- }
1482
- const streamId = this.requestToStreamMapping.get(requestId);
1483
- if (!streamId) throw new Error(`No connection established for request ID: ${String(requestId)}`);
1484
- const response = this.streamMapping.get(streamId);
1485
- if (!response) throw new Error(`No connection established for request ID: ${String(requestId)}`);
1486
- if (!this.enableJsonResponse) {
1487
- if (response.writer && response.encoder) {
1488
- let eventId;
1489
- if (this.eventStore) eventId = await this.eventStore.storeEvent(streamId, message);
1490
- const data = `${eventId ? `id: ${eventId}\n` : ""}event: message\ndata: ${JSON.stringify(message)}\n\n`;
1491
- await response.writer.write(response.encoder.encode(data));
1492
- }
1493
- }
1494
- if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
1495
- this.requestResponseMap.set(requestId, message);
1496
- const relatedIds = Array.from(this.requestToStreamMapping.entries()).filter(([, sid]) => sid === streamId).map(([id]) => id);
1497
- if (relatedIds.every((id) => this.requestResponseMap.has(id))) {
1498
- if (this.enableJsonResponse && response.resolveJson) {
1499
- const responses = relatedIds.map((id) => this.requestResponseMap.get(id));
1500
- const headers = new Headers({
1501
- "Content-Type": "application/json",
1502
- ...this.getHeaders()
1503
- });
1504
- if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
1505
- const body = responses.length === 1 ? responses[0] : responses;
1506
- response.resolveJson(new Response(JSON.stringify(body), { headers }));
1507
- } else response.cleanup();
1508
- for (const id of relatedIds) {
1509
- this.requestResponseMap.delete(id);
1510
- this.requestToStreamMapping.delete(id);
1511
- }
1512
- }
1513
- }
1285
+ async saveState() {
1286
+ if (!this._storage) return;
1287
+ const sdk = this;
1288
+ const state = {
1289
+ sessionId: sdk.sessionId,
1290
+ initialized: sdk._initialized,
1291
+ initializeParams: this._capturedInitializeParams
1292
+ };
1293
+ await Promise.resolve(this._storage.set(state));
1514
1294
  }
1515
1295
  };
1516
1296
  //#endregion
@@ -1526,30 +1306,25 @@ function runWithAuthContext(context, fn) {
1526
1306
  //#region src/mcp/handler.ts
1527
1307
  function createMcpHandler(server, options = {}) {
1528
1308
  const route = options.route ?? "/mcp";
1309
+ const { route: _route, authContext, transport: providedTransport, ...transportOptions } = options;
1529
1310
  return async (request, _env, ctx) => {
1530
1311
  const url = new URL(request.url);
1531
1312
  if (route && url.pathname !== route) return new Response("Not Found", { status: 404 });
1532
- const transport = options.transport ?? new WorkerTransport({
1533
- sessionIdGenerator: options.sessionIdGenerator,
1534
- enableJsonResponse: options.enableJsonResponse,
1535
- onsessioninitialized: options.onsessioninitialized,
1536
- corsOptions: options.corsOptions,
1537
- storage: options.storage
1538
- });
1313
+ const transport = providedTransport ?? new WorkerTransport(transportOptions);
1539
1314
  const buildAuthContext = () => {
1540
- if (options.authContext) return options.authContext;
1315
+ if (authContext) return authContext;
1541
1316
  if (ctx.props && Object.keys(ctx.props).length > 0) return { props: ctx.props };
1542
1317
  };
1543
1318
  const handleRequest = async () => {
1544
1319
  return await transport.handleRequest(request);
1545
1320
  };
1546
- const authContext = buildAuthContext();
1321
+ const resolvedAuthContext = buildAuthContext();
1547
1322
  if (!transport.started) {
1548
1323
  if (server instanceof McpServer ? server.isConnected() : server.transport !== void 0) throw new Error("Server is already connected to a transport. Create a new McpServer instance per request for stateless handlers.");
1549
1324
  await server.connect(transport);
1550
1325
  }
1551
1326
  try {
1552
- if (authContext) return await runWithAuthContext(authContext, handleRequest);
1327
+ if (resolvedAuthContext) return await runWithAuthContext(resolvedAuthContext, handleRequest);
1553
1328
  else return await handleRequest();
1554
1329
  } catch (error) {
1555
1330
  console.error("MCP handler error:", error);