# Dwarf Fortress (DFHack) (npm · dfhack-mcp)

MCP server exposing a live Dwarf Fortress fort to an AI agent as curated, semantic tools

- Trust score: 77/100 (medium)
- Change this week: +53
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-08-03

## Components

- npm · `dfhack-mcp`: 77/100 (this document), [markdown](https://verifymcp.io/servers/alexanderolvera-dfhack-mcp/dfhack-mcp.md), [page](https://verifymcp.io/servers/alexanderolvera-dfhack-mcp/dfhack-mcp)

## Channel facts

- Registry: `npm`
- Package: `dfhack-mcp`
- Version: `1.4.1`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-08-03.

- **Supply Chain Security**: 87/100
  - No malware found by supply-chain analysis.
  - Only part of the dependency tree could be resolved (108 of 112), so this covers what we could see, not the whole tree.
  - No install/post-install scripts declared.
  - Only part of the dependency tree could be resolved (108 of 112), so this covers what we could see, not the whole tree.
- **Provenance & Transparency**: 97/100
  - Source repository is publicly reachable at the declared URL.
  - Cryptographically verified build provenance (signed, bound to alexanderolvera/dfhack-mcp).
  - Clear OSI-approved license (ISC).
  - Actively maintained (last published 8 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 63/100
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 10923 tokens (~295/item across 37 items; 37 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 27/100
  - Stability observed for 8 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 100/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 100% of tool parameters carry a description.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### Claude

```bash
claude mcp add alexanderolvera-dfhack-mcp -- npx -y dfhack-mcp
```

### Codex

```bash
codex mcp add alexanderolvera-dfhack-mcp -- npx -y dfhack-mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "alexanderolvera-dfhack-mcp": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "dfhack-mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add alexanderolvera-dfhack-mcp --command npx --arg -y --arg dfhack-mcp
```

### Hermes

```yaml
mcp_servers:
  alexanderolvera-dfhack-mcp:
    command: "npx"
    args: ["-y", "dfhack-mcp"]
```

### Other

```json
{
  "mcpServers": {
    "alexanderolvera-dfhack-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "dfhack-mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-08-02 (score 77, +72)

- [security improvement] Install scripts: unverified → pass
- [security improvement] Provenance: unverified → pass
- [security improvement] Known CVEs: unverified → partial
- [security improvement] Malware scan: unverified → pass
- [security] The attested source repository moved: alexanderolvera/dfhack-mcp
- [functional regression] Security disclosure: fail → unverified
- [functional improvement] Schema quality: unverified → excellent
- [functional improvement] Tool coverage: unverified → 100
- [functional improvement] License: unverified → pass
- [functional improvement] Dependency health: unverified → partial
- [functional improvement] Maintenance: unverified → pass
- [functional improvement] Stability: unverified → 0.23
- [functional improvement] MCP protocol: unverified → pass
- [functional] Licence: ISC

### 2026-08-01 (score 5, −12)

- [functional regression] Tool coverage: 100 → unverified
- [functional] First check of Schema quality: unverified

### 2026-07-31 (score 17, −7)

- [functional] We updated how we score, so this day's move reflects our rubric, not a change to the server

### 2026-07-27 (score 24)

First indexed and scored.

## MCP tools (37)

### `artifacts_and_engravings` (~469 tokens)

Artifacts and engravings

The fort's art, as labeled facts. Returns the named ARTIFACTS (paginated), SITE-SCOPED BY DEFAULT — only artifacts belonging to the loaded fort, which is almost always what is meant by "our artifacts". Pass scope="world" for every artifact in the world (an old world holds hundreds that have nothing to do with your fort; in world scope each row carries site_local so the two are still distinguishable). artifact_count always reflects the scope actually applied, and artifact_count_world always gives the unfiltered world total. Each artifact carries its name (dwarven + translated), item type and base material, created value, quality, maker (with a live unit_id ONLY when the maker is a living current citizen, else just the historical-figure name), the decorations on it (bands/covered/rings/images with their materials), and any engraved inscription text (e.g. a slab's secret). Plus an aggregated ENGRAVINGS summary for the map: engravings grouped BY SUBJECT with counts (never itemized per tile), a quality histogram, and the top engravers. IMPORTANT precondition on engraving subjects: DF does not populate the world's art-image table in fortress mode (art_images_loaded is false on every fort observed so far), so the human-readable scene an engraving depicts is NOT available — subjects_resolvable=false and each subject is keyed by its stable image reference ("image #2:158") instead. This is reported, never fabricated, but do not plan on describing what the fort's art depicts: the engraving read is reliable for counts, quality distribution, and which dwarves did the engraving, not for iconography. Use limit + next_cursor to page through artifacts; see `caps` for all documented limits. Returns {"error":"no fort loaded"} if no fort is active.

Input parameters:

- `cursor` (string): Opaque pagination cursor from a previous call's next_cursor; omit for the first page.
- `limit` (integer): Artifacts per page (default 25, max 100). Engravings are always fully aggregated.
- `scope` (string): Which artifacts to return. "site" (default) = only this fort's artifacts. "world" = every artifact in the world, each tagged with site_local.

### `burrows` (~210 tokens)

Burrows

The fort's burrows as facts: each burrow's id, name, exact tile_count (dfhack.burrows.isAssignedBlockTile summed over every assigned block — the precise painted area, not a bounding box), assigned_units (citizens/animals manually confined to it, id-sorted and capped at 200 — assigned_units_total is always the full count and assigned_units_truncated flags when capped), and civilian_alert_linked — whether this burrow is currently one of the safety burrows for the civilian alert (see civilian_alert). civilian_alert reports the alert's own state: configured (has the fort ever set up a civilian-alert slot — false on a fresh fort), active (is it sounding right now — civilians already fleeing to the linked burrow(s)), and burrows (the linked burrow ids). Pairs with the civilian_alert actuator, which toggles a named burrow in or out of this set. Returns {"error":"no fort loaded"} if no fort is active.

### `chronicle` (~368 tokens)

Chronicle

The fort's announcement/report stream (combat, deaths, moods, artifacts, sieges, migrants, ...) as triaged, cursor-addressable events. Reads the rolling, front-pruned report window. Each event carries a stable `id` (monotonic and save/load-stable); pass the returned top-level `cursor` back as `since` to fetch only newer events (id > since). Omitting `since` returns the most recent `limit` events (default 50, max 200), oldest-to-newest. If `since` predates the retained window the response sets pruned:true (earlier events are gone — not silently omitted). Events are triaged into categories (death, birth, marriage, battle, siege, mood, artifact, migrants, diplomacy, cave-in, megabeast, other); filter with `categories`. Combat spam is tamed: repeat_count is honored, wrapped continuation lines fold into their event, and long consecutive battle runs collapse into a single marker (collapsed:true with collapsed_count) so one siege cannot flood the window — see battle_collapsed. Facts only: unit refs appear only when a report names a speaker (speaker_id != -1); combat reports carry no reliable unit, so they get a pos tile anchor instead. Returns {"error":"no fort loaded"} if no fort is active.

Input parameters:

- `categories` (array): Optional subset of categories to return: death, birth, marriage, battle, siege, mood, artifact, migrants, diplomacy, cave-in, megabeast, other.
- `limit` (integer): Max events to return (default 50, capped at 200); newest are kept.
- `since` (integer): Cursor: return only events with id greater than this (from a prior `cursor`).

### `citizen` (~259 tokens)

Citizen

A deep dossier on ONE citizen, chained by unit_id from find_unit (or chronicle). Where find_unit stays compact, this is the depth: the walkable social graph (spouse, parents, children, friends, grudges — each with a unit_id you can pass back into citizen() to walk the graph), worshipped deities with worship strength, NOTABLE personality extremes (only the top/bottom facets, not the full 50-facet dump), skills of note, likes/detests, physical highlights, and recent thoughts as the game phrases them (raw caption templates that may contain unfilled [quality]/[deity]/[relation] placeholders, surfaced verbatim), tied to current stress. Friends are positive-affection acquaintances; grudges are bonds gone negative with no positive love to offset them (each carries its raw love/trust/respect scores plus negative_dims naming the negative dimensions, as labeled facts). Empty categories degrade to []. Facts only — it senses, it does not advise. Returns {"error":...} for a missing unit_id or {"error":"no fort loaded"} if no fort is active.

Input parameters:

- `unit_id` (string, required): A live unit_id (all digits), e.g. from a find_unit match

### `defenses` (~441 tokens)

Defenses

Where the threats are versus what you have to fight them with. Returns active hostiles the fort has DISCOVERED (same fog-of-war gate and same count as threats()' active_hostiles and military()'s hostiles_on_map — a unit standing on an undiscovered tile is never listed and never leaks its position or creature token) with map positions and their geometry to the fort core and to the nearest drawbridge (dist = 8-directional tile count (Chebyshev), dz = z-levels with + meaning above the threat, dir = compass bearing; the nearest bridge is chosen on 3D distance, so a bridge many z-levels away never wins on horizontal proximity alone), plus an inventory of controllable defensive structures (drawbridges with positions, levers, floodgates, hatches, cage traps, locked doors). Terrain-aware: each threat is classified inside/outside the fort's walled perimeter — "inside" means its tile shares a walkability group with your citizens, i.e. a hostile could walk to your population through connected open space without breaching a wall (walk_group 0 = no walkable footing, e.g. a flier over open air). A perimeter_terrain field reads the busiest citizen level via the terrain substrate: an ASCII tile grid (with legend) plus counts of walls, fortifications (with positions), and open-to-sky vs covered vs undiscovered tiles. Facts only — decide the tactics yourself, and use identify() for a creature's trait facts (e.g. cage traps do not hold a TRAPAVOID creature). Caveats: inside/outside is walking connectivity, so a FLIER or BUILDING_DESTROYER can reach you while reported "outside" — cross-reference its traits. perimeter_terrain is a single z-level and does not synthesize a multi-z approach vector; undiscovered tiles are fog of war ("?") and never leak their real type. Which lever raises which bridge is not recorded in the raws, so bridges and levers are reported separately, not linked. Returns {"error":"no fort loaded"} if no fort is active.

### `environment` (~172 tokens)

Environment

The fort's ambient conditions right now: current season and dominant weather (none/rain/snow), the surface temperature with whether exposed water is currently frozen (the freezing point is 10000 DF units; composes with geology()'s freeze-in-winter fact), the alignment of the biomes visible at embark (evil / good / reanimating booleans), and — for each cavern the fort has ALREADY breached — whether it is open to fort pathing or sealed off. Fog-of-war honest: reports NOTHING about undiscovered cavern layers (a fort that has breached none returns an empty caverns list). Small fixed-size payload. Per-tile savagery is unavailable in this DFHack build, so no savage flag is reported. Returns {"error":"no fort loaded"} if no fort is active.

### `farming` (~353 tokens)

Farming

The fort's farm plots and seed stock as facts — the early-survival pipeline that stocks (food OUTPUTS) and game_data (what is plantable, in the abstract) don't cover. Each plot's tile size, whether it's open to the sky right now (open_to_sky — light/weather exposure, NOT a surface-vs-underground fact: a roofed surface plot is open_to_sky:false, indistinguishable here from a genuinely underground one), its crop assignment for each of the 4 seasons (SPRING/SUMMER/AUTUMN/WINTER; crop is the plant token or absent if that season is fallow), whether that crop is eligible to grow in that season (the plant raw's own season flag — absent when the season is fallow). no_crop_assigned flags a plot with no crop assigned in ANY season; no_eligible_crop flags a plot with no season holding BOTH an assigned crop AND eligibility (a strict superset of no_crop_assigned — a plot can have crops assigned yet still qualify if none of them are actually eligible for their season). seed_totals[]/seed_totals_count/seed_totals_truncated sums seed counts by plant across the whole fort (forbidden/dumped/rotten/trader-bound seeds excluded, capped at 100 distinct plants) — the single source for seed stock; join a plot season's crop token against it rather than looking for a per-plot seed count, which would just repeat the same fort-wide number under every plot growing that crop. plots[] is capped at 200 (plots_total/plots_truncated track the real count and any overflow). Returns {"error":"no fort loaded"} if no fort is active.

### `find_unit` (~137 tokens)

Find unit

Look up citizens by a name fragment or profession (case-insensitive, matches either). Returns a compact dossier per match: profession, age, stress level, current job, squad, and health flags (wounded/patient/unconscious). Useful for questions like "how is the chief medical dwarf" or "find Urist". Each match carries a unit_id — pass it to citizen() for a deep dossier (personality, the walkable social graph, worship, skills, preferences, recent thoughts). Returns {"error":"no fort loaded"} if no fort is active.

Input parameters:

- `query` (string, required): Name fragment or profession to search for

### `fluids` (~646 tokens)

Fluids

Water and magma engineering facts the Earthworks tier (tile_region, geology) does not cover: aquifer layers, standing/flowing water, the magma sea's top, flood exposure at the fort interior, and well water-source depth. Revealed-only, fog-of-war safe — undiscovered tiles never contribute to any field here, the same as tile_region/defenses. aquifer_layers[] groups contiguous revealed z-levels sharing the same light/heavy classification (a mix of both within a run of z-levels reads "mixed") with light_tiles/heavy_tiles tile counts (capped at 50 layers). water_layers[] is a per-z-level aggregate of revealed standing/flowing water tiles (tiles, salt_tiles/fresh_tiles, stagnant_tiles/flowing_tiles, max_depth 1..7) — NOT flood-filled into discrete named bodies (no connectivity analysis is attempted; a single lake spanning two z-levels appears as two rows) — capped at 200 z-levels. ice_layers[] is the frozen peer of water_layers[]: a per-z-level count of revealed ICE tiles (walkable_tiles = ice you can stand on, e.g. a frozen river surface; solid_tiles = ice wall you would have to mine; the two always sum to tiles), capped at 200 z-levels. Frozen water is NOT water as far as the engine is concerned — it has no flow and becomes a solid tile — so a river or brook that freezes over DISAPPEARS from water_layers[] entirely and shows up here instead. A fort reading water_layers: [] alongside a populated ice_layers[] has surface water that exists but is frozen, which is also when wells report source "frozen" and jobs cancel for "No water source". magma_sea is the highest revealed z-level with at least 20 revealed magma tiles (a size floor meant to separate a real magma sea from a small pool/volcano pipe), omitted (not a null field) if no such level is revealed. flood_risk_tiles[] lists revealed FULL-depth (max_depth 7/7) water tiles chebyshev-adjacent to a tile in the same walkable group as any citizen — a flood-EXPOSURE fact (this water sits next to fort-reachable space right n…

### `fort_health` (~397 tokens)

Fort health

The fort's computational health as facts — FPS death is the true endgame boss, and nothing else reports it. fps/gfps are the engine's own currently calculated simulation/graphics frame rates (df.global.enabler.calculated_fps/calculated_gfps — the same numbers DF's own FPS counter shows), not an average or a history. items.total is the fort-wide item-object count (df.global.world.items.all, unfiltered by forbidden/dump/rotten/construction state — every item DF is tracking counts toward this, since object count, not usable stock, is what costs simulation time); items.stone/corpses/clothes break out the three clutter candidates the issue names (stone: BOULDER; corpses: CORPSE + CORPSEPIECE + REMAINS; clothes: the wearable slots ARMOR/SHOES/HELM/GLOVES/PANTS) — these are raw totals and will run higher than stocks' counts, which filter to usable/in-play items only; the two answer different questions (clutter vs usable stock) and are not duplicates. units.active/units.dead_on_map split df.global.world.units.active by isDead — active is every currently-simulated living unit (citizens, tame animals, wildlife, hostiles, visitors), dead_on_map is a dead unit whose body hasn't yet been cleaned up into a corpse item; both are fog-of-war filtered (mcp_unitVisibility), like every other unit-enumerating tool in this server, so an undiscovered cavern's unrevealed population is never counted here even though it still costs real simulation time — this tool intentionally undercounts true computational load rather than leak an unexplored area's population as an aggregate number. Stray/unassigned animal count is intentionally NOT duplicated here: call livestock_and_pastures and read its unassigned_count. Returns {"error":"no fort loaded"} if no fort is active.

### `fort_status` (~195 tokens)

Fort status

One-call situational overview of the currently loaded Dwarf Fortress fort: name, in-game date and season, population, created wealth, a happiness breakdown (miserable/unhappy/content/happy), recent_deaths, and a pre-triaged list of alerts worth attention. recent_deaths covers deaths recorded AT THIS SITE within the last game year: how many were the fort's own citizens, what killed them (citizen_causes, e.g. THIRST/STARVATION/STRUCK_DOWN), how many of those citizens were slain BY another citizen (the murder/loyalty-cascade signal, which no other tool reports), and how many of the dead were outsiders rather than fort members. Citizenship is the victim's membership in the fort group, so another civilization's dwarves killed at your gate count as outsiders, not as your losses. Returns {"error":"no fort loaded"} if no fort is active.

### `game_data` (~273 tokens)

Game data

Look up the LOADED WORLD's raws (ground truth for THIS world) and return curated, labeled facts. This is the authoritative source for procedural creatures (demons, forgotten beasts, titans) that never appear on the wiki. Covers six kinds via the `kind` filter (default creature): creature, material, plant, reaction, item, building. Pass a token (e.g. "DEMON_4", "INORGANIC:IRON", "MAKE_SOAP_FROM_TALLOW"), a name (case-insensitive substring, e.g. "flame phantom", "plump helmet"), or — for creature — a live unit_id (all digits). A single strong hit returns a full dossier for that kind; several return a disambiguation list; none returns {"match_count":0,"matches":[]}. Returns {"error":"no game loaded"} if no game is active.

Input parameters:

- `kind` (string): Which raws table to search; defaults to creature. One of creature | material | plant | reaction | item | building.
- `query` (string, required): A raws token or case-insensitive name fragment for the chosen kind (e.g. "IRON", "plump helmet", "MAKE_SOAP_FROM_TALLOW"); for the creature kind, a live unit_id (all digits) also resolves.

### `geology` (~367 tokens)

Geology

A one-call geological survey of the embark, REVEALED-INFO ONLY by default. Returns surface_z_max — the highest open-to-sky ground z-level found ANYWHERE on the embark (NOT map_overview()'s surface_z_at_core, which is the surface directly above the fort centre only; on a sloped map the two legitimately differ by many z-levels); the layer stack the fort has exposed (each band z_top..z_bottom with a kind — soil/sedimentary/metamorphic/igneous — and the in-game material names, e.g. "limestone", that game_data/wiki_lookup resolve); the aquifer (presence, light vs. heavy type, and z-range, enough to fuse with wiki_lookup("Aquifer")); the caverns actually DISCOVERED (each with z-range and whether it holds water); whether the magma sea has been reached; and surface water (brook, river, murky-pool count, and permanent_freeze — whether the biome's base temperature keeps surface water frozen year-round, glacier/tundra, the well-gating fact; not a seasonal winter claim). Undiscovered caverns and an unreached magma sea are OMITTED (fog of war stays honest). Set reveal_hidden=true to BYPASS FOG OF WAR and also surface every undiscovered cavern (caverns_hidden) and the magma sea z-range (magma_hidden) — a debug/spoiler switch, default false. Reports what is there, not where to dig. Returns {"error":"no fort loaded"} if no fort is active.

Input parameters:

- `reveal_hidden` (boolean): Bypass fog of war: also report undiscovered caverns and the magma-sea z-range regardless of discovery. Default false (undiscovered depths omitted).

### `hauling_routes` (~429 tokens)

Hauling routes

The fort's minecart hauling infrastructure: routes, each route's stops, and the vehicle (minecart) objects fort-wide. routes[] is {id, name?, stops[], vehicles[]}. Each stop is {id, name?, pos:{x,y,z}, stockpiles[], conditions[], parked_vehicle_id?} — stockpiles[] is {building_id, take, give} for every stockpile linked to that stop (take = the cart picks items up from that stockpile, give = the cart drops items into it; a link can be both). conditions[] is that stop's departure conditions — {direction, mode, timeout, load_percent, at_most, desired} — mode is how a dwarf moves the cart onward (Push/Ride/Guide), direction is the initial departure heading, and at_most/desired describe the load_percent threshold that must be met before the cart leaves. parked_vehicle_id is the vehicle currently sitting at that exact stop right now, absent if none is. A route's own vehicles[] ({vehicle_id, current_stop_id?}) is the set of vehicles assigned to run that route — DF assigns vehicles at the route level, not per stop; current_stop_id is which of that route's own stops the vehicle currently occupies, absent if in transit or unknown. The top-level vehicles[] is every hauling vehicle fort-wide (minecarts and other cart- type haulers; battering rams excluded) as {vehicle_id, item_id, backing_item_exists, route_id?, on_track} — backing_item_exists is whether the vehicle's backing item still exists (a vehicle can persist as a stale reference after its physical cart is destroyed or stolen, regardless of whether it is assigned to a route); route_id is absent for a vehicle not currently assigned to any route. Both routes[] (capped 100, see routes_total/routes_truncated) and the top-level vehicles[] (capped 200, see vehicles_total/vehicles_truncated) are sorted by id. Returns {"error":"no fort loaded"} if no fort is active.

### `identify` (~210 tokens)

Identify

One-call "what is this creature and how do I handle it": fuses THIS WORLD's raws (ground truth) with the DF wiki (strategy). Pass a creature token (e.g. "DEMON_4"), a name ("flame phantom"), or a live unit_id (all digits) — same contract as game_data. Returns the creature dossier (flags, attacks, interactions — e.g. a TRAPAVOID flag means cage traps cannot hold it) plus 1-2 trimmed wiki strategy excerpts. Procedural creatures (demons, forgotten beasts, titans) have no wiki page, so strategy leans on their traits plus the most relevant trait page (fire, building destroyer). Use this instead of a bare wiki lookup so world-specific facts are never missed. Multiple matches return a disambiguation list; returns {"error":"no game loaded"} if no game is active.

Input parameters:

- `query` (string, required): Creature token, name fragment, or a live unit_id (all digits)

### `injuries_and_health` (~206 tokens)

Injuries and health

The fort's medical picture, counted on CURRENT condition rather than on history. wounded = dwarves carrying at least one wound with live damage (bleeding, pain, swelling, an unset fracture, a severed part, infection). old_wounds_only = dwarves whose every wound has resolved to a scar or to an inert record — DF never deletes a healed wound, so these keep a wound entry forever and are NOT casualties. patients = dwarves the game itself flags as needing healthcare, bedridden = flagged should-not-move. unconscious counts only genuine unconsciousness: a normally SLEEPING dwarf also carries a nonzero unconscious counter, so sleepers are excluded and reported as asleep instead. care_needs breaks down what care the game is asking for (diagnosis, surgery, suture, dressing, crutch, ...) so gaps in medical coverage are visible; it is empty when no dwarf currently needs care. Returns {"error":"no fort loaded"} if no fort is active.

### `jobs_and_labor` (~182 tokens)

Jobs and labor

Workforce utilization: how many working-age dwarves are busy vs. idle (children/babies excluded from the labor pool), the idle percentage, and a ranked breakdown of what jobs the fort is currently working on. High idle can mean unassigned labor or nothing queued. cancellations aggregates recent job-cancellation OCCURRENCES (the currently-retained report buffer, roughly the last few months of play) by their reason text, sorted most-frequent first — counting occurrences, not report rows: DF collapses consecutive identical cancellations into one report and tallies the extras separately, so a reason repeating many times (e.g. "Equipment mismatch") is weighted by its true frequency, not undercounted as a handful of rows. chronicle sees the same announcements one at a time but never aggregates them. Returns {"error":"no fort loaded"} if no fort is active.

### `livestock_and_pastures` (~524 tokens)

Livestock and pastures

The fort's tame animal economy as facts — every prior tool sees hostiles (threats) or nothing at all here. tame_total/pets/livestock split ownership; by_group[]/by_group_total/by_group_truncated count tame animals by species/sex/adult-or-not, capped at 100 distinct combinations. grazers reports total vs pastured, plus the individual animals NOT in any pasture zone — a grazer with no pasture cannot graze and silently starves; this is normally invisible (juveniles graze too, so grazer status is NOT gated on adulthood). egg_layers reports counts only (total, fort-wide nestbox count, how many are pastured without a nestbox in reach, how many are unpastured) since the consequence (missed eggs) is mild and the population is usually large — gated on adulthood (a juvenile of an egg-laying caste cannot actually lay yet, matching DFHack's own autonestbox behavior). marked_for_slaughter and trained (training_level Trained..MasterfullyTrained — DF's single shared training-quality scale, NOT which discipline the animal was trained for; it does not persist war-vs-hunting per animal) list individual animals, capped. cages[]/cages_truncated lists occupied cages with their occupants (dfhack.buildings.getCageOccupants); each cage's own occupants[] is independently capped too (occupants_total/occupants_truncated), so a single densely-packed cage trap can't inflate the response either. unassigned_count is animals with no pasture, cage, or chain — DFHack's zone tool calls this "unassigned" (roaming loose); reported as a count only since it is commonly large and often intentional (e.g. free-roaming cats). Every unit fact here is gated through the fog-of-war visibility check — an undiscovered cavern's wildlife never leaks in. The tame-animal enumeration (tame_total/pets/livestock/by_group/grazers/egg_layers/marked_for_slaughter/trained/unassigned_count) is ADDITIONALLY restricted to this fort's own civ, so a caravan's or diplomat's pack animal is excluded; cages[].occupants[] intentional…

### `map_overview` (~325 tokens)

Map overview

Cheap spatial orientation to run BEFORE any per-tile terrain read: map extents (x/y/z tile counts), the fort-core coordinate (the same 3D citizen centroid defenses() reports), surface_z_at_core — the surface z-level directly above the fort center, i.e. the highest open-to-sky ground tile in THAT ONE COLUMN, or null if the core is not under open sky (this is NOT geology()'s surface_z_max, which is the highest open-to-sky ground tile anywhere on the embark; on a sloped map the two legitimately differ by many z-levels, so pick the one you mean) — the z-levels that carry player activity (construction and pending digging, listed separately and as a union), and stairways collapsed to traversable single-column vertical runs (x, y, z_top, z_bottom); a run only spans levels that actually connect by DF stair rules, so a helical shaft splits into its climbable segments. The payload is fixed-size regardless of fort size: activity is a set of z-levels, never per-tile, and stair columns are RANKED BY HEIGHT (tallest run first) then capped, so when a fort exceeds the cap the tallest shafts survive and only trivial fragments are dropped (stair_columns_truncated flags the overflow; stair_columns_total gives the full count). Fog-of-war honest: undiscovered tiles never leak. Use it to decide which z-levels and area to pull grids for. Returns {"error":"no fort loaded"} if no fort is active.

### `mandates_and_justice` (~194 tokens)

Mandates and justice

The fort's nobility overhead as facts. Active production mandates (a noble's make-N-of-an-item quota with its remaining count and days to deadline) and export bans, listed by item. Unmet noble room demands (an appointed noble holds no room zone of a type their position requires: office, bedroom, dining, tomb). Justice state: open criminal cases, convictions awaiting punishment (prison sentences, scheduled beatings and hammerstrikes), and restraint capacity (chains + cages actually CONSTRUCTED vs. how many are free, with placed-but-not-yet-built ones counted separately in restraints_unbuilt) so you can see whether a sentence can be served. Reports what the nobles demand and the justice backlog, not what to build about it; threshold restatements are in alerts. Lists are capped (see the *_truncated flags). Returns {"error":"no fort loaded"} if no fort is active.

### `mechanisms` (~370 tokens)

Mechanisms

The fort's lever/pressure-plate wiring as facts — players (and an AI co-pilot) routinely forget which lever raises which bridge; this makes it legible. levers[] lists every lever with its position, current state (0/1 — the physical orientation, NOT which way any linked gate is), linked_targets (every building its mechanism items connect to — bridge/door/floodgate/hatch/support/weapon-trap — with that target's id, type, position, and a state string (raised/lowered/raising/lowering for a Bridge, closed/open/closing/opening for a Floodgate, closed/open (no transitional state) for a Door/Hatch, retracted/unretracted/retracting/unretracting for a Weapon spike) when the target exposes one), and pending_pull_jobs (PullLever jobs already queued on it, so a caller can see a pull is already in flight before queuing another). pressure_plates[] lists every plate's linked_targets the same way, plus triggers — the configured trip conditions (citizens, creatures with a weight range, a minecart-weight range on track, or water/magma depth ranges). unlinked_levers is the ids of levers wired to nothing (dead ends); unlinked_bridges is bridges no lever or plate in the fort currently operates (must be hand-opened/closed, or are permanently fixed). levers[]/pressure_plates[]/unlinked_levers[]/unlinked_bridges[] are each capped at 200 (id-sorted) with their own *_truncated flag — lever_count/plate_count/bridge_count are always the true totals regardless of truncation. Pairs with the pull_lever actuator. Returns {"error":"no fort loaded"} if no fort is active.

### `military` (~371 tokens)

Military

The fort's military: number of squads, how many living present dwarves are actually enlisted (soldiers), filled squad positions, and readiness read against hostiles currently on the map (great-danger split out). Each squad also reports: roster[] — one row per FILLED position whose occupant is a LIVING, PRESENT unit (a dead or off-map holder still counts toward filled but is omitted here, so gear sitting on a corpse never generates a false alert) — that soldier's uniform_complete flag, and (only when incomplete) uniform[], aggregated by item type (ARMOR/HELM/PANTS/GLOVES/SHOES/SHIELD/WEAPON/...) into assigned_count (items the uniform calls for — a required-but-never-found item still counts as 1) vs missing_count (of those, how many are not currently worn/wielded — DF's own uniform-unstick logic, or never assigned at all) — this is the tool-API spec's originally-promised "equipment_gaps" (e.g. "8 of them have no armor"), previously unfulfilled; a fully-equipped soldier's uniform[] is empty (uniform_complete says so already) to keep the payload proportional to actual gaps, not fort size. ammo — the squad's configured ammunition specs (item type, target_amount — the squad's shared configured total, NOT per-soldier — and how many are currently assigned) and ammo_items_assigned, the total ammo items currently carried by the squad; training — the active training-schedule month's sleep_mode, uniform_mode, and active_orders (both undefined/empty when the fort has never customized that routine's month). alerts also flags any roster member with an incomplete uniform by name. Returns {"error":"no fort loaded"} if no fort is active.

### `moods` (~157 tokens)

Strange moods

Any active strange mood (fey/secretive/possessed/macabre/fell) and its material countdown. For each moody dwarf: the mood type, the driving skill, the workshop claimed (or that none is yet, via workshop_status: unclaimed/gathering/working), the raw mood countdown, and every demanded material cross-referenced against fort stock — needed, gathered so far, and how many the fort actually has (have). The early warning is "demands bones, fort has zero": it reports the demand vs. the stock, not what to go collect. Returns {"active":[]} when no strange mood is in progress (the common case), and {"error":"no fort loaded"} if no fort is active.

### `nobles_and_administrators` (~256 tokens)

Nobles and administrators

The fort's appointed positions (manager, bookkeeper, broker, chief medical dwarf, sheriff, expedition leader/mayor, militia commander/captain, hammerer, and any higher noble the site has grown into) as facts: each position's holder(s) or vacancy — each holder always carries histfig_id, plus unit_id when that historical figure has a loaded unit on this map (a holder living off-map still has no unit_id). A vacant position is a common, otherwise-invisible cause of "why won't this validate" — work_order_create needs a manager, trade needs a broker, mandates_and_justice punishments need a hammerer. superseded_by names the position a role hands its responsibilities to once filled (e.g. sheriff -> captain of the guard, expedition leader -> mayor) — a vacancy there is often expected, not a problem. Also reports the bookkeeper's precision level (0-4, higher = more accurate stock counts, set on the Nobles screen), whether a mayoral election is currently forced/pending, and whether the civilization's monarch has arrived at the site (and if so, hastily). Returns {"error":"no fort loaded"} if no fort is active.

### `petitions` (~345 tokens)

Petitions

The fort's outstanding agreements as facts: location petitions (temple/guildhall requests from a deity's worshippers or a guild) and residency/citizenship petitions (a migrant or visitor asking to join the fort), each with its petitioner, agreed date, and resolution status. location_petitions[] carries building (TEMPLE/GUILDHALL), tier (1 = temple/guildhall, 2 = complex/grand), the petitioning deity (temple) or guild profession (guildhall) when one was named, age_days since the petition was raised, and warned_ready (the fort has already been told the location can be established — a still-outstanding petition with warned_ready true is the classic silent-failure case: agreed to but never actually zoned). residency_petitions[] carries kind (Residency/Citizenship), age_days, and deadline_days (days left before the petitioner's patience runs out, null if no timeout is tracked). Both carry awaiting_decision (true if the petition sits in the fort's pending decision queue right now) and status (outstanding/satisfied/denied/expired, derived from the agreement's own flags — see the doc for how this maps to DFHack fields). This is the demand-fulfillment counterpart to rooms_and_zones's temple/guildhall inventory (needed_by_worshippers there is inferred from citizen worship with no formal petition yet; a row here is an actual agreement DF is tracking) — compose the two rather than expecting either to duplicate the other. Lists are capped at 50 each (see the *_truncated flags). Returns {"error":"no fort loaded"} if no fort is active.

### `rooms_and_zones` (~298 tokens)

Rooms and zones

The fort's facility inventory, each count paired with its demand-side number where one exists: bedrooms (assigned/unassigned vs. adults without one), dining halls and seats, the hospital (beds, traction benches, whether a well is inside, and medical supplies physically stocked), wells (working state and water source: water/frozen/magma/unknown), temples (dedicated deities, whether an all-inclusive temple exists, and deities worshipped by citizens that lack a dedicated temple), taverns, libraries, guildhalls, and coffins free vs. dead awaiting burial (loose corpses of the fort's own race). ghosts reports active apparitions currently on the map (`active[]`, fog-of-war gated) plus unquiet_dead_count — this civ's dead who are world-flagged as unquiet ghosts (`flags.ghost`) and NOT represented in the visible `active[]` list. This is deliberately not the same as "confirmed absent locally": a ghost hidden behind fog of war is excluded from `active[]` (never leaked) but still counted here, since the world-level ghost fact is fair game even when its exact location isn't. The supply-side companion to unmet_needs(). Reports what the fort has, not what to build. Wells are capped (wells_truncated flags the overflow); bedroom and coffin detail is aggregated to counts. Returns {"error":"no fort loaded"} if no fort is active.

### `site_history` (~167 tokens)

Site history

This fort's entry in the PERMANENT world saga (the durable history event log, not the pruned live report stream). Returns the founding (year, in-game date, and owning civilization in both Dwarven and English), the fort name in Dwarven and English with a word-by-word etymology, prior sieges/battles fought AT this site (attacker/defender civ and general, capped at 20, most-recent-first), and the notable historical figures who died here (name, race, cause, slayer, capped at 25). Scoped strictly to the loaded site. A young fort with no war history degrades to empty battle/death lists (not an error). Returns {"error":"no fort loaded"} if no fort is active.

### `stockpiles` (~746 tokens)

Stockpiles

The fort's hauling/logistics picture: every stockpile as a fact sheet, plus fort-wide backlog signals stockpiles alone don't surface. piles[] is one row per stockpile building: id, exact bounds (x1/y1/x2/y2/z — the bounding box; an irregularly-shaped pile's real footprint can be smaller, see size), size (the pile's real tile count — for an irregular pile this reads its room.extents occupancy map rather than the bounding box, so holes/excluded tiles are correctly excluded), categories[] (which of the 17 top-level stockpile groups this pile accepts — animals/food/furniture/corpses/refuse/stone/ammo/coins/bars_blocks/gems/finished_goods/leather/cloth/wood/weapons/armor/sheet; note Ore has no flag of its own in DF's own stockpile_group_set — it rides under stone — and Misc (organic/inorganic) is a refuse sub-filter, not a top-level category, so neither appears in this list), barrels_allowed/bins_allowed (the pile's max_barrels/max_bins > 0 — DF stores 0 there specifically to mean "no containers of this kind," not "unlimited"), max_wheelbarrows (raw count; 0 here means no wheelbarrow is assigned so DF queues one haul job per item, NOT "wheelbarrows disallowed" — this one has no allowed/disallowed reading, unlike barrels/bins), links_only (DF's own "take from links only" toggle), give_to[]/take_from[] (ids of stockpiles this pile explicitly feeds into / pulls from via the g/q-t hauling-route UI, each capped at 50 with its own _truncated flag), item_count (exact count of non-rotten/non-dump/non-forbidden/non-construction/non-trader items physically sitting on the pile's tiles right now — via each item's resolved position, so items inside a bin or barrel parked on the pile count too — regardless of whether the pile's own categories[] actually accept that item, since this is a positional fact, not a settings-compliance check), and occupied_tiles (how many of the pile's size tiles have at least one qualifying item on them right now — a directly-counted fact, bounded [0, size…

### `stocks` (~179 tokens)

Stocks

Food and drink as estimated days-of-supply for the current population, plus counts of critical materials (wood, fuel, cloth, tanned hides, stone) and lists of notably low or high stocks. Days-of-supply assume ~2 food and ~5 drink per dwarf per season. clothing reports the citizens wearing worn (wear >= 2 — "X" heavily-worn/threadbare, or "XX" tattered/mangled; DF's own 4-stage scale is item -> x-item-x -> X-item-X -> XX-item-XX -> destroyed) shoes/armor/pants/gloves/helm — a chronic, easy-to-miss stress source — and how many citizens currently have no shoes worn at all (a count, since the population involved is usually the whole fort). Returns {"error":"no fort loaded"} if no fort is active.

### `threats` (~83 tokens)

Threats

Dangerous units currently on the map, grouped by creature type. Separates ACTIVE hostiles from CONTAINED ones (caged/chained), flags great-danger creatures (megabeasts, titans, demons, forgotten beasts), invaders, and the undead, and returns a pre-triaged alerts list. Returns {"error":"no fort loaded"} if no fort is active.

### `tile_region` (~376 tokens)

Tile region

A bounded window of ONE z-level rendered as an ASCII character grid plus a self-describing legend (every response carries the legend for exactly the glyphs it uses). Renders terrain shape (undug stone #, undug soil ",", dug floor ., ramps r/v, up/down stairs </>/x, fortifications F, trees T), constructed floor +, water ~ and magma %, and building footprints collapsed to FOUR CLASSES — workshop/furnace W, stockpile S, machine M, furniture n — never per-building detail. The grid glyph is depth-blind; a separate sparse liquids list carries per-tile [{x,y,type,depth}] (flow_size 1..7). Undiscovered tiles are ? (fog of war) and are NEVER painted over. All five parameters are OPTIONAL: with none, returns a fixed DEFAULT 60x40 window centered on the fort core (the busiest citizen z-level and that level's citizen centroid); pass z alone to recenter on THAT level's own citizen centroid; pass z,x0,y0,x1,y1 for an explicit rectangle. The window is hard-capped at 100x100 per side — an oversized request is CLAMPED (never errored) with truncated:true and the original size echoed in requested. Facts only: it renders the map, it does not design or suggest layouts. Read-only. Returns {"error":"no fort loaded"} if no fort is active.

Input parameters:

- `x0` (integer): window corner X (with y0,x1,y1 for an explicit rectangle)
- `x1` (integer): opposite window corner X
- `y0` (integer): window corner Y
- `y1` (integer): opposite window corner Y
- `z` (integer): z-level to render; defaults to the busiest citizen level

### `trade` (~318 tokens)

Trade and caravans

The trade picture right now: whether a trade depot exists, is complete, and is wagon-accessible (DF's own pathability check, not merely built); which caravans are present and their lifecycle state (none / approaching / at depot / leaving, with days remaining where knowable) and civ; whether a broker is assigned, present, at the depot, and their current job; and the count and approximate value of goods staged in the depot. Each caravan also reports manifest (count, approximate value, and a by-category breakdown of goods the caravan itself is carrying, before anything is unloaded to the depot — distinct from goods_at_depot) and agreements (active liaison price agreements as price_pct_min/max, 100 = no markup, e.g. 200 = double price: export rows are items this fort earns a bonus selling to the caravan, by DF's item type; import rows are items this fort pays a premium buying from the caravan, by DF's own request-tab category — a different, coarser taxonomy than item type, so the two lists will not line up 1:1). If reading either one fails against a real caravan (a field-path or calculation error, not simply "nothing to report"), that caravan's row carries manifest_error/agreements_error (the raw error string) instead of the field, so a live check can tell a genuine bug apart from an empty result. Reports the state and the numbers, not what to trade. Returns {"error":"no fort loaded"} if no fort is active.

### `unmet_needs` (~276 tokens)

Unmet needs

Why the fort is stressed: the dwarven needs system aggregated across all citizens. Returns the top unmet needs (e.g. prayer, drink, socializing) ranked by how many dwarves are distracted, each with the worst focus level (how starved the need is), plus how many dwarves have at least one unmet need. Reading worst_focus: it is DF's raw focus_level — 0 is neutral, positive means recently satisfied (caps at 400), negative means starved. There is NO single floor: the minimum is -16320 x that dwarf's need_level for that need, so a level-1 need bottoms out at -16320, a level-2 need at -32640, a level-5 need at -81600. An identical worst_focus repeating across several needs is therefore normal — those needs share a need_level and are all fully starved — not a clamp or a quantization artifact; and a larger magnitude on one need does not by itself mean it is more starved than another, since the two may have different floors. Compare against the floor, not against zero. Reports which needs are unmet, not how to fix them (look that up or reason from the need type). Complements fort_status happiness. Returns {"error":"no fort loaded"} if no fort is active.

### `wiki_lookup` (~152 tokens)

Wiki lookup

Fetch a Dwarf Fortress wiki article as clean, readable text, pinned to the DF2014 namespace. Follows redirects (multi-hop) and honors section fragments (e.g. "Weapon trap" resolves to the Weapon Trap section of the Trap page). Cache-first to disk (~30-day TTL); pass refresh:true to bypass. Pure HTTP — works without the game running. Returns {title, url, text, from_cache, resolved_from?} or {error} if the page is not found.

Input parameters:

- `refresh` (boolean): Bypass the disk cache and refetch
- `section` (string): Section/heading name to scope to
- `title` (string, required): Article title or topic (namespace optional)

### `wiki_search` (~87 tokens)

Wiki search

Search the Dwarf Fortress wiki (MediaWiki) for candidate article titles and cleaned snippets. Discovery/disambiguation step before wiki_lookup; biased to the DF2014 (Steam/Premium) namespace. Pure HTTP — works without the game running. Returns {results:[{title, snippet}]} (up to 8).

Input parameters:

- `query` (string, required): What to search the DF wiki for

### `work_details` (~224 tokens)

Work details

List the fort’s work details (the labor-management groups) as facts: each detail’s name, mode (OnlySelectedDoesThis / EverybodyDoesThis / NobodyDoesThis / Default), the labor tokens it enables, and its assigned citizens. The member list is id-sorted and capped at 200 per detail — member_count is always the full count and members_truncated flags when the list is capped; member_names gives readable names parallel to members. Both parameters are OPTIONAL narrowing: detail (exact name) returns ONLY that detail; members_after (a unit id) starts each member list after that id — a truncated detail carries members_cursor (its last listed id) to pass back as members_after for the next page. READ-ONLY and always available (not behind the actuator gate); also the readback sensor for assign_work_detail.

Input parameters:

- `detail` (string): exact work detail name — return ONLY that detail, e.g. "Miners"
- `members_after` (integer): member-list cursor: list members with id AFTER this (use members_cursor from a truncated response)

### `work_order_list` (~161 tokens)

Work order list

List the fort’s active manager (work) orders as facts: id, job type, output item/material tokens, amount total/left, repeat frequency, bound workshop, condition count, and per-order validation state (active + validated; validated:false means the order cannot currently be fulfilled). Also reports whether a manager noble is assigned. `count` is the fort total; the page is sorted by id and capped at 256 — when capped, truncated:true and next_cursor gives the after_id for the next page. READ-ONLY and always available (not behind the actuator gate); also the readback sensor for work_order_create / _cancel.

Input parameters:

- `after_id` (integer): pagination cursor: return only orders with id greater than this (from next_cursor)

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/alexanderolvera-dfhack-mcp/dfhack-mcp#diagnostics

## Score history

- 2026-08-03: 77
- 2026-08-02: 77
- 2026-08-01: 5
- 2026-07-31: 17
- 2026-07-30: 24
- 2026-07-28: 24
- 2026-07-27: 24

## Links

- npm package: https://www.npmjs.com/package/dfhack-mcp
- Socket report: https://socket.dev/npm/package/dfhack-mcp
- Repository: https://github.com/alexanderolvera/dfhack-mcp
- Changelog RSS feed: https://verifymcp.io/servers/alexanderolvera-dfhack-mcp/dfhack-mcp/changelog.xml
- Changelog JSON feed: https://verifymcp.io/servers/alexanderolvera-dfhack-mcp/dfhack-mcp/changelog.json
- HTML version of this page: https://verifymcp.io/servers/alexanderolvera-dfhack-mcp/dfhack-mcp
