Skip to content

Repository files navigation

Todo MCP Server

A Model Context Protocol 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.

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:

# 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.

About

MCP server with a from-scratch OAuth 2.1 authorization server (DCR, PKCE, audience-bound JWTs) inside a Next.js todo app - connect Claude to it end to end

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages