Metadata-Version: 2.5
Name: lorehub-mcp
Version: 0.4.0
Summary: MCP server that lets AI agents read and edit a LoreHub world: dialogues, lore, timeline, story map, translations, exports and stats
Project-URL: Homepage, https://lorehub.app
Project-URL: Documentation, https://lorehub.app/mcp
Author: Lostwood Games
License-Expression: MIT
License-File: LICENSE
Keywords: ai-agents,dialogue,game-development,localization,lorehub,mcp,model-context-protocol,narrative,worldbuilding
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Games/Entertainment
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: mcp<3,>=2.3
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# LoreHub MCP

<!-- mcp-name: io.github.lostwood-games/lorehub-mcp -->

An MCP server that lets AI agents (Claude, Cursor and any other MCP client) read a [LoreHub](https://lorehub.app) world: its stories and dialogues, its lore documents, its timeline, each story's route map and variables, translations, the Export Schema JSON for your engine, and counts. With a Read key it only reads. With a Write key it can also create and edit content, but it never deletes anything.

More on the [LoreHub MCP page](https://lorehub.app/mcp).

## What you can ask

Each role gets something different from the same world. Some example asks:

- **Writers**: *"Read the dialogue Tom at Grandpa's and tell me where Grandpa sounds out of character."* *"Does this scene contradict anything in the lore or the timeline?"* (the `continuity_check` prompt)
- **Lore keepers**: *"Give me an overview of the world, then list every document tagged faction."* *"What happened between 1200 and 1250, and who was involved?"*
- **Narrative designers**: *"Show me the story map of Episode 1: which links conflict, and where is has_key set and checked?"* *"Check this dialogue for dead ends and unused variables."*
- **Directors and artists**: *"Turn the Meta Areas of this dialogue into a reference brief."* *"Write a shot list for this scene."* (see [Meta Areas](#meta-areas))
- **Localisers**: *"Review the German translation of this dialogue and list what is missing or outdated."* (the `translation_review` prompt)
- **Programmers**: *"Get the export of this dialogue and write a Godot loader for it."* (the `engine_import_guide` prompt)
- **Producers**: *"How many words does each story have, how many lines does each character speak, and how far along is each translation?"*

## Install

You need a LoreHub account with at least Viewer access to the world, and [uv](https://docs.astral.sh/uv/getting-started/installation/). Nothing else to install: `uvx` downloads and runs the server.

### Create an API key

In LoreHub, open **Profile → API keys**, pick the world the key is for, give the key a name (for example "Claude Desktop"), leave its access on **Read**, choose when it expires (90 days unless you pick 30 days, 1 year or never) and select **Create key**. Copy the key, which starts with `lh_`: LoreHub shows it only once.

A key belongs to one world. LoreHub accepts it only in that world, never in other worlds, and never for your account, password, billing, members or sharing. A **Read** key only reads what your account can see. A **Write** key can also create and edit content, if your account can write in the world: pick **Write** under access when you create it. Use a Read key unless you want the agent to change your world. For another world, create another key. Each world gets a number of API calls a day from its plan (10 on Free), shared by all its members' keys. You don't put your password anywhere. If a key leaks, or you stop using it, revoke it on the same page and it stops working at once. When a key expires, LoreHub rejects it: create a new one and put it in your MCP config.

```bash
uvx lorehub-mcp
```

Or with pip: `pip install lorehub-mcp`, then run `lorehub-mcp`.

### Claude Code

```bash
claude mcp add lorehub --env LOREHUB_API_KEY=lh_your-key -- uvx lorehub-mcp
```

### Claude Desktop

Settings → Developer → Edit Config, then add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "lorehub": {
      "command": "uvx",
      "args": ["lorehub-mcp"],
      "env": {
        "LOREHUB_API_KEY": "lh_your-key"
      }
    }
  }
}
```

### Cursor

Add the same block to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):

```json
{
  "mcpServers": {
    "lorehub": {
      "command": "uvx",
      "args": ["lorehub-mcp"],
      "env": {
        "LOREHUB_API_KEY": "lh_your-key"
      }
    }
  }
}
```

Other MCP clients work the same way: run `uvx lorehub-mcp` over stdio with `LOREHUB_API_KEY` set.

### Upgrading from 0.3

0.4 adds seven read tools (`get_world_overview`, `list_documents`, `get_timeline`, `get_story_map`, `get_translations`, `get_export`, `get_world_stats`), seventeen [write tools](#writes) for Write keys, and three prompts (`continuity_check`, `translation_review`, `engine_import_guide`). The server now calls itself `lorehub` instead of `lorehub-meta`; the package is still `lorehub-mcp`, so your MCP config doesn't change. `get_dialogue` now also returns each node's `node_id`, each option's `option_id` and each link's `linkId`, for the write tools. The new tools need a LoreHub API with the 0.4 agent read endpoints (`/api/v3/worlds/{world}/overview`, `…/documents`, `…/timeline`, `…/stats`, `…/storymap`, `…/translations`, `…/export`).

### Upgrading from 0.2

0.3 adds six tools and three prompts. `get_dialogue_meta` now lists an area's nodes by short id (`node_04`) with their speaker and lines, instead of full ids, and makes 2–3 API calls instead of 4. It needs a LoreHub API with the agent read endpoints (`/api/v3/worlds/{world}/…/script`, `…/text`, `…/search`, `…/lines`).

### Upgrading from 0.1

0.1 signed in with `LOREHUB_LOGIN` and `LOREHUB_PASSWORD` (or `LOREHUB_TOKEN`). 0.2 no longer reads them. Create an API key, put it in `LOREHUB_API_KEY`, and delete the login and password from your MCP config. If that config was ever shared or synced, change your LoreHub password too.

## Find your world and dialogue ids

You don't need to look them up yourself. Ask the agent, for example *"List my LoreHub worlds, then find the dialogue Tom at Grandpa's and check it"*, and it calls `list_worlds`, `list_dialogues` and `check_dialogue` in turn. `get_world_overview` lists the stories and documents with their ids, and `search_world` finds a dialogue or character by any word in it.

To copy them by hand: open the dialogue in the Dialogue Builder. The address looks like `app.lorehub.app/<owner>/<world>/game/<story id>?dialogue=<dialogue id>`, so the dialogue id is after `?dialogue=`. The world id comes from `list_worlds`, which returns the key's world.

## Tools

The tools come in two groups: reads, which work with any key, and [writes](#writes), which need a Write key.

### Reads

Every API call counts against the world's daily limit, so every tool but two answers in one call. Each result carries `calls_left_today` when LoreHub reports it.

| Tool | API calls | What it returns |
| --- | --- | --- |
| `list_worlds()` | 1 | The key's world, with its id and your role. |
| `get_world_overview(world_id)` | 1 | The canon map: stories with their dialogue counts, documents grouped by folder and by tag, top-level documents, and counts of stories, dialogues, documents, tags and timeline events. |
| `search_world(world_id, query, limit=20)` | 1 | Matches in document names, dialogue names, dialogue lines and option texts (case-insensitive, at least 2 characters, up to 50 hits), with the dialogue, node and a snippet for lines. |
| `list_dialogues(world_id, game_id?, name?)` | 1 with `game_id`, else 1 + 1 per story | Stories and their dialogues with ids. `name` filters by dialogue name. |
| `get_dialogue(world_id, dialogue_id)` | 1 | The dialogue as a script: story, start node, and each node in reading order with its short id (`node_03`), speaker, lines, options (text, conditions, target), next links with conditions, variables it sets, meta values and the Meta Areas it is in. Also the variables and characters it uses. |
| `check_dialogue(world_id, dialogue_id)` | 2 | Nodes not reachable from the start, options without a target, nodes where the dialogue ends, variables set but never checked (or checked but never set), and a summary of the story's route map link checks. |
| `get_document(world_id, document_id)` | 1 | A document's name, parent, tags, description as Markdown-like text, relations, and the dialogues that mention it. |
| `list_documents(world_id, tag?, parent_id?, name?, limit=100)` | 1 | Documents with their parent and tags, filtered by tag, folder or name (up to 200). |
| `get_timeline(world_id, from_year?, to_year?)` | 1 | The calendar (years, months and their days) and the instant and lasting events in date order, each with a readable date and its linked documents. |
| `get_story_map(world_id, game_id)` | 1 | The story's route map (dialogues with what they require and produce, links with their check and why), its route scopes as a tree, and every variable with the dialogues and nodes that set and check it. |
| `get_character_lines(world_id, game_id, speaker_document_id?)` | 1 | Every line of a story grouped by speaker, with line and word counts, the line before each one and the node's meta values, plus the story's totals. |
| `get_translations(world_id, game_id, dialogue_id, language?, only_untranslated=false)` | 1 | The source language, each language's policy and progress, and each line and option with its key, speaker, source and per language its translation and status (translated, outdated, missing or fallback). |
| `get_export(world_id, game_id, dialogue_id)` | 1 | The dialogue's [Export Schema v2](https://lorehub.app/documentation) JSON, as the Export button gives it, with a count of each resource type. It doesn't record an export in LoreHub. |
| `get_world_stats(world_id, game_id?)` | 1 | Per story: dialogues, nodes, lines, options and words, lines and words per character, and translation progress per language. Plus the world's totals. |
| `get_dialogue_meta(world_id, dialogue_id, area?, include_nodes=true)` | 2–3 | A dialogue's Meta Areas. See [Meta Areas](#meta-areas). |

### Writes

With a Write key, the agent can create and edit a world's content through the same endpoints the LoreHub app uses, so every change goes through LoreHub's checks, plan limits and live updates (people with the dialogue open see it change). Each change shows up in LoreHub's history under the key's owner, like an edit in the app; the key's id is recorded in LoreHub's request logs. With a Read key, a write tool fails with a message saying a Write key is needed.

Agents can't delete. There are no delete tools, and no tool removes something it wasn't asked to change: `edit_node` refuses a shorter list of lines, `edit_document` only adds tags, `edit_meta_area` merges rules, and empty text is refused everywhere. Delete in LoreHub. The tools that replace a value (a name, a line, a description, a translation) are marked as destructive for MCP clients that ask before those.

The write tools take GUIDs: `node_id`, `option_id` and `linkId` from `get_dialogue`, `lineId` from `get_translations`, area ids from `get_dialogue_meta`.

| Tool | API calls | What it does |
| --- | --- | --- |
| `create_dialogue(world_id, game_id, name)` | 2 | Creates a dialogue in a story. |
| `rename_dialogue(world_id, dialogue_id, name)` | 1 | Renames a dialogue. |
| `add_node(world_id, dialogue_id, x, y, after_node_id?, speaker_document_id?, lines?)` | 1, plus 1 + 2 per speaker and line | Adds a node, optionally after another node, with a speaker and lines. |
| `edit_node(world_id, dialogue_id, node_id, lines?, speaker_document_id?)` | 1 + 1 per changed line (2 per new line) | Changes a node's speaker and lines in place. Send all its lines; new ones are added at the end. |
| `move_node(world_id, node_id, x, y)` | 1 | Moves a node on the canvas. |
| `set_node_meta(world_id, game_id, node_id, values)` | 2 + 1 per value (2 for a new one) | Sets meta values by field name, like `shot = close_up`. Other fields stay. |
| `add_option(world_id, game_id, dialogue_id, node_id, text, conditions?)` | 2, plus 1 + 1 per condition | Adds an option, optionally shown only when variables have given values. |
| `edit_option(world_id, node_id, option_id, text)` | 1 | Changes an option's text. |
| `link_nodes(world_id, game_id, dialogue_id, to_node_id, from_node_id?, from_option_id?, conditions?)` | 1, plus 1 + 1 per condition | Links a node, an option or the dialogue's start to a node, optionally with conditions. |
| `create_variable(world_id, game_id, name, default?, comment?)` | 1 | Creates a true/false story variable. |
| `set_variable_on_node(world_id, game_id, dialogue_id, node_id, variable, value?)` | 2 | Makes a node set a variable when it plays. |
| `create_document(world_id, name, description?, parent_id?, tags?)` | 1, plus 1 each for description and parent, 1 + 1–2 per tag | Creates a document. The description is simple Markdown (headings, paragraphs, lists, bold, italic). |
| `edit_document(world_id, document_id, name?, description?, parent_id?, add_tags?)` | 1 per part | Changes only what you send. A description replaces the whole text; tags are only added. |
| `relate_documents(world_id, document_id, related_document_id, relation)` | 2 | Relates two documents by a relation side name, like `Son` in Father / Son. |
| `add_meta_area(world_id, dialogue_id, title, x, y, width, height, note?, color?, game_id?, rules?)` | 1, plus 2 for rules | Draws a Meta Area. |
| `edit_meta_area(world_id, dialogue_id, area_id, …)` | 1 + 1 per part (+1 for rules) | Changes an area's text, place, size or rules. Only what you send changes; rules are merged. |
| `set_translation(world_id, line_id, language, text)` | 1 | Writes a line's translation in one language. |

### Prompts

Prompts tell the agent which tools to call, so it spends as few API calls as it can:

| Prompt | For | Arguments |
| --- | --- | --- |
| `continuity_check` | Writers, lore keepers | `world_id`, `dialogue_id`: checks a scene against the documents and the timeline. |
| `translation_review` | Localisers | `world_id`, `game_id`, `dialogue_id`, `language`: missing and outdated rows, meaning, tone and terms. |
| `engine_import_guide` | Programmers | `world_id`, `game_id`, `dialogue_id`, `engine` (Unity, Godot or Unreal): the export's shape and loader code. |
| `reference_brief` | Directors, artists | `world_id`, `dialogue_id`: per Meta Area, subject, characters, shot, background and mood. |
| `shot_list` | Directors | `world_id`, `dialogue_id`: one numbered shot per node. |
| `voice_casting_sheet` | Producers, audio | `world_id`, `game_id`: one entry per speaking character with sample lines. |

## Meta Areas

A director draws Meta Areas on the Dialogue Builder canvas: a shot, a location, a mood, with a note, a priority and meta rules such as `shot = close_up`. `get_dialogue_meta` returns each area's title, note, priority and rules as `field name: value`, the nodes inside it with their speaker and lines, and the nodes where areas overlap (higher priority first). `area` filters by title text or area id. The third API call (the story's meta schema) is made only when an area has rules. A node is inside an area when the middle of its header is, the same rule the Dialogue Builder uses. More in the [Meta Areas guide](https://lorehub.app/meta-areas-guide).

Example output:

```json
{
  "dialogue": "Tom at Grandpa's",
  "dialogue_id": "…",
  "areas": [
    {"title": "Grandpa's house: wide", "priority": 1,
     "rules": {"shot": "wide", "background": "grandpas_house"},
     "nodes": [{"id": "node_01", "speaker": "Tom", "lines": ["Where is Grandpa today?"]},
               {"id": "node_04", "speaker": "Grandpa", "lines": ["Did you find the key?"]}], "id": "…"},
    {"title": "Close-up: Tom is crying", "note": "Tears, low light", "priority": 2,
     "rules": {"shot": "close_up", "background": "grandpas_house", "tom_emotion": "crying"},
     "nodes": [{"id": "node_04", "speaker": "Grandpa", "lines": ["Did you find the key?"]}], "id": "…"}
  ],
  "overlaps": [{"node": "node_04", "areas": ["Close-up: Tom is crying (P2)", "Grandpa's house: wide (P1)"]}],
  "calls_left_today": 94
}
```

## Configuration

| Variable | Default | |
| --- | --- | --- |
| `LOREHUB_API_KEY` | | Required. Your API key from Profile → API keys: Read, or Write to let the agent change content. |
| `LOREHUB_API_URL` | `https://api.lorehub.app` | Must be `https://` and a `lorehub.app` host (plain `http://` only for `localhost`), so the key can't be sent anywhere else. |
| `LOREHUB_ALLOW_CUSTOM_API_URL` | | Set to `1` only if you run LoreHub on your own HTTPS host. |
| `LOREHUB_TIMEOUT` | `30` | Seconds per request. |
| `LOREHUB_LOG_LEVEL` | `INFO` | Logs go to stderr. |
| `LOREHUB_NODE_WIDTH`, `LOREHUB_NODE_HEADER_HEIGHT` | `200`, `32` | The canvas node card size. Leave them as they are. |

## How it works

The server calls the LoreHub API with your API key: GET for the read tools, and the app's own POST and PUT endpoints for the write tools. LoreHub has read-only endpoints shaped for agents (a dialogue's script, a document's text, the world's overview, timeline and stats, a story's lines, map and translations, a dialogue's export), so each tool needs as few calls as possible. No read writes anything: not export history, and not the empty route map or default timeline the app creates the first time you open one.

When the world's calls for the day are used up, tools fail with a clear message and the time the count resets.

Security: ids must be GUIDs, so a crafted id can't reach other API endpoints. The server doesn't follow redirects, doesn't log the key, and doesn't pass the API's error text on to the agent. Dialogue lines, names, area titles, notes, rule values, document text and translations are written by your world's collaborators, so the server tells the agent to treat them as data, not instructions.

Not yet: gallery images, document text in search, timeline events and route map edits.

## License

MIT
