fix(mcp-middlewares): import the MCP SSE transport lazily so eventsource stays out of the module graph - #2307
Draft
jpr5 wants to merge 2 commits into
Draft
fix(mcp-middlewares): import the MCP SSE transport lazily so eventsource stays out of the module graph#2307jpr5 wants to merge 2 commits into
jpr5 wants to merge 2 commits into
Conversation
The static `@modelcontextprotocol/sdk/client/sse.js` import pulled
`eventsource` into the module graph on every import of the middleware,
even for consumers that only ever configure `type: "http"` servers.
Under Bun that is fatal, not merely wasteful: `eventsource`'s `bun`
export condition points at its ESM build and Bun resolves `bun` before
`require`, so the SDK's CJS `require("eventsource")` gets an async ESM
module back and throws at load time. Because it depends on load order it
surfaces intermittently.
`buildMCPTransport` is module-private and all three call sites are
already inside `private async` methods, so making it async and deferring
the import to the `sse` branch is contained. The dynamic import survives
the tsdown cjs+esm minified build as a real `import()`.
Same defect as @ag-ui/mcp-apps-middleware: the static `@modelcontextprotocol/sdk/client/sse.js` import dragged `eventsource` into the module graph for every consumer, which throws at load time under Bun (its `bun` export condition resolves to ESM, so the SDK's CJS `require` receives an async module). `connect()` is already `private async`, so the import moves into the `sse` branch with no signature change. `Transport` is imported as a type-only import, which is erased and never enters the runtime graph.
Contributor
Python Preview PackagesVersion
Install with uvAdd the TestPyPI index to your [[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
explicit = trueThen install the packages you need: # Core SDK
uv add 'ag-ui-protocol==0.0.0.dev1785784335' --index testpypi
# Integrations (each already depends on the matching ag-ui-protocol preview)
uv add 'ag-ui-langgraph==0.0.0.dev1785784335' --index testpypi
uv add 'ag-ui-crewai==0.0.0.dev1785784335' --index testpypi
# NOTE: ag-ui-agent-spec depends on pyagentspec (git-only, not on PyPI).
# You will need to install pyagentspec separately from its git repo.
uv add 'ag-ui-agent-spec==0.0.0.dev1785784335' --index testpypi
uv add 'ag_ui_adk==0.0.0.dev1785784335' --index testpypi
uv add 'ag_ui_strands==0.0.0.dev1785784335' --index testpypiInstall with pippip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
ag-ui-protocol==0.0.0.dev1785784335
Commit: 96453b7 |
@ag-ui/a2a-middleware
@ag-ui/a2ui-middleware
@ag-ui/event-throttle-middleware
@ag-ui/mcp-apps-middleware
@ag-ui/mcp-middleware
@ag-ui/a2a
@ag-ui/adk
@ag-ui/ag2
@ag-ui/agno
@ag-ui/aws-strands
@ag-ui/claude-agent-sdk
@ag-ui/claude-managed-agents
@ag-ui/crewai
@ag-ui/langchain
@ag-ui/langgraph
@ag-ui/llamaindex
@ag-ui/mastra
@ag-ui/pydantic-ai
@ag-ui/vercel-ai-sdk
@ag-ui/watsonx
@ag-ui/a2ui-toolkit
create-ag-ui-app
@ag-ui/client
@ag-ui/core
@ag-ui/encoder
@ag-ui/proto
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Make the MCP SSE client transport a lazy
await import()in both@ag-ui/mcp-apps-middlewareand@ag-ui/mcp-middleware, so thateventsourceno longer enters the module graph just because the middlewarewas imported.
Why
@modelcontextprotocol/sdk/client/sse.jspulls ineventsource. Bothmiddlewares imported it statically at the top level, unconditionally — so every
consumer paid for it, including the (typical) ones that only configure
type: "http"servers.Under Bun this is fatal rather than merely wasteful:
eventsource'sbunexport condition points at its ESM build, and Bun resolves the
bunconditionahead of
require. The SDK's CJSrequire("eventsource")therefore receives anasync ESM module and throws at import time. Because it depends on load order,
the failure is intermittent.
In both middlewares the SSE construction already sits behind a
config.type === "sse"check insideprivate asyncmethods, so deferring theimport is contained:
mcp-apps-middleware:buildMCPTransportis module-private; it becomesasyncand its three call sites (executeMCPRequest,executeToolCall,fetchToolsFromServer) gain anawait. All three were alreadyprivate async.mcp-middleware:connect()is alreadyprivate async; the import movesinto the
ssebranch with no signature change.Transportis brought in as atype-only import, which is erased at compile time and never enters the
runtime graph.
No
as any, no@ts-ignore, no implicitany.The dynamic import survives the packages' own tsdown build (
format: ["cjs","esm"],minify: true) as a realimport()rather than being rewritten torequire:Red / green proof
The load-bearing claim is "
eventsourceno longer enters the module graph whenthe middleware is imported." The probe loads one target per fresh Bun
process (so
require.cachecannot leak between targets) and reports whetherany
eventsourcepackage file — excludingeventsource-parser— landed in thegraph. Resolution is anchored at the middleware package directory via
createRequire, so bare specifiers hit the real pnpmnode_modulesand neverBun's auto-install cache.
Both a positive control (import the SDK's
client/sse.jsdirectly — MUST betrue) and a negative control (import the SDK's
client/streamableHttp.js—MUST be false) run on every invocation, so the probe cannot pass vacuously.
Probe driver (
probe.sh), unchanged between the two runs:probe-one.cjs:RED — at
origin/main(0880dff), before the changeGREEN — this branch, same command, probe unchanged
Both controls are identical across the two runs; both subjects flip
eventsourceInGraphfromtruetofalse. Module counts drop 347 → 344 and493 → 490 (the three
eventsource/eventsource-parser/sse.jsmodules).Test suites
Both existing suites cover the SSE branch (
mcp-apps-middleware.test.tsmocks@modelcontextprotocol/sdk/client/sse.jsand asserts the SSE transport URL andheader wiring). They pass before and after — which proves the change is
non-regressive, not that it works; the probe above is the evidence.
pnpm test(vitest 4.0.18)middlewares/mcp-apps-middlewarepnpm test(vitest 4.0.18)middlewares/mcp-middlewareAlso run from the repo root:
pnpm --filter @ag-ui/mcp-apps-middleware --filter @ag-ui/mcp-middleware -r build— both packages build clean (cjs + esm + dts).pnpm --filter @ag-ui/mcp-middleware typecheck(tsc --noEmit) — clean.npx prettier --writeon both changed files — clean.Pre-existing failure, not introduced here:
pnpm --filter @ag-ui/mcp-apps-middleware typecheckfails with
src/index.ts: error TS2307: Cannot find module 'crypto' or its corresponding type declarations.Verified on unmodifiedorigin/main(sameerror, one line lower). Untouched by this PR.
Follow-up (not fixed here): the published
exportsfieldPublished
@ag-ui/mcp-apps-middleware@0.0.3has noexportsfield, which iswhy
mainwins for CJS consumers and the whole CJS chain gets walked. I tracedit, and the release tooling did not drop it — the field did not exist yet:
0.0.12025-12-05,0.0.22026-01-13,0.0.32026-01-15.middlewares/mcp-apps-middleware/package.jsonfirst appears at945ef738(2026-01-22) already carrying
"version": "0.0.3", withmain+moduleandno
exports.exports(andexports: trueintsdown.config.ts) were added later, in41ea3ff8— PR feat(middleware): add @ag-ui/mcp-middleware #1818,feat(middleware): add @ag-ui/mcp-middleware,2026-06-01 — i.e. months after
0.0.3was cut.Would the next release repeat it? No. Packing the current source confirms the
field ships:
files: ["dist/**"]does not strip package.json fields, and tsdown'sexports: trueregenerates the field at build time (it only reorders therequire/importkeys, which are mutually exclusive conditions — cosmetic).So the next
@ag-ui/mcp-apps-middlewarerelease will carryexportsand CJSconsumers will stop being force-routed through
main. No change needed; flaggedhere only so it is not re-diagnosed.
Scope note
This does not retire the downstream
eventsourcepatch. Consumers are stillon the published
0.0.3, which has the static import baked intodist/. Thepatch can only be dropped after a new release of both middlewares and a
consumer bump to it.