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
- Don't call start() manually — client.connect() handles it
- Create a fresh StdioMCPTransport for each connection; never reuse a started transport
- 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
- Let client.connect() own the transport lifecycle; don't call start() directly
- Construct a fresh StdioMCPTransport per client and per reconnect
- Never share one stdio transport across multiple clients or retry attempts
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
- MCP HTTP Transport Error: Transport already started. Note: c
- StdioClientTransport not connected
- Invalid argument for parameter requests: requests must not b
- Invalid argument for parameter requests: request IDs must no
- Invalid argument for parameter requests: request IDs must be
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/c46cd970b1a1033e.
Report an issue: GitHub.