Minor Changes
-
#1701
6caa6e8Thanks @mattzcarey! - RefactorWorkerTransportto extend the official MCP SDK'sWebStandardStreamableHTTPServerTransportinstead of being a hand-rolled implementation.The wrapper is now a thin subclass that layers Workers-specific concerns on top of the SDK transport:
- CORS — preflight handling and response-header injection (
corsOptions). - Persistent transport state across DO hibernation via the existing
MCPStorageApiadapter.sessionId,initialized, andinitializeParamsare snapshotted after each request and replayed on cold start so client capabilities are restored without a fresh initialize round-trip. - SSE keepalive — preserves the issue #1583 fix. Uses the shared
KEEPALIVE_FRAME(: keepalive\n\n) atKEEPALIVE_INTERVAL_MS(25s) fromsse-keepalive.ts. Keepalive is unconditional on POST response streams and disabled on the standalone GET stream when aneventStoreis configured (clients recover idle drops viaLast-Event-IDinstead).
Everything else — session validation, SSE streaming, protocol-version negotiation, event-store resumability, send/close lifecycle — is delegated to the SDK transport. Net: ~500 fewer lines of code to maintain.
The exported shape is unchanged:
WorkerTransport,WorkerTransportOptions,MCPStorageApi, andTransportStatekeep the same names, andWorkerTransportOptionsnow also extends the SDK's transport options. The defaultcreateMcpHandlerpath (a fresh transport per request) is unaffected.There are, however, a few observable behaviour changes for callers who used
WorkerTransportdirectly or relied on its previous quirks:handleRequest's second argument is now{ parsedBody?, authInfo? }(the SDK shape) instead of a positionalparsedBody.createMcpHandlerandMcpAgentdon't pass it, but callers invokingtransport.handleRequest(request, parsedBody)directly must wrap it astransport.handleRequest(request, { parsedBody }).retryIntervalpriming now follows the SDK contract. Previously aretry:priming frame was written to any GET SSE stream wheneverretryIntervalwas set. The SDK only writes a priming event when aneventStoreis configured and the negotiated protocol version is>= 2025-11-25(older clients can't parse the empty-data:priming frame), and on POST streams rather than the standalone GET stream.retryIntervalis still accepted but only affects that SDK priming event.onerrornow fires on client/protocol validation failures. The SDK invokesonerrorfor responses such as 400/405/406/415 and session-not-found. The old transport only surfaced internal errors, so handlers that logonerrorwill now see normal client mistakes.onsessionclosedfires before the underlyingclose()(and therefore beforeonclose) on DELETE, instead of after. Ordering only; the session id is still passed.startedis now read-only. It was a writable instance field and is now a getter backed by the SDK's internal_startedflag. Reading it (e.g.createMcpHandler's reconnect guard) is unchanged; assigning to it is no longer supported.createMcpHandlernow forwards SDK transport options. BecauseWorkerTransportOptionsextends the SDK options, the handler passes through everything except its ownroute/authContext/transportfields — includingeventStore,retryInterval,onsessionclosed, and the SDK DNS-rebinding options (enableDnsRebindingProtection,allowedHosts,allowedOrigins). The previous handler silently dropped these.
The SDK dependency is pinned exactly (
@modelcontextprotocol/sdk1.29.0, no caret) because the wrapper relies on a handful of SDK internals for state restore and keepalive cleanup. The exact pin stops a patch release from shifting those out from under us, and the tests assert against the SDK field names so a bump fails CI loudly rather than breaking at runtime. - CORS — preflight handling and response-header injection (