gamebook
# Gamebook
A minimal choose-your-own-adventure system with a FastAPI book backend and a
Python MCP server that exposes the story to an LLM.
## Run it
Play the adventure directly in a terminal. This starts the backend automatically
if it is not already running:
```bash
uv run gamebook-play
```
Select a story with `--story`; the current observatory adventure is the default:
```bash
uv run gamebook-play --story observatory
uv run gamebook --story observatory
uv run gamebook-backend --story observatory
```
Each built-in story is defined in a YAML file under `src/gamebook/stories` and
registered in `gamebook.story.STORY_MODULES`.
## Story YAML
Each story file has a `start_scene_id` and a `scenes` mapping. A scene defines
its narrative text, `items`, optional `stateful_items`, and outgoing `edges`.
The loader validates the YAML and converts it into the same NetworkX graph and
runtime models used by the game engine.
```yaml
start_scene_id: hall
scenes:
hall:
title: Hall
context: A locked door leads into the library.
hint: The door needs a key.
items: []
stateful_items:
- id: library_door
name: Door
is_takeable: false
initial_state: Locked
descriptions:
Locked: The Door is Locked.
Unlocked: The Door is Unlocked.
edges:
- id: library_door
destination_scene_id: library
stateful_item_id: library_door
choice: {id: library, description: Enter the library.}
rule:
required_item_states:
- {item_id: library_door, state: Unlocked}
bidirectional: true
reverse:
choice: {id: hall, description: Return to the hall.}
library:
title: Library
context: Dusty shelves fill the room.
hint: The shelves are worth examining.
items: []
```
State predicates use `{item_id, state}` mappings. Stateful-item transitions use
`{from, to}`, and an item-use conditional rule can set states and replace the
item's normal `action_message` when its required states match.
Use story-level `stateful_item_groups` for rules that depend on a combination of
stateful items, including items in different scenes. Rules are evaluated in order after a group member changes;
the first matching rule applies its effects and optional `action_message`.
Stories use a NetworkX `MultiDiGraph`. Each graph node stores a `StoryNode`
containing the scene text and items, while each directed edge stores the choice,
visibility conditions, traversal rules, and state changes for moving to another
scene. A multigraph allows multiple distinct choices to connect the same pair of
scenes.
Each `StoryNode` declares its outgoing `StoryEdge` values. `create_story`
validates those declarations and builds the graph, expanding a bidirectional
edge into reciprocal directed edges. The reciprocal edges share one stable edge
ID; when supplied with a non-takeable `StatefulItem`, its mutable state is stored
once per adventure and is therefore visible from both connected scenes. The
graph remains a shared story template and never stores player session state.
Enter a choice number to move through the story. Other commands include
`use <item> [on <target>]`, `examine <item>`, `take <item>`, `drop <item>`, `inventory`, `look`,
`hint`, `hint direct`,
`restart`, and `quit`.
Set `requires_target: true` on an item to require the `on <target>` form.
Start the book backend and Streamable HTTP MCP server together:
```bash
uv run gamebook
```
## Docker
Build the image and start both the MCP server and backend:
```bash
make build
make run
```
The MCP server is available at `http://localhost:8000/mcp`; the backend is
available at `http://localhost:8001`. Set `STORY` to choose a registered story,
for example `make run STORY=observatory`.
Start the terminal player in a container with:
```bash
make play
```
The MCP endpoint is `http://localhost:8000/mcp`. It listens on all WSL network
interfaces so applications running on Windows can connect to it. The backend
listens internally at `http://127.0.0.1:8001`; its interactive API docs are at
`http://127.0.0.1:8001/docs`.
Configure LM Studio's `mcp.json` to connect to the already-running server:
```json
{
"mcpServers": {
"gamebook": {
"url": "http://localhost:8000/mcp"
}
}
}
```
VS Code is configured separately to launch `gamebook-mcp` over stdio from
`.vscode/mcp.json`. Open the MCP Servers view or run **MCP: List Servers**, then
start `gamebook` to use or debug it. The HTTP backend must also be running for
tools launched this way.
For development, the components can also run separately:
```bash
uv run gamebook-backend
uv run gamebook-mcp
```
Point the MCP bridge at another backend by setting `GAMEBOOK_BACKEND_URL`.
## MCP tools
- `begin_adventure` starts a session and returns the opening scene.
- `choose_action` sends one of the offered choice IDs to the backend.
- `use` uses an inventory item or a stateful item in the current scene.
- `examine` describes an inventory item or an item in the current scene.
- `look_around` returns the current scene and items present there.
- `request_hint` asks the backend for a subtle or direct clue.
- `check_inventory` returns the player's health and inventory.
- `take_item` moves an item from the current place into the player's inventory.
- `drop_item` moves an inventory item into the player's current place.
Run the tests with `uv run pytest`.TDQS
Scored across 9 tools
Most tools map cleanly to distinct actions: starting, choosing, examining, looking, using, taking, dropping, hinting, and checking status. Minor overlap exists between look_around and examine since both inspect the current environment, but their intended purposes remain distinguishable.
The naming pattern is mostly verb_noun with clear action-oriented names like begin_adventure, take_item, and drop_item. The bare verbs 'use' and 'examine' deviate slightly from the pattern, but they are still intuitive and readable.
Nine tools is well-scoped for a gamebook server, covering the core interaction loop without unnecessary bloat. Each tool serves a clear gameplay purpose, and the count feels appropriate for the domain.
The tool set covers the core interactive fiction lifecycle: starting, choosing, looking, examining, using, taking, dropping, hinting, and status tracking. A minor gap is the lack of a dedicated way to refetch the current scene's available actions without beginning or choosing, which could leave agents stuck if context is lost.