Skip to main content
Glama

Scheherazade's Hoard

Who's alive? Who's where? What have you promised them?

A world-state and continuity keeper for interactive fiction and tabletop play — it remembers everything a language model forgets, and hands the narrator exactly the slice it needs for the next scene.

Español · Quick start · Connect to Faustus · MCP reference · Portfolio

The Play screen, mid-scene, with the dice tray and threads/clocks panel The real application with synthetic demo data ("El Archipiélago de Sal", an original setting seeded by --demo).

Why

Language models are good narrators and terrible continuity editors. After an hour of interactive fiction or a tabletop session they forget who knows what, resurrect dead characters, move towns, lose the plot threads and fudge dice. Scheherazade keeps the world state outside the model — entities, relationships, secrets, places, a chronology, open threads, clocks, tables and an audited dice log — and builds the narrator a compact, ranked, budget-bound brief for the next scene, then records what that scene actually changed. It can narrate on its own with a shared local model, or hand its tools to another AI (Faustus) so that AI narrates while this app remembers everything.

Related MCP server: Timelines MCP Server

What is implemented

Area

Available now

Boundary

World state

Entities (character/location/faction/item/lore/creature) with aliases, stats and GM secrets, relations, facts with provenance and a canon flag, timeline, threads, clocks, random tables; SQLite + FTS5 with accent-insensitive search

One local writer; no multi-user editing

Dice

NdM, +/-, kh/kl/dh/dl, exploding !, adv(d20)/dis(d20), dF; seeded reproducibility; every roll in an append-only log; with a world, 2d6 rolls get their PbtA band and d20 rolls their natural value and crit

Bounded on purpose (200 dice per roll, 100 characters per expression)

Context builder

world_context(): premise, content boundaries, current scene and cast, ranked facts, live threads, clocks at least half full and recent turns, each line with a citable id, within a character budget

Ranking is lexical and rule-based, not embedding search

Delta engine

Validation per item, then the delta and its turn in one SQLite transaction; the scene carries over between turns; undo walks back turn by turn within the current session

No redo; turns of an earlier session cannot be undone

Narrator (standalone)

Builds the prompt, calls the shared model through Hoard Link, separates narration from the delta (fenced or bare JSON; repairs typographic quotes, comments, trailing commas, a delta nested under "delta" and a reply cut at the token limit, keeping the complete items), never leaves JSON in the story, and lets you accept or reject each proposed change

Not streamed: one reply, with a spinner

Consistency check

Rules for a dead character acting, a character placed away from where they were last seen and a contradicted relation (whole-word, alias-aware), plus an LLM judge over matching facts that must cite fact ids; the result says whether the judge ran

Heuristics: it misses contradictions that no rule covers and no fact states

Export

Session as a manuscript-clean Markdown chapter (no ids, dice or out-of-character lines; actions in italics; Spanish raya dialogue), optionally polished by the shared model (falls back to the plain chapter and says why), world bible in Markdown, full JSON export and import

Polish is instructed not to add facts but is not checked against the source; chapters over 6000 characters are not polished; an imported world keeps its sessions and turns but not their undo history

Illustrations

"Illustrate" calls Prospero's Hoard's agent API when it answers on 127.0.0.1:8815; the button is hidden otherwise

Needs that separate app; the image is shown, not stored on the turn

Shared model backend

Hoard Link vendored unmodified (scheherazades_hoard/hoard_link/): explicit settings, then Faustus's model registry, then resident models on loopback (llama.cpp, Ollama, OpenAI-compatible); Settings shows the reason, can clear overrides and never returns the token

Only the language-model capability is used; the app never loads a model itself

UI

Play (Narration/Action/Dialogue/Out-of-character modes and a scene editor for place, cast and mood), Bible (create and edit: status, summary, aliases, tags, secrets), Map of relations (SVG), Timeline, Threads & clocks (kanban and segmented clocks), Tables, Sessions (start, rename, export), Dice log, Backends, Assistant activity, Settings; a continuity check box in Play; accent-insensitive Bible search; Spanish and English, light and dark

The map uses a fixed circular layout, not a physics simulation

Use cases

Eight scenarios for a solo player who also writes, each walked in the browser and over MCP before and after the usability review (docs/USE_CASES.md; findings and a verdict per use case in docs/USABILITY_REPORT.md). Among them:

  • First evening without a model: create a world, its people and places, set the scene and play it by hand, dice included.

  • A long night narrated by a local model through Faustus: 44 beats in Spanish where the dead stay dead, people stay where they were last seen and a bad turn can be taken back.

  • A clean chapter for the manuscript: the session exported as prose and raya dialogue, without ids, dice or out-of-character lines.

  • The app narrates on its own: a 27B model's sloppy JSON is repaired and every proposed change is reviewed before it touches the world.

  • Continuity questions while writing: "who has died?", "what does Iria know?", "is it consistent that Mateo opens the door?", answered in one or two tool calls, asked in Spanish.

  • The next evening, fixing by hand: correct a summary, mark someone missing, tick a clock, undo a turn — no model needed.

Connect it to Faustus

The app declares itself with faustus-plugin.json. Start the app, then in Faustus: Connectors → Nearby apps → Add. Faustus finds it by scanning loopback ports and reading that manifest, which also tells it how to start the app (python -m scheherazades_hoard --no-browser, ready when /api/health answers) and how to launch the MCP server (python scheherazades_hoard/mcp_server.py over stdio, with SCHEHERAZADE_URL pointing at the running app).

Two ways to play, same world: connected, Faustus is the narrator and uses the tools below (the narrator-loop skill tells it in which order); on its own, the app narrates with the model Faustus already has loaded, found through Hoard Link, so nothing is loaded twice.

Tool

Read-only

What it does

story_worlds()

yes

List every world with counts and current session

story_world_create(...)

no

Create a new world

world_context(world, ...)

yes

The narrator's brief for the next scene

world_search(world, query, ...)

yes

Search entities/facts

entity_get(world, ref, ...)

yes

One entity with relations and facts

entity_upsert(world, kind, name, ...)

no

Create or update an entity

story_append(world, text, ...)

no

Record a turn and apply a delta, atomically

dice_roll(expression, ...)

no

Roll dice with an audited log

table_roll(world, table)

no

Roll on a random table

thread_update(world, thread, ...)

no

Advance/resolve/abandon a thread

clock_tick(world, clock, ticks=1)

no

Advance a clock

world_check(world, statement)

yes

Check a statement against established facts

session_start(world, title="")

no

Start a new, optionally named session

session_rename(world, session, title)

no

Rename a session

session_export(world, ...)

yes

Export a chapter / the bible / full JSON, a page at a time

story_undo(world)

no

Revert the last turn and everything its delta changed

Full argument lists, return shapes and limits: docs/MCP.md.

It also works with any other MCP client over stdio (on Linux/macOS the interpreter is .venv/bin/python):

{
  "mcpServers": {
    "scheherazade": {
      "command": "/absolute/path/to/scheherazades-hoard/.venv/Scripts/python.exe",
      "args": ["/absolute/path/to/scheherazades-hoard/scheherazades_hoard/mcp_server.py"],
      "env": { "SCHEHERAZADE_URL": "http://127.0.0.1:8816" }
    }
  }
}

Quick start

Windows

Double-click Iniciar Scheherazade's Hoard.cmd. It runs scripts/start.ps1, which finds Python 3.11 or newer, creates .venv and installs requirements-lock.txt (again whenever the lock changes), builds the interface if frontend/dist is missing (Node 22 is needed only then), starts the app with the repo root as working directory, waits for /api/health and opens the browser. If the app is already running it only opens the browser. Stop it with Detener Scheherazade's Hoard.cmd (scripts/stop.ps1), which also stops an instance Faustus started.

Manual steps, from a fresh clone, in PowerShell:

git clone https://github.com/Luissalet/ScheherazadesHoard.git
cd ScheherazadesHoard
python -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements-lock.txt
cd frontend; npm ci; npm run build; cd ..
.venv\Scripts\python.exe -m scheherazades_hoard --demo

Linux / macOS

The app itself is plain Python + a static frontend build, so it runs the same way outside Windows (only the .cmd launchers and scripts/*.ps1 are Windows-only):

git clone https://github.com/Luissalet/ScheherazadesHoard.git
cd ScheherazadesHoard
python3 -m venv .venv
.venv/bin/python -m pip install -r requirements-lock.txt
cd frontend && npm ci && npm run build && cd ..
.venv/bin/python -m scheherazades_hoard --demo

--demo uses a synthetic seeded world in data-demo/ instead of your real data/, so you can try everything without touching (or needing) any real data. Drop --demo for your own worlds; add --no-browser to skip the automatic tab.

The app never starts or loads a model of its own. It uses Hoard Link, vendored byte-identical in scheherazades_hoard/hoard_link/ (version in VENDORED.txt), to find the language model that is already running: explicit settings from the Settings screen or HOARD_* environment variables first, then the model Faustus already uses, then resident models on loopback (llama.cpp, Ollama, any OpenAI-compatible server). When nothing answers, Settings says why and everything except "Narrate", "Polish" and the continuity judge keeps working.

Architecture

flowchart LR
  UI["React UI<br/>(frontend/dist)"] -->|REST| API["FastAPI app<br/>127.0.0.1:8816"]
  AI["Faustus or any<br/>MCP client"] -->|stdio| MCP["mcp_server.py"]
  MCP -->|loopback HTTP<br/>/api/agent/*| API
  API --> Core["context · delta · consistency<br/>dice · export"]
  Core --> DB[("SQLite + FTS5<br/>data/")]
  API -->|Hoard Link| LLM["Local model server"]
  API -.->|optional| P["Prospero's Hoard<br/>(illustrations)"]

Modules, data model, the atomic delta engine, and the decisions behind them: docs/ARCHITECTURE.md.

Development

.venv\Scripts\python.exe -m pip install -r requirements-lock.txt
.venv\Scripts\python.exe -m pytest tests/ -q
cd frontend; npm ci; npm run build
.venv/bin/python -m pip install -r requirements-lock.txt
.venv/bin/python -m pytest tests/ -q
cd frontend && npm ci && npm run build

The same commands run in CI (.github/workflows/ci.yml: Ubuntu, Python 3.12, Node 22).

294 tests, offline (no real model: Hoard Link, the narrator and the Prospero adapter run against httpx.MockTransport), in well under a minute. They cover the dice grammar and its bounds, ruleset readings, the context builder's budget, ranking and secret exclusion, delta validation, the single-transaction turn and undo (including repeated updates in one delta), scene carry-over, JSON extraction including the sloppy shapes a small model sends, first-name entity lookup, the consistency rules, accent-insensitive and status search, the manuscript-clean chapter, exports and import, the static file server against path traversal, the Host/Origin guard, the CLI start with its pid file and log, and an MCP protocol test that spawns the real adapter over stdio against a live instance (list_tools, annotations, Keywords lines, and a create, roll, append, context and undo round trip), plus a check that a picker reading only line one of each tool description finds the right tool for Spanish requests.

npm run build (inside frontend/) runs tsc -b && vite build with TypeScript strict mode, noUnusedLocals and noUnusedParameters on.

Privacy and security

  • Binds 127.0.0.1 only, rejects other Host headers and cross-site writes, and refuses to be framed by web pages. No telemetry. The only network calls are to model servers on your machine (or the Faustus you configured) and to Prospero's Hoard, both on loopback.

  • Your worlds live in data/ (gitignored) as one SQLite file; the log is data/logs/app.log and records tool names and timings, never story text, secrets or tokens.

  • Agent tools never return GM secrets unless asked with include_secrets=true; every agent call is written to an audit table (agent_calls: tool, ok/error, duration) that "Assistant activity" shows, and your own clicks in the interface are kept out of it.

  • The narrator's structured output is parsed from free text; a reply that cannot be parsed is kept as narration marked unparsed, never guessed at.

Roadmap / known limits

Open items from the project's own design notes (docs/USABILITY_REPORT.md), roughly in order of value:

  • world_context returns about 1.5 times its character budget, because the scene, lore, threads and recent_turns fields repeat what the brief already says.

  • An agent cannot create a clock or add a standalone relation/fact outside of a turn; both need the HTTP route today.

  • "Where was X last seen" has no direct tool answer outside world_check.

  • story_append over MCP has no rolls argument, so a roll the model makes is logged but not linked to its turn.

  • thread_update needs a thread's id or full title; part of a title does not match.

  • The proposal card in Play shows facts, updates and clock ticks but not a proposed scene move, cast or mood change.

  • The relations map uses a fixed circular layout that overlaps labels once a world has 30+ entities, and does not visually distinguish dead or missing entities.

  • Below 760px there is no way to open the navigation sidebar.

  • Play always opens at the top of the transcript, so a long session needs a scroll to reach the latest turn.

  • A JSON world backup over MCP pages a large string through the model's context; importing through the UI does not have this limit.

  • Some messages are still English-only in an otherwise Spanish session: delta rejections ("… is dead and cannot act"), raw roll bands (weak_hit), status badges and some Backends reasons.

  • world_check's dead-acting rule has a false positive when a dead character is only mentioned, not acting.

  • The polished-chapter route does not check the model's reply for length or truncation before it can replace the original chapter, and refuses to polish chapters over 6000 characters.

None of these block using the app today — see the verdict per use case in docs/USABILITY_REPORT.md.

License

MIT — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables creation and management of structured game worlds for text adventures and RPGs with character creation, world generation, and natural language interaction through AI integration.
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Helps LLMs maintain coherent timelines, track events, and manage characters across long-form narratives for fiction writing or historical accounts.
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A narrative graph engine that enables LLMs to generate, track, and mutate complex fictional worlds while maintaining consistency between factions, characters, and locations. It acts as a specialized RAG framework for storytelling, allowing models to manage thousands of entities without exceeding context limits.
    MIT
  • F
    license
    A
    quality
    C
    maintenance
    Gives an AI game master a persistent memory for tabletop roleplaying games by storing worlds, characters, plots, and scenes. It enables saving, searching, and loading game state (via MCP tools) so the GM doesn't have to recall plot and details only from the context window.
    2
    13
    1
    -