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,343 @@
1
+ import { z } from "zod";
2
+ import * as characterTools from "../tools/character.js";
3
+ import * as worldTools from "../tools/world.js";
4
+ import * as inventoryTools from "../tools/inventory.js";
5
+ import * as combatTools from "../tools/combat.js";
6
+ import * as narrativeTools from "../tools/narrative.js";
7
+ import { LIMITS } from "../utils/validation.js";
8
+ import { ANNOTATIONS } from "../utils/tool-annotations.js";
9
+ import { imageGenSchema, voiceSchema } from "../schemas/index.js";
10
+ export function registerBatchTools(server) {
11
+ // ============================================================================
12
+ // BATCH CREATE NPCS - Create multiple NPCs at once
13
+ // ============================================================================
14
+ server.registerTool("batch_create_npcs", {
15
+ description: "Create multiple NPCs at once. Use this when populating a location with several characters. Returns all created characters.",
16
+ inputSchema: {
17
+ gameId: z.string().describe("The game ID"),
18
+ npcs: z
19
+ .array(z.object({
20
+ name: z.string().min(1).max(LIMITS.NAME_MAX).describe("NPC name"),
21
+ attributes: z.record(z.string(), z.number()).optional(),
22
+ skills: z.record(z.string(), z.number()).optional(),
23
+ status: z
24
+ .object({
25
+ health: z.number().optional(),
26
+ maxHealth: z.number().optional(),
27
+ conditions: z.array(z.string()).optional(),
28
+ })
29
+ .optional(),
30
+ locationId: z.string().optional().describe("Starting location"),
31
+ notes: z.string().max(LIMITS.CONTENT_MAX).optional(),
32
+ voice: voiceSchema.optional(),
33
+ imageGen: imageGenSchema.optional(),
34
+ }))
35
+ .min(1)
36
+ .max(20)
37
+ .describe("Array of NPCs to create (max 20)"),
38
+ },
39
+ annotations: ANNOTATIONS.CREATE,
40
+ }, async ({ gameId, npcs }) => {
41
+ const created = [];
42
+ const errors = [];
43
+ // Check for duplicate names within the batch
44
+ const nameCount = new Map();
45
+ for (const npc of npcs) {
46
+ const lowerName = npc.name.toLowerCase();
47
+ nameCount.set(lowerName, (nameCount.get(lowerName) || 0) + 1);
48
+ }
49
+ const duplicates = Array.from(nameCount.entries())
50
+ .filter(([, count]) => count > 1)
51
+ .map(([name]) => name);
52
+ if (duplicates.length > 0) {
53
+ return {
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: JSON.stringify({
58
+ isError: true,
59
+ errorCode: "DUPLICATE_NAMES_IN_BATCH",
60
+ message: `Duplicate NPC names in batch: ${duplicates.join(", ")}`,
61
+ suggestions: ["Ensure all NPC names in the batch are unique"],
62
+ }, null, 2),
63
+ },
64
+ ],
65
+ isError: true,
66
+ };
67
+ }
68
+ for (const npc of npcs) {
69
+ try {
70
+ const character = characterTools.createCharacter({
71
+ gameId,
72
+ name: npc.name,
73
+ isPlayer: false,
74
+ attributes: npc.attributes,
75
+ skills: npc.skills,
76
+ status: npc.status,
77
+ locationId: npc.locationId,
78
+ notes: npc.notes,
79
+ voice: npc.voice,
80
+ imageGen: npc.imageGen,
81
+ });
82
+ created.push(character);
83
+ }
84
+ catch (e) {
85
+ errors.push({ name: npc.name, error: e.message });
86
+ }
87
+ }
88
+ const result = {
89
+ created,
90
+ errors,
91
+ summary: `Created ${created.length}/${npcs.length} NPCs`,
92
+ };
93
+ return {
94
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
95
+ structuredContent: result,
96
+ };
97
+ });
98
+ // ============================================================================
99
+ // SETUP COMBAT ENCOUNTER - Create NPCs, start combat with all participants
100
+ // ============================================================================
101
+ server.registerTool("setup_combat_encounter", {
102
+ description: "Complete combat setup in one call: creates enemy NPCs and starts combat with all participants (enemies + players at location). Returns the ready-to-play combat state.",
103
+ inputSchema: {
104
+ gameId: z.string().describe("The game ID"),
105
+ locationId: z.string().describe("Location where combat takes place"),
106
+ enemies: z
107
+ .array(z.object({
108
+ name: z.string().min(1).max(LIMITS.NAME_MAX).describe("Enemy name"),
109
+ attributes: z.record(z.string(), z.number()).optional(),
110
+ status: z
111
+ .object({
112
+ health: z.number().optional(),
113
+ maxHealth: z.number().optional(),
114
+ })
115
+ .optional(),
116
+ }))
117
+ .min(1)
118
+ .max(10)
119
+ .describe("Enemies to create and add to combat"),
120
+ includePlayersAtLocation: z
121
+ .boolean()
122
+ .default(true)
123
+ .describe("Auto-add player characters at this location"),
124
+ },
125
+ annotations: ANNOTATIONS.CREATE,
126
+ }, async ({ gameId, locationId, enemies, includePlayersAtLocation = true }) => {
127
+ // 1. Create enemy NPCs
128
+ const createdEnemies = [];
129
+ for (const enemy of enemies) {
130
+ const character = characterTools.createCharacter({
131
+ gameId,
132
+ name: enemy.name,
133
+ isPlayer: false,
134
+ attributes: enemy.attributes,
135
+ status: enemy.status,
136
+ locationId,
137
+ });
138
+ createdEnemies.push(character);
139
+ }
140
+ // 2. Get players at location if needed
141
+ const participantIds = [...createdEnemies.map((e) => e.id)];
142
+ if (includePlayersAtLocation) {
143
+ const playersAtLocation = characterTools.listCharacters(gameId, {
144
+ isPlayer: true,
145
+ locationId,
146
+ });
147
+ participantIds.push(...playersAtLocation.map((p) => p.id));
148
+ }
149
+ // 3. Start combat with all participants
150
+ const combat = combatTools.startCombat({
151
+ gameId,
152
+ locationId,
153
+ participantIds,
154
+ });
155
+ // 4. Log the encounter start
156
+ narrativeTools.logEvent({
157
+ gameId,
158
+ eventType: "combat",
159
+ content: `Combat begins! ${createdEnemies.length} enemies attack.`,
160
+ metadata: {
161
+ enemyIds: createdEnemies.map((e) => e.id),
162
+ locationId,
163
+ },
164
+ });
165
+ const result = {
166
+ combat,
167
+ createdEnemies: createdEnemies.map((e) => ({ id: e.id, name: e.name })),
168
+ participantCount: participantIds.length,
169
+ summary: `Combat started with ${participantIds.length} participants. ${createdEnemies.length} enemies created.`,
170
+ };
171
+ return {
172
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
173
+ structuredContent: result,
174
+ };
175
+ });
176
+ // ============================================================================
177
+ // SCENE TRANSITION - Move characters, log event
178
+ // ============================================================================
179
+ server.registerTool("scene_transition", {
180
+ description: "Complete scene transition in one call: moves specified characters to a new location and logs a narrative event. Perfect for moving between scenes.",
181
+ inputSchema: {
182
+ gameId: z.string().describe("The game ID"),
183
+ characterIds: z
184
+ .array(z.string())
185
+ .min(1)
186
+ .describe("Characters to move to the new scene"),
187
+ destinationId: z.string().describe("Destination location ID"),
188
+ narrativeDescription: z
189
+ .string()
190
+ .max(LIMITS.DESCRIPTION_MAX)
191
+ .describe("Description of the transition for the narrative log"),
192
+ },
193
+ annotations: ANNOTATIONS.UPDATE,
194
+ }, async ({ gameId, characterIds, destinationId, narrativeDescription }) => {
195
+ // 1. Move all characters
196
+ const moved = [];
197
+ const errors = [];
198
+ for (const characterId of characterIds) {
199
+ try {
200
+ const character = characterTools.getCharacter(characterId);
201
+ if (character) {
202
+ characterTools.updateCharacter(characterId, { locationId: destinationId });
203
+ moved.push({ id: characterId, name: character.name });
204
+ }
205
+ else {
206
+ errors.push({ characterId, error: "Character not found" });
207
+ }
208
+ }
209
+ catch (e) {
210
+ errors.push({ characterId, error: e.message });
211
+ }
212
+ }
213
+ // 2. Log the narrative event
214
+ narrativeTools.logEvent({
215
+ gameId,
216
+ eventType: "scene",
217
+ content: narrativeDescription,
218
+ metadata: {
219
+ characterIds: moved.map((m) => m.id),
220
+ locationId: destinationId,
221
+ },
222
+ });
223
+ // 3. Get destination location details
224
+ const destination = worldTools.getLocation(destinationId);
225
+ const result = {
226
+ moved,
227
+ errors,
228
+ destination: destination ? { id: destination.id, name: destination.name } : null,
229
+ summary: `Moved ${moved.length} characters to ${destination?.name || destinationId}`,
230
+ };
231
+ return {
232
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
233
+ structuredContent: result,
234
+ };
235
+ });
236
+ // ============================================================================
237
+ // GET CHARACTER CONTEXT - Character with all related data
238
+ // ============================================================================
239
+ server.registerTool("get_character_context", {
240
+ description: "Get comprehensive character context in one call: character details, inventory, and current location info. Reduces multiple tool calls to one.",
241
+ inputSchema: {
242
+ characterId: z.string().describe("The character ID"),
243
+ },
244
+ annotations: ANNOTATIONS.READ_ONLY,
245
+ }, async ({ characterId }) => {
246
+ const character = characterTools.getCharacter(characterId);
247
+ if (!character) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: JSON.stringify({
253
+ isError: true,
254
+ errorCode: "CHARACTER_NOT_FOUND",
255
+ message: `Character '${characterId}' not found`,
256
+ suggestions: [
257
+ "Use list_characters to see available characters",
258
+ "Create a character with create_character",
259
+ ],
260
+ }, null, 2),
261
+ },
262
+ ],
263
+ isError: true,
264
+ };
265
+ }
266
+ // Get inventory
267
+ const inventory = inventoryTools.getInventory(characterId, "character");
268
+ // Get location details if available
269
+ let location = null;
270
+ if (character.locationId) {
271
+ location = worldTools.getLocation(character.locationId);
272
+ }
273
+ const result = {
274
+ character,
275
+ inventory,
276
+ location: location
277
+ ? {
278
+ id: location.id,
279
+ name: location.name,
280
+ description: location.description,
281
+ }
282
+ : null,
283
+ summary: `${character.name} at ${location?.name || "unknown location"} with ${inventory.length} items`,
284
+ };
285
+ return {
286
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
287
+ structuredContent: result,
288
+ };
289
+ });
290
+ // ============================================================================
291
+ // GET LOCATION CONTEXT - Location with all related data
292
+ // ============================================================================
293
+ server.registerTool("get_location_context", {
294
+ description: "Get comprehensive location context in one call: location details, present characters, and items here. Reduces multiple tool calls to one.",
295
+ inputSchema: {
296
+ locationId: z.string().describe("The location ID"),
297
+ gameId: z.string().describe("The game ID (needed for character lookup)"),
298
+ },
299
+ annotations: ANNOTATIONS.READ_ONLY,
300
+ }, async ({ locationId, gameId }) => {
301
+ const location = worldTools.getLocation(locationId);
302
+ if (!location) {
303
+ return {
304
+ content: [
305
+ {
306
+ type: "text",
307
+ text: JSON.stringify({
308
+ isError: true,
309
+ errorCode: "LOCATION_NOT_FOUND",
310
+ message: `Location '${locationId}' not found`,
311
+ suggestions: [
312
+ "Use list_locations to see available locations",
313
+ "Create a location with create_location",
314
+ ],
315
+ }, null, 2),
316
+ },
317
+ ],
318
+ isError: true,
319
+ };
320
+ }
321
+ // Get characters at this location
322
+ const characters = characterTools.listCharacters(gameId, { locationId });
323
+ // Get items at this location
324
+ const items = inventoryTools.getInventory(locationId, "location");
325
+ const result = {
326
+ location,
327
+ characters: characters.map((c) => ({
328
+ id: c.id,
329
+ name: c.name,
330
+ isPlayer: c.isPlayer,
331
+ })),
332
+ items: items.map((i) => ({
333
+ id: i.id,
334
+ name: i.name,
335
+ })),
336
+ summary: `${location.name}: ${characters.length} characters, ${items.length} items`,
337
+ };
338
+ return {
339
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
340
+ structuredContent: result,
341
+ };
342
+ });
343
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerCharacterTools(server: McpServer): void;
@@ -0,0 +1,324 @@
1
+ import { z } from "zod";
2
+ import * as characterTools from "../tools/character.js";
3
+ import { imageGenSchema, voiceSchema } from "../schemas/index.js";
4
+ import { LIMITS } from "../utils/validation.js";
5
+ import { ANNOTATIONS } from "../utils/tool-annotations.js";
6
+ import { characterOutputSchema, characterStatusSchema, conditionModifyOutputSchema, } from "../utils/output-schemas.js";
7
+ import { verbositySchema, applyVerbosity } from "../utils/verbosity.js";
8
+ export function registerCharacterTools(server) {
9
+ // ============================================================================
10
+ // CREATE CHARACTER - with annotations and output schema
11
+ // ============================================================================
12
+ server.registerTool("create_character", {
13
+ description: "Create a new character (PC or NPC). IMPORTANT: Call this IMMEDIATELY when introducing any named NPC in narrative, BEFORE continuing the scene. Every named character mentioned in dialogue or description should exist in the database.",
14
+ inputSchema: {
15
+ gameId: z.string().max(100).describe("The game ID"),
16
+ name: z.string().min(1).max(LIMITS.NAME_MAX).describe("Character name"),
17
+ isPlayer: z.boolean().describe("True for player character, false for NPC"),
18
+ attributes: z.record(z.string().max(50), z.number()).optional().describe("Character attributes"),
19
+ skills: z.record(z.string().max(50), z.number()).optional().describe("Character skills"),
20
+ status: z.object({
21
+ health: z.number().optional(),
22
+ maxHealth: z.number().optional(),
23
+ conditions: z.array(z.string().max(100)).max(LIMITS.ARRAY_MAX).optional(),
24
+ experience: z.number().optional(),
25
+ level: z.number().optional(),
26
+ }).optional().describe("Initial status"),
27
+ locationId: z.string().max(100).optional().describe("Starting location"),
28
+ notes: z.string().max(LIMITS.CONTENT_MAX).optional().describe("Character notes"),
29
+ voice: voiceSchema.optional().describe("Voice characteristics for TTS/voice mode"),
30
+ imageGen: imageGenSchema.optional().describe("Image generation metadata for character portraits"),
31
+ },
32
+ outputSchema: characterOutputSchema,
33
+ annotations: ANNOTATIONS.CREATE,
34
+ }, async (params) => {
35
+ const character = characterTools.createCharacter(params);
36
+ return {
37
+ content: [{ type: "text", text: JSON.stringify(character, null, 2) }],
38
+ structuredContent: character,
39
+ };
40
+ });
41
+ // ============================================================================
42
+ // GET CHARACTER - read-only with output schema
43
+ // ============================================================================
44
+ server.registerTool("get_character", {
45
+ description: "Get character details",
46
+ inputSchema: {
47
+ characterId: z.string().describe("The character ID"),
48
+ },
49
+ outputSchema: characterOutputSchema,
50
+ annotations: ANNOTATIONS.READ_ONLY,
51
+ }, async ({ characterId }) => {
52
+ const character = characterTools.getCharacter(characterId);
53
+ if (!character) {
54
+ return {
55
+ content: [{ type: "text", text: "Character not found" }],
56
+ isError: true,
57
+ };
58
+ }
59
+ return {
60
+ content: [{ type: "text", text: JSON.stringify(character, null, 2) }],
61
+ structuredContent: character,
62
+ };
63
+ });
64
+ // ============================================================================
65
+ // UPDATE CHARACTER
66
+ // ============================================================================
67
+ server.registerTool("update_character", {
68
+ description: "Update character attributes, skills, status, or voice",
69
+ inputSchema: {
70
+ characterId: z.string().describe("The character ID"),
71
+ name: z.string().optional().describe("New name"),
72
+ attributes: z.record(z.string(), z.number()).optional().describe("Attribute updates"),
73
+ skills: z.record(z.string(), z.number()).optional().describe("Skill updates"),
74
+ status: z.record(z.string(), z.unknown()).optional().describe("Status updates"),
75
+ locationId: z.string().optional().describe("New location"),
76
+ notes: z.string().optional().describe("Notes update"),
77
+ voice: voiceSchema.nullable().optional().describe("Voice characteristics (null to remove)"),
78
+ imageGen: imageGenSchema.nullable().optional().describe("Image generation metadata (null to remove)"),
79
+ },
80
+ outputSchema: characterOutputSchema,
81
+ annotations: ANNOTATIONS.UPDATE,
82
+ }, async ({ characterId, ...updates }) => {
83
+ const character = characterTools.updateCharacter(characterId, updates);
84
+ if (!character) {
85
+ return {
86
+ content: [{ type: "text", text: "Character not found" }],
87
+ isError: true,
88
+ };
89
+ }
90
+ return {
91
+ content: [{ type: "text", text: JSON.stringify(character, null, 2) }],
92
+ structuredContent: character,
93
+ };
94
+ });
95
+ // ============================================================================
96
+ // LIST CHARACTERS - read-only with verbosity control
97
+ // ============================================================================
98
+ server.registerTool("list_characters", {
99
+ description: "List characters in a game. Use verbosity to control response size: 'minimal' for quick lookups (id/name only), 'standard' for common fields, 'full' for all details.",
100
+ inputSchema: {
101
+ gameId: z.string().describe("The game ID"),
102
+ isPlayer: z.boolean().optional().describe("Filter by player/NPC"),
103
+ locationId: z.string().optional().describe("Filter by location"),
104
+ verbosity: verbositySchema,
105
+ },
106
+ outputSchema: {
107
+ characters: z.array(z.object({
108
+ id: z.string(),
109
+ name: z.string(),
110
+ isPlayer: z.boolean().optional(),
111
+ status: characterStatusSchema.optional(),
112
+ locationId: z.string().nullable().optional(),
113
+ })),
114
+ },
115
+ annotations: ANNOTATIONS.READ_ONLY,
116
+ }, async ({ gameId, isPlayer, locationId, verbosity = 'standard' }) => {
117
+ const characters = characterTools.listCharacters(gameId, { isPlayer, locationId });
118
+ const filtered = applyVerbosity(characters, 'character', verbosity);
119
+ const output = { characters: filtered };
120
+ return {
121
+ content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }],
122
+ structuredContent: output,
123
+ };
124
+ });
125
+ // ============================================================================
126
+ // MOVE CHARACTER
127
+ // ============================================================================
128
+ server.registerTool("move_character", {
129
+ description: "Move a character to a different location",
130
+ inputSchema: {
131
+ characterId: z.string().describe("The character ID"),
132
+ locationId: z.string().describe("The destination location ID"),
133
+ },
134
+ outputSchema: {
135
+ success: z.boolean(),
136
+ characterId: z.string(),
137
+ newLocationId: z.string(),
138
+ tip: z.string().optional(),
139
+ },
140
+ annotations: ANNOTATIONS.UPDATE,
141
+ }, async ({ characterId, locationId }) => {
142
+ const success = characterTools.moveCharacter(characterId, locationId);
143
+ if (!success) {
144
+ return {
145
+ content: [{ type: "text", text: "Failed to move character" }],
146
+ isError: true,
147
+ };
148
+ }
149
+ const output = {
150
+ success: true,
151
+ characterId,
152
+ newLocationId: locationId,
153
+ tip: "Consider describing the new location to the player or showing relevant details about what they see.",
154
+ };
155
+ return {
156
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
157
+ structuredContent: output,
158
+ };
159
+ });
160
+ // ============================================================================
161
+ // MODIFY HEALTH - CONSOLIDATED (replaces apply_damage + heal_character)
162
+ // ============================================================================
163
+ server.registerTool("modify_health", {
164
+ description: "Modify a character's health. Use mode 'damage' to reduce health, or 'heal' to restore health.",
165
+ inputSchema: {
166
+ characterId: z.string().describe("The character ID"),
167
+ mode: z.enum(["damage", "heal"]).describe("'damage' to reduce health, 'heal' to restore health"),
168
+ amount: z.number().describe("Amount of damage or healing"),
169
+ },
170
+ outputSchema: {
171
+ characterId: z.string(),
172
+ previousHealth: z.number(),
173
+ newHealth: z.number(),
174
+ maxHealth: z.number(),
175
+ mode: z.enum(["damage", "heal"]),
176
+ },
177
+ annotations: ANNOTATIONS.UPDATE,
178
+ }, async ({ characterId, mode, amount }) => {
179
+ const result = characterTools.modifyHealth(characterId, { mode, amount });
180
+ if (!result) {
181
+ return {
182
+ content: [{ type: "text", text: "Character not found" }],
183
+ isError: true,
184
+ };
185
+ }
186
+ const output = {
187
+ characterId,
188
+ previousHealth: result.previousHealth,
189
+ newHealth: result.newHealth,
190
+ maxHealth: result.character.status.maxHealth,
191
+ mode,
192
+ };
193
+ return {
194
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
195
+ structuredContent: output,
196
+ };
197
+ });
198
+ // ============================================================================
199
+ // MODIFY CONDITIONS - CONSOLIDATED (replaces add_condition + remove_condition)
200
+ // ============================================================================
201
+ server.registerTool("modify_conditions", {
202
+ description: "Add and/or remove conditions from a character in a single call. More efficient than separate add/remove calls.",
203
+ inputSchema: {
204
+ characterId: z.string().describe("The character ID"),
205
+ add: z.array(z.string().max(100)).max(LIMITS.ARRAY_MAX).optional().describe("Conditions to add"),
206
+ remove: z.array(z.string().max(100)).max(LIMITS.ARRAY_MAX).optional().describe("Conditions to remove"),
207
+ },
208
+ outputSchema: conditionModifyOutputSchema,
209
+ annotations: ANNOTATIONS.UPDATE,
210
+ }, async ({ characterId, add, remove }) => {
211
+ if (!add?.length && !remove?.length) {
212
+ return {
213
+ content: [{ type: "text", text: "No conditions to add or remove" }],
214
+ isError: true,
215
+ };
216
+ }
217
+ const result = characterTools.modifyConditions(characterId, { add, remove });
218
+ if (!result) {
219
+ return {
220
+ content: [{ type: "text", text: "Character not found" }],
221
+ isError: true,
222
+ };
223
+ }
224
+ const output = {
225
+ characterId: result.character.id,
226
+ characterName: result.character.name,
227
+ conditions: result.character.status.conditions,
228
+ action: result.added.length > 0 && result.removed.length > 0
229
+ ? "modified"
230
+ : result.added.length > 0
231
+ ? "added"
232
+ : "removed",
233
+ added: result.added,
234
+ removed: result.removed,
235
+ };
236
+ return {
237
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
238
+ structuredContent: output,
239
+ };
240
+ });
241
+ // ============================================================================
242
+ // GET CHARACTER BY NAME - lookup by name instead of UUID
243
+ // ============================================================================
244
+ server.registerTool("get_character_by_name", {
245
+ description: "Look up a character by name within a game. Supports exact, partial, and fuzzy matching. Returns the best match or an error if no reasonable match found.",
246
+ inputSchema: {
247
+ gameId: z.string().describe("The game ID to search within"),
248
+ name: z.string().describe("Character name to search for (case-insensitive)"),
249
+ },
250
+ outputSchema: characterOutputSchema,
251
+ annotations: ANNOTATIONS.READ_ONLY,
252
+ }, async ({ gameId, name }) => {
253
+ const character = characterTools.getCharacterByName(gameId, name);
254
+ if (!character) {
255
+ return {
256
+ content: [{ type: "text", text: `No character found matching "${name}"` }],
257
+ isError: true,
258
+ };
259
+ }
260
+ return {
261
+ content: [{ type: "text", text: JSON.stringify(character, null, 2) }],
262
+ structuredContent: character,
263
+ };
264
+ });
265
+ // ============================================================================
266
+ // LIST CHARACTER SUMMARIES - quick identification info
267
+ // ============================================================================
268
+ server.registerTool("list_character_summaries", {
269
+ description: "List characters with quick-identification summaries. Use this to help identify characters by appearance before looking up their full details. More efficient than loading full character data when you just need to identify who's who.",
270
+ inputSchema: {
271
+ gameId: z.string().describe("The game ID"),
272
+ },
273
+ outputSchema: {
274
+ summaries: z.array(z.object({
275
+ id: z.string(),
276
+ name: z.string(),
277
+ isPlayer: z.boolean(),
278
+ summary: z.string().describe("One-line summary: gender, age, role"),
279
+ hasImageGen: z.boolean().describe("Whether character has imageGen data"),
280
+ hasImages: z.boolean().describe("Whether character has stored images"),
281
+ })),
282
+ },
283
+ annotations: ANNOTATIONS.READ_ONLY,
284
+ }, async ({ gameId }) => {
285
+ // Dynamic import to avoid circular dependency
286
+ const { listCharacterSummaries } = await import("../tools/image-prompt.js");
287
+ const summaries = listCharacterSummaries(gameId);
288
+ const output = { summaries };
289
+ return {
290
+ content: [{ type: "text", text: JSON.stringify(summaries, null, 2) }],
291
+ structuredContent: output,
292
+ };
293
+ });
294
+ // ============================================================================
295
+ // DELETE CHARACTER - destructive operation
296
+ // ============================================================================
297
+ server.registerTool("delete_character", {
298
+ description: "Delete a character permanently. This is IRREVERSIBLE and will remove the character and all associated data.",
299
+ inputSchema: {
300
+ characterId: z.string().describe("The character ID to delete"),
301
+ },
302
+ outputSchema: {
303
+ success: z.boolean(),
304
+ deletedId: z.string(),
305
+ },
306
+ annotations: ANNOTATIONS.DELETE,
307
+ }, async ({ characterId }) => {
308
+ const success = characterTools.deleteCharacter(characterId);
309
+ if (!success) {
310
+ return {
311
+ content: [{ type: "text", text: "Character not found or already deleted" }],
312
+ isError: true,
313
+ };
314
+ }
315
+ const output = {
316
+ success: true,
317
+ deletedId: characterId,
318
+ };
319
+ return {
320
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
321
+ structuredContent: output,
322
+ };
323
+ });
324
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerCombatTools(server: McpServer): void;