/** * Transport factory: creates the appropriate MCP transport based on the * plugin's resolved config. Stdio spawns a child process (with credential * scrubbing); Streamable HTTP connects to a URL. * * @module */ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import type { Config } from './index.ts' /** * Credential-shaped ambient env vars are NOT forwarded to the child by default * (the parent harness's own secrets must not leak into a spawned process * implicitly). Same pattern as `dsh-subagent-acp`. */ const SENSITIVE_ENV_PATTERN = /KEY|SECRET|TOKEN/i /** The ambient env minus credential-shaped vars, plus the spec's explicit env. */ function buildChildEnv(extra: Record): Record { const env: Record = {} for (const [key, value] of Object.entries(process.env)) { if (value !== undefined && !SENSITIVE_ENV_PATTERN.test(key)) env[key] = value } return { ...env, ...extra } } /** * Create an MCP transport from the resolved plugin config. * * @param config - Resolved plugin config discriminated on `transport`. * @returns A connected-ready MCP Transport (stdio or Streamable HTTP). */ export function createTransport(config: Config): Transport { switch (config.transport) { case 'stdio': return new StdioClientTransport({ command: config.command, args: config.args, env: buildChildEnv(config.env), cwd: config.cwd, }) case 'streamable-http': // The MCP SDK's StreamableHTTPClientTransport has optional callback // properties typed without `| undefined` (exactOptionalPropertyTypes // mismatch with the Transport interface). The cast is safe — the SDK // constructed the object, it simply doesn't declare the optionals // strictly enough for our tsconfig. return new StreamableHTTPClientTransport( new URL(config.url), { requestInit: { headers: config.headers } }, ) as Transport } }