minion.town
by kriscendobot
README.md
# minion.town ποΈ
A **toy MCP (Model Context Protocol) server with OAuth 2.1 authentication and
authorization**, built to validate one design: an MCP server that has an
*excellent local developer experience* and a *clean path to AWS* β with the
**same code** running in both places, differing only by configuration.
It's a tiny "minion town": three MCP tools, each gated by an OAuth scope.
> Full design rationale: **[`designs/mcp-oauth.md`](designs/mcp-oauth.md)**.
> AWS deploy: **[`infra/README.md`](infra/README.md)**.
## Quickstart (local, no cloud, no Docker)
```sh
cp .env.example .env
npm install
npm run dev # mock OAuth server (:9000) + MCP server (:3000), hot-reload
```
In another terminal, run the full OAuth + MCP flow:
```sh
npm run client # PKCE flow β every tool succeeds
npm run client -- read-only # omit the write scope β summon_minion is denied
```
```
β running OAuth flow (scope: mcp:tools mcp:minions:read mcp:minions:write)
β got access token (eyJraWQiOiI0NjY5β¦)
tools: minion_status, list_minions, summon_minion
minion_status: β ποΈ minion.town has 0 minion(s).
summon_minion: β β¨ Summoned Bob (minion-1) to do "guard the gate".
list_minions: β β’ Bob β guard the gate
```
Run the end-to-end authN/authZ tests (real mock AS + real middleware, nothing
about the auth path is stubbed):
```sh
npm test
```
## The tools and their scopes
| Tool | Scope required | Does |
| --- | --- | --- |
| `minion_status` | `mcp:tools` | count minions in town |
| `list_minions` | `mcp:minions:read` | list the roster |
| `summon_minion` | `mcp:minions:write` | summon a new minion |
A token only carrying `mcp:tools mcp:minions:read` can list but **cannot**
summon β authorization is enforced from the token's scopes.
## How it works (one paragraph)
The MCP server is an OAuth 2.1 **resource server**. Unauthenticated requests get
`401` + `WWW-Authenticate` pointing at **RFC 9728 Protected Resource Metadata**,
which advertises the authorization server. The client runs **PKCE** auth-code
with an **RFC 8707 `resource`** parameter; the server validates the JWT's
signature, issuer, expiry, and **audience** (so a token for another server is
rejected), then maps the token's **scopes β which tools** it may call. Locally
the authorization server is [`oauth2-mock-server`](https://www.npmjs.com/package/oauth2-mock-server);
on AWS it's Amazon Cognito. The server code is identical; only `src/config.ts`
reads different env vars. Built on `@modelcontextprotocol/sdk` and grounded in
the MCP authorization spec (2025-06-18 + Nov 2025 update).
## Layout
```
src/ MCP resource server (config seam, verifier, scopeβtool map, tools, HTTP)
dev/ local dev harness: mock OAuth AS + browserless PKCE client
test/ end-to-end authN/authZ tests
infra/ AWS CDK deploy (App Runner + Cognito + Route53/ACM) β documented path
designs/ the design document
```
## Security
No secrets are committed. `.env` is gitignored; AWS secrets are injected by the
platform. See `designs/mcp-oauth.md` Β§ Threat-model notes.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing