MCP Server with Durable History
by gavinarori
README.md
# MCP server — OAuth, stateless instances, external session state, load balancer
## Run it
```bash
npm install
# Single instance, no load balancer:
npm run dev # server on :3000
npm run simulate # in another terminal: OAuth + traffic simulation
npm run simulate:oauth # step-by-step OAuth walkthrough
# Full cluster: 3 backends + load balancer on :3000
npm run cluster # or start pieces individually, see below
```
To start the cluster manually (useful for watching each backend's logs separately):
```bash
PORT=3001 INSTANCE_ID=backend-1 PUBLIC_URL=http://localhost:3000 npm run dev
PORT=3002 INSTANCE_ID=backend-2 PUBLIC_URL=http://localhost:3000 npm run dev
PORT=3003 INSTANCE_ID=backend-3 PUBLIC_URL=http://localhost:3000 npm run dev
npm run lb # load balancer on :3000, routes to the 3 above
```
## Files
**Storage layer** (swap these for real infra later — see bottom of this file)
- `src/session-store.ts` — "hot" tier, stands in for **Redis**.
- `src/history-store.ts` — "durable" tier, stands in for **Postgres**, idempotency-key guarded.
**MCP server**
- `src/mcp-instance.ts` — registers the `chat` tool: resolve session → append turn → reply → update session.
- `src/server.ts` — Express + Streamable HTTP transport + OAuth wiring. Distinguishes the **MCP protocol session** (transport handshake, in-memory `Map`, dies with the process) from the **application conversation session** (your data, in the stores above).
**OAuth 2.1** (RFC 7591 dynamic registration + PKCE, via the MCP SDK's built-in auth router)
- `src/oauth-provider.ts` — in-memory authorization server: clients, codes, tokens. Auto-approves consent (see the big comment at the top — this is the one thing to swap for real login before production).
- `src/oauth-client-helper.ts` — shared client-side helper: `registerClient()` once per app, `getAccessToken()` once per user/login.
- `src/oauth-simulate.ts` — didactic step-by-step walkthrough of the whole OAuth flow, with an unauthenticated call and a bad-token call to prove enforcement.
**Load balancer**
- `src/load-balancer.ts` — health-checked reverse proxy across N backends. Currently **IP-sticky**, not round-robin — see the file's top comment for exactly why (OAuth tokens + MCP protocol sessions are still per-instance in-memory, so a client's whole lifecycle must land on one backend until that state is externalized too).
**Simulation**
- `src/simulate-traffic.ts` — real MCP client: registers one OAuth client (app-level), then runs one multi-turn conversation (continuity proof) and 25 concurrent new conversations (load proof), each with its own per-user token.
## What's simulated vs. real here
Real: MCP protocol handshake, tool registration/invocation, OAuth 2.1 (PKCE, dynamic registration, token issuance/verification/revocation), the reverse proxy, health checks.
Simulated, so you can run this with zero external infra: `SessionStore`, `HistoryStore`, and the OAuth provider's clients/codes/tokens are all in-memory `Map`s. The login screen in `oauth-provider.authorize()` is also stubbed to auto-approve as a single demo user.
## Next steps to production
1. **Redis** for `session-store.ts` (`ioredis`, same method signatures).
2. **Postgres** for `history-store.ts` (`pg`/Drizzle/Prisma, same method signatures).
3. **Shared token store** for `oauth-provider.ts` (Redis again works well) — this is what lets the load balancer drop IP-stickiness and go pure round-robin, since any instance can then verify any token.
4. **Real login/consent UI** in `oauth-provider.authorize()`, replacing the auto-approve.
5. **MCP protocol session** (the `Map` in `server.ts`) still wants either sticky routing by `Mcp-Session-Id`, or a client that reconnects per-request — this one is inherent to the Streamable HTTP transport, not something storage-swapping fixes.
6. A real queue in front for burst absorption once traffic exceeds what autoscaling can react to in time.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues