vercel/ai · error · MCPClientError

StdioMCPTransport already started.

Error message

StdioMCPTransport already started.

What it means

StdioMCPTransport.start() spawns the child MCP server process and keeps a reference to it; calling start() again while that process reference exists would spawn a duplicate process, so it throws MCPClientError. Like the HTTP transport, client.connect() already calls start() for you.

Source

Thrown at packages/mcp/src/tool/mcp-stdio/mcp-stdio-transport.ts:33

export class StdioMCPTransport implements MCPTransport {
  readonly supportsProtocolVersionDiscovery = true;
  private process?: ChildProcess;
  private abortController: AbortController = new AbortController();
  private readBuffer: ReadBuffer = new ReadBuffer();
  private serverParams: StdioConfig;

  onclose?: () => void;
  onerror?: (error: unknown) => void;
  onmessage?: (message: JSONRPCMessage) => void;

  constructor(server: StdioConfig) {
    this.serverParams = server;
  }

  async start(): Promise<void> {
    if (this.process) {
      throw new MCPClientError({
        message: 'StdioMCPTransport already started.',
      });
    }

    return new Promise((resolve, reject) => {
      try {
        const process = createChildProcess(
          this.serverParams,
          this.abortController.signal,
        );

        this.process = process;

        this.process.on('error', error => {
          if (error.name === 'AbortError') {
            this.onclose?.();
            return;
          }

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Don't call start() manually — client.connect() handles it
  2. Create a fresh StdioMCPTransport for each connection; never reuse a started transport
  3. Ensure each client instance owns its own transport instance

Example fix

// before
const transport = new StdioMCPTransport({ command: 'mcp-server' });
await client.connect();
await transport.start(); // throws
// after
const transport = new StdioMCPTransport({ command: 'mcp-server' });
await client.connect(); // connect() calls start() internally
Defensive patterns

Strategy: try-catch

Validate before calling

// track transport lifecycle if managed manually
let stdioStarted = false;
async function ensureStdioStarted(transport) {
  if (!stdioStarted) { await transport.start(); stdioStarted = true; }
}

Try / catch

try {
  await client.connect(); // never call transport.start() yourself
} catch (error) {
  if (MCPClientError.isInstance(error) && error.message.includes('already started')) {
    // the transport is already running; reuse it
  } else throw error;
}

Prevention

When it happens

Trigger: Manually calling transport.start() after client.connect(); sharing one StdioMCPTransport across two clients; retrying connect() on the same transport after a failure without creating a new transport instance.

Common situations: Combining low-level start() examples with the high-level client API; restart/retry logic reusing the transport; hot-reload in dev keeping a stale transport with a live process reference.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/c46cd970b1a1033e. Report an issue: GitHub.