pogo-mcp
# pogo-mcp
An MCP server that gives an LLM reliable, current Pokémon GO mechanics data and
(eventually) PvE/raid analysis tools.
> This project is not affiliated with Niantic, The Pokémon Company, Nintendo, or
> Pokémon GO. Game data is obtained from
> [pokemon-go-api](https://pokemon-go-api.github.io/pokemon-go-api), a
> community-maintained public source, and is **not** an official Niantic API.
> Its Pokémon/move data is derived primarily from PokeMiners' Game Master
> dumps, with raid/quest data scraped from Leek Duck, Pokebattler, and
> Snacknap.
## Status
Data integration is done - species, moves, movesets, forms, and the type
chart, all backed by a validated local snapshot. Core mechanics are in too:
CP/HP calculation, power-up economics, and PvE (raid/gym) move damage -
STAB, type effectiveness, weather, Shadow, Mega/Primal boost, and
friendship, with a full multiplier breakdown returned alongside the final
damage. None of this data comes from pokemon-go-api (it has no CP, economy,
damage-formula, or Shadow/Mega-boost data at all) - every constant is
sourced directly from the game's own datamined Game Master file with exact
citations; see `docs/mechanics-constants.md`.
### Supported now
- Current Pokémon GO species data: base stats, types, legal fast/charged/elite
movesets, generation, Pokédex class.
- Every alternate battle form as its own first-class entity - Mega/Primal
evolutions, and forms like Giratina Altered/Origin, Deoxys
Normal/Attack/Defense/Speed, Zamazenta Hero/Crowned, Kyurem/Black
Kyurem/White Kyurem - never merged just because they share a Pokédex number.
- Move data with PvE fields (power/energy/duration) kept strictly separate
from PvP fields (different power curve, buff/debuff chances).
- Pokémon GO's actual 2-tier type effectiveness chart and the weather→type
boost mapping (sourced from the provider, not hand-maintained).
- Fuzzy-but-safe name resolution (`"Ho-Oh"` / `"ho oh"`, `"Mr. Mime"`,
`"Farfetch'd"` / `"farfetchd"`) that returns a structured
`ambiguous_pokemon`/`ambiguous_move` error with every candidate instead of
silently guessing which battle form or move you meant.
- A locally-owned, validated, versioned snapshot with atomic promotion - tool
calls never make a live network request; refreshes are explicit
(auto on staleness, or via `refresh_data`) and never discard the last known
good snapshot until a new one has been fetched, normalized, *and* validated.
- CP/HP/actual-Attack/actual-Defense calculation for any exact IV spread and
level (1.0-50.0 in 0.5 increments), including the Shadow attack/defense
modifiers the real game applies to displayed CP.
- Power-up Stardust/Candy/Candy XL cost between any two levels, including
Shadow surcharge, Purified discount, and Lucky's Stardust-only discount
(Lucky + Purified can combine; Shadow can't combine with either).
- PvE (raid/gym) move damage for a single hit - STAB, type effectiveness,
weather boost, Shadow attack/defense, same/different-type Mega/Primal raid
boost, friendship attack bonus (Not Friends through Best Friend), and
dodge damage reduction - returning both the final damage and every
intermediate multiplier for transparency.
### Not yet implemented (see `docs/provider-gap-analysis.md`)
DPS/TDO/ER moveset evaluation, raid counter ranking, fast-move breakpoints,
and a full raid battle timeline simulator are all still to come. **PvP is
explicitly out of scope for this project.**
## Architecture
```
pokemon-go-api → PokemonGoApiProvider → Raw* models → normalizer
│
▼
validator → local snapshot (.cache/)
│
▼
in-memory repository
│
▼
MCP tools
```
Nothing outside `src/pogo_mcp/providers/` ever makes a network request or
knows pokemon-go-api's JSON shape. Everything else - normalization, the
repository, and every tool - depends only on the normalized Pydantic models in
`src/pogo_mcp/models/`, so the data source can be replaced later without
touching the rest of the codebase.
## Caching and freshness
On startup, the server loads `.cache/current/` immediately if it exists (no
network call). If it doesn't (first run), it fetches synchronously before
serving any tool call. If the loaded snapshot is older than
`POGO_MCP_REFRESH_INTERVAL_HOURS` (default 6), a refresh is triggered
automatically; otherwise use the `refresh_data` tool to force one.
A refresh always fetches, normalizes, and validates a *new* snapshot into a
staging directory before touching anything - `.cache/current/` is only
replaced (via an atomic directory rename, moving the old `current/` to
`previous/`) once the new snapshot is fully written and passes validation. If
the upstream API is unreachable or the new data fails validation, the
previously-served snapshot keeps being used and the failure is surfaced
through `get_data_status` as `last_refresh_error` - tool calls never silently
fall back to an empty dataset.
## Installation
Requires Python 3.12+ and [`uv`](https://docs.astral.sh/uv/).
```bash
uv sync
cp .env.example .env # optional; defaults work out of the box
```
Run the server directly (stdio transport):
```bash
uv run pogo-mcp
```
### MCP client setup
For Claude Code:
```bash
claude mcp add pogo-mcp -- uv run --directory /path/to/pogo-mcp pogo-mcp
```
Or add manually to your MCP client config (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"pogo-mcp": {
"command": "uv",
"args": ["run", "--directory", "/path/to/pogo-mcp", "pogo-mcp"]
}
}
}
```
## Tools
| Tool | Description |
|---|---|
| `get_pokemon` | Look up one Pokémon battle form by name or id. Returns a structured `ambiguous_pokemon` error with every candidate if the name matches more than one form. |
| `search_pokemon` | Substring search across Pokémon display names, for browsing. |
| `get_move` | Look up one move by name or id. Returns PvE and PvP fields separately. |
| `search_moves` | Substring search across move display names. |
| `get_moveset` | Every legal fast/charged/elite move for one Pokémon, resolved to full move records. |
| `get_data_status` | Snapshot freshness, validation status, record counts, and whether cached data is currently being served after a failed refresh. |
| `refresh_data` | Force an immediate fetch/normalize/validate/promote cycle. |
| `calculate_cp` | CP and actual Attack/Defense/HP for a Pokémon at an exact level + IV spread, with optional `shadow`. |
| `calculate_powerup_cost` | Total Stardust/Candy/Candy XL to power up between two levels, with optional `shadow`/`purified`/`lucky`. |
| `calculate_move_damage` | Damage for one hit of a move (attacker vs. defender), with the full STAB/effectiveness/weather/Mega/friendship/dodge multiplier breakdown. Not a battle simulator - one deterministic hit. |
All tools return structured JSON - errors use a consistent
`{"error": "...", ...}` shape (`pokemon_not_found`, `ambiguous_pokemon`,
`move_not_found`, `ambiguous_move`, `invalid_input`) rather than guessing or
returning empty results.
## Validation strategy
Every snapshot is validated before promotion (`src/pogo_mcp/data/validator.py`):
non-empty datasets, positive base stats, every referenced move id actually
exists, all types are recognized against the fetched type chart, no duplicate
ids, sane move power/duration ranges, and sentinel lookups for well-known
Pokémon (Pikachu, Mewtwo, Machamp, Groudon, Kyogre, Rayquaza, Zamazenta) and
moves (Counter, Psycho Cut, Psystrike, Hydro Cannon, Precipice Blades) by
name - not by pinning exact stat values, since balance changes are legitimate
and shouldn't fail validation.
## Mechanics constants
CP multipliers, power-up costs, Shadow attack/defense modifiers, Mega/Primal
raid-boost multipliers, and PvE damage formula constants (STAB, weather
boost, friendship bonus, dodge reduction) aren't in pokemon-go-api. They're
sourced directly from the game's own datamined Game Master file
(`PokeMiners/game_masters`), with the exact commit, template, and field cited
in the module that defines each one (`src/pogo_mcp/mechanics/levels.py`,
`powerups.py`, `shadows.py`, `megas.py`, `types.py`, `damage.py`). See
`docs/mechanics-constants.md` for the consolidated index, including the two
places confidence is lower than the rest (half-level CPM extrapolation past
level 38.5, and Candy XL cost step granularity) and why.
## Development
```bash
uv sync --all-groups
uv run pytest # unit tests only (no network)
uv run pytest -m integration # + live pokemon-go-api tests
uv run ruff check .
uv run ruff format .
uv run pyright
```
Tests under `tests/fixtures/` are real recorded pokemon-go-api payloads
(trimmed to a handful of species chosen to exercise every tricky case: Mega
and Primal forms, gendered species with colliding names, and multi-form
legendaries) so normalization tests don't depend on network access or on
upstream data staying byte-for-byte identical forever.
## Scripts
```bash
uv run python scripts/refresh_data.py # force a fetch/normalize/validate/promote cycle
uv run python scripts/validate_snapshot.py # re-validate the current local snapshot offline
```
TDQS
Scored across 10 tools
Every tool has a clear, unique purpose: search vs exact lookup for both Pokemon and moves, separate calculation tools for CP, power-up cost, and damage, plus distinct data-management tools for status and refresh. There is no apparent overlap or ambiguity between any pair of tools.
All tool names follow a consistent verb_noun pattern using snake_case: get_pokemon, search_pokemon, get_move, search_moves, get_moveset, get_data_status, refresh_data, calculate_cp, calculate_powerup_cost, calculate_move_damage. Grouping by verb (get/search/calculate/refresh) creates a predictable and readable structure.
10 tools is well-scoped for a Pokemon GO data and calculation server. The set covers lookup, discovery, data management, and simulation without being bloated or thin, and each tool earns its place.
The domain is well covered with Pokemon/move lookup and search, moveset resolution, CP and damage calculations, and data-refresh management. Minor gaps exist, such as no PvP moveset endpoint and no standalone type-effectiveness lookup, but these are non-critical and the core workflows are fully supported.