diff --git a/deploy/README.md b/deploy/README.md index 551e59e..07594db 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -36,9 +36,13 @@ the `vector` + `pg_trgm` extensions Navi's DDL expects. 4. Builds `.venv` and installs navi — the `navi-server` and `navi-code` entry points. -5. Writes and enables the systemd unit `navi.service` (`Restart=always`): +5. Syncs the private venv of every in-repo MCP server (`mcp-servers/*/.venv`, + the interpreter `mcp_servers.d/*.json` points at) via + `deploy/sync-mcp-venvs.sh`. Stamped by a hash of each `pyproject.toml`, so a + re-run only installs what actually changed. +6. Writes and enables the systemd unit `navi.service` (`Restart=always`): the server lives from installation — starts on boot, restarts on crash. -6. Waits for `/health` to answer, then symlinks `navi-code` and `navi-server` +7. Waits for `/health` to answer, then symlinks `navi-code` and `navi-server` into `/usr/local/bin`. After the install finishes, fill in the one remaining secret and restart: @@ -91,8 +95,10 @@ sudo docker exec -it navi-postgres psql -U navi -d navi ``` -Updating: `git pull && sudo systemctl restart navi` (the install script is -idempotent — re-running it after a pull also refreshes the venv). +Updating: `git pull && bash deploy/sync-mcp-venvs.sh && sudo systemctl restart +navi` (the install script is idempotent — re-running it after a pull also +refreshes the venvs; the sync script is a no-op when no MCP server's +`pyproject.toml` changed). Full runbook for an update, with preflight, verification commands, rollback and the traps (frontend dist is committed and must be rebuilt before pushing, the @@ -102,6 +108,9 @@ ## Files - `install.sh` — everything above, in one command +- `sync-mcp-venvs.sh` — (re)installs `mcp-servers/*/.venv` from each server's + `pyproject.toml`, skipping the ones whose hash is unchanged; also runnable on + its own after a pull (see `UPDATE.md`) - `docker-compose.yml` — the postgres container - `env.template` — copied to `.env` on first install; the `deploy` branch carries a filled-in copy as its only diff from master \ No newline at end of file diff --git a/deploy/UPDATE.md b/deploy/UPDATE.md index 2fc488b..a4e16c3 100644 --- a/deploy/UPDATE.md +++ b/deploy/UPDATE.md @@ -13,6 +13,13 @@ - `deploy/install.sh` is only needed when `pyproject.toml` gained dependencies — it re-runs `pip install -e .`. It is idempotent and safe to re-run any time. +- **MCP servers have venvs of their own.** `mcp-servers//.venv` is what + `mcp_servers.d/*.json` points `command` at, and Navi's venv does not carry + their dependencies. A dependency added to one of those `pyproject.toml` + files needs `bash deploy/sync-mcp-venvs.sh` (stamped by a hash of the + `pyproject.toml`, so it is a no-op when nothing changed). Missing this step + is invisible in the API: the server dies at import and its tools simply + disappear from the list. - Database changes are additive DDL executed at startup (`CREATE TABLE IF NOT EXISTS`). No migration step, no DB rollback needed. - The API server must be restarted for backend changes. Config files under @@ -76,9 +83,8 @@ cp .env # again, right before touching git git fetch origin git pull --ff-only # on deploy: git reset --hard origin/deploy -bash deploy/install.sh # ONLY if pyproject.toml changed since the deployed commit -# ...and it does NOT touch mcp-servers/*/.venv: an MCP server whose own -# pyproject.toml changed needs its install too (docs/mcp.md#the-servers-own-venv) +bash deploy/install.sh # ONLY if the root pyproject.toml changed since the deployed commit +bash deploy/sync-mcp-venvs.sh # ALWAYS — cheap and stamped; covers mcp-servers/*/pyproject.toml sudo systemctl restart navi systemctl status navi --no-pager journalctl -u navi -n 50 --no-pager @@ -109,6 +115,14 @@ # 5. Table created at startup sudo docker exec -it navi-postgres psql -U navi -d navi -c '\d mcp_user_keys' + +# 6. Every MCP server connected — a dependency missing from a server's own venv +# kills it at import, and its tools vanish with no error in the chat. +journalctl -u navi -n 300 --no-pager \ + | grep -E "MCP server .* failed to connect|missing tools" \ + || echo "no MCP failures" +# Startup log also names what registered: +journalctl -u navi -n 300 --no-pager | grep "Registering startup MCP tools" ``` Then in the UI, after logging in: **Settings → MCP** must list the servers @@ -130,6 +144,12 @@ - **dist not rebuilt or not committed** → old UI on prod, new code in git. The server never builds the frontend. +- **An MCP server's venv is not Navi's venv.** A change to + `mcp-servers/*/pyproject.toml` needs `bash deploy/sync-mcp-venvs.sh` on the + host — `install.sh` runs it, but a plain `git pull` does not. Missed in Nov + 2026 with `trafilatura`: `navi-web` died at import, and + `web_search`/`web_view`/`http_request` silently disappeared from every + profile while SearXNG itself was healthy the whole time. - **`vendor/gnexus-ui-kit/dist` is gitignored** (`.gitignore:12-14`) — a fresh clone cannot run `npm run build`. Production is unaffected while `webclient/dist` is committed, but any pipeline that builds the frontend on diff --git a/deploy/install.sh b/deploy/install.sh index cdee0d4..eeeba03 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -11,9 +11,10 @@ # 3. starts dockerized PostgreSQL (pgvector, restart: always, 127.0.0.1 only) # and installs the vector + pg_trgm extensions # 4. builds the venv and installs navi (navi-server + navi-code entry points) -# 5. writes + enables + starts the systemd unit (Restart=always — the +# 5. installs the private venv of every in-repo MCP server (mcp-servers/*) +# 6. writes + enables + starts the systemd unit (Restart=always — the # server lives from installation and survives reboots) -# 6. waits for /health, symlinks navi-code/navi-server into PATH +# 7. waits for /health, symlinks navi-code/navi-server into PATH # # Default deployed state: web UI off, auth off, terminal client only. # Re-enable the web panel later: set NAVI_WEBCLIENT_ENABLED=true in .env @@ -187,7 +188,15 @@ fi chmod 600 .swarm-key 2>/dev/null || true -# ── 5. systemd unit ───────────────────────────────────────────────── +# ── 5. MCP servers' own venvs ─────────────────────────────────────── +# Each in-repo MCP server runs from mcp-servers//.venv (its config points +# `command` there); Navi's venv does not carry their dependencies. A missed +# sync is silent at the API level — the server dies at import and its tools +# just vanish — so it belongs here rather than in a release checklist. +say "Syncing MCP servers' own venvs" +bash "$REPO_DIR/deploy/sync-mcp-venvs.sh" + +# ── 6. systemd unit ───────────────────────────────────────────────── say "Installing systemd unit (navi.service)" RUN_USER="$(id -un)" cat > /tmp/navi.service </.venv/bin/python — and +# Navi's .venv does not carry those dependencies. A dependency added to one of +# those pyproject.toml files therefore needs this install; rebuilding Navi's +# venv does not cover it. When the step was missed (trafilatura, Nov 2026) the +# server died at import, the manager could not connect, and every one of its +# tools silently disappeared from the tool list — no error reached the chat. +# +# Idempotent and cheap: the sha256 of pyproject.toml is stamped inside the venv +# and an unchanged project is skipped, so this is safe to run after every pull. +# `deploy/install.sh` calls it; run it directly on a host where install.sh is +# not part of the update (prod) or after editing one server's pyproject.toml. + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_DIR" + +say() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; } +err() { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; } + +# Only needed for a venv that does not exist yet — an existing one keeps its +# own interpreter. Fall back to the standalone build install.sh may have +# fetched into .python/. +PYBIN="" +for cand in python3.13 python3.12 python3.11 python3; do + command -v "$cand" >/dev/null 2>&1 || continue + if "$cand" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 11) else 1)'; then + PYBIN="$cand" + break + fi +done +if [ -z "$PYBIN" ]; then + # .python/ is install.sh's standalone fallback; a bare `python3.11` from + # the loop above is a PATH name, so only the absolute fallback is `-x`-tested. + PYBIN="$REPO_DIR/.python/python/bin/python3" + if [ ! -x "$PYBIN" ]; then + err "no python >= 3.11 and no .python/ build — run deploy/install.sh first" + exit 1 + fi +fi + +say "Syncing MCP servers' own venvs" + +shopt -s nullglob +synced=0 +for proj in mcp-servers/*/pyproject.toml; do + dir="$(dirname "$proj")" + name="$(basename "$dir")" + # _template is scaffolding for a new server, not a server. + [ "$name" != "_template" ] || continue + synced=1 + + want="$(sha256sum "$proj" | cut -d' ' -f1)" + stamp="$dir/.venv/.navi-pyproject.sha256" + if [ -f "$stamp" ] && [ "$(cat "$stamp")" = "$want" ]; then + echo "OK: $name (pyproject unchanged)" + continue + fi + + if [ ! -x "$dir/.venv/bin/python" ]; then + "$PYBIN" -m venv "$dir/.venv" + fi + "$dir/.venv/bin/pip" install -q --upgrade pip + "$dir/.venv/bin/pip" install -q -e "$dir" + printf '%s\n' "$want" > "$stamp" + echo "Installed: $name" +done + +if [ "$synced" = 0 ]; then + echo "No MCP servers with a pyproject.toml — nothing to sync." +fi + +echo "MCP server venvs are in sync." diff --git a/docs/mcp.md b/docs/mcp.md index 31e9889..0faf4b6 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -147,19 +147,31 @@ #### The server's own venv -`navi-web` runs from `mcp-servers/navi-web/.venv`, which is separate from Navi's: +Each in-repo server runs from `mcp-servers//.venv`, which is separate from +Navi's and is what `mcp_servers.d/.json` points `command` at: `./mcp-servers/navi-web/.venv/bin/python -m app.mcp_server` with its working -directory there. `deploy/install.sh` rebuilds only Navi's venv, so a dependency -added to `mcp-servers/navi-web/pyproject.toml` — `trafilatura` today — needs its -own install on the host: +directory there. Navi's venv does not carry those dependencies, so a dependency +added to `mcp-servers//pyproject.toml` needs an install into that venv — +`deploy/sync-mcp-venvs.sh` does it for every server, and `deploy/install.sh` +calls it. It is stamped by a hash of each `pyproject.toml`, so an unchanged +server is skipped; run it on its own after a `git pull` (see +[`deploy/UPDATE.md`](../deploy/UPDATE.md)). ```bash -VIRTUAL_ENV=mcp-servers/navi-web/.venv uv pip install -e mcp-servers/navi-web +bash deploy/sync-mcp-venvs.sh # all servers, no-op for unchanged ones sudo systemctl restart navi ``` -Without it the module does not import and the server fails to start, which -`/mcp/status` shows as a disconnected server rather than as degraded output. +For a one-off install outside a deploy checkout, the same thing by hand: + +```bash +VIRTUAL_ENV=mcp-servers/navi-web/.venv uv pip install -e mcp-servers/navi-web +``` + +Skipping it is silent: the module does not import, `McpManager` cannot connect, +and `build_tool_list` reports the server's tools as missing — the model simply +never sees them. `/admin/mcp/status` and the `MCP server %r failed to connect` +line in the journal are where it shows up. ## Runtime