msnav — microservice navigation server (Ruby)
A pure-Ruby daemon, run the way ruby-lsp is run, that serves
coderag's cross-service navigation API inside a Ruby
devcontainer with no Python. The host builds the index with coderag; the
container mounts the shared data dir and runs msnav — same endpoints, same
DB, same editor extension UX.
HOST CONTAINER (ruby image, no Python)
───────────────────────────── ──────────────────────────────────
coderag index <hub> ──DB──▶ msnav daemon ◀── editor extension
~/.local/share/coderag/hubs/<hub>/ reads coderag.db through the
(coderag.db stays here) ~/.local/share/coderag bind mount
msnav is a navigation face over the shared index: it never indexes. When
anything (a host coderag index, a coderag daemon elsewhere) bumps the DB's
index_generation, every msnav on the hub hot-reloads within a second.
What it serves
The exact endpoints the editor extension calls, wire-compatible with the Python daemon (verified response-for-response against it — see Tests):
| Endpoint | Purpose |
|---|---|
GET /api/health |
identity probe: root, services, service_roots, pid |
GET /api/nav/definition |
cross-service go-to-definition (HTTP call → route, publish → consumer, route → handler) |
GET /api/nav/references |
cross-service callers of the endpoint under the cursor |
GET /api/nav/hover |
target endpoint's YARD doc for the call on this line |
GET /api/nav/file-targets |
every cross-service jump point in a file (CodeLens) |
POST /api/register-window GET /api/window-commands GET /api/windows POST /api/open |
bridge-mode window registry + open routing, shared with coderag daemons through the DB |
GET /api/services GET /api/routes |
diagnostics |
Not included (by design — they need the indexer/LLM stack): reindex, watch, search, ask/chat, MCP, the composite LSP. Indexing freshness comes from the host.
Quickstart
On the host (once per hub, with coderag installed):
cd /path/to/workspace # the folder containing all services, with coderag.yml
coderag index . --no-embed
In the devcontainer (Ruby image):
gem install msnav # or from this repo: gem build msnav.gemspec && gem install msnav-*.gem
msnav up # idempotent; the editor extension runs this itself
msnav status
The only container config needed is the data-dir mount — one line, identical for every service of every hub:
"mounts": [
"source=${localEnv:HOME}/.local/share/coderag,target=/root/.local/share/coderag,type=bind"
]
msnav up finds the hub the same way coderag up does: nearest ancestor
coderag.yml → the conventional hub mount ($CODERAG_HUB, default
/coderag-hub) → the data dir, matching the folder against each hub's index
by directory name and then by file contents — so compose-style anonymous
mounts (workspaceFolder: /app) resolve without any settings. The resolved
service travels to the daemon, whose /api/health reports the scope
(container path ↔ canonical host root) that the extension uses as its exact
path mapping. Same exit codes too: 0 healthy, 1 failure, 2 no hub, 3 the
port serves a different hub. $CODERAG_DATA_DIR / $CODERAG_CONFIG are
honored. msnav doctor explains resolution from any folder.
Because service locations come straight from the shared DB, an in-container msnav serves host paths for cross-service targets — jumps land in the window that owns the service (routed via the DB-backed window registry, so it works across per-container daemons, Ruby and Python alike) or open a new host window.
Editor extension
editor/msnav/ is the coderag extension adapted for msnav
(msnav.* settings, default daemon command msnav up, provider + bridge
modes). Package with npx @vscode/vsce package --no-dependencies. The
original coderag extension also works against msnav — the protocol is
identical — if you point its coderag.daemonCommand at ["msnav", "up"].
Devcontainer example
examples/devcontainer/ has the complete per-service
pattern: Ruby-only Dockerfile (libsqlite3-dev for the sqlite3 gem, no
Python), the single data-dir mount, and a postCreate that installs msnav and
the extension.
CLI
msnav daemon [--config PATH] [--host H] [--port P] # run in the foreground
msnav up [ROOT] [--port P] [--timeout S] # ensure one is running (idempotent)
msnav status [--port P] # health + registered windows
msnav down [--port P] # SIGTERM by health-reported pid
daemon/up also accept --service-root/--service/--no-embed/--no-watch
for drop-in compatibility with coderag up command lines; they change
nothing (msnav never indexes).
Compatibility
- Opens
coderag.dbstrictly read-only. Compatibility is checked via the index'sread_contractmeta key (accepts 1.x) when present; older indexes are accepted from schema v4 up. - The editor-window registry and open-command queue are msnav-owned
(coderag dropped this responsibility entirely): a sidecar
msnav-windows.dbnext to the index in the hub's storage dir, shared through the same bind mount, so cross-container open-routing between msnav daemons keeps working. msnav versions it itself (PRAGMA user_version). It is ephemeral liveness state; deleting it is safe. - Ruby ≥ 2.6; gems:
sqlite3,webrick. - The Docker-Desktop caveat from coderag applies here too: SQLite (WAL) over bind mounts with several concurrent writers is the sensitive spot; msnav writes only to its small sidecar, never to the index.
Troubleshooting
msnav up exits 2 / "no hub found" in a devcontainer — hub resolution
failed. Run msnav doctor in the container terminal (from the workspace
folder): it walks the same ladder up uses and shows what each step found.
The folder is matched against each hub's index first by directory name, then
by content (which service's indexed file paths are actually on disk —
≥3 files and ≥60% present), so anonymous mounts like /app resolve too.
Common causes when it still fails:
- Hub not indexed / stale — run
coderag indexon the HOST at the hub (the folder containing all services); content matching compares against that index. - Data dir not visible — the mount targets
/root/...but the container runs as a non-rootremoteUser, so msnav looks in/home/<user>/.... Mount into the remote user's home, or mount anywhere and set$CODERAG_DATA_DIRto the target. - Escape hatches:
$CODERAG_HUB=<hub dir>or--config <hub-dir>/coderag.ymlinmsnav.daemonCommand.
can't find executable msnav for gem msnav. msnav is not currently included in the bundle — msnav was invoked inside a Bundler context
(bundle exec, or an environment exporting RUBYOPT=-rbundler/setup /
BUNDLE_GEMFILE): rubygems binstubs then refuse any gem the active Gemfile
doesn't list. msnav never needs Bundler. Fixes, best first:
- Invoke it with the Bundler context stripped —
env -u RUBYOPT -u RUBYLIB -u BUNDLE_GEMFILE -u BUNDLE_BIN_PATH msnav up(the editor extension andexamples/devcontainer/postCreate.shalready do this, andmsnav upscrubs the same variables when spawning the daemon). - Or add
gem "msnav", group: :developmentto the service's Gemfile so the bundle knows it — the ruby-lsp way; only needed if you insist on running it through Bundler.
can't find gem msnav (>= 0.a) with executable msnav (Gem::GemNotFoundException) — msnav was installed as a git gem in a
Gemfile (gem "msnav", github: …). Git gems are visible only inside
bundle exec for that Gemfile; the binstub Bundler leaves on PATH cannot
activate them, so the extension's msnav up fails. msnav never loads your
app's code (unlike ruby-lsp), so don't put it in a Gemfile at all — install
it as a plain gem in the image (gem install msnav, or build+install from a
git checkout). Quick fix inside a running container:
cd /usr/local/bundle/bundler/gems/msnav-*/ && gem build msnav.gemspec -o /tmp/m.gem && gem install /tmp/m.gem.
WARN: Unresolved or ambiguous specs during Gem::Specification.reset: stringio (…) during install — benign RubyGems noise, not an msnav error.
It appears when the image carries two versions of a default gem (the one
bundled with its Ruby plus a newer one some earlier gem install pulled in);
any gem command then prints it. Install and runtime are unaffected —
verified with the exact duplicate pair on Ruby 3.1. Silence it by upgrading
RubyGems in the image (gem update --system) or just ignore it.
Tests
rake test # or: ruby -Ilib -Itest test/test_*.rb
Unit tests run against a generated schema-v4 fixture DB; the HTTP layer is
tested through a real WEBrick boot. Parity was verified against the Python
daemon on coderag's examples/shop: 776/776 identical responses across
every line of every file for definition / references / hover / file-targets
(+ services and routes), plus cross-daemon window routing (register with the
Python daemon, open via msnav, and vice versa) and generation hot-reload
after a Python-side reindex.