lab-mcp
by rce
README.md
# lab-mcp
Remote MCP servers for Claude Cowork, hosted on the homelab.
Cowork only attaches *remote* MCP servers, and it reaches them from
Anthropic's cloud over public HTTPS, whichever client you use. So every server
here is a small web service on `salvage`: an app entry in the `homelab` repo's
`provision.yml`, which turns into a quadlet behind Caddy, image pulled from
GHCR. Each server is also its own OAuth 2.1 authorization server for Claude,
delegating the actual login to Google and refusing every Google account but
the listed ones.
## Layout
packages/mcp_common settings, Google login with the email allowlist, serving
servers/hello the dummy server: `ping` and `whoami`
tests pytest; run with `uv run pytest`
Containerfile one image for all servers; CMD picks the server
One uv workspace. Add a server as `servers/<name>` with a console script,
register it in the root `pyproject.toml`, and give it an app entry in
`homelab`.
## Configuration
Everything is an environment variable, and anything missing stops startup
with a message naming what to set. On the homelab the quadlet sets the first
two and injects the rest as podman secrets.
| Variable | Meaning |
| --- | --- |
| `BASE_URL` | Public URL, e.g. `https://hello-mcp.rce.fi`. Also the OAuth issuer, so it must match what Caddy serves. |
| `PORT` | Listen port, default `8080`. |
| `MCP_GOOGLE_CLIENT_ID` | Google OAuth client id. |
| `MCP_GOOGLE_CLIENT_SECRET` | Its secret. |
| `MCP_JWT_SIGNING_KEY` | High-entropy string. Signs the tokens handed to Claude and encrypts stored client registrations. |
| `MCP_ALLOWED_EMAILS` | Comma-separated Google accounts allowed in. |
| `FASTMCP_HOME` | Where FastMCP persists Claude's registrations and Google sessions. A volume in the container. |
| `MCP_LISTEN_HOST` | Default `0.0.0.0`. |
Claude is pointed at `<BASE_URL>/mcp`. `/healthz` answers `ok` without a
token.
## Google OAuth client
One client serves every server in this repo. In Google Cloud Console:
1. Create a project, configure the consent screen as **External** and
**publish** it. Only `openid` and `email` are requested, which needs no
verification and, unlike Testing status, does not expire refresh tokens
after seven days.
2. Create an OAuth client of type **Web application**. Add one redirect URI
per server: `https://hello-mcp.rce.fi/auth/callback`, and
`http://localhost:8080/auth/callback` for local runs.
Login-only scopes are allowed under Advanced Protection and by Workspace
admins. Gmail and Drive scopes are not, unless the admin trusts the app; that
is a separate decision for the servers that need them.
## Deploying
Secrets first, once, from the `homelab` checkout. Each prompts for the value:
./set-secret.sh hello-mcp MCP_GOOGLE_CLIENT_ID
./set-secret.sh hello-mcp MCP_GOOGLE_CLIENT_SECRET
./set-secret.sh hello-mcp MCP_ALLOWED_EMAILS
head -c 48 /dev/urandom | base64 -w0 | ./set-secret.sh hello-mcp MCP_JWT_SIGNING_KEY
Then `./run-playbook.sh provision.yml`. After that, pushing to `main` here
runs the tests, pushes `ghcr.io/rce/lab-mcp:main`, and `podman auto-update` on
`salvage` picks it up within five minutes.
In Claude: Settings, Connectors, Add custom connector, URL
`https://hello-mcp.rce.fi/mcp`. It discovers the OAuth endpoints, registers
itself, sends you through Google, and `whoami` should answer with your
account.
## Running locally
export MCP_GOOGLE_CLIENT_ID=...apps.googleusercontent.com
export MCP_ALLOWED_EMAILS=you@example.com
printf '%s' 'GOCSPX-...' > .local/google_client_secret
./scripts/run-hello.sh
Cowork cannot reach localhost, so this is for the OAuth endpoints and for an
MCP client run by hand, e.g. the MCP Inspector.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues