Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions middlewares/mcp-apps-middleware/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -297,7 +307,7 @@ export class MCPAppsMiddleware extends Middleware {
method: string,
params?: Record<string, unknown>,
): Promise<unknown> {
const transport = buildMCPTransport(serverConfig);
const transport = await buildMCPTransport(serverConfig);

const client = new Client(
{ name: "mcp-apps-middleware", version: "1.0.0" },
Expand Down Expand Up @@ -468,7 +478,7 @@ export class MCPAppsMiddleware extends Middleware {
toolName: string,
args: Record<string, unknown>,
): Promise<unknown> {
const transport = buildMCPTransport(serverConfig);
const transport = await buildMCPTransport(serverConfig);

const client = new Client(
{ name: "mcp-apps-middleware", version: "1.0.0" },
Expand Down Expand Up @@ -573,7 +583,7 @@ export class MCPAppsMiddleware extends Middleware {
private async fetchToolsFromServer(
serverConfig: MCPClientConfig,
): Promise<UIToolInfo[]> {
const transport = buildMCPTransport(serverConfig);
const transport = await buildMCPTransport(serverConfig);

const client = new Client(
{ name: "mcp-apps-middleware", version: "1.0.0" },
Expand Down
26 changes: 21 additions & 5 deletions middlewares/mcp-middleware/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Client> {
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",
Expand Down
Loading