| 441 | // --------------------------------------------------------------------------- |
| 442 | |
| 443 | export const createMcpConnector = (input: ConnectorInput): McpConnector => { |
| 444 | if (input.transport === "stdio") { |
| 445 | const command = input.command.trim(); |
| 446 | if (!command) { |
| 447 | return Effect.fail( |
| 448 | new McpConnectionError({ |
| 449 | transport: "stdio", |
| 450 | message: "MCP stdio transport requires a command", |
| 451 | }), |
| 452 | ); |
| 453 | } |
| 454 | |
| 455 | return Effect.gen(function* () { |
| 456 | // Dynamic import so the underlying module (which evaluates |
| 457 | // `node:child_process`) is only loaded when stdio is actually used. |
| 458 | const { createStdioTransport } = yield* Effect.tryPromise({ |
| 459 | try: () => import("./stdio-connector"), |
| 460 | catch: () => |
| 461 | new McpConnectionError({ |
| 462 | transport: "stdio", |
| 463 | message: "Failed to load stdio transport module", |
| 464 | }), |
| 465 | }); |
| 466 | |
| 467 | return yield* connectClient({ |
| 468 | transport: "stdio", |
| 469 | // Opt-in per integration (default legacy) — see |
| 470 | // `McpStdioVersionNegotiation` for why stdio does not follow the |
| 471 | // remote transport's unconditional auto. |
| 472 | ...(input.versionNegotiation === "auto" |
| 473 | ? { versionNegotiation: { mode: "auto" as const } } |
| 474 | : {}), |
| 475 | createTransport: () => |
| 476 | createStdioTransport({ |
| 477 | command, |
| 478 | args: input.args, |
| 479 | env: input.env, |
| 480 | cwd: input.cwd?.trim().length ? input.cwd.trim() : undefined, |
| 481 | }), |
| 482 | }); |
| 483 | }); |
| 484 | } |
| 485 | |
| 486 | // Remote transport |
| 487 | const headers = input.headers ?? {}; |
| 488 | const remoteTransport = input.remoteTransport ?? "auto"; |
| 489 | const requestInit = Object.keys(headers).length > 0 ? { headers } : undefined; |
| 490 | const fetch = input.httpClientLayer ? fetchFromHttpClientLayer(input.httpClientLayer) : undefined; |
| 491 | |
| 492 | const endpoint = buildEndpointUrl(input.endpoint, input.queryParams ?? {}); |
| 493 | |
| 494 | // Auto-negotiate the 2026-07-28 era unconditionally only on Streamable |
| 495 | // HTTP. SSE is a legacy-only transport; stdio negotiates per the |
| 496 | // integration's `versionNegotiation` (default legacy — see the stdio |
| 497 | // branch above). |
| 498 | const connectStreamableHttp = connectClient({ |
| 499 | transport: "streamable-http", |
| 500 | versionNegotiation: { mode: "auto" }, |