Skip to main content
Glama
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.