dfhack-mcp

dfhack-mcp

An MCP server that exposes a live Dwarf Fortress fort to an AI agent as a small set of curated, semantic tools — a fortress co-pilot and early-warning advisor, not an autopilot. It provides read-only sensors and reference tools to observe the game state and retrieve game data.

Category
访问服务器

README

dfhack-mcp

An MCP server that exposes a live Dwarf Fortress fort to an AI agent as a small set of curated, semantic tools — a fortress co-pilot and early-warning advisor, not an autopilot. Read-only in v1: the tools observe the game, they never change it.

It rests on two pillars:

  • Sensors (fort_status … find_unit) answer what is this fort doing right now? — happiness, stocks, threats, jobs, military, health, defenses.
  • Reference (game_data, wiki_*, identify) answer how does DF work? — game_data is this world's ground truth (the loaded raws), the wiki_* tools are the general explanation, and identify fuses the two.

Most sensor tools are a thin TypeScript wrapper over one purpose-written DFHack Lua query that returns already-summarized JSON, so responses stay small and glanceable. It talks to DFHack's Remote RPC (localhost:5000) through the sibling dfhack-remote-node client.

Requirements

  • Node 24+ — runs the TypeScript sources directly via type-stripping, so the dev workflow needs no build step. (Local imports use explicit .ts extensions.)
  • Dwarf Fortress running with DFHack, a fort loaded.
  • DFHack Remote RPC reachable on localhost:5000 (allow_remote may stay false — localhost is enough).

Setup

This server depends on the sibling dfhack-remote-node client as a local file: dependency (file:../dfhack-remote-node). That package ships as built TS — its package.json main points at dist/index.js — so install it first so its dist/ is built before this server links to it.

# 1. Clone both repos side by side (keep the client folder name dfhack-remote-node):
git clone https://github.com/alexanderolvera/dfhack-remote-node.git
git clone https://github.com/alexanderolvera/dfhack-mcp.git

# 2. Build the client FIRST (its `prepare` script builds dist/):
cd dfhack-remote-node
npm install

# 3. Then install this server (links the client + pulls dev tooling):
cd ../dfhack-mcp
npm install

Run

The server speaks MCP over stdio; an MCP client launches it. In development, run the TypeScript entry directly (no build):

node src/index.ts

MCP client config (e.g. Claude Desktop / Claude Code):

{
  "mcpServers": {
    "dfhack": {
      "command": "node",
      "args": ["C:/Users/Xalex/Desktop/DF-AI-Projects/dfhack-mcp-server/src/index.ts"]
    }
  }
}

Environment overrides: DFHACK_HOST (default 127.0.0.1), DFHACK_PORT (5000).

Verify against a live fort

The harness spawns the server over stdio like a real MCP client, lists the tools, and calls one:

npm run call                       # fort_status (default)
npm run call threats
npm run call find_unit query=medical
npm run call game_data query="flame phantom"
npm run call wiki_lookup title=Trap

Sample output (fort "Bustlanterns", DFHack 53.15-r2):

{
  "fort_name": "Bustlanterns",
  "date": "15th Malachite, Year 105",
  "season": "Summer",
  "population": 106,
  "wealth": 427944,
  "happiness": { "miserable": 0, "unhappy": 28, "content": 43, "happy": 35 },
  "alerts": ["28 dwarves unhappy", "2 hostiles on map"]
}

If no fort is loaded a tool returns {"error":"no fort loaded"}; if DFHack can't be reached at all it returns an isError result explaining that. Every tool is verified against a real running fort before it ships — never mocks.

Tools

The sensors (no arguments; report on the loaded fort):

  • fort_status() — name, date/season, population, wealth, happiness breakdown, pre-triaged alerts.
  • stocks() — food/drink as days-of-supply, plus critical material counts and notable-low/high lists.
  • threats() — dangerous units grouped by type; active vs. contained, great-danger/invader/undead flags, plus each group's creature token and decisive traits (trapavoid, flier, fire, webber, building_destroyer, ranged).
  • unmet_needs() — the needs system aggregated: top unmet needs ranked by how many dwarves are distracted, each with a build/action suggestion.
  • jobs_and_labor() — workforce utilization: busy vs. idle adults (children excluded), idle %, and a ranked breakdown of active jobs.
  • military() — squads, enlisted soldiers, filled positions, and readiness against hostiles on the map (great-danger split out).
  • injuries_and_health() — wounded / patients / bedridden / unconscious counts, plus what care is needed (diagnosis, surgery, suture, …).
  • defenses() — active hostiles with map positions and tile-distance/direction/z-delta to the fort core and nearest drawbridge, plus a controllable-structure inventory (bridges, levers, floodgates, hatches, cage traps, doors).
  • find_unit(query) — look up citizens by name fragment or profession; a compact dossier per match (profession, age, stress, job, squad, health flags).

The reference tools (wiki_* are pure HTTP and work without the game; game_data/identify need a loaded world):

  • game_data(query, kind?) — the loaded world's raws; ground truth for procedural creatures (demons, forgotten beasts, titans) that never reach the wiki. query is a creature token (DEMON_4), a name, or a live unit_id. One strong hit → a dossier; several → a disambiguation list. Only kind: creature is implemented; other kinds report "not yet implemented" and land in this same tool.
  • wiki_search(query) — search the DF wiki for candidate titles + cleaned snippets, biased to the DF2014 namespace.
  • wiki_lookup(title, section?, refresh?) — fetch a wiki article as clean text, pinned to DF2014; follows multi-hop redirects, honors section fragments, cache-first to a git-ignored cache/ dir (~30-day TTL; refresh bypasses).
  • identify(query) — "what is this creature and how do I handle it" in one call: fuses game_data (this world's raws) with wiki_lookup (strategy). Returns the dossier, a tactics list pairing each decisive trait with a hard-fact implication (e.g. TRAPAVOID → mechanical traps don't work), and 1–2 trimmed wiki excerpts. Use it instead of a bare wiki lookup when a threat appears.

run_lua(snippet) — dev only

A raw DFHack Lua escape hatch returning printed output verbatim. Not registered unless DFHACK_MCP_DEV is set — arbitrary Lua can read and write game state, so it is off by default and intended only for field-probing while authoring curated tools.

DFHACK_MCP_DEV=1 node src/index.ts

Layout

src/
  index.ts            server construction + the 13 tool registrations + stdio wiring
  register.ts         registerReadTool / registerQueryTool helpers (result + error framing)
  dfclient.ts         single RPC connection: lazy connect, one-shot reconnect,
                      registers dfhack-queries/ as a DFHack script path
  query.ts            invoke a named DFHack script -> parse JSON -> normalize list fields
  dfhack-queries/     the version-FRAGILE boundary: one real DFHack .lua script per query
    mcp_fortStatus.lua mcp_stocks.lua mcp_threats.lua mcp_unmetNeeds.lua
    mcp_jobsAndLabor.lua mcp_military.lua mcp_injuriesAndHealth.lua
    mcp_defenses.lua mcp_findUnit.lua mcp_gameData.lua
  wiki/               MediaWiki client (pure HTTP; the other external boundary)
    api.ts              fetch + search + redirect/namespace/section resolution
    clean.ts            rendered HTML -> readable text (dependency-free)
    cache.ts            git-ignored disk cache (cache-first, ~30-day TTL)
    lookup.ts           the wiki_lookup orchestration
    index.ts            public re-exports
  tools/              one file per tool: run the query/client call, parse, normalize
    identify/           tactics.ts (trait derivation) + wiki.ts (topic selection) + index.ts (fusion)
scripts/
  call-tool.mjs       end-to-end harness (real MCP client over stdio)
cache/                git-ignored disk cache of cleaned wiki pages

The dfhack-queries/ folder is deliberate: all version-fragile DFHack field access (the exact df.global.* / dfhack.units.* / caste paths that can shift between DF/DFHack builds) is confined there, so a version bump is a localized fix — you edit the .lua scripts, never the tools or the server.

These are real DFHack .lua scripts, not Lua embedded in TypeScript. On connect the server registers the folder with DFHack (dfhack.internal's script path) and each tool invokes its script by name with native argv — so query parameters are injection-safe by construction (no string escaping), and the Lua gets proper editor tooling. (This needs dfhack.internal.addScriptPath, present in the pinned build; the server errors clearly if a DFHack lacks it.)

Scripts

Script Does
npm start run the server directly (node src/index.ts, type-strip)
npm run build bundle src/index.ts → dist/index.js (ESM, shebang)
npm run typecheck tsc --noEmit
npm run lint eslint (flat config)
npm run format prettier --write
npm run call the live harness (see Verify above)

Contributing

Facts, not advice. These tools sense — they return what is true about the fort and the world, the way a player reads a screen or looks something up. They do not tell the agent what to build or how to fight; that judgment is the agent's job. A field that says what to do is advice — leave it out (or, if it is a genuine limitation the caller must know, put it in the tool description, not the per-call payload). Restating a fact that crossed a threshold ("28 dwarves unhappy") is fine — that mirrors the game's own announcements. Curated strategy, if it ever ships, belongs in a separate authored knowledge layer an agent chooses to consult, never scattered through sensor output.

New tools follow the existing split: write the version-fragile Lua as a real src/dfhack-queries/mcp_<name>.lua script (read parameters from local args = {...}, print(require('json').encode(...)) one JSON object), add a thin wrapper in src/tools/<name>.ts that calls runJsonScript('<name>', args, listFields) and types the result, and register it in src/index.ts. Verify every tool against a live fort with the harness — no mocks. Keep npm run typecheck and npm run lint clean.

License

ISC — see LICENSE.md.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选