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.
- package/LICENSE +22 -0
- package/README.md +79 -0
- package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
- package/dist/__tests__/engineVocabulary.test.js +147 -0
- package/dist/db/__tests__/connection.test.d.ts +1 -0
- package/dist/db/__tests__/connection.test.js +72 -0
- package/dist/db/__tests__/testDb.d.ts +33 -0
- package/dist/db/__tests__/testDb.js +41 -0
- package/dist/db/connection.d.ts +22 -0
- package/dist/db/connection.js +107 -0
- package/dist/db/schema.d.ts +1 -0
- package/dist/db/schema.js +725 -0
- package/dist/events/emitter.d.ts +22 -0
- package/dist/events/emitter.js +71 -0
- package/dist/http/server.d.ts +3 -0
- package/dist/http/server.js +649 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +92 -0
- package/dist/register/abilities.d.ts +2 -0
- package/dist/register/abilities.js +165 -0
- package/dist/register/audio.d.ts +2 -0
- package/dist/register/audio.js +326 -0
- package/dist/register/batch.d.ts +2 -0
- package/dist/register/batch.js +343 -0
- package/dist/register/character.d.ts +2 -0
- package/dist/register/character.js +324 -0
- package/dist/register/combat.d.ts +2 -0
- package/dist/register/combat.js +207 -0
- package/dist/register/core.d.ts +2 -0
- package/dist/register/core.js +1040 -0
- package/dist/register/display.d.ts +2 -0
- package/dist/register/display.js +263 -0
- package/dist/register/factions.d.ts +2 -0
- package/dist/register/factions.js +186 -0
- package/dist/register/images.d.ts +2 -0
- package/dist/register/images.js +400 -0
- package/dist/register/inventory.d.ts +2 -0
- package/dist/register/inventory.js +115 -0
- package/dist/register/mcp-prompts.d.ts +2 -0
- package/dist/register/mcp-prompts.js +684 -0
- package/dist/register/mcp-resources.d.ts +2 -0
- package/dist/register/mcp-resources.js +335 -0
- package/dist/register/narrative.d.ts +2 -0
- package/dist/register/narrative.js +242 -0
- package/dist/register/notes.d.ts +2 -0
- package/dist/register/notes.js +170 -0
- package/dist/register/pause.d.ts +2 -0
- package/dist/register/pause.js +580 -0
- package/dist/register/quests.d.ts +2 -0
- package/dist/register/quests.js +118 -0
- package/dist/register/relationships.d.ts +2 -0
- package/dist/register/relationships.js +147 -0
- package/dist/register/resources.d.ts +2 -0
- package/dist/register/resources.js +277 -0
- package/dist/register/secrets.d.ts +2 -0
- package/dist/register/secrets.js +192 -0
- package/dist/register/status.d.ts +2 -0
- package/dist/register/status.js +130 -0
- package/dist/register/tables.d.ts +2 -0
- package/dist/register/tables.js +146 -0
- package/dist/register/tags.d.ts +2 -0
- package/dist/register/tags.js +114 -0
- package/dist/register/time.d.ts +2 -0
- package/dist/register/time.js +281 -0
- package/dist/register/world.d.ts +2 -0
- package/dist/register/world.js +127 -0
- package/dist/schemas/index.d.ts +921 -0
- package/dist/schemas/index.js +121 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +13 -0
- package/dist/tools/__tests__/audio.test.d.ts +1 -0
- package/dist/tools/__tests__/audio.test.js +59 -0
- package/dist/tools/__tests__/conserved.test.d.ts +1 -0
- package/dist/tools/__tests__/conserved.test.js +488 -0
- package/dist/tools/__tests__/constraint.test.d.ts +1 -0
- package/dist/tools/__tests__/constraint.test.js +212 -0
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
- package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
- package/dist/tools/__tests__/images.test.d.ts +1 -0
- package/dist/tools/__tests__/images.test.js +59 -0
- package/dist/tools/__tests__/relationship.test.d.ts +1 -0
- package/dist/tools/__tests__/relationship.test.js +132 -0
- package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
- package/dist/tools/__tests__/resource-constraints.test.js +131 -0
- package/dist/tools/__tests__/resource.test.d.ts +1 -0
- package/dist/tools/__tests__/resource.test.js +190 -0
- package/dist/tools/__tests__/time.test.d.ts +1 -0
- package/dist/tools/__tests__/time.test.js +404 -0
- package/dist/tools/__tests__/timers.test.d.ts +1 -0
- package/dist/tools/__tests__/timers.test.js +426 -0
- package/dist/tools/__tests__/world.test.d.ts +1 -0
- package/dist/tools/__tests__/world.test.js +70 -0
- package/dist/tools/ability.d.ts +48 -0
- package/dist/tools/ability.js +238 -0
- package/dist/tools/audio.d.ts +24 -0
- package/dist/tools/audio.js +365 -0
- package/dist/tools/character.d.ts +70 -0
- package/dist/tools/character.js +309 -0
- package/dist/tools/combat.d.ts +13 -0
- package/dist/tools/combat.js +195 -0
- package/dist/tools/constraint.d.ts +132 -0
- package/dist/tools/constraint.js +269 -0
- package/dist/tools/dice.d.ts +23 -0
- package/dist/tools/dice.js +111 -0
- package/dist/tools/display.d.ts +120 -0
- package/dist/tools/display.js +528 -0
- package/dist/tools/faction.d.ts +61 -0
- package/dist/tools/faction.js +269 -0
- package/dist/tools/game.d.ts +96 -0
- package/dist/tools/game.js +526 -0
- package/dist/tools/image-prompt.d.ts +49 -0
- package/dist/tools/image-prompt.js +479 -0
- package/dist/tools/images.d.ts +47 -0
- package/dist/tools/images.js +449 -0
- package/dist/tools/inventory.d.ts +20 -0
- package/dist/tools/inventory.js +145 -0
- package/dist/tools/narrative.d.ts +58 -0
- package/dist/tools/narrative.js +237 -0
- package/dist/tools/notes.d.ts +41 -0
- package/dist/tools/notes.js +220 -0
- package/dist/tools/pause.d.ts +110 -0
- package/dist/tools/pause.js +1254 -0
- package/dist/tools/quest.d.ts +34 -0
- package/dist/tools/quest.js +164 -0
- package/dist/tools/relationship.d.ts +74 -0
- package/dist/tools/relationship.js +324 -0
- package/dist/tools/resource.d.ts +93 -0
- package/dist/tools/resource.js +374 -0
- package/dist/tools/rules.d.ts +4 -0
- package/dist/tools/rules.js +30 -0
- package/dist/tools/secrets.d.ts +49 -0
- package/dist/tools/secrets.js +195 -0
- package/dist/tools/status.d.ts +36 -0
- package/dist/tools/status.js +218 -0
- package/dist/tools/tables.d.ts +33 -0
- package/dist/tools/tables.js +209 -0
- package/dist/tools/tags.d.ts +52 -0
- package/dist/tools/tags.js +176 -0
- package/dist/tools/time.d.ts +33 -0
- package/dist/tools/time.js +276 -0
- package/dist/tools/timers.d.ts +41 -0
- package/dist/tools/timers.js +215 -0
- package/dist/tools/world.d.ts +78 -0
- package/dist/tools/world.js +331 -0
- package/dist/types/index.d.ts +969 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/__tests__/json.test.d.ts +1 -0
- package/dist/utils/__tests__/json.test.js +55 -0
- package/dist/utils/__tests__/validation.test.d.ts +1 -0
- package/dist/utils/__tests__/validation.test.js +90 -0
- package/dist/utils/errors.d.ts +44 -0
- package/dist/utils/errors.js +121 -0
- package/dist/utils/json.d.ts +9 -0
- package/dist/utils/json.js +23 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.js +50 -0
- package/dist/utils/output-schemas.d.ts +594 -0
- package/dist/utils/output-schemas.js +331 -0
- package/dist/utils/tool-annotations.d.ts +147 -0
- package/dist/utils/tool-annotations.js +98 -0
- package/dist/utils/validation.d.ts +34 -0
- package/dist/utils/validation.js +52 -0
- package/dist/utils/verbosity.d.ts +57 -0
- package/dist/utils/verbosity.js +67 -0
- package/dist/utils/webui.d.ts +20 -0
- package/dist/utils/webui.js +35 -0
- 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,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
|
+
}
|