run-dmcp 0.1.0

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 (167) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +79 -0
  3. package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
  4. package/dist/__tests__/engineVocabulary.test.js +147 -0
  5. package/dist/db/__tests__/connection.test.d.ts +1 -0
  6. package/dist/db/__tests__/connection.test.js +72 -0
  7. package/dist/db/__tests__/testDb.d.ts +33 -0
  8. package/dist/db/__tests__/testDb.js +41 -0
  9. package/dist/db/connection.d.ts +22 -0
  10. package/dist/db/connection.js +107 -0
  11. package/dist/db/schema.d.ts +1 -0
  12. package/dist/db/schema.js +725 -0
  13. package/dist/events/emitter.d.ts +22 -0
  14. package/dist/events/emitter.js +71 -0
  15. package/dist/http/server.d.ts +3 -0
  16. package/dist/http/server.js +649 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +92 -0
  19. package/dist/register/abilities.d.ts +2 -0
  20. package/dist/register/abilities.js +165 -0
  21. package/dist/register/audio.d.ts +2 -0
  22. package/dist/register/audio.js +326 -0
  23. package/dist/register/batch.d.ts +2 -0
  24. package/dist/register/batch.js +343 -0
  25. package/dist/register/character.d.ts +2 -0
  26. package/dist/register/character.js +324 -0
  27. package/dist/register/combat.d.ts +2 -0
  28. package/dist/register/combat.js +207 -0
  29. package/dist/register/core.d.ts +2 -0
  30. package/dist/register/core.js +1040 -0
  31. package/dist/register/display.d.ts +2 -0
  32. package/dist/register/display.js +263 -0
  33. package/dist/register/factions.d.ts +2 -0
  34. package/dist/register/factions.js +186 -0
  35. package/dist/register/images.d.ts +2 -0
  36. package/dist/register/images.js +400 -0
  37. package/dist/register/inventory.d.ts +2 -0
  38. package/dist/register/inventory.js +115 -0
  39. package/dist/register/mcp-prompts.d.ts +2 -0
  40. package/dist/register/mcp-prompts.js +684 -0
  41. package/dist/register/mcp-resources.d.ts +2 -0
  42. package/dist/register/mcp-resources.js +335 -0
  43. package/dist/register/narrative.d.ts +2 -0
  44. package/dist/register/narrative.js +242 -0
  45. package/dist/register/notes.d.ts +2 -0
  46. package/dist/register/notes.js +170 -0
  47. package/dist/register/pause.d.ts +2 -0
  48. package/dist/register/pause.js +580 -0
  49. package/dist/register/quests.d.ts +2 -0
  50. package/dist/register/quests.js +118 -0
  51. package/dist/register/relationships.d.ts +2 -0
  52. package/dist/register/relationships.js +147 -0
  53. package/dist/register/resources.d.ts +2 -0
  54. package/dist/register/resources.js +277 -0
  55. package/dist/register/secrets.d.ts +2 -0
  56. package/dist/register/secrets.js +192 -0
  57. package/dist/register/status.d.ts +2 -0
  58. package/dist/register/status.js +130 -0
  59. package/dist/register/tables.d.ts +2 -0
  60. package/dist/register/tables.js +146 -0
  61. package/dist/register/tags.d.ts +2 -0
  62. package/dist/register/tags.js +114 -0
  63. package/dist/register/time.d.ts +2 -0
  64. package/dist/register/time.js +281 -0
  65. package/dist/register/world.d.ts +2 -0
  66. package/dist/register/world.js +127 -0
  67. package/dist/schemas/index.d.ts +921 -0
  68. package/dist/schemas/index.js +121 -0
  69. package/dist/test-setup.d.ts +1 -0
  70. package/dist/test-setup.js +13 -0
  71. package/dist/tools/__tests__/audio.test.d.ts +1 -0
  72. package/dist/tools/__tests__/audio.test.js +59 -0
  73. package/dist/tools/__tests__/conserved.test.d.ts +1 -0
  74. package/dist/tools/__tests__/conserved.test.js +488 -0
  75. package/dist/tools/__tests__/constraint.test.d.ts +1 -0
  76. package/dist/tools/__tests__/constraint.test.js +212 -0
  77. package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
  78. package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
  79. package/dist/tools/__tests__/images.test.d.ts +1 -0
  80. package/dist/tools/__tests__/images.test.js +59 -0
  81. package/dist/tools/__tests__/relationship.test.d.ts +1 -0
  82. package/dist/tools/__tests__/relationship.test.js +132 -0
  83. package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
  84. package/dist/tools/__tests__/resource-constraints.test.js +131 -0
  85. package/dist/tools/__tests__/resource.test.d.ts +1 -0
  86. package/dist/tools/__tests__/resource.test.js +190 -0
  87. package/dist/tools/__tests__/time.test.d.ts +1 -0
  88. package/dist/tools/__tests__/time.test.js +404 -0
  89. package/dist/tools/__tests__/timers.test.d.ts +1 -0
  90. package/dist/tools/__tests__/timers.test.js +426 -0
  91. package/dist/tools/__tests__/world.test.d.ts +1 -0
  92. package/dist/tools/__tests__/world.test.js +70 -0
  93. package/dist/tools/ability.d.ts +48 -0
  94. package/dist/tools/ability.js +238 -0
  95. package/dist/tools/audio.d.ts +24 -0
  96. package/dist/tools/audio.js +365 -0
  97. package/dist/tools/character.d.ts +70 -0
  98. package/dist/tools/character.js +309 -0
  99. package/dist/tools/combat.d.ts +13 -0
  100. package/dist/tools/combat.js +195 -0
  101. package/dist/tools/constraint.d.ts +132 -0
  102. package/dist/tools/constraint.js +269 -0
  103. package/dist/tools/dice.d.ts +23 -0
  104. package/dist/tools/dice.js +111 -0
  105. package/dist/tools/display.d.ts +120 -0
  106. package/dist/tools/display.js +528 -0
  107. package/dist/tools/faction.d.ts +61 -0
  108. package/dist/tools/faction.js +269 -0
  109. package/dist/tools/game.d.ts +96 -0
  110. package/dist/tools/game.js +526 -0
  111. package/dist/tools/image-prompt.d.ts +49 -0
  112. package/dist/tools/image-prompt.js +479 -0
  113. package/dist/tools/images.d.ts +47 -0
  114. package/dist/tools/images.js +449 -0
  115. package/dist/tools/inventory.d.ts +20 -0
  116. package/dist/tools/inventory.js +145 -0
  117. package/dist/tools/narrative.d.ts +58 -0
  118. package/dist/tools/narrative.js +237 -0
  119. package/dist/tools/notes.d.ts +41 -0
  120. package/dist/tools/notes.js +220 -0
  121. package/dist/tools/pause.d.ts +110 -0
  122. package/dist/tools/pause.js +1254 -0
  123. package/dist/tools/quest.d.ts +34 -0
  124. package/dist/tools/quest.js +164 -0
  125. package/dist/tools/relationship.d.ts +74 -0
  126. package/dist/tools/relationship.js +324 -0
  127. package/dist/tools/resource.d.ts +93 -0
  128. package/dist/tools/resource.js +374 -0
  129. package/dist/tools/rules.d.ts +4 -0
  130. package/dist/tools/rules.js +30 -0
  131. package/dist/tools/secrets.d.ts +49 -0
  132. package/dist/tools/secrets.js +195 -0
  133. package/dist/tools/status.d.ts +36 -0
  134. package/dist/tools/status.js +218 -0
  135. package/dist/tools/tables.d.ts +33 -0
  136. package/dist/tools/tables.js +209 -0
  137. package/dist/tools/tags.d.ts +52 -0
  138. package/dist/tools/tags.js +176 -0
  139. package/dist/tools/time.d.ts +33 -0
  140. package/dist/tools/time.js +276 -0
  141. package/dist/tools/timers.d.ts +41 -0
  142. package/dist/tools/timers.js +215 -0
  143. package/dist/tools/world.d.ts +78 -0
  144. package/dist/tools/world.js +331 -0
  145. package/dist/types/index.d.ts +969 -0
  146. package/dist/types/index.js +1 -0
  147. package/dist/utils/__tests__/json.test.d.ts +1 -0
  148. package/dist/utils/__tests__/json.test.js +55 -0
  149. package/dist/utils/__tests__/validation.test.d.ts +1 -0
  150. package/dist/utils/__tests__/validation.test.js +90 -0
  151. package/dist/utils/errors.d.ts +44 -0
  152. package/dist/utils/errors.js +121 -0
  153. package/dist/utils/json.d.ts +9 -0
  154. package/dist/utils/json.js +23 -0
  155. package/dist/utils/logger.d.ts +7 -0
  156. package/dist/utils/logger.js +50 -0
  157. package/dist/utils/output-schemas.d.ts +594 -0
  158. package/dist/utils/output-schemas.js +331 -0
  159. package/dist/utils/tool-annotations.d.ts +147 -0
  160. package/dist/utils/tool-annotations.js +98 -0
  161. package/dist/utils/validation.d.ts +34 -0
  162. package/dist/utils/validation.js +52 -0
  163. package/dist/utils/verbosity.d.ts +57 -0
  164. package/dist/utils/verbosity.js +67 -0
  165. package/dist/utils/webui.d.ts +20 -0
  166. package/dist/utils/webui.js +35 -0
  167. package/package.json +75 -0
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerMcpResources(server: McpServer): void;
@@ -0,0 +1,335 @@
1
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as gameTools from "../tools/game.js";
3
+ import * as characterTools from "../tools/character.js";
4
+ import * as worldTools from "../tools/world.js";
5
+ import * as questTools from "../tools/quest.js";
6
+ import * as narrativeTools from "../tools/narrative.js";
7
+ import * as rulesTools from "../tools/rules.js";
8
+ export function registerMcpResources(server) {
9
+ // ============================================================================
10
+ // STATIC RESOURCES
11
+ // ============================================================================
12
+ // List all games
13
+ server.registerResource("games", "dmcp://games", {
14
+ description: "List all games",
15
+ mimeType: "application/json",
16
+ }, async (uri) => {
17
+ const games = gameTools.listGames();
18
+ return {
19
+ contents: [
20
+ {
21
+ uri: uri.href,
22
+ mimeType: "application/json",
23
+ text: JSON.stringify(games, null, 2),
24
+ },
25
+ ],
26
+ };
27
+ });
28
+ // ============================================================================
29
+ // GAME-SCOPED RESOURCES (templated)
30
+ // ============================================================================
31
+ // Helper to create list callback for game-scoped resources
32
+ const createGameListCallback = (suffix, nameFormatter) => {
33
+ return async () => {
34
+ const games = gameTools.listGames();
35
+ return {
36
+ resources: games.map((g) => ({
37
+ uri: `dmcp://game/${g.id}${suffix}`,
38
+ name: nameFormatter(g),
39
+ })),
40
+ };
41
+ };
42
+ };
43
+ // Game details
44
+ server.registerResource("game", new ResourceTemplate("dmcp://game/{gameId}", {
45
+ list: createGameListCallback("", (g) => g.name),
46
+ }), {
47
+ description: "Game metadata and preferences",
48
+ mimeType: "application/json",
49
+ }, async (uri, variables) => {
50
+ const gameId = variables.gameId;
51
+ const game = gameTools.loadGame(gameId);
52
+ if (!game) {
53
+ return {
54
+ contents: [
55
+ {
56
+ uri: uri.href,
57
+ mimeType: "application/json",
58
+ text: JSON.stringify({ error: "Game not found" }),
59
+ },
60
+ ],
61
+ };
62
+ }
63
+ return {
64
+ contents: [
65
+ {
66
+ uri: uri.href,
67
+ mimeType: "application/json",
68
+ text: JSON.stringify(game, null, 2),
69
+ },
70
+ ],
71
+ };
72
+ });
73
+ // Game state (overview)
74
+ server.registerResource("game-state", new ResourceTemplate("dmcp://game/{gameId}/state", {
75
+ list: createGameListCallback("/state", (g) => `${g.name} - State`),
76
+ }), {
77
+ description: "Current game state snapshot with counts",
78
+ mimeType: "application/json",
79
+ }, async (uri, variables) => {
80
+ const gameId = variables.gameId;
81
+ const state = gameTools.getGameState(gameId);
82
+ if (!state) {
83
+ return {
84
+ contents: [
85
+ {
86
+ uri: uri.href,
87
+ mimeType: "application/json",
88
+ text: JSON.stringify({ error: "Game not found" }),
89
+ },
90
+ ],
91
+ };
92
+ }
93
+ return {
94
+ contents: [
95
+ {
96
+ uri: uri.href,
97
+ mimeType: "application/json",
98
+ text: JSON.stringify(state, null, 2),
99
+ },
100
+ ],
101
+ };
102
+ });
103
+ // Game characters
104
+ server.registerResource("game-characters", new ResourceTemplate("dmcp://game/{gameId}/characters", {
105
+ list: createGameListCallback("/characters", (g) => `${g.name} - Characters`),
106
+ }), {
107
+ description: "All characters in the game",
108
+ mimeType: "application/json",
109
+ }, async (uri, variables) => {
110
+ const gameId = variables.gameId;
111
+ const characters = characterTools.listCharacters(gameId);
112
+ return {
113
+ contents: [
114
+ {
115
+ uri: uri.href,
116
+ mimeType: "application/json",
117
+ text: JSON.stringify(characters, null, 2),
118
+ },
119
+ ],
120
+ };
121
+ });
122
+ // Game locations
123
+ server.registerResource("game-locations", new ResourceTemplate("dmcp://game/{gameId}/locations", {
124
+ list: createGameListCallback("/locations", (g) => `${g.name} - Locations`),
125
+ }), {
126
+ description: "All locations in the game",
127
+ mimeType: "application/json",
128
+ }, async (uri, variables) => {
129
+ const gameId = variables.gameId;
130
+ const locations = worldTools.listLocations(gameId);
131
+ return {
132
+ contents: [
133
+ {
134
+ uri: uri.href,
135
+ mimeType: "application/json",
136
+ text: JSON.stringify(locations, null, 2),
137
+ },
138
+ ],
139
+ };
140
+ });
141
+ // Game map (JSON data)
142
+ server.registerResource("game-map", new ResourceTemplate("dmcp://game/{gameId}/map", {
143
+ list: createGameListCallback("/map", (g) => `${g.name} - Map`),
144
+ }), {
145
+ description: "World map data with location nodes and connections",
146
+ mimeType: "application/json",
147
+ }, async (uri, variables) => {
148
+ const gameId = variables.gameId;
149
+ const game = gameTools.loadGame(gameId);
150
+ const mapResult = worldTools.renderMap(gameId, {
151
+ playerLocationId: game?.currentLocationId || undefined,
152
+ });
153
+ if (!mapResult) {
154
+ return {
155
+ contents: [
156
+ {
157
+ uri: uri.href,
158
+ mimeType: "application/json",
159
+ text: JSON.stringify({ error: "No locations in this game" }),
160
+ },
161
+ ],
162
+ };
163
+ }
164
+ return {
165
+ contents: [
166
+ {
167
+ uri: uri.href,
168
+ mimeType: "application/json",
169
+ text: JSON.stringify(mapResult, null, 2),
170
+ },
171
+ ],
172
+ };
173
+ });
174
+ // Game quests
175
+ server.registerResource("game-quests", new ResourceTemplate("dmcp://game/{gameId}/quests", {
176
+ list: createGameListCallback("/quests", (g) => `${g.name} - Quests`),
177
+ }), {
178
+ description: "All quests in the game",
179
+ mimeType: "application/json",
180
+ }, async (uri, variables) => {
181
+ const gameId = variables.gameId;
182
+ const quests = questTools.listQuests(gameId);
183
+ return {
184
+ contents: [
185
+ {
186
+ uri: uri.href,
187
+ mimeType: "application/json",
188
+ text: JSON.stringify(quests, null, 2),
189
+ },
190
+ ],
191
+ };
192
+ });
193
+ // Game history (recent narrative events)
194
+ server.registerResource("game-history", new ResourceTemplate("dmcp://game/{gameId}/history", {
195
+ list: createGameListCallback("/history", (g) => `${g.name} - History`),
196
+ }), {
197
+ description: "Recent narrative events (last 50)",
198
+ mimeType: "application/json",
199
+ }, async (uri, variables) => {
200
+ const gameId = variables.gameId;
201
+ const history = narrativeTools.getHistory(gameId, { limit: 50 });
202
+ return {
203
+ contents: [
204
+ {
205
+ uri: uri.href,
206
+ mimeType: "application/json",
207
+ text: JSON.stringify(history, null, 2),
208
+ },
209
+ ],
210
+ };
211
+ });
212
+ // Game rules
213
+ server.registerResource("game-rules", new ResourceTemplate("dmcp://game/{gameId}/rules", {
214
+ list: createGameListCallback("/rules", (g) => `${g.name} - Rules`),
215
+ }), {
216
+ description: "Rule system for the game",
217
+ mimeType: "application/json",
218
+ }, async (uri, variables) => {
219
+ const gameId = variables.gameId;
220
+ const rules = rulesTools.getRules(gameId);
221
+ if (!rules) {
222
+ return {
223
+ contents: [
224
+ {
225
+ uri: uri.href,
226
+ mimeType: "application/json",
227
+ text: JSON.stringify({ error: "No rules configured for this game" }),
228
+ },
229
+ ],
230
+ };
231
+ }
232
+ return {
233
+ contents: [
234
+ {
235
+ uri: uri.href,
236
+ mimeType: "application/json",
237
+ text: JSON.stringify(rules, null, 2),
238
+ },
239
+ ],
240
+ };
241
+ });
242
+ // ============================================================================
243
+ // ENTITY RESOURCES (direct access by ID)
244
+ // ============================================================================
245
+ // Character by ID (rendered sheet)
246
+ server.registerResource("character", new ResourceTemplate("dmcp://character/{characterId}", {
247
+ list: undefined, // No enumeration - access by ID only
248
+ }), {
249
+ description: "Character sheet data",
250
+ mimeType: "application/json",
251
+ }, async (uri, variables) => {
252
+ const characterId = variables.characterId;
253
+ const sheet = characterTools.renderCharacterSheet(characterId);
254
+ if (!sheet) {
255
+ return {
256
+ contents: [
257
+ {
258
+ uri: uri.href,
259
+ mimeType: "application/json",
260
+ text: JSON.stringify({ error: "Character not found" }),
261
+ },
262
+ ],
263
+ };
264
+ }
265
+ return {
266
+ contents: [
267
+ {
268
+ uri: uri.href,
269
+ mimeType: "application/json",
270
+ text: JSON.stringify(sheet, null, 2),
271
+ },
272
+ ],
273
+ };
274
+ });
275
+ // Location by ID
276
+ server.registerResource("location", new ResourceTemplate("dmcp://location/{locationId}", {
277
+ list: undefined, // No enumeration - access by ID only
278
+ }), {
279
+ description: "Location details with exits",
280
+ mimeType: "application/json",
281
+ }, async (uri, variables) => {
282
+ const locationId = variables.locationId;
283
+ const location = worldTools.getLocation(locationId);
284
+ if (!location) {
285
+ return {
286
+ contents: [
287
+ {
288
+ uri: uri.href,
289
+ mimeType: "application/json",
290
+ text: JSON.stringify({ error: "Location not found" }),
291
+ },
292
+ ],
293
+ };
294
+ }
295
+ return {
296
+ contents: [
297
+ {
298
+ uri: uri.href,
299
+ mimeType: "application/json",
300
+ text: JSON.stringify(location, null, 2),
301
+ },
302
+ ],
303
+ };
304
+ });
305
+ // Quest by ID
306
+ server.registerResource("quest", new ResourceTemplate("dmcp://quest/{questId}", {
307
+ list: undefined, // No enumeration - access by ID only
308
+ }), {
309
+ description: "Quest details with objectives",
310
+ mimeType: "application/json",
311
+ }, async (uri, variables) => {
312
+ const questId = variables.questId;
313
+ const quest = questTools.getQuest(questId);
314
+ if (!quest) {
315
+ return {
316
+ contents: [
317
+ {
318
+ uri: uri.href,
319
+ mimeType: "application/json",
320
+ text: JSON.stringify({ error: "Quest not found" }),
321
+ },
322
+ ],
323
+ };
324
+ }
325
+ return {
326
+ contents: [
327
+ {
328
+ uri: uri.href,
329
+ mimeType: "application/json",
330
+ text: JSON.stringify(quest, null, 2),
331
+ },
332
+ ],
333
+ };
334
+ });
335
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerNarrativeTools(server: McpServer): void;
@@ -0,0 +1,242 @@
1
+ import { z } from "zod";
2
+ import * as narrativeTools from "../tools/narrative.js";
3
+ import { LIMITS } from "../utils/validation.js";
4
+ import { ANNOTATIONS } from "../utils/tool-annotations.js";
5
+ export function registerNarrativeTools(server) {
6
+ server.registerTool("log_event", {
7
+ description: "Log a narrative event. CRITICAL: Call this after EVERY significant narrative beat - dialogue exchanges, player actions, combat rounds, discoveries, travel, decisions. This is the primary way to preserve game history. Event types: 'dialogue', 'action', 'discovery', 'combat', 'travel', 'decision', 'scene_transition'.",
8
+ inputSchema: {
9
+ gameId: z.string().max(100).describe("The game ID"),
10
+ eventType: z.string().max(100).describe("Type of event (e.g., 'dialogue', 'action', 'discovery', 'combat')"),
11
+ content: z.string().max(LIMITS.CONTENT_MAX).describe("Event content/description"),
12
+ metadata: z.record(z.string(), z.unknown()).optional().describe("Additional metadata"),
13
+ },
14
+ annotations: ANNOTATIONS.CREATE,
15
+ }, async (params) => {
16
+ const event = narrativeTools.logEvent(params);
17
+ return {
18
+ content: [{ type: "text", text: JSON.stringify(event, null, 2) }],
19
+ };
20
+ });
21
+ server.registerTool("get_history", {
22
+ description: "Get narrative history",
23
+ inputSchema: {
24
+ gameId: z.string().max(100).describe("The game ID"),
25
+ limit: z.number().optional().describe("Maximum events to return"),
26
+ eventType: z.string().max(100).optional().describe("Filter by event type"),
27
+ since: z.string().max(100).optional().describe("Only events after this timestamp"),
28
+ },
29
+ annotations: ANNOTATIONS.READ_ONLY,
30
+ }, async ({ gameId, limit, eventType, since }) => {
31
+ const events = narrativeTools.getHistory(gameId, { limit, eventType, since });
32
+ return {
33
+ content: [{ type: "text", text: JSON.stringify(events, null, 2) }],
34
+ };
35
+ });
36
+ server.registerTool("get_summary", {
37
+ description: "Get a summary of the narrative so far",
38
+ inputSchema: {
39
+ gameId: z.string().max(100).describe("The game ID"),
40
+ },
41
+ annotations: ANNOTATIONS.READ_ONLY,
42
+ }, async ({ gameId }) => {
43
+ const summary = narrativeTools.getSummary(gameId);
44
+ return {
45
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }],
46
+ };
47
+ });
48
+ server.registerTool("get_export_styles", {
49
+ description: "Get available narrative styles for story export",
50
+ inputSchema: {},
51
+ annotations: ANNOTATIONS.READ_ONLY,
52
+ }, async () => {
53
+ const styles = [
54
+ { id: "literary-fiction", name: "Literary Fiction", description: "Sophisticated prose with deep character introspection" },
55
+ { id: "pulp-adventure", name: "Pulp Adventure", description: "Fast-paced action with bold heroes and cliffhangers" },
56
+ { id: "epic-fantasy", name: "Epic Fantasy", description: "Grand, sweeping narrative with mythic undertones" },
57
+ { id: "noir", name: "Noir", description: "Hardboiled prose with moral ambiguity" },
58
+ { id: "horror", name: "Horror", description: "Atmospheric dread and tension" },
59
+ { id: "comedic", name: "Comedic", description: "Witty and humorous with clever dialogue" },
60
+ { id: "young-adult", name: "Young Adult", description: "Accessible and engaging with relatable characters" },
61
+ { id: "screenplay", name: "Screenplay", description: "Formatted as a film script" },
62
+ { id: "journal", name: "Journal/Diary", description: "Personal entries from protagonist's perspective" },
63
+ { id: "chronicle", name: "Chronicle", description: "Historical documentation with a sense of legacy" },
64
+ ];
65
+ return {
66
+ content: [{
67
+ type: "text",
68
+ text: JSON.stringify({
69
+ styles,
70
+ instruction: "Present these style options to the player and let them choose how their story should be written.",
71
+ }, null, 2),
72
+ }],
73
+ };
74
+ });
75
+ server.registerTool("export_story", {
76
+ description: "Export the game history as structured data for reconstruction into a narrative book. Use get_chapter_for_export to fetch individual chapters for writing.",
77
+ inputSchema: {
78
+ gameId: z.string().max(100).describe("The game ID"),
79
+ style: z.string().max(100).describe("Narrative style (e.g., 'literary-fiction', 'pulp-adventure', 'epic-fantasy', 'noir', or custom)"),
80
+ },
81
+ annotations: ANNOTATIONS.READ_ONLY,
82
+ }, async ({ gameId, style }) => {
83
+ const exportData = narrativeTools.exportStoryData(gameId, style);
84
+ if (!exportData) {
85
+ return {
86
+ content: [{ type: "text", text: "Game not found" }],
87
+ isError: true,
88
+ };
89
+ }
90
+ const chapterSummaries = exportData.chapters.map((ch, idx) => ({
91
+ chapterNumber: idx + 1,
92
+ title: ch.title,
93
+ eventCount: ch.events.length,
94
+ eventTypes: [...new Set(ch.events.map(e => e.eventType))],
95
+ timeSpan: ch.events.length > 0 ? {
96
+ start: ch.events[0].timestamp,
97
+ end: ch.events[ch.events.length - 1].timestamp,
98
+ } : null,
99
+ }));
100
+ return {
101
+ content: [{
102
+ type: "text",
103
+ text: JSON.stringify({
104
+ game: exportData.game,
105
+ characters: exportData.characters,
106
+ locations: exportData.locations,
107
+ quests: exportData.quests,
108
+ chapterSummaries,
109
+ totalChapters: exportData.chapters.length,
110
+ totalEvents: exportData.totalEvents,
111
+ exportStyle: exportData.exportStyle,
112
+ instruction: exportData.instruction,
113
+ meta: {
114
+ exportedAt: new Date().toISOString(),
115
+ workflow: [
116
+ "1. Review the game, characters, locations, and quests for context",
117
+ "2. For each chapter, use get_chapter_for_export to fetch the full event data",
118
+ "3. Spawn a subagent for each chapter with the style instruction and chapter data",
119
+ "4. Each subagent writes its chapter as narrative prose",
120
+ "5. Combine chapters into the final book with front matter",
121
+ ],
122
+ },
123
+ }, null, 2),
124
+ }],
125
+ };
126
+ });
127
+ server.registerTool("get_chapter_for_export", {
128
+ description: "Get a single chapter's full event data for writing. Use this to fetch chapters one at a time for subagent processing.",
129
+ inputSchema: {
130
+ gameId: z.string().max(100).describe("The game ID"),
131
+ chapterNumber: z.number().describe("Chapter number (1-indexed)"),
132
+ style: z.string().max(100).describe("Narrative style for the instruction"),
133
+ },
134
+ annotations: ANNOTATIONS.READ_ONLY,
135
+ }, async ({ gameId, chapterNumber, style }) => {
136
+ const exportData = narrativeTools.exportStoryData(gameId, style);
137
+ if (!exportData) {
138
+ return {
139
+ content: [{ type: "text", text: "Game not found" }],
140
+ isError: true,
141
+ };
142
+ }
143
+ const chapterIndex = chapterNumber - 1;
144
+ if (chapterIndex < 0 || chapterIndex >= exportData.chapters.length) {
145
+ return {
146
+ content: [{ type: "text", text: `Chapter ${chapterNumber} not found. Total chapters: ${exportData.chapters.length}` }],
147
+ isError: true,
148
+ };
149
+ }
150
+ const chapter = exportData.chapters[chapterIndex];
151
+ return {
152
+ content: [{
153
+ type: "text",
154
+ text: JSON.stringify({
155
+ chapterNumber,
156
+ totalChapters: exportData.chapters.length,
157
+ title: chapter.title,
158
+ events: chapter.events,
159
+ context: {
160
+ gameName: exportData.game.name,
161
+ setting: exportData.game.setting,
162
+ style: exportData.exportStyle,
163
+ },
164
+ instruction: exportData.instruction,
165
+ subagentPrompt: `You are writing Chapter ${chapterNumber} of "${exportData.game.name}". ${exportData.instruction} Transform the following events into engaging narrative prose. Maintain consistency with the ${exportData.game.setting} setting.`,
166
+ }, null, 2),
167
+ }],
168
+ };
169
+ });
170
+ // PLAYER INTERACTION TOOLS
171
+ server.registerTool("present_choices", {
172
+ description: "Present choices to the player with multi-select and free-form input support. Returns structured choice data for the DM agent to display.",
173
+ inputSchema: {
174
+ gameId: z.string().max(100).describe("The game ID"),
175
+ prompt: z.string().max(LIMITS.DESCRIPTION_MAX).describe("The question or situation description to present"),
176
+ choices: z.array(z.object({
177
+ id: z.string().max(100).describe("Unique identifier for this choice"),
178
+ label: z.string().max(LIMITS.NAME_MAX).describe("Short label for the choice (1-5 words)"),
179
+ description: z.string().max(LIMITS.DESCRIPTION_MAX).describe("Fuller description of what this choice means"),
180
+ consequences: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Hint at consequences (optional, for DM reference)"),
181
+ })).min(1).max(6).describe("Available choices (1-6 options)"),
182
+ allowMultiple: z.boolean().optional().describe("Allow selecting multiple choices (default: false)"),
183
+ allowFreeform: z.boolean().optional().describe("Allow player to type a custom response (default: true)"),
184
+ freeformPlaceholder: z.string().max(LIMITS.NAME_MAX).optional().describe("Placeholder text for free-form input (default: 'Or describe what you want to do...')"),
185
+ context: z.object({
186
+ locationId: z.string().max(100).optional(),
187
+ characterIds: z.array(z.string().max(100)).max(LIMITS.ARRAY_MAX).optional(),
188
+ urgency: z.enum(["low", "medium", "high", "critical"]).optional(),
189
+ }).optional().describe("Context for the choice"),
190
+ },
191
+ annotations: ANNOTATIONS.CREATE,
192
+ }, async ({ gameId, prompt, choices, allowMultiple, allowFreeform, freeformPlaceholder, context }) => {
193
+ narrativeTools.logEvent({
194
+ gameId,
195
+ eventType: "choice_presented",
196
+ content: prompt,
197
+ metadata: { choices, allowMultiple, allowFreeform, context },
198
+ });
199
+ return {
200
+ content: [{
201
+ type: "text",
202
+ text: JSON.stringify({
203
+ type: "player_choice",
204
+ prompt,
205
+ choices,
206
+ allowMultiple: allowMultiple ?? false,
207
+ allowFreeform: allowFreeform ?? true,
208
+ freeformPlaceholder: freeformPlaceholder ?? "Or describe what you want to do...",
209
+ context,
210
+ instruction: "Present these choices to the player. Use AskUserQuestion - players can always select 'Other' to provide free-form input.",
211
+ }, null, 2),
212
+ }],
213
+ };
214
+ });
215
+ server.registerTool("record_choice", {
216
+ description: "Record the player's choice after they've selected",
217
+ inputSchema: {
218
+ gameId: z.string().max(100).describe("The game ID"),
219
+ choiceIds: z.array(z.string().max(100)).max(LIMITS.ARRAY_MAX).describe("The ID(s) of the choice(s) the player selected"),
220
+ customResponse: z.string().max(LIMITS.CONTENT_MAX).optional().describe("If player chose 'Other', their custom response"),
221
+ },
222
+ annotations: ANNOTATIONS.CREATE,
223
+ }, async ({ gameId, choiceIds, customResponse }) => {
224
+ const event = narrativeTools.logEvent({
225
+ gameId,
226
+ eventType: "choice_made",
227
+ content: customResponse || `Player chose: ${choiceIds.join(", ")}`,
228
+ metadata: { choiceIds, customResponse },
229
+ });
230
+ return {
231
+ content: [{
232
+ type: "text",
233
+ text: JSON.stringify({
234
+ recorded: true,
235
+ choiceIds,
236
+ customResponse,
237
+ event,
238
+ }, null, 2),
239
+ }],
240
+ };
241
+ });
242
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerNoteTools(server: McpServer): void;