run-dmcp 0.1.0 → 0.2.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/README.md +76 -10
- package/dist/bin/run-dmcp.d.ts +2 -0
- package/dist/bin/run-dmcp.js +55 -0
- package/dist/db/connection.d.ts +32 -0
- package/dist/db/connection.js +38 -16
- package/dist/db/schema.d.ts +29 -1
- package/dist/db/schema.js +439 -7
- package/dist/http/server.js +3 -3
- package/dist/index.d.ts +36 -2
- package/dist/index.js +184 -92
- package/dist/mcp-server.d.ts +49 -0
- package/dist/mcp-server.js +127 -0
- package/dist/reader/turnReader.d.ts +185 -0
- package/dist/reader/turnReader.js +288 -0
- package/dist/register/batch.js +5 -79
- package/dist/register/mcp-resources.d.ts +9 -0
- package/dist/register/mcp-resources.js +16 -62
- package/dist/register/render.d.ts +17 -0
- package/dist/register/render.js +50 -0
- package/dist/register/resolve.d.ts +14 -0
- package/dist/register/resolve.js +102 -0
- package/dist/register/resources.js +11 -4
- package/dist/register/timeline.d.ts +2 -0
- package/dist/register/timeline.js +311 -0
- package/dist/rpg/index.d.ts +29 -0
- package/dist/rpg/index.js +55 -0
- package/dist/rpg/register/abilities.d.ts +2 -0
- package/dist/rpg/register/abilities.js +165 -0
- package/dist/rpg/register/batch.d.ts +2 -0
- package/dist/rpg/register/batch.js +92 -0
- package/dist/rpg/register/combat.d.ts +2 -0
- package/dist/rpg/register/combat.js +207 -0
- package/dist/rpg/register/mcp-prompts.d.ts +2 -0
- package/dist/rpg/register/mcp-prompts.js +684 -0
- package/dist/rpg/register/mcp-resources.d.ts +2 -0
- package/dist/rpg/register/mcp-resources.js +61 -0
- package/dist/rpg/register/quests.d.ts +2 -0
- package/dist/rpg/register/quests.js +118 -0
- package/dist/rpg/register/status.d.ts +2 -0
- package/dist/rpg/register/status.js +130 -0
- package/dist/rpg/register/tables.d.ts +2 -0
- package/dist/rpg/register/tables.js +146 -0
- package/dist/rpg/tools/ability.d.ts +48 -0
- package/dist/rpg/tools/ability.js +238 -0
- package/dist/rpg/tools/combat.d.ts +13 -0
- package/dist/rpg/tools/combat.js +195 -0
- package/dist/rpg/tools/dice.d.ts +23 -0
- package/dist/rpg/tools/dice.js +111 -0
- package/dist/rpg/tools/quest.d.ts +34 -0
- package/dist/rpg/tools/quest.js +164 -0
- package/dist/rpg/tools/status.d.ts +36 -0
- package/dist/rpg/tools/status.js +218 -0
- package/dist/rpg/tools/tables.d.ts +33 -0
- package/dist/rpg/tools/tables.js +209 -0
- package/dist/schemas/index.d.ts +12 -12
- package/dist/timeline/adjudication.d.ts +150 -0
- package/dist/timeline/adjudication.js +174 -0
- package/dist/timeline/changes.d.ts +100 -0
- package/dist/timeline/changes.js +161 -0
- package/dist/timeline/checkpoint.d.ts +69 -0
- package/dist/timeline/checkpoint.js +131 -0
- package/dist/timeline/clock.d.ts +89 -0
- package/dist/timeline/clock.js +173 -0
- package/dist/timeline/constrained.d.ts +220 -0
- package/dist/timeline/constrained.js +671 -0
- package/dist/timeline/export.d.ts +171 -0
- package/dist/timeline/export.js +329 -0
- package/dist/timeline/irreversible.d.ts +85 -0
- package/dist/timeline/irreversible.js +108 -0
- package/dist/timeline/kinds.d.ts +14 -0
- package/dist/timeline/kinds.js +22 -0
- package/dist/timeline/narration.d.ts +175 -0
- package/dist/timeline/narration.js +259 -0
- package/dist/timeline/projection.d.ts +97 -0
- package/dist/timeline/projection.js +330 -0
- package/dist/timeline/provenance.d.ts +66 -0
- package/dist/timeline/provenance.js +45 -0
- package/dist/timeline/registry.d.ts +95 -0
- package/dist/timeline/registry.js +124 -0
- package/dist/timeline/render.d.ts +121 -0
- package/dist/timeline/render.js +187 -0
- package/dist/timeline/replay.d.ts +64 -0
- package/dist/timeline/replay.js +104 -0
- package/dist/timeline/resolve.d.ts +262 -0
- package/dist/timeline/resolve.js +226 -0
- package/dist/timeline/schema.d.ts +13 -0
- package/dist/timeline/schema.js +262 -0
- package/dist/timeline/t.d.ts +80 -0
- package/dist/timeline/t.js +37 -0
- package/dist/tools/constraint.d.ts +44 -80
- package/dist/tools/constraint.js +115 -124
- package/dist/tools/relationship.d.ts +83 -2
- package/dist/tools/relationship.js +139 -62
- package/dist/tools/resource.d.ts +31 -6
- package/dist/tools/resource.js +106 -153
- package/dist/types/index.d.ts +19 -1
- package/dist/utils/output-schemas.d.ts +593 -2
- package/dist/utils/output-schemas.js +3 -0
- package/dist/utils/webui.d.ts +32 -0
- package/dist/utils/webui.js +54 -1
- package/package.json +20 -4
- package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
- package/dist/__tests__/engineVocabulary.test.js +0 -147
- package/dist/db/__tests__/connection.test.d.ts +0 -1
- package/dist/db/__tests__/connection.test.js +0 -72
- package/dist/db/__tests__/testDb.d.ts +0 -33
- package/dist/db/__tests__/testDb.js +0 -41
- package/dist/test-setup.d.ts +0 -1
- package/dist/test-setup.js +0 -13
- package/dist/tools/__tests__/audio.test.d.ts +0 -1
- package/dist/tools/__tests__/audio.test.js +0 -59
- package/dist/tools/__tests__/conserved.test.d.ts +0 -1
- package/dist/tools/__tests__/conserved.test.js +0 -488
- package/dist/tools/__tests__/constraint.test.d.ts +0 -1
- package/dist/tools/__tests__/constraint.test.js +0 -212
- package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
- package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
- package/dist/tools/__tests__/images.test.d.ts +0 -1
- package/dist/tools/__tests__/images.test.js +0 -59
- package/dist/tools/__tests__/relationship.test.d.ts +0 -1
- package/dist/tools/__tests__/relationship.test.js +0 -132
- package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
- package/dist/tools/__tests__/resource-constraints.test.js +0 -131
- package/dist/tools/__tests__/resource.test.d.ts +0 -1
- package/dist/tools/__tests__/resource.test.js +0 -190
- package/dist/tools/__tests__/time.test.d.ts +0 -1
- package/dist/tools/__tests__/time.test.js +0 -404
- package/dist/tools/__tests__/timers.test.d.ts +0 -1
- package/dist/tools/__tests__/timers.test.js +0 -426
- package/dist/tools/__tests__/world.test.d.ts +0 -1
- package/dist/tools/__tests__/world.test.js +0 -70
- package/dist/utils/__tests__/json.test.d.ts +0 -1
- package/dist/utils/__tests__/json.test.js +0 -55
- package/dist/utils/__tests__/validation.test.d.ts +0 -1
- package/dist/utils/__tests__/validation.test.js +0 -90
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { Mechanic } from "../timeline/resolve.js";
|
|
3
|
+
import type { RenderVocabulary } from "../timeline/render.js";
|
|
4
|
+
/**
|
|
5
|
+
* Register every RPG-layer tool, resource and prompt onto an existing
|
|
6
|
+
* server. Combat brings dice with it (registerCombatTools registers both,
|
|
7
|
+
* matching how the pre-split assembly registered them as one domain).
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerRpgTools(server: McpServer): void;
|
|
10
|
+
/**
|
|
11
|
+
* Build the FULL assembly: every core tool plus every RPG tool, resource and
|
|
12
|
+
* prompt this engine has always served. Same name, same `{ mechanics?,
|
|
13
|
+
* vocabulary? }` options as the pre-split `createMcpServer` in
|
|
14
|
+
* src/mcp-server.ts -- this is that function, now composed from two layers
|
|
15
|
+
* instead of one, so src/bin/run-dmcp.ts's zero-argument call site keeps
|
|
16
|
+
* working unchanged and the registered tool/resource/prompt set is
|
|
17
|
+
* unchanged too (src/__tests__/layerBoundary.test.ts asserts this exactly).
|
|
18
|
+
*/
|
|
19
|
+
export declare function createMcpServer(options?: {
|
|
20
|
+
mechanics?: readonly Mechanic[];
|
|
21
|
+
vocabulary?: RenderVocabulary;
|
|
22
|
+
}): McpServer;
|
|
23
|
+
export { createHttpServer, startHttpServer } from "../http/server.js";
|
|
24
|
+
export * from "./tools/dice.js";
|
|
25
|
+
export * from "./tools/combat.js";
|
|
26
|
+
export * from "./tools/ability.js";
|
|
27
|
+
export * from "./tools/status.js";
|
|
28
|
+
export * from "./tools/tables.js";
|
|
29
|
+
export * from "./tools/quest.js";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createCoreMcpServer } from "../mcp-server.js";
|
|
2
|
+
import { registerCombatTools } from "./register/combat.js";
|
|
3
|
+
import { registerAbilityTools } from "./register/abilities.js";
|
|
4
|
+
import { registerStatusTools } from "./register/status.js";
|
|
5
|
+
import { registerTableTools } from "./register/tables.js";
|
|
6
|
+
import { registerQuestTools } from "./register/quests.js";
|
|
7
|
+
import { registerRpgBatchTools } from "./register/batch.js";
|
|
8
|
+
import { registerRpgMcpResources } from "./register/mcp-resources.js";
|
|
9
|
+
import { registerMcpPrompts } from "./register/mcp-prompts.js";
|
|
10
|
+
/**
|
|
11
|
+
* Register every RPG-layer tool, resource and prompt onto an existing
|
|
12
|
+
* server. Combat brings dice with it (registerCombatTools registers both,
|
|
13
|
+
* matching how the pre-split assembly registered them as one domain).
|
|
14
|
+
*/
|
|
15
|
+
export function registerRpgTools(server) {
|
|
16
|
+
registerCombatTools(server); // Combat, Dice, Checks
|
|
17
|
+
registerQuestTools(server); // Quests, Objectives
|
|
18
|
+
registerTableTools(server); // Random Tables
|
|
19
|
+
registerStatusTools(server); // Status Effects
|
|
20
|
+
registerAbilityTools(server); // Abilities/Powers
|
|
21
|
+
registerRpgBatchTools(server); // setup_combat_encounter
|
|
22
|
+
registerRpgMcpResources(server); // game-quests, quest
|
|
23
|
+
registerMcpPrompts(server); // Reusable prompt templates (game-master session library)
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build the FULL assembly: every core tool plus every RPG tool, resource and
|
|
27
|
+
* prompt this engine has always served. Same name, same `{ mechanics?,
|
|
28
|
+
* vocabulary? }` options as the pre-split `createMcpServer` in
|
|
29
|
+
* src/mcp-server.ts -- this is that function, now composed from two layers
|
|
30
|
+
* instead of one, so src/bin/run-dmcp.ts's zero-argument call site keeps
|
|
31
|
+
* working unchanged and the registered tool/resource/prompt set is
|
|
32
|
+
* unchanged too (src/__tests__/layerBoundary.test.ts asserts this exactly).
|
|
33
|
+
*/
|
|
34
|
+
export function createMcpServer(options) {
|
|
35
|
+
const server = createCoreMcpServer(options);
|
|
36
|
+
registerRpgTools(server);
|
|
37
|
+
return server;
|
|
38
|
+
}
|
|
39
|
+
// The web UI. An application opts into serving it; importing this never
|
|
40
|
+
// does. It lives on disk at src/http/server.ts, not under src/rpg/ -- it
|
|
41
|
+
// imports RPG tools (quest/ability/combat), which is why it cannot be
|
|
42
|
+
// reachable from the core entry point, but its CLIENT_DIST path resolution
|
|
43
|
+
// is depth-sensitive in both src/ and compiled dist/, so it stays put and
|
|
44
|
+
// only its EXPORT moves up to this layer.
|
|
45
|
+
export { createHttpServer, startHttpServer } from "../http/server.js";
|
|
46
|
+
// The RPG tool modules, exported as library functions -- the same shape
|
|
47
|
+
// core's index.ts uses for the timeline (replay, changesWithin, and so on):
|
|
48
|
+
// a consumer that wants to call combat/quest/table/status/ability/dice logic
|
|
49
|
+
// directly, without going through an MCP tool call, can.
|
|
50
|
+
export * from "./tools/dice.js";
|
|
51
|
+
export * from "./tools/combat.js";
|
|
52
|
+
export * from "./tools/ability.js";
|
|
53
|
+
export * from "./tools/status.js";
|
|
54
|
+
export * from "./tools/tables.js";
|
|
55
|
+
export * from "./tools/quest.js";
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as abilityTools from "../tools/ability.js";
|
|
3
|
+
import { LIMITS } from "../../utils/validation.js";
|
|
4
|
+
import { ANNOTATIONS } from "../../utils/tool-annotations.js";
|
|
5
|
+
export function registerAbilityTools(server) {
|
|
6
|
+
server.registerTool("create_ability", {
|
|
7
|
+
description: "Create an ability template or character-owned ability",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
gameId: z.string().max(100).describe("The game ID"),
|
|
10
|
+
ownerType: z.enum(["template", "character"]).describe("'template' for reusable, 'character' for owned"),
|
|
11
|
+
ownerId: z.string().max(100).optional().describe("Character ID if ownerType is 'character'"),
|
|
12
|
+
name: z.string().min(1).max(LIMITS.NAME_MAX).describe("Ability name"),
|
|
13
|
+
description: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Ability description"),
|
|
14
|
+
category: z.string().max(100).optional().describe("Category (e.g., 'spell', 'skill', 'power')"),
|
|
15
|
+
cost: z.record(z.number()).optional().describe("Resource costs (e.g., {mana: 10, stamina: 5})"),
|
|
16
|
+
cooldown: z.number().optional().describe("Cooldown in rounds"),
|
|
17
|
+
effects: z.array(z.string().max(LIMITS.DESCRIPTION_MAX)).max(LIMITS.ARRAY_MAX).optional().describe("Effect descriptions"),
|
|
18
|
+
requirements: z.record(z.number()).optional().describe("Requirements (e.g., {level: 5, strength: 10})"),
|
|
19
|
+
tags: z.array(z.string().max(LIMITS.NAME_MAX)).max(LIMITS.ARRAY_MAX).optional().describe("Tags for categorization"),
|
|
20
|
+
},
|
|
21
|
+
annotations: ANNOTATIONS.CREATE,
|
|
22
|
+
}, async (params) => {
|
|
23
|
+
const ability = abilityTools.createAbility(params);
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: JSON.stringify(ability, null, 2) }],
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
server.registerTool("get_ability", {
|
|
29
|
+
description: "Get an ability by ID",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
abilityId: z.string().max(100).describe("The ability ID"),
|
|
32
|
+
},
|
|
33
|
+
annotations: ANNOTATIONS.READ_ONLY,
|
|
34
|
+
}, async ({ abilityId }) => {
|
|
35
|
+
const ability = abilityTools.getAbility(abilityId);
|
|
36
|
+
if (!ability) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: "Ability not found" }],
|
|
39
|
+
isError: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
content: [{ type: "text", text: JSON.stringify(ability, null, 2) }],
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
server.registerTool("update_ability", {
|
|
47
|
+
description: "Update an ability's details",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
abilityId: z.string().max(100).describe("The ability ID"),
|
|
50
|
+
name: z.string().max(LIMITS.NAME_MAX).optional().describe("New name"),
|
|
51
|
+
description: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("New description"),
|
|
52
|
+
category: z.string().max(100).nullable().optional().describe("New category"),
|
|
53
|
+
cost: z.record(z.number()).optional().describe("New cost"),
|
|
54
|
+
cooldown: z.number().nullable().optional().describe("New cooldown"),
|
|
55
|
+
effects: z.array(z.string().max(LIMITS.DESCRIPTION_MAX)).max(LIMITS.ARRAY_MAX).optional().describe("New effects"),
|
|
56
|
+
requirements: z.record(z.number()).optional().describe("New requirements"),
|
|
57
|
+
tags: z.array(z.string().max(LIMITS.NAME_MAX)).max(LIMITS.ARRAY_MAX).optional().describe("New tags"),
|
|
58
|
+
},
|
|
59
|
+
annotations: ANNOTATIONS.UPDATE,
|
|
60
|
+
}, async ({ abilityId, ...updates }) => {
|
|
61
|
+
const ability = abilityTools.updateAbility(abilityId, updates);
|
|
62
|
+
if (!ability) {
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: "Ability not found" }],
|
|
65
|
+
isError: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: JSON.stringify(ability, null, 2) }],
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
server.registerTool("delete_ability", {
|
|
73
|
+
description: "Delete an ability",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
abilityId: z.string().max(100).describe("The ability ID"),
|
|
76
|
+
},
|
|
77
|
+
annotations: ANNOTATIONS.DESTRUCTIVE,
|
|
78
|
+
}, async ({ abilityId }) => {
|
|
79
|
+
const success = abilityTools.deleteAbility(abilityId);
|
|
80
|
+
return {
|
|
81
|
+
content: [{ type: "text", text: success ? "Ability deleted" : "Ability not found" }],
|
|
82
|
+
isError: !success,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
server.registerTool("list_abilities", {
|
|
86
|
+
description: "List abilities with optional filters",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
gameId: z.string().max(100).describe("The game ID"),
|
|
89
|
+
ownerType: z.enum(["template", "character"]).optional().describe("Filter by owner type"),
|
|
90
|
+
ownerId: z.string().max(100).optional().describe("Filter by owner ID"),
|
|
91
|
+
category: z.string().max(100).optional().describe("Filter by category"),
|
|
92
|
+
},
|
|
93
|
+
annotations: ANNOTATIONS.READ_ONLY,
|
|
94
|
+
}, async ({ gameId, ...filter }) => {
|
|
95
|
+
const abilities = abilityTools.listAbilities(gameId, filter);
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: JSON.stringify(abilities, null, 2) }],
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
server.registerTool("learn_ability", {
|
|
101
|
+
description: "Copy a template ability to a character",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
templateId: z.string().max(100).describe("The template ability ID"),
|
|
104
|
+
characterId: z.string().max(100).describe("The character ID to learn the ability"),
|
|
105
|
+
},
|
|
106
|
+
annotations: ANNOTATIONS.CREATE,
|
|
107
|
+
}, async ({ templateId, characterId }) => {
|
|
108
|
+
const ability = abilityTools.learnAbility(templateId, characterId);
|
|
109
|
+
if (!ability) {
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: "text", text: "Template not found or is not a template" }],
|
|
112
|
+
isError: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: JSON.stringify(ability, null, 2) }],
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
server.registerTool("use_ability", {
|
|
120
|
+
description: "Use an ability (checks cooldown, sets new cooldown)",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
abilityId: z.string().max(100).describe("The ability ID"),
|
|
123
|
+
characterId: z.string().max(100).describe("The character using the ability"),
|
|
124
|
+
},
|
|
125
|
+
annotations: ANNOTATIONS.UPDATE,
|
|
126
|
+
}, async ({ abilityId, characterId }) => {
|
|
127
|
+
const result = abilityTools.useAbility(abilityId, characterId);
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
130
|
+
isError: !result.success,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
server.registerTool("tick_ability_cooldowns", {
|
|
134
|
+
description: "Reduce all ability cooldowns (call at end of round)",
|
|
135
|
+
inputSchema: {
|
|
136
|
+
gameId: z.string().max(100).describe("The game ID"),
|
|
137
|
+
amount: z.number().optional().describe("Rounds to tick (default: 1)"),
|
|
138
|
+
},
|
|
139
|
+
annotations: ANNOTATIONS.UPDATE,
|
|
140
|
+
}, async ({ gameId, amount }) => {
|
|
141
|
+
const updated = abilityTools.tickCooldowns(gameId, amount);
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: JSON.stringify({ updatedCount: updated.length, abilities: updated }, null, 2) }],
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
server.registerTool("check_ability_requirements", {
|
|
147
|
+
description: "Check if a character meets an ability's requirements",
|
|
148
|
+
inputSchema: {
|
|
149
|
+
abilityId: z.string().max(100).describe("The ability ID"),
|
|
150
|
+
characterId: z.string().max(100).describe("The character ID"),
|
|
151
|
+
},
|
|
152
|
+
annotations: ANNOTATIONS.READ_ONLY,
|
|
153
|
+
}, async ({ abilityId, characterId }) => {
|
|
154
|
+
const result = abilityTools.checkRequirements(abilityId, characterId);
|
|
155
|
+
if (!result) {
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: "text", text: "Ability or character not found" }],
|
|
158
|
+
isError: true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as characterTools from "../../tools/character.js";
|
|
3
|
+
import * as narrativeTools from "../../tools/narrative.js";
|
|
4
|
+
import * as combatTools from "../tools/combat.js";
|
|
5
|
+
import { LIMITS } from "../../utils/validation.js";
|
|
6
|
+
import { ANNOTATIONS } from "../../utils/tool-annotations.js";
|
|
7
|
+
// The one multi-entity workflow tool that reaches into combat, split out of
|
|
8
|
+
// src/register/batch.ts (design §8, issue #17): everything else there --
|
|
9
|
+
// batch_create_npcs, scene_transition, get_character_context,
|
|
10
|
+
// get_location_context -- touches only entity/property concepts and stays in
|
|
11
|
+
// core. This one calls combatTools.startCombat, so it is genuinely
|
|
12
|
+
// game-shaped and belongs in the RPG layer above it, not beside it.
|
|
13
|
+
export function registerRpgBatchTools(server) {
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// SETUP COMBAT ENCOUNTER - Create NPCs, start combat with all participants
|
|
16
|
+
// ============================================================================
|
|
17
|
+
server.registerTool("setup_combat_encounter", {
|
|
18
|
+
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.",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
gameId: z.string().describe("The game ID"),
|
|
21
|
+
locationId: z.string().describe("Location where combat takes place"),
|
|
22
|
+
enemies: z
|
|
23
|
+
.array(z.object({
|
|
24
|
+
name: z.string().min(1).max(LIMITS.NAME_MAX).describe("Enemy name"),
|
|
25
|
+
attributes: z.record(z.string(), z.number()).optional(),
|
|
26
|
+
status: z
|
|
27
|
+
.object({
|
|
28
|
+
health: z.number().optional(),
|
|
29
|
+
maxHealth: z.number().optional(),
|
|
30
|
+
})
|
|
31
|
+
.optional(),
|
|
32
|
+
}))
|
|
33
|
+
.min(1)
|
|
34
|
+
.max(10)
|
|
35
|
+
.describe("Enemies to create and add to combat"),
|
|
36
|
+
includePlayersAtLocation: z
|
|
37
|
+
.boolean()
|
|
38
|
+
.default(true)
|
|
39
|
+
.describe("Auto-add player characters at this location"),
|
|
40
|
+
},
|
|
41
|
+
annotations: ANNOTATIONS.CREATE,
|
|
42
|
+
}, async ({ gameId, locationId, enemies, includePlayersAtLocation = true }) => {
|
|
43
|
+
// 1. Create enemy NPCs
|
|
44
|
+
const createdEnemies = [];
|
|
45
|
+
for (const enemy of enemies) {
|
|
46
|
+
const character = characterTools.createCharacter({
|
|
47
|
+
gameId,
|
|
48
|
+
name: enemy.name,
|
|
49
|
+
isPlayer: false,
|
|
50
|
+
attributes: enemy.attributes,
|
|
51
|
+
status: enemy.status,
|
|
52
|
+
locationId,
|
|
53
|
+
});
|
|
54
|
+
createdEnemies.push(character);
|
|
55
|
+
}
|
|
56
|
+
// 2. Get players at location if needed
|
|
57
|
+
const participantIds = [...createdEnemies.map((e) => e.id)];
|
|
58
|
+
if (includePlayersAtLocation) {
|
|
59
|
+
const playersAtLocation = characterTools.listCharacters(gameId, {
|
|
60
|
+
isPlayer: true,
|
|
61
|
+
locationId,
|
|
62
|
+
});
|
|
63
|
+
participantIds.push(...playersAtLocation.map((p) => p.id));
|
|
64
|
+
}
|
|
65
|
+
// 3. Start combat with all participants
|
|
66
|
+
const combat = combatTools.startCombat({
|
|
67
|
+
gameId,
|
|
68
|
+
locationId,
|
|
69
|
+
participantIds,
|
|
70
|
+
});
|
|
71
|
+
// 4. Log the encounter start
|
|
72
|
+
narrativeTools.logEvent({
|
|
73
|
+
gameId,
|
|
74
|
+
eventType: "combat",
|
|
75
|
+
content: `Combat begins! ${createdEnemies.length} enemies attack.`,
|
|
76
|
+
metadata: {
|
|
77
|
+
enemyIds: createdEnemies.map((e) => e.id),
|
|
78
|
+
locationId,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const result = {
|
|
82
|
+
combat,
|
|
83
|
+
createdEnemies: createdEnemies.map((e) => ({ id: e.id, name: e.name })),
|
|
84
|
+
participantCount: participantIds.length,
|
|
85
|
+
summary: `Combat started with ${participantIds.length} participants. ${createdEnemies.length} enemies created.`,
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
89
|
+
structuredContent: result,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as combatTools from "../tools/combat.js";
|
|
3
|
+
import * as diceTools from "../tools/dice.js";
|
|
4
|
+
import { LIMITS } from "../../utils/validation.js";
|
|
5
|
+
import { ANNOTATIONS } from "../../utils/tool-annotations.js";
|
|
6
|
+
export function registerCombatTools(server) {
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// DICE & CHECK TOOLS
|
|
9
|
+
// ============================================================================
|
|
10
|
+
server.registerTool("roll", {
|
|
11
|
+
description: "Roll dice using standard notation",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
expression: z.string().max(100).describe("Dice expression (e.g., '2d6+3', '1d20', '4d6-2')"),
|
|
14
|
+
},
|
|
15
|
+
annotations: ANNOTATIONS.READ_ONLY,
|
|
16
|
+
}, async ({ expression }) => {
|
|
17
|
+
try {
|
|
18
|
+
const result = diceTools.roll(expression);
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: error.message }],
|
|
26
|
+
isError: true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
server.registerTool("check", {
|
|
31
|
+
description: "Perform a skill or ability check",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
gameId: z.string().max(100).describe("The game ID"),
|
|
34
|
+
characterId: z.string().max(100).describe("The character making the check"),
|
|
35
|
+
skill: z.string().max(100).optional().describe("Skill to use"),
|
|
36
|
+
attribute: z.string().max(100).optional().describe("Attribute to use"),
|
|
37
|
+
difficulty: z.number().describe("Difficulty threshold"),
|
|
38
|
+
bonusModifier: z.number().optional().describe("Additional modifier"),
|
|
39
|
+
},
|
|
40
|
+
annotations: ANNOTATIONS.READ_ONLY,
|
|
41
|
+
}, async (params) => {
|
|
42
|
+
try {
|
|
43
|
+
const result = diceTools.check(params);
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text", text: error.message }],
|
|
51
|
+
isError: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
server.registerTool("contest", {
|
|
56
|
+
description: "Opposed check between two characters",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
gameId: z.string().max(100).describe("The game ID"),
|
|
59
|
+
attackerId: z.string().max(100).describe("First character ID"),
|
|
60
|
+
defenderId: z.string().max(100).describe("Second character ID"),
|
|
61
|
+
attackerSkill: z.string().max(100).optional().describe("Skill for first character"),
|
|
62
|
+
defenderSkill: z.string().max(100).optional().describe("Skill for second character"),
|
|
63
|
+
attackerAttribute: z.string().max(100).optional().describe("Attribute for first character"),
|
|
64
|
+
defenderAttribute: z.string().max(100).optional().describe("Attribute for second character"),
|
|
65
|
+
},
|
|
66
|
+
annotations: ANNOTATIONS.READ_ONLY,
|
|
67
|
+
}, async (params) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = diceTools.contest(params);
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{ type: "text", text: error.message }],
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// COMBAT TOOLS
|
|
83
|
+
// ============================================================================
|
|
84
|
+
server.registerTool("start_combat", {
|
|
85
|
+
description: "Initialize a combat encounter",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
gameId: z.string().max(100).describe("The game ID"),
|
|
88
|
+
locationId: z.string().max(100).describe("Location where combat occurs"),
|
|
89
|
+
participantIds: z.array(z.string().max(100)).max(LIMITS.ARRAY_MAX).describe("Character IDs of all combatants"),
|
|
90
|
+
},
|
|
91
|
+
annotations: ANNOTATIONS.CREATE,
|
|
92
|
+
}, async (params) => {
|
|
93
|
+
const combat = combatTools.startCombat(params);
|
|
94
|
+
return {
|
|
95
|
+
content: [{ type: "text", text: JSON.stringify(combat, null, 2) }],
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
server.registerTool("get_combat", {
|
|
99
|
+
description: "Get current combat state",
|
|
100
|
+
inputSchema: {
|
|
101
|
+
combatId: z.string().max(100).describe("The combat ID"),
|
|
102
|
+
},
|
|
103
|
+
annotations: ANNOTATIONS.READ_ONLY,
|
|
104
|
+
}, async ({ combatId }) => {
|
|
105
|
+
const combat = combatTools.getCombat(combatId);
|
|
106
|
+
if (!combat) {
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: "Combat not found" }],
|
|
109
|
+
isError: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text: JSON.stringify(combat, null, 2) }],
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
server.registerTool("get_active_combat", {
|
|
117
|
+
description: "Get the active combat for a game",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
gameId: z.string().max(100).describe("The game ID"),
|
|
120
|
+
},
|
|
121
|
+
annotations: ANNOTATIONS.READ_ONLY,
|
|
122
|
+
}, async ({ gameId }) => {
|
|
123
|
+
const combat = combatTools.getActiveCombat(gameId);
|
|
124
|
+
if (!combat) {
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: "text", text: "No active combat" }],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
content: [{ type: "text", text: JSON.stringify(combat, null, 2) }],
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
server.registerTool("next_turn", {
|
|
134
|
+
description: "Advance to the next combatant's turn",
|
|
135
|
+
inputSchema: {
|
|
136
|
+
combatId: z.string().max(100).describe("The combat ID"),
|
|
137
|
+
},
|
|
138
|
+
annotations: ANNOTATIONS.UPDATE,
|
|
139
|
+
}, async ({ combatId }) => {
|
|
140
|
+
const combat = combatTools.nextTurn(combatId);
|
|
141
|
+
if (!combat) {
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: "Combat not found or ended" }],
|
|
144
|
+
isError: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
content: [{ type: "text", text: JSON.stringify(combat, null, 2) }],
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
server.registerTool("add_combat_log", {
|
|
152
|
+
description: "Add an entry to the combat log",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
combatId: z.string().max(100).describe("The combat ID"),
|
|
155
|
+
entry: z.string().max(LIMITS.DESCRIPTION_MAX).describe("Log entry text"),
|
|
156
|
+
},
|
|
157
|
+
annotations: ANNOTATIONS.UPDATE,
|
|
158
|
+
}, async ({ combatId, entry }) => {
|
|
159
|
+
const combat = combatTools.addCombatLog(combatId, entry);
|
|
160
|
+
if (!combat) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: "Combat not found" }],
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
content: [{ type: "text", text: JSON.stringify(combat, null, 2) }],
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
server.registerTool("remove_combatant", {
|
|
171
|
+
description: "Remove a character from combat (defeated, fled, etc.)",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
combatId: z.string().max(100).describe("The combat ID"),
|
|
174
|
+
characterId: z.string().max(100).describe("The character to remove"),
|
|
175
|
+
},
|
|
176
|
+
annotations: ANNOTATIONS.DESTRUCTIVE,
|
|
177
|
+
}, async ({ combatId, characterId }) => {
|
|
178
|
+
const combat = combatTools.removeParticipant(combatId, characterId);
|
|
179
|
+
if (!combat) {
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: "text", text: "Combat not found" }],
|
|
182
|
+
isError: true,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: "text", text: JSON.stringify(combat, null, 2) }],
|
|
187
|
+
};
|
|
188
|
+
});
|
|
189
|
+
server.registerTool("end_combat", {
|
|
190
|
+
description: "End a combat encounter",
|
|
191
|
+
inputSchema: {
|
|
192
|
+
combatId: z.string().max(100).describe("The combat ID"),
|
|
193
|
+
},
|
|
194
|
+
annotations: ANNOTATIONS.UPDATE,
|
|
195
|
+
}, async ({ combatId }) => {
|
|
196
|
+
const combat = combatTools.endCombat(combatId);
|
|
197
|
+
if (!combat) {
|
|
198
|
+
return {
|
|
199
|
+
content: [{ type: "text", text: "Combat not found" }],
|
|
200
|
+
isError: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: "text", text: JSON.stringify(combat, null, 2) }],
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|