Builds on the merged familiar serve HTTP transport (#63) to make familiar production-ready as a remote MCP server: a Docker image that's built and pushed to the Gitea registry on every merge to dev/main, hardening for a public bind, an optional OAuth mode for the claude.ai web connector, and complete per-client connection docs.
stdio is untouched: familiar / python -m familiar still run exactly as before.
Serve hardening
/healthz — an unauthenticated liveness route returning only {status, server, version} (no account data, no token state), for Docker/proxy probes. Never served over stdio.
Fail-closed bind — familiar already refused to start without a token; now the refusal message distinguishes a loopback bind from a public (0.0.0.0) one, and there's an explicit FAMILIAR_ALLOW_INSECURE_BIND=true escape hatch to run unauthenticated (with a loud warning) when auth is handled upstream.
FAMILIAR_AUTH_MODE=oauth — opt-in OAuth 2.1 resource server. A JWTVerifier validates external-issuer JWTs (JWKS via FAMILIAR_OAUTH_JWKS_URI, or discovered from FAMILIAR_OAUTH_ISSUER's OIDC metadata) against issuer + audience, wrapped in a RemoteAuthProvider that serves /.well-known/oauth-protected-resource/<path>. familiar is a resource server only — no authorization server, no DCR; that needs an external IdP (Authentik/Keycloak). bearer mode is unchanged and stays the default; needed only for the claude.ai web connector.
Docker + CI
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, HEALTHCHECK on /healthz. No token baked in.
docker-compose.yml — external proxy network, Traefik TLS labels (nginx/Caddy documented as alternatives), read-only rootfs, tmpfs /tmp, no-new-privileges, cap_drop: ALL, non-root; pulls FAMILIAR_IMAGE with a local build fallback; writes off by default.
.gitea/workflows/docker.yml — a standalone workflow (the existing ci.yml wheel publish is left intact). Lint → test → build → smoke-test (non-root uid, /healthz 200, MCP 401 without / 200 with a token) → push on a main/dev merge to git.hiddenden.cafe/hiddenden/familiar (main → :latest/:main, dev → :dev, plus :sha-<12>) via the existing REGISTRY_TOKEN, then scoped cleanup.
doc/deployment.md — remote-server guide: secure defaults, bearer vs OAuth, full env-var reference, one-time login / token-store provisioning (incl. headless), published images, docker compose, and TLS reverse-proxy blocks for nginx (proxy_buffering off; + long read timeout), Caddy, and Traefik.
doc/clients/ — one page each with exact steps: Claude Code, VS Code (.vscode/mcp.json with a masked token input + the workspace-.mcp.json header gotcha), Mistral Le Chat, Claude Desktop + Cowork (mcp-remote bridge, per-OS paths), and Claude web (oauth + external IdP, with "use Desktop + bearer" as the simpler alternative). Plus an index.
README gets a Connect a client matrix + a compact Deploy with Docker section; doc/README.md index and CLAUDE.md env vars updated.
Note: I placed the new docs under the repo's existing doc/ directory (not a separate docs/) to keep a single documentation tree.
Tests
Extends tests/test_serve.py (conftest style, no async plugin): /healthz (200, no leak); fail-closed public/loopback bind + the escape hatch; oauth (valid JWT 200; missing/expired/wrong-audience 401 + WWW-Authenticate; protected-resource metadata contents); plus the new config readers. Bearer mode and the stdio path are unchanged.
uv run ruff format --check . && uv run ruff check . && uv run pytest is green — 189 passed, coverage 90% (≥ 80%). The OAuth path was also verified end-to-end in-process with a local RSA keypair (valid/expired/wrong-audience → correct 401/200 and the metadata endpoint).
## What
Builds on the merged `familiar serve` HTTP transport (#63) to make familiar production-ready as a remote MCP server: a Docker image that's built and pushed to the Gitea registry on every merge to `dev`/`main`, hardening for a public bind, an optional OAuth mode for the claude.ai web connector, and complete per-client connection docs.
**stdio is untouched**: `familiar` / `python -m familiar` still run exactly as before.
## Serve hardening
- **`/healthz`** — an unauthenticated liveness route returning only `{status, server, version}` (no account data, no token state), for Docker/proxy probes. Never served over stdio.
- **Fail-closed bind** — familiar already refused to start without a token; now the refusal message distinguishes a loopback bind from a public (`0.0.0.0`) one, and there's an explicit `FAMILIAR_ALLOW_INSECURE_BIND=true` escape hatch to run unauthenticated (with a loud warning) when auth is handled upstream.
- **`FAMILIAR_AUTH_MODE=oauth`** — opt-in OAuth 2.1 **resource server**. A `JWTVerifier` validates external-issuer JWTs (JWKS via `FAMILIAR_OAUTH_JWKS_URI`, or discovered from `FAMILIAR_OAUTH_ISSUER`'s OIDC metadata) against issuer + audience, wrapped in a `RemoteAuthProvider` that serves `/.well-known/oauth-protected-resource/<path>`. familiar is a resource server only — **no** authorization server, **no** DCR; that needs an external IdP (Authentik/Keycloak). `bearer` mode is unchanged and stays the default; needed only for the claude.ai web connector.
## Docker + CI
- **`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, `HEALTHCHECK` on `/healthz`. No token baked in.
- **`docker-compose.yml`** — external `proxy` network, Traefik TLS labels (nginx/Caddy documented as alternatives), read-only rootfs, tmpfs `/tmp`, `no-new-privileges`, `cap_drop: ALL`, non-root; pulls `FAMILIAR_IMAGE` with a local build fallback; writes off by default.
- **`.gitea/workflows/docker.yml`** — a standalone workflow (the existing `ci.yml` wheel publish is left intact). Lint → test → build → smoke-test (non-root uid, `/healthz` 200, MCP 401 without / 200 with a token) → push on a `main`/`dev` merge to `git.hiddenden.cafe/hiddenden/familiar` (`main` → `:latest`/`:main`, `dev` → `:dev`, plus `:sha-<12>`) via the existing `REGISTRY_TOKEN`, then scoped cleanup.
- `.dockerignore` + `.env.example` (every `FAMILIAR_*` incl. auth mode, OAuth settings, escape hatch).
## Docs
- **`doc/deployment.md`** — remote-server guide: secure defaults, bearer vs OAuth, full env-var reference, one-time login / token-store provisioning (incl. headless), published images, docker compose, and TLS reverse-proxy blocks for nginx (`proxy_buffering off;` + long read timeout), Caddy, and Traefik.
- **`doc/clients/`** — one page each with exact steps: Claude Code, VS Code (`.vscode/mcp.json` with a masked token input + the workspace-`.mcp.json` header gotcha), Mistral Le Chat, Claude Desktop + Cowork (`mcp-remote` bridge, per-OS paths), and Claude web (oauth + external IdP, with "use Desktop + bearer" as the simpler alternative). Plus an index.
- README gets a **Connect a client** matrix + a compact **Deploy with Docker** section; `doc/README.md` index and `CLAUDE.md` env vars updated.
> Note: I placed the new docs under the repo's existing **`doc/`** directory (not a separate `docs/`) to keep a single documentation tree.
## Tests
Extends `tests/test_serve.py` (conftest style, no async plugin): `/healthz` (200, no leak); fail-closed public/loopback bind + the escape hatch; oauth (valid JWT 200; missing/expired/wrong-audience 401 + `WWW-Authenticate`; protected-resource metadata contents); plus the new config readers. Bearer mode and the stdio path are unchanged.
`uv run ruff format --check . && uv run ruff check . && uv run pytest` is green — **189 passed, coverage 90%** (≥ 80%). The OAuth path was also verified end-to-end in-process with a local RSA keypair (valid/expired/wrong-audience → correct 401/200 and the metadata endpoint).
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.
- 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.
Latte
merged commit 0fdba707fe into dev2026-07-05 09:54:01 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
Builds on the merged
familiar serveHTTP transport (#63) to make familiar production-ready as a remote MCP server: a Docker image that's built and pushed to the Gitea registry on every merge todev/main, hardening for a public bind, an optional OAuth mode for the claude.ai web connector, and complete per-client connection docs.stdio is untouched:
familiar/python -m familiarstill run exactly as before.Serve hardening
/healthz— an unauthenticated liveness route returning only{status, server, version}(no account data, no token state), for Docker/proxy probes. Never served over stdio.0.0.0.0) one, and there's an explicitFAMILIAR_ALLOW_INSECURE_BIND=trueescape hatch to run unauthenticated (with a loud warning) when auth is handled upstream.FAMILIAR_AUTH_MODE=oauth— opt-in OAuth 2.1 resource server. AJWTVerifiervalidates external-issuer JWTs (JWKS viaFAMILIAR_OAUTH_JWKS_URI, or discovered fromFAMILIAR_OAUTH_ISSUER's OIDC metadata) against issuer + audience, wrapped in aRemoteAuthProviderthat serves/.well-known/oauth-protected-resource/<path>. familiar is a resource server only — no authorization server, no DCR; that needs an external IdP (Authentik/Keycloak).bearermode is unchanged and stays the default; needed only for the claude.ai web connector.Docker + CI
docker/Dockerfile— multi-stage uv wheel build; runtime runsfamiliar serveas a non-root user (uid 1000) on port 8080,/datavolume for the encrypted token store,HEALTHCHECKon/healthz. No token baked in.docker-compose.yml— externalproxynetwork, Traefik TLS labels (nginx/Caddy documented as alternatives), read-only rootfs, tmpfs/tmp,no-new-privileges,cap_drop: ALL, non-root; pullsFAMILIAR_IMAGEwith a local build fallback; writes off by default..gitea/workflows/docker.yml— a standalone workflow (the existingci.ymlwheel publish is left intact). Lint → test → build → smoke-test (non-root uid,/healthz200, MCP 401 without / 200 with a token) → push on amain/devmerge togit.hiddenden.cafe/hiddenden/familiar(main→:latest/:main,dev→:dev, plus:sha-<12>) via the existingREGISTRY_TOKEN, then scoped cleanup..dockerignore+.env.example(everyFAMILIAR_*incl. auth mode, OAuth settings, escape hatch).Docs
doc/deployment.md— remote-server guide: secure defaults, bearer vs OAuth, full env-var reference, one-time login / token-store provisioning (incl. headless), published images, docker compose, and TLS reverse-proxy blocks for nginx (proxy_buffering off;+ long read timeout), Caddy, and Traefik.doc/clients/— one page each with exact steps: Claude Code, VS Code (.vscode/mcp.jsonwith a masked token input + the workspace-.mcp.jsonheader gotcha), Mistral Le Chat, Claude Desktop + Cowork (mcp-remotebridge, per-OS paths), and Claude web (oauth + external IdP, with "use Desktop + bearer" as the simpler alternative). Plus an index.doc/README.mdindex andCLAUDE.mdenv vars updated.Tests
Extends
tests/test_serve.py(conftest style, no async plugin):/healthz(200, no leak); fail-closed public/loopback bind + the escape hatch; oauth (valid JWT 200; missing/expired/wrong-audience 401 +WWW-Authenticate; protected-resource metadata contents); plus the new config readers. Bearer mode and the stdio path are unchanged.uv run ruff format --check . && uv run ruff check . && uv run pytestis green — 189 passed, coverage 90% (≥ 80%). The OAuth path was also verified end-to-end in-process with a local RSA keypair (valid/expired/wrong-audience → correct 401/200 and the metadata endpoint).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.