release: remote HTTP MCP — bearer + OAuth, Docker image, CI publish, per-client docs #65

Merged
Latte merged 16 commits from dev into main 2026-07-05 15:50:53 +00:00

Release the remote-MCP epic from dev to main (9 commits ahead).

Brings main up to date with everything landed via #63 and #64:

  • familiar serve: opt-in streamable-HTTP transport alongside the default stdio, bearer-auth gate (401 + WWW-Authenticate), HTTP writes off by default, headless login --no-browser.
  • Production hardening: unauthenticated /healthz liveness route, fail-closed bind (loopback vs public messaging, FAMILIAR_ALLOW_INSECURE_BIND escape hatch), and FAMILIAR_AUTH_MODE=oauth resource-server mode (JWT verify + /.well-known/oauth-protected-resource).
  • Docker: multi-stage non-root image, hardened compose (read-only rootfs, cap_drop ALL, no-new-privileges), .env.example, .dockerignore.
  • CI: docker.yml builds, smoke-tests and publishes to git.hiddenden.cafe on merge to main/dev (main -> :latest/:main, dev -> :dev, plus :sha-<12>); wheel publish untouched.
  • Docs: doc/deployment.md, per-client setup pages (Claude Code, VS Code, Mistral Le Chat, Claude Desktop/Cowork, Claude web), and a connect matrix in the README.

Merging to main will also trigger the :latest / :main image publish.

Release the remote-MCP epic from `dev` to `main` (9 commits ahead). Brings `main` up to date with everything landed via #63 and #64: - `familiar serve`: opt-in streamable-HTTP transport alongside the default stdio, bearer-auth gate (401 + WWW-Authenticate), HTTP writes off by default, headless `login --no-browser`. - Production hardening: unauthenticated `/healthz` liveness route, fail-closed bind (loopback vs public messaging, `FAMILIAR_ALLOW_INSECURE_BIND` escape hatch), and `FAMILIAR_AUTH_MODE=oauth` resource-server mode (JWT verify + `/.well-known/oauth-protected-resource`). - Docker: multi-stage non-root image, hardened compose (read-only rootfs, cap_drop ALL, no-new-privileges), `.env.example`, `.dockerignore`. - CI: `docker.yml` builds, smoke-tests and publishes to git.hiddenden.cafe on merge to main/dev (main -> :latest/:main, dev -> :dev, plus :sha-<12>); wheel publish untouched. - Docs: `doc/deployment.md`, per-client setup pages (Claude Code, VS Code, Mistral Le Chat, Claude Desktop/Cowork, Claude web), and a connect matrix in the README. Merging to `main` will also trigger the `:latest` / `:main` image publish.
Ghost added 9 commits 2026-07-05 09:56:52 +00:00
Add a `familiar serve` subcommand that runs the FastMCP streamable-HTTP
transport for use as a remote MCP server (e.g. a Mistral custom connector),
while stdio stays the default and is completely unchanged.

- config: FAMILIAR_HTTP_HOST/PORT/PATH (default 127.0.0.1:8080/mcp) and a
  live http_token() reader for FAMILIAR_HTTP_TOKEN.
- server: configure_http()/serve_http() attach a StaticTokenVerifier so a
  missing/wrong bearer token gets 401 + WWW-Authenticate: Bearer and a valid
  one passes the handshake. Refuses to start (SystemExit) without a token.
- writes: in HTTP mode the write default flips to disabled unless
  FAMILIAR_ENABLE_WRITES is explicitly truthy; guard() logic is untouched.
- auth: manual_login() + parse_callback() power `login --no-browser`, printing
  the SSO URL and reading the pasted callback from stdin (no local browser).
- bump fastmcp pin to >=3.0.0 for the streamable-HTTP + verifier API.
- 401 without/with a bad token (incl. WWW-Authenticate: Bearer), 200 with a
  valid one, driven through the ASGI app in-process.
- configure_http refuses to start when the token is unset or blank.
- FAMILIAR_HTTP_* config defaults and overrides; http_token() strip/empty.
- HTTP write default disabled by default, env still wins, stdio stays enabled.
- parse_callback (URL / bare code / empty) and the manual_login exchange flow.
docs: document the remote-HTTP serve mode, env vars, nginx and Mistral setup
CI / test (pull_request) Successful in 58s
CI / publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 51s
ceb907b2fe
- README + getting-started: a "Serve over HTTPS (remote MCP)" section with the
  `familiar serve` command, the FAMILIAR_HTTP_* env vars, the write-off-by-
  default note, an nginx location block (proxy_buffering off, long read
  timeout), the headless-login / copy-the-store options, and the steps to add
  it as a Mistral custom connector.
- architecture: describe the stdio/HTTP transport split, the bearer-auth gate,
  and the HTTP write default.
- CLAUDE.md: add the new commands and env vars to Commands + Architecture.
Merge pull request 'feat: serve familiar as a remote HTTP MCP server (bearer auth)' (#63) from feat/remote-http into dev
CI / lint (push) Successful in 50s
CI / test (push) Successful in 58s
CI / publish (push) Successful in 51s
3b2c04cde3
Reviewed-on: #63
Extend the HTTP transport (from the remote-MCP work) so it's production-ready.

- /healthz: an unauthenticated GET liveness route that returns only
  {status, server, version} — no account data, no token state. For Docker and
  reverse-proxy probes; on stdio the route is never served.
- Fail-closed bind: refuse to start without authentication, and make the
  message distinguish a loopback bind from a public (0.0.0.0) one. The only
  override is the explicit FAMILIAR_ALLOW_INSECURE_BIND=true escape hatch, which
  permits an unauthenticated bind (with a loud warning).
- FAMILIAR_AUTH_MODE=oauth: run as an OAuth 2.1 resource server. A JWTVerifier
  validates external-issuer JWTs (JWKS from FAMILIAR_OAUTH_JWKS_URI, else
  discovered from FAMILIAR_OAUTH_ISSUER's OIDC metadata) against issuer +
  audience, wrapped in a RemoteAuthProvider that serves
  /.well-known/oauth-protected-resource so OAuth-only clients (Claude web) can
  discover the authorization server. familiar is a resource server only — no
  authorization server, no DCR. bearer mode is unchanged and stays the default.

Tests cover healthz (200, no leak), fail-closed vs escape hatch, and oauth
(valid JWT 200; missing/expired/wrong-audience 401 + WWW-Authenticate; the
protected-resource metadata), plus the new config readers.
- docker/Dockerfile: multi-stage uv wheel build; runtime runs `familiar serve`
  as a non-root user (uid 1000) on port 8080, /data volume for the encrypted
  token store, and a HEALTHCHECK against /healthz. No token is baked in — serve
  refuses to start without FAMILIAR_HTTP_TOKEN, so it's supplied at run time.
- docker-compose.yml: joins an external `proxy` network (Traefik labels for TLS,
  nginx/Caddy documented as alternatives), read-only rootfs, tmpfs /tmp,
  no-new-privileges, cap_drop ALL, non-root, /healthz healthcheck. Pulls the
  published FAMILIAR_IMAGE with a local build as fallback; writes default off.
- .env.example: every FAMILIAR_* knob incl. the bearer token, auth mode, the
  OAuth resource-server settings, and the insecure-bind escape hatch.
- .dockerignore: keep local state (data/, *.enc, key) and dev files out.
New .gitea/workflows/docker.yml (alongside the untouched ci.yml wheel publish).
Runs lint + test, then builds the image and smoke-tests it — non-root uid,
/healthz 200, MCP 401 without a token and 200 with a valid bearer. On a merge to
main/dev it pushes to git.hiddenden.cafe/hiddenden/familiar (main -> :latest +
:main, dev -> :dev, plus :sha-<12>) via the existing REGISTRY_TOKEN, then does a
scoped cleanup. The two workflows are independent and don't interfere.
docs: deployment guide + per-client setup + connect matrix
CI / lint (pull_request) Successful in 55s
docker / lint (pull_request) Successful in 56s
CI / test (pull_request) Successful in 1m6s
docker / test (pull_request) Successful in 1m4s
CI / publish (pull_request) Has been skipped
docker / docker (pull_request) Failing after 1m11s
df9e06a3e7
- doc/deployment.md: run familiar as a remote MCP server — secure defaults,
  bearer vs OAuth modes, the full env-var reference, one-time login / token-store
  provisioning, the published images, docker compose, and TLS reverse-proxy
  blocks for nginx (proxy_buffering off + long read timeout), Caddy, and Traefik.
- doc/clients/: one page per client with exact working steps — Claude Code,
  VS Code (.vscode/mcp.json with a masked token input, and the workspace
  .mcp.json header gotcha), Mistral Le Chat, Claude Desktop + Cowork (mcp-remote
  bridge, per-OS config paths), and Claude web (oauth mode + external IdP, with
  "use Desktop + bearer" as the simpler alternative). Plus a clients/ index.
- README: a "Connect a client" matrix (client -> auth -> guide) and a compact
  "Deploy with Docker" section linking to doc/deployment.md; oauth mode noted.
- doc/README.md index: rows for Deployment and Client setup.
- doc/getting-started.md: pointer from the HTTPS quickstart to the full docs.
- CLAUDE.md: FAMILIAR_AUTH_MODE / FAMILIAR_OAUTH_* / FAMILIAR_ALLOW_INSECURE_BIND
  and /healthz in the transport note; the split ci.yml / docker.yml workflows.

Deployment docs live under the existing doc/ tree (the repo uses doc/, not a
separate docs/) to keep a single documentation directory.
Merge pull request 'feat: production-ready remote MCP — Docker image, CI publish, per-client docs, OAuth mode' (#64) from feat/docker-serve into dev
CI / lint (push) Successful in 54s
docker / lint (push) Successful in 56s
CI / test (push) Successful in 1m5s
docker / test (push) Successful in 1m3s
CI / publish (push) Successful in 52s
docker / docker (push) Failing after 1m9s
CI / lint (pull_request) Successful in 52s
docker / lint (pull_request) Successful in 56s
CI / test (pull_request) Successful in 1m5s
docker / test (pull_request) Successful in 1m5s
CI / publish (pull_request) Has been skipped
docker / docker (pull_request) Failing after 1m8s
0fdba707fe
Reviewed-on: #64
Latte scheduled this pull request to auto merge when all checks succeed 2026-07-05 09:57:52 +00:00
Latte added 2 commits 2026-07-05 11:46:34 +00:00
fix(ci): smoke-test the image from inside its own container via docker exec
CI / lint (pull_request) Successful in 56s
docker / lint (pull_request) Successful in 54s
CI / test (pull_request) Successful in 1m2s
CI / publish (pull_request) Has been skipped
docker / test (pull_request) Successful in 57s
docker / docker (pull_request) Successful in 41s
945e40b5d8
The smoke test probed the server over the network, which fails on the
self-hosted runner: the job step runs in a container that shares the host
Docker socket but sits on a different network, so it can reach neither a
published host port (-p lands on the runner host) nor the server
container's bridge IP (different network → curl connect hangs, so the job
looked stuck rather than failing).

Probe from inside the server container with `docker exec` instead: it goes
through the Docker socket and runs in the server's own netns, where
127.0.0.1:8080 always works regardless of runner topology. The image ships
python + httpx, so no curl or extra image pull is needed. Drop the now
unnecessary -p publish.
Merge pull request 'fix(ci): smoke-test image from inside its container via docker exec' (#66) from fix/smoke-test-container-ip into dev
CI / lint (push) Successful in 55s
docker / lint (push) Successful in 57s
CI / test (push) Successful in 1m6s
docker / test (push) Successful in 1m5s
CI / lint (pull_request) Successful in 50s
docker / lint (pull_request) Successful in 53s
CI / test (pull_request) Successful in 1m1s
docker / test (pull_request) Successful in 1m0s
CI / publish (pull_request) Has been skipped
CI / publish (push) Successful in 55s
docker / docker (push) Failing after 44s
docker / docker (pull_request) Failing after 1m10s
733757f22a
Reviewed-on: #66
Latte added 3 commits 2026-07-05 11:46:49 +00:00
chore(deploy): drop Traefik labels, document Pangolin as the proxy
CI / lint (pull_request) Successful in 53s
docker / lint (pull_request) Successful in 53s
CI / test (pull_request) Successful in 1m3s
CI / publish (pull_request) Has been skipped
docker / test (pull_request) Successful in 57s
docker / docker (pull_request) Failing after 1m7s
ef1567b071
The compose service already joins the external `proxy` network and
exposes :8080; the reverse proxy owns TLS and routing. Remove the
Traefik router labels (and the now-unused FAMILIAR_DOMAIN /
TRAEFIK_CERTRESOLVER env vars) and document Pangolin — which discovers
the service on the shared network — as the proxy instead. nginx/Caddy
guidance is unchanged.
Merge branch 'dev' into chore/pangolin-proxy
docker / test (pull_request) Successful in 1m1s
CI / lint (pull_request) Successful in 50s
docker / lint (pull_request) Successful in 50s
CI / test (pull_request) Successful in 1m2s
CI / publish (pull_request) Has been skipped
docker / docker (pull_request) Failing after 22s
aeb79fdbb7
Merge pull request 'chore(deploy): drop Traefik labels, use Pangolin as the proxy' (#67) from chore/pangolin-proxy into dev
CI / lint (push) Successful in 51s
CI / test (push) Successful in 57s
docker / lint (push) Successful in 50s
docker / test (push) Successful in 57s
CI / lint (pull_request) Successful in 51s
docker / lint (pull_request) Successful in 51s
CI / test (pull_request) Successful in 1m0s
docker / test (pull_request) Successful in 57s
CI / publish (pull_request) Has been skipped
CI / publish (push) Successful in 53s
docker / docker (pull_request) Failing after 12s
docker / docker (push) Failing after 1m11s
6c4cbd13ad
Reviewed-on: #67
Latte added 2 commits 2026-07-05 12:00:28 +00:00
fix(ci): give the smoke-test container a per-commit name
CI / lint (pull_request) Successful in 55s
docker / lint (pull_request) Successful in 56s
CI / test (pull_request) Successful in 1m6s
docker / test (pull_request) Successful in 1m4s
CI / publish (pull_request) Has been skipped
docker / docker (pull_request) Successful in 41s
288c696f28
The Docker smoke-test ran the server container under the fixed name
familiar-smoke. On the self-hosted runner the job shares the host
Docker daemon, so a container left behind by a killed or re-run job
(or a concurrent dev push + PR) collides on that name and 'docker run'
fails with exit 125.

Derive the name from the commit SHA, remove any stale container of that
name before starting, and thread the name through the smoke-test and
cleanup steps.
Merge pull request 'fix(ci): give the smoke-test container a per-commit name' (#68) from fix/smoke-test-container-name into dev
CI / lint (push) Successful in 55s
docker / lint (push) Successful in 57s
CI / test (push) Successful in 1m6s
docker / test (push) Successful in 1m6s
CI / lint (pull_request) Successful in 50s
CI / test (pull_request) Successful in 58s
docker / lint (pull_request) Successful in 52s
CI / publish (pull_request) Has been skipped
docker / docker (pull_request) Successful in 1m18s
docker / test (pull_request) Successful in 1m1s
CI / publish (push) Successful in 54s
docker / docker (push) Successful in 48s
d69c994da0
Reviewed-on: #68
Latte scheduled this pull request to auto merge when all checks succeed 2026-07-05 15:50:15 +00:00
Latte merged commit 3aeb4a80d9 into main 2026-07-05 15:50:53 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Hiddenden/familiar#65