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.
- package/README.md +101 -11
- 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 +594 -10
- package/dist/http/server.js +25 -4
- package/dist/index.d.ts +69 -2
- package/dist/index.js +262 -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 +13 -6
- 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 +108 -0
- package/dist/timeline/changes.js +169 -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 +181 -0
- package/dist/timeline/export.js +339 -0
- package/dist/timeline/irreversible.d.ts +87 -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 +86 -0
- package/dist/timeline/replay.js +126 -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 +264 -0
- package/dist/timeline/t.d.ts +80 -0
- package/dist/timeline/t.js +37 -0
- package/dist/tools/audio.js +13 -9
- package/dist/tools/constraint.d.ts +44 -80
- package/dist/tools/constraint.js +115 -124
- package/dist/tools/game.js +33 -1
- package/dist/tools/images.js +17 -10
- package/dist/tools/relationship.d.ts +83 -2
- package/dist/tools/relationship.js +139 -62
- package/dist/tools/resource.d.ts +33 -8
- package/dist/tools/resource.js +106 -153
- package/dist/tools/time.js +18 -3
- package/dist/types/index.d.ts +20 -2
- package/dist/utils/media-path.d.ts +52 -0
- package/dist/utils/media-path.js +106 -0
- package/dist/utils/output-schemas.d.ts +594 -3
- package/dist/utils/output-schemas.js +4 -1
- package/dist/utils/webui.d.ts +32 -0
- package/dist/utils/webui.js +54 -1
- package/package.json +25 -5
- 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
package/dist/register/batch.js
CHANGED
|
@@ -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 '
|
|
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");
|