Skip to content

Add deploy infra for contacts.nlma.io#1

Open
NextLevelManagementAdvisors wants to merge 7 commits into
masterfrom
deploy/contacts-nlma-io
Open

Add deploy infra for contacts.nlma.io#1
NextLevelManagementAdvisors wants to merge 7 commits into
masterfrom
deploy/contacts-nlma-io

Conversation

@NextLevelManagementAdvisors

Copy link
Copy Markdown
Owner

Summary

  • Dockerfile + docker-compose (loopback-only 127.0.0.1:8000 bind) for containerized runtime
  • Systemd unit (deploy/systemd/contacts-mcp.service) as alt runtime for a venv install at /opt/contacts-mcp
  • Nginx reverse-proxy example with TLS + basic auth in deploy/nginx.conf.example
  • deploy/README.md runbook covering both runtimes, TLS issuance, verify, update, rollback
  • .gitignore covers .env / .env.* (keeps .env.example)

Test plan

  • DNS for contacts.nlma.io points at the VPS
  • Build image locally: docker compose build
  • On VPS: .env populated, credentials.json placed, docker compose up -d
  • curl -fsS http://127.0.0.1:8000/ returns from VPS loopback
  • Nginx config deployed, nginx -t passes, certbot issues cert
  • curl -u <user>:<pw> https://contacts.nlma.io/ returns 200 from outside
  • Confirm port 8000 is NOT reachable publicly (only 80/443)

🤖 Generated with Claude Code

ForrestOfFidum and others added 7 commits April 16, 2026 23:18
Dockerfile + docker-compose (loopback-only bind), systemd unit as
alternative runtime, nginx reverse-proxy example with TLS + basic auth,
and deploy/README.md runbook. Extends .gitignore to cover .env files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop Dockerfile/compose in favor of the house pattern: systemd +
Python 3.12 venv at /opt/contacts-mcp, nginx proxy to 127.0.0.1:8020,
certbot-managed TLS. Unit runs as root to match hospitable-mcp /
baselane-mcp services. Nginx config mirrors hospitable.nlma.io's
shape so certbot's --nginx flow slots in without surprises.

Runbook rewritten to target the NLMA Hostinger VPS directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FastMCP.run() takes only (transport, mount_path) and transport must be
one of stdio/sse/streamable-http — not the plain 'http' string. Host
and port are set via mcp.settings before run(). Without this the
service crashed on boot with TypeError: unexpected keyword argument
'host'.

Also ignore the local .venv-oauth helper venv and oauth_consent.py
one-shot script used during first-time deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FastMCP's transport_security rejects any Host header not in its
allowlist (DNS rebinding protection). Behind a reverse proxy with a
public hostname, this meant every external POST /mcp returned 421
Misdirected Request until the vhost was whitelisted. Reads a comma-
separated list from MCP_ALLOWED_HOSTS and appends to the allowlist
before run().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When GOOGLE_REFRESH_TOKEN is supplied via env, the constructed
Credentials object has a refresh token but no access token, so
.valid is False while .expired is also False (no expiry to
compare). The old gate required .expired=True before calling
.refresh(), so the code fell through to InstalledAppFlow
.run_local_server() — which fails on a headless server with
"could not locate runnable browser" and leaves the service in
"Google Contacts service is not available" state despite a
perfectly good refresh token in the environment.

Drop the .expired check: if we have a refresh token and the
creds aren't valid (no access token yet, or expired), refresh
is always the correct action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keeps today's single-tenant mode fully working (AUTH_MODE defaults to
'single'; the old env-provided refresh token code path is unchanged).

When AUTH_MODE=multi:

- The server runs as an OAuth 2.1 authorization server for its MCP
  clients (SDK-provided endpoints: /.well-known/oauth-authorization-server,
  /authorize, /token, /register) and as a resource server on /mcp.
- GoogleOAuthProvider in auth_provider.py implements the
  OAuthAuthorizationServerProvider protocol. authorize() redirects the
  user through Google consent using a Web OAuth client; the browser
  lands back at /oauth/google/callback (new Starlette route in
  google_oauth_routes.py), where we exchange the Google code for a
  refresh token, upsert the user keyed by verified email, and mint our
  own authorization code for the MCP client.
- db.py (stdlib sqlite3, WAL) persists: users (email -> refresh_token),
  oauth_states, auth_codes, access_tokens, refresh_tokens, clients
  (for DCR). DB path defaults to /opt/contacts-mcp/data.db.
- tools.py now has _resolve_service(): in multi mode it reads the
  caller's identity from get_access_token() (populated by
  AuthContextMiddleware via our GoogleAccessToken subclass, which
  carries google_email), looks up the user's refresh_token, and builds
  a per-caller GoogleContactsService via a new from_tokens() classmethod
  on GoogleContactsService. A small thread-safe OrderedDict LRU caches
  the built services briefly so consecutive tool calls in a session
  don't hammer Google's token endpoint.
- GoogleContactsService gained a cache_token flag (token.json is off
  in multi mode so users don't stomp each other) and an explicit
  refresh_token constructor arg that outranks the env var.

Config additions (env): AUTH_MODE, GOOGLE_WEB_CLIENT_ID,
GOOGLE_WEB_CLIENT_SECRET, OAUTH_ISSUER_URL, GOOGLE_OAUTH_REDIRECT_URI,
DB_PATH. All optional; main.py validates them when multi mode is chosen
and fails fast at startup if any are missing.

Nginx config is unchanged — the vhost already proxies all paths to
127.0.0.1:8020, so /authorize, /token, /oauth/google/callback, and
/.well-known/* pass through without edits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants