vercel/ai · error · MCPClientError

StdioClientTransport not connected

Error message

StdioClientTransport not connected

What it means

StdioMCPTransport.send() throws MCPClientError with this message when the transport has no live child process with a writable stdin. The stdio transport wraps an MCP server spawned as a child process; until start() succeeds there is no process, and after the process exits or fails to spawn, this.process or this.process.stdin is undefined. send() is invoked internally when tools are called, so a missing process makes all MCP communication impossible.

Source

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

      try {
        const message = await deserializeMessage(line);
        this.onmessage?.(message);
      } catch (error) {
        this.onerror?.(error as Error);
      }
    }
  }

  async close(): Promise<void> {
    this.abortController.abort();
    this.process = undefined;
    this.readBuffer.clear();
  }

  send(message: JSONRPCMessage): Promise<void> {
    return new Promise(resolve => {
      if (!this.process?.stdin) {
        throw new MCPClientError({
          message: 'StdioClientTransport not connected',
        });
      }

      const json = serializeMessage(message);
      if (this.process.stdin.write(json)) {
        resolve();
      } else {
        this.process.stdin.once('drain', resolve);
      }
    });
  }
}

class ReadBuffer {
  private buffer?: Buffer;

  append(chunk: Buffer): void {

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Verify the MCP server command actually starts: run the command (e.g. `npx -y @modelcontextprotocol/server-... args`) in a shell and check it stays alive and prints nothing to stderr.
  2. Ensure the client/transport lifecycle is respected: create the DefaultMCPClient with the stdio transport config and call its methods only after connection/init completes (don't call send() on a raw transport directly).
  3. Check the command, args, and env in your StdioMCPTransport config for typos or missing binaries; use absolute paths (e.g. `command: 'node'` with full script path) when PATH may differ.
  4. If the process crashed mid-session, recreate the MCP client/transport instead of reusing the dead instance.
  5. Check server logs/stderr captured by the transport for the underlying spawn or runtime failure.

Example fix

// before: reusing transport after process died, or sending before start
const transport = new StdioMCPTransport({ command: 'npx', args: ['-y', 'server'] });
await transport.send(message); // throws: not connected
// after: start first and recreate after close/crash
await transport.start();
await transport.send(message);
// if previously closed/crashed:
const fresh = new StdioMCPTransport({ command: 'npx', args: ['-y', 'server'] });
await fresh.start();
Defensive patterns

Strategy: try-catch

Validate before calling

const isConnected = (t: StdioMCPTransport) =>
  Boolean((t as any).process?.stdin); // check before send, or prefer transport state
export function canSend(t: unknown): boolean {
  return t instanceof StdioMCPTransport && Boolean((t as any).process?.stdin);
}

Type guard

export function isStdioTransportConnected(t: unknown): t is StdioMCPTransport {
  return t instanceof StdioMCPTransport && Boolean((t as any).process?.stdin);
}

Try / catch

import { MCPClientError } from '../error/mcp-client-error';
try {
  await transport.send(message);
} catch (error) {
  if (MCPClientError.isInstance(error) && error.message === 'StdioMCPTransport not connected') {
    // recreate the transport/client: the child process is gone
    const fresh = new StdioMCPTransport(config);
    await fresh.start();
    await fresh.send(message);
  } else {
    throw error;
  }
}

Prevention

When it happens

Trigger: Calling any MCP client operation (tools(), tool, resources, prompts) before calling transport.start(); using a transport instance whose child process has already exited or was closed; constructing StdioMCPTransport where the spawned command failed so this.process was never set; calling send() directly on a manually created transport that was never started.

Common situations: The MCP server command is invalid or not on PATH so the spawn fails silently before send; the server process crashed mid-session (bad args, missing runtime, port/env problems) and the next tool call hits a dead stdin; forgetting to await client init/connect before the first tool call; reusing a closed transport after transport.close().

Related errors


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