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

Exchange with other apps

world_export / world_import: the cast, places, factions, relations and timeline as the neutral hoard.world/1 JSON (docs/WORLD_SCHEMA.md), keyed by ref and revision; import is idempotent, never deletes, never overwrites a record edited here and links an existing name instead of duplicating it. Writer Desktop reads and writes the same format. scene_narrate reads a turn aloud with Prospero's voice through the hub and keeps the audio with the turn; the session view gets a play button

Secrets, images, facts, threads, clocks and sessions never leave in hoard.world/1; narration needs the hub and Prospero's Hoard running

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

world_export(world_id)

yes

The world's cast, places, factions, relations and timeline as hoard.world/1 JSON

world_import(data, world_id=None)

no

Merge a hoard.world/1 document into a world, idempotently, never deleting

scene_narrate(session_id, turn=None, ...)

no

Read a turn aloud with Prospero's voice; the audio is kept with the turn

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" }
    }
  }
}

Faustus assistant

Once the shared Hoard Link launcher is installed, the app shows the family's Faustus panel (a floating button). The conversation, the model and the tools live in Faustus; this app only tells the panel what is open.

  • Context per view. The panel receives the section and the objects open in it, with real ids: world (genre, tone, ruleset, language, premise cut to 240 characters), entity (kind, status, aliases, tags, summary cut to 240; always read without the game master's secrets), session, up to 8 open or advanced thread objects, up to 6 clock objects and, in play, the scene's location and up to 8 entities present. Creation forms (new) give no object.

  • Unsaved fields. The panel asks for them with the scheherazade:context-request window event. The forms scene-form, entity-form, session-form, turn-draft, entity-new, world-new, session-new, thread-new, clock-new, table-new and timeline-new answer with the fields typed so far. Secrets are stripped from every snapshot, whatever the form holds.

  • Refresh. When the assistant finishes a turn the open view reloads in place (hoard:copilot-turn-end / window.hoardCopilot.onTurnEnd) and the forms you are editing keep what you typed.

  • Saving. Forms send only the fields you changed. PATCH /api/worlds/{id}/entities/{ref} leaves every field that is not sent untouched, clears one sent empty, and fields_patch merges single keys into the entity's fields; world and session updates behave the same. The secrets travel only when the editor shows them.

  • Tests. node --test frontend/copilot-context.test.mjs and python -m pytest tests/test_partial_updates.py tests/test_frontend_source.py.

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. The same vendored library also provides the request guard, the bearer tokens, the atomic write of backend.json, the accent-folding and the FTS query builder used by the world search.

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 shared request guard and the token on the tool routes, atomic settings, 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 (the shared Hoard Link guard; SCHEHERAZADE_ALLOWED_HOSTS opens a LAN or tailnet name on purpose), asks the assistant's tool routes for the bearer token in data/mcp-token, 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.

Maintenance

ActivityMaintained
ResponsivenessNo issues

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.
    13
    1
    -