Todo MCP Server
by shubhamc1947
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).
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues