Skip to main content
Glama
ImKevinLe

MCP OAuth Gateway

by ImKevinLe
README.md
# MCP OAuth Gateway (multi-tenant)

One deploy that lets any client connect **their own ChatGPT or Claude** to their
brand's n8n MCP toolkit with a one-click **"Sign in"** — no token to paste, no CLI.

It speaks the OAuth flow the AI apps require, authenticates the human with a
**passwordless magic-link email**, then transparently proxies MCP traffic to the
brand's n8n endpoint with the n8n bearer token injected server-side. n8n is
untouched. Each brand is a **tenant** served at its own subdomain; a single
gateway instance serves them all.

```
client's ChatGPT / Claude  →  https://<brand>.mcp.lucyboo.io/mcp
        │  OAuth: discover → register → sign in (email link) → token
        ▼
  ┌──────────────────────────────┐
  │  MCP OAuth Gateway (this app) │   one Coolify service, many tenants
  │  • per-host OAuth server      │
  │  • magic-link sign-in         │
  │  • tenant registry (SQLite)   │
  └───────────────┬──────────────┘
                  │  Authorization: Bearer <that tenant's n8n token>
                  ▼
   n8n MCP endpoint for that brand  (lucyboo.io/mcp/<brand>)
```

## Client prerequisite

Custom MCP connectors need a **paid** plan:

- **ChatGPT:** Plus, Pro, Business, or Enterprise (Developer mode / connectors).
- **Claude:** Pro, Max, Team, or Enterprise.

Free tiers can't add any connector. Confirm before onboarding.

## One-time setup

1. **Wildcard DNS:** point `*.mcp.lucyboo.io` at the server.
2. **Wildcard TLS:** in Coolify, use a wildcard cert (DNS challenge) for
   `*.mcp.lucyboo.io`, or add each brand subdomain to the app so Coolify issues a
   cert per host. Wildcard means zero per-client cert steps.
3. **Deploy this repo** in Coolify: New Resource → Application → Dockerfile.
   - Persistent volume mounted at `/data`.
   - Env vars from [`.env.example`](./.env.example): `BASE_DOMAIN`, `SESSION_SECRET`,
     the `SMTP_*` values, `DB_PATH=/data/gateway.db`.
   - Bind the app to `*.mcp.lucyboo.io` (or the wildcard domain).
4. Deploy. `https://anything.mcp.lucyboo.io/healthz` should return
   `{"ok":true,...}` once DNS resolves.

## Onboard a client (per brand)

Two steps, no redeploy:

**1. Stand up the brand's n8n workflow** — clone the Le Design MCP workflow, swap
in that client's tools, credentials, brand data, and a fresh bearer token,
activate it. (This is the real per-client work; it's the brand's actual toolkit.)

**2. Register the tenant** — run the admin CLI in the container (Coolify → the
service's Terminal), or over SSH:

```bash
node src/admin.js add \
  --slug a1grass \
  --brand "A-1 Grass" \
  --n8n https://lucyboo.io/mcp/a1-grass \
  --token <that-workflow's-n8n-bearer-token> \
  --emails owner@a1grass.com,ops@a1grass.com
```

The client's connector URL is then `https://a1grass.mcp.lucyboo.io/mcp`.

Other commands: `node src/admin.js list`, `node src/admin.js remove --slug a1grass`.
(You can also seed tenants on boot with a `TENANTS_SEED` JSON file — see
[`tenants.json.example`](./tenants.json.example).)

## What you send the client

- Connector URL: `https://<brand>.mcp.lucyboo.io/mcp`
- In **Claude**: Add custom connector → that URL → **Sign in now** → OAuth client
  **Register automatically**. (Not "Use Claude's published identity".)
- In **ChatGPT**: add a custom MCP server with that URL, complete the sign-in.

They enter an approved email, click the link in their inbox, and it's live.

## Security

- Tenants are isolated by subdomain; a token issued for one brand can't call
  another. Sessions, codes, and tokens are all tagged by tenant.
- `allowed_emails` per tenant is the allowlist — only those addresses can sign in
  or hold a token. Remove one to revoke that person (tokens fail on next check).
- The n8n token never reaches the client; the proxy injects it server-side.
- Access tokens last 1 hour; refresh tokens are issued. HTTPS only; cookies are
  `Secure` + `HttpOnly`.
- The DB (on the private `/data` volume) holds each tenant's n8n token. Keep the
  volume private; it's the one secret store.

## Known limitations (v1)

- **Magic link is same-device** — open the email link in the browser that started
  the connect flow so the AI app's callback resumes cleanly.
- SQLite on a mounted volume — right for this scale, not multi-node.
- All of a tenant's tools are exposed to any signed-in user for that brand; no
  per-user tool scoping yet.

## Local smoke test

```bash
npm install
BASE_DOMAIN=mcp.local SESSION_SECRET=dev DB_PATH=./dev.db \
  SMTP_HOST=localhost SMTP_PORT=25 SMTP_USER=x SMTP_PASS=x npm start
# add a tenant, then curl with a Host header:
node src/admin.js add --slug demo --brand Demo --n8n https://example.com/mcp --token t --emails you@x.com
curl -s -H 'Host: demo.mcp.local' http://localhost:8080/.well-known/oauth-authorization-server
```