Skip to main content
Glama
README.md
# Todo MCP Server

A [Model Context Protocol](https://modelcontextprotocol.io) server with a
**from-scratch OAuth 2.1 authorization server**, built inside a Next.js todo
app. Connect Claude (or any MCP client) to it and the full flow works end to
end: dynamic client registration → PKCE authorization → token exchange →
authenticated tool calls scoped to the signed-in user.

I built this to understand what actually happens when an LLM client connects
to a remote MCP server — not just the tool definitions, but the entire auth
handshake underneath. No auth library for the OAuth server; every endpoint is
hand-rolled against the RFCs.

## What's implemented

**MCP server** (`src/mcp/`, served at `/api/mcp` via Streamable HTTP):

- 6 task tools: `list_tasks`, `get_task`, `create_task`, `update_task`,
  `complete_task`, `delete_task`
- Zod input validation and tool annotations (`readOnlyHint`,
  `destructiveHint`, `idempotentHint`)
- `userId` comes **only** from the validated Bearer token, never from tool
  arguments — an LLM cannot ask for another user's tasks
- Stateless per-request handling, serverless-friendly

**OAuth 2.1 authorization server** (hand-rolled):

- Dynamic Client Registration — RFC 7591 (`/register`)
- Authorization Code + PKCE S256 (`/authorize` with a real consent page)
- Server metadata discovery — RFC 8414
  (`/.well-known/oauth-authorization-server`) plus protected-resource
  metadata for MCP clients
- Token endpoint with `authorization_code` and `refresh_token` grants
- Access tokens: stateless JWTs, 1h TTL, **audience-bound to `/api/mcp`**
  (a token minted for this resource is rejected everywhere else)
- Single-use authorization codes with 5-minute expiry; persisted refresh
  tokens with 30-day TTL

**The app itself**: Next.js 16 + React 19 + Prisma 7 + PostgreSQL, task CRUD
behind a service layer that both the web UI and the MCP tools share.

## Quick start

Requires Node 20+ and PostgreSQL.

```bash
npm install
cp .env.example .env        # set DATABASE_URL and JWT_SECRET
npx prisma migrate deploy
node scripts/seed-mock-users.mjs
npm run dev
```

Then either use the web UI at `http://localhost:3000`, or run the proof
scripts:

```bash
# Bearer-token MCP session: lists tools, calls them, proves per-user scoping
node scripts/mcp-test.mjs

# Full OAuth 2.1 handshake: DCR → PKCE authorize → token exchange →
# authenticated MCP call → refresh grant. Exactly what Claude does internally.
node scripts/oauth-e2e.mjs

# Against a public tunnel instead of localhost:
BASE=https://your-tunnel.example.com node scripts/oauth-e2e.mjs
```

### Connecting Claude

Add the server URL (`https://<host>/api/mcp`) as a custom connector — the
client discovers the authorization server via `.well-known`, registers
itself, and walks the PKCE flow against the consent page.

## Security notes

- Auth codes are consumed atomically before validation — replay gets a 400
- PKCE S256 verified on every code exchange
- JWT `aud` claim pins tokens to the MCP resource (no token passthrough)
- Every service-layer query is scoped `where: { userId }` — ownership is
  enforced below the tool layer, so a buggy tool can't leak across users
- Sign-in is passwordless-by-email **for local development only**; wiring a
  real identity provider is the obvious next step for production use

## Design

The phased plan and decisions (why audience binding, why stateless access
tokens + persisted refresh tokens, why the service layer owns scoping) are in
[docs/specs/2026-07-03-todo-mcp-server-design.md](docs/specs/2026-07-03-todo-mcp-server-design.md).