Skip to main content
Glama
kriscendobot

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.