agents 0.13.3 → 0.14.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.
Files changed (74) hide show
  1. package/README.md +6 -4
  2. package/dist/{agent-tool-types-l98LCbBl.d.ts → agent-tool-types-LInzZfLo.d.ts} +463 -116
  3. package/dist/agent-tool-types.d.ts +13 -11
  4. package/dist/{agent-tools-Bg5ilERh.d.ts → agent-tools-BE9xosUG.d.ts} +2 -2
  5. package/dist/agent-tools.d.ts +14 -20
  6. package/dist/agent-tools.js +10 -6
  7. package/dist/agent-tools.js.map +1 -1
  8. package/dist/browser/ai.d.ts +1 -1
  9. package/dist/browser/ai.js +1 -1
  10. package/dist/browser/index.d.ts +1 -1
  11. package/dist/browser/index.js +1 -1
  12. package/dist/browser/tanstack-ai.d.ts +1 -1
  13. package/dist/browser/tanstack-ai.js +1 -1
  14. package/dist/chat/index.d.ts +138 -19
  15. package/dist/chat/index.js +96 -12
  16. package/dist/chat/index.js.map +1 -1
  17. package/dist/chat-sdk/index.d.ts +4 -4
  18. package/dist/classPrivateMethodInitSpec-bG0tD96O.js +7 -0
  19. package/dist/{client-D1kFXo80.js → client-NradHZZz.js} +206 -75
  20. package/dist/client-NradHZZz.js.map +1 -0
  21. package/dist/client.d.ts +1 -1
  22. package/dist/{compaction-helpers-fJyf8j4m.js → compaction-helpers-BjT2NKRZ.js} +22 -3
  23. package/dist/compaction-helpers-BjT2NKRZ.js.map +1 -0
  24. package/dist/{compaction-helpers-B-pG5J22.d.ts → compaction-helpers-DpP_XP9J.d.ts} +59 -33
  25. package/dist/{do-oauth-client-provider-4OKQU9rT.d.ts → do-oauth-client-provider-CPm9rK5I.d.ts} +1 -1
  26. package/dist/{email-J0GGS3sa.d.ts → email-1fTSJwPm.d.ts} +1 -1
  27. package/dist/email.d.ts +2 -2
  28. package/dist/experimental/memory/session/index.d.ts +30 -25
  29. package/dist/experimental/memory/session/index.js +7 -2
  30. package/dist/experimental/memory/session/index.js.map +1 -1
  31. package/dist/experimental/memory/utils/index.d.ts +12 -10
  32. package/dist/experimental/memory/utils/index.js +2 -2
  33. package/dist/{index-DKey3P4s.d.ts → index-Brdu5nMI.d.ts} +270 -1
  34. package/dist/index.d.ts +74 -67
  35. package/dist/index.js +467 -63
  36. package/dist/index.js.map +1 -1
  37. package/dist/{internal_context-BZrMS0B5.d.ts → internal_context-CcZy2Em7.d.ts} +1 -1
  38. package/dist/internal_context.d.ts +1 -1
  39. package/dist/mcp/client.d.ts +17 -13
  40. package/dist/mcp/client.js +2 -2
  41. package/dist/mcp/do-oauth-client-provider.d.ts +1 -1
  42. package/dist/mcp/index.d.ts +35 -27
  43. package/dist/mcp/index.js +402 -69
  44. package/dist/mcp/index.js.map +1 -1
  45. package/dist/observability/index.d.ts +1 -1
  46. package/dist/observability/index.js +15 -1
  47. package/dist/observability/index.js.map +1 -1
  48. package/dist/react.d.ts +3 -3
  49. package/dist/{retries-BVdRl5ZE.d.ts → retries-ClWwxADl.d.ts} +1 -1
  50. package/dist/retries.d.ts +1 -1
  51. package/dist/serializable.d.ts +1 -1
  52. package/dist/{shared-Cvj92byG.d.ts → shared-CpY1FLvm.d.ts} +1 -1
  53. package/dist/{shared-CiKaIK4h.js → shared-DdOn6sp4.js} +3 -7
  54. package/dist/{shared-CiKaIK4h.js.map → shared-DdOn6sp4.js.map} +1 -1
  55. package/dist/skills/index.d.ts +236 -0
  56. package/dist/skills/index.js +1326 -0
  57. package/dist/skills/index.js.map +1 -0
  58. package/dist/sub-routing.d.ts +6 -6
  59. package/dist/{tool-output-truncation-CH-khbZ3.js → tool-output-truncation-BF4AZQlw.js} +1 -1
  60. package/dist/{tool-output-truncation-CH-khbZ3.js.map → tool-output-truncation-BF4AZQlw.js.map} +1 -1
  61. package/dist/{types-_JjKmv-l.d.ts → types-B0GymtN_.d.ts} +1 -1
  62. package/dist/types.d.ts +1 -1
  63. package/dist/vite.d.ts +1 -1
  64. package/dist/vite.js +248 -2
  65. package/dist/vite.js.map +1 -1
  66. package/dist/{workflow-types-Dkzg4hAx.d.ts → workflow-types-DPkuBi--.d.ts} +1 -1
  67. package/dist/workflow-types.d.ts +1 -1
  68. package/dist/workflows.d.ts +13 -3
  69. package/dist/workflows.js +10 -1
  70. package/dist/workflows.js.map +1 -1
  71. package/package.json +21 -3
  72. package/skills-module.d.ts +22 -0
  73. package/dist/client-D1kFXo80.js.map +0 -1
  74. package/dist/compaction-helpers-fJyf8j4m.js.map +0 -1
package/dist/mcp/index.js CHANGED
@@ -1,11 +1,43 @@
1
1
  import "../types.js";
2
- import { a as RPCServerTransport, i as RPCClientTransport, o as RPC_DO_PREFIX } from "../client-D1kFXo80.js";
2
+ import { c as RPC_DO_PREFIX, i as normalizeServerId, n as MCP_SERVER_ID_MAX_LENGTH, o as RPCClientTransport, s as RPCServerTransport } from "../client-NradHZZz.js";
3
3
  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
7
  import { ElicitRequestSchema, InitializeRequestSchema, JSONRPCMessageSchema, SUPPORTED_PROTOCOL_VERSIONS, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCNotification, isJSONRPCRequest, isJSONRPCResultResponse } from "@modelcontextprotocol/sdk/types.js";
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ //#region src/mcp/sse-keepalive.ts
10
+ /**
11
+ * Shared SSE keepalive utility for MCP transports.
12
+ *
13
+ * Cloudflare's edge closes idle SSE responses after ~5 minutes. Writers
14
+ * that may sit silent for that long (long-running tool calls, idle
15
+ * standalone GET streams) arm a keepalive to keep the response under the
16
+ * watchdog.
17
+ *
18
+ * See cloudflare/agents#1583.
19
+ */
20
+ /** Interval between SSE keepalive comment frames, in ms.
21
+ *
22
+ * The WHATWG SSE spec recommends a comment line every "15 seconds or so"
23
+ * (html.spec.whatwg.org §9.2.7). 25s gives comfortable headroom below
24
+ * both the ~30s post-handler background-work cancellation window on
25
+ * Workers and the ~5min Cloudflare edge idle-stream watchdog.
26
+ */
27
+ const KEEPALIVE_INTERVAL_MS = 25e3;
28
+ /** SSE comment frame the parser drops before any event dispatch. */
29
+ const KEEPALIVE_FRAME = ": keepalive\n\n";
30
+ /**
31
+ * Start an SSE keepalive on `writer`. Returns a `clearInterval` handle
32
+ * that the stream cleanup must invoke when the stream closes.
33
+ */
34
+ function startKeepalive(writer, encoder) {
35
+ const handle = setInterval(() => {
36
+ writer.write(encoder.encode(KEEPALIVE_FRAME)).catch(() => clearInterval(handle));
37
+ }, KEEPALIVE_INTERVAL_MS);
38
+ return handle;
39
+ }
40
+ //#endregion
9
41
  //#region src/mcp/utils.ts
10
42
  /**
11
43
  * Since we use WebSockets to bridge the client to the
@@ -175,6 +207,14 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {
175
207
  return new Response(body, { status: 500 });
176
208
  }
177
209
  ws.accept();
210
+ if (messages.every((msg) => isJSONRPCNotification(msg) || isJSONRPCResultResponse(msg))) {
211
+ ws.close();
212
+ return new Response(null, {
213
+ headers: corsHeaders(request, options.corsOptions),
214
+ status: 202
215
+ });
216
+ }
217
+ const keepAlive = startKeepalive(writer, encoder);
178
218
  ws.addEventListener("message", (event) => {
179
219
  async function onMessage(event) {
180
220
  try {
@@ -183,6 +223,7 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {
183
223
  if (message.type !== "cf_mcp_agent_event") return;
184
224
  await writer.write(encoder.encode(message.event));
185
225
  if (message.close) {
226
+ clearInterval(keepAlive);
186
227
  ws?.close();
187
228
  await writer.close().catch(() => {});
188
229
  }
@@ -194,23 +235,18 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {
194
235
  });
195
236
  ws.addEventListener("error", (error) => {
196
237
  async function onError(_error) {
238
+ clearInterval(keepAlive);
197
239
  await writer.close().catch(() => {});
198
240
  }
199
241
  onError(error).catch(console.error);
200
242
  });
201
243
  ws.addEventListener("close", () => {
202
244
  async function onClose() {
245
+ clearInterval(keepAlive);
203
246
  await writer.close().catch(() => {});
204
247
  }
205
248
  onClose().catch(console.error);
206
249
  });
207
- if (messages.every((msg) => isJSONRPCNotification(msg) || isJSONRPCResultResponse(msg))) {
208
- ws.close();
209
- return new Response(null, {
210
- headers: corsHeaders(request, options.corsOptions),
211
- status: 202
212
- });
213
- }
214
250
  return new Response(readable, {
215
251
  headers: {
216
252
  "Cache-Control": "no-cache",
@@ -531,6 +567,9 @@ var McpSSETransport = class {
531
567
  this.onclose?.();
532
568
  }
533
569
  };
570
+ function isClearableEventStore(store) {
571
+ return typeof store.clearStream === "function";
572
+ }
534
573
  /**
535
574
  * Adapted from: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/client/streamableHttp.ts
536
575
  * - Validation and initialization are removed as they're handled in `McpAgent.serve()` handler.
@@ -540,10 +579,12 @@ var McpSSETransport = class {
540
579
  *
541
580
  * Besides these points, the implementation is the same and should be updated to match the original as new features are added.
542
581
  */
582
+ /** Fixed streamId for the standalone GET listen stream. */
583
+ const STANDALONE_STREAM_ID = "_GET_stream";
543
584
  var StreamableHTTPServerTransport = class {
544
585
  constructor(options) {
545
586
  this._started = false;
546
- this._requestResponseMap = /* @__PURE__ */ new Map();
587
+ this._streamResponseIds = /* @__PURE__ */ new Map();
547
588
  const { agent } = getCurrentAgent();
548
589
  if (!agent) throw new Error("McpAgent was not found in Transport constructor");
549
590
  this.sessionId = agent.getSessionId();
@@ -558,19 +599,63 @@ var StreamableHTTPServerTransport = class {
558
599
  this._started = true;
559
600
  }
560
601
  /**
561
- * Handles GET requests for SSE stream
602
+ * Handles GET requests for SSE stream.
603
+ *
604
+ * Two roles a GET can play:
605
+ * 1. Fresh standalone listen stream — carries server-initiated
606
+ * requests/notifications unrelated to any in-progress POST.
607
+ * 2. Resumption of a previously-disconnected stream via
608
+ * `Last-Event-ID`. The disconnected stream may have been the
609
+ * standalone stream OR a POST tool-call response stream; per the
610
+ * MCP 2025-03-26 spec the server replays missed messages "on the
611
+ * stream that was disconnected" and continues delivering
612
+ * subsequent messages on that same stream.
613
+ *
614
+ * To resume a POST stream we recover the original streamId from the
615
+ * event-store and the original `requestIds` from durable storage,
616
+ * then write them onto the new WS connection so `send()` keeps
617
+ * routing in-flight tool responses to it.
562
618
  */
563
619
  async handleGetRequest(req) {
564
- const { connection } = getCurrentAgent();
620
+ const { connection, agent } = getCurrentAgent();
565
621
  if (!connection) throw new Error("Connection was not found in handleGetRequest");
566
- if (this._eventStore) {
567
- const lastEventId = req.headers.get("last-event-id");
568
- if (lastEventId) {
622
+ if (!agent) throw new Error("Agent was not found in handleGetRequest");
623
+ const lastEventId = req.headers.get("last-event-id");
624
+ if (this._eventStore && lastEventId) {
625
+ const resumedStreamId = await this._eventStore.getStreamIdForEventId?.(lastEventId);
626
+ if (resumedStreamId) {
627
+ const resumeState = { streamId: resumedStreamId };
628
+ if (resumedStreamId === STANDALONE_STREAM_ID) resumeState._standaloneSse = true;
629
+ else {
630
+ const persistedReqs = await agent.getStreamRequestIds(resumedStreamId);
631
+ if (persistedReqs && persistedReqs.length > 0) resumeState.requestIds = persistedReqs;
632
+ }
633
+ this.supersedePriorStreamConnections(agent, connection.id, resumedStreamId);
634
+ connection.setState(resumeState);
569
635
  await this.replayEvents(lastEventId);
570
636
  return;
571
637
  }
572
638
  }
573
- connection.setState({ _standaloneSse: true });
639
+ this.supersedePriorStreamConnections(agent, connection.id, STANDALONE_STREAM_ID);
640
+ const standaloneState = {
641
+ streamId: STANDALONE_STREAM_ID,
642
+ _standaloneSse: true
643
+ };
644
+ connection.setState(standaloneState);
645
+ }
646
+ /**
647
+ * Close any connection (other than `selfId`) currently bound to
648
+ * `streamId`, so at most one live connection serves a given stream.
649
+ * Closing rather than mutating sibling state mirrors how the SDK's
650
+ * single `_streamMapping` entry gives last-writer-wins for free, and
651
+ * keeps `send()` from routing to a stale bridge.
652
+ */
653
+ supersedePriorStreamConnections(agent, selfId, streamId) {
654
+ for (const other of agent.getConnections()) {
655
+ if (other.id === selfId) continue;
656
+ if (other.state?.streamId !== streamId) continue;
657
+ other.close(1e3, "Superseded by resumed stream");
658
+ }
574
659
  }
575
660
  /**
576
661
  * Replays events that would have been sent after the specified event ID
@@ -635,10 +720,17 @@ var StreamableHTTPServerTransport = class {
635
720
  });
636
721
  }
637
722
  else if (hasRequests) {
638
- const { connection } = getCurrentAgent();
723
+ const { connection, agent } = getCurrentAgent();
639
724
  if (!connection) throw new Error("Connection was not found in handlePostRequest");
725
+ if (!agent) throw new Error("Agent was not found in handlePostRequest");
640
726
  const requestIds = messages.filter(isJSONRPCRequest).map((message) => message.id);
641
- connection.setState({ requestIds });
727
+ const streamId = connection.id;
728
+ const postState = {
729
+ streamId,
730
+ requestIds
731
+ };
732
+ connection.setState(postState);
733
+ if (this._eventStore) await agent.setStreamRequestIds(streamId, requestIds);
642
734
  for (const message of messages) {
643
735
  if (this.messageInterceptor) {
644
736
  if (await this.messageInterceptor(message, {
@@ -659,36 +751,228 @@ var StreamableHTTPServerTransport = class {
659
751
  for (const conn of agent.getConnections()) conn.close(1e3, "Session closed");
660
752
  this.onclose?.();
661
753
  }
754
+ /**
755
+ * Store the event, decide whether this is the final response, write
756
+ * the SSE frame iff a live connection is attached, then run cleanup.
757
+ * Caller resolves `streamId` and `relatedIds` (from connection state
758
+ * or persisted reverse lookup) and passes `liveConnection` as null
759
+ * when the originating WS has dropped.
760
+ */
761
+ async sendOnStream(agent, streamId, relatedIds, liveConnection, message, requestId) {
762
+ const eventId = await this._eventStore?.storeEvent(streamId, message);
763
+ let shouldClose = false;
764
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
765
+ let responseIds = this._streamResponseIds.get(streamId);
766
+ if (!responseIds) {
767
+ responseIds = /* @__PURE__ */ new Set();
768
+ this._streamResponseIds.set(streamId, responseIds);
769
+ }
770
+ responseIds.add(requestId);
771
+ shouldClose = relatedIds.every((id) => responseIds.has(id));
772
+ if (shouldClose) this._streamResponseIds.delete(streamId);
773
+ }
774
+ if (liveConnection) try {
775
+ this.writeSSEEvent(liveConnection, message, eventId, shouldClose);
776
+ } catch (error) {
777
+ this.onerror?.(error);
778
+ }
779
+ if (shouldClose) {
780
+ await agent.deleteStreamRequestIds(streamId);
781
+ if (this._eventStore && isClearableEventStore(this._eventStore)) await this._eventStore.clearStream(streamId);
782
+ }
783
+ }
662
784
  async send(message, options) {
785
+ const isResponse = isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message);
786
+ const requestId = isResponse ? message.id : options?.relatedRequestId;
787
+ if (requestId === void 0) {
788
+ if (isResponse) throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");
789
+ return this.sendStandalone(message);
790
+ }
791
+ return this.sendForRequest(message, requestId);
792
+ }
793
+ /**
794
+ * Server-initiated message on the standalone GET stream. Stored under
795
+ * a fixed streamId so it's replayable even when no live connection is
796
+ * currently attached.
797
+ *
798
+ * Sent on exactly one stream, per MCP: "the server MUST send each of
799
+ * its JSON-RPC messages on only one of the connected streams; it MUST
800
+ * NOT broadcast the same message across multiple streams."
801
+ * `handleGetRequest` supersedes prior standalone connections, so
802
+ * there is at most one to send on.
803
+ */
804
+ async sendStandalone(message) {
663
805
  const { agent } = getCurrentAgent();
664
806
  if (!agent) throw new Error("Agent was not found in send");
665
- let requestId = options?.relatedRequestId;
666
- if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) requestId = message.id;
667
- if (requestId === void 0) {
668
- if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) throw new Error("Cannot send a response on a standalone SSE stream unless resuming a previous client request");
669
- let standaloneConnection;
670
- for (const conn of agent.getConnections()) if (conn.state?._standaloneSse) standaloneConnection = conn;
671
- if (standaloneConnection === void 0) return;
672
- let eventId;
673
- if (this._eventStore) eventId = await this._eventStore.storeEvent(standaloneConnection.id, message);
674
- this.writeSSEEvent(standaloneConnection, message, eventId);
807
+ const eventId = await this._eventStore?.storeEvent(STANDALONE_STREAM_ID, message);
808
+ const standalone = Array.from(agent.getConnections()).find((conn) => conn.state?._standaloneSse);
809
+ if (standalone) this.writeSSEEvent(standalone, message, eventId);
810
+ }
811
+ /**
812
+ * Message scoped to a specific in-flight client request: a tool
813
+ * response, error, or progress notification. Resolves which stream
814
+ * owns the request id (live POST connection, resumed GET, or
815
+ * persisted reverse lookup for a dropped WS) and delegates to
816
+ * {@link sendOnStream} for the actual store / write / cleanup.
817
+ */
818
+ async sendForRequest(message, requestId) {
819
+ const { agent, connection: originatingConnection } = getCurrentAgent();
820
+ if (!agent) throw new Error("Agent was not found in send");
821
+ const matchingConnections = Array.from(agent.getConnections()).filter((conn) => conn.state?.requestIds?.includes(requestId));
822
+ const liveConnection = matchingConnections.find((conn) => conn.id === originatingConnection?.id) ?? (matchingConnections.length === 1 ? matchingConnections[0] : null);
823
+ if (!liveConnection && matchingConnections.length > 1) {
824
+ const routingError = {
825
+ jsonrpc: "2.0",
826
+ id: requestId,
827
+ error: {
828
+ code: -32603,
829
+ message: "Internal error"
830
+ }
831
+ };
832
+ await Promise.all(matchingConnections.map((candidate) => this.sendOnStream(agent, candidate.state?.streamId ?? candidate.id, candidate.state?.requestIds ?? [], candidate, routingError, requestId)));
675
833
  return;
676
834
  }
677
- const connection = Array.from(agent.getConnections()).find((conn) => conn.state?.requestIds?.includes(requestId));
678
- if (!connection) throw new Error(`No connection established for request ID: ${String(requestId)}`);
679
- let eventId;
680
- if (this._eventStore) eventId = await this._eventStore.storeEvent(connection.id, message);
681
- let shouldClose = false;
682
- if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
683
- this._requestResponseMap.set(requestId, message);
684
- const relatedIds = connection.state?.requestIds ?? [];
685
- shouldClose = relatedIds.every((id) => this._requestResponseMap.has(id));
686
- if (shouldClose) for (const id of relatedIds) this._requestResponseMap.delete(id);
835
+ let streamId = liveConnection?.state?.streamId;
836
+ let relatedIds = liveConnection?.state?.requestIds;
837
+ if (!streamId) {
838
+ const stored = await agent.getStreamForRequestId(requestId);
839
+ if (!stored) throw new Error(`No active stream found for request ID: ${String(requestId)}`);
840
+ streamId = stored.streamId;
841
+ relatedIds = stored.requestIds;
687
842
  }
688
- this.writeSSEEvent(connection, message, eventId, shouldClose);
843
+ await this.sendOnStream(agent, streamId, relatedIds ?? [], liveConnection, message, requestId);
689
844
  }
690
845
  };
691
846
  //#endregion
847
+ //#region src/mcp/event-store.ts
848
+ /**
849
+ * Durable Object–backed {@link EventStore} for SSE resumability.
850
+ *
851
+ * Default for `McpAgent`. Override `McpAgent.getEventStore()` to swap
852
+ * or disable.
853
+ *
854
+ * ## Storage layout
855
+ *
856
+ * Events are stored under `__mcp_event__:<streamId>:<seqHex>`, where
857
+ * `<seqHex>` is a 16-char zero-padded counter so events in a stream
858
+ * sort lexicographically and `getStreamIdForEventId` can recover the
859
+ * stream from `eventId` without a storage hit.
860
+ *
861
+ * ## Lifecycle
862
+ *
863
+ * Each POST tool-call stream's events live only until the final
864
+ * response is delivered. The transport calls {@link clearStream}
865
+ * immediately after writing the close frame, so storage growth is
866
+ * bounded by the in-flight POST streams plus the standalone GET
867
+ * stream. There is no background sweep — quiescent agents do no work,
868
+ * and the DO itself dies with the session.
869
+ *
870
+ * Standalone GET stream events (`_GET_stream`) are *not* cleared
871
+ * automatically; they accumulate for the lifetime of the DO. Bounded
872
+ * by session length in practice.
873
+ *
874
+ * Trade-off: if the client TCP connection dies *after* the close
875
+ * frame has been enqueued on the WS but before the bytes reach the
876
+ * client, the final message is unreplayable. Every earlier event in
877
+ * the stream is still replayable while the in-flight stream is open.
878
+ *
879
+ * ## Stream id constraints
880
+ *
881
+ * `streamId` MUST NOT contain `:`. `storeEvent` asserts this so
882
+ * embedders using custom stream ids fail loudly rather than risk
883
+ * prefix-scan collisions (e.g. clearing `a` accidentally hitting
884
+ * `a:b`). Default ids (`connection.id` UUIDs and the literal
885
+ * `_GET_stream`) already satisfy this.
886
+ */
887
+ var DurableObjectEventStore = class DurableObjectEventStore {
888
+ constructor(storage) {
889
+ this.seqByStream = /* @__PURE__ */ new Map();
890
+ this.seqInit = /* @__PURE__ */ new Map();
891
+ this.storage = storage;
892
+ }
893
+ async storeEvent(streamId, message) {
894
+ if (streamId.includes(":")) throw new Error(`DurableObjectEventStore: streamId must not contain ':' (got ${JSON.stringify(streamId)})`);
895
+ await this.ensureSeqLoaded(streamId);
896
+ const seq = (this.seqByStream.get(streamId) ?? 0) + 1;
897
+ this.seqByStream.set(streamId, seq);
898
+ const eventId = `${streamId}:${seq.toString(16).padStart(DurableObjectEventStore.SEQ_PAD, "0")}`;
899
+ const eventKey = `${DurableObjectEventStore.EVENT_KEY_PREFIX}${eventId}`;
900
+ await this.storage.put(eventKey, message);
901
+ return eventId;
902
+ }
903
+ async getStreamIdForEventId(eventId) {
904
+ const idx = eventId.lastIndexOf(":");
905
+ return idx > 0 ? eventId.slice(0, idx) : void 0;
906
+ }
907
+ async replayEventsAfter(lastEventId, { send }) {
908
+ const streamId = await this.getStreamIdForEventId(lastEventId);
909
+ if (!streamId) return "";
910
+ const prefix = `${DurableObjectEventStore.EVENT_KEY_PREFIX}${streamId}:`;
911
+ const startKey = `${DurableObjectEventStore.EVENT_KEY_PREFIX}${lastEventId}\x00`;
912
+ const rows = await this.storage.list({
913
+ prefix,
914
+ start: startKey,
915
+ limit: DurableObjectEventStore.REPLAY_LIMIT
916
+ });
917
+ for (const [key, message] of rows) await send(key.slice(DurableObjectEventStore.EVENT_KEY_PREFIX.length), message);
918
+ return streamId;
919
+ }
920
+ /**
921
+ * Drop the event log for a single stream. Called by the transport
922
+ * immediately after a POST's final response has been written to the
923
+ * wire — no future `Last-Event-ID` for this stream is expected to
924
+ * resolve.
925
+ *
926
+ * Lists and deletes in chunks of {@link DELETE_CHUNK} (128, the DO
927
+ * storage cap) so we never load the entire event log into memory.
928
+ * After deleting, the next `list` call won't see the deleted keys,
929
+ * so passing `start: <prefix>` again is enough — no cursor bookkeeping.
930
+ */
931
+ async clearStream(streamId) {
932
+ const prefix = `${DurableObjectEventStore.EVENT_KEY_PREFIX}${streamId}:`;
933
+ for (;;) {
934
+ const rows = await this.storage.list({
935
+ prefix,
936
+ limit: DurableObjectEventStore.DELETE_CHUNK
937
+ });
938
+ if (rows.size === 0) break;
939
+ await this.storage.delete([...rows.keys()]);
940
+ }
941
+ this.seqByStream.delete(streamId);
942
+ this.seqInit.delete(streamId);
943
+ }
944
+ async ensureSeqLoaded(streamId) {
945
+ if (this.seqByStream.has(streamId)) return;
946
+ let pending = this.seqInit.get(streamId);
947
+ if (!pending) {
948
+ pending = (async () => {
949
+ const prefix = `${DurableObjectEventStore.EVENT_KEY_PREFIX}${streamId}:`;
950
+ const rows = await this.storage.list({
951
+ prefix,
952
+ reverse: true,
953
+ limit: 1
954
+ });
955
+ let seq = 0;
956
+ for (const key of rows.keys()) {
957
+ const parsed = Number.parseInt(key.slice(prefix.length), 16);
958
+ if (Number.isFinite(parsed)) seq = parsed;
959
+ }
960
+ if (!this.seqByStream.has(streamId)) this.seqByStream.set(streamId, seq);
961
+ })();
962
+ this.seqInit.set(streamId, pending);
963
+ }
964
+ try {
965
+ await pending;
966
+ } finally {
967
+ this.seqInit.delete(streamId);
968
+ }
969
+ }
970
+ };
971
+ DurableObjectEventStore.EVENT_KEY_PREFIX = "__mcp_event__:";
972
+ DurableObjectEventStore.SEQ_PAD = 16;
973
+ DurableObjectEventStore.DELETE_CHUNK = 128;
974
+ DurableObjectEventStore.REPLAY_LIMIT = 1e3;
975
+ //#endregion
692
976
  //#region src/mcp/client-transports.ts
693
977
  /**
694
978
  * Deprecated transport wrappers
@@ -886,21 +1170,16 @@ var WorkerTransport = class {
886
1170
  ...this.getHeaders()
887
1171
  });
888
1172
  if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
889
- const keepAlive = setInterval(() => {
890
- try {
891
- writer.write(encoder.encode("event: ping\ndata: \n\n"));
892
- } catch {
893
- clearInterval(keepAlive);
894
- }
895
- }, 3e4);
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(() => {});
1178
+ };
896
1179
  this.streamMapping.set(streamId, {
897
1180
  writer,
898
1181
  encoder,
899
- cleanup: () => {
900
- clearInterval(keepAlive);
901
- this.streamMapping.delete(streamId);
902
- writer.close().catch(() => {});
903
- }
1182
+ cleanup
904
1183
  });
905
1184
  if (this.retryInterval !== void 0) await writer.write(encoder.encode(`retry: ${this.retryInterval}\n\n`));
906
1185
  if (lastEventId && this.eventStore) {
@@ -914,11 +1193,7 @@ var WorkerTransport = class {
914
1193
  this.streamMapping.set(streamId, {
915
1194
  writer,
916
1195
  encoder,
917
- cleanup: () => {
918
- clearInterval(keepAlive);
919
- this.streamMapping.delete(streamId);
920
- writer.close().catch(() => {});
921
- }
1196
+ cleanup
922
1197
  });
923
1198
  }
924
1199
  }
@@ -1072,13 +1347,7 @@ var WorkerTransport = class {
1072
1347
  ...this.getHeaders()
1073
1348
  });
1074
1349
  if (this.sessionId !== void 0) headers.set("mcp-session-id", this.sessionId);
1075
- const keepAlive = setInterval(() => {
1076
- try {
1077
- writer.write(encoder.encode("event: ping\ndata: \n\n"));
1078
- } catch {
1079
- clearInterval(keepAlive);
1080
- }
1081
- }, 3e4);
1350
+ const keepAlive = startKeepalive(writer, encoder);
1082
1351
  this.streamMapping.set(streamId, {
1083
1352
  writer,
1084
1353
  encoder,
@@ -1325,6 +1594,49 @@ var McpAgent = class McpAgent extends Agent {
1325
1594
  async getInitializeRequest() {
1326
1595
  return this.ctx.storage.get("initializeRequest");
1327
1596
  }
1597
+ /** Persist the `requestIds` for a POST stream. @internal */
1598
+ async setStreamRequestIds(streamId, requestIds) {
1599
+ await this.ctx.storage.put(`${McpAgent.STREAM_REQS_KEY_PREFIX}${streamId}`, requestIds);
1600
+ }
1601
+ /** Read the persisted `requestIds` for a POST stream. @internal */
1602
+ async getStreamRequestIds(streamId) {
1603
+ return this.ctx.storage.get(`${McpAgent.STREAM_REQS_KEY_PREFIX}${streamId}`);
1604
+ }
1605
+ /** Drop the persisted `requestIds` for a POST stream. @internal */
1606
+ async deleteStreamRequestIds(streamId) {
1607
+ await this.ctx.storage.delete(`${McpAgent.STREAM_REQS_KEY_PREFIX}${streamId}`);
1608
+ }
1609
+ /**
1610
+ * Reverse lookup: find which POST stream a given `requestId` belongs
1611
+ * to, and return the stream's full `requestIds` list in the same
1612
+ * pass. Used by the transport when the originating WS has dropped,
1613
+ * so `send()` can still record events for replay and decide whether
1614
+ * the stream is fully responded — mirrors the SDK's
1615
+ * `_requestToStreamMapping` which outlives connection loss.
1616
+ *
1617
+ * Returning `requestIds` alongside `streamId` lets `send()` skip a
1618
+ * second `getStreamRequestIds` read on the same key.
1619
+ *
1620
+ * O(n) in the number of in-flight POST streams — single-digit in
1621
+ * practice since each stream is cleaned up on its final response.
1622
+ * The `limit` is a defensive ceiling so an abandoned-POST leak can't
1623
+ * unbounded-load this scan; if you hit it, something else has gone
1624
+ * wrong and `send()` will throw `No active stream found`.
1625
+ *
1626
+ * @internal
1627
+ */
1628
+ async getStreamForRequestId(requestId) {
1629
+ const STREAM_REQS_SCAN_LIMIT = 1e3;
1630
+ const rows = await this.ctx.storage.list({
1631
+ prefix: McpAgent.STREAM_REQS_KEY_PREFIX,
1632
+ limit: STREAM_REQS_SCAN_LIMIT
1633
+ });
1634
+ if (rows.size === STREAM_REQS_SCAN_LIMIT) console.warn(`McpAgent: getStreamForRequestId hit the ${STREAM_REQS_SCAN_LIMIT}-key scan cap; stale __mcp_stream_reqs__ entries may be accumulating from abandoned POSTs`);
1635
+ for (const [key, requestIds] of rows) if (requestIds?.includes(requestId)) return {
1636
+ streamId: key.slice(McpAgent.STREAM_REQS_KEY_PREFIX.length),
1637
+ requestIds
1638
+ };
1639
+ }
1328
1640
  /** Read the transport type for this agent.
1329
1641
  * This relies on the naming scheme being `sse:${sessionId}`,
1330
1642
  * `streamable-http:${sessionId}`, or `rpc:${sessionId}`.
@@ -1369,12 +1681,29 @@ var McpAgent = class McpAgent extends Agent {
1369
1681
  getRpcTransportOptions() {
1370
1682
  return {};
1371
1683
  }
1684
+ /**
1685
+ * Returns the {@link EventStore} for SSE resumability. Defaults to a
1686
+ * {@link DurableObjectEventStore} backed by this agent's storage,
1687
+ * letting clients reconnect with `Last-Event-ID` after the Cloudflare
1688
+ * edge closes an idle SSE stream (~5 minute watchdog) instead of
1689
+ * relying on a server-side keepalive that would block hibernation.
1690
+ *
1691
+ * Per-stream events are cleared by the transport immediately after
1692
+ * the final response is written to the wire, so there's no
1693
+ * background cleanup — storage cost is bounded by the in-flight
1694
+ * streams alone.
1695
+ *
1696
+ * Override to disable (`return undefined`) or swap implementations.
1697
+ */
1698
+ getEventStore() {
1699
+ return new DurableObjectEventStore(this.ctx.storage);
1700
+ }
1372
1701
  /** Returns a new transport matching the type of the Agent. */
1373
1702
  initTransport() {
1374
1703
  switch (this.getTransportType()) {
1375
1704
  case "sse": return new McpSSETransport();
1376
1705
  case "streamable-http": {
1377
- const transport = new StreamableHTTPServerTransport({});
1706
+ const transport = new StreamableHTTPServerTransport({ eventStore: this.getEventStore() });
1378
1707
  transport.messageInterceptor = (message) => {
1379
1708
  return Promise.resolve(this._handleElicitationResponse(message));
1380
1709
  };
@@ -1542,11 +1871,14 @@ var McpAgent = class McpAgent extends Agent {
1542
1871
  async handleMcpMessage(message) {
1543
1872
  await this.__unsafe_ensureInitialized();
1544
1873
  if (!(this._transport instanceof RPCServerTransport)) throw new Error("Expected RPC transport");
1545
- if (!Array.isArray(message)) {
1546
- const parseResult = JSONRPCMessageSchema.safeParse(message);
1547
- if (parseResult.success && this._handleElicitationResponse(parseResult.data)) return await this._transport._awaitPendingResponse();
1548
- }
1549
- return await this._transport.handle(message);
1874
+ const transport = this._transport;
1875
+ return await this.keepAliveWhile(async () => {
1876
+ if (!Array.isArray(message)) {
1877
+ const parseResult = JSONRPCMessageSchema.safeParse(message);
1878
+ if (parseResult.success && this._handleElicitationResponse(parseResult.data)) return await transport._awaitPendingResponse();
1879
+ }
1880
+ return await transport.handle(message);
1881
+ });
1550
1882
  }
1551
1883
  /** Return a handler for the given path for this MCP.
1552
1884
  * Defaults to Streamable HTTP transport.
@@ -1589,7 +1921,8 @@ var McpAgent = class McpAgent extends Agent {
1589
1921
  });
1590
1922
  }
1591
1923
  };
1924
+ McpAgent.STREAM_REQS_KEY_PREFIX = "__mcp_stream_reqs__:";
1592
1925
  //#endregion
1593
- export { ElicitRequestSchema, McpAgent, RPCClientTransport, RPCServerTransport, RPC_DO_PREFIX, SSEEdgeClientTransport, StreamableHTTPEdgeClientTransport, WorkerTransport, createMcpHandler, experimental_createMcpHandler, getMcpAuthContext };
1926
+ export { DurableObjectEventStore, ElicitRequestSchema, MCP_SERVER_ID_MAX_LENGTH, McpAgent, RPCClientTransport, RPCServerTransport, RPC_DO_PREFIX, SSEEdgeClientTransport, StreamableHTTPEdgeClientTransport, WorkerTransport, createMcpHandler, experimental_createMcpHandler, getMcpAuthContext, normalizeServerId };
1594
1927
 
1595
1928
  //# sourceMappingURL=index.js.map