ruvnet/ruflo · error · Error

MCP Server already running (PID: ${status.pid})

Error message

MCP Server already running (PID: ${status.pid})

What it means

Thrown by MCPServerManager.start() when getStatus() reports a running server whose PID differs from the current process. The guard exists so a second `mcp start` does not try to bind a port / spawn a duplicate stdio child while a prior server holds the PID file. The current-process PID is excluded because in stdio mode getStatus() can transiently report running=true for the very process about to start.

Source

Thrown at v3/@claude-flow/cli/src/mcp-server.ts:179

    // spread last below and therefore takes precedence over this env fallback.
    const environmentTools = parseMcpToolSelection(process.env.CLAUDE_FLOW_MCP_TOOLS);
    this.options = {
      ...DEFAULT_OPTIONS,
      ...(environmentTools === 'all' ? {} : { tools: environmentTools }),
      ...options,
    };
  }

  /**
   * Start the MCP server
   */
  async start(): Promise<MCPServerStatus> {
    // Check if already running (skip if status reports our own PID —
    // getStatus() returns running=true for the current process in stdio mode
    // even before the server is actually started)
    const status = await this.getStatus();
    if (status.running && status.pid !== process.pid) {
      throw new Error(`MCP Server already running (PID: ${status.pid})`);
    }

    const startTime = performance.now();
    this.startTime = new Date();

    this.emit('starting', { options: this.options });

    try {
      if (this.options.transport === 'stdio') {
        // For stdio transport, spawn the server process
        await this.startStdioServer();
      } else {
        // For HTTP/WebSocket, start in-process server
        await this.startHttpServer();
      }

      const duration = performance.now() - startTime;

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Check the reported PID is actually an MCP server: `ps -p <pid> -o command=` and `npx @claude-flow/cli mcp status`.
  2. If it is a stale/zombie entry, stop it cleanly: `npx @claude-flow/cli mcp stop`, or `kill <pid>` then remove the PID file.
  3. If you need a second instance, set a different CLAUDE_FLOW_MCP_PORT / PID file path for the new one.
  4. If start() was called twice in-process, call stop() between them, or guard with getStatus() first.

Example fix

// before — second start races the first
await manager.start();
await manager.start(); // throws
// after — stop or reuse
await manager.start();
// ... work ...
await manager.stop();
await manager.start();
Defensive patterns

Strategy: validation

Validate before calling

const status = await manager.getStatus();
if (status.running && status.pid !== process.pid) {
  // decide: stop the existing, or use a different port
  throw new Error(`refusing to start: another MCP server holds the lock (PID ${status.pid})`);
}

Type guard

null

Try / catch

try { await manager.start(); }
catch (e) {
  if (/MCP Server already running \(PID:/.test(String(e?.message ?? ''))) {
    await manager.stop(true); // or pick a new port
    await manager.start();
  } else throw e;
}

Prevention

When it happens

Trigger: A previous `mcp start` (HTTP or stdio) crashed without removing its PID file, leaving a stale lock; a daemon is genuinely running on the configured port; calling start() twice in the same script without stop(); a prior server forked a child that survived its parent.

Common situations: Daemon was killed -9 and left the PID file; port 3000 (or CLAUDE_FLOW_MCP_PORT) is held by an older instance; a CI runner reused a workspace without cleanup; two concurrent MCP startup paths (hook + manual start) race.

Related errors


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