History

Soften MCP SSE rotation max age (#1318)

Rhys Sullivan committed 4 months ago

Commit dfbc1041 parent 8e490722 files · +20 −10Browse files

Unified · Split

2 files
  1. packages/hosts/cloudflare/src/mcp/agents-sse-max-age.test.ts +7 −0
  2. patches/agents@0.17.3.patch +13 −10
packages/hosts/cloudflare/src/mcp/agents-sse-max-age.test.ts+7−0
@@ -2,6 +2,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "@effect/vitest" · 1 unmodified line
2import { MAX_SSE_AGE_MS, McpAgent } from "agents/mcp";2import { MAX_SSE_AGE_MS, McpAgent } from "agents/mcp";
3import { Effect, Option, Schema } from "effect";3import { Effect, Option, Schema } from "effect";
4 4
5import { SESSION_TIMEOUT_MS } from "./session-alarm-policy";
6
5const KEEPALIVE_INTERVAL_MS = 25_000;7const KEEPALIVE_INTERVAL_MS = 25_000;
6const MAX_PENDING_SSE_BYTES = 8 * 1024 * 1024;8const MAX_PENDING_SSE_BYTES = 8 * 1024 * 1024;
7 9
@@ -264,6 +266,11 @@ describe("agents SSE max-age rotation", () => { · 256 unmodified lines
264 vi.useRealTimers();266 vi.useRealTimers();
265 });267 });
266 268
269 it("keeps the default max age well above the session idle timeout", () => {
270 expect(MAX_SSE_AGE_MS).toBe(30 * 60 * 1000);
271 expect(MAX_SSE_AGE_MS).toBeGreaterThanOrEqual(6 * SESSION_TIMEOUT_MS);
272 });
273
267 it("closes a healthy draining SSE connection within one keepalive tick after max age", async () => {274 it("closes a healthy draining SSE connection within one keepalive tick after max age", async () => {
268 const { response, ws } = await openSse();275 const { response, ws } = await openSse();
269 const drained = drainResponse(response);276 const drained = drainResponse(response);
patches/agents@0.17.3.patch+13−10
@@ -6,7 +6,7 @@ index c8fad448e8797b89690a99d93490d1363851b225..439da01e789713d217778e8bdebfa1a9 · 5 unmodified lines
6 xt as MCPClientOAuthCallbackConfig,6 xt as MCPClientOAuthCallbackConfig,
7 zt as ElicitResult7 zt as ElicitResult
8 } from "../agent-tool-types-CNyE1iz_.js";8 } from "../agent-tool-types-CNyE1iz_.js";
9+declare const MAX_SSE_AGE_MS = 300000;9+declare const MAX_SSE_AGE_MS = 1800000;
10 export {10 export {
11 type ClearableEventStore,11 type ClearableEventStore,
12 type CreateMcpHandlerOptions,12 type CreateMcpHandlerOptions,
@@ -22,11 +22,14 @@ diff --git a/dist/mcp/index.js b/dist/mcp/index.js · 9 unmodified lines
22index 1edcf0c8c9e67aa211ae515e7672cdf79912101e..0ba4f5bff8e6a968aa18c609952e576cedf13d8f 10064422index 1edcf0c8c9e67aa211ae515e7672cdf79912101e..0ba4f5bff8e6a968aa18c609952e576cedf13d8f 100644
23--- a/dist/mcp/index.js23--- a/dist/mcp/index.js
24+++ b/dist/mcp/index.js24+++ b/dist/mcp/index.js
25@@ -28,13 +28,14 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/25@@ -28,13 +28,17 @@ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/
26 const KEEPALIVE_INTERVAL_MS = 25e3;26 const KEEPALIVE_INTERVAL_MS = 25e3;
27 /** SSE comment frame the parser drops before any event dispatch. */27 /** SSE comment frame the parser drops before any event dispatch. */
28 const KEEPALIVE_FRAME = ": keepalive\n\n";28 const KEEPALIVE_FRAME = ": keepalive\n\n";
29+const MAX_SSE_AGE_MS = 300_000;29+// Max age is a stalled-client memory backstop, not session idleness. Keep it
30+// well above Executor's 5 minute session idle timeout so active clients rarely
31+// rotate.
32+const MAX_SSE_AGE_MS = 30 * 60 * 1000;
30 /**33 /**
31 * Start an SSE keepalive on `writer`. Returns a `clearInterval` handle34 * Start an SSE keepalive on `writer`. Returns a `clearInterval` handle
32 * that the stream cleanup must invoke when the stream closes.35 * that the stream cleanup must invoke when the stream closes.
@@ -39,7 +42,7 @@ index 1edcf0c8c9e67aa211ae515e7672cdf79912101e..0ba4f5bff8e6a968aa18c609952e576c · 6 unmodified lines
39 }, KEEPALIVE_INTERVAL_MS);42 }, KEEPALIVE_INTERVAL_MS);
40 return handle;43 return handle;
41 }44 }
42@@ -180,10 +181,15 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {45@@ -180,10 +184,15 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {
43 });46 });
44 return new Response(body, { status: 404 });47 return new Response(body, { status: 404 });
45 }48 }
@@ -59,7 +62,7 @@ index 1edcf0c8c9e67aa211ae515e7672cdf79912101e..0ba4f5bff8e6a968aa18c609952e576c · 13 unmodified lines
59 request.headers.forEach((value, key) => {62 request.headers.forEach((value, key) => {
60 existingHeaders[key] = value;63 existingHeaders[key] = value;
61 });64 });
62@@ -206,45 +212,71 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {65@@ -206,45 +215,71 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {
63 jsonrpc: "2.0"66 jsonrpc: "2.0"
64 });67 });
65 return new Response(body, { status: 500 });68 return new Response(body, { status: 500 });
@@ -157,7 +160,7 @@ index 1edcf0c8c9e67aa211ae515e7672cdf79912101e..0ba4f5bff8e6a968aa18c609952e576c · 91 unmodified lines
157 }160 }
158 onClose().catch(console.error);161 onClose().catch(console.error);
159 });162 });
160@@ -279,10 +311,16 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {163@@ -279,10 +314,16 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {
161 id: null,164 id: null,
162 jsonrpc: "2.0"165 jsonrpc: "2.0"
163 }), { status: 400 });166 }), { status: 400 });
@@ -178,7 +181,7 @@ index 1edcf0c8c9e67aa211ae515e7672cdf79912101e..0ba4f5bff8e6a968aa18c609952e576c · 14 unmodified lines
178 props: ctx.props,181 props: ctx.props,
179 jurisdiction: options.jurisdiction182 jurisdiction: options.jurisdiction
180 });183 });
181@@ -306,27 +344,86 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {184@@ -306,27 +347,86 @@ const createStreamingHttpHandler = (basePath, namespace, options = {}) => {
182 if (!ws) {185 if (!ws) {
183 await writer.close();186 await writer.close();
184 return new Response("Failed to establish WS to DO", { status: 500 });187 return new Response("Failed to establish WS to DO", { status: 500 });
@@ -280,7 +283,7 @@ index 1edcf0c8c9e67aa211ae515e7672cdf79912101e..0ba4f5bff8e6a968aa18c609952e576c · 95 unmodified lines
280 return new Response(readable, {283 return new Response(readable, {
281 headers: {284 headers: {
282 "Cache-Control": "no-cache",285 "Cache-Control": "no-cache",
283@@ -389,10 +486,16 @@ const createLegacySseHandler = (basePath, namespace, options = {}) => {286@@ -389,10 +489,16 @@ const createLegacySseHandler = (basePath, namespace, options = {}) => {
284 const url = new URL(request.url);287 const url = new URL(request.url);
285 if (request.method === "GET" && basePattern.test(url)) {288 if (request.method === "GET" && basePattern.test(url)) {
286 const sessionId = url.searchParams.get("sessionId") || namespace.newUniqueId().toString();289 const sessionId = url.searchParams.get("sessionId") || namespace.newUniqueId().toString();
@@ -301,7 +304,7 @@ index 1edcf0c8c9e67aa211ae515e7672cdf79912101e..0ba4f5bff8e6a968aa18c609952e576c · 14 unmodified lines
301 endpointUrl.pathname = encodeURI(`${basePath}/message`);304 endpointUrl.pathname = encodeURI(`${basePath}/message`);
302 endpointUrl.searchParams.set("sessionId", sessionId);305 endpointUrl.searchParams.set("sessionId", sessionId);
303 const endpointMessage = `event: endpoint\ndata: ${endpointUrl.pathname + endpointUrl.search + endpointUrl.hash}\n\n`;306 const endpointMessage = `event: endpoint\ndata: ${endpointUrl.pathname + endpointUrl.search + endpointUrl.hash}\n\n`;
304@@ -414,35 +517,94 @@ const createLegacySseHandler = (basePath, namespace, options = {}) => {307@@ -414,35 +520,94 @@ const createLegacySseHandler = (basePath, namespace, options = {}) => {
305 console.error("Failed to establish WebSocket connection");308 console.error("Failed to establish WebSocket connection");
306 await writer.close();309 await writer.close();
307 return new Response("Failed to establish WebSocket connection", { status: 500 });310 return new Response("Failed to establish WebSocket connection", { status: 500 });
@@ -414,7 +417,7 @@ index 1edcf0c8c9e67aa211ae515e7672cdf79912101e..0ba4f5bff8e6a968aa18c609952e576c · 106 unmodified lines
414 console.error("Error closing SSE connection:", error);417 console.error("Error closing SSE connection:", error);
415 }418 }
416 }419 }
417@@ -1698,6 +1860,6 @@ var McpAgent = class McpAgent extends Agent {420@@ -1698,6 +1863,6 @@ var McpAgent = class McpAgent extends Agent {
418 };421 };
419 McpAgent.STREAM_REQS_KEY_PREFIX = "__mcp_stream_reqs__:";422 McpAgent.STREAM_REQS_KEY_PREFIX = "__mcp_stream_reqs__:";
420 //#endregion423 //#endregion