ruvnet/ruflo · error · MCPServerError

Server already running

Error message

Server already running

What it means

MCPServer.start() is single-shot: an internal running flag is set while the transport and tool registry come up, and a second start() on the same instance throws MCPServerError('Server already running'). The guard prevents double-binding transports and duplicated event-handler registration. stop() resets the flag, after which start() is legal again.

Source

Thrown at v3/@claude-flow/shared/src/mcp/server.ts:156

    // Initialize connection pool if enabled
    if (this.config.connectionPool) {
      this.connectionPool = createConnectionPool(
        this.config.connectionPool,
        logger,
        this.config.transport
      );
    }

    // Setup event handlers
    this.setupEventHandlers();
  }

  /**
   * Start the MCP server
   */
  async start(): Promise<void> {
    if (this.running) {
      throw new MCPServerError('Server already running');
    }

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

    this.logger.info('Starting MCP server', {
      name: this.config.name,
      version: this.config.version,
      transport: this.config.transport,
    });

    try {
      // Create and start transport
      this.transport = createTransport(this.config.transport, this.logger, {
        type: this.config.transport,
        host: this.config.host,
        port: this.config.port,
        corsEnabled: this.config.corsEnabled,

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Memoize startup so duplicate calls coalesce: const ready = startPromise ??= server.start()
  2. For a genuine restart, await server.stop() first, then start() again
  3. Make init single-entry: create the server in one module-level singleton reachable from exactly one code path

Example fix

// before
await server.start();
// later, another init path:
await server.start(); // MCPServerError: Server already running

// after
let startPromise: Promise<void> | null = null;
function ensureStarted() { return (startPromise ??= server.start()); }
await ensureStarted();
await ensureStarted(); // coalesced, no throw
Defensive patterns

Strategy: validation

Validate before calling

let startPromise: Promise<void> | null = null;
function ensureStarted(): Promise<void> {
  return (startPromise ??= server.start());
}
await ensureStarted(); // safe to call from any init path

Try / catch

import { MCPServerError } from '@claude-flow/shared/dist/mcp/types';
try {
  await server.start();
} catch (e) {
  if (e instanceof MCPServerError && e.message === 'Server already running') {
    return; // treat as idempotent success
  }
  throw e;
}

Prevention

When it happens

Trigger: Awaiting server.start() from two init paths (main + bootstrap helper); a start-retry wrapper that re-invokes start() after a timeout even though the first call actually succeeded; test beforeEach starting a shared server instance that was never stopped.

Common situations: Hot reload / dual ESM-CJS module loading so init code runs twice; backoff logic wrapped blindly around startup; conditional branches that both call start(); assuming a previous start failed because a dependent health check was slow.

Related errors


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