Skip to main content
Glama
pregnantboy

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`.