MCP Gateway
A self-hosted MCP gateway with Google login and role-based access control. One Next.js app provides:
- An MCP endpoint (
/api/mcp, Streamable HTTP) that MCP clients like Claude and Cursor connect to. The first time a user performs any action, the standard MCP OAuth 2.1 flow sends them to Google to sign in — no tokens to copy around. - An admin dashboard (shadcn/ui) where the admin configures Zendesk and Chargebee connectors, invites users, and defines roles that control exactly which tools each user gets.
How access control works
- The first user ever to sign in becomes admin. Admins can use every tool of every connector and manage the dashboard.
- Everyone else must be invited by email first — un-invited Google accounts are rejected at sign-in.
- Each user has one role. A role grants access to specific connectors with one of three levels:
- Read — only that connector's read tools
- Read + Write — all of its tools
- Custom — an explicit per-tool selection
- Enforcement happens at the MCP layer:
tools/listonly ever contains the tools your role grants, and everytools/callre-checks the database, so permission changes apply immediately.
Connectors are instances, not singletons — you can add two Zendesk accounts and grant a role access to only one of them. Tool names embed the connector slug, e.g. zendesk_support_search_tickets.
Setup
1. Prerequisites
- Node.js 20+
- PostgreSQL (
docker compose up -dstarts one locally)
2. Environment
cp .env.example .env
Fill in:
| Variable | Value |
|---|---|
DATABASE_URL |
PostgreSQL connection string (the docker-compose default works out of the box) |
BETTER_AUTH_SECRET |
openssl rand -base64 32 |
BETTER_AUTH_URL |
The app's public base URL, e.g. http://localhost:3000 |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
See below |
ENCRYPTION_KEY |
openssl rand -hex 32 — encrypts connector API keys at rest (AES-256-GCM) |
Google credentials: in Google Cloud Console, create an OAuth client of type Web application and add the authorized redirect URI:
{BETTER_AUTH_URL}/api/auth/callback/google
3. Database & run
npm install
npm run db:migrate
npm run dev
Open http://localhost:3000, sign in with Google — you're now the admin.
4. Configure
- Connectors → add Zendesk (subdomain + agent email + API token) or Chargebee (site + API key). Use Test connection before saving.
- Roles → create a role and pick an access level per connector.
- Users → invite teammates by email and assign their role.
5. Connect an MCP client
claude mcp add --transport http gateway http://localhost:3000/api/mcp
or in a JSON-config client (Cursor, Claude Desktop):
{ "mcpServers": { "gateway": { "type": "http", "url": "http://localhost:3000/api/mcp" } } }
The client discovers the OAuth metadata, registers itself dynamically, and opens a browser for Google sign-in. Consent is automatic after login.
Tool catalog
| Connector | Read | Write |
|---|---|---|
| Zendesk | search_tickets, get_ticket, list_ticket_comments, list_users, get_user | create_ticket, update_ticket, add_ticket_comment |
| Chargebee | list_customers, get_customer, list_subscriptions, get_subscription, list_invoices, get_invoice | create_customer, update_customer, cancel_subscription |
Chargebee's cancel_subscription targets Product Catalog 2.0 sites (cancel_for_items).
Architecture notes
- Auth: better-auth with the Google social provider for the dashboard and its
mcpplugin as the OAuth 2.1 authorization server (discovery metadata, dynamic client registration, PKCE) for MCP clients. Both share one user table. - MCP serving: a small built-in Streamable HTTP handler (
src/app/api/mcp/route.ts, stateless JSON responses per the MCP spec) that runs on both Node and Cloudflare Workers; the tool list is rebuilt per request from the caller's current role. - Secrets: connector credentials are AES-256-GCM encrypted with
ENCRYPTION_KEY; they are never sent to the browser. - DB: Drizzle ORM + PostgreSQL.
npm run auth:generateregeneratessrc/db/auth-schema.tsafter changing the better-auth config;npm run db:generate && npm run db:migratecreate and apply migrations.
Dev-only login
Setting ENABLE_DEV_LOGIN=true in .env (ignored in production builds) enables email/password endpoints so you can exercise the dashboard and MCP flow without Google credentials — useful for local testing with multiple fake users.
Deploying to Cloudflare Workers
The app is pre-configured for Cloudflare via @opennextjs/cloudflare (wrangler.jsonc, open-next.config.ts). The gateway stays private on Workers exactly as it is locally: every dashboard page and MCP call requires Google sign-in, only invited emails can log in, and the dev email/password login is compiled out of production builds.
1. You need a Postgres reachable from Cloudflare — e.g. Neon, Supabase, or any managed Postgres. Run the migrations against it once:
DATABASE_URL="postgresql://user:pass@host/db" npm run db:migrate
2. Create a Hyperdrive config (Cloudflare's connection pooler; the app reads its connection string automatically from the HYPERDRIVE binding):
npx wrangler login
npx wrangler hyperdrive create mcp-gateway-db --connection-string="postgresql://user:pass@host/db"
Paste the returned id into wrangler.jsonc under hyperdrive[0].id, and set vars.BETTER_AUTH_URL to your production URL (e.g. https://mcp-gateway.<your-subdomain>.workers.dev).
3. Set secrets:
npx wrangler secret put BETTER_AUTH_SECRET # openssl rand -base64 32
npx wrangler secret put GOOGLE_CLIENT_ID
npx wrangler secret put GOOGLE_CLIENT_SECRET
npx wrangler secret put ENCRYPTION_KEY # openssl rand -hex 32
4. Add the production redirect URI to your Google OAuth client:https://<your-domain>/api/auth/callback/google
5. Deploy:
npm run deploy
Then open the deployed URL, sign in with Google (first login = admin), and point MCP clients at https://<your-domain>/api/mcp.
To test the Workers runtime locally before deploying: npm run preview (uses .dev.vars — copy from .dev.vars.example — and the localConnectionString in wrangler.jsonc).
Notes:
scripts/patch-pg-cloudflare.mjs(run automatically onnpm install) fixes a broken published build of pg's Cloudflare socket adapter; without it the Workers bundle fails withCould not resolve "pg-cloudflare".- Old MCP access tokens expire after 1 hour; clients refresh automatically.