ruvnet/ruflo · error · Error
MCP Server already running
Error message
MCP Server already running (PID: ${status.pid}) What it means
MCPServer.start() first calls getStatus(); if it reports a running server under a different PID than the current process, it throws 'MCP Server already running (PID: N)'. The PID self-check exists because in stdio mode getStatus() reports the current process as running before it actually starts — any other PID means a genuine second instance, and the server refuses to double-bind the transport/state.
Solutions
- Stop the existing instance first: `kill <PID from message>` or call MCPServer.stop() (mcp-server.ts:222), then start again
- If the reported PID no longer exists (`ps -p <PID>` empty), the status record is stale — remove the status/PID file the getStatus() probe reads, or run `npx @claude-flow/cli@latest doctor` to clean up
- Wrap start in a guard script: check getStatus().running before calling start()
Example fix
# before — second start collides npx @claude-flow/cli@latest mcp start # Error: MCP Server already running (PID: 4242) # after — stop, then start (or reuse the running one) kill 4242 # or: npx @claude-flow/cli@latest daemon stop npx @claude-flow/cli@latest mcp start
Defensive patterns
Strategy: validation
Validate before calling
const status = await server.getStatus();
if (status.running && status.pid !== process.pid) {
console.log(`server already running (PID ${status.pid}) — stopping first`);
await server.stop(); // mcp-server.ts:222
}
await server.start(); Try / catch
try { await server.start(); }
catch (e) {
const m = /already running \(PID: (\d+)\)/.exec((e as Error).message);
if (m) { await server.stop(); await server.start(); } // or: kill stale PID and retry once
else throw e;
} Prevention
- Check getStatus() before start() in scripts and CI jobs
- Always stop the server/daemon in cleanup handlers (finally blocks, trap EXIT) so no instance leaks
- On 'already running' with a dead PID, treat the status record as stale and clean it before retrying
When it happens
Trigger: A previous `mcp start`/daemon still alive in another terminal or tmux pane; a background server from an earlier session that survived shell exit; an orphaned process after a crash whose status record still resolves to a live PID; CI jobs overlapping because the previous job's server was never stopped.
Common situations: Dev loop where the daemon was started once and forgotten; machine/CI runner reuse where stray processes persist between jobs; running both an interactive MCP session and a background daemon against the same state directory.
Related errors
- INTERNAL_ERROR
- Server already running
- at least one candidate is required
- candidate must ingest at least one vector
- Config manager is disabled
AI-assisted analysis of ruvnet/ruflo@2602b642d9 (2026-08-18).
Data as JSON: /api/errors/c5f421aceac090c4.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/mcp-server.ts:185
// 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 2602b642d9)