pokemon-mcp
by pregnantboy
README.md
# Pokemon MCP Workshop ⚡
Build an MCP server, teach Claude to catch 'em all, and beat the Elite Four live on the projector.
You'll clone this repo, connect a half-finished MCP server to MCP Inspector and Claude Desktop, and implement the missing tool. The moment your party is ready and you call `challenge_league`, your name is on the room's leaderboard.
## Pre-workshop setup (do this before you arrive)
Requires **Node.js 20+**.
```sh
git clone <this-repo>
cd pokemon-mcp
npm install
npm run register -- "YourTrainerName" # writes your token into .env
npm run verify # ✅/❌ table — everything except trainer registration should be green
```
If `npm run verify` shows green, you're done until workshop day.
## Architecture
```
┌────────────┐ MCP (stdio) ┌───────────────────┐ HTTPS ┌──────────────┐
│ MCP client │ ───────────────▶│ YOUR MCP SERVER │──────────▶│ PokeAPI │
│ (Inspector,│ stdin/stdout │ (this repo) │ ├──────────────┤
│ Claude) │ │ SDK │──────────▶│ League server│
└────────────┘ └───────────────────┘ Bearer │ (hosted) │
token └──────────────┘
```
- **`src/server.ts`** — assembles the `McpServer`, registers every tool, and connects it to the stdio transport. You'll live here.
- **`src/tools/`** — one file per tool. `get_pokemon` is complete (your worked example); the other three are yours.
- **`src/lib/pokeapi.ts` & `src/lib/league.ts`** — pre-written fetch helpers. You never write HTTP plumbing, only tool logic.
- All your trainer state (party, badges) lives on the league server — your MCP server is stateless.
## 1 · Register as a trainer
```sh
npm run register -- "YourTrainerName"
```
This calls the league's `POST /api/register` and writes your token into `.env`. (Browser fallback: open `<LEAGUE_URL>/register`, copy the token, paste it into `.env` yourself — the classic "create an API key in the dashboard" ritual.)
## 2 · Connect Inspector
```sh
npm run inspect # launches Inspector, which spawns `npm run dev` over stdio
```
In the Inspector UI: transport **STDIO** should already be selected (it launched your server for you). Open **Tools → List Tools** and call `get_pokemon` with `"pikachu"`. That round-trip — schema in, structured result out — is the whole MCP tool model.
## 3 · The worksheet: implement the stub
The stub in `src/tools/addPokemonToParty.ts` already has its zod schema, description and a TODO block with hints. Copy the `get_pokemon` pattern. Test it in Inspector as you go.
### 3a · `add_pokemon_to_party` — what it adds: **an authed POST**
- **What:** validate the name via `getPokemon()` (from `../lib/pokeapi.js`), then `fetchLeague("/api/party", { method: "POST", body: { pokemon } })`.
- **Where:** `src/tools/addPokemonToParty.ts`
- **Hints:** `fetchLeague` already attaches your Bearer token. Catch `PokeApiError` / `LeagueError` and return their `.message` with `isError: true` — the league's error messages (party full, over budget, Kanto rule) are written for the model to self-correct from, so pass them through verbatim.
- **Done when:** Inspector shows your updated party, and adding a 4th member returns a friendly error instead of crashing.
### 3b · `challenge_league` — provided for you: **interpreting a rich response**
`challenge_league` (`src/tools/challengeLeague.ts`) is already implemented — read it after finishing 3a to see the second idea: turning a rich response (`ChallengeResponse` in `src/lib/league.ts`) into a battle report the model can read, instead of `JSON.stringify`. Once your party has 3 members, challenge — your name appears on the projector within ~3 seconds. 🎉
**Stuck?** `git checkout solution` is the escape hatch (then `git checkout main` to keep working).
## 4 · The league rules
| Elite | Types | BST |
| -------- | ------------ | --- |
| Gyarados | water/flying | 540 |
| Gengar | ghost/poison | 500 |
| Articuno | ice/flying | 580 |
| Snorlax | normal | 540 |
**Battle rule (fully deterministic, no dice):** `power = BST × type multiplier` — the best single-type effectiveness among your Pokémon's types against the elite's full type combo. Your Pokémon beats an elite only if its power is **strictly greater** both ways (ties go to the elite — Alakazam vs Gengar is an exact tie, Gengar wins). Immunities multiply to 0: Normal attackers literally cannot touch Gengar. An elite falls if _any_ of your 3 party members beats them.
**Party rules:** exactly 3 to challenge · **original 151 only, legendaries banned** (Articuno, Zapdos, Moltres, Mewtwo, Mew) · combined **BST budget of 1500** across the party. Big stats or good typing — you can't have both. Re-challenge freely: only your best result counts, and post-best attempts don't hurt your rank.
**Leaderboard:** badges → fewest attempts to your best → earliest. Lowest party BST among equal trainers is bragging rights ("won with the cheapest team").
## 5 · Beat the League with Claude 🏆
This is the punchline of the whole workshop. Connect your server to Claude Desktop and give it this prompt, verbatim:
> **Look up each Elite Four member's weaknesses and build me a party of 3 that sweeps them.**
Watch Claude chain `get_pokemon` → `add_pokemon_to_party` → `challenge_league`, read the league's error messages when it busts the budget or picks a legendary, and self-correct. That loop — model + tools + good error messages — is what you just built.
### Connecting Claude Desktop
```sh
npm run mcp-config
```
This prints a `claude_desktop_config.json` snippet with `cwd` already set to your project path — copy it in (Settings → Developer → Edit Config) instead of hand-editing a placeholder path.
Restart Claude Desktop, then ask it things like "look up snorlax" or "add gengar to my party".
## Stretch goals
- `get_type_effectiveness` tool — PokeAPI has `/type/{name}`; save Claude a lookup step.
- `remove_pokemon_from_party` is already provided (`src/tools/removePokemonFromParty.ts`, an authed DELETE) — read it to see how the model swaps members to fix a busted budget.
## Troubleshooting
- **"No trainer token"** → `npm run register -- "YourName"`, then reconnect Inspector/Claude Desktop (the server reads `.env` at boot).
- **PokeAPI slow/down on venue Wi-Fi** → responses are cached in-process after first fetch; the league bundles its own battle data, so challenges still work.
- **Inspector won't connect** → make sure you picked transport **STDIO**, with command `npx` and args `tsx src/server.ts`.
- **`mr. mime`? `farfetch'd`?** → PokeAPI naming is lowercase-hyphenated: `mr-mime`, `farfetchd`, `nidoran-f`.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues