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.
- package/README.md +6 -4
- package/dist/{agent-tool-types-l98LCbBl.d.ts → agent-tool-types-LInzZfLo.d.ts} +463 -116
- package/dist/agent-tool-types.d.ts +13 -11
- package/dist/{agent-tools-Bg5ilERh.d.ts → agent-tools-BE9xosUG.d.ts} +2 -2
- package/dist/agent-tools.d.ts +14 -20
- package/dist/agent-tools.js +10 -6
- package/dist/agent-tools.js.map +1 -1
- package/dist/browser/ai.d.ts +1 -1
- package/dist/browser/ai.js +1 -1
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/tanstack-ai.d.ts +1 -1
- package/dist/browser/tanstack-ai.js +1 -1
- package/dist/chat/index.d.ts +138 -19
- package/dist/chat/index.js +96 -12
- package/dist/chat/index.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +4 -4
- package/dist/classPrivateMethodInitSpec-bG0tD96O.js +7 -0
- package/dist/{client-D1kFXo80.js → client-NradHZZz.js} +206 -75
- package/dist/client-NradHZZz.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/{compaction-helpers-fJyf8j4m.js → compaction-helpers-BjT2NKRZ.js} +22 -3
- package/dist/compaction-helpers-BjT2NKRZ.js.map +1 -0
- package/dist/{compaction-helpers-B-pG5J22.d.ts → compaction-helpers-DpP_XP9J.d.ts} +59 -33
- package/dist/{do-oauth-client-provider-4OKQU9rT.d.ts → do-oauth-client-provider-CPm9rK5I.d.ts} +1 -1
- package/dist/{email-J0GGS3sa.d.ts → email-1fTSJwPm.d.ts} +1 -1
- package/dist/email.d.ts +2 -2
- package/dist/experimental/memory/session/index.d.ts +30 -25
- package/dist/experimental/memory/session/index.js +7 -2
- package/dist/experimental/memory/session/index.js.map +1 -1
- package/dist/experimental/memory/utils/index.d.ts +12 -10
- package/dist/experimental/memory/utils/index.js +2 -2
- package/dist/{index-DKey3P4s.d.ts → index-Brdu5nMI.d.ts} +270 -1
- package/dist/index.d.ts +74 -67
- package/dist/index.js +467 -63
- package/dist/index.js.map +1 -1
- package/dist/{internal_context-BZrMS0B5.d.ts → internal_context-CcZy2Em7.d.ts} +1 -1
- package/dist/internal_context.d.ts +1 -1
- package/dist/mcp/client.d.ts +17 -13
- package/dist/mcp/client.js +2 -2
- package/dist/mcp/do-oauth-client-provider.d.ts +1 -1
- package/dist/mcp/index.d.ts +35 -27
- package/dist/mcp/index.js +402 -69
- package/dist/mcp/index.js.map +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +15 -1
- package/dist/observability/index.js.map +1 -1
- package/dist/react.d.ts +3 -3
- package/dist/{retries-BVdRl5ZE.d.ts → retries-ClWwxADl.d.ts} +1 -1
- package/dist/retries.d.ts +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/{shared-Cvj92byG.d.ts → shared-CpY1FLvm.d.ts} +1 -1
- package/dist/{shared-CiKaIK4h.js → shared-DdOn6sp4.js} +3 -7
- package/dist/{shared-CiKaIK4h.js.map → shared-DdOn6sp4.js.map} +1 -1
- package/dist/skills/index.d.ts +236 -0
- package/dist/skills/index.js +1326 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/sub-routing.d.ts +6 -6
- package/dist/{tool-output-truncation-CH-khbZ3.js → tool-output-truncation-BF4AZQlw.js} +1 -1
- package/dist/{tool-output-truncation-CH-khbZ3.js.map → tool-output-truncation-BF4AZQlw.js.map} +1 -1
- package/dist/{types-_JjKmv-l.d.ts → types-B0GymtN_.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +248 -2
- package/dist/vite.js.map +1 -1
- package/dist/{workflow-types-Dkzg4hAx.d.ts → workflow-types-DPkuBi--.d.ts} +1 -1
- package/dist/workflow-types.d.ts +1 -1
- package/dist/workflows.d.ts +13 -3
- package/dist/workflows.js +10 -1
- package/dist/workflows.js.map +1 -1
- package/package.json +21 -3
- package/skills-module.d.ts +22 -0
- package/dist/client-D1kFXo80.js.map +0 -1
- 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 {
|
|
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.
|
|
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 (
|
|
567
|
-
|
|
568
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
if (
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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.
|
|
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 =
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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 =
|
|
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
|
-
|
|
1546
|
-
|
|
1547
|
-
if (
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|