{"record":{"id":"c46cd970b1a1033e","repo":"vercel/ai","slug":"stdiomcptransport-already-started","errorCode":null,"errorMessage":"StdioMCPTransport already started.","messagePattern":"StdioMCPTransport already started\\.","errorType":"exception","errorClass":"MCPClientError","httpStatus":null,"severity":"error","filePath":"packages/mcp/src/tool/mcp-stdio/mcp-stdio-transport.ts","lineNumber":33,"sourceCode":"\nexport class StdioMCPTransport implements MCPTransport {\n  readonly supportsProtocolVersionDiscovery = true;\n  private process?: ChildProcess;\n  private abortController: AbortController = new AbortController();\n  private readBuffer: ReadBuffer = new ReadBuffer();\n  private serverParams: StdioConfig;\n\n  onclose?: () => void;\n  onerror?: (error: unknown) => void;\n  onmessage?: (message: JSONRPCMessage) => void;\n\n  constructor(server: StdioConfig) {\n    this.serverParams = server;\n  }\n\n  async start(): Promise<void> {\n    if (this.process) {\n      throw new MCPClientError({\n        message: 'StdioMCPTransport already started.',\n      });\n    }\n\n    return new Promise((resolve, reject) => {\n      try {\n        const process = createChildProcess(\n          this.serverParams,\n          this.abortController.signal,\n        );\n\n        this.process = process;\n\n        this.process.on('error', error => {\n          if (error.name === 'AbortError') {\n            this.onclose?.();\n            return;\n          }","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/vercel/ai/blob/69428b1f8b037e4d118fb4853428d5c4e620493c/packages/mcp/src/tool/mcp-stdio/mcp-stdio-transport.ts#L15-L51","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst transport = new StdioMCPTransport({ command: 'mcp-server' });\nawait client.connect();\nawait transport.start(); // throws\n// after\nconst transport = new StdioMCPTransport({ command: 'mcp-server' });\nawait client.connect(); // connect() calls start() internally","handlingStrategy":"try-catch","validationCode":"// track transport lifecycle if managed manually\nlet stdioStarted = false;\nasync function ensureStdioStarted(transport) {\n  if (!stdioStarted) { await transport.start(); stdioStarted = true; }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await client.connect(); // never call transport.start() yourself\n} catch (error) {\n  if (MCPClientError.isInstance(error) && error.message.includes('already started')) {\n    // the transport is already running; reuse it\n  } else throw error;\n}","preventionTips":["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"],"tags":["lifecycle","double-start","stdio"],"backgroundTag":"transport-already-started","analyzedSha":"69428b1f8b037e4d118fb4853428d5c4e620493c","analyzedAt":"2026-08-30T12:32:21.016Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}