Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

.nornir/mcp-relay-client.md

The nornir-mcp relay client — architecture, the local lock, and running several agents

The one fact that trips everyone up: even in client mode (NORNIR_SERVER set), the relay client still opens one local single-writer redb on disk — the database behind nornir's funnel_* idea-intake/planning tools, at ~/.nornir/funnel by default. redb is single-writer, so only one relay client can run per store path per machine. Two agents (Claude Code + Copilot CLI) both pointed at the default path → the second dies with redb database error: Database already open. Cannot acquire lock.

This is the code-derived design behind that failure, why nornir's MCP is not a network endpoint, and the pattern that lets several agents share one nornir-server from the same machine. Copy-paste lives in mcp-second-agent-for-idiots.md.


1. The topology — what speaks to what

 Agent (Copilot CLI / Claude Code / …)
   │   MCP = JSON-RPC 2.0, TEXT, over STDIO     ← the agent spawns a LOCAL child process
   ▼
 nornir-mcp (relay client)        ← the MCP layer; runs on the agent's machine
   │   gRPC = HTTP/2, BINARY/protobuf, over the NETWORK → oden:7878
   ▼
 nornir-server                    ← owns the iceberg/redb warehouse

2. Why client mode is not stateless — the local redb lock

NornirServer::new opens that local store unconditionally, before any mode branch (src/bin/nornir-mcp.rs):

let store_root = FunnelStore::resolve_root(
    &loaded.workspace_root, &loaded.nornir.storage.local_path, None);
let store = FunnelStore::open_async(&store_root).await
    .with_context(|| format!("open funnel warehouse at {}", store_root.display()))?;

That store backs nornir's idea intake → planning feature (the funnel_* MCP tools). It is the only local state a relay client keeps — every other tool routes to the server over gRPC (the telemetry path even special-cases server_target().is_some()). But because it is a redb database and redb takes an exclusive lock on open:

One live relay client per store path. The default path is <home>/.nornir/funnel (src/funnel/store.rs, resolve_root: an empty [storage].local_path → the home-derived default).

So a second relay client for the same user and same store path fails on startup — regardless of client/embedded mode. This is the root cause of the "Database already open" symptom in henrik-bug-nornir-mcp-install.md whenever two agents (or an orphaned relay client + a fresh one) overlap.

3. Coexistence pattern (today) — one store path per relay client

resolve_root honours [storage].local_path (absolute = verbatim; relative = under the config dir). So give each agent's relay client its own store path via a tiny NORNIR_CONFIG, and they stop fighting:

# ~/.nornir-copilot/nornir.toml — the Copilot CLI relay client's private store
[storage]
local_path = "/home/you/.nornir-copilot"     # → store at .../.nornir-copilot/warehouse

Launch env: NORNIR_SERVER=http://oden:7878, NORNIR_SERVER_TOKEN=<tok>, NORNIR_CONFIG=~/.nornir-copilot/nornir.toml. Result (verified 2026-06-19): Copilot CLI's relay client and Claude Code's relay client run side by side, each serving the full 68-tool surface over gRPC to oden:7878, with isolated local stores:

Claude   → ~/.nornir/funnel            (default path)
Copilot  → ~/.nornir-copilot/warehouse (NORNIR_CONFIG local_path)

All real data (repos, knowledge, jobs, bench, search) is identical — it is the server's one warehouse. Only the per-agent idea-intake/planning store is separate.

4. The proper fix (future) — make client relays truly stateless

The one-store-path-per-client pattern is a workaround; the clean design is to not open a local store in client mode at all:

Then any number of client relays run with zero local lock — and a real network MCP gateway (rmcp transport-streamable-http-server) becomes possible: one nornir-mcp --http <addr> on the host, agents attach over the wire, no local binary. See cli-client-modes.md for the parallel fat/thin split on the CLI side, and mcp-from-claude-code.md for the basic wiring.


Verified 2026-06-19 on oden (nornir 0.4.49): two relay clients (Copilot CLI + Claude Code), separate local store paths, both tools/list → 68 tools over http://oden:7878, no lock contention.