run-dmcp 0.1.0 → 0.3.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 (141) hide show
  1. package/README.md +101 -11
  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 +594 -10
  8. package/dist/http/server.js +25 -4
  9. package/dist/index.d.ts +69 -2
  10. package/dist/index.js +262 -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 +13 -6
  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 +108 -0
  59. package/dist/timeline/changes.js +169 -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 +181 -0
  67. package/dist/timeline/export.js +339 -0
  68. package/dist/timeline/irreversible.d.ts +87 -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 +86 -0
  83. package/dist/timeline/replay.js +126 -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 +264 -0
  88. package/dist/timeline/t.d.ts +80 -0
  89. package/dist/timeline/t.js +37 -0
  90. package/dist/tools/audio.js +13 -9
  91. package/dist/tools/constraint.d.ts +44 -80
  92. package/dist/tools/constraint.js +115 -124
  93. package/dist/tools/game.js +33 -1
  94. package/dist/tools/images.js +17 -10
  95. package/dist/tools/relationship.d.ts +83 -2
  96. package/dist/tools/relationship.js +139 -62
  97. package/dist/tools/resource.d.ts +33 -8
  98. package/dist/tools/resource.js +106 -153
  99. package/dist/tools/time.js +18 -3
  100. package/dist/types/index.d.ts +20 -2
  101. package/dist/utils/media-path.d.ts +52 -0
  102. package/dist/utils/media-path.js +106 -0
  103. package/dist/utils/output-schemas.d.ts +594 -3
  104. package/dist/utils/output-schemas.js +4 -1
  105. package/dist/utils/webui.d.ts +32 -0
  106. package/dist/utils/webui.js +54 -1
  107. package/package.json +25 -5
  108. package/dist/__tests__/engineVocabulary.test.d.ts +0 -1
  109. package/dist/__tests__/engineVocabulary.test.js +0 -147
  110. package/dist/db/__tests__/connection.test.d.ts +0 -1
  111. package/dist/db/__tests__/connection.test.js +0 -72
  112. package/dist/db/__tests__/testDb.d.ts +0 -33
  113. package/dist/db/__tests__/testDb.js +0 -41
  114. package/dist/test-setup.d.ts +0 -1
  115. package/dist/test-setup.js +0 -13
  116. package/dist/tools/__tests__/audio.test.d.ts +0 -1
  117. package/dist/tools/__tests__/audio.test.js +0 -59
  118. package/dist/tools/__tests__/conserved.test.d.ts +0 -1
  119. package/dist/tools/__tests__/conserved.test.js +0 -488
  120. package/dist/tools/__tests__/constraint.test.d.ts +0 -1
  121. package/dist/tools/__tests__/constraint.test.js +0 -212
  122. package/dist/tools/__tests__/expiry-consequences.test.d.ts +0 -1
  123. package/dist/tools/__tests__/expiry-consequences.test.js +0 -110
  124. package/dist/tools/__tests__/images.test.d.ts +0 -1
  125. package/dist/tools/__tests__/images.test.js +0 -59
  126. package/dist/tools/__tests__/relationship.test.d.ts +0 -1
  127. package/dist/tools/__tests__/relationship.test.js +0 -132
  128. package/dist/tools/__tests__/resource-constraints.test.d.ts +0 -1
  129. package/dist/tools/__tests__/resource-constraints.test.js +0 -131
  130. package/dist/tools/__tests__/resource.test.d.ts +0 -1
  131. package/dist/tools/__tests__/resource.test.js +0 -190
  132. package/dist/tools/__tests__/time.test.d.ts +0 -1
  133. package/dist/tools/__tests__/time.test.js +0 -404
  134. package/dist/tools/__tests__/timers.test.d.ts +0 -1
  135. package/dist/tools/__tests__/timers.test.js +0 -426
  136. package/dist/tools/__tests__/world.test.d.ts +0 -1
  137. package/dist/tools/__tests__/world.test.js +0 -70
  138. package/dist/utils/__tests__/json.test.d.ts +0 -1
  139. package/dist/utils/__tests__/json.test.js +0 -55
  140. package/dist/utils/__tests__/validation.test.d.ts +0 -1
  141. package/dist/utils/__tests__/validation.test.js +0 -90
@@ -2,11 +2,15 @@ import { z } from "zod";
2
2
  import * as characterTools from "../tools/character.js";
3
3
  import * as worldTools from "../tools/world.js";
4
4
  import * as inventoryTools from "../tools/inventory.js";
5
- import * as combatTools from "../tools/combat.js";
6
5
  import * as narrativeTools from "../tools/narrative.js";
7
6
  import { LIMITS } from "../utils/validation.js";
8
7
  import { ANNOTATIONS } from "../utils/tool-annotations.js";
9
8
  import { imageGenSchema, voiceSchema } from "../schemas/index.js";
9
+ // Multi-entity workflow tools over entity/property concepts -- characters,
10
+ // locations, inventory. `setup_combat_encounter` used to live here too, but
11
+ // it is the one tool in this file that reaches into combat (design §8, issue
12
+ // #17), so it moved to `src/rpg/register/batch.ts`'s `registerRpgBatchTools`
13
+ // and this file no longer imports combat at all.
10
14
  export function registerBatchTools(server) {
11
15
  // ============================================================================
12
16
  // BATCH CREATE NPCS - Create multiple NPCs at once
@@ -96,84 +100,6 @@ export function registerBatchTools(server) {
96
100
  };
97
101
  });
98
102
  // ============================================================================
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
103
  // SCENE TRANSITION - Move characters, log event
178
104
  // ============================================================================
179
105
  server.registerTool("scene_transition", {
@@ -1,2 +1,11 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const createGameListCallback: (suffix: string, nameFormatter: (g: {
3
+ id: string;
4
+ name: string;
5
+ }) => string) => () => Promise<{
6
+ resources: {
7
+ uri: string;
8
+ name: string;
9
+ }[];
10
+ }>;
2
11
  export declare function registerMcpResources(server: McpServer): void;
@@ -2,9 +2,24 @@ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import * as gameTools from "../tools/game.js";
3
3
  import * as characterTools from "../tools/character.js";
4
4
  import * as worldTools from "../tools/world.js";
5
- import * as questTools from "../tools/quest.js";
6
5
  import * as narrativeTools from "../tools/narrative.js";
7
6
  import * as rulesTools from "../tools/rules.js";
7
+ // Helper to create list callback for game-scoped resources. Hoisted to
8
+ // module scope and exported (design §8, issue #17) so that
9
+ // src/rpg/register/mcp-resources.ts's game-quests and quest resources --
10
+ // the two RPG-shaped entries pulled out of this file -- share this one
11
+ // implementation rather than growing a second copy of it.
12
+ export const createGameListCallback = (suffix, nameFormatter) => {
13
+ return async () => {
14
+ const games = gameTools.listGames();
15
+ return {
16
+ resources: games.map((g) => ({
17
+ uri: `dmcp://game/${g.id}${suffix}`,
18
+ name: nameFormatter(g),
19
+ })),
20
+ };
21
+ };
22
+ };
8
23
  export function registerMcpResources(server) {
9
24
  // ============================================================================
10
25
  // STATIC RESOURCES
@@ -28,18 +43,6 @@ export function registerMcpResources(server) {
28
43
  // ============================================================================
29
44
  // GAME-SCOPED RESOURCES (templated)
30
45
  // ============================================================================
31
- // Helper to create list callback for game-scoped resources
32
- const createGameListCallback = (suffix, nameFormatter) => {
33
- return async () => {
34
- const games = gameTools.listGames();
35
- return {
36
- resources: games.map((g) => ({
37
- uri: `dmcp://game/${g.id}${suffix}`,
38
- name: nameFormatter(g),
39
- })),
40
- };
41
- };
42
- };
43
46
  // Game details
44
47
  server.registerResource("game", new ResourceTemplate("dmcp://game/{gameId}", {
45
48
  list: createGameListCallback("", (g) => g.name),
@@ -171,25 +174,6 @@ export function registerMcpResources(server) {
171
174
  ],
172
175
  };
173
176
  });
174
- // Game quests
175
- server.registerResource("game-quests", new ResourceTemplate("dmcp://game/{gameId}/quests", {
176
- list: createGameListCallback("/quests", (g) => `${g.name} - Quests`),
177
- }), {
178
- description: "All quests in the game",
179
- mimeType: "application/json",
180
- }, async (uri, variables) => {
181
- const gameId = variables.gameId;
182
- const quests = questTools.listQuests(gameId);
183
- return {
184
- contents: [
185
- {
186
- uri: uri.href,
187
- mimeType: "application/json",
188
- text: JSON.stringify(quests, null, 2),
189
- },
190
- ],
191
- };
192
- });
193
177
  // Game history (recent narrative events)
194
178
  server.registerResource("game-history", new ResourceTemplate("dmcp://game/{gameId}/history", {
195
179
  list: createGameListCallback("/history", (g) => `${g.name} - History`),
@@ -302,34 +286,4 @@ export function registerMcpResources(server) {
302
286
  ],
303
287
  };
304
288
  });
305
- // Quest by ID
306
- server.registerResource("quest", new ResourceTemplate("dmcp://quest/{questId}", {
307
- list: undefined, // No enumeration - access by ID only
308
- }), {
309
- description: "Quest details with objectives",
310
- mimeType: "application/json",
311
- }, async (uri, variables) => {
312
- const questId = variables.questId;
313
- const quest = questTools.getQuest(questId);
314
- if (!quest) {
315
- return {
316
- contents: [
317
- {
318
- uri: uri.href,
319
- mimeType: "application/json",
320
- text: JSON.stringify({ error: "Quest not found" }),
321
- },
322
- ],
323
- };
324
- }
325
- return {
326
- contents: [
327
- {
328
- uri: uri.href,
329
- mimeType: "application/json",
330
- text: JSON.stringify(quest, null, 2),
331
- },
332
- ],
333
- };
334
- });
335
289
  }
@@ -0,0 +1,17 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { StateRenderer } from "../timeline/render.js";
3
+ /**
4
+ * The MCP surface for the engine's state-to-text projection (design §7,
5
+ * §8, GitHub issue #16) -- one tool, `render_state_at`. Registered ONLY
6
+ * when a caller supplies a `StateRenderer` to `createCoreMcpServer`
7
+ * (src/mcp-server.ts), itself built by `createStateRenderer({ vocabulary
8
+ * })` (timeline/render.ts) over a caller-injected, caller-owned vocabulary.
9
+ * An engine with no injected vocabulary has nothing to name and registers
10
+ * no render tool at all -- the same "injection at construction" shape
11
+ * `registerResolveTools` (resolve.ts) already uses for its `Resolver`, and
12
+ * for the identical reason: there is no global renderer anywhere in this
13
+ * codebase, and this file carries no vocabulary of its own, not even an
14
+ * example (root CLAUDE.md hard rule 3, design §7's "mechanism is core, the
15
+ * vocabulary is injected").
16
+ */
17
+ export declare function registerRenderTools(server: McpServer, renderer: StateRenderer): void;
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+ import { ANNOTATIONS } from "../utils/tool-annotations.js";
3
+ import { createLogger } from "../utils/logger.js";
4
+ const log = createLogger("render");
5
+ /**
6
+ * The MCP surface for the engine's state-to-text projection (design §7,
7
+ * §8, GitHub issue #16) -- one tool, `render_state_at`. Registered ONLY
8
+ * when a caller supplies a `StateRenderer` to `createCoreMcpServer`
9
+ * (src/mcp-server.ts), itself built by `createStateRenderer({ vocabulary
10
+ * })` (timeline/render.ts) over a caller-injected, caller-owned vocabulary.
11
+ * An engine with no injected vocabulary has nothing to name and registers
12
+ * no render tool at all -- the same "injection at construction" shape
13
+ * `registerResolveTools` (resolve.ts) already uses for its `Resolver`, and
14
+ * for the identical reason: there is no global renderer anywhere in this
15
+ * codebase, and this file carries no vocabulary of its own, not even an
16
+ * example (root CLAUDE.md hard rule 3, design §7's "mechanism is core, the
17
+ * vocabulary is injected").
18
+ */
19
+ export function registerRenderTools(server, renderer) {
20
+ server.registerTool("render_state_at", {
21
+ description: "Render a game's world at t as positive concrete nouns, drawn only from the caller's own injected " +
22
+ "vocabulary -- 'the grain stores are full and the treasury coffers overflow', never 'the grain stores " +
23
+ "are no longer empty'. The renderer's sole source of state is replay(t): a fact that does not hold at " +
24
+ "t produces nothing, not a phrase about its absence. A fact that DOES hold but has no vocabulary entry " +
25
+ "is reported as an unnamed row rather than described or invented -- that is how a caller learns its " +
26
+ "vocabulary is too thin, without the engine passing judgement on it. No diff, no comparison against " +
27
+ "another t, and no transition/change form: state at one t, full stop.",
28
+ inputSchema: {
29
+ gameId: z.string().max(100).describe("The game ID"),
30
+ t: z
31
+ .number()
32
+ .finite()
33
+ .describe("An opaque ordinal on this game's declared time axis -- never a datetime, and never an index " +
34
+ "into units you might later re-cut."),
35
+ },
36
+ annotations: ANNOTATIONS.READ_ONLY,
37
+ }, async ({ gameId, t }) => {
38
+ try {
39
+ const rendered = renderer.render({ gameId, t });
40
+ return { content: [{ type: "text", text: JSON.stringify(rendered, null, 2) }] };
41
+ }
42
+ catch (error) {
43
+ log.error("render_state_at failed", { gameId, t, error: error.message });
44
+ return {
45
+ content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
46
+ isError: true,
47
+ };
48
+ }
49
+ });
50
+ }
@@ -0,0 +1,14 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type Resolver } from "../timeline/resolve.js";
3
+ /**
4
+ * The MCP surface for the resolve protocol (design §5.2a, issue #10) --
5
+ * `resolve` and `list_mechanics`. Registered ONLY when a caller supplies a
6
+ * non-empty `mechanics` array to `createCoreMcpServer` (src/mcp-server.ts): an
7
+ * engine with no mechanics has no resolve surface, and that is the correct
8
+ * shape rather than a resolve tool that could only ever answer
9
+ * "unknown-mechanic" and a list_mechanics that could only ever answer `[]`.
10
+ * This keeps injection at construction -- there is still no global resolver
11
+ * anywhere in this codebase, only the one `Resolver` instance a caller built
12
+ * and handed in, closed over by the two tool handlers below.
13
+ */
14
+ export declare function registerResolveTools(server: McpServer, resolver: Resolver): void;
@@ -0,0 +1,102 @@
1
+ import { z } from "zod";
2
+ import { ANNOTATIONS } from "../utils/tool-annotations.js";
3
+ import { createLogger } from "../utils/logger.js";
4
+ import { errors, formatErrorResponse } from "../utils/errors.js";
5
+ import { ConstraintViolationError } from "../timeline/registry.js";
6
+ import { ResolveProtocolError } from "../timeline/resolve.js";
7
+ const log = createLogger("resolve");
8
+ /**
9
+ * The MCP surface for the resolve protocol (design §5.2a, issue #10) --
10
+ * `resolve` and `list_mechanics`. Registered ONLY when a caller supplies a
11
+ * non-empty `mechanics` array to `createCoreMcpServer` (src/mcp-server.ts): an
12
+ * engine with no mechanics has no resolve surface, and that is the correct
13
+ * shape rather than a resolve tool that could only ever answer
14
+ * "unknown-mechanic" and a list_mechanics that could only ever answer `[]`.
15
+ * This keeps injection at construction -- there is still no global resolver
16
+ * anywhere in this codebase, only the one `Resolver` instance a caller built
17
+ * and handed in, closed over by the two tool handlers below.
18
+ */
19
+ export function registerResolveTools(server, resolver) {
20
+ server.registerTool("resolve", {
21
+ description: "Propose a resolution to a registered mechanic and get back its outcome. The engine enforces the " +
22
+ "PROTOCOL -- resolution happens before narration, writes go through the audited path, declared " +
23
+ "expectations are checked before dispatch -- without knowing what the mechanic itself means. " +
24
+ "Refuses (no window opens, nothing is written) for an unknown mechanic, a game with no timeline " +
25
+ "clock, or a declared expectation that does not hold at the game's current t -- the last of these " +
26
+ "carries one hop of causality per contradicted expectation, so a caller can tell whether the fact " +
27
+ "is wrong or the claim is. A constraint violated mid-adjudication (bounded, monotonic, conserved, " +
28
+ "resolve_only, irreversible) rolls back every change in the resolution and records no event.",
29
+ inputSchema: {
30
+ gameId: z.string().max(100).describe("The game ID"),
31
+ mechanic: z.string().min(1).max(200).describe("The name of a mechanic registered with this resolver"),
32
+ parameters: z
33
+ .record(z.string(), z.unknown())
34
+ .optional()
35
+ .describe("Opaque to the engine -- handed to the mechanic verbatim, never inspected here"),
36
+ expects: z
37
+ .array(z.object({
38
+ entityId: z.string().max(100).describe("The entity this expectation is about"),
39
+ key: z.string().max(200).describe("The fact key this expectation is about"),
40
+ value: z.union([z.string(), z.number()]).describe("The value this proposal declares it depends on"),
41
+ }))
42
+ .optional()
43
+ .describe("Facts this proposal declares it depends on, verified BEFORE the mechanic is dispatched. A caller's " +
44
+ "own declared precondition -- the engine only reports whether it holds, never why it should."),
45
+ },
46
+ annotations: ANNOTATIONS.UPDATE,
47
+ }, async ({ gameId, mechanic, parameters, expects }) => {
48
+ try {
49
+ const outcome = resolver.resolve({ gameId, mechanic, parameters, expects });
50
+ return { content: [{ type: "text", text: JSON.stringify(outcome, null, 2) }] };
51
+ }
52
+ catch (error) {
53
+ if (error instanceof ResolveProtocolError) {
54
+ log.error("resolve refused", { gameId, mechanic, reason: error.reason, error: error.message });
55
+ return {
56
+ content: [
57
+ {
58
+ type: "text",
59
+ text: JSON.stringify({
60
+ error: error.message,
61
+ reason: error.reason,
62
+ contradictions: error.contradictions ?? undefined,
63
+ }, null, 2),
64
+ },
65
+ ],
66
+ isError: true,
67
+ };
68
+ }
69
+ if (error instanceof ConstraintViolationError) {
70
+ log.error("resolve rolled back", {
71
+ gameId,
72
+ mechanic,
73
+ constraintKind: error.constraintKind,
74
+ error: error.message,
75
+ });
76
+ return formatErrorResponse(errors.constraintViolation(error.resourceId, error.message));
77
+ }
78
+ log.error("resolve failed", { gameId, mechanic, error: error.message });
79
+ return {
80
+ content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
81
+ isError: true,
82
+ };
83
+ }
84
+ });
85
+ server.registerTool("list_mechanics", {
86
+ description: "List every mechanic name registered with this resolver. The engine holds these names; it never " +
87
+ "reads meaning into them -- they are exactly what a caller handed to createCoreMcpServer's mechanics option.",
88
+ inputSchema: {},
89
+ annotations: ANNOTATIONS.READ_ONLY,
90
+ }, async () => {
91
+ try {
92
+ return { content: [{ type: "text", text: JSON.stringify(resolver.mechanics(), null, 2) }] };
93
+ }
94
+ catch (error) {
95
+ log.error("list_mechanics failed", { error: error.message });
96
+ return {
97
+ content: [{ type: "text", text: JSON.stringify({ error: error.message }) }],
98
+ isError: true,
99
+ };
100
+ }
101
+ });
102
+ }
@@ -9,7 +9,7 @@ export function registerResourceTools(server) {
9
9
  description: "Create a new resource (currency, reputation, counter, etc.)",
10
10
  inputSchema: {
11
11
  gameId: z.string().max(100).describe("The game ID"),
12
- ownerType: z.enum(["game", "character"]).describe("Owner type: 'game' for party/global resources, 'character' for personal resources"),
12
+ ownerType: z.enum(["game", "character", "faction", "location"]).describe("Owner type: 'game' for party/global resources, 'character' for personal resources, 'faction' or 'location' for resources owned by one of those entities"),
13
13
  ownerId: z.string().max(100).optional().describe("Character ID if ownerType is 'character' (omit for game-level resources)"),
14
14
  name: z.string().min(1).max(LIMITS.NAME_MAX).describe("Resource name (e.g., 'Gold', 'Sanity', 'Thieves Guild Reputation')"),
15
15
  description: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Resource description"),
@@ -105,7 +105,7 @@ export function registerResourceTools(server) {
105
105
  description: "List resources in a game",
106
106
  inputSchema: {
107
107
  gameId: z.string().max(100).describe("The game ID"),
108
- ownerType: z.enum(["game", "character"]).optional().describe("Filter by owner type"),
108
+ ownerType: z.enum(["game", "character", "faction", "location"]).optional().describe("Filter by owner type"),
109
109
  ownerId: z.string().max(100).optional().describe("Filter by owner ID (for character resources)"),
110
110
  category: z.string().max(100).optional().describe("Filter by category"),
111
111
  },
@@ -207,17 +207,19 @@ export function registerResourceTools(server) {
207
207
  description: "Declare a server-enforced invariant on one or more resources, so update_resource_value cannot write a value that violates it. " +
208
208
  "'bounded': the resource must already have minValue and/or maxValue set (via create_resource/update_resource) -- once declared, writes outside those bounds are REJECTED instead of the default silent clamp. " +
209
209
  "'monotonic': the resource's value may only move in one direction ('increasing' = never decreases, 'decreasing' = never increases); holding steady is always allowed. " +
210
- "'conserved': declares a set of 2+ resources that must always sum to a fixed total -- the members' current values must already sum to `total` (this does not rewrite them to match). Once declared, update_resource_value REJECTS direct writes to any member (ambiguous -- it can't know where the offsetting change comes from); use transfer_resource_value to move value between two members of the set atomically instead. A resource can belong to at most one 'conserved' set at a time.",
210
+ "'conserved': declares a set of 2+ resources that must always sum to a fixed total -- the members' current values must already sum to `total` (this does not rewrite them to match). Once declared, update_resource_value REJECTS direct writes to any member (ambiguous -- it can't know where the offsetting change comes from); use transfer_resource_value to move value between two members of the set atomically instead. A resource can belong to at most one 'conserved' set at a time. " +
211
+ "'resolve_only': every DIRECT write to the given fact key is refused -- both update_resource_value and transfer_resource_value -- so it can move only through an adjudicating call. Scoped to one fact key (factKey, default 'value'); a resource can hold a separate 'resolve_only' declaration per fact key.",
211
212
  inputSchema: {
212
213
  gameId: z.string().max(100).describe("The game ID"),
213
- kind: z.enum(["bounded", "monotonic", "conserved"]).describe("Constraint kind"),
214
- resourceId: z.string().max(100).optional().describe("Required for 'bounded' or 'monotonic': the resource to constrain"),
214
+ kind: z.enum(["bounded", "monotonic", "conserved", "resolve_only"]).describe("Constraint kind"),
215
+ resourceId: z.string().max(100).optional().describe("Required for 'bounded', 'monotonic' or 'resolve_only': the resource to constrain"),
215
216
  resourceIds: z.array(z.string().max(100)).optional().describe("Required for 'conserved': 2 or more resource IDs that must sum to `total`"),
216
217
  direction: z.enum(["increasing", "decreasing"]).optional().describe("Required for 'monotonic': the only direction the value may move"),
217
218
  total: z.number().optional().describe("Required for 'conserved': the fixed sum the resource set must maintain"),
219
+ factKey: z.string().max(100).optional().describe("For 'resolve_only' only: the fact key the constraint governs, defaulting to 'value'"),
218
220
  },
219
221
  annotations: ANNOTATIONS.CREATE,
220
- }, async ({ gameId, kind, resourceId, resourceIds, direction, total }) => {
222
+ }, async ({ gameId, kind, resourceId, resourceIds, direction, total, factKey }) => {
221
223
  try {
222
224
  let constraint;
223
225
  if (kind === "bounded") {
@@ -231,6 +233,11 @@ export function registerResourceTools(server) {
231
233
  }
232
234
  constraint = constraintTools.declareMonotonicConstraint({ gameId, resourceId, direction });
233
235
  }
236
+ else if (kind === "resolve_only") {
237
+ if (!resourceId)
238
+ throw new Error("resourceId is required for a 'resolve_only' constraint");
239
+ constraint = constraintTools.declareResolveOnlyConstraint({ gameId, resourceId, factKey });
240
+ }
234
241
  else {
235
242
  if (!resourceIds || total === undefined) {
236
243
  throw new Error("resourceIds and total are required for a 'conserved' constraint");
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerTimelineTools(server: McpServer): void;