Skip to main content
Glama
hasnaties

MCP Connector Test

by hasnaties
README.md
# MCP Connector — OAuth 2.1 auth prototype (Firebase-backed)

A small, self-contained **remote MCP server** that demonstrates the full authentication handshake a
[Claude Connector](https://modelcontextprotocol.io/) goes through — OAuth 2.1 discovery, dynamic
client registration, PKCE, an interactive sign-in page, and a token-protected tool call. Users are
authenticated against a **Firebase** project, and a single `whoami` tool returns their real identity
(profile + plan) read from Firestore.

It was built as a prototype for a Smodin Claude Connector, but it works with any Firebase project —
plug in your own and run it.

> ⚠️ **Prototype, not production.** This server's own access/refresh tokens are HS256 JWTs signed
> with a shared secret, and all OAuth state (registered clients, auth codes) is kept in memory and
> resets on restart. The user identity is real (verified Firebase ID token + Firestore lookup), but
> the token layer is intentionally simple. Don't deploy this as-is.

## How it works

```
Claude / Inspector          This server (:8000)              Firebase
     │  POST /mcp (no token) ──▶ 401 + WWW-Authenticate
     │  reads /.well-known/*  ──▶ OAuth discovery + PKCE metadata
     │  POST /register        ──▶ dynamic client registration
     │  opens /authorize ─────▶ 302 → /login (our sign-in page)
     │                              │  Firebase JS SDK sign-in ──▶ ID token
     │                              │  POST /login/complete (ID token)
     │                              │  verify token + read Firestore profile/plan
     │  POST /token (+PKCE) ──▶ access + refresh token
     │  POST /mcp (Bearer) ───▶ whoami → real identity
```

The MCP SDK's `mcpAuthRouter` and `requireBearerAuth` provide the OAuth HTTP plumbing. This repo
implements the authorization-server logic (`OAuthServerProvider`), the Firebase sign-in page, and the
Firestore identity lookup.

## Project structure

| File | Purpose |
|------|---------|
| `src/index.ts` | Express app: OAuth router, sign-in routes, token-protected `/mcp` (Streamable HTTP). |
| `src/config.ts` | Environment config; derives all OAuth URLs from `PUBLIC_BASE_URL`. |
| `src/tokens.ts` | Sign/verify the server's JWT access + refresh tokens. |
| `src/auth/provider.ts` | `OAuthServerProvider` implementation (authorize / token / PKCE / verify). |
| `src/auth/store.ts` | In-memory stores: clients, pending authorizations, auth codes, refresh tokens. |
| `src/auth/login-page.ts` | Firebase sign-in screen (`GET /login`) + `POST /login/complete`. |
| `src/firebase/admin.ts` | `firebase-admin` init + `verifyIdToken`. |
| `src/firebase/smodin-user.ts` | Firestore profile/plan lookup + paid-status logic. |
| `src/mcp/server.ts` | MCP server + the `whoami` tool. |
| `scripts/seed-user.mjs` | Seed a Firestore profile for a test user (optional). |

## Prerequisites

- Node.js 18+
- A Firebase project (a free/throwaway one is fine)

## Firebase setup

1. **Authentication** → enable a sign-in method. **Email/Password** is recommended (works on
   `localhost` and any tunnel with no extra config). Google works too, but see the caveat below.
2. **Firestore Database** → create it (Native mode). Required even if empty — the profile lookup
   errors if no database exists.
3. **Service account** (Project settings → Service accounts → *Generate new private key*) → download
   the JSON. This lets the server read Firestore. **Keep it outside the repo.**
4. Grab your web app config (Project settings → General → *Your apps*) for the `FIREBASE_*` values.

## Configuration

```bash
cp .env.example .env
```

Fill in `.env` (see comments in `.env.example`):

| Variable | What it is |
|----------|-----------|
| `FIREBASE_PROJECT_ID` | Your Firebase project id |
| `FIREBASE_WEB_API_KEY` | Web API key (public — shipped to the browser) |
| `FIREBASE_AUTH_DOMAIN` | `<project-id>.firebaseapp.com` |
| `GOOGLE_APPLICATION_CREDENTIALS` | Absolute path to the service-account JSON |
| `TOKEN_SECRET` | Long random string for signing this server's JWTs |
| `PUBLIC_BASE_URL` | Only needed behind a tunnel (see below) |

## Run

```bash
npm install
npm run build      # type-check + compile
npm run dev        # watch mode on http://localhost:8000
```

Quick health check:

```bash
curl http://localhost:8000/.well-known/oauth-authorization-server
curl -i -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'   # -> 401 + WWW-Authenticate
```

## Test the full flow (MCP Inspector)

```bash
npm run inspect
```

In the Inspector: transport **Streamable HTTP**, URL `http://localhost:8000/mcp`, **Connect**. It runs
OAuth discovery → registration → opens the sign-in page. Sign in with a test user (email/password),
then call **`whoami`** — it returns that user's real identity.

On a fresh project with no Firestore documents, `whoami` returns the real `uid`/`email` with
`plan: free`. To see a real plan, seed a profile:

```bash
npm run seed -- <FIREBASE_UID> test@example.com "Test User" premium
```

(Get `<FIREBASE_UID>` from Firebase console → Authentication → Users. Pass `free` instead of
`premium` for a free account.)

## Add it to claude.ai

Custom connectors require a paid Claude plan and a **public HTTPS URL**.

1. Tunnel to the local server:
   ```bash
   cloudflared tunnel --url http://localhost:8000   # or: ngrok http 8000
   ```
2. Set `PUBLIC_BASE_URL` in `.env` to the tunnel URL and restart (all OAuth URLs derive from it).
3. In claude.ai → **Settings → Connectors → Add custom connector** → paste
   `https://<your-tunnel-host>/mcp`.
4. Sign in on the connector's page, then ask Claude to run `whoami`.

## Caveats

- **Google sign-in needs an authorized domain.** `signInWithPopup` requires the app origin to be in
  the Firebase project's **Authorized domains**. `localhost` is allowed by default; ad-hoc tunnel
  hosts are not (you'd add them). **Email/password has no such restriction.**
- **Project alignment.** The ID token's `aud`, the client-config `projectId`, and the admin
  `projectId` must all be the same Firebase project.
- The profile/plan is snapshotted into the access token at login (fine for a prototype).

## Security notes

- Never commit `.env` or the service-account JSON — both are gitignored.
- The web API key is intentionally public; the service-account key is a **secret** — keep it out of
  the repo and rotate it if exposed.
- Replace the in-memory token layer before any real deployment.

## License

MIT — see [LICENSE](LICENSE).