Metadata-Version: 2.5
Name: lorehub-mcp
Version: 0.3.0
Summary: MCP server that lets AI agents read LoreHub dialogues, documents and Meta Areas
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,lorehub,mcp,model-context-protocol,narrative
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: dialogues as scripts, the **Meta Areas** a director marked, documents such as characters and places, a search across the world, and a story's lines by speaker.

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`. With this server, your agent reads those areas, the lines inside them and the characters who speak them, and turns them into references, shot lists, mood boards or voice casting sheets. It only reads: it never changes your world.

More on the [LoreHub MCP page](https://lorehub.app/mcp) and in the [Meta Areas guide](https://lorehub.app/meta-areas-guide).

## 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 Read key belongs to one world. LoreHub accepts it only for reading what your account can see in that world, never other worlds, never to change anything, and never for your account, password or billing. This server only reads, so it needs no Write key. 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.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 give me its Meta Areas"*, and it calls `list_worlds`, `list_dialogues` and `get_dialogue_meta` in turn. `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

Every API call counts against the world's daily limit, so most tools answer 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. |
| `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. |
| `get_dialogue_meta(world_id, dialogue_id, area?, include_nodes=true)` | 2–3 | Each Meta Area's title, note, priority and meta rules as `field name: value`, the nodes inside it with speaker and lines, and the nodes where areas overlap (higher priority first). `area` filters by title text or area id. The third call (the story's meta schema) is made only when an area has rules. |
| `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. |
| `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. |
| `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. |
| `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. |

Prompts: `reference_brief(world_id, dialogue_id)` (per Meta Area: subject, characters, shot, background, mood), `shot_list(world_id, dialogue_id)` and `voice_casting_sheet(world_id, game_id)`. Each tells the agent which tools to call, so it spends as few API calls as it can.

Example output of `get_dialogue_meta`:

```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 read-only API key from Profile → API keys. |
| `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 makes GET calls to the LoreHub API with your API key. LoreHub has read-only endpoints shaped for agents (a dialogue's script, a document's text, a search, a story's lines), so each tool needs as few calls as possible; none of them writes anything, not even export history. A node is inside a Meta Area when the middle of its header is, the same rule the Dialogue Builder uses, and LoreHub works that out for the script.

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, area titles, notes, rule values and document text are written by your world's collaborators, so the server tells the agent to treat them as data, not instructions.

Not yet: gallery images, translations, timelines and exports.

## License

MIT
