Skip to main content
Glama
rce

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.