mcp server
acore-data
MCP server and agent tooling for AzerothCore WotLK: unified queries over DBC, live SQL, terrain and playerbots data
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 25, 2026acore-data
MCP server providing unified query access to 475 AzerothCore game datastores — DBC binary files, SQL tables, SQL overlays, and auxiliary stores. Plus terrain/pathfinding queries against MMap navmesh data, creature spawn analysis, database state audit, encounter rollups, record digests, mod configuration lookup, and C++ enum decoding. Exposes thirteen tools (query, lookup, list, spawns, dbversion, encounter, travel, explain, config, enums, sql, terrain, refs) over the JSON-RPC based Model Context Protocol.
Overview
AzerothCore stores game data in four categories of datastores. This server gives any MCP-compatible client (LLM, IDE plugin, CLI) a single entry point to query all of them by name, with automatic name resolution, type-aware field annotation, and smart SQL routing.
| Category | Count | Description |
|---|---|---|
dbc_backed | ~112 | Binary .dbc files loaded into packed C structs, with optional SQL overlay (*_dbc tables) |
sql_objectmgr | ~95 | ObjectMgr SQL tables — creature templates, gameobjects, items, quests, gossip, etc. |
sql_manager | ~48 | Tables loaded by other singleton managers (SpellMgr, PoolMgr, GameEventMgr, LootStore, …) |
sql_auxiliary | ~171 | Discovered SQL tables not in the static registry |
For the full technical reference on how each category is loaded in AzerothCore, see docs/datastores/README.md.
Tools
query
Query any datastore by name. Accepts a DBC file name ("Spell"), SQL table name ("quest_template"), or C++ struct name ("SpellEntry").
| Parameter | Type | Description |
|---|---|---|
name | string | Required. Datastore name. Use lookup to find valid names. |
id | number | Primary key for O(1) lookup. If a filter is also given, both must match (AND). |
filter | object | Named field filters. Supports $like / $ilike patterns. |
fields | array | Select fields by index [38, 39] or name ["BaseLevel", "SpellLevel"]. Strict: unknown names are errors with suggestions. |
limit | number | Max records to return (default 100). |
compact | boolean | Strip null/zero fields (default true). |
annotate | boolean | DBC rows as legacy per-field arrays with index/type/sql_column/source (default false = flat {name: value} rows). On SQL tables, attaches column types. |
hints | boolean | Include field_references / referenced_by cross-reference metadata (default false). |
links | boolean | Add metadata.links: one-hop relation map (related NPC/item/spell names) for the returned rows (default false; capped at 25 rows × 10 fields). |
resolve | boolean or array | Resolve type-specific data fields. true = all, or ["dbc", "sql", "loot"]. |
resolve_max | number | Max items per loot table resolution (default 10). Use 0 for unlimited. |
Result shape: rows are flat {field: value} objects. id/row_index lookups return a single object (or a specific error if the id is absent / does not match the filter); filter and unconstrained queries return a list. Locale arrays (e.g. name[0..15]) collapse to a scalar (or a list of non-empty values).
Examples:
query(name="Spell", id=118)
→ O(1) lookup for Polymorph, ~1 KB flat object
query(name="quest_template", filter={"Title": {"$ilike": "%murloc%"}})
→ SQL $ilike search across all quest titles
query(name="gameobject_template", id=180013, resolve=true)
→ Returns gameobject data with data[0-19] annotated (lockId, lootId, spellId, etc.)
query(name="quest_template", id=46, links=true)
→ metadata.links: quest NPC (Guard Thomas), required item (Torn Murloc Fin), reward items
query(name="Spell", id=118, fields=[38, 39])
→ Only BaseLevel and SpellLevel fields
For DBC-backed stores that also have an SQL overlay table, query merges both sources — SQL overlay data replaces or supplements the binary DBC data, and errors report which source failed.
lookup
Get schema and metadata for any datastore. Resolves by C++ struct name, SQL table, DBC file, or store variable (e.g. sSpellStore).
| Parameter | Type | Description |
|---|---|---|
query | string | Required. Name to resolve. |
detail | string | "schema" (default) — full field list. "summary" — 10 sample fields. |
Returns the per-field mapping (index, name, type, sql_column, plus notes/references where available), referenced_by cross-references, and access hints (e.g. sSpellStore.LookupEntry(id) -> SpellEntry const*). SQL-native tables additionally list live columns as name:type [PK].
Examples:
lookup(query="SpellEntry")
→ Full schema with all 183 fields, SQL columns, and cross-references
lookup(query="creature_template", detail="summary")
→ Compact 10-field sample + live SQL columns from the database
lookup(query="sSpellStore")
→ Resolves store variable to SpellEntry with access pattern hints
list
List available datastores with optional search and category filtering.
| Parameter | Type | Description |
|---|---|---|
search | string | Filter by struct name, table, DBC file, or store variable. |
category | string | "all" (default), "dbc_backed", "sql_objectmgr", "sql_manager", "sql_auxiliary". |
limit | number | Max entries to return (default 50). When truncated, metadata.total reports the full count. |
Examples:
list(category="dbc_backed")
→ First 50 DBC-backed stores with field counts (total reported in metadata)
list(search="Quest")
→ Stores matching "Quest" in struct name, table, or DBC file
spawns
Creature spawn analysis for a creature_template entry — total world spawns, per-map breakdown (with map names resolved from DBC), and sample positions. Read-only; uses the creature, creature_template and map/Map.dbc tables. No mod required — works on any azerothcore-wotlk install.
| Parameter | Type | Description |
|---|---|---|
entry | number | Required. creature_template entry (the creature id). |
map | number | Restrict the analysis to a single map ID. |
limit | number | Sample positions to return (default 3, max 20). |
Examples:
spawns(entry=32820)
→ Wild Turkey: 3125 spawns, map 0 (Eastern Kingdoms), sample positions
spawns(entry=1)
→ template exists but has_world_spawn=false (Waypoint, GM-only)
dbversion
Database state audit for this install — no arguments. Reports the core/DB version row (acore_world.version), per-database SQL update state (RELEASED/ARCHIVED/CUSTOM/MODULE/PENDING counts plus applied/pending totals), pending update names, and which of the four AzerothCore databases are present. acore_playerbots only exists with mod-playerbots; its absence is reported as installed: false, not an error.
Examples:
dbversion()
→ core_version (with fork/branch marker), db_version, per-DB tables/applied/pending,
mod_playerbots_installed=true|false
enums
C++ enum decoder — indexes every named enum in the AzerothCore source tree (~1,044 enums, ~17.7k members, one cached scan) and resolves magic numbers. Answers "what does this value mean?" (enums(enum='Mechanics', value=17) → MECHANIC_POLYMORPH) without the agent grepping the source. The source tree is optional (override with ACORE_SRC_ROOT); absence is a clean error.
| Parameter | Type | Description |
|---|---|---|
enum | string | Exact enum name. |
value | number | Value to decode (with enum=) or scan across all enums. |
member | string | Member-name substring for reverse lookup (with enum=). |
search | string | Case-insensitive substring match on enum names. |
Examples:
enums()
→ 1044 enums, 17713 members, largest enums by size
enums(enum="Mechanics", value=17)
→ MECHANIC_POLYMORPH (src/server/shared/SharedDefines.h)
enums(enum="SpellEffects") full value table (capped)
enums(value=17) which enums contain 17 (ambiguity note)
config
mod-playerbots configuration index — indexes playerbots.conf.dist (setting → default + line) and the C++ GetOption call sites that read each key in one cached pass. Answers "what does this setting do / what's its default / where is it used" without reading the 900-line conf or grepping the mod. The mod source tree is optional (override its location with PLAYERBOTS_ROOT); absence is a clean error.
| Parameter | Type | Description |
|---|---|---|
key | string | Exact setting key → full detail incl. code refs. |
search | string | Case-insensitive substring match on key names (capped 50). |
rebuild | boolean | Force re-scanning the mod conf + source. |
Examples:
config()
→ 888 settings, 384 code refs, counts by prefix
config(search="strategy")
→ EnableNewRpgStrategy, Max/MinRandomBotChangeStrategyTime, ...
config(key="AiPlayerbot.RandomBotCombatStrategies")
→ default "", conf line 1370, code ref: PlayerbotAIConfig.cpp:459
explain
Agent-friendly digest of a single record from any datastore — wraps the query pipeline (annotate + links) and distills it to: a one-line summary, the non-trivial fields (capped at 20, name pinned first), one-hop cross-references with resolved target names, and source provenance. For DBC-backed stores it reports which fields the live SQL overlay changed vs the vanilla DBC (overlay_overrides), and flags overlay-only records (ids that exist only in the *_dbc overlay tables, as in this reduced build).
| Parameter | Type | Description |
|---|---|---|
name | string | Required. Datastore name. |
id | number | Required. The record's primary key (0 allowed). |
Examples:
explain(name="Spell", id=118)
→ Polymorph (dbc) — 46 fields with values, 20 shown — 4 cross-reference(s)
explain(name="Spell", id=19)
→ SWORDSPECIAL (DND) — overlay-only record (no vanilla DBC row)
explain(name="quest_template", id=46)
→ Bounty on Murlocs — starters/enders: Guard Thomas, reward items resolved
encounter
Map/instance encounter rollup — what is in this place. Top creatures by spawn count with level ranges, rank (0=normal 1=rare 2=elite 3=worldboss) and loot item names for the top 5; top game objects (ores, doors, chests); instance metadata (script, allow_mount) when the map is an instance; and the mod-playerbots travel graph size when installed. Read-only, acore_world based — works on any azerothcore-wotlk install.
| Parameter | Type | Description |
|---|---|---|
map | number | Required. Map id. |
limit | number | Max creatures to list (default 20, max 100). |
Examples:
encounter(map=43)
→ Wailing Caverns (instance_wailing_caverns): Druid of the Fang (19, rare),
Deviate Lasher (19), ... with loot; travel graph: 10 nodes / 1968 points
travel
Inspect the mod-playerbots travel graph and verify paths against the MMap navmesh — no running server needed. Three modes (all read-only; requires acore_playerbots, its absence is a clean error):
| Parameter | Type | Description |
|---|---|---|
map | number | Required. Map id. |
node | number | Node mode: node details + named neighbours (capped 50 each). |
from / to | number | Path mode: decode the stored path between two nodes (requires both). |
verify | boolean | Path mode: check each point against MMap ground (default true; degrades to a note where the install has no .map data for the map). |
Examples:
travel(map=0)
→ 644 nodes, 3010 edges, 295623 path points, sample named nodes
travel(map=0, node=0)
→ "Human start" + neighbours (Goldshire innkeeper, Northshire Valley spirithealer, ...)
travel(map=0, from=0, to=2776)
→ "Human start" → "Elwynn Forest Goldshire": 103 path points + navmesh_verification
sql
Execute raw SQL queries with automatic database routing and typo suggestions.
| Parameter | Type | Description |
|---|---|---|
query | string | Required. SQL query (SELECT, INSERT, UPDATE, DELETE only). |
Features:
- Smart routing — automatically directs queries to
acore_world,acore_characters, oracore_authbased on table name - Typo suggestions — suggests correct table and column names on errors
- Safety — blocks DROP, TRUNCATE, ALTER, GRANT, REVOKE
- Context hints — e.g. empty loot_template results suggest trying questitem tables
Examples:
sql(query="SELECT entry, name FROM creature_template WHERE entry = 1")
→ Routes to acore_world
sql(query="SELECT id, username FROM account LIMIT 5")
→ Auto-routes to acore_auth
sql(query="SELECT * FROM creature_templat LIMIT 1")
→ Error with suggestion: "Did you mean: creature_template?"
terrain
Query map, VMap, and MMap terrain data the way the running server does —
ADT heights/liquids, WMO/M2 vmap raycasts (floors, line of sight, hits),
area/zone resolution, and Detour pathfinding equivalent to the .mmap path
GM command (uncapped by default).
| Parameter | Type | Description |
|---|---|---|
subcommand | string | Required. One of: list_maps, list_tiles, height, position, liquid, area, coord, tile_info, vmap_info, tile_stats, map_info, pathfind, los, raycast. |
mapId | string or number | Map ID (numeric) or name (e.g. "Eastern Kingdoms", "571"). |
x, y, z | number | World coordinates (required by subcommand). |
orientation | number | Optional orientation echoed by position. |
tileX, tileY | number | Tile coordinates 0-63 (for tile-level queries). |
data_type | string | "maps", "vmaps", or "mmaps" (for list_tiles, tile_info). |
x1, y1, z1, x2, y2, z2 | number | Start/end coordinates (for pathfind, los, raycast). |
mode | string | pathfind building mode: smooth (default, .mmap path), straight (.mmap path true), raycast (.mmap path ray). |
unit | string | Movement profile: player (default), creature, flying (2-point shortcut). |
flying | boolean | Legacy alias for unit="flying". |
normalize | boolean | Clamp path Z to the server floor/water level (default true). |
headless | boolean | Use the stricter playerbots filter (no steep slopes, avoid water). |
collision_height | number | Unit collision height for Z offsets (default 2.03128). |
max_nodes / max_polys / max_points | number | Optional caps (default unlimited; the server caps at 1024 nodes and 74/148 polys). |
limit | number | Max path points returned (default 2000; points_truncated is set when capped). |
ignore_m2 | boolean | los only: ignore M2 doodad collision. |
modify_dist | number | raycast only: offset applied to the hit point. |
include_mmap | boolean | position only: include nearest navmesh poly (default true). |
Subcommands:
| Subcommand | Purpose |
|---|---|
list_maps | List all maps with file counts (ADT, VMap, MMap) |
list_tiles | List tiles for a map (maps, vmaps, or mmaps) |
height | ADT terrain height plus the vmap-aware floor height (pass z) |
position | .gps-equivalent: terrain Z, floor Z, area/zone, liquid, indoors/outdoors, nearest navmesh poly |
liquid | Liquid level/status at (x, y, z) (WMO liquid first, then ADT) |
area | Area and zone IDs/names at (x, y[, z]) |
coord | Convert world coordinates to grid/tile |
tile_info | MMap/VMap tile header info |
vmap_info | VMap spawn/model metadata for a tile (or the tree) |
tile_stats | Navmesh statistics for a tile (poly count, vertex count) |
map_info | MMap navmesh parameters for a map |
pathfind | Server-style path between two points (smooth/straight/raycast, uncapped) |
los | Static line of sight between two points (vmap geometry) |
raycast | First vmap hit position along a segment (GetObjectHitPos) |
Pathfinding:
pathfind is a faithful port of AzerothCore's PathGenerator: start/end
polygon resolution with the same search extents, Detour A* with AzerothCore's
slope-aware traversal cost, FindSmoothPath (4-yard steps along the navmesh
surface, the .mmap path default), straight-path string pulling, raycast
mode, and UpdateAllowedPositionZ normalization (floor/water clamping) using
the same vmap collision data as the server. Unlike the worldserver there is
no node/path cap unless you pass one, so long routes (>900 yards) complete
in a single call.
terrain(subcommand="position", mapId=0, x=-8832, y=628, z=94)
→ {"terrain_z": 59.46, "floor_z": 94.01, "area": {"id": 1519, "name": "Stormwind City"}, "outdoors": true, …}
terrain(subcommand="pathfind", mapId=0, mode="smooth",
x1=-9464, y1=64, z1=55, x2=-8832, y2=628, z2=100)
→ {"found": true, "path_type_names": ["NORMAL"], "distance": 880.8, "point_count": 220, …}
terrain(subcommand="list_tiles", mapId=571, data_type="mmaps")
→ 433 MMap tiles for Wintergrasp
terrain(subcommand="tile_stats", mapId=571, tileX=23, tileY=24)
→ {"poly_count": 3639, "vert_count": 5832, "tile": [23, 24]}
terrain(subcommand="pathfind", mapId=571, x1=4683, y1=3824, z1=355, x2=4538, y2=3230, z2=403)
→ {"found": true, "distance": 649.0, "raw_path_length": 38, "smooth_path_length": 3,
"smooth_path": [{"x": 4683, "y": 3824, "z": 355}, {"x": 4528, "y": 3244, "z": 357}, {"x": 4538, "y": 3230, "z": 403}]}
Cross-tile pathfinding works automatically: tiles are loaded on-demand as the A* search expands, and external edges (DT_EXT_LINK) are resolved by finding matching polygons in neighbor tiles via BV-tree search. Long-distance paths (2000+ yards) across many tiles are supported.
Type Resolver
The resolve parameter on query enables type-aware field resolution for tables whose fields change meaning based on a type column. Resolution types:
"dbc"— resolve to DBC entries (e.g.LockEntry,SpellEntry,MapEntry)"sql"— resolve to SQL tables (e.g.quest_template,gossip_menu,page_text)"loot"— expand loot templates into item lists with names
Registry-driven resolution
Any table with cross-reference metadata in datastore_registry.json gets automatic field resolution via _resolve_generic(). Fields like faction, lootId, spellId are resolved to their target entries by name.
Specialized table resolvers
In addition to generic registry-driven resolution, these tables have dedicated resolver modules that enrich results with custom data:
| Table | Resolver module | What it resolves |
|---|---|---|
gameobject_template | resolvers/gameobject.py | type-aware data[0-19] annotation (lockId, lootId, spellId …) |
smart_scripts | resolvers/smart_scripts.py | EVENT_ID/ACTION_ID/TARGET_ID → enum names + value meaning |
quest_template | resolvers/quest.py | starter/ender NPCs, POIs, quest chain (prev/next/breadcrumb) |
conditions | resolvers/condition.py | polymorphic SourceType → entity name (incl. loot templates: SourceGroup→creature/GO/item template, SourceEntry→item), ConditionType enum + value decoding (AURA, QUEST, ITEM, TEAM, REPUTATION_RANK, SKILL, ZONEID, ACHIEVEMENT, WORLD_STATE, ACTIVE_EVENT, INSTANCE_INFO, UNIT_STATE, RELATION_TO, STAND_STATE, etc.), TYPEID/GENDER/RACE enums, condition_target (caster/target), stable composite row key |
achievement_criteria_data | resolvers/achievement_criteria.py | CriterionType-specific field interpretation |
item_template | resolvers/item.py | loot template for openable items (Flags & 0x04) |
Spell (DBC) | resolvers/spell.py | cast conditions from conditions table with full enum resolution (incl. condition_target caster-vs-target) |
query(name="quest_template", id=4512, resolve=true)
→ Start/ender NPCs, chain info, POIs, plus faction/spell/item refs
query(name="Spell", id=15698, resolve=true)
→ Cast conditions: "OBJECT_ENTRY_GUID(UNIT)=creature_template [Cursed Ooze], NOT_ALIVE"
query(name="item_template", id=11912, resolve=["loot"])
→ Openable item → 6x Empty Cursed Jar, 6x Empty Tainted Jar
query(name="conditions", filter={"SourceTypeOrReferenceId": 17, "SourceEntry": 15698}, resolve=true)
→ Polymorphic: source type name, condition type enum, resolved entity names
query(name="gameobject_template", id=12345, resolve=["loot"], resolve_max=20)
→ Expand loot template into up to 20 item names
Setup
Requirements
- Python 3.10+
pymysql— installed viarequirements.txtinto.venv- Access to an AzerothCore MySQL instance (for SQL tools)
- DBC binary files and
DBCfmt.hfrom the AzerothCore source/build - MMap data files (
.mmtile,.mm) for terrain/pathfinding queries (optional — terrain tool degrades cleanly without them)
Environment Variables
| Variable | Default | Purpose |
|---|---|---|
ACORE_DBC_PATH (alias DBC_PATH) | /root/azerothcore-wotlk/env/dist/bin/dbc | DBC binary directory |
ACORE_FORMAT_FILE (alias DBC_FORMAT_FILE) | /root/azerothcore-wotlk/src/server/shared/DataStores/DBCfmt.h | DBC format strings |
ACORE_DATA_PATH (alias DATA_PATH) | /root/azerothcore-wotlk/env/dist/bin | Terrain data base dir (uses maps/, vmaps/, mmaps/ subdirectories) |
DB_HOST / DB_USER / DB_PASSWORD | auto-detected | Base DB connection (see below) |
DB_PORT | 3306 | Base DB port |
DB_NAME | acore_world | Base database name |
DB_AUTH_{HOST,PORT,USER,PASSWORD,NAME} | — | Per-database override for acore_auth |
DB_CHAR_{HOST,PORT,USER,PASSWORD,NAME} | — | Per-database override for acore_characters |
ACORE_SRC_ROOT | /root/azerothcore-wotlk | AzerothCore source tree (enums tool; optional) |
PLAYERBOTS_ROOT | /root/azerothcore-wotlk/modules/mod-playerbots | mod-playerbots tree (config tool; optional) |
ACORE_SQL_TOOL_MODE | full | full = any statement (destructive blocked), readonly = SELECT only |
ACORE_DATA_ROOT | — | Project root for the pi bridge (see below) |
ACORE_WORLDSERVER_CONF | — | Path to your worldserver.conf when it lives outside the default locations (checked first) |
Database configuration. When DB_HOST and DB_USER are unset, the server auto-detects from worldserver.conf (checked at ACORE_WORLDSERVER_CONF if set, then /root/azerothcore-wotlk/env/dist/etc/worldserver.conf, then ~/azerothcore-wotlk/env/dist/etc/worldserver.conf). The *DatabaseInfo lines may point Login (acore_auth) and Character (acore_characters) at a different MySQL host — detected differences are applied as per-database connection overrides, so a shared auth DB across realm machines works out of the box. acore_playerbots is not configured by worldserver and rides the base connection. Explicit DB_* env vars disable auto-detection entirely (single connection for all DBs). Precedence, merged per key: DB_AUTH_* / DB_CHAR_* env > worldserver.conf auto-detection > base config. If neither env vars nor a detectable worldserver.conf exist, the server warns on stderr and falls back to root@localhost.
Missing DBC path or format file produces a clear error at startup (and the server continues in SQL-only mode); missing terrain data warns and the terrain tool degrades cleanly per query.
Running
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python3 server.py
Recommended: launch with the .venv Python (.venv/bin/python3). Without pymysql the server falls back to a mysql CLI subprocess — it works (params are interpolated and MYSQL_PWD is honored) but is slower.
If run via an MCP client, point the command at the venv's Python:
"command": ["/path/to/acore-data/.venv/bin/python3", "server.py"]
To test manually:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | .venv/bin/python3 server.py
pi (coding agent)
This project ships a first-class pi bridge at .pi/extensions/acore-data.ts. pi loads it automatically and registers all of the server's tools (fetched live via tools/list, so every current and future tool is available) as native pi tools. It hardcodes no paths or credentials — it inherits the ambient environment and the server self-configures (DBC path defaults + DB creds auto-detected from worldserver.conf). The project root is auto-located (in order): ACORE_DATA_ROOT env, pi's cwd, the extension's own symlink-resolved location (so a global ~/.pi/agent/extensions/acore-data.ts symlink works from any cwd), then ~/acore-data. To use a non-default project root or a non-default AzerothCore layout, export the relevant vars (ACORE_DATA_ROOT, DB_*, DBC_PATH, …) in pi's environment.
Windows
Fully supported (the server, tests, and pi bridge all run under PowerShell / Git Bash):
python -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt
.venv\Scripts\python.exe server.py
- The venv interpreter is
.venv\Scripts\python.exe(POSIX:.venv/bin/python3,setup.shis the POSIX twin). The pi bridge picks the right one automatically. - The
ACORE_*/DB_*defaults point at/root/azerothcore-wotlk, so on Windows set them explicitly (e.g. a machine-localenv.local.ps1, kept gitignored) — e.g.ACORE_DBC_PATH=C:\GIT\azerothcore-wotlk\env\dist\bin\dbc. worldserver.confauto-detection only helps if a conf exists locally; dev checkouts usually don't have one, so explicitDB_HOST/DB_USER/DB_PASSWORDpointing at the realm's MySQL (LAN or localhost) is the normal setup.- Client data layout is the same as Linux:
env\dist\bin\{dbc,maps,vmaps,mmaps}. Populate via./acore.sh client-data(Git Bash, from the AzerothCore checkout) or copy the directory from the realm machine. - Test note: the DB-free suites run as-is; one
test_helperstest (chmod-000 inaccessible parent) skips on Windows because POSIX mode bits are no-ops there.
Design boundaries
This is a local agent tool for a single AzerothCore WotLK install: one process, stdio JSON-RPC, reading local DBC/terrain files and the local (or realm-shared) MySQL. It is deliberately not a service, and the following are non-goals rather than gaps:
- No Docker/systemd packaging, SLA, or backup strategy — it runs next to the realm it describes.
dbversionis the identity/health check by design. - No metrics, rate limiting, or HTTP health endpoint — the consumer is an LLM agent on a private stdio channel.
- No structured error codes or JSON logging — actionable string errors with suggestions (did-you-mean, schema hints) are the right interface for an LLM consumer.
- No eviction policy on the DBC cache — the store set is bounded (~114 DBCs, one reader per store); it is not a cache of unbounded working set.
- No input validation beyond statement blocking on
sql— it is a deliberate admin escape hatch; setACORE_SQL_TOOL_MODE=readonlyto restrict toSELECT. - Single-install scope — no multi-realm aggregation; point it at the install you care about.
Testing
# Regression suite — 21 tests, the output-shape/strictness/protocol contract
.venv/bin/python3 tests/test_regression.py
# Integration tests — 75 tests (requires running MySQL + DBC files)
.venv/bin/python3 tests/test_integration.py
# Fast unit tests — 40 tests (no DB)
.venv/bin/python3 tests/test_helpers.py
# Everything at once
.venv/bin/python3 -m pytest tests/ -q
Output-size baselines (for LLM context budgeting):
.venv/bin/python3 scripts/capture_output_sizes.py /tmp/after
.venv/bin/python3 scripts/capture_output_sizes.py --compare /tmp/before /tmp/after
Project Structure
acore-data/
├── server.py # MCP server entry point (JSON-RPC over stdio)
├── datastore_registry.json # Static metadata for all 475 datastores
├── requirements.txt # pymysql / pytest (pinned)
│
├── core/
│ ├── annotation.py # DBC field annotation, filter conversion, schema errors
│ ├── database.py # MySQL connection (pymysql), table discovery, smart routing,
│ │ # per-DB credentials (shared-DB topologies)
│ ├── dbc.py # WDBC binary file reader
│ ├── enums.py # Shared enum dicts: condition types, SOURCE_TYPE, TYPEID
│ ├── enum_index.py # C++ source-tree enum scanner (~1,044 enums, one cached pass)
│ ├── formats.py # DBCfmt.h parser (format strings → field types)
│ ├── registry.py # Datastore registry: name resolution, fuzzy matching
│ ├── type_resolver.py # Dispatcher + generic registry-driven resolution engine
│ ├── resolvers/ # Specialized table-specific resolver modules
│ │ ├── __init__.py # Resolver registry (table_name → func)
│ │ ├── gameobject.py # data[0-19] annotation for GAMEOBJECT_TYPE subtypes
│ │ ├── smart_scripts.py # EVENT_ID/ACTION_ID/TARGET_ID enum + value meaning
│ │ ├── quest.py # Starter/ender NPCs, POIs, chain info (prev/next/breadcrumb)
│ │ ├── condition.py # Polymorphic: SourceType → entity, ConditionType, enums
│ │ ├── achievement_criteria.py # CriterionType-specific field interpretation
│ │ ├── item.py # Loot template for openable items (Flags & 0x04)
│ │ ├── spell.py # Cast conditions from `conditions` table with enum resolution
│ │ └── ref_utils.py # Shared helpers: resolve_dbc_ref, resolve_sql_ref, …
│ └── terrain/ # Terrain data: map/vmap/mmap readers, navmesh pathfinding
│ ├── coords.py # World ↔ tile coordinate conversions
│ ├── map_reader.py # ADT map file reader (server-exact height/liquid/area indexing)
│ ├── vmap_reader.py # VMap tile/tree metadata reader
│ ├── mmap_reader.py # MMap tile header + dtNavMeshParams reader
│ ├── detour_parser.py # Detour tile parser (polygons, BV tree, off-mesh connections)
│ ├── map_resolver.py # Server map logic: GetHeight, full terrain status, areas, liquids
│ ├── path_generator.py # PathGenerator port: smooth/straight/raycast + normalization
│ ├── detour/ # Detour navmesh query port
│ │ ├── navmesh.py # Multi-tile store, link construction (internal/external/off-mesh)
│ │ ├── query.py # findPath/findStraightPath/moveAlongSurface/raycast ports
│ │ ├── filter.py # dtQueryFilter + AzerothCore slope-aware dtQueryFilterExt
│ │ └── mathutil.py # Detour geometry helpers
│ └── vmap/ # VMAP collision stack
│ ├── bih.py # Bounding Interval Hierarchy traversal
│ ├── world_model.py # .vmo WMO/M2 parser, group models, WMO liquid
│ ├── tree.py # StaticMapTree: spawns, tiles, raycasts, location info
│ └── math3d.py # G3D-compatible matrix/ray math
│
├── tools/
│ ├── __init__.py # Tool schema registry, SQL-mode gate (ACORE_SQL_TOOL_MODE)
│ ├── query.py # Unified query tool (DBC + SQL + overlay merge)
│ ├── lookup.py # Schema/metadata lookup tool
│ ├── list.py # Datastore listing tool
│ ├── sql.py # Raw SQL execution with routing and suggestions
│ ├── terrain.py # Terrain/pathfinding tool (map/vmap/mmap, A* navmesh)
│ ├── spawns.py # Creature spawn rollup (where does creature N spawn?)
│ ├── dbversion.py # Server state: core/DB version, per-DB update state
│ ├── encounter.py # Instance/map encounter rollup (creatures, gameobjects)
│ ├── travel.py # mod-playerbots travel graph + navmesh path verification
│ ├── explain.py # Agent-friendly single-record digest
│ ├── config.py # mod-playerbots conf index (settings + C++ call sites)
│ └── enums.py # C++ enum decoder (magic numbers → MECHANIC_POLYMORPH …)
│
├── core/api.py # Scripting facade (open_store/sql) - see docs/scripting.md
│
├── scripts/
│ ├── audit_registry.py # Registry health gate (CI: structural signals must be 0)
│ ├── capture_output_sizes.py # LLM context-budget output baselines + diff
│ ├── source_crossrefs.py # FK heuristic cross-reference discovery
│ └── archive/ # One-off registry migrations (idempotent no-ops)
│
├── generators/ # Registry generation tooling (docs/datastores is the
│ ├── generate_registry.py # human reference; --write regenerates the registry
│ ├── generate_supplementary.py
│ ├── patch_known_refs.py # Fix known-wrong annotations (Talent RankID, ItemSet spells)
│ └── … (cross-ref / annotation generators)
│
├── evals/
│ ├── run_eval.py # LLM tool-calling eval harness (optional, time-costly)
│ └── report.py
│
├── .pi/extensions/
│ └── acore-data.ts # pi bridge (spawns server.py over stdio JSON-RPC)
│
├── .github/workflows/
│ └── ci.yml # Fast suites + structural audit on push/PR
│
├── tests/
│ ├── test_helpers.py # Fast unit tests (no DB)
│ ├── test_regression.py # Rework contract: shape, strictness, links, protocol, audit
│ └── test_integration.py # Integration tests (live DB + DBC)
│
└── docs/
├── scripting.md # Bulk local scripting: core.api, venv, refs-vs-field traps
└── datastores/ # Technical reference for AzerothCore datastore internals
├── README.md # Overview of DBC pipeline, SQL overlay, format strings
├── dbc-backed-stores.md
├── sql-objectmgr-stores.md
├── sql-manager-stores.md
├── sql-auxiliary-stores.md
└── cross-reference.md
How It Works
MCP Client (LLM / IDE / CLI)
│
│ JSON-RPC over stdio
▼
server.py ───────────────────────────────────────
│
├─► registry.py datastore_registry.json
│ Name resolution, fuzzy matching (475 entries)
│
├─► tools/query.py ┌──► dbc.py .dbc binary files
│ Unified query │ WDBCReader (Spell.dbc, Map.dbc, …)
│ │
│ ├──► database.py MySQL
│ │ Smart routing (acore_world, _characters, _auth)
│ │
│ ├──► annotation.py Field annotation
│ │ DBC ↔ SQL merge + type-aware resolution
│ │
│ └──► type_resolver.py gameobject_template
│ data[] → lockId/lootId/spellId
│
├─► tools/lookup.py Schema + live SQL columns + cross-refs
│
├─► tools/list.py Category filtering + search
│
├─► tools/sql.py Raw SQL with routing + typo suggestions
│
└─► tools/terrain.py Terrain queries + pathfinding
(map/vmap/mmap, A* navmesh)
License
GPL-2.0 (GNU General Public License, version 2) — see LICENSE. This matches the license of the AzerothCore server it serves. No AzerothCore code is bundled — the server reads your own install's DBC files, terrain data and SQL schema at runtime.