diff --git a/middlewares/mcp-apps-middleware/src/index.ts b/middlewares/mcp-apps-middleware/src/index.ts index 7f49b97401..403af13667 100644 --- a/middlewares/mcp-apps-middleware/src/index.ts +++ b/middlewares/mcp-apps-middleware/src/index.ts @@ -14,7 +14,6 @@ import { } from "@ag-ui/client"; import { Observable, from, switchMap } from "rxjs"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { randomUUID, createHash } from "crypto"; @@ -100,14 +99,25 @@ export function getServerHash(config: MCPClientConfig): string { * headers (e.g. auth) to the underlying HTTP/SSE request. Both transports accept * headers via `requestInit`; previously HTTP carried no headers field at all and * SSE's headers were never wired through. See #1862. + * + * The SSE transport is imported lazily so that `eventsource` — which it pulls + * in transitively, and which only some consumers ever need — stays out of the + * module graph unless an SSE server is actually configured. Under Bun a static + * import of it breaks at load time: `eventsource`'s `bun` export condition + * resolves to its ESM build, so the SDK's CJS `require` gets an async module + * back and throws. */ -function buildMCPTransport(config: MCPClientConfig) { +async function buildMCPTransport(config: MCPClientConfig) { const options = config.headers ? { requestInit: { headers: config.headers } } : undefined; - return config.type === "sse" - ? new SSEClientTransport(new URL(config.url), options) - : new StreamableHTTPClientTransport(new URL(config.url), options); + if (config.type === "sse") { + const { SSEClientTransport } = await import( + "@modelcontextprotocol/sdk/client/sse.js" + ); + return new SSEClientTransport(new URL(config.url), options); + } + return new StreamableHTTPClientTransport(new URL(config.url), options); } /** @@ -297,7 +307,7 @@ export class MCPAppsMiddleware extends Middleware { method: string, params?: Record, ): Promise { - const transport = buildMCPTransport(serverConfig); + const transport = await buildMCPTransport(serverConfig); const client = new Client( { name: "mcp-apps-middleware", version: "1.0.0" }, @@ -468,7 +478,7 @@ export class MCPAppsMiddleware extends Middleware { toolName: string, args: Record, ): Promise { - const transport = buildMCPTransport(serverConfig); + const transport = await buildMCPTransport(serverConfig); const client = new Client( { name: "mcp-apps-middleware", version: "1.0.0" }, @@ -573,7 +583,7 @@ export class MCPAppsMiddleware extends Middleware { private async fetchToolsFromServer( serverConfig: MCPClientConfig, ): Promise { - const transport = buildMCPTransport(serverConfig); + const transport = await buildMCPTransport(serverConfig); const client = new Client( { name: "mcp-apps-middleware", version: "1.0.0" }, diff --git a/middlewares/mcp-middleware/src/index.ts b/middlewares/mcp-middleware/src/index.ts index 2267a452c9..6c117013b7 100644 --- a/middlewares/mcp-middleware/src/index.ts +++ b/middlewares/mcp-middleware/src/index.ts @@ -11,8 +11,9 @@ import { } from "@ag-ui/client"; import { Observable, type Subscription } from "rxjs"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +// Type-only: erased at compile time, so it never enters the runtime graph. +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; /** * MCP Client configuration for HTTP (streamable) transport. @@ -560,15 +561,30 @@ export class MCPMiddleware extends Middleware { * Caveat: for the SSE transport, `requestInit.headers` only applies to * the POST channel — the SSE event stream uses `eventSourceInit`. For * streamable HTTP (the typical case) it covers all traffic. + * + * The SSE transport is imported lazily so that `eventsource` — which it + * pulls in transitively, and which only some consumers ever need — stays out + * of the module graph unless an SSE server is actually configured. Under Bun + * a static import of it breaks at load time: `eventsource`'s `bun` export + * condition resolves to its ESM build, so the SDK's CJS `require` gets an + * async module back and throws. */ private async connect(serverConfig: MCPClientConfig): Promise { const opts = serverConfig.headers ? { requestInit: { headers: serverConfig.headers } } : undefined; - const transport = - serverConfig.type === "sse" - ? new SSEClientTransport(new URL(serverConfig.url), opts) - : new StreamableHTTPClientTransport(new URL(serverConfig.url), opts); + let transport: Transport; + if (serverConfig.type === "sse") { + const { SSEClientTransport } = await import( + "@modelcontextprotocol/sdk/client/sse.js" + ); + transport = new SSEClientTransport(new URL(serverConfig.url), opts); + } else { + transport = new StreamableHTTPClientTransport( + new URL(serverConfig.url), + opts, + ); + } const client = new Client({ name: "ag-ui-mcp-middleware", version: "0.0.1",