run-dmcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +79 -0
  3. package/dist/__tests__/engineVocabulary.test.d.ts +1 -0
  4. package/dist/__tests__/engineVocabulary.test.js +147 -0
  5. package/dist/db/__tests__/connection.test.d.ts +1 -0
  6. package/dist/db/__tests__/connection.test.js +72 -0
  7. package/dist/db/__tests__/testDb.d.ts +33 -0
  8. package/dist/db/__tests__/testDb.js +41 -0
  9. package/dist/db/connection.d.ts +22 -0
  10. package/dist/db/connection.js +107 -0
  11. package/dist/db/schema.d.ts +1 -0
  12. package/dist/db/schema.js +725 -0
  13. package/dist/events/emitter.d.ts +22 -0
  14. package/dist/events/emitter.js +71 -0
  15. package/dist/http/server.d.ts +3 -0
  16. package/dist/http/server.js +649 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.js +92 -0
  19. package/dist/register/abilities.d.ts +2 -0
  20. package/dist/register/abilities.js +165 -0
  21. package/dist/register/audio.d.ts +2 -0
  22. package/dist/register/audio.js +326 -0
  23. package/dist/register/batch.d.ts +2 -0
  24. package/dist/register/batch.js +343 -0
  25. package/dist/register/character.d.ts +2 -0
  26. package/dist/register/character.js +324 -0
  27. package/dist/register/combat.d.ts +2 -0
  28. package/dist/register/combat.js +207 -0
  29. package/dist/register/core.d.ts +2 -0
  30. package/dist/register/core.js +1040 -0
  31. package/dist/register/display.d.ts +2 -0
  32. package/dist/register/display.js +263 -0
  33. package/dist/register/factions.d.ts +2 -0
  34. package/dist/register/factions.js +186 -0
  35. package/dist/register/images.d.ts +2 -0
  36. package/dist/register/images.js +400 -0
  37. package/dist/register/inventory.d.ts +2 -0
  38. package/dist/register/inventory.js +115 -0
  39. package/dist/register/mcp-prompts.d.ts +2 -0
  40. package/dist/register/mcp-prompts.js +684 -0
  41. package/dist/register/mcp-resources.d.ts +2 -0
  42. package/dist/register/mcp-resources.js +335 -0
  43. package/dist/register/narrative.d.ts +2 -0
  44. package/dist/register/narrative.js +242 -0
  45. package/dist/register/notes.d.ts +2 -0
  46. package/dist/register/notes.js +170 -0
  47. package/dist/register/pause.d.ts +2 -0
  48. package/dist/register/pause.js +580 -0
  49. package/dist/register/quests.d.ts +2 -0
  50. package/dist/register/quests.js +118 -0
  51. package/dist/register/relationships.d.ts +2 -0
  52. package/dist/register/relationships.js +147 -0
  53. package/dist/register/resources.d.ts +2 -0
  54. package/dist/register/resources.js +277 -0
  55. package/dist/register/secrets.d.ts +2 -0
  56. package/dist/register/secrets.js +192 -0
  57. package/dist/register/status.d.ts +2 -0
  58. package/dist/register/status.js +130 -0
  59. package/dist/register/tables.d.ts +2 -0
  60. package/dist/register/tables.js +146 -0
  61. package/dist/register/tags.d.ts +2 -0
  62. package/dist/register/tags.js +114 -0
  63. package/dist/register/time.d.ts +2 -0
  64. package/dist/register/time.js +281 -0
  65. package/dist/register/world.d.ts +2 -0
  66. package/dist/register/world.js +127 -0
  67. package/dist/schemas/index.d.ts +921 -0
  68. package/dist/schemas/index.js +121 -0
  69. package/dist/test-setup.d.ts +1 -0
  70. package/dist/test-setup.js +13 -0
  71. package/dist/tools/__tests__/audio.test.d.ts +1 -0
  72. package/dist/tools/__tests__/audio.test.js +59 -0
  73. package/dist/tools/__tests__/conserved.test.d.ts +1 -0
  74. package/dist/tools/__tests__/conserved.test.js +488 -0
  75. package/dist/tools/__tests__/constraint.test.d.ts +1 -0
  76. package/dist/tools/__tests__/constraint.test.js +212 -0
  77. package/dist/tools/__tests__/expiry-consequences.test.d.ts +1 -0
  78. package/dist/tools/__tests__/expiry-consequences.test.js +110 -0
  79. package/dist/tools/__tests__/images.test.d.ts +1 -0
  80. package/dist/tools/__tests__/images.test.js +59 -0
  81. package/dist/tools/__tests__/relationship.test.d.ts +1 -0
  82. package/dist/tools/__tests__/relationship.test.js +132 -0
  83. package/dist/tools/__tests__/resource-constraints.test.d.ts +1 -0
  84. package/dist/tools/__tests__/resource-constraints.test.js +131 -0
  85. package/dist/tools/__tests__/resource.test.d.ts +1 -0
  86. package/dist/tools/__tests__/resource.test.js +190 -0
  87. package/dist/tools/__tests__/time.test.d.ts +1 -0
  88. package/dist/tools/__tests__/time.test.js +404 -0
  89. package/dist/tools/__tests__/timers.test.d.ts +1 -0
  90. package/dist/tools/__tests__/timers.test.js +426 -0
  91. package/dist/tools/__tests__/world.test.d.ts +1 -0
  92. package/dist/tools/__tests__/world.test.js +70 -0
  93. package/dist/tools/ability.d.ts +48 -0
  94. package/dist/tools/ability.js +238 -0
  95. package/dist/tools/audio.d.ts +24 -0
  96. package/dist/tools/audio.js +365 -0
  97. package/dist/tools/character.d.ts +70 -0
  98. package/dist/tools/character.js +309 -0
  99. package/dist/tools/combat.d.ts +13 -0
  100. package/dist/tools/combat.js +195 -0
  101. package/dist/tools/constraint.d.ts +132 -0
  102. package/dist/tools/constraint.js +269 -0
  103. package/dist/tools/dice.d.ts +23 -0
  104. package/dist/tools/dice.js +111 -0
  105. package/dist/tools/display.d.ts +120 -0
  106. package/dist/tools/display.js +528 -0
  107. package/dist/tools/faction.d.ts +61 -0
  108. package/dist/tools/faction.js +269 -0
  109. package/dist/tools/game.d.ts +96 -0
  110. package/dist/tools/game.js +526 -0
  111. package/dist/tools/image-prompt.d.ts +49 -0
  112. package/dist/tools/image-prompt.js +479 -0
  113. package/dist/tools/images.d.ts +47 -0
  114. package/dist/tools/images.js +449 -0
  115. package/dist/tools/inventory.d.ts +20 -0
  116. package/dist/tools/inventory.js +145 -0
  117. package/dist/tools/narrative.d.ts +58 -0
  118. package/dist/tools/narrative.js +237 -0
  119. package/dist/tools/notes.d.ts +41 -0
  120. package/dist/tools/notes.js +220 -0
  121. package/dist/tools/pause.d.ts +110 -0
  122. package/dist/tools/pause.js +1254 -0
  123. package/dist/tools/quest.d.ts +34 -0
  124. package/dist/tools/quest.js +164 -0
  125. package/dist/tools/relationship.d.ts +74 -0
  126. package/dist/tools/relationship.js +324 -0
  127. package/dist/tools/resource.d.ts +93 -0
  128. package/dist/tools/resource.js +374 -0
  129. package/dist/tools/rules.d.ts +4 -0
  130. package/dist/tools/rules.js +30 -0
  131. package/dist/tools/secrets.d.ts +49 -0
  132. package/dist/tools/secrets.js +195 -0
  133. package/dist/tools/status.d.ts +36 -0
  134. package/dist/tools/status.js +218 -0
  135. package/dist/tools/tables.d.ts +33 -0
  136. package/dist/tools/tables.js +209 -0
  137. package/dist/tools/tags.d.ts +52 -0
  138. package/dist/tools/tags.js +176 -0
  139. package/dist/tools/time.d.ts +33 -0
  140. package/dist/tools/time.js +276 -0
  141. package/dist/tools/timers.d.ts +41 -0
  142. package/dist/tools/timers.js +215 -0
  143. package/dist/tools/world.d.ts +78 -0
  144. package/dist/tools/world.js +331 -0
  145. package/dist/types/index.d.ts +969 -0
  146. package/dist/types/index.js +1 -0
  147. package/dist/utils/__tests__/json.test.d.ts +1 -0
  148. package/dist/utils/__tests__/json.test.js +55 -0
  149. package/dist/utils/__tests__/validation.test.d.ts +1 -0
  150. package/dist/utils/__tests__/validation.test.js +90 -0
  151. package/dist/utils/errors.d.ts +44 -0
  152. package/dist/utils/errors.js +121 -0
  153. package/dist/utils/json.d.ts +9 -0
  154. package/dist/utils/json.js +23 -0
  155. package/dist/utils/logger.d.ts +7 -0
  156. package/dist/utils/logger.js +50 -0
  157. package/dist/utils/output-schemas.d.ts +594 -0
  158. package/dist/utils/output-schemas.js +331 -0
  159. package/dist/utils/tool-annotations.d.ts +147 -0
  160. package/dist/utils/tool-annotations.js +98 -0
  161. package/dist/utils/validation.d.ts +34 -0
  162. package/dist/utils/validation.js +52 -0
  163. package/dist/utils/verbosity.d.ts +57 -0
  164. package/dist/utils/verbosity.js +67 -0
  165. package/dist/utils/webui.d.ts +20 -0
  166. package/dist/utils/webui.js +35 -0
  167. package/package.json +75 -0
@@ -0,0 +1,118 @@
1
+ import { z } from "zod";
2
+ import * as questTools from "../tools/quest.js";
3
+ import { LIMITS } from "../utils/validation.js";
4
+ import { ANNOTATIONS } from "../utils/tool-annotations.js";
5
+ export function registerQuestTools(server) {
6
+ server.registerTool("create_quest", {
7
+ description: "Create a new quest. Call this whenever the player receives a mission, task, or goal - whether from an NPC, discovered through exploration, or self-initiated. Include clear objectives that can be tracked.",
8
+ inputSchema: {
9
+ gameId: z.string().max(100).describe("The game ID"),
10
+ name: z.string().min(1).max(LIMITS.NAME_MAX).describe("Quest name"),
11
+ description: z.string().max(LIMITS.DESCRIPTION_MAX).describe("Quest description"),
12
+ objectives: z.array(z.object({
13
+ description: z.string().max(LIMITS.DESCRIPTION_MAX),
14
+ completed: z.boolean().optional(),
15
+ optional: z.boolean().optional(),
16
+ })).max(LIMITS.ARRAY_MAX).describe("Quest objectives"),
17
+ rewards: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Quest rewards"),
18
+ },
19
+ annotations: ANNOTATIONS.CREATE,
20
+ }, async (params) => {
21
+ const quest = questTools.createQuest({
22
+ ...params,
23
+ objectives: params.objectives.map((obj) => ({
24
+ description: obj.description,
25
+ completed: obj.completed ?? false,
26
+ optional: obj.optional,
27
+ })),
28
+ });
29
+ return {
30
+ content: [{ type: "text", text: JSON.stringify(quest, null, 2) }],
31
+ };
32
+ });
33
+ server.registerTool("get_quest", {
34
+ description: "Get quest details",
35
+ inputSchema: {
36
+ questId: z.string().max(100).describe("The quest ID"),
37
+ },
38
+ annotations: ANNOTATIONS.READ_ONLY,
39
+ }, async ({ questId }) => {
40
+ const quest = questTools.getQuest(questId);
41
+ if (!quest) {
42
+ return {
43
+ content: [{ type: "text", text: "Quest not found" }],
44
+ isError: true,
45
+ };
46
+ }
47
+ return {
48
+ content: [{ type: "text", text: JSON.stringify(quest, null, 2) }],
49
+ };
50
+ });
51
+ server.registerTool("update_quest", {
52
+ description: "Update a quest",
53
+ inputSchema: {
54
+ questId: z.string().max(100).describe("The quest ID"),
55
+ name: z.string().max(LIMITS.NAME_MAX).optional().describe("New name"),
56
+ description: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("New description"),
57
+ status: z.enum(["active", "completed", "failed", "abandoned"]).optional().describe("New status"),
58
+ rewards: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Updated rewards"),
59
+ },
60
+ annotations: ANNOTATIONS.UPDATE,
61
+ }, async ({ questId, ...updates }) => {
62
+ const quest = questTools.updateQuest(questId, updates);
63
+ if (!quest) {
64
+ return {
65
+ content: [{ type: "text", text: "Quest not found" }],
66
+ isError: true,
67
+ };
68
+ }
69
+ return {
70
+ content: [{ type: "text", text: JSON.stringify(quest, null, 2) }],
71
+ };
72
+ });
73
+ // ============================================================================
74
+ // MODIFY OBJECTIVES - CONSOLIDATED (replaces add_objective + complete_objective)
75
+ // ============================================================================
76
+ server.registerTool("modify_objectives", {
77
+ description: "Add new objectives and/or mark existing objectives as completed in a single call. Call this immediately when: (1) player completes a quest step, (2) new sub-tasks are discovered, (3) objectives change based on player choices.",
78
+ inputSchema: {
79
+ questId: z.string().max(100).describe("The quest ID"),
80
+ add: z.array(z.object({
81
+ description: z.string().max(LIMITS.DESCRIPTION_MAX).describe("Objective description"),
82
+ optional: z.boolean().optional().describe("Is this objective optional?"),
83
+ })).max(LIMITS.ARRAY_MAX).optional().describe("New objectives to add"),
84
+ complete: z.array(z.string().max(100)).max(LIMITS.ARRAY_MAX).optional().describe("Objective IDs to mark as completed"),
85
+ },
86
+ annotations: ANNOTATIONS.UPDATE,
87
+ }, async ({ questId, add, complete }) => {
88
+ if (!add?.length && !complete?.length) {
89
+ return {
90
+ content: [{ type: "text", text: "No objectives to add or complete" }],
91
+ isError: true,
92
+ };
93
+ }
94
+ const result = questTools.modifyObjectives(questId, { add, complete });
95
+ if (!result) {
96
+ return {
97
+ content: [{ type: "text", text: "Quest not found" }],
98
+ isError: true,
99
+ };
100
+ }
101
+ return {
102
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
103
+ };
104
+ });
105
+ server.registerTool("list_quests", {
106
+ description: "List quests in a game",
107
+ inputSchema: {
108
+ gameId: z.string().max(100).describe("The game ID"),
109
+ status: z.enum(["active", "completed", "failed", "abandoned"]).optional().describe("Filter by status"),
110
+ },
111
+ annotations: ANNOTATIONS.READ_ONLY,
112
+ }, async ({ gameId, status }) => {
113
+ const quests = questTools.listQuests(gameId, status ? { status } : undefined);
114
+ return {
115
+ content: [{ type: "text", text: JSON.stringify(quests, null, 2) }],
116
+ };
117
+ });
118
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerRelationshipTools(server: McpServer): void;
@@ -0,0 +1,147 @@
1
+ import { z } from "zod";
2
+ import * as relationshipTools from "../tools/relationship.js";
3
+ import { LIMITS } from "../utils/validation.js";
4
+ import { ANNOTATIONS } from "../utils/tool-annotations.js";
5
+ export function registerRelationshipTools(server) {
6
+ server.registerTool("create_relationship", {
7
+ description: "Create a relationship between two entities (characters, factions, etc.). Call this when establishing NPC attitudes toward the player, alliances, enmities, or any significant social connection mentioned in narrative.",
8
+ inputSchema: {
9
+ gameId: z.string().max(100).describe("The game ID"),
10
+ sourceId: z.string().max(100).describe("Source entity ID"),
11
+ sourceType: z.string().max(100).describe("Source entity type (e.g., 'character', 'faction')"),
12
+ targetId: z.string().max(100).describe("Target entity ID"),
13
+ targetType: z.string().max(100).describe("Target entity type"),
14
+ relationshipType: z.string().max(100).describe("Type of relationship (e.g., 'attitude', 'bond', 'rivalry', 'loyalty')"),
15
+ value: z.number().optional().describe("Initial value (-100 to 100, default: 0)"),
16
+ label: z.string().max(LIMITS.NAME_MAX).optional().describe("Descriptive label (e.g., 'friendly', 'hostile')"),
17
+ notes: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Notes about the relationship"),
18
+ },
19
+ annotations: ANNOTATIONS.CREATE,
20
+ }, async (params) => {
21
+ const relationship = relationshipTools.createRelationship(params);
22
+ return {
23
+ content: [{ type: "text", text: JSON.stringify(relationship, null, 2) }],
24
+ };
25
+ });
26
+ server.registerTool("get_relationship", {
27
+ description: "Get a relationship by ID",
28
+ inputSchema: {
29
+ relationshipId: z.string().max(100).describe("The relationship ID"),
30
+ },
31
+ annotations: ANNOTATIONS.READ_ONLY,
32
+ }, async ({ relationshipId }) => {
33
+ const relationship = relationshipTools.getRelationship(relationshipId);
34
+ if (!relationship) {
35
+ return {
36
+ content: [{ type: "text", text: "Relationship not found" }],
37
+ isError: true,
38
+ };
39
+ }
40
+ return {
41
+ content: [{ type: "text", text: JSON.stringify(relationship, null, 2) }],
42
+ };
43
+ });
44
+ server.registerTool("get_relationship_between", {
45
+ description: "Get the relationship between two specific entities",
46
+ inputSchema: {
47
+ gameId: z.string().max(100).describe("The game ID"),
48
+ sourceId: z.string().max(100).describe("Source entity ID"),
49
+ targetId: z.string().max(100).describe("Target entity ID"),
50
+ relationshipType: z.string().max(100).optional().describe("Filter by relationship type"),
51
+ },
52
+ annotations: ANNOTATIONS.READ_ONLY,
53
+ }, async ({ gameId, sourceId, targetId, relationshipType }) => {
54
+ const relationship = relationshipTools.getRelationshipBetween(gameId, sourceId, targetId, relationshipType);
55
+ if (!relationship) {
56
+ return {
57
+ content: [{ type: "text", text: "No relationship found between these entities" }],
58
+ };
59
+ }
60
+ return {
61
+ content: [{ type: "text", text: JSON.stringify(relationship, null, 2) }],
62
+ };
63
+ });
64
+ // ============================================================================
65
+ // UPDATE RELATIONSHIP VALUE - CONSOLIDATED (replaces update_relationship + modify_relationship)
66
+ // ============================================================================
67
+ server.registerTool("update_relationship_value", {
68
+ description: "Update a relationship value and/or metadata. Use mode 'delta' to change by amount with history logging, or 'set' to set absolute value. Call this after any interaction that would affect how characters feel about each other - helpful actions, betrayals, gifts, insults, etc.",
69
+ inputSchema: {
70
+ relationshipId: z.string().max(100).describe("The relationship ID"),
71
+ mode: z.enum(["delta", "set"]).describe("'delta' to change by amount, 'set' to set absolute value"),
72
+ value: z.number().describe("Value to add (delta mode) or set (set mode)"),
73
+ reason: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Reason for the change (logged to history)"),
74
+ minValue: z.number().optional().describe("Minimum bound"),
75
+ maxValue: z.number().optional().describe("Maximum bound"),
76
+ relationshipType: z.string().max(100).optional().describe("Update relationship type"),
77
+ label: z.string().max(LIMITS.NAME_MAX).nullable().optional().describe("Update label"),
78
+ notes: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Update notes"),
79
+ },
80
+ annotations: ANNOTATIONS.UPDATE,
81
+ }, async ({ relationshipId, mode, value, reason, minValue, maxValue, relationshipType, label, notes }) => {
82
+ const result = relationshipTools.updateRelationshipValue({
83
+ relationshipId,
84
+ mode,
85
+ value,
86
+ reason,
87
+ minValue,
88
+ maxValue,
89
+ relationshipType,
90
+ label,
91
+ notes,
92
+ });
93
+ if (!result) {
94
+ return {
95
+ content: [{ type: "text", text: "Relationship not found" }],
96
+ isError: true,
97
+ };
98
+ }
99
+ const suggestedLabel = relationshipTools.getRelationshipLabel(result.relationship.value);
100
+ return {
101
+ content: [{ type: "text", text: JSON.stringify({ ...result, suggestedLabel }, null, 2) }],
102
+ };
103
+ });
104
+ server.registerTool("delete_relationship", {
105
+ description: "Delete a relationship",
106
+ inputSchema: {
107
+ relationshipId: z.string().max(100).describe("The relationship ID"),
108
+ },
109
+ annotations: ANNOTATIONS.DESTRUCTIVE,
110
+ }, async ({ relationshipId }) => {
111
+ const success = relationshipTools.deleteRelationship(relationshipId);
112
+ return {
113
+ content: [{ type: "text", text: success ? "Relationship deleted" : "Relationship not found" }],
114
+ isError: !success,
115
+ };
116
+ });
117
+ server.registerTool("list_relationships", {
118
+ description: "List relationships with optional filters",
119
+ inputSchema: {
120
+ gameId: z.string().max(100).describe("The game ID"),
121
+ entityId: z.string().max(100).optional().describe("Filter by entity (either source or target)"),
122
+ sourceId: z.string().max(100).optional().describe("Filter by source entity"),
123
+ targetId: z.string().max(100).optional().describe("Filter by target entity"),
124
+ relationshipType: z.string().max(100).optional().describe("Filter by relationship type"),
125
+ entityType: z.string().max(100).optional().describe("Filter by entity type"),
126
+ },
127
+ annotations: ANNOTATIONS.READ_ONLY,
128
+ }, async ({ gameId, ...filter }) => {
129
+ const relationships = relationshipTools.listRelationships(gameId, filter);
130
+ return {
131
+ content: [{ type: "text", text: JSON.stringify(relationships, null, 2) }],
132
+ };
133
+ });
134
+ server.registerTool("get_relationship_history", {
135
+ description: "Get change history for a relationship",
136
+ inputSchema: {
137
+ relationshipId: z.string().max(100).describe("The relationship ID"),
138
+ limit: z.number().optional().describe("Maximum entries to return"),
139
+ },
140
+ annotations: ANNOTATIONS.READ_ONLY,
141
+ }, async ({ relationshipId, limit }) => {
142
+ const history = relationshipTools.getRelationshipHistory(relationshipId, limit);
143
+ return {
144
+ content: [{ type: "text", text: JSON.stringify(history, null, 2) }],
145
+ };
146
+ });
147
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerResourceTools(server: McpServer): void;
@@ -0,0 +1,277 @@
1
+ import { z } from "zod";
2
+ import * as resourceTools from "../tools/resource.js";
3
+ import * as constraintTools from "../tools/constraint.js";
4
+ import { LIMITS } from "../utils/validation.js";
5
+ import { ANNOTATIONS } from "../utils/tool-annotations.js";
6
+ import { errors, formatErrorResponse } from "../utils/errors.js";
7
+ export function registerResourceTools(server) {
8
+ server.registerTool("create_resource", {
9
+ description: "Create a new resource (currency, reputation, counter, etc.)",
10
+ inputSchema: {
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"),
13
+ ownerId: z.string().max(100).optional().describe("Character ID if ownerType is 'character' (omit for game-level resources)"),
14
+ name: z.string().min(1).max(LIMITS.NAME_MAX).describe("Resource name (e.g., 'Gold', 'Sanity', 'Thieves Guild Reputation')"),
15
+ description: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Resource description"),
16
+ category: z.string().max(100).optional().describe("Category for grouping (e.g., 'currency', 'reputation', 'pool', 'counter')"),
17
+ value: z.number().optional().describe("Initial value (default: 0)"),
18
+ minValue: z.number().optional().describe("Minimum bound (null for unbounded)"),
19
+ maxValue: z.number().optional().describe("Maximum bound (null for unbounded)"),
20
+ },
21
+ annotations: ANNOTATIONS.CREATE,
22
+ }, async (params) => {
23
+ const resource = resourceTools.createResource(params);
24
+ return {
25
+ content: [{ type: "text", text: JSON.stringify(resource, null, 2) }],
26
+ };
27
+ });
28
+ server.registerTool("get_resource", {
29
+ description: "Get resource details",
30
+ inputSchema: {
31
+ resourceId: z.string().max(100).describe("The resource ID"),
32
+ },
33
+ annotations: ANNOTATIONS.READ_ONLY,
34
+ }, async ({ resourceId }) => {
35
+ const resource = resourceTools.getResource(resourceId);
36
+ if (!resource) {
37
+ return {
38
+ content: [{ type: "text", text: "Resource not found" }],
39
+ isError: true,
40
+ };
41
+ }
42
+ return {
43
+ content: [{ type: "text", text: JSON.stringify(resource, null, 2) }],
44
+ };
45
+ });
46
+ server.registerTool("update_resource", {
47
+ description: "Update resource metadata (name, description, category, bounds)",
48
+ inputSchema: {
49
+ resourceId: z.string().max(100).describe("The resource 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 (null to clear)"),
53
+ minValue: z.number().nullable().optional().describe("New minimum bound (null for unbounded)"),
54
+ maxValue: z.number().nullable().optional().describe("New maximum bound (null for unbounded)"),
55
+ },
56
+ annotations: ANNOTATIONS.UPDATE,
57
+ }, async ({ resourceId, ...updates }) => {
58
+ try {
59
+ const resource = resourceTools.updateResource(resourceId, updates);
60
+ if (!resource) {
61
+ return {
62
+ content: [{ type: "text", text: "Resource not found" }],
63
+ isError: true,
64
+ };
65
+ }
66
+ return {
67
+ content: [{ type: "text", text: JSON.stringify(resource, null, 2) }],
68
+ };
69
+ }
70
+ catch (error) {
71
+ if (error instanceof constraintTools.ConstraintViolationError) {
72
+ return formatErrorResponse(errors.constraintViolation(error.resourceId, error.message));
73
+ }
74
+ return {
75
+ content: [{ type: "text", text: error.message }],
76
+ isError: true,
77
+ };
78
+ }
79
+ });
80
+ server.registerTool("delete_resource", {
81
+ description: "Delete a resource and its history",
82
+ inputSchema: {
83
+ resourceId: z.string().max(100).describe("The resource ID"),
84
+ },
85
+ annotations: ANNOTATIONS.DESTRUCTIVE,
86
+ }, async ({ resourceId }) => {
87
+ try {
88
+ const success = resourceTools.deleteResource(resourceId);
89
+ return {
90
+ content: [{ type: "text", text: success ? "Resource deleted" : "Resource not found" }],
91
+ isError: !success,
92
+ };
93
+ }
94
+ catch (error) {
95
+ if (error instanceof constraintTools.ConstraintViolationError) {
96
+ return formatErrorResponse(errors.constraintViolation(error.resourceId, error.message));
97
+ }
98
+ return {
99
+ content: [{ type: "text", text: error.message }],
100
+ isError: true,
101
+ };
102
+ }
103
+ });
104
+ server.registerTool("list_resources", {
105
+ description: "List resources in a game",
106
+ inputSchema: {
107
+ gameId: z.string().max(100).describe("The game ID"),
108
+ ownerType: z.enum(["game", "character"]).optional().describe("Filter by owner type"),
109
+ ownerId: z.string().max(100).optional().describe("Filter by owner ID (for character resources)"),
110
+ category: z.string().max(100).optional().describe("Filter by category"),
111
+ },
112
+ annotations: ANNOTATIONS.READ_ONLY,
113
+ }, async ({ gameId, ownerType, ownerId, category }) => {
114
+ const resources = resourceTools.listResources(gameId, { ownerType, ownerId, category });
115
+ return {
116
+ content: [{ type: "text", text: JSON.stringify(resources, null, 2) }],
117
+ };
118
+ });
119
+ // ============================================================================
120
+ // UPDATE RESOURCE VALUE - CONSOLIDATED (replaces modify_resource + set_resource)
121
+ // ============================================================================
122
+ server.registerTool("update_resource_value", {
123
+ description: "Update a resource's value. Use mode 'delta' to add/subtract, or 'set' to set an absolute value. Changes are logged to history.",
124
+ inputSchema: {
125
+ resourceId: z.string().max(100).describe("The resource ID"),
126
+ mode: z.enum(["delta", "set"]).describe("'delta' to add/subtract, 'set' to set absolute value"),
127
+ value: z.number().describe("Amount to add/subtract (for delta) or absolute value (for set)"),
128
+ reason: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Reason for the change (logged to history)"),
129
+ },
130
+ annotations: ANNOTATIONS.UPDATE,
131
+ }, async ({ resourceId, mode, value, reason }) => {
132
+ try {
133
+ const result = resourceTools.updateResourceValue({ resourceId, mode, value, reason });
134
+ if (!result) {
135
+ return {
136
+ content: [{ type: "text", text: "Resource not found" }],
137
+ isError: true,
138
+ };
139
+ }
140
+ return {
141
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
142
+ };
143
+ }
144
+ catch (error) {
145
+ if (error instanceof constraintTools.ConstraintViolationError) {
146
+ return formatErrorResponse(errors.constraintViolation(error.resourceId, error.message));
147
+ }
148
+ return {
149
+ content: [{ type: "text", text: error.message }],
150
+ isError: true,
151
+ };
152
+ }
153
+ });
154
+ // ============================================================================
155
+ // TRANSFER RESOURCE VALUE - the only write path for a 'conserved' member
156
+ // ============================================================================
157
+ server.registerTool("transfer_resource_value", {
158
+ description: "Move an amount from one resource to another, atomically, keeping their combined total unchanged. " +
159
+ "This is the ONLY way to change the value of a resource that belongs to a declared 'conserved' constraint -- " +
160
+ "update_resource_value rejects direct writes to such resources because a single-resource write can't say " +
161
+ "where the offsetting change should come from. fromResourceId and toResourceId must both already be members " +
162
+ "of the SAME declared 'conserved' constraint (see declare_resource_constraint). amount must be >= 0; swap " +
163
+ "fromResourceId/toResourceId to reverse direction. Never clamps -- if either side would cross its own " +
164
+ "minValue/maxValue, the whole transfer is rejected rather than applying an uneven amount to each side.",
165
+ inputSchema: {
166
+ fromResourceId: z.string().max(100).describe("Resource to subtract the amount from"),
167
+ toResourceId: z.string().max(100).describe("Resource to add the amount to"),
168
+ amount: z.number().min(0).describe("Amount to move (must be >= 0)"),
169
+ reason: z.string().max(LIMITS.DESCRIPTION_MAX).optional().describe("Reason for the transfer (logged to history on both sides)"),
170
+ },
171
+ annotations: ANNOTATIONS.UPDATE,
172
+ }, async ({ fromResourceId, toResourceId, amount, reason }) => {
173
+ try {
174
+ const result = resourceTools.transferResourceValue({ fromResourceId, toResourceId, amount, reason });
175
+ return {
176
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
177
+ };
178
+ }
179
+ catch (error) {
180
+ if (error instanceof constraintTools.ConstraintViolationError) {
181
+ return formatErrorResponse(errors.constraintViolation(error.resourceId, error.message));
182
+ }
183
+ return {
184
+ content: [{ type: "text", text: error.message }],
185
+ isError: true,
186
+ };
187
+ }
188
+ });
189
+ server.registerTool("get_resource_history", {
190
+ description: "Get change history for a resource",
191
+ inputSchema: {
192
+ resourceId: z.string().max(100).describe("The resource ID"),
193
+ limit: z.number().optional().describe("Maximum number of entries to return"),
194
+ },
195
+ annotations: ANNOTATIONS.READ_ONLY,
196
+ }, async ({ resourceId, limit }) => {
197
+ const history = resourceTools.getResourceHistory(resourceId, limit);
198
+ return {
199
+ content: [{ type: "text", text: JSON.stringify(history, null, 2) }],
200
+ };
201
+ });
202
+ // ============================================================================
203
+ // RESOURCE CONSTRAINTS - optional, server-enforced invariants (opt-in; a
204
+ // resource with no declared constraint is unaffected -- see src/tools/constraint.ts)
205
+ // ============================================================================
206
+ server.registerTool("declare_resource_constraint", {
207
+ description: "Declare a server-enforced invariant on one or more resources, so update_resource_value cannot write a value that violates it. " +
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
+ "'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.",
211
+ inputSchema: {
212
+ 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"),
215
+ resourceIds: z.array(z.string().max(100)).optional().describe("Required for 'conserved': 2 or more resource IDs that must sum to `total`"),
216
+ direction: z.enum(["increasing", "decreasing"]).optional().describe("Required for 'monotonic': the only direction the value may move"),
217
+ total: z.number().optional().describe("Required for 'conserved': the fixed sum the resource set must maintain"),
218
+ },
219
+ annotations: ANNOTATIONS.CREATE,
220
+ }, async ({ gameId, kind, resourceId, resourceIds, direction, total }) => {
221
+ try {
222
+ let constraint;
223
+ if (kind === "bounded") {
224
+ if (!resourceId)
225
+ throw new Error("resourceId is required for a 'bounded' constraint");
226
+ constraint = constraintTools.declareBoundedConstraint({ gameId, resourceId });
227
+ }
228
+ else if (kind === "monotonic") {
229
+ if (!resourceId || !direction) {
230
+ throw new Error("resourceId and direction are required for a 'monotonic' constraint");
231
+ }
232
+ constraint = constraintTools.declareMonotonicConstraint({ gameId, resourceId, direction });
233
+ }
234
+ else {
235
+ if (!resourceIds || total === undefined) {
236
+ throw new Error("resourceIds and total are required for a 'conserved' constraint");
237
+ }
238
+ constraint = constraintTools.declareConservedConstraint({ gameId, resourceIds, total });
239
+ }
240
+ return {
241
+ content: [{ type: "text", text: JSON.stringify(constraint, null, 2) }],
242
+ };
243
+ }
244
+ catch (error) {
245
+ return {
246
+ content: [{ type: "text", text: error.message }],
247
+ isError: true,
248
+ };
249
+ }
250
+ });
251
+ server.registerTool("list_resource_constraints", {
252
+ description: "List declared resource constraints for a game, optionally filtered to those governing one resource.",
253
+ inputSchema: {
254
+ gameId: z.string().max(100).describe("The game ID"),
255
+ resourceId: z.string().max(100).optional().describe("Filter to constraints governing this resource"),
256
+ },
257
+ annotations: ANNOTATIONS.READ_ONLY,
258
+ }, async ({ gameId, resourceId }) => {
259
+ const constraints = constraintTools.listConstraints(gameId, resourceId);
260
+ return {
261
+ content: [{ type: "text", text: JSON.stringify(constraints, null, 2) }],
262
+ };
263
+ });
264
+ server.registerTool("remove_resource_constraint", {
265
+ description: "Remove a declared resource constraint by id. The resource(s) it governed revert to unconstrained (default clamp) behavior.",
266
+ inputSchema: {
267
+ constraintId: z.string().max(100).describe("The constraint ID"),
268
+ },
269
+ annotations: ANNOTATIONS.DESTRUCTIVE,
270
+ }, async ({ constraintId }) => {
271
+ const success = constraintTools.removeConstraint(constraintId);
272
+ return {
273
+ content: [{ type: "text", text: success ? "Constraint removed" : "Constraint not found" }],
274
+ isError: !success,
275
+ };
276
+ });
277
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerSecretTools(server: McpServer): void;