Skip to main content
Glama
hjoliveira

blizzard-wow-mcp

by hjoliveira
README.md
# blizzard-wow-mcp

[![CI](https://github.com/hjoliveira/wow-armory-mcp2/actions/workflows/ci.yml/badge.svg)](https://github.com/hjoliveira/wow-armory-mcp2/actions/workflows/ci.yml)

An MCP server exposing World of Warcraft character data from the [Blizzard API](https://develop.battle.net/documentation/world-of-warcraft).

It targets the MCP 2026-07-28 specification via the Python SDK v2 (`mcp>=2`). Everything it does is read-only: it authenticates with the OAuth2 client-credentials flow, so it can reach any public character profile, but none of the `/profile/user/*` endpoints — those require the authorization-code flow with an end user in the loop.

## Requirements

- Python 3.14+
- [uv](https://docs.astral.sh/uv/)
- Blizzard API credentials — create a client at [develop.battle.net/access/clients](https://develop.battle.net/access/clients)

## Setup

```bash
uv sync
```

Then set your credentials:

```bash
export BLIZZARD_CLIENT_ID=...
export BLIZZARD_CLIENT_SECRET=...
```

| Variable | Required | Default | Notes |
| --- | --- | --- | --- |
| `BLIZZARD_CLIENT_ID` | yes | — | Server refuses to start without it |
| `BLIZZARD_CLIENT_SECRET` | yes | — | Server refuses to start without it |
| `BLIZZARD_REGION` | no | `eu` | One of `us`, `eu`, `apac`. Realms differ between regions |
| `BLIZZARD_LOCALE` | no | `en_GB` | Any locale the region supports, e.g. `en_US` |

## Running

```bash
uv run server.py           # stdio (default)
uv run server.py --http    # stateless streamable HTTP on :8000/mcp
```

The package also installs a `blizzard-wow-mcp` console script that does the same thing.

### Claude Desktop / MCP client config

```json
{
  "mcpServers": {
    "wow": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/wow-armory-mcp2", "server.py"],
      "env": {
        "BLIZZARD_CLIENT_ID": "...",
        "BLIZZARD_CLIENT_SECRET": "...",
        "BLIZZARD_REGION": "eu"
      }
    }
  }
}
```

## Verifying it works

In `--http` mode the server is stateless and returns plain JSON, so curl is enough to smoke-test it — no session ID and no `initialized` notification needed.

Start it in one shell:

```bash
BLIZZARD_CLIENT_ID=... BLIZZARD_CLIENT_SECRET=... uv run server.py --http
```

Check it is up and negotiating:

```bash
curl -s -X POST http://localhost:8000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
```

List the tools — the quickest single check that the server is wired up:

```bash
curl -s -X POST http://localhost:8000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
```

Call a tool, which exercises the credentials and the round-trip to Blizzard. `wow_find_realm` is the cheapest one — it needs no character name:

```bash
curl -s -X POST http://localhost:8000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"wow_find_realm","arguments":{"query":"twisting"}}}'
```

A few gotchas:

- The `Accept` header must list **both** `application/json` and `text/event-stream`. That is a streamable-HTTP requirement, not optional.
- If your shell has `HTTPS_PROXY` or `ALL_PROXY` set, add `--noproxy '*'` or curl will try to proxy localhost and hang.
- A tool call that comes back with `"isError": true` and a 403 usually means bad or unset credentials, or that outbound access to `oauth.battle.net` is blocked.

## Tools

| Tool | Purpose |
| --- | --- |
| `wow_character_summary` | Level, class, spec, race, faction, guild, item level, achievement points, last login, Armory URL |
| `wow_character_equipment` | Every equipped item with slot, name, item ID, item level, quality, enchantments, stats, sockets and tier set |
| `wow_character_mythic_keystone` | Mythic+ season rating and best runs, sorted by rating. Defaults to the character's most recent season |
| `wow_item_search` | Find items by name, filtered by quality and item level, to get the ID `wow_item` needs |
| `wow_item` | An item's own record by ID: stats, weapon damage, effects, binding, durability and vendor prices |
| `wow_character_resource` | Escape hatch for any other character sub-resource — raid progression, talents, professions, PvP, reputations, collections, and so on |
| `wow_find_realm` | Resolve a realm name to the slug the other tools need |

All tools are annotated read-only, idempotent and non-destructive.

Everything except `wow_character_resource` and `wow_find_realm` returns structured Pydantic models rather than raw Blizzard JSON. `wow_character_resource` returns the raw payload with Blizzard's self-referential `key`/`href` link objects stripped, which typically removes 30–50% of the tokens without losing anything a model would use. Its `resource` argument is constrained to a fixed list: `achievements`, `appearance`, `collections/mounts`, `collections/pets`, `collections/toys`, `encounters/dungeons`, `encounters/raids`, `hunter-pets`, `professions`, `pvp-summary`, `quests/completed`, `reputations`, `specializations`, `statistics`, `titles`.

### Item stats: base vs. worn

There are two sources of item stats, and they answer different questions.

`wow_item` reads `/data/wow/item/{id}`, whose `preview_item` block carries the item's **base** form — Blizzard renders it at the item's default bonus list. The endpoint takes no bonus-list or context parameter, so for gear that scales (most modern raid and Mythic+ drops, which ship at many item levels under one item ID) the stats it reports will not match any particular character's copy.

`wow_character_equipment` reports what a character is **actually wearing**, rendered with that instance's real bonus list — so its `stats`, `sockets` and `item_level` are authoritative for that character. Each equipped item also carries an `item_id`, which is what you feed to `wow_item`.

Neither can resolve arbitrary bonus IDs — an auction listing's `bonus_lists` cannot be turned into stats through any public endpoint.

### Searching for items

`wow_item_search` is the only way into the item data by name; Blizzard exposes no endpoint that enumerates items. Matching is token-based rather than substring, so `Thunderfury` matches and `fury` does not. Results are sorted by item level descending and carry classification only — feed an `id` to `wow_item` for the stat block.

The search API filters on a **locale-qualified** field name (`name.en_GB`, not `name`), which makes `BLIZZARD_LOCALE` load-bearing in a way it is not for any other endpoint. A locale the region does not serve yields either an empty result set or a 400; both are reported with a message naming the locale. If you change `BLIZZARD_REGION`, change `BLIZZARD_LOCALE` to match.

The filter encoding — locale-qualified fields, `[min,max]` ranges, `_page` / `_pageSize` — is taken from Blizzard's documentation rather than from a captured response, and lives entirely in `_item_search_params`. If a live call disagrees, that one function is the only thing to change.

## Notes on names and realms

Realm names are slugified automatically — `Kil'jaeden` becomes `kiljaeden`, `Área 52` becomes `area-52` — and character names are lowercased and percent-encoded, so you can pass them as a player would write them.

If a lookup 404s, the likely causes are: the character does not exist on that realm, the realm slug is wrong, the character is below level 10, or it has not logged in since the last expansion. Call `wow_find_realm` to confirm the slug before retrying, and check `BLIZZARD_REGION`.

Blizzard's quota is 100 requests/second and 36,000/hour per client. The server caches the access token for its lifetime (refreshing 60s before expiry) and retries once on a 401, so ordinary use stays well inside that.

## Development

Linting is configured for [ruff](https://docs.astral.sh/ruff/) (line length 100, targeting py314):

```bash
uvx ruff check .
uvx ruff format .
```

### Tests

```bash
uv sync          # installs the dev group
uv run pytest
```

The suite is offline — every Blizzard call is served by an `httpx.MockTransport`, so no
credentials and no network access are needed:

- `tests/test_helpers.py` — realm slugging, character normalisation, `slim()`, timestamp
  conversion, and the localised-name fallback search results need.
- `tests/test_client.py` — token caching and refresh, the single 401 retry, extra query
  parameters merging without clobbering the namespace, and the mapping from HTTP status
  codes to readable `BlizzardError` messages.
- `tests/test_tools.py` — all seven tools, including field mapping, missing optional fields,
  equipment stat/socket/set parsing, item detail from `preview_item`, item search filter
  encoding and locale handling, Mythic+ season defaulting and run ordering, and realm-search
  matching.

### CI

`.github/workflows/ci.yml` runs ruff and the test suite on Python 3.14 for every pull request,
plus weekly on Mondays at 06:00 UTC and on demand via workflow dispatch.

Pull request runs use `uv sync --locked`, so they install exactly what `uv.lock` pins and fail if
the lock has drifted from `pyproject.toml`. The weekly run uses `--upgrade` instead, resolving
dependencies fresh so it surfaces upstream releases that break the server — which a run pinned to
the lockfile would never catch.

## License

[MIT](LICENSE)

TDQS

A4.6/5.0

Scored across 5 tools

Disambiguation5/5

Each tool targets a distinct aspect of WoW character data: summary, equipment, Mythic+ details, generic sub-resources, and realm resolution. The generic resource tool is explicitly for sub-resources without dedicated tools, so no overlap exists.

Naming Consistency4/5

All tool names use lowercase snake_case and the 'wow_' prefix, with four of five sharing 'wow_character_*'. The exception is 'wow_find_realm', which uses a verb and different resource prefix, creating a minor but noticeable pattern deviation.

Tool Count5/5

Five tools is a well-scoped count for a character-information server. It covers core workflows without overwhelming users, and the generic resource tool prevents unnecessary tool proliferation.

Completeness5/5

The dedicated tools handle the most common character lookups, while the generic resource tool provides access to many additional sub-resources (raids, talents, professions, etc.). This effectively covers the domain of WoW character data without significant gaps.

Maintenance

ActivitySlowing
ResponsivenessNo issues