ruvnet/ruflo · error · Error

Stdio transport already running

Error message

Stdio transport already running

What it means

StdioTransport.start() wires a readline interface over process.stdin (or a configured stream) and is single-shot: a private `running` flag makes a second start() throw 'Stdio transport already running'. Only stop(), which closes the interface, resets the flag — and because process.stdin cannot be reopened once closed, restarting the same stdio instance is usually wrong anyway.

Source

Thrown at v3/@claude-flow/mcp/src/transport/stdio.ts:55

  private errors = 0;

  private readonly inputStream: NodeJS.ReadableStream;
  private readonly outputStream: NodeJS.WritableStream;
  private readonly maxMessageSize: number;

  constructor(
    private readonly logger: ILogger,
    config: StdioTransportConfig = {}
  ) {
    super();
    this.inputStream = config.inputStream || process.stdin;
    this.outputStream = config.outputStream || process.stdout;
    this.maxMessageSize = config.maxMessageSize || 10 * 1024 * 1024;
  }

  async start(): Promise<void> {
    if (this.running) {
      throw new Error('Stdio transport already running');
    }

    this.logger.info('Starting stdio transport');

    this.rl = readline.createInterface({
      input: this.inputStream,
      crlfDelay: Infinity,
    });

    this.rl.on('line', (line) => {
      this.handleLine(line);
    });

    this.rl.on('close', () => {
      this.handleClose();
    });

    this.inputStream.on('error', (error) => {

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Ensure exactly one start() per instance; remove the duplicate init path
  2. For restarts, construct a new StdioTransport (stdin cannot be re-read after close) instead of stop/start on the same instance
  3. Route lifecycle through TransportManager.startAll()/stopAll()

Example fix

// before
const transport = createStdioTransport(logger);
await transport.start();
await transport.start(); // throws

// after
const transport = createStdioTransport(logger);
await transport.start();
// restart path: build a fresh instance
// const fresh = createStdioTransport(logger); await fresh.start();
Defensive patterns

Strategy: validation

Validate before calling

// StdioTransport.running is private; guard at the call site
let stdioStarted = false;
async function ensureStdioStarted(t: ITransport) {
  if (stdioStarted) return;
  await t.start();
  stdioStarted = true;
}

Try / catch

try {
  await stdioTransport.start();
} catch (e) {
  if (e instanceof Error && e.message === 'Stdio transport already running') {
    // already up - no-op
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Starting the MCP stdio server twice (e.g. start called in main and again in a spawned worker); an init block re-running under nodemon/vitest watch while a module-level singleton survives; a retry wrapper around start().

Common situations: CLI that starts the server in both foreground and spawn mode; double 'ready' handlers firing start(); shared module state surviving hot reloads.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/6ea2cf38d99d6770. Report an issue: GitHub.