Add deploy infra for contacts.nlma.io#1
Open
NextLevelManagementAdvisors wants to merge 7 commits into
Open
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
deploy/systemd/contacts-mcp.service) as alt runtime for a venv install at/opt/contacts-mcpdeploy/nginx.conf.exampledeploy/README.mdrunbook covering both runtimes, TLS issuance, verify, update, rollback.gitignorecovers.env/.env.*(keeps.env.example)Test plan
contacts.nlma.iopoints at the VPSdocker compose build.envpopulated,credentials.jsonplaced,docker compose up -dcurl -fsS http://127.0.0.1:8000/returns from VPS loopbacknginx -tpasses, certbot issues certcurl -u <user>:<pw> https://contacts.nlma.io/returns 200 from outside🤖 Generated with Claude Code