MCP Gateway
by anantgarg
README.md
# MCP Gateway
A self-hosted [MCP](https://modelcontextprotocol.io) 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/list` only ever contains the tools your role grants, and every `tools/call` re-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 -d` starts one locally)
### 2. Environment
```bash
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](https://console.cloud.google.com/apis/credentials), create an OAuth client of type **Web application** and add the authorized redirect URI:
```
{BETTER_AUTH_URL}/api/auth/callback/google
```
### 3. Database & run
```bash
npm install
npm run db:migrate
npm run dev
```
Open http://localhost:3000, sign in with Google — you're now the admin.
### 4. Configure
1. **Connectors** → add Zendesk (subdomain + agent email + API token) or Chargebee (site + API key). Use *Test connection* before saving.
2. **Roles** → create a role and pick an access level per connector.
3. **Users** → invite teammates by email and assign their role.
### 5. Connect an MCP client
```bash
claude mcp add --transport http gateway http://localhost:3000/api/mcp
```
or in a JSON-config client (Cursor, Claude Desktop):
```json
{ "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](https://better-auth.com) with the Google social provider for the dashboard and its `mcp` plugin 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:generate` regenerates `src/db/auth-schema.ts` after changing the better-auth config; `npm run db:generate && npm run db:migrate` create 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`](https://opennext.js.org/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](https://neon.tech), Supabase, or any managed Postgres. Run the migrations against it once:
```bash
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):
```bash
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:**
```bash
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:**
```bash
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 on `npm install`) fixes a broken published build of pg's Cloudflare socket adapter; without it the Workers bundle fails with `Could not resolve "pg-cloudflare"`.
- Old MCP access tokens expire after 1 hour; clients refresh automatically.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing