feat(mcp): systemd socket activation and healthz under /mcp (v0.1.1)

Patch release. Two changes to cmd/server, both motivated by running the
service behind a unix socket on a reverse-proxied host: the binary now
inherits a systemd-passed listener when present, and the healthz handler
moves under the same path prefix as the MCP endpoint so a single proxy
location forwards both. Bundled with two repository-hygiene changes.

cmd/server:
- chooseListener (new) — picks a listener at startup. When systemd has
  passed a LISTEN_FDS fd via github.com/coreos/go-systemd/v22/activation,
  the binary uses the inherited listener; otherwise it binds TCP at -addr
  as before. The startup log records "mode":"socket-activated" or
  "mode":"tcp" so operators can confirm which path is live. Same binary
  works for local dev and for systemd-managed deployment with no flags
  or env vars to toggle.
- Health probe path is now derived from -path. With the default -path /mcp
  the probe is served at /mcp/healthz; the legacy /healthz route is no
  longer registered. A reverse proxy can now route the whole MCP service
  through a single "/mcp" location prefix instead of maintaining a second
  forward for /healthz. Consumers of v0.1.0 that hit /healthz directly
  must switch to /mcp/healthz.

Dependencies:
- github.com/coreos/go-systemd/v22 v22.7.0 — listener inheritance via
  LISTEN_FDS. Loaded only by cmd/server.

Docs:
- README.md "Deployment" section rewritten to be hosting-agnostic. The
  v0.1.0 draft prescribed a specific systemd-on-HestiaCP layout; the new
  text points at the Dockerfile and at systemd socket activation as a
  supported binary mode without dictating one operator's setup. Adds an
  explicit note that any reverse proxy must disable response buffering on
  the /mcp location — streamable MCP delivers tool results via Server-Sent
  Events and default proxy buffering breaks the stream.

Repository hygiene:
- /deploy/ is now .gitignored. Local deployment artefacts (systemd units,
  reverse-proxy templates, per-release scripts) are operator-specific by
  design and live outside the public repository. The Dockerfile at the
  module root remains the only portable, public-facing build artefact.

No tool surface, no validation rules, no index schema, and no behaviour of
the indexer changed. Operators upgrading from v0.1.0 must update their
health-probe URL to /mcp/healthz (or whichever path matches their -path
flag); MCP-protocol clients (Claude, Cursor, Zed, etc.) need no changes.
This commit is contained in:
2026-05-29 14:09:06 -06:00
parent cc62906c6f
commit e23e86b06c
6 changed files with 100 additions and 4 deletions

View File

@@ -159,6 +159,37 @@ All four commands must produce clean output before a PR will be reviewed.
---
## Deployment
The server is a single self-contained static binary. There is no canonical
hosting shape — pick whichever matches the rest of your infrastructure. Two
patterns cover most cases:
- **Container.** The [`Dockerfile`](Dockerfile) at the module root produces a
distroless runtime image. Build from the einherjar repository root so the
indexer can reach every sibling module at image-build time:
```bash
docker build -f mcp/Dockerfile -t einherjar-mcp:0.1.0 .
docker run --rm -p 8080:8080 einherjar-mcp:0.1.0
```
- **Systemd / socket-activated binary.** The server detects an inherited
listener via `github.com/coreos/go-systemd/v22/activation` and uses it when
present, falling back to `-addr` TCP binding otherwise. Same binary works in
both modes — no flag, no env var. Drop the binary into `/opt/<somewhere>/`
and write a `.socket` + `.service` pair that matches your conventions.
Whatever shape you pick, the public-facing reverse proxy must keep response
buffering **off** on the `/mcp` location. Streamable MCP delivers tool results
via Server-Sent Events; default nginx, Envoy, or Caddy buffering batches the
stream and breaks Claude's session before the first event arrives. For nginx
that means `proxy_buffering off; proxy_cache off; proxy_request_buffering off;
chunked_transfer_encoding on;` plus an extended `proxy_read_timeout` for
long-lived sessions.
---
## Architecture Decisions
No ADRs at `v0.1.0`. The structural decisions in this release (synthetic `wire`