thedotmack/claude-mem · error · ChromaUnavailableError

chroma-mcp connection failed

Error message

chroma-mcp connection failed: ${connectionError instanceof Error ? connectionError.message : String(connectionError)}

What it means

When the chroma-mcp handshake (spawn, initialize, or first send) fails for any reason, connectInternal wraps the underlying error into ChromaUnavailableError with message 'chroma-mcp connection failed: <cause>' so all connect failures take the reconnect-backoff / skip-the-write path instead of surfacing a raw error (e.g. a bare 'Error: Not connected' from a mid-handshake crash).

Solutions

  1. Look at the wrapped cause after 'chroma-mcp connection failed:' and in logs (recordChromaVectorSearchUnavailable) for the root error.
  2. Run `uvx chroma-mcp --help` manually to reproduce and see the subprocess's own error output.
  3. Update uv/uvx and the chroma-mcp package to compatible versions.
  4. Inspect/repair or move aside the local persistent Chroma data directory if it is corrupted.
  5. Retry after the reconnect backoff; transient crashes and timeouts often recover.
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await manager.ensureConnected();
} catch (e) {
  if (e instanceof ChromaUnavailableError && e.message.startsWith('chroma-mcp connection failed')) {
    logger.warn('chroma-mcp unavailable, skipping write', { cause: e.cause ?? e.message });
    queueWriteForRetry();
  } else throw e;
}

Prevention

When it happens

Trigger: Calling ensureConnected/connectInternal when the uvx chroma-mcp subprocess fails to start, dies during MCP handshake, times out, or the transport send throws because the subprocess exited mid-initialization.

Common situations: Incompatible or broken uvx/Chroma versions, Python env issues under uvx, corrupted local Chroma data directory, resource exhaustion killing the subprocess, or a connection timeout on a slow machine.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/968126102212bae5. Report an issue: GitHub.

Appendix: source

Thrown at src/services/sync/ChromaMcpManager.ts:314

      }
      const stderrTail = transportStderrTail();
      logger.warn('CHROMA_MCP', 'Connection failed, killing subprocess tree to prevent zombie', {
        error: connectionError instanceof Error ? connectionError.message : String(connectionError),
        ...(stderrTail ? { stderrTail } : {})
      });
      // Tree-kill (not just transport.close) so failed-connect descendants
      // can't survive on Linux (#2313).
      await this.disposeCurrentSubprocess();
      // A failed MCP handshake means Chroma is unavailable, the same as a
      // missing uvx, a failed prewarm, or a lost writer lock. The SDK sends
      // `notifications/initialized` right after `initialize`; when the
      // subprocess dies mid-handshake that send throws a bare
      // `Error: Not connected`. Classify every connect failure as
      // ChromaUnavailableError so callers take the reconnect-backoff and
      // skip-the-write path instead of surfacing a raw error to error tracking.
      const unavailableMessage = `chroma-mcp connection failed: ${connectionError instanceof Error ? connectionError.message : String(connectionError)}`;
      recordChromaVectorSearchUnavailable(unavailableMessage);
      throw new ChromaUnavailableError(unavailableMessage, connectionError instanceof Error ? connectionError : undefined);
    }
    clearTimeout(timeoutId!);

    this.connected = true;
    this.registerManagedProcess();
    clearDependencyStatus('chroma');

    logger.info('CHROMA_MCP', 'Connected to chroma-mcp successfully');

    const currentTransport = this.transport;
    // Captured HERE, while the child is alive and attached — not in the
    // onclose handler below, which by definition runs after it has died.
    const transportChild = (this.transport as unknown as { _process?: ChildProcess })._process;
    const currentTracked = transportChild ? trackChild(transportChild) : null;
    const currentTrackedPid = currentTracked?.pid;
    this.transport.onclose = () => {
      if (this.transport !== currentTransport) {
        logger.debug('CHROMA_MCP', 'Ignoring stale onclose from previous transport');

View on GitHub (pinned to d8bc9755e7)