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.
Files changed (135) hide show
  1. package/README.md +76 -10
  2. package/dist/bin/run-dmcp.d.ts +2 -0
  3. package/dist/bin/run-dmcp.js +55 -0
  4. package/dist/db/connection.d.ts +32 -0
  5. package/dist/db/connection.js +38 -16
  6. package/dist/db/schema.d.ts +29 -1
  7. package/dist/db/schema.js +439 -7
  8. package/dist/http/server.js +3 -3
  9. package/dist/index.d.ts +36 -2
  10. package/dist/index.js +184 -92
  11. package/dist/mcp-server.d.ts +49 -0
  12. package/dist/mcp-server.js +127 -0
  13. package/dist/reader/turnReader.d.ts +185 -0
  14. package/dist/reader/turnReader.js +288 -0
  15. package/dist/register/batch.js +5 -79
  16. package/dist/register/mcp-resources.d.ts +9 -0
  17. package/dist/register/mcp-resources.js +16 -62
  18. package/dist/register/render.d.ts +17 -0
  19. package/dist/register/render.js +50 -0
  20. package/dist/register/resolve.d.ts +14 -0
  21. package/dist/register/resolve.js +102 -0
  22. package/dist/register/resources.js +11 -4
  23. package/dist/register/timeline.d.ts +2 -0
  24. package/dist/register/timeline.js +311 -0
  25. package/dist/rpg/index.d.ts +29 -0
  26. package/dist/rpg/index.js +55 -0
  27. package/dist/rpg/register/abilities.d.ts +2 -0
  28. package/dist/rpg/register/abilities.js +165 -0
  29. package/dist/rpg/register/batch.d.ts +2 -0
  30. package/dist/rpg/register/batch.js +92 -0
  31. package/dist/rpg/register/combat.d.ts +2 -0
  32. package/dist/rpg/register/combat.js +207 -0
  33. package/dist/rpg/register/mcp-prompts.d.ts +2 -0
  34. package/dist/rpg/register/mcp-prompts.js +684 -0
  35. package/dist/rpg/register/mcp-resources.d.ts +2 -0
  36. package/dist/rpg/register/mcp-resources.js +61 -0
  37. package/dist/rpg/register/quests.d.ts +2 -0
  38. package/dist/rpg/register/quests.js +118 -0
  39. package/dist/rpg/register/status.d.ts +2 -0
  40. package/dist/rpg/register/status.js +130 -0
  41. package/dist/rpg/register/tables.d.ts +2 -0
  42. package/dist/rpg/register/tables.js +146 -0
  43. package/dist/rpg/tools/ability.d.ts +48 -0
  44. package/dist/rpg/tools/ability.js +238 -0
  45. package/dist/rpg/tools/combat.d.ts +13 -0
  46. package/dist/rpg/tools/combat.js +195 -0
  47. package/dist/rpg/tools/dice.d.ts +23 -0
  48. package/dist/rpg/tools/dice.js +111 -0
  49. package/dist/rpg/tools/quest.d.ts +34 -0
  50. package/dist/rpg/tools/quest.js +164 -0
  51. package/dist/rpg/tools/status.d.ts +36 -0
  52. package/dist/rpg/tools/status.js +218 -0
  53. package/dist/rpg/tools/tables.d.ts +33 -0
  54. package/dist/rpg/tools/tables.js +209 -0
  55. package/dist/schemas/index.d.ts +12 -12
  56. package/dist/timeline/adjudication.d.ts +150 -0
  57. package/dist/timeline/adjudication.js +174 -0
  58. package/dist/timeline/changes.d.ts +100 -0
  59. package/dist/timeline/changes.js +161 -0
  60. package/dist/timeline/checkpoint.d.ts +69 -0
  61. package/dist/timeline/checkpoint.js +131 -0
  62. package/dist/timeline/clock.d.ts +89 -0
  63. package/dist/timeline/clock.js +173 -0
  64. package/dist/timeline/constrained.d.ts +220 -0
  65. package/dist/timeline/constrained.js +671 -0
  66. package/dist/timeline/export.d.ts +171 -0
  67. package/dist/timeline/export.js +329 -0
  68. package/dist/timeline/irreversible.d.ts +85 -0
  69. package/dist/timeline/irreversible.js +108 -0
  70. package/dist/timeline/kinds.d.ts +14 -0
  71. package/dist/timeline/kinds.js +22 -0
  72. package/dist/timeline/narration.d.ts +175 -0
  73. package/dist/timeline/narration.js +259 -0
  74. package/dist/timeline/projection.d.ts +97 -0
  75. package/dist/timeline/projection.js +330 -0
  76. package/dist/timeline/provenance.d.ts +66 -0
  77. package/dist/timeline/provenance.js +45 -0
  78. package/dist/timeline/registry.d.ts +95 -0
  79. package/dist/timeline/registry.js +124 -0
  80. package/dist/timeline/render.d.ts +121 -0
  81. package/dist/timeline/render.js +187 -0
  82. package/dist/timeline/replay.d.ts +64 -0
  83. package/dist/timeline/replay.js +104 -0
  84. package/dist/timeline/resolve.d.ts +262 -0
  85. package/dist/timeline/resolve.js +226 -0
  86. package/dist/timeline/schema.d.ts +13 -0
  87. package/dist/timeline/schema.js +262 -0
  88. package/dist/timeline/t.d.ts +80 -0
  89. package/dist/timeline/t.js +37 -0
  90. package/dist/tools/constraint.d.ts +44 -80
  91. package/dist/tools/constraint.js +115 -124
  92. package/dist/tools/relationship.d.ts +83 -2
  93. package/dist/tools/relationship.js +139 -62
  94. package/dist/tools/resource.d.ts +31 -6
  95. package/dist/tools/resource.js +106 -153
  96. package/dist/types/index.d.ts +19 -1
  97. package/dist/utils/output-schemas.d.ts +593 -2
  98. package/dist/utils/output-schemas.js +3 -0
  99. package/dist/utils/webui.d.ts +32 -0
  100. package/dist/utils/webui.js +54 -1
  101. package/package.json +20 -4
  102. package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
  103. package/dist/__tests__/engineVocabulary.test.js +0 -147
  104. package/dist/db/__tests__/connection.test.d.ts +0 -1
  105. package/dist/db/__tests__/connection.test.js +0 -72
  106. package/dist/db/__tests__/testDb.d.ts +0 -33
  107. package/dist/db/__tests__/testDb.js +0 -41
  108. package/dist/test-setup.d.ts +0 -1
  109. package/dist/test-setup.js +0 -13
  110. package/dist/tools/__tests__/audio.test.d.ts +0 -1
  111. package/dist/tools/__tests__/audio.test.js +0 -59
  112. package/dist/tools/__tests__/conserved.test.d.ts +0 -1
  113. package/dist/tools/__tests__/conserved.test.js +0 -488
  114. package/dist/tools/__tests__/constraint.test.d.ts +0 -1
  115. package/dist/tools/__tests__/constraint.test.js +0 -212
  116. package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
  117. package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
  118. package/dist/tools/__tests__/images.test.d.ts +0 -1
  119. package/dist/tools/__tests__/images.test.js +0 -59
  120. package/dist/tools/__tests__/relationship.test.d.ts +0 -1
  121. package/dist/tools/__tests__/relationship.test.js +0 -132
  122. package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
  123. package/dist/tools/__tests__/resource-constraints.test.js +0 -131
  124. package/dist/tools/__tests__/resource.test.d.ts +0 -1
  125. package/dist/tools/__tests__/resource.test.js +0 -190
  126. package/dist/tools/__tests__/time.test.d.ts +0 -1
  127. package/dist/tools/__tests__/time.test.js +0 -404
  128. package/dist/tools/__tests__/timers.test.d.ts +0 -1
  129. package/dist/tools/__tests__/timers.test.js +0 -426
  130. package/dist/tools/__tests__/world.test.d.ts +0 -1
  131. package/dist/tools/__tests__/world.test.js +0 -70
  132. package/dist/utils/__tests__/json.test.d.ts +0 -1
  133. package/dist/utils/__tests__/json.test.js +0 -55
  134. package/dist/utils/__tests__/validation.test.d.ts +0 -1
  135. 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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerAbilityTools(server: McpServer): void;
@@ -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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerRpgBatchTools(server: McpServer): void;
@@ -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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerCombatTools(server: McpServer): void;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerMcpPrompts(server: McpServer): void;