dndkit 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. dndkit-0.4.0/LICENSE +22 -0
  2. dndkit-0.4.0/MANIFEST.in +2 -0
  3. dndkit-0.4.0/PKG-INFO +567 -0
  4. dndkit-0.4.0/README.md +535 -0
  5. dndkit-0.4.0/pyproject.toml +87 -0
  6. dndkit-0.4.0/setup.cfg +4 -0
  7. dndkit-0.4.0/src/dndkit/__init__.py +10 -0
  8. dndkit-0.4.0/src/dndkit/__main__.py +6 -0
  9. dndkit-0.4.0/src/dndkit/activity.py +68 -0
  10. dndkit-0.4.0/src/dndkit/app.py +105 -0
  11. dndkit-0.4.0/src/dndkit/config.py +104 -0
  12. dndkit-0.4.0/src/dndkit/errors.py +48 -0
  13. dndkit-0.4.0/src/dndkit/game/__init__.py +1 -0
  14. dndkit-0.4.0/src/dndkit/game/areas.py +181 -0
  15. dndkit-0.4.0/src/dndkit/game/battlefield.py +154 -0
  16. dndkit-0.4.0/src/dndkit/game/bestiary.py +155 -0
  17. dndkit-0.4.0/src/dndkit/game/characters.py +227 -0
  18. dndkit-0.4.0/src/dndkit/game/combat.py +209 -0
  19. dndkit-0.4.0/src/dndkit/game/combatants.py +188 -0
  20. dndkit-0.4.0/src/dndkit/game/concentration.py +43 -0
  21. dndkit-0.4.0/src/dndkit/game/dice.py +186 -0
  22. dndkit-0.4.0/src/dndkit/game/effects.py +183 -0
  23. dndkit-0.4.0/src/dndkit/game/grid.py +88 -0
  24. dndkit-0.4.0/src/dndkit/game/journal.py +47 -0
  25. dndkit-0.4.0/src/dndkit/game/movement.py +264 -0
  26. dndkit-0.4.0/src/dndkit/game/pathing.py +153 -0
  27. dndkit-0.4.0/src/dndkit/game/proficiencies.py +104 -0
  28. dndkit-0.4.0/src/dndkit/game/progression.py +77 -0
  29. dndkit-0.4.0/src/dndkit/game/readiness.py +214 -0
  30. dndkit-0.4.0/src/dndkit/game/reference.py +123 -0
  31. dndkit-0.4.0/src/dndkit/game/registry.py +172 -0
  32. dndkit-0.4.0/src/dndkit/game/resources.py +96 -0
  33. dndkit-0.4.0/src/dndkit/game/rests.py +98 -0
  34. dndkit-0.4.0/src/dndkit/game/rules.py +196 -0
  35. dndkit-0.4.0/src/dndkit/game/seating.py +71 -0
  36. dndkit-0.4.0/src/dndkit/game/sight.py +142 -0
  37. dndkit-0.4.0/src/dndkit/game/spellbook.py +97 -0
  38. dndkit-0.4.0/src/dndkit/game/tables.py +216 -0
  39. dndkit-0.4.0/src/dndkit/game/world.py +125 -0
  40. dndkit-0.4.0/src/dndkit/models/__init__.py +69 -0
  41. dndkit-0.4.0/src/dndkit/models/base.py +26 -0
  42. dndkit-0.4.0/src/dndkit/models/battle.py +40 -0
  43. dndkit-0.4.0/src/dndkit/models/bestiary.py +56 -0
  44. dndkit-0.4.0/src/dndkit/models/campaign.py +209 -0
  45. dndkit-0.4.0/src/dndkit/models/character.py +149 -0
  46. dndkit-0.4.0/src/dndkit/models/world.py +62 -0
  47. dndkit-0.4.0/src/dndkit/skills/__init__.py +14 -0
  48. dndkit-0.4.0/src/dndkit/skills/catalog.py +113 -0
  49. dndkit-0.4.0/src/dndkit/skills/dnd-battle-map/SKILL.md +78 -0
  50. dndkit-0.4.0/src/dndkit/skills/dnd-game-master/SKILL.md +125 -0
  51. dndkit-0.4.0/src/dndkit/skills/dnd-monsters/SKILL.md +85 -0
  52. dndkit-0.4.0/src/dndkit/skills/extension.py +93 -0
  53. dndkit-0.4.0/src/dndkit/storage/__init__.py +6 -0
  54. dndkit-0.4.0/src/dndkit/storage/catalog.py +72 -0
  55. dndkit-0.4.0/src/dndkit/storage/confirmations.py +54 -0
  56. dndkit-0.4.0/src/dndkit/storage/files.py +55 -0
  57. dndkit-0.4.0/src/dndkit/storage/sharing.py +67 -0
  58. dndkit-0.4.0/src/dndkit/storage/store.py +278 -0
  59. dndkit-0.4.0/src/dndkit/text.py +13 -0
  60. dndkit-0.4.0/src/dndkit/tools/__init__.py +47 -0
  61. dndkit-0.4.0/src/dndkit/tools/apps.py +47 -0
  62. dndkit-0.4.0/src/dndkit/tools/battle.py +117 -0
  63. dndkit-0.4.0/src/dndkit/tools/bestiary.py +121 -0
  64. dndkit-0.4.0/src/dndkit/tools/campaign.py +208 -0
  65. dndkit-0.4.0/src/dndkit/tools/characters.py +189 -0
  66. dndkit-0.4.0/src/dndkit/tools/combat.py +169 -0
  67. dndkit-0.4.0/src/dndkit/tools/deps.py +42 -0
  68. dndkit-0.4.0/src/dndkit/tools/journal.py +102 -0
  69. dndkit-0.4.0/src/dndkit/tools/locations.py +98 -0
  70. dndkit-0.4.0/src/dndkit/tools/npcs.py +101 -0
  71. dndkit-0.4.0/src/dndkit/tools/play.py +118 -0
  72. dndkit-0.4.0/src/dndkit/tools/players.py +44 -0
  73. dndkit-0.4.0/src/dndkit/tools/progression.py +41 -0
  74. dndkit-0.4.0/src/dndkit/tools/quests.py +95 -0
  75. dndkit-0.4.0/src/dndkit/tools/recorder.py +63 -0
  76. dndkit-0.4.0/src/dndkit/tools/render.py +281 -0
  77. dndkit-0.4.0/src/dndkit/tools/render_combat.py +121 -0
  78. dndkit-0.4.0/src/dndkit/tools/render_map.py +141 -0
  79. dndkit-0.4.0/src/dndkit/tools/resources.py +104 -0
  80. dndkit-0.4.0/src/dndkit/tools/schema.py +47 -0
  81. dndkit-0.4.0/src/dndkit/tools/screen.py +47 -0
  82. dndkit-0.4.0/src/dndkit/tools/spells.py +67 -0
  83. dndkit-0.4.0/src/dndkit/tools/utility.py +42 -0
  84. dndkit-0.4.0/src/dndkit/ui/__init__.py +52 -0
  85. dndkit-0.4.0/src/dndkit/ui/backstage.js +41 -0
  86. dndkit-0.4.0/src/dndkit/ui/battlemap.js +70 -0
  87. dndkit-0.4.0/src/dndkit/ui/character.html +23 -0
  88. dndkit-0.4.0/src/dndkit/ui/combat.html +18 -0
  89. dndkit-0.4.0/src/dndkit/ui/components.css +283 -0
  90. dndkit-0.4.0/src/dndkit/ui/components.js +261 -0
  91. dndkit-0.4.0/src/dndkit/ui/desk.js +91 -0
  92. dndkit-0.4.0/src/dndkit/ui/dice.html +42 -0
  93. dndkit-0.4.0/src/dndkit/ui/dicetray.js +99 -0
  94. dndkit-0.4.0/src/dndkit/ui/gm.html +59 -0
  95. dndkit-0.4.0/src/dndkit/ui/party.html +18 -0
  96. dndkit-0.4.0/src/dndkit/ui/player.html +61 -0
  97. dndkit-0.4.0/src/dndkit/ui/seating.html +18 -0
  98. dndkit-0.4.0/src/dndkit/ui/shared.css +195 -0
  99. dndkit-0.4.0/src/dndkit/ui/shared.js +242 -0
  100. dndkit-0.4.0/src/dndkit/ui/sheet.js +75 -0
  101. dndkit-0.4.0/src/dndkit/ui/table.html +41 -0
  102. dndkit-0.4.0/src/dndkit/ui/words.js +120 -0
  103. dndkit-0.4.0/src/dndkit/ui/world.js +56 -0
  104. dndkit-0.4.0/src/dndkit/views/__init__.py +7 -0
  105. dndkit-0.4.0/src/dndkit/views/backstage.py +49 -0
  106. dndkit-0.4.0/src/dndkit/views/battle.py +92 -0
  107. dndkit-0.4.0/src/dndkit/views/desk.py +76 -0
  108. dndkit-0.4.0/src/dndkit/views/everyone.py +194 -0
  109. dndkit-0.4.0/src/dndkit/views/public.py +89 -0
  110. dndkit-0.4.0/src/dndkit/views/widgets.py +58 -0
  111. dndkit-0.4.0/src/dndkit/views/world.py +70 -0
  112. dndkit-0.4.0/src/dndkit/web/__init__.py +10 -0
  113. dndkit-0.4.0/src/dndkit/web/access.py +136 -0
  114. dndkit-0.4.0/src/dndkit/web/audiences.py +111 -0
  115. dndkit-0.4.0/src/dndkit/web/changes.py +84 -0
  116. dndkit-0.4.0/src/dndkit/web/feed.py +112 -0
  117. dndkit-0.4.0/src/dndkit/web/routes.py +134 -0
  118. dndkit-0.4.0/src/dndkit/web/server.py +131 -0
  119. dndkit-0.4.0/src/dndkit.egg-info/PKG-INFO +567 -0
  120. dndkit-0.4.0/src/dndkit.egg-info/SOURCES.txt +151 -0
  121. dndkit-0.4.0/src/dndkit.egg-info/dependency_links.txt +1 -0
  122. dndkit-0.4.0/src/dndkit.egg-info/entry_points.txt +2 -0
  123. dndkit-0.4.0/src/dndkit.egg-info/requires.txt +18 -0
  124. dndkit-0.4.0/src/dndkit.egg-info/top_level.txt +1 -0
  125. dndkit-0.4.0/tests/__init__.py +0 -0
  126. dndkit-0.4.0/tests/conftest.py +30 -0
  127. dndkit-0.4.0/tests/fixtures/legacy_campaign.json +79 -0
  128. dndkit-0.4.0/tests/helpers.py +39 -0
  129. dndkit-0.4.0/tests/test_apps.py +105 -0
  130. dndkit-0.4.0/tests/test_battle.py +204 -0
  131. dndkit-0.4.0/tests/test_battle_effects.py +137 -0
  132. dndkit-0.4.0/tests/test_bosses.py +86 -0
  133. dndkit-0.4.0/tests/test_concentration.py +79 -0
  134. dndkit-0.4.0/tests/test_deletion.py +102 -0
  135. dndkit-0.4.0/tests/test_desk.py +92 -0
  136. dndkit-0.4.0/tests/test_dice_and_rules.py +75 -0
  137. dndkit-0.4.0/tests/test_game.py +183 -0
  138. dndkit-0.4.0/tests/test_levels_and_life.py +132 -0
  139. dndkit-0.4.0/tests/test_live_fixes.py +123 -0
  140. dndkit-0.4.0/tests/test_more_tools.py +99 -0
  141. dndkit-0.4.0/tests/test_names_and_fights.py +128 -0
  142. dndkit-0.4.0/tests/test_proficiencies.py +107 -0
  143. dndkit-0.4.0/tests/test_readiness.py +116 -0
  144. dndkit-0.4.0/tests/test_release.py +72 -0
  145. dndkit-0.4.0/tests/test_rules_and_table.py +155 -0
  146. dndkit-0.4.0/tests/test_seating.py +108 -0
  147. dndkit-0.4.0/tests/test_skills.py +88 -0
  148. dndkit-0.4.0/tests/test_srd.py +328 -0
  149. dndkit-0.4.0/tests/test_storage.py +175 -0
  150. dndkit-0.4.0/tests/test_tools.py +165 -0
  151. dndkit-0.4.0/tests/test_web.py +232 -0
  152. dndkit-0.4.0/tests/test_web_views.py +168 -0
  153. dndkit-0.4.0/tests/web_helpers.py +92 -0
dndkit-0.4.0/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Joel Casimir and contributors
4
+ Copyright (c) 2026 Sylvain Boily (dndkit)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ graft tests
2
+ global-exclude __pycache__ *.py[cod] .coverage
dndkit-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,567 @@
1
+ Metadata-Version: 2.4
2
+ Name: dndkit
3
+ Version: 0.4.0
4
+ Summary: A D&D 5e campaign management MCP server, built for voice game masters
5
+ Author: Sylvain Boily, Joel Casimir
6
+ Maintainer: Sylvain Boily
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/sboily/dndkit
9
+ Project-URL: Changelog, https://github.com/sboily/dndkit/blob/main/CHANGELOG.md
10
+ Project-URL: Upstream, https://github.com/study-flamingo/gamemaster-mcp
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: fastmcp<5,>=4.1.0
15
+ Requires-Dist: platformdirs<5,>=4.0
16
+ Requires-Dist: pydantic<3,>=2.11
17
+ Requires-Dist: pyyaml<7,>=6.0
18
+ Requires-Dist: segno<2,>=1.6
19
+ Requires-Dist: shortuuid<2,>=1.0.13
20
+ Requires-Dist: starlette<2,>=0.46
21
+ Requires-Dist: uvicorn<1,>=0.34
22
+ Provides-Extra: dev
23
+ Requires-Dist: bandit>=1.8; extra == "dev"
24
+ Requires-Dist: httpx>=0.28; extra == "dev"
25
+ Requires-Dist: mypy>=1.15; extra == "dev"
26
+ Requires-Dist: pre-commit>=4.0; extra == "dev"
27
+ Requires-Dist: pytest>=8.0; extra == "dev"
28
+ Requires-Dist: pytest-cov>=6.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.11; extra == "dev"
30
+ Requires-Dist: types-pyyaml>=6.0; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # dndkit
34
+
35
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an
36
+ AI be the game master of a Dungeons & Dragons 5e campaign: it keeps the
37
+ characters, the world, the fights and the dice, and holds the rules, so the
38
+ model narrates while the server keeps count. It is built with
39
+ [FastMCP](https://gofastmcp.com) 4 and made for **voice play with several
40
+ players around one microphone**, as an MCP service in
41
+ [roomkit-ui](https://github.com/roomkit-live/roomkit-ui), while working just as
42
+ well from Claude Desktop or any MCP client, with a cloud model or a local one.
43
+
44
+ dndkit began as a fork of
45
+ [study-flamingo/gamemaster-mcp](https://github.com/study-flamingo/gamemaster-mcp)
46
+ by Joel Casimir, and has since been largely rewritten: voice play with several
47
+ players, rules kept by the server (combat, the battle map, hit dice,
48
+ concentration, a campaign checked before play), skills, on-screen widgets and
49
+ live web pages. It took a name of its own so as not to take the original
50
+ project's. See [CHANGELOG.md](CHANGELOG.md) for what changed.
51
+
52
+ ## What it does
53
+
54
+ - **Campaigns**: several campaigns, one active at a time, each saved as one JSON
55
+ file. Creating a campaign never overwrites an existing one; deleting one takes
56
+ a confirmation code and moves its file to a trash folder.
57
+ - **Characters**: full 5e sheets; damage and healing with temporary hit points
58
+ applied first, unconsciousness and death saves at 0 HP, massive damage;
59
+ conditions and exhaustion; inventory that stacks; known spells and spell slots;
60
+ short and long rests; experience and levelling up (hit points, proficiency,
61
+ spell slots by class, French class names understood).
62
+ - **Combat**: party characters and monsters in one initiative order, tracked by
63
+ position so three "Goblin"s become "Goblin 1/2/3"; monster hit points, AC and
64
+ conditions; a monster at 0 HP leaves the fight; newcomers mid-fight; the end of
65
+ the fight sums the XP of the defeated.
66
+ - **Bestiary and encounters**: saved stat blocks fill monsters in; planned
67
+ encounters are rated easy to deadly against the party (DMG rules) and started
68
+ with one call, their initiative rolled.
69
+ - **World**: NPCs (with private bios), locations and quests with numbered objectives.
70
+ - **Journal**: session notes and a per-campaign adventure log with search.
71
+ - **Dice and rules**: `d20`, `1d20+5`, `2d6+1d4+3`, advantage and disadvantage;
72
+ short rule reminders (conditions, cover, grapple, death saves...) by English
73
+ or French name.
74
+ - **Several players**: each spoken turn names its speaker; a roll call says
75
+ who plays which character tonight, and only the characters at the table
76
+ count for difficulty and XP.
77
+ - **Battle map**: a grid where moves follow the 5e rules (speed, terrain,
78
+ opportunity attacks), areas of effect, cover, hidden foes.
79
+ - **Before play**: `check_campaign` says what breaks the rules or is missing
80
+ in a new campaign, each with the tool that fixes it.
81
+ - **Skills**: how-to guides the model loads when it needs them: being the
82
+ game master and preparing a campaign, monsters and bosses, the battle map.
83
+ - **On-screen widgets** (MCP Apps): rolling dice, a live combat tracker, a party
84
+ board, the table and character sheets, in clients that show MCP Apps.
85
+ - **Live web pages**: a table screen, each player's sheet on their phone, and
86
+ a backstage where you follow every call the model makes.
87
+
88
+ Names are matched without case or accents ("elodie" finds "Élodie"), and a part
89
+ of a name is enough when only one name contains it ("phare" finds "Le phare
90
+ éteint"), the way a speech transcript spells them. When a call fails, the error
91
+ says why and lists the known names, so the model can correct itself.
92
+
93
+ ## Install and run
94
+
95
+ Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/).
96
+
97
+ ```bash
98
+ # From PyPI (from the first release, 0.4.0)
99
+ uvx dndkit
100
+
101
+ # Straight from GitHub
102
+ uvx --from git+https://github.com/sboily/dndkit dndkit
103
+
104
+ # From a checkout
105
+ git clone https://github.com/sboily/dndkit.git
106
+ cd dndkit
107
+ uv sync
108
+ uv run dndkit
109
+ ```
110
+
111
+ The server speaks MCP over stdio: an MCP client starts it, you don't keep it
112
+ running yourself. `uvx` fetches it on first use and keeps it cached.
113
+
114
+ ### Configuration
115
+
116
+ | Variable | Default | Purpose |
117
+ | --- | --- | --- |
118
+ | `DNDKIT_STORAGE_DIR` | the per-user data directory (below) | Where campaigns are stored |
119
+ | `DNDKIT_LOG_LEVEL` | `INFO` | Log level; logs go to stderr |
120
+ | `DNDKIT_WEB_PORT` | unset: no web pages | Serve the [live web pages](#live-web-pages) on this port |
121
+ | `DNDKIT_WEB_HOST` | `127.0.0.1` | `0.0.0.0` lets phones on the same network open them |
122
+ | `DNDKIT_WEB_URL` | guessed | The address phones should use, when the server cannot tell (a DNS name, Tailscale) |
123
+
124
+ Default data directory:
125
+
126
+ - macOS: `~/Library/Application Support/dndkit`
127
+ - Linux: `~/.local/share/dndkit` (or `$XDG_DATA_HOME/dndkit`)
128
+ - Windows: `%LOCALAPPDATA%\dndkit`
129
+
130
+ Before its rename the project was `gamemaster-mcp`: its `GAMEMASTER_*`
131
+ variables still work, and its data directory is used as long as no `dndkit`
132
+ one exists.
133
+
134
+ To start a campaign from a prepared world document, drop a `.md` or `.txt` file
135
+ in the `settings/` folder of the data directory and pass its name as
136
+ `setting_file` to `create_campaign` or `update_campaign`. Only files directly in
137
+ that folder can be read.
138
+
139
+ ### With roomkit-ui
140
+
141
+ In **Settings → Integrations → MCP**, add a server:
142
+
143
+ | Field | Value |
144
+ | --- | --- |
145
+ | Name | `dndkit` |
146
+ | Transport | `stdio` |
147
+ | Command | `uvx` |
148
+ | Arguments | `dndkit` (from PyPI), or `--from git+https://github.com/sboily/dndkit dndkit` |
149
+ | Environment variables | `DNDKIT_STORAGE_DIR=/Users/you/Games/dnd` (optional)<br>`DNDKIT_WEB_PORT=8765` and `DNDKIT_WEB_HOST=0.0.0.0` for the web pages (optional) |
150
+
151
+ From a local checkout instead: Command `uv`, Arguments
152
+ `run --directory /path/to/dndkit dndkit`.
153
+
154
+ roomkit-ui reads what the server publishes besides its tools: its skills
155
+ (roomkit 0.98 and later) and its instructions, which tell the model how the
156
+ tools are meant to be used (roomkit 0.99). Put the system prompt below in the
157
+ assistant's personality.
158
+
159
+ The 62 tool schemas weigh about 8,900 tokens by roomkit's own estimate, over
160
+ the 8,000 at which roomkit may hide tools behind `find_tools`: keep the skills
161
+ on (the tools they bring then wait for them: about 7,300 tokens), or raise
162
+ roomkit's threshold (`tool_search_threshold_tokens`). roomkit-ui's automatic
163
+ Tool Search leaves a local voice model every tool. Optional parameters are
164
+ advertised without their `null` alternative to save room.
165
+
166
+ #### Local models
167
+
168
+ A model running on your own computer can be the game master. The
169
+ [local model bench](poc/local-model/RESULTS.md) plays scripted table turns
170
+ through roomkit with Ornith 1.5 35B-A3B (llama.cpp): with **thinking on** it
171
+ passes 26 to 30 of 36 scenarios; without thinking it mostly narrates instead
172
+ of calling the tools. Tool Search hurts such a model: leave every tool in
173
+ sight. A thinking turn takes about a minute, which roomkit-ui's background
174
+ tasks (on a llama.cpp model of their own) keep away from the voice.
175
+
176
+ #### Skills
177
+
178
+ The server ships three Agent Skills, how-to guides the model loads when it
179
+ needs them, and publishes them by the official MCP Skills extension
180
+ ([SEP-2640](https://modelcontextprotocol.io/seps/2640-skills-extension),
181
+ `io.modelcontextprotocol/skills`): it declares the extension in its
182
+ capabilities, serves every skill file as a `skill://<name>/<file>` resource,
183
+ and answers `skills/list` and `skills/get` with each skill's frontmatter and
184
+ the SHA-256 digest and size of its files. The server instructions also point
185
+ at each skill's URI, for hosts that only read resources. Load
186
+ `dnd-game-master` first: the system prompt below asks for it.
187
+
188
+ | Skill | Teaches | Gates (roomkit) |
189
+ | --- | --- | --- |
190
+ | `dnd-game-master` | To load first: the game master leads and acts, what the tools hold, opening and ending a session, a session zero by the rules (scores, armor, weapon, skills and spells for each class), `check_campaign` before play | nothing |
191
+ | `dnd-monsters` | Inventing monsters and bosses sized to the party, preparing and rating a fight, legendary actions and resistances, lair actions | `save_monster`, `plan_encounter` |
192
+ | `dnd-battle-map` | Fights on the grid: laying out the room, placing and hiding combatants, moving by the rules, areas of effect, cover | `set_battlefield`, `place_tokens`, `move_token`, `area_of_effect`, `check_target` |
193
+
194
+ A skill's `allowed_tools` frontmatter is roomkit's: roomkit keeps those tools
195
+ out of the catalogue, and out of its token count, until the model activates
196
+ the skill. With the skills on, the catalogue weighs about 7,300 tokens.
197
+ roomkit-ui lists them under the server's name; a client that does not speak
198
+ the extension can load them from `src/dndkit/skills/` as a skill folder.
199
+
200
+ ### With Claude Desktop
201
+
202
+ ```json
203
+ {
204
+ "mcpServers": {
205
+ "dndkit": {
206
+ "command": "uvx",
207
+ "args": ["dndkit"],
208
+ "env": { "DNDKIT_STORAGE_DIR": "/Users/you/Games/dnd" }
209
+ }
210
+ }
211
+ }
212
+ ```
213
+
214
+ ## System prompt for a voice game master
215
+
216
+ ```markdown
217
+ You are the game master of a Dungeons & Dragons 5e session played by voice.
218
+ Speak the players' language, in short spoken sentences: no lists, no markdown.
219
+ You lead the game: you decide when a roll is needed, make it and apply it
220
+ with the tools, without asking leave. Before anything else, activate the
221
+ dnd-game-master skill.
222
+
223
+ Several players sit around one microphone; each of their turns opens with
224
+ who said it. Open every session the same way:
225
+ - Greet the table and ask who is playing tonight. When a voice is not named
226
+ yet, ask who it is and call name_speaker.
227
+ - Call seat_players with start_over and each player with their character.
228
+ Someone new gets a character first: create_character with their name as
229
+ player_name.
230
+ - Remind the table where the last session ended (get_sessions), then play.
231
+ Call seat_players again when someone arrives or leaves. A player's "I attack"
232
+ is their character's action; speak to players by name and to characters in
233
+ the story. Absent players' characters stay in the background.
234
+
235
+ The dndkit tools hold the campaign. Trust them over your memory:
236
+ - Before describing a character, place or quest, read it (get_character,
237
+ get_location, get_quest, get_game_state).
238
+ - Record every change as it happens: damage and healing with change_hit_points
239
+ (characters and monsters), conditions with set_condition, a spell held
240
+ with set_concentration (damage then asks for the save), items with
241
+ add_item_to_character and remove_item_from_character, spells with
242
+ use_spell_slot, the party's location and date with update_game_state.
243
+ - Roll every die with roll_dice and say the result; never invent a roll.
244
+ Resolve each roll yourself: the attack against the target's AC, then the
245
+ damage, applied with change_hit_points. Never ask the players for a total
246
+ or a number the tools hold (get_character gives bonuses, spell attack and
247
+ save DC).
248
+ What the players must not see (a hidden foe's Stealth, an NPC's lie) is a
249
+ secret roll: say only what it means.
250
+ - Ability checks and saves use the sheet's bonuses (get_character); a short
251
+ rest spends Hit Dice with rest and hit_dice.
252
+ - In combat, start_combat with everyone's initiative (or a planned encounter),
253
+ then next_turn after each turn. Monsters at 0 HP leave the fight on their own.
254
+ When positions matter, play it on the battle map (the dnd-battle-map skill:
255
+ set_battlefield, then move_token for every move).
256
+ At the end, end_combat, then award_experience with the XP it reports.
257
+ - When you're unsure of a rule, lookup_rule.
258
+ - Never call get_screen_data: it feeds the on-screen widgets.
259
+ - Log notable moments with add_event, and summarise the session with
260
+ add_session_note when the players stop.
261
+ - When a tool reports an unknown name, pick the right one from the names it
262
+ lists, or ask the players; never create a duplicate.
263
+ - You run the monsters. One the story needs and the bestiary lacks is yours
264
+ to create: save_monster with a challenge rating fitting the party (and
265
+ boss for the boss), then plan_encounter to check the fight's difficulty.
266
+ Spend a boss's legendary actions and resistances with use_legendary, and
267
+ any creature's reaction (an opportunity attack, Shield) with take_reaction.
268
+ A condition that lasts a while gets its rounds in set_condition.
269
+ - remove_from_campaign and delete_campaign work in two calls: the first
270
+ says what would be lost and gives a code; ask the players, and call again
271
+ with the code only after a clear yes.
272
+
273
+ Starting a new campaign, run a short session zero, one question at a time:
274
+ the campaign's name and pitch, then each player's character (name, race,
275
+ class, level, ability scores, then their skills with update_character), the
276
+ starting location, a first NPC and a first quest, creating each with its tool
277
+ as soon as you have it (the dnd-game-master skill has the rules for each).
278
+ Then call check_campaign and fix what it lists before the first scene.
279
+ ```
280
+
281
+ ## Tools
282
+
283
+ | Area | Tools |
284
+ | --- | --- |
285
+ | Campaign | `create_campaign`, `get_campaign_info`, `check_campaign`, `list_campaigns`, `load_campaign`, `update_campaign`, `delete_campaign`, `remove_from_campaign` |
286
+ | Characters | `create_character`, `get_character` 🖥, `list_characters`, `update_character` |
287
+ | Resources | `add_item_to_character`, `remove_item_from_character`, `record_death_save`, `rest` |
288
+ | Spells | `set_spell_slots`, `use_spell_slot`, `set_known_spell` |
289
+ | Progression | `award_experience`, `level_up` |
290
+ | NPCs | `create_npc`, `get_npc`, `update_npc`, `list_npcs` |
291
+ | Locations | `create_location`, `get_location`, `update_location`, `list_locations` |
292
+ | Quests | `create_quest`, `get_quest`, `update_quest`, `list_quests` |
293
+ | Table | `seat_players` 🖥, `get_game_state` 🖥, `update_game_state`, `change_hit_points`, `set_condition`, `set_concentration` |
294
+ | Combat | `start_combat` 🖥, `add_combatant`, `next_turn`, `remove_combatant`, `use_legendary`, `take_reaction`, `end_combat` |
295
+ | Battle map | `set_battlefield` 🖥, `place_tokens`, `move_token`, `area_of_effect`, `check_target` |
296
+ | Bestiary | `save_monster`, `get_monsters`, `plan_encounter`, `list_encounters` |
297
+ | Journal | `add_session_note`, `get_sessions`, `add_event`, `get_events` |
298
+ | Utility | `roll_dice` 🖥, `lookup_rule`, `get_screen_data` (for the widgets) |
299
+
300
+ 🖥 opens an on-screen widget in clients that support MCP Apps.
301
+
302
+ `update_character` sets absolute values; `change_hit_points` applies damage or
303
+ healing to characters and monsters alike, and changes nobody if one name is
304
+ unknown. A tool that fails changes nothing, in memory or on disk.
305
+
306
+ A few rules the tools keep:
307
+
308
+ - **Before play**: `check_campaign` lists what breaks the 5e rules (hit
309
+ points the hit dice cannot give, AC under Unarmored Defense, a caster with
310
+ no spells) and what a first campaign forgets (a fighter without armor,
311
+ scores left at 10, no weapon, no quest or place), each with its fix.
312
+ - **Proficiencies**: `update_character` takes skills, saving throws (English
313
+ or French names: *Discrétion*, *Sagesse*), languages and features; a new
314
+ character is proficient in its class's saves. The sheets give each
315
+ proficient skill and save with its bonus. `expertise` doubles the
316
+ proficiency of a rogue's or a bard's chosen skills, and a bard from level 2
317
+ adds half of it to every other check, initiative included (Jack of All
318
+ Trades).
319
+ - **Hit Dice**: `rest` with `hit_dice` spends them on a short rest (rolled on
320
+ the dice tray, each die plus the Constitution modifier); a long rest gives
321
+ half of them back.
322
+ - **Concentration**: `set_concentration` holds one spell. Damage then asks for
323
+ the Constitution save (DC 10 or half the damage); 0 HP or an incapacitating
324
+ condition ends it.
325
+ - **Secret rolls**: `roll_dice` with `secret` shows on the backstage only, not
326
+ on the table screen, the phones or the dice widget.
327
+ - **Down at 0 HP**: three death save successes or `stabilized` (Spare the
328
+ Dying, Medicine) leave a character stable, without death saves until hit or
329
+ healed; sheets say dying, stable or dead.
330
+ - **Exhaustion** 4 halves the hit point maximum, 6 kills.
331
+ - **Reactions**: one a round (`take_reaction`), back at the start of the
332
+ creature's turn; one that has reacted provokes no more opportunity attacks.
333
+ - **Timed conditions**: `set_condition` with `rounds` ends by itself at the end
334
+ of the target's turn that many turns on; `next_turn` says so.
335
+
336
+ ### Monsters and bosses
337
+
338
+ The game master invents the monsters its story needs: `save_monster` with a
339
+ challenge rating alone fills armor class, hit points and a line of attack
340
+ bonus, damage a round and save DC from the DMG's "Monster Statistics by
341
+ Challenge Rating", for the model to flesh out. `boss` marks the fight's boss,
342
+ a legendary creature with 3 legendary actions a round and 3 legendary
343
+ resistances a day unless told otherwise. In combat, `use_legendary` spends
344
+ them (actions at the end of another creature's turn, back at the start of
345
+ its own); the screens crown the boss, and the game master's show what it has
346
+ left. Combatants are found by a unique part of their name ("gardien" for
347
+ "Gardien de Magma").
348
+
349
+ Characters created without hit points or armor class get the PHB's: the
350
+ class hit die at level 1 and its average after, plus Constitution each level;
351
+ 10 + Dexterity without armor. A caster gets its class's spell slots and
352
+ casting ability, and its sheet (`get_character`, the widget and the player's
353
+ page) gives the spell attack bonus and save DC; the sheet also shows the
354
+ initiative bonus, background, alignment, appearance and story, spells with
355
+ their descriptions, gear, features, proficiencies and languages.
356
+ `get_character` without a name reads whose turn it is.
357
+
358
+ The running fight keeps up with the campaign: a combatant named after a party
359
+ character (even "Fizbin the gnome") plays from that sheet, including one
360
+ created once the fight began, and a monster that joined without numbers takes
361
+ its stat block as soon as `save_monster` saves it.
362
+
363
+ ### Battle map
364
+
365
+ A fight can be played on a grid of 5-foot squares named like a chessboard (a
366
+ column letter and a row number, A1 at the top left). `set_battlefield` lays it
367
+ out with walls and difficult terrain; `start_combat` puts everyone without a
368
+ square at an edge (the party left, its foes right) and `place_tokens` sets
369
+ chosen squares (an ambush). `move_token` moves a combatant by the 5e rules
370
+ (PHB ch. 9) and refuses an impossible move:
371
+
372
+ - 5 feet a square, diagonals included (the PHB grid rule), 10 in difficult
373
+ terrain or through another creature's space; walls are walked around and
374
+ never cut diagonally;
375
+ - allies can be passed through, foes only when two sizes apart; nobody ends a
376
+ move on someone else; a Large creature holds 2x2 squares, Huge 3x3,
377
+ Gargantuan 4x4 (`save_monster`'s `size`);
378
+ - speed from the sheet (25 ft for dwarves, halflings and gnomes, else 30;
379
+ `create_character`'s `speed` overrides it) or the stat block's `speed`;
380
+ grappled, restrained, stunned, paralyzed, petrified or unconscious: 0;
381
+ exhaustion halves it from level 2; Dash doubles the turn's movement;
382
+ - a prone mover stands up first, for half its speed;
383
+ - leaving a foe's reach provokes an opportunity attack, which the result
384
+ names, unless the mover Disengaged; `forced` movement (a shove, a spell, a
385
+ teleport) spends nothing and provokes nothing;
386
+ - `toward` someone stops within reach or as close as it can ("still 15 ft
387
+ away"); `away_from` backs off; `next_turn` restores the next combatant's
388
+ movement.
389
+
390
+ Around the moves:
391
+
392
+ - `area_of_effect` says who a spell or a breath catches: a `sphere` (or
393
+ cylinder) of radius R `at` a square or a creature holds every square within
394
+ R feet as the grid counts them; a `cube` is centred there, or set against
395
+ its `source`'s side facing `toward`; a `cone` (as wide as it is far) and a
396
+ `line` start at the edge of the caster's space, and take a square when at
397
+ least half of it is covered (DMG). Walls stop an area. Everyone caught is
398
+ named, allies first; the area shows on the map until the next turn.
399
+ - `check_target` gives what an attack needs: distance, reach, cover by the
400
+ DMG corner rule (walls give half, three-quarters or total cover, creatures
401
+ half at most), a foe adjacent to a ranged attacker, hidden attacker or
402
+ target.
403
+ - Hidden foes (`place_tokens` with `hidden`, or a planned group's `hidden`)
404
+ stay off the players' screens altogether: no token, no line in the
405
+ initiative they see, no turn named, no hole in the squares a character can
406
+ reach. The game master's screens show them faint. `hidden: false` reveals.
407
+ - `plan_encounter` can carry a `battlefield` and, per monster group, the
408
+ squares `at` which they start (checked against the map, its walls and each
409
+ other); `start_combat` with that encounter lays it all out.
410
+
411
+ `remove_combatant` takes a creature out of the fight and off the map: a
412
+ `defeated` or `fled` foe counts for XP, a `withdrawn` one (a mistake, a
413
+ change of plan) leaves no trace; `add_combatant` brings one in at its side's
414
+ edge.
415
+
416
+ The model reads the map in text: `set_battlefield` and `place_tokens` return
417
+ a grid with a legend, every move says what it cost, what is left and who is
418
+ in reach, and `get_game_state` lists the squares and the distances from
419
+ whoever is acting. The combat widget and the web pages draw it: tokens gold for
420
+ the party and red for its foes, the squares the acting character can still
421
+ reach, and on a player's phone their own token ringed. The players' pages show
422
+ how hurt a foe looks, never its hit points. The map goes away with
423
+ `end_combat`.
424
+
425
+ ### Who plays
426
+
427
+ roomkit-ui tells the voices apart (enrolled voices, or Meta Muse labels that
428
+ the user or the model's `name_speaker` names) and opens each spoken turn with
429
+ the speaker's name. The game keeps the rest: a character's `player_name` ties
430
+ it to a person, and `seat_players` records who is at the table tonight, in
431
+ seat order. Players are matched by name, case and accents aside; the server
432
+ keeps no voice data. Once there has been a roll call:
433
+
434
+ - `get_game_state` says who plays whom, who is absent and which characters
435
+ nobody plays, so the model finds its way after a break;
436
+ - encounter difficulty (`plan_encounter`, `list_encounters`) counts the
437
+ characters at the table only: two players out of four turn a medium fight
438
+ into a deadly one;
439
+ - `award_experience` without names shares the XP between the characters at
440
+ the table; name absent characters to include them.
441
+
442
+ Without a roll call, the whole party counts as present.
443
+
444
+ `delete_campaign` works in two calls. The first deletes nothing: it says what
445
+ the campaign holds and returns a 4-digit code valid for 5 minutes. Only a
446
+ second call with that code deletes, by moving the file to `<data dir>/trash/`,
447
+ from where it can be put back by hand. A wrong code cancels the request.
448
+
449
+ ## On-screen widgets (MCP Apps)
450
+
451
+ Six tools carry an MCP App (`_meta.ui.resourceUri`): the client opens their
452
+ page next to the conversation.
453
+
454
+ | Tool | Widget |
455
+ | --- | --- |
456
+ | `roll_dice` | Dice tray: each die tumbles and lands on its face; a natural 20 glows, a natural 1 shakes; with advantage the discarded d20 fades out; recent rolls below. |
457
+ | `start_combat`, `set_battlefield` | Combat tracker, live: the battle map when there is one, initiative order, current turn, hit point bars that flash on damage or healing, AC, conditions, the defeated and their XP. |
458
+ | `get_game_state` | Party board, live: location, date, funds, every character's hit points, spell slots, conditions and death saves, quest progress, combat banner. |
459
+ | `seat_players` | The table, live: the players seated around it with their characters, newcomers without one yet, absent players and characters nobody plays. |
460
+ | `get_character` | Character sheet, live: hit points, AC, proficiency, XP to next level, abilities, conditions, spell slots, spells, inventory. |
461
+
462
+ Each page is self-contained (no network access), French or English after the
463
+ system language, and reads its data with the `get_screen_data` tool, refreshing
464
+ every couple of seconds: the combat tracker follows `change_hit_points` or
465
+ `next_turn` calls made later in the conversation. The widgets only display; the
466
+ game changes through the conversation, so the model always knows the state.
467
+
468
+ ## Live web pages
469
+
470
+ With `DNDKIT_WEB_PORT` set, the server also serves web pages that follow
471
+ the game as it happens: every change made through a tool, and every roll, is
472
+ pushed to the open pages at once (server-sent events). The pages run inside
473
+ the MCP server's process, so they are up while roomkit-ui runs the server and
474
+ reconnect by themselves when it restarts. A taken port only logs a warning;
475
+ the game goes on without the pages.
476
+
477
+ | Address | For | Shows |
478
+ | --- | --- | --- |
479
+ | `/table/` | a shared screen (TV, laptop) | The campaign's name and pitch, the session, place and date; initiative order, the party's hit points and conditions (absent players' characters faded), the table with who sits where, the dice tray, active quests, the adventure log, and a QR code per character |
480
+ | `/p/<code>/` | each player's phone | Their full character sheet, the fight, the dice, quests and log, the party; "Your turn!" and a buzz when their turn comes |
481
+ | `/gm/` | you, on the computer running the game | Everything, to follow and debug what the model does: the **model's activity** (every tool call with its arguments, its answer or error and its time, and the skills the client fetched); the fight with exact monster hit points and AC, or the last fight between two; the adventure log with importance, characters and tags; planned encounters and their difficulty; every quest with its objectives and notes; the notebook (current situation, world notes, setting, past sessions, version, campaign file, last save, skills); the world (NPCs with their secrets and ties, locations with the party's marked, the bestiary's full stat blocks and how many are fighting); every character's full sheet with the game master's notes; the players' join codes |
482
+
483
+ - **Joining**: players scan their character's code on the table screen. Each
484
+ link carries a code derived from the campaign, the character and a key kept
485
+ in the data directory (`player-links.key`): it cannot be guessed, survives
486
+ restarts, and belongs to the active campaign. Delete the key file to change
487
+ every link.
488
+ - **What players see** is built field by field from what they may know: the
489
+ campaign's description (a pitch: `create_campaign` asks for no spoilers), their
490
+ party in full, but a monster only as *unhurt*, *hurt*, *bloodied* or *down*,
491
+ and never an NPC's biography or notes, a planned encounter, a monster's
492
+ statistics or the game master's notes.
493
+ - **The activity** lives in memory for the session, like the dice log, and
494
+ skips the widgets' own `get_screen_data` polling. A call the client refuses
495
+ before sending it (roomkit checks required arguments) never reaches the
496
+ server, so it is not there: look at the client's log for those.
497
+ - **The backstage** opens only from the computer running the server: a
498
+ loopback peer, asking for this server by name (`localhost`, its address or
499
+ `DNDKIT_WEB_URL`'s host), so a web page whose domain points at this
500
+ computer (DNS rebinding) is refused. Requests carrying proxy headers are
501
+ refused too, but a reverse proxy or tunnel on that computer that adds none
502
+ would make every visitor look local: do not put one in front of the pages.
503
+ - **Read-only**: the pages never change the game; it changes through the
504
+ conversation, so the model always knows the state.
505
+ - **Phones** need `DNDKIT_WEB_HOST=0.0.0.0` and the same network. The table
506
+ screen then puts this computer's network address in the codes, even when it
507
+ is open at `127.0.0.1`. For players elsewhere, put a private network such as
508
+ Tailscale between you and set `DNDKIT_WEB_URL`; the pages carry no other
509
+ authentication.
510
+ - The log names the addresses at startup: `Table screen: http://127.0.0.1:8765/table/`.
511
+
512
+ ## Data
513
+
514
+ See [docs/STORAGE_STRUCTURE.md](docs/STORAGE_STRUCTURE.md). Campaigns saved by
515
+ gamemaster-mcp, dndkit's upstream, load as they are and are rewritten in the
516
+ current format at their first change. Its global `events/adventure_log.json`
517
+ is not imported: events now belong to their campaign.
518
+
519
+ ## Development
520
+
521
+ ```bash
522
+ uv sync --extra dev
523
+ uv run pytest
524
+ uv run ruff check . && uv run ruff format --check .
525
+ uv run mypy src/
526
+ uv run bandit -r src/ -c pyproject.toml
527
+ npm ci && npx knip # unused files and exports in the pages' scripts
528
+ ```
529
+
530
+ The pages' scripts (`src/dndkit/ui/*.js`) are ES modules, so knip can
531
+ follow who imports what; each page inlines them as one script. GitHub Actions
532
+ runs all of the above on every push and pull request (Python 3.12 and 3.13),
533
+ and `pre-commit install` runs most of it before each commit.
534
+
535
+ [CLAUDE.md](CLAUDE.md) describes the architecture and the project's rules.
536
+
537
+ ### Releasing
538
+
539
+ Each change worth telling goes into [CHANGELOG.md](CHANGELOG.md) under
540
+ `## Unreleased` with the commit that makes it. To release, on an up-to-date
541
+ `main`:
542
+
543
+ ```bash
544
+ uv run python scripts/release.py prepare 0.5.0 # versions follow semver
545
+ git push origin main v0.5.0
546
+ ```
547
+
548
+ `prepare` names the Unreleased section `## 0.5.0 (<date>)`, sets the version
549
+ in `pyproject.toml` and `uv.lock`, commits `release: 0.5.0` and tags
550
+ `v0.5.0`; it refuses outside `main`, with uncommitted changes, an empty
551
+ Unreleased section or an existing tag. The tag runs
552
+ `.github/workflows/release.yml`: it checks that the tag matches the version,
553
+ sits on `main` and has its changelog section, runs the tests and builds, then
554
+ publishes to PyPI by trusted publishing (no token: PyPI trusts the workflow in
555
+ its `pypi` environment) and makes the GitHub release, its notes the version's
556
+ changelog section, the built files attached. A failed step stops what follows:
557
+ nothing reaches GitHub's releases before PyPI has the version.
558
+
559
+ Once, on pypi.org: add a pending trusted publisher for the project `dndkit`
560
+ (owner `sboily`, repository `dndkit`, workflow `release.yml`, environment
561
+ `pypi`). GitHub creates the `pypi` environment at the first release; required
562
+ reviewers set on it (Settings → Environments) hold each release for approval.
563
+
564
+ ## License
565
+
566
+ MIT, see [LICENSE](LICENSE). dndkit keeps the copyright of gamemaster-mcp,
567
+ the project it was forked from.