legendsviewer-mcp
# legendsviewer-mcp
An [MCP](https://modelcontextprotocol.io) server that lets a language model read and query the
history of a **Dwarf Fortress** world: search the deeds recorded in the legends, read the biography
of a figure or the history of a civilization, and check whether what looks remarkable actually is.
It is a thin client over the analysis API added by
[LegendsViewer-AIPowered](https://github.com/Hadows/LegendsViewer-AIPowered), a fork of
[Kromtec/LegendsViewer-Next](https://github.com/Kromtec/LegendsViewer-Next).
## Requirements
**LegendsViewer-AIPowered must be running.** This server owns no world state: parsing a large export
takes about 30 seconds and 800 MB, which is fine once per application run and unacceptable per MCP
session. It therefore queries a long-lived backend rather than loading anything itself.
```bash
# in the LegendsViewer-AIPowered checkout
dotnet run --project LegendsViewer.Backend
```
A stock LegendsViewer-Next will not work: it has no `/api/Analysis` routes. `world_status` says so
explicitly if you point this at one.
## Install
```bash
npm install
npm run build
```
Then register it with your MCP client. For Claude Code:
```json
{
"mcpServers": {
"legendsviewer": {
"command": "node",
"args": ["/absolute/path/to/legendsviewer-mcp/dist/index.js"]
}
}
}
```
Set `LEGENDSVIEWER_URL` if the backend does not listen on `http://localhost:15421`.
## Tools
| Tool | Purpose |
|---|---|
| `world_status` | is the backend reachable, is a world loaded, which object types exist |
| `list_worlds` | worlds opened before, with the XML path each loads from |
| `load_world` | parse an export into memory (~30 s) |
| `world_summary` | overview: civilizations, eras, wars, most eventful figures |
| `read_object` | one object's history, `full` or `digest` |
| `search_objects` | find by name |
| `search_properties` | find by goal, race, position, affiliation |
| `search_events` | full text search over the prose of every event |
| `base_rates` | how common a property value is |
| `rankings` | who holds the maximum of a numeric measure |
| `breakdown` | one property grouped by another, with aggregates |
Two of these deserve emphasis, because they cover what the others cannot. **Only `search_events`
finds deeds**, which appear in no property; **only `search_properties` finds goals and
affiliations**, which appear in no event.
And `base_rates` is the one to reach for before concluding anything. A trait shared by three
notable figures looks like a pattern until you learn that half the world shares it — most properties
are recorded for only part of the objects, so the tool reports both denominators rather than letting
you assume one.
`breakdown` is the one to reach for when the question has two halves — *age at death by caste*,
*casualties by attacker race*. The other tools each read a single property, so such a question used
to be answered by pulling the objects and joining them by hand, which for the classic API means
recovering ids from HTML anchors. Do not do that: if a property needed for the join is missing, add
it as a facet in the backend instead.
Arguments are validated strictly: an unknown parameter is an error, not a silently dropped key. The
tool's cap is `maxEvents` for both detail levels, whereas the backend route behind the digest calls
it `maxNotableEvents` — passing the backend's name used to look like it worked while the server
default applied instead.
## Two habits worth having
**Ask for the digest first.** `read_object` defaults to it. The full dossier of a large civilization
can exceed 600 KB, most of it recurring festivals; the digest is around 17 KB and keeps the events
that are rare for that object.
**Filter event searches when you can.** There is no index — the text only exists once the prose has
been rendered — so an unfiltered search on a large world renders every event and takes a few
seconds. Passing `eventTypes` brings it to milliseconds.
**Drop `q` to read history rather than search it.** `search_events` with only `fromYear`/`toYear`
returns everything that happened then, which is the one question no keyword can express. It is also
the fastest call in the set: with no text to match, nothing is rendered at all — a full year of a
494,436 event world comes back in 9 ms.
## Development
```bash
npm test # unit tests, plus an in-memory MCP client exercising every tool
npm run watch # recompile on change
```
The tests use no network: the HTTP client is stubbed, and the MCP surface is driven through the
SDK's in-memory transport.
## License
MIT.
TDQS
Scored across 10 tools
Each tool targets a distinct function: world management (list/load), overview (summary/status), object access (read/search), property search, event search, and statistical analysis (rates/rankings). No two tools overlap in purpose, and the descriptions make the boundaries clear.
Most tools follow a verb_noun pattern (list_worlds, load_world, read_object, search_*), but a few use noun-only names (world_status, world_summary, base_rates, rankings). This is a minor deviation; the names are still readable and self-explanatory, but not fully consistent.
10 tools is well within the ideal 3-15 range and matches the complexity of the domain. Each tool earns its place, covering distinct workflows without redundancy.
The toolset covers the full workflow: locating worlds, loading them, getting summaries, drilling into objects, searching by name/property/event, and performing statistical analysis. There are no obvious missing operations for typical analysis tasks.