MCP server for integration with a Next.js AI chat to manage todo lists.
mcp-server
MCP server for integration with a Next.js AI chat. Built with Hono + Node.js, using Streamable HTTP transport (MCP spec 2025-03-26).
| Target | Adapter | Persistence |
|---|---|---|
| Render / Fly.io | InMemoryAdapter |
❌ lost on restart |
| Cloudflare Workers | D1Adapter |
✅ persisted in D1 SQLite |
Quickstart (Node.js / local)
cd mcp-server
npm install
cp .env.example .env # leave everything empty for dev
npm run dev
# → http://localhost:3001/mcp
Set this in Next.js .env.local:
MCP_URL=http://localhost:3001/mcp
Structure
mcp-server/
├── src/
│ ├── index.ts # Node.js entry point (Render / Fly.io) — InMemoryAdapter
│ ├── worker.ts # Cloudflare Workers entry point — D1Adapter
│ ├── auth.ts # Extract userId from Bearer + JWT headers
│ ├── tools.ts # 5 MCP tools (list, add, complete, update, delete)
│ └── db/
│ ├── adapter.ts # TodoDB interface
│ ├── memory.ts # InMemoryAdapter
│ └── d1.ts # D1Adapter (Cloudflare Workers only)
├── migrations/
│ └── 0001_init.sql # D1 schema
├── wrangler.toml # Cloudflare config
├── Dockerfile # For Render / Fly.io
├── fly.toml
└── render.yaml
Tools
| Tool | Description | Input |
|---|---|---|
list_todos |
Display all todos | filter?: "all"|"done"|"pending" |
add_todo |
Add a new todo | title: string |
complete_todo |
Mark as completed | id: string |
update_todo |
Change the title | id: string, title: string |
delete_todo |
Delete a todo | id: string |
Auth
Two layers, both optional — leave empty for dev (fallback X-User-Id):
# .env
MCP_TOKEN= # Bearer token — verifies service identity
MCP_JWT_SECRET= # JWT secret — must match Next.js
TypeScript
This project requires "module": "NodeNext" and "moduleResolution": "NodeNext" in tsconfig.json. The MCP SDK exposes McpServer via a package.json exports wildcard — older settings like "moduleResolution": "bundler" with "module": "ESNext" resolve imports in CJS mode and silently skip the exports map, causing:
Cannot find module '@modelcontextprotocol/sdk/server/mcp.js'
NodeNext reads the project package.json "type": "module" field, resolves in ESM mode, and correctly follows the exports wildcard.
Developer Guide
Add a new tool
Edit src/tools.ts, add server.registerTool(...) inside the registerTools function.
The tool will automatically be available in all adapters because db: TodoDB is passed from outside.
Manual testing with curl
The MCP Streamable HTTP transport (spec 2025-03-26) requires every POST request to include:
Accept: application/json, text/event-stream
The server needs to know the client can handle either a direct JSON response or an SSE stream — omitting this header returns a -32000 Not Acceptable error.
When MCP_TOKEN and MCP_JWT_SECRET are unset, auth falls back to the plain X-User-Id header.
# Health check
curl http://localhost:3001/
# List tools
curl -s -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-User-Id: dev-user" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq
# list_todos (all)
curl -s -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-User-Id: dev-user" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_todos","arguments":{}}}' | jq
# list_todos (filter: pending | done)
curl -s -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-User-Id: dev-user" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_todos","arguments":{"filter":"pending"}}}' | jq
# add_todo
curl -s -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-User-Id: dev-user" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"add_todo","arguments":{"title":"Learn MCP"}}}' | jq
# complete_todo
curl -s -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-User-Id: dev-user" \
-d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"complete_todo","arguments":{"id":"1"}}}' | jq
# update_todo
curl -s -X POST http://localhost:3001/mcp \
-H "Content-Type: application/json" \
-H "Accept: applic
Tools (5)
list_todosDisplay all todosadd_todoAdd a new todocomplete_todoMark as completedupdate_todoChange the titledelete_todoDelete a todoEnvironment Variables
MCP_TOKENBearer token for service identity verificationMCP_JWT_SECRETJWT secret for authentication matching Next.jsConfiguration
{"mcpServers": {"todo": {"command": "node", "args": ["/path/to/mcp-server/dist/index.js"]}}}