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,580 @@
1
+ import { z } from "zod";
2
+ import * as pauseTools from "../tools/pause.js";
3
+ import { LIMITS } from "../utils/validation.js";
4
+ import { ANNOTATIONS } from "../utils/tool-annotations.js";
5
+ // Zod schemas for nested types
6
+ const NarrativeThreadSchema = z.object({
7
+ name: z.string().max(LIMITS.NAME_MAX).describe("Thread name/label"),
8
+ description: z.string().max(LIMITS.DESCRIPTION_MAX).describe("What this thread is about"),
9
+ status: z
10
+ .enum(["active", "background", "climax", "resolving"])
11
+ .describe("Thread status"),
12
+ urgency: z.enum(["low", "medium", "high", "critical"]).describe("Urgency level"),
13
+ involvedCharacterIds: z.array(z.string().max(100)).max(LIMITS.ARRAY_MAX).optional(),
14
+ involvedLocationIds: z.array(z.string().max(100)).max(LIMITS.ARRAY_MAX).optional(),
15
+ relatedQuestId: z.string().max(100).optional(),
16
+ notes: z.string().max(LIMITS.DESCRIPTION_MAX).optional(),
17
+ });
18
+ const ActiveConversationSchema = z.object({
19
+ npcId: z.string().max(100).describe("Character ID of NPC"),
20
+ topic: z.string().max(LIMITS.NAME_MAX).describe("What's being discussed"),
21
+ npcEmotionalState: z.string().max(LIMITS.NAME_MAX).describe("How they're feeling"),
22
+ lastNpcStatement: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("What they just said"),
23
+ playerIntent: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("What player seems to want"),
24
+ });
25
+ export function registerPauseTools(server) {
26
+ // ============================================================================
27
+ // PAUSE PREPARATION
28
+ // ============================================================================
29
+ server.registerTool("prepare_pause", {
30
+ description: `Prepare to pause the game. Returns a checklist of context to save and current game state.
31
+
32
+ WHEN TO CALL: Before ending a game or when the player says they need to stop.
33
+
34
+ This tool returns:
35
+ - Current game state summary
36
+ - Checklist of ephemeral context that should be saved
37
+ - Any existing pause state
38
+ - Instructions for what to save
39
+
40
+ After calling this, use save_pause_state to persist your context.`,
41
+ inputSchema: {
42
+ gameId: z.string().max(100).describe("The game ID"),
43
+ },
44
+ annotations: ANNOTATIONS.READ_ONLY,
45
+ }, async ({ gameId }) => {
46
+ const checklist = pauseTools.preparePause(gameId);
47
+ if (!checklist) {
48
+ return {
49
+ content: [{ type: "text", text: `Game ${gameId} not found` }],
50
+ isError: true,
51
+ };
52
+ }
53
+ return {
54
+ content: [{ type: "text", text: JSON.stringify(checklist, null, 2) }],
55
+ };
56
+ });
57
+ // ============================================================================
58
+ // SAVE PAUSE STATE
59
+ // ============================================================================
60
+ server.registerTool("save_pause_state", {
61
+ description: `Save your context for seamless game resumption. Call this before ending a game.
62
+
63
+ REQUIRED FIELDS:
64
+ - currentScene: Where we are in the story
65
+ - immediateSituation: What's happening RIGHT NOW (be specific!)
66
+
67
+ The more detail you provide, the smoother the resume will be for the next DM
68
+ (which might be you in a new context window, or a different model entirely).
69
+
70
+ Write as if briefing a replacement DM who's taking over mid-game.`,
71
+ inputSchema: {
72
+ gameId: z.string().max(100).describe("The game ID"),
73
+ // Required
74
+ currentScene: z
75
+ .string()
76
+ .max(LIMITS.CONTENT_MAX)
77
+ .describe("Description of where we are in the story - the scene, location, narrative moment"),
78
+ immediateSituation: z
79
+ .string()
80
+ .max(LIMITS.CONTENT_MAX)
81
+ .describe("What is happening RIGHT NOW - the exact moment we're pausing at. Be specific!"),
82
+ // Scene context
83
+ sceneAtmosphere: z
84
+ .string()
85
+ .max(LIMITS.DESCRIPTION_MAX)
86
+ .optional()
87
+ .describe("Mood, lighting, sounds, emotional tension"),
88
+ recentTone: z
89
+ .string()
90
+ .max(LIMITS.NAME_MAX)
91
+ .optional()
92
+ .describe("Recent narrative tone (tense, comedic, romantic, etc.)"),
93
+ // Player interaction
94
+ pendingPlayerAction: z
95
+ .string()
96
+ .max(LIMITS.DESCRIPTION_MAX)
97
+ .optional()
98
+ .describe("What action was the player about to take?"),
99
+ awaitingResponseTo: z
100
+ .string()
101
+ .max(LIMITS.DESCRIPTION_MAX)
102
+ .optional()
103
+ .describe("What question/prompt awaits player response?"),
104
+ presentedChoices: z
105
+ .array(z.string().max(LIMITS.DESCRIPTION_MAX))
106
+ .max(LIMITS.ARRAY_MAX)
107
+ .optional()
108
+ .describe("Formal choices presented to player"),
109
+ // Player context
110
+ playerApparentGoals: z
111
+ .string()
112
+ .max(LIMITS.DESCRIPTION_MAX)
113
+ .optional()
114
+ .describe("What the player seems to be trying to accomplish"),
115
+ unresolvedHooks: z
116
+ .array(z.string().max(LIMITS.DESCRIPTION_MAX))
117
+ .max(LIMITS.ARRAY_MAX)
118
+ .optional()
119
+ .describe("Plot hooks player noticed but hasn't pursued"),
120
+ // Narrative threads
121
+ activeThreads: z
122
+ .array(NarrativeThreadSchema)
123
+ .max(LIMITS.ARRAY_MAX)
124
+ .optional()
125
+ .describe("Ongoing storylines, investigations, subplots"),
126
+ // DM notes
127
+ dmShortTermPlans: z
128
+ .string()
129
+ .max(LIMITS.CONTENT_MAX)
130
+ .optional()
131
+ .describe("What was about to happen next? Planned encounters/reveals?"),
132
+ dmLongTermPlans: z
133
+ .string()
134
+ .max(LIMITS.CONTENT_MAX)
135
+ .optional()
136
+ .describe("Major plot arcs being developed"),
137
+ upcomingReveals: z
138
+ .array(z.string().max(LIMITS.DESCRIPTION_MAX))
139
+ .max(LIMITS.ARRAY_MAX)
140
+ .optional()
141
+ .describe("Secrets close to being discovered"),
142
+ // NPC state
143
+ npcAttitudes: z
144
+ .record(z.string(), z.string().max(LIMITS.NAME_MAX))
145
+ .optional()
146
+ .describe("NPC emotional states/attitudes (characterId -> disposition)"),
147
+ activeConversations: z
148
+ .array(ActiveConversationSchema)
149
+ .max(LIMITS.ARRAY_MAX)
150
+ .optional()
151
+ .describe("Ongoing conversations and where they left off"),
152
+ // Metadata
153
+ pauseReason: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Why is the game pausing?"),
154
+ modelUsed: z
155
+ .string()
156
+ .max(LIMITS.NAME_MAX)
157
+ .optional()
158
+ .describe("Which model/agent is saving this state"),
159
+ },
160
+ annotations: ANNOTATIONS.CREATE,
161
+ }, async (params) => {
162
+ const pauseState = pauseTools.savePauseState(params);
163
+ return {
164
+ content: [
165
+ {
166
+ type: "text",
167
+ text: JSON.stringify({
168
+ saved: true,
169
+ pauseStateId: pauseState.id,
170
+ message: "Context saved successfully. Game can be resumed seamlessly using get_resume_context.",
171
+ savedFields: {
172
+ required: {
173
+ currentScene: true,
174
+ immediateSituation: true,
175
+ },
176
+ optional: {
177
+ sceneAtmosphere: !!pauseState.sceneAtmosphere,
178
+ pendingPlayerAction: !!pauseState.pendingPlayerAction,
179
+ awaitingResponseTo: !!pauseState.awaitingResponseTo,
180
+ presentedChoices: (pauseState.presentedChoices?.length || 0) > 0,
181
+ activeThreads: pauseState.activeThreads.length > 0,
182
+ dmShortTermPlans: !!pauseState.dmShortTermPlans,
183
+ dmLongTermPlans: !!pauseState.dmLongTermPlans,
184
+ upcomingReveals: pauseState.upcomingReveals.length > 0,
185
+ npcAttitudes: Object.keys(pauseState.npcAttitudes).length > 0,
186
+ activeConversations: pauseState.activeConversations.length > 0,
187
+ recentTone: !!pauseState.recentTone,
188
+ playerApparentGoals: !!pauseState.playerApparentGoals,
189
+ unresolvedHooks: pauseState.unresolvedHooks.length > 0,
190
+ },
191
+ },
192
+ }, null, 2),
193
+ },
194
+ ],
195
+ };
196
+ });
197
+ // ============================================================================
198
+ // GET PAUSE STATE
199
+ // ============================================================================
200
+ server.registerTool("get_pause_state", {
201
+ description: "Get the saved pause state for a game (if any). Use this to check what context was preserved.",
202
+ inputSchema: {
203
+ gameId: z.string().max(100).describe("The game ID"),
204
+ },
205
+ annotations: ANNOTATIONS.READ_ONLY,
206
+ }, async ({ gameId }) => {
207
+ const pauseState = pauseTools.getPauseState(gameId);
208
+ if (!pauseState) {
209
+ return {
210
+ content: [
211
+ {
212
+ type: "text",
213
+ text: JSON.stringify({
214
+ hasPauseState: false,
215
+ message: "No pause state saved for this game. If resuming, rely on narrative_events and game state.",
216
+ }),
217
+ },
218
+ ],
219
+ };
220
+ }
221
+ return {
222
+ content: [{ type: "text", text: JSON.stringify(pauseState, null, 2) }],
223
+ };
224
+ });
225
+ // ============================================================================
226
+ // GET RESUME CONTEXT
227
+ // ============================================================================
228
+ server.registerTool("get_resume_context", {
229
+ description: `Get everything needed to resume a paused game seamlessly.
230
+
231
+ WHEN TO CALL: At the start of a game when continuing a paused game.
232
+
233
+ Returns:
234
+ - Full pause state (DM context, scene, plans)
235
+ - Current game state (characters, quests, location)
236
+ - Recent narrative events
237
+ - A ready-to-use resume briefing/prompt
238
+ - Warnings about things that need attention (active combat, etc.)
239
+
240
+ Use this to get up to speed quickly and resume exactly where the game left off.`,
241
+ inputSchema: {
242
+ gameId: z.string().max(100).describe("The game ID"),
243
+ },
244
+ annotations: ANNOTATIONS.READ_ONLY,
245
+ }, async ({ gameId }) => {
246
+ const context = pauseTools.getResumeContext(gameId);
247
+ if (!context) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: `No pause state found for game ${gameId}. Use load_game and get_history instead.`,
253
+ },
254
+ ],
255
+ isError: true,
256
+ };
257
+ }
258
+ // Return the formatted resume prompt prominently
259
+ return {
260
+ content: [
261
+ {
262
+ type: "text",
263
+ text: context.resumePrompt +
264
+ "\n\n" +
265
+ "═══════════════════════════════════════════════════════════════════════════════\n" +
266
+ " FULL DATA (JSON)\n" +
267
+ "═══════════════════════════════════════════════════════════════════════════════\n\n" +
268
+ JSON.stringify({
269
+ pauseState: context.pauseState,
270
+ gameState: context.gameState,
271
+ warnings: context.warnings,
272
+ }, null, 2),
273
+ },
274
+ ],
275
+ };
276
+ });
277
+ // ============================================================================
278
+ // CONTEXT SNAPSHOT (Lightweight incremental save)
279
+ // ============================================================================
280
+ server.registerTool("save_context_snapshot", {
281
+ description: `Quick lightweight context save - use this DURING play to preserve context incrementally.
282
+
283
+ CRITICAL: Call this regularly to prevent context loss:
284
+ - After EVERY significant story beat
285
+ - After important NPC interactions
286
+ - When player makes major decisions
287
+ - Every 10-15 turns as a safety net
288
+ - Before any potentially long narrative sequence
289
+
290
+ This is faster than full save_pause_state but captures essential state.
291
+ For end-of-session, use prepare_pause + save_pause_state instead.`,
292
+ inputSchema: {
293
+ gameId: z.string().max(100).describe("The game ID"),
294
+ situation: z
295
+ .string()
296
+ .max(LIMITS.DESCRIPTION_MAX)
297
+ .describe("Brief description of current situation (what's happening right now)"),
298
+ notes: z.string().max(LIMITS.CONTENT_MAX).optional().describe("Any additional context to preserve"),
299
+ npcMood: z
300
+ .record(z.string(), z.string().max(LIMITS.NAME_MAX))
301
+ .optional()
302
+ .describe("Current NPC attitudes (characterId -> mood)"),
303
+ playerIntent: z
304
+ .string()
305
+ .max(LIMITS.DESCRIPTION_MAX)
306
+ .optional()
307
+ .describe("What the player seems to be trying to do"),
308
+ },
309
+ annotations: ANNOTATIONS.CREATE,
310
+ }, async (params) => {
311
+ const result = pauseTools.saveContextSnapshot(params);
312
+ return {
313
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
314
+ };
315
+ });
316
+ // ============================================================================
317
+ // CHECK CONTEXT FRESHNESS
318
+ // ============================================================================
319
+ server.registerTool("check_context_freshness", {
320
+ description: `Check if context needs to be saved. Returns a reminder if it's been too long since last save.
321
+
322
+ IMPORTANT: Call this every 5-10 narrative turns to ensure context is being preserved. If stale, immediately call save_context_snapshot.`,
323
+ inputSchema: {
324
+ gameId: z.string().max(100).describe("The game ID"),
325
+ },
326
+ annotations: ANNOTATIONS.READ_ONLY,
327
+ }, async ({ gameId }) => {
328
+ const result = pauseTools.checkContextFreshness(gameId);
329
+ return {
330
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
331
+ };
332
+ });
333
+ // ============================================================================
334
+ // CLEAR PAUSE STATE
335
+ // ============================================================================
336
+ server.registerTool("clear_pause_state", {
337
+ description: "Clear the saved pause state for a game. Use after successfully resuming to start fresh.",
338
+ inputSchema: {
339
+ gameId: z.string().max(100).describe("The game ID"),
340
+ },
341
+ annotations: ANNOTATIONS.DESTRUCTIVE,
342
+ }, async ({ gameId }) => {
343
+ const deleted = pauseTools.deletePauseState(gameId);
344
+ return {
345
+ content: [
346
+ {
347
+ type: "text",
348
+ text: deleted
349
+ ? "Pause state cleared. The game can now accumulate fresh context."
350
+ : "No pause state to clear.",
351
+ },
352
+ ],
353
+ };
354
+ });
355
+ // ============================================================================
356
+ // EXTERNAL UPDATES - Multi-agent collaboration
357
+ // ============================================================================
358
+ server.registerTool("push_external_update", {
359
+ description: `Push an update from an external agent into the game game.
360
+
361
+ USE CASE: External agents (research agents, worldbuilders, lore generators, etc.)
362
+ can push updates that the primary DM agent will receive and incorporate.
363
+
364
+ EXAMPLES:
365
+ - A research agent discovers detailed lore about a faction
366
+ - A worldbuilder generates NPC backstory
367
+ - A consistency checker flags a plot hole
368
+ - An atmosphere generator provides environmental details
369
+
370
+ The DM agent should periodically check for pending updates via get_pending_updates.`,
371
+ inputSchema: {
372
+ gameId: z.string().max(100).describe("The game ID"),
373
+ sourceAgent: z.string().max(LIMITS.NAME_MAX).describe("ID/name of the agent pushing this update"),
374
+ sourceDescription: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Description of what this agent does"),
375
+ updateType: z
376
+ .string()
377
+ .max(100)
378
+ .describe("Type of update (e.g., 'lore', 'npc_backstory', 'world_event', 'item_details', 'plot_suggestion')"),
379
+ category: z.string().max(100).optional().describe("Optional category for organization"),
380
+ title: z.string().max(LIMITS.NAME_MAX).describe("Brief title/summary of the update"),
381
+ content: z.string().max(LIMITS.CONTENT_MAX).describe("The full content/information being pushed"),
382
+ structuredData: z
383
+ .record(z.string(), z.unknown())
384
+ .optional()
385
+ .describe("Optional structured data (JSON) for programmatic use"),
386
+ targetEntityId: z.string().max(100).optional().describe("If this relates to a specific entity, its ID"),
387
+ targetEntityType: z
388
+ .string()
389
+ .max(100)
390
+ .optional()
391
+ .describe("Type of target entity (character, location, item, quest, etc.)"),
392
+ priority: z
393
+ .enum(["low", "normal", "high", "urgent"])
394
+ .optional()
395
+ .describe("Priority level (default: normal). Urgent updates demand immediate DM attention."),
396
+ },
397
+ annotations: ANNOTATIONS.CREATE,
398
+ }, async (params) => {
399
+ const update = pauseTools.pushExternalUpdate(params);
400
+ return {
401
+ content: [
402
+ {
403
+ type: "text",
404
+ text: JSON.stringify({
405
+ pushed: true,
406
+ updateId: update.id,
407
+ priority: update.priority,
408
+ message: `Update pushed successfully. The DM agent will see this when checking pending updates.`,
409
+ tip: update.priority === "urgent"
410
+ ? "This urgent update will be highlighted for immediate DM attention."
411
+ : "The DM will see this in their pending updates queue.",
412
+ }, null, 2),
413
+ },
414
+ ],
415
+ };
416
+ });
417
+ server.registerTool("get_pending_updates", {
418
+ description: `Check for pending updates from external agents.
419
+
420
+ WHEN TO CALL:
421
+ - At the start of each scene/turn
422
+ - Before making major narrative decisions
423
+ - Periodically during long sessions
424
+
425
+ Returns all pending updates, prioritized by urgency.
426
+ Use acknowledge_update, apply_update, or reject_update to process them.`,
427
+ inputSchema: {
428
+ gameId: z.string().max(100).describe("The game ID"),
429
+ },
430
+ annotations: ANNOTATIONS.READ_ONLY,
431
+ }, async ({ gameId }) => {
432
+ const result = pauseTools.getPendingUpdates(gameId);
433
+ return {
434
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
435
+ };
436
+ });
437
+ server.registerTool("acknowledge_update", {
438
+ description: "Mark an external update as acknowledged (seen by DM). Use when you've read but haven't yet incorporated an update.",
439
+ inputSchema: {
440
+ updateId: z.string().max(100).describe("The update ID"),
441
+ },
442
+ annotations: ANNOTATIONS.UPDATE,
443
+ }, async ({ updateId }) => {
444
+ const update = pauseTools.acknowledgeUpdate(updateId);
445
+ if (!update) {
446
+ return {
447
+ content: [{ type: "text", text: "Update not found" }],
448
+ isError: true,
449
+ };
450
+ }
451
+ return {
452
+ content: [{ type: "text", text: JSON.stringify(update, null, 2) }],
453
+ };
454
+ });
455
+ server.registerTool("apply_update", {
456
+ description: "Mark an external update as applied (incorporated into the narrative). Use when you've woven the update into the story.",
457
+ inputSchema: {
458
+ updateId: z.string().max(100).describe("The update ID"),
459
+ dmNotes: z
460
+ .string()
461
+ .max(LIMITS.DESCRIPTION_MAX)
462
+ .optional()
463
+ .describe("Notes on how the update was incorporated"),
464
+ },
465
+ annotations: ANNOTATIONS.UPDATE,
466
+ }, async ({ updateId, dmNotes }) => {
467
+ const update = pauseTools.applyUpdate(updateId, dmNotes);
468
+ if (!update) {
469
+ return {
470
+ content: [{ type: "text", text: "Update not found" }],
471
+ isError: true,
472
+ };
473
+ }
474
+ return {
475
+ content: [
476
+ {
477
+ type: "text",
478
+ text: JSON.stringify({
479
+ applied: true,
480
+ update,
481
+ message: "Update marked as applied. It will no longer appear in pending updates.",
482
+ }, null, 2),
483
+ },
484
+ ],
485
+ };
486
+ });
487
+ server.registerTool("reject_update", {
488
+ description: "Reject an external update (not appropriate for the narrative). Use when an update doesn't fit the current story direction.",
489
+ inputSchema: {
490
+ updateId: z.string().max(100).describe("The update ID"),
491
+ dmNotes: z
492
+ .string()
493
+ .max(LIMITS.DESCRIPTION_MAX)
494
+ .optional()
495
+ .describe("Reason for rejection (helps external agents improve)"),
496
+ },
497
+ annotations: ANNOTATIONS.UPDATE,
498
+ }, async ({ updateId, dmNotes }) => {
499
+ const update = pauseTools.rejectUpdate(updateId, dmNotes);
500
+ if (!update) {
501
+ return {
502
+ content: [{ type: "text", text: "Update not found" }],
503
+ isError: true,
504
+ };
505
+ }
506
+ return {
507
+ content: [
508
+ {
509
+ type: "text",
510
+ text: JSON.stringify({
511
+ rejected: true,
512
+ update,
513
+ message: "Update rejected. It will no longer appear in pending updates.",
514
+ }, null, 2),
515
+ },
516
+ ],
517
+ };
518
+ });
519
+ server.registerTool("list_external_updates", {
520
+ description: "List all external updates for a game with optional status filter.",
521
+ inputSchema: {
522
+ gameId: z.string().max(100).describe("The game ID"),
523
+ status: z
524
+ .enum(["pending", "acknowledged", "applied", "rejected"])
525
+ .optional()
526
+ .describe("Filter by status"),
527
+ },
528
+ annotations: ANNOTATIONS.READ_ONLY,
529
+ }, async ({ gameId, status }) => {
530
+ const updates = pauseTools.listExternalUpdates(gameId, status);
531
+ return {
532
+ content: [
533
+ {
534
+ type: "text",
535
+ text: JSON.stringify({
536
+ count: updates.length,
537
+ status: status || "all",
538
+ updates,
539
+ }, null, 2),
540
+ },
541
+ ],
542
+ };
543
+ });
544
+ server.registerTool("get_external_update", {
545
+ description: "Get details of a specific external update by ID.",
546
+ inputSchema: {
547
+ updateId: z.string().max(100).describe("The update ID"),
548
+ },
549
+ annotations: ANNOTATIONS.READ_ONLY,
550
+ }, async ({ updateId }) => {
551
+ const update = pauseTools.getExternalUpdate(updateId);
552
+ if (!update) {
553
+ return {
554
+ content: [{ type: "text", text: "Update not found" }],
555
+ isError: true,
556
+ };
557
+ }
558
+ return {
559
+ content: [{ type: "text", text: JSON.stringify(update, null, 2) }],
560
+ };
561
+ });
562
+ server.registerTool("delete_external_update", {
563
+ description: "Delete an external update entirely.",
564
+ inputSchema: {
565
+ updateId: z.string().max(100).describe("The update ID"),
566
+ },
567
+ annotations: ANNOTATIONS.DESTRUCTIVE,
568
+ }, async ({ updateId }) => {
569
+ const deleted = pauseTools.deleteExternalUpdate(updateId);
570
+ return {
571
+ content: [
572
+ {
573
+ type: "text",
574
+ text: deleted ? "Update deleted" : "Update not found",
575
+ },
576
+ ],
577
+ isError: !deleted,
578
+ };
579
+ });
580
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerQuestTools(server: McpServer): void;