blizzard-wow-mcp
An MCP server that exposes World of Warcraft character data from the Blizzard API, offering read-only tools for character summaries, equipment, Mythic+ scores, realm lookup, and other character sub-resources.
README
blizzard-wow-mcp
An MCP server exposing World of Warcraft character data from the Blizzard API.
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
- Blizzard API credentials — create a client at develop.battle.net/access/clients
Setup
uv sync
Then set your credentials:
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
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
{
"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:
BLIZZARD_CLIENT_ID=... BLIZZARD_CLIENT_SECRET=... uv run server.py --http
Check it is up and negotiating:
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:
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:
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
Acceptheader must list bothapplication/jsonandtext/event-stream. That is a streamable-HTTP requirement, not optional. - If your shell has
HTTPS_PROXYorALL_PROXYset, add--noproxy '*'or curl will try to proxy localhost and hang. - A tool call that comes back with
"isError": trueand a 403 usually means bad or unset credentials, or that outbound access tooauth.battle.netis 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 (line length 100, targeting py314):
uvx ruff check .
uvx ruff format .
Tests
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 readableBlizzardErrormessages.tests/test_tools.py— all seven tools, including field mapping, missing optional fields, equipment stat/socket/set parsing, item detail frompreview_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
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。