holonovel 2026.9.1 → 2026.9.2

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/dist/index.js CHANGED
@@ -53,7 +53,7 @@ state.buildFingerprint.lastSpecReview = new Date().toISOString();
53
53
  // ── Server ─────────────────────────────────────────────────────────
54
54
  const server = new McpServer({
55
55
  name: "inform-holonovel",
56
- version: "2026.09.01",
56
+ version: "2026.09.02",
57
57
  });
58
58
  // REQ-426c — MCP Apps capability negotiation: the server declares the
59
59
  // `io.modelcontextprotocol/ui` extension in its capabilities; a client that
@@ -452,7 +452,10 @@ function terseOutput(tool, args, normal, terse) {
452
452
  }
453
453
  // REQ-409 — normalize the per-call detail request: absent → summary (lean
454
454
  // default); explicit `true` → full entries; explicit `false` → summary.
455
- const detailZod = { detail: z.boolean().optional() };
455
+ // REQ-427 every advertised input parameter carries a JSON Schema description
456
+ // (verified by T509); REQ-024 — three-clause tool descriptions ("Use when" /
457
+ // "Do NOT use when").
458
+ const detailZod = { detail: z.boolean().optional().describe("When true, return full schema and description instead of a summary.") };
456
459
  function wantsDetail(detail) {
457
460
  return detail === true;
458
461
  }
@@ -1233,8 +1236,8 @@ function badgeLabel(badge) {
1233
1236
  // context; REQ-305 — observer mode: read-only spectator, AI plays both roles.
1234
1237
  server.registerTool("set_badge", {
1235
1238
  title: "Set Active Badge",
1236
- description: "Switch active badge: player, game_master, observer, or none (Editor). Always callable.",
1237
- inputSchema: { badge: z.enum(["player", "game_master", "observer", "none"]) },
1239
+ description: "Switch the active badge to player, game_master, observer, or none (Editor), gating tool access server-side for the session; always callable. Use when: entering the story, spectating, or stepping away to edit. Do NOT use when: answering a pending workflow decision — use respond.",
1240
+ inputSchema: { badge: z.enum(["player", "game_master", "observer", "none"]).describe("The badge to activate: player, game_master, observer, or none (Editor).") },
1238
1241
  }, async ({ badge }) => {
1239
1242
  const novel = state.activeNovel;
1240
1243
  if (novel) {
@@ -1258,8 +1261,8 @@ function canon(text) {
1258
1261
  }
1259
1262
  server.registerTool("respond", {
1260
1263
  title: "Respond to Workflow Decision",
1261
- description: "Respond to a pending workflow decision.",
1262
- inputSchema: { decision: z.string(), option: z.string() },
1264
+ description: "Answer a pending workflow decision, atomically draining it and persisting the outcome to the Novel. Use when: the server emitted a [NEED_INPUT] prompt and the caller must choose. Do NOT use when: no decision is pending — use set_badge or a state tool instead.",
1265
+ inputSchema: { decision: z.string().describe("The canonical decision text the workflow is waiting on."), option: z.string().describe("The chosen option, or 'cancel' to abort the workflow and restore its snapshot.") },
1263
1266
  }, async ({ decision, option }) => {
1264
1267
  requireNotObserver();
1265
1268
  const novel = requireNovel();
@@ -1493,7 +1496,7 @@ function kwMatch(canonicalDecision, keywords) {
1493
1496
  }
1494
1497
  server.registerTool("undo", {
1495
1498
  title: "Undo",
1496
- description: "Undo the most recent mutation. Restores previous snapshot.",
1499
+ description: "Undo the most recent state mutation, restoring the prior per-badge snapshot. Use when: reverting a mistaken or unwanted change. Do NOT use when: re-applying an undone change — use redo.",
1497
1500
  inputSchema: {},
1498
1501
  }, async () => {
1499
1502
  requireNotObserver();
@@ -1506,7 +1509,7 @@ server.registerTool("undo", {
1506
1509
  });
1507
1510
  server.registerTool("redo", {
1508
1511
  title: "Redo",
1509
- description: "Redo the most recently undone mutation.",
1512
+ description: "Re-apply the most recently undone mutation, restoring the per-badge snapshot that undo removed. Use when: an undo was issued by mistake and the change should be restored. Do NOT use when: reverting a new change — use undo.",
1510
1513
  inputSchema: {},
1511
1514
  }, async () => {
1512
1515
  requireNotObserver();
@@ -1519,8 +1522,8 @@ server.registerTool("redo", {
1519
1522
  });
1520
1523
  server.registerTool("help", {
1521
1524
  title: "Help and Tool Discovery",
1522
- description: "Show available commands and tools. Accepts optional query for focused search.",
1523
- inputSchema: { query: z.string().optional() },
1525
+ description: "Show the available tools grouped by category, badge-filtered for the active badge. Use when: the caller needs to discover what tools exist or find one by keyword. Do NOT use when: reading the current badge's guidance — use the badge_briefing prompt.",
1526
+ inputSchema: { query: z.string().optional().describe("Optional search term matched against tool name, description, and title.") },
1524
1527
  }, async ({ query }) => {
1525
1528
  // REQ-024 — tool documentation: tools carry a human title and descriptions
1526
1529
  // using the ruleset's own terms; full descriptions remain at resources/read.
@@ -1597,8 +1600,8 @@ server.registerTool("help", {
1597
1600
  });
1598
1601
  server.registerTool("set_help_category", {
1599
1602
  title: "Set Help Category Override",
1600
- description: "Override the builder-assigned category for a tool. Game Master only. Set category to empty string or null to restore defaults.",
1601
- inputSchema: { tool_name: z.string(), category: z.string().nullable() },
1603
+ description: "Override the builder-assigned category a tool appears under in help output, persisted to the Novel (Game Master only). Use when: reorganizing the tool catalog for a session. Do NOT use when: setting briefing section order — use set_briefing_order.",
1604
+ inputSchema: { tool_name: z.string().describe("The registered tool name to reassign."), category: z.string().nullable().describe("The new category label, or null/empty to restore the builder default.") },
1602
1605
  }, async ({ tool_name, category }) => {
1603
1606
  requireGM();
1604
1607
  const novel = requireNovel();
@@ -1688,30 +1691,10 @@ function buildCharacterStats(build, rules) {
1688
1691
  }
1689
1692
  server.registerTool("create_character", {
1690
1693
  title: "Create Character",
1691
- description: "Create a character. Quick-create: pass name, species, classes. Step-by-step: call with no params to begin a guided [NEED_INPUT] workflow.",
1694
+ description: "Create a character via quick-create (name, species, classes) or a guided [NEED_INPUT] step-by-step workflow when called without a name, persisting the entity to the Novel. Use when: introducing a new player character or NPC. Do NOT use when: modifying an existing entity — use set_personality, set_voice_examples, or set_active_entity.",
1692
1695
  // REQ-408 — parameter ceiling (8): compact entry (name + identity + source)
1693
1696
  // with mechanical and personality detail grouped into refinement objects.
1694
- inputSchema: {
1695
- name: z.string().optional(),
1696
- species: z.string().optional(),
1697
- classes: z.union([z.string(), z.array(z.object({ className: z.string(), levels: z.number().optional() }))]).optional(),
1698
- stat_method: z.string().optional(),
1699
- seed: z.string().optional(),
1700
- stage_to_roster: z.boolean().optional(),
1701
- personality: z.object({
1702
- description: z.string().optional(),
1703
- voice: z.string().optional(),
1704
- background: z.string().optional(),
1705
- goals: z.string().optional(),
1706
- }).optional(),
1707
- details: z.object({
1708
- ability_scores: z.union([z.string(), z.array(z.number())]).optional(),
1709
- skills: z.union([z.string(), z.array(z.string())]).optional(),
1710
- feats: z.union([z.string(), z.array(z.string())]).optional(),
1711
- talents: z.union([z.string(), z.array(z.string())]).optional(),
1712
- equipment: z.union([z.string(), z.array(z.string())]).optional(),
1713
- }).optional(),
1714
- },
1697
+ inputSchema: { name: z.string().optional().describe("The character's name; omit to begin the step-by-step creation workflow."), species: z.string().optional().describe("The species name; required for quick-create."), classes: z.union([z.string(), z.array(z.object({ className: z.string(), levels: z.number().optional() }))]).optional().describe("Class levels as 'Class 5 / Other 2' or an array of {className, levels}."), stat_method: z.string().optional().describe("The stat-generation method; defaults to the ruleset's first method."), seed: z.string().optional().describe("Deterministic seed for ability-score generation."), stage_to_roster: z.boolean().optional().describe("When true, also stage the entity into the persistent roster."), personality: z.object({ description: z.string().optional().describe("Narrative description."), voice: z.string().optional().describe("Voice and speech pattern."), background: z.string().optional().describe("Backstory."), goals: z.string().optional().describe("Character goals.") }).optional().describe("Grouped personality fields."), details: z.object({ ability_scores: z.union([z.string(), z.array(z.number())]).optional().describe("Ability scores as '15 14 13 12 10 8' or an array."), skills: z.union([z.string(), z.array(z.string())]).optional().describe("Trained skills."), feats: z.union([z.string(), z.array(z.string())]).optional().describe("Feats."), talents: z.union([z.string(), z.array(z.string())]).optional().describe("Talents."), equipment: z.union([z.string(), z.array(z.string())]).optional().describe("Starting equipment.") }).optional().describe("Grouped mechanical details.") },
1715
1698
  }, async ({ name, species, classes, stat_method, seed, stage_to_roster, personality: personalityObj, details, description, voice, background, goals, ability_scores, skills, feats, talents, equipment }) => {
1716
1699
  // Legacy-tolerant normalization: accept the grouped objects or their
1717
1700
  // top-level spellings interchangeably.
@@ -1819,8 +1802,8 @@ ${stage_to_roster ? `Staged to roster as ${entity.id}.` : `Character '${name}' c
1819
1802
  });
1820
1803
  server.registerTool("stage_character", {
1821
1804
  title: "Stage Character to Roster",
1822
- description: "Stage an existing novel entity into the persistent roster for later import.",
1823
- inputSchema: { entity_id: z.string().optional() },
1805
+ description: "Stage an existing Novel entity into the persistent roster, so it survives the Novel and can be imported elsewhere. Use when: persisting a character for reuse in another Novel. Do NOT use when: importing a roster character into the Novel — use import_character.",
1806
+ inputSchema: { entity_id: z.string().optional().describe("The entity to stage; defaults to the active entity.") },
1824
1807
  }, async ({ entity_id }) => {
1825
1808
  requireNotObserver();
1826
1809
  const novel = requireNovel();
@@ -1833,8 +1816,8 @@ server.registerTool("stage_character", {
1833
1816
  });
1834
1817
  server.registerTool("import_character", {
1835
1818
  title: "Import Character",
1836
- description: "Import a roster character into the active novel.",
1837
- inputSchema: { roster_id: z.string() },
1819
+ description: "Import a roster character into the active Novel, copying name, personality, voice examples, and inventory. Use when: bringing a staged character into play. Do NOT use when: persisting a Novel entity to the roster — use stage_character.",
1820
+ inputSchema: { roster_id: z.string().describe("The roster identifier to import.") },
1838
1821
  }, async ({ roster_id }) => {
1839
1822
  requireNotObserver();
1840
1823
  const novel = requireNovel();
@@ -1847,16 +1830,13 @@ server.registerTool("import_character", {
1847
1830
  });
1848
1831
  server.registerTool("character_sheet", {
1849
1832
  title: "Character Sheet",
1850
- description: "Render a character sheet for an entity. Formats: markdown (default), json, html, ascii.",
1833
+ description: "Render a character sheet for an entity or NPC in markdown (default), json, html, or ascii. Use when: the caller needs the full mechanical or profile view of a character. Do NOT use when: creating or editing a character — use create_character or set_personality.",
1851
1834
  // REQ-120 — NPC rendering via the same sheet mechanism; REQ-124 — NPC damage
1852
1835
  // resolution targets NPCs by identifier; REQ-129 — property group cardinality.
1853
1836
  // REQ-425a/b — the `format` selector is drawn from the output format catalog
1854
1837
  // (STATBLOCK_FORMATS) and validated at call time; REQ-426b — the result
1855
1838
  // carries `ui://` linkage metadata when the Apps extension is negotiated.
1856
- inputSchema: {
1857
- entity_id: z.string().optional(),
1858
- format: z.string().optional(),
1859
- },
1839
+ inputSchema: { entity_id: z.string().optional().describe("The entity or NPC to render; defaults to the active entity."), format: z.string().optional().describe("Output format: markdown (default), json, html, or ascii.") },
1860
1840
  }, async ({ entity_id, format }) => {
1861
1841
  const entity = resolveEntityOrNpc(entity_id);
1862
1842
  if (!entity)
@@ -1881,8 +1861,8 @@ server.registerTool("character_sheet", {
1881
1861
  });
1882
1862
  server.registerTool("set_active_entity", {
1883
1863
  title: "Set Active Entity",
1884
- description: "Set the currently active entity.",
1885
- inputSchema: { entity_id: z.string(), pov: z.enum(["character", "omniscient"]).optional() },
1864
+ description: "Set the currently active entity and, optionally, the point-of-view mode (character or omniscient), persisting the choice to the Novel. Use when: switching which character the Player inhabits. Do NOT use when: changing the acting badge — use set_badge.",
1865
+ inputSchema: { entity_id: z.string().describe("The entity to make active."), pov: z.enum(["character", "omniscient"]).optional().describe("Point-of-view mode for the active entity.") },
1886
1866
  }, async ({ entity_id, pov }) => {
1887
1867
  requireNotObserver();
1888
1868
  const novel = requireNovel();
@@ -1896,19 +1876,13 @@ server.registerTool("set_active_entity", {
1896
1876
  });
1897
1877
  server.registerTool("set_personality", {
1898
1878
  title: "Set Entity or NPC Personality",
1899
- description: "Set narrative personality fields for an entity or NPC.",
1879
+ description: "Set narrative personality fields for an entity or NPC, persisting the most recent write across all read surfaces. Use when: defining how a character speaks, thinks, or what they want. Do NOT use when: setting dialogue examples — use set_voice_examples.",
1900
1880
  // REQ-127 — ruleset-native personality mapping (set_personality description
1901
1881
  // references ruleset-native construct names when a ruleset defines them);
1902
1882
  // REQ-165 — entity ownership gating (Player for own entities, GM for all);
1903
1883
  // REQ-166 — personality briefing rendering (fields surfaced in badge_briefing
1904
1884
  // alongside stats); REQ-122 — NPC narrative fields (NPC identifiers accepted).
1905
- inputSchema: {
1906
- entity_id: z.string(),
1907
- description: z.string().optional(),
1908
- voice: z.string().optional(),
1909
- background: z.string().optional(),
1910
- goals: z.string().optional(),
1911
- },
1885
+ inputSchema: { entity_id: z.string().describe("The entity or NPC to update."), description: z.string().optional().describe("Narrative description."), voice: z.string().optional().describe("Voice and speech pattern."), background: z.string().optional().describe("Backstory."), goals: z.string().optional().describe("Character goals.") },
1912
1886
  }, async ({ entity_id, description, voice, background, goals }) => {
1913
1887
  requireNotObserver();
1914
1888
  const novel = requireNovel();
@@ -1938,13 +1912,10 @@ server.registerTool("set_personality", {
1938
1912
  });
1939
1913
  server.registerTool("set_voice_examples", {
1940
1914
  title: "Set Voice Examples",
1941
- description: "Set voice and dialogue examples for an entity or NPC.",
1915
+ description: "Set voice and dialogue examples for an entity or NPC, rendered ahead of trait descriptions to show rather than tell. Use when: giving the narrator concrete lines to imitate. Do NOT use when: setting abstract personality traits — use set_personality.",
1942
1916
  // REQ-126 — voice examples render ahead of trait descriptions in prompts
1943
1917
  // (show-don't-tell); REQ-077f — the primary dialogue-consistency mechanism.
1944
- inputSchema: {
1945
- entity_id: z.string(),
1946
- examples: z.array(z.object({ context: z.string(), dialogue: z.string(), tag: z.string().optional() })),
1947
- },
1918
+ inputSchema: { entity_id: z.string().describe("The entity or NPC to update."), examples: z.array(z.object({ context: z.string().describe("When the line was spoken."), dialogue: z.string().describe("The example line."), tag: z.string().optional().describe("Optional tag.") })).describe("Voice and dialogue examples.") },
1948
1919
  }, async ({ entity_id, examples }) => {
1949
1920
  requireNotObserver();
1950
1921
  const novel = requireNovel();
@@ -1959,11 +1930,8 @@ server.registerTool("set_voice_examples", {
1959
1930
  // REQ-069 — player feedback signal to the GM.
1960
1931
  server.registerTool("player_signal", {
1961
1932
  title: "Player Signal",
1962
- description: "Send a narrative signal from the player to the GM.",
1963
- inputSchema: {
1964
- signal: z.enum(["pace", "difficulty", "tone", "focus", "boundary", "voice_feedback"]),
1965
- value: z.string(),
1966
- },
1933
+ description: "Send a narrative feedback signal from the Player to the GM (Player badge only), recording it to the Novel and correcting the entity's voice examples when the signal is voice_feedback. Use when: the player wants to steer pacing, difficulty, tone, focus, or set a boundary. Do NOT use when: making a game action — use command or a mechanical tool.",
1934
+ inputSchema: { signal: z.enum(["pace", "difficulty", "tone", "focus", "boundary", "voice_feedback"]).describe("The feedback category."), value: z.string().describe("The feedback text.") },
1967
1935
  }, async ({ signal, value }) => {
1968
1936
  requirePlayer();
1969
1937
  const novel = requireNovel();
@@ -1992,13 +1960,8 @@ server.registerTool("player_signal", {
1992
1960
  // ── Autonomy (REQ-306) ────────────────────────────────────────────
1993
1961
  server.registerTool("set_autonomy", {
1994
1962
  title: "Adjustable Autonomy",
1995
- description: "Set the AI autonomy sliders for the active Novel. level: full/mechanical_prompt/manual; confirmation: auto/confirm/prompt; safety: safe/moderate/hardcore; creativity: predictable/standard/chaotic. Game Master only.",
1996
- inputSchema: {
1997
- level: z.enum(["full", "mechanical_prompt", "manual"]).optional(),
1998
- confirmation: z.enum(["auto", "confirm", "prompt"]).optional(),
1999
- safety: z.enum(["safe", "moderate", "hardcore"]).optional(),
2000
- creativity: z.enum(["predictable", "standard", "chaotic"]).optional(),
2001
- },
1963
+ description: "Set the AI autonomy sliders for the active Novel (level, confirmation, safety, creativity), persisting them and escalating safety raises through a confirmation workflow (Game Master only). Use when: tuning how much the AI auto-plays. Do NOT use when: switching the acting badge — use set_badge.",
1964
+ inputSchema: { level: z.enum(["full", "mechanical_prompt", "manual"]).optional().describe("How much the AI auto-plays: full, mechanical_prompt, or manual."), confirmation: z.enum(["auto", "confirm", "prompt"]).optional().describe("When the AI asks before acting: auto, confirm, or prompt."), safety: z.enum(["safe", "moderate", "hardcore"]).optional().describe("Safety tier: safe, moderate, or hardcore."), creativity: z.enum(["predictable", "standard", "chaotic"]).optional().describe("Creativity: predictable, standard, or chaotic.") },
2002
1965
  }, async ({ level, confirmation, safety, creativity }) => {
2003
1966
  requireGM();
2004
1967
  const novel = requireNovel();
@@ -2127,8 +2090,8 @@ function recordExplorationKnowledge(novel, entity, type, name) {
2127
2090
  }
2128
2091
  server.registerTool("command", {
2129
2092
  title: "Parser Command",
2130
- description: "Execute a natural-language parser command against the world model. Use for navigation (go, n/s/e/w), inspection (look, examine), object interaction (take, drop, open, close), inventory, and wait.",
2131
- inputSchema: { command: z.string() },
2093
+ description: "Execute a natural-language parser command against the world model, mutating it for navigation, inspection, object interaction, inventory, and wait. Use when: a player or narrator takes a physical action in the world. Do NOT use when: checking the outcome of an action without mutating state — use resolve_intent.",
2094
+ inputSchema: { command: z.string().describe("The natural-language command, e.g. 'go north', 'look', 'take torch', 'open door'.") },
2132
2095
  }, async ({ command }) => {
2133
2096
  const novel = requireNovel();
2134
2097
  // REQ-197 — description mode commands are always recognized verbs.
@@ -2293,11 +2256,8 @@ function findMatchingThing(name, world, roomName) {
2293
2256
  // --- World-Model CRUD (GM-only) ---
2294
2257
  server.registerTool("create_room", {
2295
2258
  title: "Create Room",
2296
- description: "Create a new room in the world model. Game Master only.",
2297
- inputSchema: {
2298
- name: z.string(),
2299
- description: z.string().optional(),
2300
- },
2259
+ description: "Create a new room in the world model, persisting it to the Novel save file (Game Master only). Use when: the GM needs to add a location to the world model. Do NOT use when: adding a thing or an exit — use create_thing or create_exit.",
2260
+ inputSchema: { name: z.string().describe("The room name."), description: z.string().optional().describe("Optional room description.") },
2301
2261
  }, async ({ name, description }) => {
2302
2262
  requireGM();
2303
2263
  const novel = requireNovel();
@@ -2319,8 +2279,8 @@ server.registerTool("create_room", {
2319
2279
  });
2320
2280
  server.registerTool("remove_room", {
2321
2281
  title: "Remove Room",
2322
- description: "Remove a room and its contained things and exits. Game Master only.",
2323
- inputSchema: { name: z.string() },
2282
+ description: "Remove a room along with its contained things and exits, persisting the removal (Game Master only). Use when: deleting a location that is no longer needed. Do NOT use when: removing a single thing or exit — use remove_thing or remove_exit.",
2283
+ inputSchema: { name: z.string().describe("The room to remove.") },
2324
2284
  }, async ({ name }) => {
2325
2285
  requireGM();
2326
2286
  const novel = requireNovel();
@@ -2348,29 +2308,8 @@ server.registerTool("remove_room", {
2348
2308
  });
2349
2309
  server.registerTool("create_thing", {
2350
2310
  title: "Create Thing",
2351
- description: "Create a new thing in the world model. Game Master only.",
2352
- inputSchema: {
2353
- name: z.string(),
2354
- kind: z.string().optional(),
2355
- description: z.string().optional(),
2356
- location: z.string().optional(),
2357
- location_type: z.enum(["room", "container", "supporter"]).optional(),
2358
- fixed: z.boolean().optional(),
2359
- openable: z.boolean().optional(),
2360
- lockable: z.boolean().optional(),
2361
- locked: z.boolean().optional(),
2362
- lit: z.boolean().optional(),
2363
- switched_on: z.boolean().optional(),
2364
- switchable: z.boolean().optional(),
2365
- transparent: z.boolean().optional(),
2366
- readable: z.boolean().optional(),
2367
- read_text: z.string().optional(),
2368
- wearable: z.boolean().optional(),
2369
- edible: z.boolean().optional(),
2370
- drinkable: z.boolean().optional(),
2371
- enterable: z.boolean().optional(),
2372
- climbable: z.boolean().optional(),
2373
- },
2311
+ description: "Create a new thing in the world model with an optional kind, container, and properties, persisting it (Game Master only). Use when: the GM needs to place an object, door, or device. Do NOT use when: adding a room — use create_room.",
2312
+ inputSchema: { name: z.string().describe("The thing name."), kind: z.string().optional().describe("Optional kind from the hierarchy (thing, container, supporter, door, device, vehicle, person, backdrop, region)."), description: z.string().optional().describe("Optional description."), location: z.string().optional().describe("Optional containing room or thing name."), location_type: z.enum(["room", "container", "supporter"]).optional().describe("Where the thing is placed: room, container, or supporter."), fixed: z.boolean().optional().describe("When true the thing cannot be taken."), openable: z.boolean().optional().describe("When true the thing can be opened."), lockable: z.boolean().optional().describe("When true the thing can be locked."), locked: z.boolean().optional().describe("When true the thing starts locked."), lit: z.boolean().optional().describe("When true the thing is lit."), switched_on: z.boolean().optional().describe("When true the thing is switched on."), switchable: z.boolean().optional().describe("When true the thing can be switched."), transparent: z.boolean().optional().describe("When true the thing is transparent."), readable: z.boolean().optional().describe("When true the thing can be read."), read_text: z.string().optional().describe("Text revealed when the thing is read."), wearable: z.boolean().optional().describe("When true the thing can be worn."), edible: z.boolean().optional().describe("When true the thing can be eaten."), drinkable: z.boolean().optional().describe("When true the thing can be drunk."), enterable: z.boolean().optional().describe("When true the thing can be entered."), climbable: z.boolean().optional().describe("When true the thing can be climbed.") },
2374
2313
  }, async ({ name, kind, description, location, location_type, fixed, openable, lockable, locked, lit, switched_on, switchable, transparent, readable, read_text, wearable, edible, drinkable, enterable, climbable }) => {
2375
2314
  requireGM();
2376
2315
  const novel = requireNovel();
@@ -2424,8 +2363,8 @@ server.registerTool("create_thing", {
2424
2363
  });
2425
2364
  server.registerTool("remove_thing", {
2426
2365
  title: "Remove Thing",
2427
- description: "Remove a thing from the world model. Game Master only.",
2428
- inputSchema: { name: z.string() },
2366
+ description: "Remove a thing from the world model, persisting the removal (Game Master only). Use when: deleting an object that no longer exists in the fiction. Do NOT use when: removing a room — use remove_room.",
2367
+ inputSchema: { name: z.string().describe("The thing to remove.") },
2429
2368
  }, async ({ name }) => {
2430
2369
  requireGM();
2431
2370
  const novel = requireNovel();
@@ -2440,12 +2379,8 @@ server.registerTool("remove_thing", {
2440
2379
  });
2441
2380
  server.registerTool("create_exit", {
2442
2381
  title: "Create Exit",
2443
- description: "Create a directional exit between two rooms. Reverse exit created implicitly. Game Master only.",
2444
- inputSchema: {
2445
- direction: z.string(),
2446
- room_a: z.string(),
2447
- room_b: z.string(),
2448
- },
2382
+ description: "Create a directional exit between two rooms, creating the reverse exit implicitly and persisting both (Game Master only). Use when: connecting two locations. Do NOT use when: removing a connection — use remove_exit.",
2383
+ inputSchema: { direction: z.string().describe("The direction from room_a (n/s/e/w/up/down/out or a named direction)."), room_a: z.string().describe("The source room."), room_b: z.string().describe("The destination room.") },
2449
2384
  }, async ({ direction, room_a, room_b }) => {
2450
2385
  requireGM();
2451
2386
  const novel = requireNovel();
@@ -2467,11 +2402,8 @@ server.registerTool("create_exit", {
2467
2402
  });
2468
2403
  server.registerTool("remove_exit", {
2469
2404
  title: "Remove Exit",
2470
- description: "Remove a directional exit from a room. Game Master only.",
2471
- inputSchema: {
2472
- direction: z.string(),
2473
- room: z.string(),
2474
- },
2405
+ description: "Remove a directional exit from a room, persisting the removal (Game Master only). Use when: severing a connection between locations. Do NOT use when: creating a connection — use create_exit.",
2406
+ inputSchema: { direction: z.string().describe("The direction of the exit to remove."), room: z.string().describe("The room the exit belongs to.") },
2475
2407
  }, async ({ direction, room: roomName }) => {
2476
2408
  requireGM();
2477
2409
  const novel = requireNovel();
@@ -2491,8 +2423,8 @@ server.registerTool("remove_exit", {
2491
2423
  });
2492
2424
  server.registerTool("convert_source", {
2493
2425
  title: "Convert Source",
2494
- description: "Parse hybrid world-model assertions and populate the Novel's world model. Game Master only. Only on an empty world model.",
2495
- inputSchema: { source: z.string() },
2426
+ description: "Parse hybrid world-model assertions and populate the Novel's world model (Game Master only, empty world model only). Use when: importing a large room/thing map from prose or structured text. Do NOT use when: adding a single room or thing — use create_room or create_thing.",
2427
+ inputSchema: { source: z.string().describe("Hybrid world-model assertions in prose or structured text.") },
2496
2428
  }, async ({ source }) => {
2497
2429
  requireGM();
2498
2430
  const novel = requireNovel();
@@ -2608,7 +2540,7 @@ function composeRoomContext(room, novel, world) {
2608
2540
  server.registerTool("resolve_intent", {
2609
2541
  title: "Resolve Intent",
2610
2542
  description: "Resolve a natural-language spatial intent against the world model without mutating state. Use when: a player or the AI narrator needs to determine the outcome of a movement or inspection against the world model. Do NOT use when: you are the Game Master inspecting the model directly — use the parser command tool for that.",
2611
- inputSchema: { intent: z.string() },
2543
+ inputSchema: { intent: z.string().describe("The natural-language spatial intent to resolve (e.g. 'go north', 'open the door').") },
2612
2544
  }, async ({ intent }) => {
2613
2545
  const badge = getBadge();
2614
2546
  if (badge === "player") {
@@ -2622,12 +2554,8 @@ server.registerTool("resolve_intent", {
2622
2554
  // --- Combat (GM, auto-advance in ruleset-free mode) ---
2623
2555
  server.registerTool("init_combat", {
2624
2556
  title: "Initiate Combat",
2625
- description: "Start a combat encounter. Game Master only. In ruleset-free mode, all participants auto-advance.",
2626
- inputSchema: {
2627
- participants: z.array(z.string()),
2628
- dangers: z.array(z.object({ name: z.string(), ac: z.number().optional(), hp: z.number().optional(), initiative_bonus: z.number().optional() })).optional(),
2629
- seed: z.string().optional(),
2630
- },
2557
+ description: "Start a combat encounter with participants and optional dangers, persisting the combat state (Game Master only). Use when: initiating a fight. Do NOT use when: progressing an active fight — use advance_combat.",
2558
+ inputSchema: { participants: z.array(z.string()).describe("Entity identifiers participating in combat."), dangers: z.array(z.object({ name: z.string(), ac: z.number().optional(), hp: z.number().optional(), initiative_bonus: z.number().optional() })).optional().describe("Optional non-entity combatants with armor class, hit points, and initiative bonus."), seed: z.string().optional().describe("Optional deterministic seed for initiative and rolls.") },
2631
2559
  }, async ({ participants, dangers, seed }) => {
2632
2560
  requireGM();
2633
2561
  const novel = requireNovel();
@@ -2650,7 +2578,7 @@ server.registerTool("init_combat", {
2650
2578
  });
2651
2579
  server.registerTool("advance_combat", {
2652
2580
  title: "Advance Combat",
2653
- description: "Advance to the next turn in combat. Game Master only.",
2581
+ description: "Advance combat to the next turn, applying round effects and persisting the change (Game Master only). Use when: moving the active fight forward one turn. Do NOT use when: starting or ending combat — use init_combat or end_combat.",
2654
2582
  inputSchema: {},
2655
2583
  }, async () => {
2656
2584
  requireGM();
@@ -2670,8 +2598,8 @@ server.registerTool("advance_combat", {
2670
2598
  });
2671
2599
  server.registerTool("end_combat", {
2672
2600
  title: "End Combat",
2673
- description: "End the active combat encounter. Game Master only.",
2674
- inputSchema: { outcome: z.string().optional() },
2601
+ description: "End the active combat encounter, clearing combat state and persisting it (Game Master only). Use when: the fight is resolved. Do NOT use when: advancing the fight — use advance_combat.",
2602
+ inputSchema: { outcome: z.string().optional().describe("Optional text describing how the combat ended.") },
2675
2603
  }, async ({ outcome }) => {
2676
2604
  requireGM();
2677
2605
  const novel = requireNovel();
@@ -2683,8 +2611,8 @@ server.registerTool("end_combat", {
2683
2611
  });
2684
2612
  server.registerTool("add_combat_participant", {
2685
2613
  title: "Add Combat Participant",
2686
- description: "Add a participant to active combat. Game Master only.",
2687
- inputSchema: { participant_id: z.string() },
2614
+ description: "Add a participant to the active combat, persisting the roster change (Game Master only). Use when: a new combatant joins mid-fight. Do NOT use when: removing a combatant — use remove_combat_participant.",
2615
+ inputSchema: { participant_id: z.string().describe("The entity identifier to add to combat.") },
2688
2616
  }, async ({ participant_id }) => {
2689
2617
  requireGM();
2690
2618
  const novel = requireNovel();
@@ -2695,8 +2623,8 @@ server.registerTool("add_combat_participant", {
2695
2623
  });
2696
2624
  server.registerTool("remove_combat_participant", {
2697
2625
  title: "Remove Combat Participant",
2698
- description: "Remove a participant from active combat. Game Master only.",
2699
- inputSchema: { participant_id: z.string() },
2626
+ description: "Remove a participant from the active combat, persisting the roster change (Game Master only). Use when: a combatant flees or is removed. Do NOT use when: adding a combatant — use add_combat_participant.",
2627
+ inputSchema: { participant_id: z.string().describe("The participant to remove from combat.") },
2700
2628
  }, async ({ participant_id }) => {
2701
2629
  requireGM();
2702
2630
  const novel = requireNovel();
@@ -2846,28 +2774,28 @@ function advanceSceneTransitionCountdowns(novel) {
2846
2774
  }
2847
2775
  server.registerTool("set_scene_state", {
2848
2776
  title: "Set Scene State",
2849
- description: "Set the scene description and location. Game Master only.",
2777
+ description: "Set the scene description, location, time of day, atmosphere, and scene type, persisting them (Game Master only). Use when: the fiction moves to a new scene. Do NOT use when: setting the narrative directive — use set_narrative_directive.",
2850
2778
  inputSchema: {
2851
- description: z.string(),
2852
- location: z.string().optional(),
2853
- time_of_day: z.string().optional(),
2854
- atmosphere: z.string().optional(),
2779
+ description: z.string().describe("The scene description."),
2780
+ location: z.string().optional().describe("Optional scene location."),
2781
+ time_of_day: z.string().optional().describe("Optional time of day."),
2782
+ atmosphere: z.string().optional().describe("Optional atmosphere."),
2855
2783
  // REQ-087 — scene type tagging: scene_type accepts a single tag or an array
2856
2784
  // from the canonical catalog (social/exploration/neutral; combat is a
2857
2785
  // resolution mode, added automatically on init_combat).
2858
- scene_type: z.union([z.enum(["combat", "social", "exploration", "neutral"]), z.array(z.enum(["combat", "social", "exploration", "neutral"]))]).optional(),
2859
- beat: z.enum(BEAT_VALUES).optional(),
2860
- skip_transition_hook: z.boolean().optional(),
2786
+ scene_type: z.union([z.enum(["combat", "social", "exploration", "neutral"]), z.array(z.enum(["combat", "social", "exploration", "neutral"]))]).optional().describe("A scene-type tag or array of tags from combat/social/exploration/neutral."),
2787
+ beat: z.enum(BEAT_VALUES).optional().describe("Optional story-beat tag."),
2788
+ skip_transition_hook: z.boolean().optional().describe("When true, skip the scene-transition hook."),
2861
2789
  // REQ-250 — adventure scene waypoint: heading anchor from the adventure
2862
2790
  // structural index; empty/null clears; unknown anchors → [NOT_FOUND].
2863
- adventure_scene: z.string().nullable().optional(),
2791
+ adventure_scene: z.string().nullable().optional().describe("Optional adventure-scene waypoint anchor; empty or null clears it."),
2864
2792
  // REQ-252 — narrative fast-forward: skip intervening time with a bridging
2865
2793
  // summary, countdown adjustments, and NPC changes.
2866
2794
  fast_forward: z.object({
2867
- interval: z.string(),
2868
- changes: z.array(z.object({ npc_id: z.string(), location: z.string().optional(), disposition: z.string().optional(), condition: z.string().optional() })).optional(),
2869
- skip_countdowns: z.boolean().optional(),
2870
- }).optional(),
2795
+ interval: z.string().describe("The time interval to skip."),
2796
+ changes: z.array(z.object({ npc_id: z.string().describe("The NPC to change."), location: z.string().optional().describe("New location."), disposition: z.string().optional().describe("New disposition."), condition: z.string().optional().describe("New condition.") })).optional().describe("Optional NPC changes during the skip."),
2797
+ skip_countdowns: z.boolean().optional().describe("When true, do not advance countdowns during the skip."),
2798
+ }).optional().describe("Optional fast-forward: skip intervening time with a bridging summary, countdown adjustments, and NPC changes."),
2871
2799
  },
2872
2800
  }, async ({ description, location, time_of_day, atmosphere, scene_type, beat, skip_transition_hook, adventure_scene, fast_forward }) => {
2873
2801
  requireGM();
@@ -2998,8 +2926,8 @@ server.registerTool("set_scene_state", {
2998
2926
  });
2999
2927
  server.registerTool("set_narrative_directive", {
3000
2928
  title: "Set Narrative Directive",
3001
- description: "Set overarching narrative directive for the current scene. Game Master only.",
3002
- inputSchema: { directive: z.string() },
2929
+ description: "Set the overarching narrative directive for the current scene, persisting it (Game Master only). Use when: steering tone or scene focus. Do NOT use when: setting scene location or description — use set_scene_state.",
2930
+ inputSchema: { directive: z.string().describe("The overarching narrative directive for the current scene.") },
3003
2931
  }, async ({ directive }) => {
3004
2932
  requireGM();
3005
2933
  const novel = requireNovel();
@@ -3011,15 +2939,15 @@ server.registerTool("set_narrative_directive", {
3011
2939
  // --- NPCs (GM) ---
3012
2940
  server.registerTool("create_npc", {
3013
2941
  title: "Create NPC",
3014
- description: "Create a named NPC with optional description and narrative fields. Game Master only.",
2942
+ description: "Create a named NPC with optional description, disposition, location, and goals, persisting it to the Novel (Game Master only). Use when: introducing a non-player character. Do NOT use when: editing an NPC — use update_npc.",
3015
2943
  inputSchema: {
3016
- name: z.string(),
3017
- description: z.string().optional(),
3018
- disposition: z.string().optional(),
3019
- location: z.string().optional(),
3020
- goals: z.string().optional(),
2944
+ name: z.string().describe("The NPC name."),
2945
+ description: z.string().optional().describe("Optional description."),
2946
+ disposition: z.string().optional().describe("Optional disposition."),
2947
+ location: z.string().optional().describe("Optional location."),
2948
+ goals: z.string().optional().describe("Optional goals."),
3021
2949
  // REQ-119 — optional ruleset stat-block reference.
3022
- ruleset_reference: z.string().optional(),
2950
+ ruleset_reference: z.string().optional().describe("Optional ruleset stat-block reference."),
3023
2951
  },
3024
2952
  }, async ({ name, description, disposition, location, goals, ruleset_reference }) => {
3025
2953
  requireGM();
@@ -3069,17 +2997,10 @@ server.registerTool("create_npc", {
3069
2997
  });
3070
2998
  server.registerTool("update_npc", {
3071
2999
  title: "Update NPC",
3072
- description: "Update an existing NPC's fields. Game Master only.",
3000
+ description: "Update an existing NPC's name, description, disposition, location, or goals, persisting the change (Game Master only). Use when: revising a non-player character. Do NOT use when: creating an NPC — use create_npc.",
3073
3001
  // REQ-123 — builder-defined NPC stat fields: fields are builder-determined
3074
3002
  // from the ruleset's stat conventions; every field optional except name.
3075
- inputSchema: {
3076
- npc_id: z.string(),
3077
- name: z.string().optional(),
3078
- description: z.string().optional(),
3079
- disposition: z.string().optional(),
3080
- location: z.string().optional(),
3081
- goals: z.string().optional(),
3082
- },
3003
+ inputSchema: { npc_id: z.string().describe("The NPC identifier."), name: z.string().optional().describe("Optional new name."), description: z.string().optional().describe("Optional description."), disposition: z.string().optional().describe("Optional disposition."), location: z.string().optional().describe("Optional location."), goals: z.string().optional().describe("Optional goals.") },
3083
3004
  }, async ({ npc_id, name, description, disposition, location, goals }) => {
3084
3005
  requireGM();
3085
3006
  const novel = requireNovel();
@@ -3103,8 +3024,8 @@ server.registerTool("update_npc", {
3103
3024
  });
3104
3025
  server.registerTool("remove_npc", {
3105
3026
  title: "Remove NPC",
3106
- description: "Remove an NPC from the novel. Game Master only.",
3107
- inputSchema: { npc_id: z.string() },
3027
+ description: "Remove an NPC from the Novel, persisting the removal (Game Master only). Use when: a non-player character leaves the story. Do NOT use when: removing a player entity — use remove_entity.",
3028
+ inputSchema: { npc_id: z.string().describe("The NPC to remove.") },
3108
3029
  }, async ({ npc_id }) => {
3109
3030
  requireGM();
3110
3031
  const novel = requireNovel();
@@ -3119,26 +3040,26 @@ server.registerTool("remove_npc", {
3119
3040
  // --- Countdowns (GM) ---
3120
3041
  server.registerTool("set_countdown", {
3121
3042
  title: "Set Countdown",
3122
- description: "Set a countdown timer. Game Master only.",
3043
+ description: "Set a countdown timer with ticks, type, scope, direction, and optional world-model triggers, persisting it (Game Master only). Use when: starting a clock or timer. Do NOT use when: advancing a clock — use advance_countdown.",
3123
3044
  inputSchema: {
3124
- name: z.string(),
3125
- ticks: z.number().min(1),
3126
- type: z.enum(["round", "narrative"]).optional(),
3127
- scope: z.string().optional(),
3128
- direction: z.string().optional(),
3129
- on_scene_transition: z.boolean().optional(),
3045
+ name: z.string().describe("The countdown name."),
3046
+ ticks: z.number().min(1).describe("Starting number of ticks (minimum 1)."),
3047
+ type: z.enum(["round", "narrative"]).optional().describe("round or narrative."),
3048
+ scope: z.string().optional().describe("Optional scope name."),
3049
+ direction: z.string().optional().describe("Optional direction: increment or decrement."),
3050
+ on_scene_transition: z.boolean().optional().describe("When true, advance on each scene transition."),
3130
3051
  // REQ-329 — world-model coupling triggers (on_room_enter/on_thing_take/
3131
3052
  // on_door_open). Any match advances one tick; supplements normal advancement.
3132
- triggers: z.array(z.string()).optional(),
3053
+ triggers: z.array(z.string()).optional().describe("Optional world-model triggers (on_room_enter/on_thing_take/on_door_open)."),
3133
3054
  // REQ-368 — world-model effect coupling applied when the countdown fires.
3134
3055
  world_effect: z.object({
3135
- type: z.enum(["describe", "property", "exit"]),
3136
- target: z.string(),
3137
- direction: z.string().optional(),
3138
- destination: z.string().optional(),
3139
- property: z.string().optional(),
3140
- value: z.string().optional(),
3141
- }).optional(),
3056
+ type: z.enum(["describe", "property", "exit"]).describe("The effect kind."),
3057
+ target: z.string().describe("The effect target."),
3058
+ direction: z.string().optional().describe("Optional direction (for exits)."),
3059
+ destination: z.string().optional().describe("Optional destination (for exits)."),
3060
+ property: z.string().optional().describe("Optional property (for property effects)."),
3061
+ value: z.string().optional().describe("Optional value (for property effects)."),
3062
+ }).optional().describe("Optional world-model effect applied when the countdown fires."),
3142
3063
  },
3143
3064
  }, async ({ name, ticks, type, scope, direction, on_scene_transition, triggers, world_effect }) => {
3144
3065
  requireGM();
@@ -3152,8 +3073,8 @@ server.registerTool("set_countdown", {
3152
3073
  });
3153
3074
  server.registerTool("advance_countdown", {
3154
3075
  title: "Advance Countdown",
3155
- description: "Advance a countdown timer by one tick. Game Master only.",
3156
- inputSchema: { name: z.string() },
3076
+ description: "Advance a countdown timer by one tick, firing it when it reaches its end (Game Master only). Use when: the fiction moves a clock forward. Do NOT use when: creating or removing a clock — use set_countdown or remove_countdown.",
3077
+ inputSchema: { name: z.string().describe("The countdown to advance.") },
3157
3078
  }, async ({ name }) => {
3158
3079
  requireGM();
3159
3080
  const novel = requireNovel();
@@ -3193,8 +3114,8 @@ server.registerTool("advance_countdown", {
3193
3114
  });
3194
3115
  server.registerTool("remove_countdown", {
3195
3116
  title: "Remove Countdown",
3196
- description: "Remove a countdown timer. Game Master only.",
3197
- inputSchema: { name: z.string() },
3117
+ description: "Remove a countdown timer, persisting the removal (Game Master only). Use when: a clock is no longer relevant. Do NOT use when: advancing a clock — use advance_countdown.",
3118
+ inputSchema: { name: z.string().describe("The countdown to remove.") },
3198
3119
  }, async ({ name }) => {
3199
3120
  requireGM();
3200
3121
  const novel = requireNovel();
@@ -3209,17 +3130,17 @@ server.registerTool("remove_countdown", {
3209
3130
  // --- Lore (GM) ---
3210
3131
  server.registerTool("set_lore_entry", {
3211
3132
  title: "Set Lore Entry",
3212
- description: "Log a lore entry for the current novel. Game Master only.",
3133
+ description: "Create a lore entry with content, triggers, badge scope, priority, and group for the active Novel (Game Master only). Use when: recording world facts the narrator should recall. Do NOT use when: recording a story beat — use record_story.",
3213
3134
  inputSchema: {
3214
- key: z.string(),
3215
- content: z.string(),
3216
- triggers: z.array(z.string()).optional(),
3217
- badge_scope: z.enum(["game_master", "shared"]).optional(),
3218
- priority: z.number().optional(),
3219
- sticky: z.number().optional(),
3220
- group: z.string().optional(),
3135
+ key: z.string().describe("The lore key."),
3136
+ content: z.string().describe("The lore content."),
3137
+ triggers: z.array(z.string()).optional().describe("Optional recall triggers."),
3138
+ badge_scope: z.enum(["game_master", "shared"]).optional().describe("game_master or shared."),
3139
+ priority: z.number().optional().describe("Optional priority."),
3140
+ sticky: z.number().optional().describe("Optional sticky weight."),
3141
+ group: z.string().optional().describe("Optional group name."),
3221
3142
  // REQ-328 — lore-world coupling: world-model target (room/thing/exit ref).
3222
- world_target: z.string().optional(),
3143
+ world_target: z.string().optional().describe("Optional world-model target reference."),
3223
3144
  },
3224
3145
  }, async ({ key, content, triggers, badge_scope, priority, sticky, group, world_target }) => {
3225
3146
  requireGM();
@@ -3244,16 +3165,8 @@ server.registerTool("set_lore_entry", {
3244
3165
  });
3245
3166
  server.registerTool("update_lore_entry", {
3246
3167
  title: "Update Lore Entry",
3247
- description: "Update fields of an existing lore entry. Game Master only.",
3248
- inputSchema: {
3249
- key: z.string(),
3250
- content: z.string().optional(),
3251
- triggers: z.array(z.string()).optional(),
3252
- badge_scope: z.enum(["game_master", "shared"]).optional(),
3253
- priority: z.number().optional(),
3254
- sticky: z.number().optional(),
3255
- group: z.string().nullable().optional(),
3256
- },
3168
+ description: "Update fields of an existing lore entry, persisting the change (Game Master only). Use when: revising a world fact. Do NOT use when: creating a lore entry — use set_lore_entry.",
3169
+ inputSchema: { key: z.string().describe("The lore key to update."), content: z.string().optional().describe("Optional new content."), triggers: z.array(z.string()).optional().describe("Optional recall triggers."), badge_scope: z.enum(["game_master", "shared"]).optional().describe("game_master or shared."), priority: z.number().optional().describe("Optional priority."), sticky: z.number().optional().describe("Optional sticky weight."), group: z.string().nullable().optional().describe("Optional group name, or null to clear.") },
3257
3170
  }, async ({ key, content, triggers, badge_scope, priority, sticky, group }) => {
3258
3171
  requireGM();
3259
3172
  const novel = requireNovel();
@@ -3284,8 +3197,8 @@ server.registerTool("update_lore_entry", {
3284
3197
  });
3285
3198
  server.registerTool("remove_lore_entry", {
3286
3199
  title: "Remove Lore Entry",
3287
- description: "Remove a lore entry. Game Master only.",
3288
- inputSchema: { key: z.string() },
3200
+ description: "Remove a lore entry from the Novel, persisting the removal (Game Master only). Use when: a world fact is obsolete. Do NOT use when: hiding a lore entry without deleting it — use toggle_lore_entry.",
3201
+ inputSchema: { key: z.string().describe("The lore key to remove.") },
3289
3202
  }, async ({ key }) => {
3290
3203
  requireGM();
3291
3204
  const novel = requireNovel();
@@ -3298,8 +3211,8 @@ server.registerTool("remove_lore_entry", {
3298
3211
  });
3299
3212
  server.registerTool("toggle_lore_entry", {
3300
3213
  title: "Toggle Lore Entry",
3301
- description: "Enable or disable a lore entry. Game Master only.",
3302
- inputSchema: { key: z.string() },
3214
+ description: "Enable or disable a lore entry without deleting it, persisting the toggle (Game Master only). Use when: temporarily hiding a world fact. Do NOT use when: deleting a lore entry — use remove_lore_entry.",
3215
+ inputSchema: { key: z.string().describe("The lore key to toggle.") },
3303
3216
  }, async ({ key }) => {
3304
3217
  requireGM();
3305
3218
  const novel = requireNovel();
@@ -3313,8 +3226,8 @@ server.registerTool("toggle_lore_entry", {
3313
3226
  });
3314
3227
  server.registerTool("set_lore_group", {
3315
3228
  title: "Set Lore Group",
3316
- description: "Assign or remove a lore entry from a named group. Game Master only.",
3317
- inputSchema: { key: z.string(), group: z.string().nullable() },
3229
+ description: "Assign or remove a lore entry from a named group, persisting the grouping (Game Master only). Use when: organizing lore by theme or faction. Do NOT use when: creating a lore entry — use set_lore_entry.",
3230
+ inputSchema: { key: z.string().describe("The lore key."), group: z.string().nullable().describe("The group name, or null to remove from its group.") },
3318
3231
  }, async ({ key, group }) => {
3319
3232
  requireGM();
3320
3233
  const novel = requireNovel();
@@ -3331,7 +3244,7 @@ server.registerTool("set_lore_group", {
3331
3244
  });
3332
3245
  server.registerTool("suggest_lore", {
3333
3246
  title: "Suggest Lore",
3334
- description: "Suggest lore entries from enrichment templates based on current scene. Game Master only.",
3247
+ description: "Suggest lore entries from enrichment templates based on the current scene, without persisting them (Game Master only). Use when: the GM wants candidate world facts to adopt. Do NOT use when: creating a lore entry directly — use set_lore_entry.",
3335
3248
  inputSchema: {},
3336
3249
  }, async () => {
3337
3250
  requireGM();
@@ -3344,12 +3257,12 @@ server.registerTool("suggest_lore", {
3344
3257
  });
3345
3258
  server.registerTool("export_lorebook", {
3346
3259
  title: "Export Lorebook",
3347
- description: "Export novel lore entries in interchange format. Game Master only.",
3260
+ description: "Export the Novel's lore entries in interchange format for backup or transfer (Game Master only). Use when: moving lore between Novels. Do NOT use when: importing lore — use import_lorebook.",
3348
3261
  // REQ-094 — lorebook interchange: lore-only export/import with merge, replace,
3349
3262
  // and dry-run modes; round-trip preserves lore metadata (Appendix L).
3350
3263
  // REQ-425b — interchange surfaces accept only json/markdown; html is a
3351
3264
  // presentation-only format and returns [INVALID_INPUT] enumerating the set.
3352
- inputSchema: { format: z.string().optional() },
3265
+ inputSchema: { format: z.string().optional().describe("Optional output format.") },
3353
3266
  }, async ({ format: fmt }) => {
3354
3267
  requireGM();
3355
3268
  if (fmt && !INTERCHANGE_FORMATS.includes(fmt)) {
@@ -3372,11 +3285,8 @@ server.registerTool("export_lorebook", {
3372
3285
  });
3373
3286
  server.registerTool("import_lorebook", {
3374
3287
  title: "Import Lorebook",
3375
- description: "Import lore entries from JSON or Markdown. Modes: dry-run, merge, or replace. Game Master only.",
3376
- inputSchema: {
3377
- data: z.string(),
3378
- mode: z.enum(["dry-run", "merge", "replace"]).optional(),
3379
- },
3288
+ description: "Import lore entries from JSON or Markdown in dry-run, merge, or replace mode (Game Master only). Use when: loading a lorebook. Do NOT use when: exporting lore — use export_lorebook.",
3289
+ inputSchema: { data: z.string().describe("JSON or Markdown lorebook data."), mode: z.enum(["dry-run", "merge", "replace"]).optional().describe("dry-run, merge, or replace.") },
3380
3290
  }, async ({ data, mode }) => {
3381
3291
  requireGM();
3382
3292
  const novel = requireNovel();
@@ -3422,8 +3332,8 @@ function conditionCatalogue(novel) {
3422
3332
  }
3423
3333
  server.registerTool("apply_condition", {
3424
3334
  title: "Apply Condition",
3425
- description: "Apply a condition to an entity.",
3426
- inputSchema: { entity_id: z.string(), condition: z.string(), rounds: z.number().optional() },
3335
+ description: "Apply a condition to an entity, persisting it to the Novel. Use when: an entity gains a mechanical or narrative status. Do NOT use when: clearing a condition — use remove_condition.",
3336
+ inputSchema: { entity_id: z.string().describe("The entity to affect."), condition: z.string().describe("The condition name."), rounds: z.number().optional().describe("Optional duration in rounds.") },
3427
3337
  }, async ({ entity_id, condition, rounds }) => {
3428
3338
  requireGM();
3429
3339
  const novel = requireNovel();
@@ -3452,8 +3362,8 @@ server.registerTool("apply_condition", {
3452
3362
  });
3453
3363
  server.registerTool("remove_condition", {
3454
3364
  title: "Remove Condition",
3455
- description: "Remove a condition from an entity.",
3456
- inputSchema: { entity_id: z.string(), condition: z.string() },
3365
+ description: "Remove a condition from an entity, persisting the change. Use when: a status ends or is cured. Do NOT use when: applying a condition — use apply_condition.",
3366
+ inputSchema: { entity_id: z.string().describe("The entity to affect."), condition: z.string().describe("The condition to remove.") },
3457
3367
  }, async ({ entity_id, condition }) => {
3458
3368
  requireGM();
3459
3369
  const novel = requireNovel();
@@ -3473,8 +3383,8 @@ server.registerTool("remove_condition", {
3473
3383
  // --- Factions (GM) ---
3474
3384
  server.registerTool("create_faction", {
3475
3385
  title: "Create Faction",
3476
- description: "Create a named faction with goals, resources, and a progress clock. Game Master only.",
3477
- inputSchema: { name: z.string(), description: z.string().optional(), goals: z.array(z.string()).optional(), resources: z.string().optional(), territory: z.array(z.string()).optional() },
3386
+ description: "Create a named faction with goals, resources, and a progress clock, persisting it (Game Master only). Use when: introducing an organization. Do NOT use when: editing a faction — use update_faction.",
3387
+ inputSchema: { name: z.string().describe("The faction name."), description: z.string().optional().describe("Optional description."), goals: z.array(z.string()).optional().describe("Optional goals."), resources: z.string().optional().describe("Optional resources."), territory: z.array(z.string()).optional().describe("Optional territory names.") },
3478
3388
  }, async ({ name, description, goals, resources, territory }) => {
3479
3389
  requireGM();
3480
3390
  const novel = requireNovel();
@@ -3492,8 +3402,8 @@ server.registerTool("create_faction", {
3492
3402
  });
3493
3403
  server.registerTool("update_faction", {
3494
3404
  title: "Update Faction",
3495
- description: "Update a faction's fields. Game Master only.",
3496
- inputSchema: { faction_id: z.string(), description: z.string().optional(), goals: z.array(z.string()).optional(), resources: z.string().optional(), territory: z.array(z.string()).optional() },
3405
+ description: "Update a faction's fields, persisting the change (Game Master only). Use when: revising an organization. Do NOT use when: creating a faction — use create_faction.",
3406
+ inputSchema: { faction_id: z.string().describe("The faction identifier."), description: z.string().optional().describe("Optional description."), goals: z.array(z.string()).optional().describe("Optional goals."), resources: z.string().optional().describe("Optional resources."), territory: z.array(z.string()).optional().describe("Optional territory names.") },
3497
3407
  }, async ({ faction_id, ...fields }) => {
3498
3408
  requireGM();
3499
3409
  const novel = requireNovel();
@@ -3506,8 +3416,8 @@ server.registerTool("update_faction", {
3506
3416
  });
3507
3417
  server.registerTool("remove_faction", {
3508
3418
  title: "Remove Faction",
3509
- description: "Remove a faction and its clock. Game Master only.",
3510
- inputSchema: { faction_id: z.string() },
3419
+ description: "Remove a faction and its progress clock, persisting the removal (Game Master only). Use when: an organization leaves the story. Do NOT use when: editing a faction — use update_faction.",
3420
+ inputSchema: { faction_id: z.string().describe("The faction to remove.") },
3511
3421
  }, async ({ faction_id }) => {
3512
3422
  requireGM();
3513
3423
  const novel = requireNovel();
@@ -3523,8 +3433,8 @@ server.registerTool("remove_faction", {
3523
3433
  // --- Secrets (GM) ---
3524
3434
  server.registerTool("set_secret", {
3525
3435
  title: "Set Secret",
3526
- description: "Create a secret lore entry. GM-only; visible to entities after reveal_secret. Game Master only.",
3527
- inputSchema: { key: z.string(), content: z.string(), triggers: z.array(z.string()).optional(), badge_scope: z.enum(["game_master", "shared"]).optional(), world_target: z.string().optional() },
3436
+ description: "Create a GM-only secret lore entry that becomes visible to an entity only after reveal_secret (Game Master only). Use when: recording hidden information. Do NOT use when: creating public lore — use set_lore_entry.",
3437
+ inputSchema: { key: z.string().describe("The secret key."), content: z.string().describe("The secret content."), triggers: z.array(z.string()).optional().describe("Optional recall triggers."), badge_scope: z.enum(["game_master", "shared"]).optional().describe("game_master or shared."), world_target: z.string().optional().describe("Optional world-model target.") },
3528
3438
  }, async ({ key, content, triggers, badge_scope, world_target }) => {
3529
3439
  requireGM();
3530
3440
  const novel = requireNovel();
@@ -3536,8 +3446,8 @@ server.registerTool("set_secret", {
3536
3446
  });
3537
3447
  server.registerTool("reveal_secret", {
3538
3448
  title: "Reveal Secret",
3539
- description: "Make a secret known to a specific entity. Game Master only.",
3540
- inputSchema: { key: z.string(), entity_id: z.string() },
3449
+ description: "Reveal a secret to a specific entity, persisting that entity's knowledge (Game Master only). Use when: a character learns hidden information. Do NOT use when: creating a secret — use set_secret.",
3450
+ inputSchema: { key: z.string().describe("The secret to reveal."), entity_id: z.string().describe("The entity to reveal it to.") },
3541
3451
  }, async ({ key, entity_id }) => {
3542
3452
  requireGM();
3543
3453
  const novel = requireNovel();
@@ -3553,8 +3463,8 @@ server.registerTool("reveal_secret", {
3553
3463
  });
3554
3464
  server.registerTool("get_knowledge", {
3555
3465
  title: "Get Knowledge",
3556
- description: "Return what secrets an entity knows. Game Master only.",
3557
- inputSchema: { entity_id: z.string(), key: z.string().optional() },
3466
+ description: "Return which secrets an entity currently knows (Game Master only). Use when: checking what a character has learned. Do NOT use when: revealing a secret — use reveal_secret.",
3467
+ inputSchema: { entity_id: z.string().describe("The entity whose knowledge to read."), key: z.string().optional().describe("Optional secret key filter.") },
3558
3468
  }, async ({ entity_id, key }) => {
3559
3469
  requireGM();
3560
3470
  const novel = requireNovel();
@@ -3570,8 +3480,8 @@ server.registerTool("get_knowledge", {
3570
3480
  // --- Relationships (GM) ---
3571
3481
  server.registerTool("set_relationship", {
3572
3482
  title: "Set Relationship",
3573
- description: "Set a directed relationship between entities, NPCs, or factions. Types: ally, rival, neutral, mentor, dependent, suspicious. Game Master only.",
3574
- inputSchema: { entity_a: z.string(), entity_b: z.string(), type: z.enum(["ally", "rival", "neutral", "mentor", "dependent", "suspicious"]), value: z.number().optional(), description: z.string().optional() },
3483
+ description: "Set a directed relationship (ally, rival, neutral, mentor, dependent, suspicious) between entities, NPCs, or factions, persisting it (Game Master only). Use when: recording how two parties relate. Do NOT use when: listing relationships — use get_relationships.",
3484
+ inputSchema: { entity_a: z.string().describe("The source entity."), entity_b: z.string().describe("The target entity."), type: z.enum(["ally", "rival", "neutral", "mentor", "dependent", "suspicious"]).describe("Relationship type."), value: z.number().optional().describe("Optional relationship strength."), description: z.string().optional().describe("Optional description.") },
3575
3485
  }, async ({ entity_a, entity_b, type, value, description }) => {
3576
3486
  requireGM();
3577
3487
  const novel = requireNovel();
@@ -3581,8 +3491,8 @@ server.registerTool("set_relationship", {
3581
3491
  });
3582
3492
  server.registerTool("get_relationships", {
3583
3493
  title: "Get Relationships",
3584
- description: "Return all relationships (incoming and outgoing) for an entity. Game Master only.",
3585
- inputSchema: { entity_id: z.string() },
3494
+ description: "Return all incoming and outgoing relationships for an entity (Game Master only). Use when: reviewing how a character is connected. Do NOT use when: setting a relationship — use set_relationship.",
3495
+ inputSchema: { entity_id: z.string().describe("The entity whose relationships to list.") },
3586
3496
  }, async ({ entity_id }) => {
3587
3497
  requireGM();
3588
3498
  const novel = requireNovel();
@@ -3593,8 +3503,8 @@ server.registerTool("get_relationships", {
3593
3503
  // --- Vows (GM) ---
3594
3504
  server.registerTool("set_vow", {
3595
3505
  title: "Set Vow",
3596
- description: "Track a narrative vow, quest, or obligation. Game Master only.",
3597
- inputSchema: { name: z.string(), description: z.string(), parties: z.array(z.string()), difficulty: z.enum(["troublesome", "dangerous", "formidable", "extreme", "epic"]), scope: z.enum(["gm", "shared", "faction", "party"]).optional() },
3506
+ description: "Track a narrative vow, quest, or obligation with milestones, persisting it (Game Master only). Use when: the story commits a character to a goal. Do NOT use when: advancing a vow — use mark_milestone.",
3507
+ inputSchema: { name: z.string().describe("The vow name."), description: z.string().describe("The vow description."), parties: z.array(z.string()).describe("Parties bound by the vow."), difficulty: z.enum(["troublesome", "dangerous", "formidable", "extreme", "epic"]).describe("troublesome, dangerous, formidable, extreme, or epic."), scope: z.enum(["gm", "shared", "faction", "party"]).optional().describe("gm, shared, faction, or party.") },
3598
3508
  }, async ({ name, description, parties, difficulty, scope }) => {
3599
3509
  requireGM();
3600
3510
  const novel = requireNovel();
@@ -3621,8 +3531,8 @@ server.registerTool("set_vow", {
3621
3531
  });
3622
3532
  server.registerTool("mark_milestone", {
3623
3533
  title: "Mark Milestone",
3624
- description: "Advance a vow's progress by one milestone. Game Master only.",
3625
- inputSchema: { vow_name: z.string() },
3534
+ description: "Advance a vow's progress by one milestone, persisting it (Game Master only). Use when: the character makes progress. Do NOT use when: closing a vow — use resolve_vow.",
3535
+ inputSchema: { vow_name: z.string().describe("The vow to advance.") },
3626
3536
  }, async ({ vow_name }) => {
3627
3537
  requireGM();
3628
3538
  const novel = requireNovel();
@@ -3646,8 +3556,8 @@ server.registerTool("mark_milestone", {
3646
3556
  });
3647
3557
  server.registerTool("resolve_vow", {
3648
3558
  title: "Resolve Vow",
3649
- description: "Close a completed vow with outcome and consequences. Game Master only.",
3650
- inputSchema: { vow_name: z.string(), outcome: z.string(), consequences: z.string().optional() },
3559
+ description: "Close a completed vow with an outcome and consequences, persisting it (Game Master only). Use when: the goal is achieved. Do NOT use when: abandoning a vow — use forsake_vow.",
3560
+ inputSchema: { vow_name: z.string().describe("The vow to close."), outcome: z.string().describe("The resolution outcome."), consequences: z.string().optional().describe("Optional consequences.") },
3651
3561
  }, async ({ vow_name, outcome, consequences }) => {
3652
3562
  requireGM();
3653
3563
  const novel = requireNovel();
@@ -3662,8 +3572,8 @@ server.registerTool("resolve_vow", {
3662
3572
  });
3663
3573
  server.registerTool("forsake_vow", {
3664
3574
  title: "Forsake Vow",
3665
- description: "Abandon a vow with a reason. Game Master only.",
3666
- inputSchema: { vow_name: z.string(), reason: z.string() },
3575
+ description: "Abandon a vow with a reason, persisting the abandonment (Game Master only). Use when: the character gives up. Do NOT use when: completing a vow — use resolve_vow.",
3576
+ inputSchema: { vow_name: z.string().describe("The vow to abandon."), reason: z.string().describe("The reason for abandoning.") },
3667
3577
  }, async ({ vow_name, reason }) => {
3668
3578
  requireGM();
3669
3579
  const novel = requireNovel();
@@ -3680,8 +3590,8 @@ server.registerTool("forsake_vow", {
3680
3590
  // immutable → [RULE_VIOLATION]; existing key → [STATE_CONFLICT]).
3681
3591
  server.registerTool("promote_story_to_lore", {
3682
3592
  title: "Promote Story to Lore",
3683
- description: "Promote a story journal entry into a lore entry. Game Master only.",
3684
- inputSchema: { index: z.number(), key: z.string().optional() },
3593
+ description: "Promote a story journal entry into a lore entry, persisting it (Game Master only). Use when: a story moment becomes a durable world fact. Do NOT use when: recording a new story beat — use record_story.",
3594
+ inputSchema: { index: z.number().describe("The story journal index to promote."), key: z.string().optional().describe("Optional lore key.") },
3685
3595
  }, async ({ index, key }) => {
3686
3596
  requireGM();
3687
3597
  const novel = requireNovel();
@@ -3710,8 +3620,8 @@ server.registerTool("promote_story_to_lore", {
3710
3620
  // --- Story Journal (GM) ---
3711
3621
  server.registerTool("record_story", {
3712
3622
  title: "Record Story",
3713
- description: "Record a narrative memory in the story journal. Types: decision, moment, revelation, bond, consequence. Game Master only.",
3714
- inputSchema: { type: z.enum(["decision", "moment", "revelation", "bond", "consequence"]), entry: z.string() },
3623
+ description: "Record a narrative memory (decision, moment, revelation, bond, or consequence) in the story journal (Game Master only). Use when: capturing a story beat. Do NOT use when: recording a durable world fact — use set_lore_entry.",
3624
+ inputSchema: { type: z.enum(["decision", "moment", "revelation", "bond", "consequence"]).describe("The story entry type."), entry: z.string().describe("The story entry text.") },
3715
3625
  }, async ({ type, entry }) => {
3716
3626
  requireGM();
3717
3627
  const novel = requireNovel();
@@ -3735,8 +3645,8 @@ server.registerTool("record_story", {
3735
3645
  });
3736
3646
  server.registerTool("update_story", {
3737
3647
  title: "Update Story",
3738
- description: "Edit a story journal entry by index. Decision and consequence entries are immutable. Game Master only.",
3739
- inputSchema: { index: z.number().min(0), type: z.enum(["decision", "moment", "revelation", "bond", "consequence"]).optional(), entry: z.string().optional() },
3648
+ description: "Edit a story journal entry by index; decision and consequence entries are immutable (Game Master only). Use when: correcting a recorded memory. Do NOT use when: deleting an entry — use remove_story.",
3649
+ inputSchema: { index: z.number().min(0).describe("The story entry index."), type: z.enum(["decision", "moment", "revelation", "bond", "consequence"]).optional().describe("Optional new type."), entry: z.string().optional().describe("Optional new entry text.") },
3740
3650
  }, async ({ index, type, entry }) => {
3741
3651
  requireGM();
3742
3652
  const novel = requireNovel();
@@ -3754,8 +3664,8 @@ server.registerTool("update_story", {
3754
3664
  });
3755
3665
  server.registerTool("remove_story", {
3756
3666
  title: "Remove Story",
3757
- description: "Delete a story journal entry by index. Game Master only.",
3758
- inputSchema: { index: z.number().min(0) },
3667
+ description: "Delete a story journal entry by index, persisting the removal (Game Master only). Use when: removing a mistaken memory. Do NOT use when: editing an entry — use update_story.",
3668
+ inputSchema: { index: z.number().min(0).describe("The story entry index to remove.") },
3759
3669
  }, async ({ index }) => {
3760
3670
  requireGM();
3761
3671
  const novel = requireNovel();
@@ -3767,8 +3677,8 @@ server.registerTool("remove_story", {
3767
3677
  });
3768
3678
  server.registerTool("list_stories", {
3769
3679
  title: "List Stories",
3770
- description: "List story journal entries with optional type filter and pagination. Game Master only.",
3771
- inputSchema: { filter: z.enum(["decision", "moment", "revelation", "bond", "consequence"]).optional(), offset: z.number().min(0).optional(), limit: z.number().min(0).optional(), ...detailZod },
3680
+ description: "List story journal entries with an optional type filter and pagination (Game Master only). Use when: reviewing the story so far. Do NOT use when: recording a new entry — use record_story.",
3681
+ inputSchema: { filter: z.enum(["decision", "moment", "revelation", "bond", "consequence"]).optional().describe("Optional type filter."), offset: z.number().min(0).optional().describe("Optional pagination offset."), limit: z.number().min(0).optional().describe("Optional page size."), ...detailZod },
3772
3682
  }, async ({ filter, offset, limit, detail }) => {
3773
3683
  requireGM();
3774
3684
  const novel = requireNovel();
@@ -3786,8 +3696,8 @@ server.registerTool("list_stories", {
3786
3696
  // --- Notes ---
3787
3697
  server.registerTool("set_note", {
3788
3698
  title: "Set Note",
3789
- description: "Create or update a key-value note. Badge-scoped: game_master (default), player, or shared.",
3790
- inputSchema: { key: z.string(), content: z.string(), badge_scope: z.enum(["game_master", "player", "shared"]).optional() },
3699
+ description: "Create or update a key-value note, badge-scoped to game_master (default), player, or shared. Use when: storing scratch state the caller will reuse. Do NOT use when: recording durable world facts — use set_lore_entry.",
3700
+ inputSchema: { key: z.string().describe("The note key."), content: z.string().describe("The note content."), badge_scope: z.enum(["game_master", "player", "shared"]).optional().describe("game_master, player, or shared.") },
3791
3701
  }, async ({ key, content, badge_scope }) => {
3792
3702
  requireNotObserver();
3793
3703
  const novel = requireNovel();
@@ -3813,8 +3723,8 @@ server.registerTool("set_note", {
3813
3723
  });
3814
3724
  server.registerTool("remove_note", {
3815
3725
  title: "Remove Note",
3816
- description: "Remove a note by key. Badge-scoped: caller's badge must own the scope.",
3817
- inputSchema: { key: z.string() },
3726
+ description: "Remove a note by key; the caller's badge must own the note's scope. Use when: discarding scratch state. Do NOT use when: creating a note — use set_note.",
3727
+ inputSchema: { key: z.string().describe("The note key to remove.") },
3818
3728
  }, async ({ key }) => {
3819
3729
  requireNotObserver();
3820
3730
  const novel = requireNovel();
@@ -3831,7 +3741,7 @@ server.registerTool("remove_note", {
3831
3741
  });
3832
3742
  server.registerTool("list_notes", {
3833
3743
  title: "List Notes",
3834
- description: "List all notes (100-character preview), badge-filtered.",
3744
+ description: "List all notes with a 100-character preview, badge-filtered to the caller's scope. Use when: reviewing stored scratch state. Do NOT use when: setting a note — use set_note.",
3835
3745
  inputSchema: {},
3836
3746
  }, async () => {
3837
3747
  const novel = requireNovel();
@@ -3843,8 +3753,8 @@ server.registerTool("list_notes", {
3843
3753
  // --- Server Notes (GM) ---
3844
3754
  server.registerTool("set_server_note", {
3845
3755
  title: "Set Server Note",
3846
- description: "Create or update a server-level note. Game Master only.",
3847
- inputSchema: { key: z.string(), content: z.string(), narrative_tag: z.enum(["campaign_bible", "house_rules", "lore_seed", "session_reminder"]).optional() },
3756
+ description: "Create or update a server-level note that survives Novels and rebuilds (Game Master only). Use when: storing cross-Novel scratch state. Do NOT use when: storing Novel-scoped notes — use set_note.",
3757
+ inputSchema: { key: z.string().describe("The note key."), content: z.string().describe("The note content."), narrative_tag: z.enum(["campaign_bible", "house_rules", "lore_seed", "session_reminder"]).optional().describe("Optional narrative tag: campaign_bible, house_rules, lore_seed, or session_reminder.") },
3848
3758
  }, async ({ key, content, narrative_tag }) => {
3849
3759
  requireGM();
3850
3760
  state.serverNotes.set(key, { content, narrative_tag });
@@ -3853,8 +3763,8 @@ server.registerTool("set_server_note", {
3853
3763
  });
3854
3764
  server.registerTool("remove_server_note", {
3855
3765
  title: "Remove Server Note",
3856
- description: "Remove a server-level note. Game Master only.",
3857
- inputSchema: { key: z.string() },
3766
+ description: "Remove a server-level note, persisting the removal (Game Master only). Use when: discarding cross-Novel scratch state. Do NOT use when: creating a server note — use set_server_note.",
3767
+ inputSchema: { key: z.string().describe("The server note key to remove.") },
3858
3768
  }, async ({ key }) => {
3859
3769
  requireGM();
3860
3770
  if (!state.serverNotes.has(key))
@@ -3865,7 +3775,7 @@ server.registerTool("remove_server_note", {
3865
3775
  });
3866
3776
  server.registerTool("list_server_notes", {
3867
3777
  title: "List Server Notes",
3868
- description: "List all server-level notes. Game Master only.",
3778
+ description: "List all server-level notes (Game Master only). Use when: reviewing cross-Novel scratch state. Do NOT use when: listing Novel-scoped notes — use list_notes.",
3869
3779
  inputSchema: {},
3870
3780
  }, async () => {
3871
3781
  requireGM();
@@ -3875,8 +3785,8 @@ server.registerTool("list_server_notes", {
3875
3785
  // --- Pause/Resume (GM) ---
3876
3786
  server.registerTool("set_pause_context", {
3877
3787
  title: "Set Pause Context",
3878
- description: "Save GM context for session resumption. Game Master only.",
3879
- inputSchema: { current_scene: z.string().optional(), immediate_situation: z.string().optional(), pending_player_action: z.string().optional(), short_term_plans: z.string().optional(), long_term_plans: z.string().optional(), player_goals: z.string().optional() },
3788
+ description: "Save GM context for session resumption, persisting it (Game Master only). Use when: ending a session with notes for the next. Do NOT use when: reading saved context — use get_pause_context.",
3789
+ inputSchema: { current_scene: z.string().optional().describe("Optional current-scene summary."), immediate_situation: z.string().optional().describe("Optional immediate situation."), pending_player_action: z.string().optional().describe("Optional pending player action."), short_term_plans: z.string().optional().describe("Optional short-term plans."), long_term_plans: z.string().optional().describe("Optional long-term plans."), player_goals: z.string().optional().describe("Optional player goals.") },
3880
3790
  }, async (fields) => {
3881
3791
  requireGM();
3882
3792
  const novel = requireNovel();
@@ -3899,7 +3809,7 @@ server.registerTool("set_pause_context", {
3899
3809
  });
3900
3810
  server.registerTool("get_pause_context", {
3901
3811
  title: "Get Pause Context",
3902
- description: "Return the saved GM context plus Novel state summary for session resumption.",
3812
+ description: "Return the saved GM context plus a Novel state summary for session resumption. Use when: resuming after a break. Do NOT use when: saving context — use set_pause_context.",
3903
3813
  inputSchema: {},
3904
3814
  }, async () => {
3905
3815
  const novel = requireNovel();
@@ -3915,8 +3825,8 @@ server.registerTool("get_pause_context", {
3915
3825
  // --- Checkpoints (GM) ---
3916
3826
  server.registerTool("set_checkpoint", {
3917
3827
  title: "Set Checkpoint",
3918
- description: "Save a named checkpoint of the full Novel state. Game Master only.",
3919
- inputSchema: { label: z.string() },
3828
+ description: "Save a named checkpoint of the full Novel state, persisting it (Game Master only). Use when: marking a returnable point in the story. Do NOT use when: restoring a checkpoint — use restore_checkpoint.",
3829
+ inputSchema: { label: z.string().describe("The checkpoint label.") },
3920
3830
  }, async ({ label }) => {
3921
3831
  requireGM();
3922
3832
  const novel = requireNovel();
@@ -3926,7 +3836,7 @@ server.registerTool("set_checkpoint", {
3926
3836
  });
3927
3837
  server.registerTool("list_checkpoints", {
3928
3838
  title: "List Checkpoints",
3929
- description: "List all checkpoints for the active Novel. Game Master only.",
3839
+ description: "List all checkpoints for the active Novel (Game Master only). Use when: reviewing available return points. Do NOT use when: creating a checkpoint — use set_checkpoint.",
3930
3840
  inputSchema: {},
3931
3841
  }, async () => {
3932
3842
  requireGM();
@@ -3935,8 +3845,8 @@ server.registerTool("list_checkpoints", {
3935
3845
  });
3936
3846
  server.registerTool("restore_checkpoint", {
3937
3847
  title: "Restore Checkpoint",
3938
- description: "Restore a checkpoint (confirmation required). Game Master only.",
3939
- inputSchema: { label: z.string() },
3848
+ description: "Restore a checkpoint after a confirmation workflow, replacing the Novel state (Game Master only). Use when: returning to a prior point. Do NOT use when: saving a checkpoint — use set_checkpoint.",
3849
+ inputSchema: { label: z.string().describe("The checkpoint to restore.") },
3940
3850
  }, async ({ label }) => {
3941
3851
  requireGM();
3942
3852
  const novel = requireNovel();
@@ -3957,8 +3867,8 @@ Options: yes, cancel`);
3957
3867
  });
3958
3868
  server.registerTool("remove_checkpoint", {
3959
3869
  title: "Remove Checkpoint",
3960
- description: "Remove a named checkpoint. Game Master only.",
3961
- inputSchema: { label: z.string() },
3870
+ description: "Remove a named checkpoint, persisting the removal (Game Master only). Use when: discarding a return point. Do NOT use when: restoring a checkpoint — use restore_checkpoint.",
3871
+ inputSchema: { label: z.string().describe("The checkpoint to remove.") },
3962
3872
  }, async ({ label }) => {
3963
3873
  requireGM();
3964
3874
  const novel = requireNovel();
@@ -3972,8 +3882,8 @@ server.registerTool("remove_checkpoint", {
3972
3882
  // --- Novel Lifecycle additions (GM) ---
3973
3883
  server.registerTool("rename_novel", {
3974
3884
  title: "Rename Novel",
3975
- description: "Rename the active Novel on disk. Game Master only.",
3976
- inputSchema: { new_slug: z.string() },
3885
+ description: "Rename the active Novel on disk, persisting the change (Game Master only). Use when: correcting the Novel's name. Do NOT use when: switching to another Novel — use switch_novel.",
3886
+ inputSchema: { new_slug: z.string().describe("The new Novel slug.") },
3977
3887
  }, async ({ new_slug }) => {
3978
3888
  requireGM();
3979
3889
  const novel = requireNovel();
@@ -3993,8 +3903,8 @@ server.registerTool("rename_novel", {
3993
3903
  // description, surfaced in novel_info and badge_briefing.
3994
3904
  server.registerTool("update_novel_description", {
3995
3905
  title: "Update Novel Description",
3996
- description: "Set or replace the active Novel's description. An empty string clears it. Game Master only.",
3997
- inputSchema: { description: z.string() },
3906
+ description: "Set or replace the active Novel's description; an empty string clears it (Game Master only). Use when: summarizing the campaign premise. Do NOT use when: setting the genre — use set_genre.",
3907
+ inputSchema: { description: z.string().describe("The new Novel description; empty clears it.") },
3998
3908
  }, async ({ description }) => {
3999
3909
  requireGM();
4000
3910
  const novel = requireNovel();
@@ -4005,8 +3915,8 @@ server.registerTool("update_novel_description", {
4005
3915
  });
4006
3916
  server.registerTool("list_novels", {
4007
3917
  title: "List Novels",
4008
- description: "List all Novels on disk with metadata. Always callable.",
4009
- inputSchema: { ...detailZod, filter: z.enum(["active", "archived", "all"]).optional() },
3918
+ description: "List all Novels on disk with metadata, always callable. Use when: discovering available save files. Do NOT use when: inspecting one Novel — use novel_info.",
3919
+ inputSchema: { ...detailZod, filter: z.enum(["active", "archived", "all"]).optional().describe("Optional filter: active, archived, or all.") },
4010
3920
  }, async ({ detail, filter }) => {
4011
3921
  const arch = state.archivedNovels();
4012
3922
  const archivedSlugs = new Set(arch.map((a) => a.slug));
@@ -4025,8 +3935,8 @@ server.registerTool("list_novels", {
4025
3935
  // REQ-334 — archive/unarchive Novel (Game Master only).
4026
3936
  server.registerTool("archive_novel", {
4027
3937
  title: "Archive Novel",
4028
- description: "Move a Novel to the long-term archive (read-only). Game Master only.",
4029
- inputSchema: { slug: z.string() },
3938
+ description: "Move a Novel to the long-term read-only archive (Game Master only). Use when: retiring a finished campaign. Do NOT use when: restoring an archived Novel — use unarchive_novel.",
3939
+ inputSchema: { slug: z.string().describe("The Novel slug to archive.") },
4030
3940
  }, async ({ slug }) => {
4031
3941
  requireGM();
4032
3942
  if (state.activeNovelId === slug)
@@ -4037,8 +3947,8 @@ server.registerTool("archive_novel", {
4037
3947
  });
4038
3948
  server.registerTool("unarchive_novel", {
4039
3949
  title: "Unarchive Novel",
4040
- description: "Restore an archived Novel to active status. Game Master only.",
4041
- inputSchema: { slug: z.string() },
3950
+ description: "Restore an archived Novel to active status (Game Master only). Use when: reviving a retired campaign. Do NOT use when: archiving a Novel — use archive_novel.",
3951
+ inputSchema: { slug: z.string().describe("The Novel slug to restore.") },
4042
3952
  }, async ({ slug }) => {
4043
3953
  requireGM();
4044
3954
  const novel = state.unarchiveNovel(slug);
@@ -4047,8 +3957,8 @@ server.registerTool("unarchive_novel", {
4047
3957
  });
4048
3958
  server.registerTool("novel_info", {
4049
3959
  title: "Novel Info",
4050
- description: "Return extended metadata for a Novel. Always callable.",
4051
- inputSchema: { slug: z.string().optional() },
3960
+ description: "Return extended metadata for a Novel, always callable. Use when: inspecting a Novel's settings and stats. Do NOT use when: listing all Novels — use list_novels.",
3961
+ inputSchema: { slug: z.string().optional().describe("Optional Novel slug; defaults to the active Novel.") },
4052
3962
  }, async ({ slug }) => {
4053
3963
  const novel = slug ? state.novels.get(slug) : state.activeNovel;
4054
3964
  if (!novel)
@@ -4068,8 +3978,8 @@ server.registerTool("novel_info", {
4068
3978
  const GENRE_CATALOG = ["noir", "high_fantasy", "sword_and_sorcery", "sci_fi_horror", "cosmic_horror", "historical", "western", "modern", "cyberpunk"];
4069
3979
  server.registerTool("set_genre", {
4070
3980
  title: "Set Genre",
4071
- description: "Set the active Novel's genre tag. Game Master only. Valid: noir, high_fantasy, sword_and_sorcery, sci_fi_horror, cosmic_horror, historical, western, modern, cyberpunk.",
4072
- inputSchema: { genre: z.string() },
3981
+ description: "Set the active Novel's genre tag from the fixed catalog (noir, high_fantasy, sword_and_sorcery, sci_fi_horror, cosmic_horror, historical, western, modern, cyberpunk) (Game Master only). Use when: declaring the campaign's genre. Do NOT use when: setting the Novel description — use update_novel_description.",
3982
+ inputSchema: { genre: z.string().describe("The genre tag from the fixed catalog.") },
4073
3983
  }, async ({ genre }) => {
4074
3984
  requireGM();
4075
3985
  const novel = requireNovel();
@@ -4083,8 +3993,8 @@ server.registerTool("set_genre", {
4083
3993
  });
4084
3994
  server.registerTool("clone_novel", {
4085
3995
  title: "Clone Novel",
4086
- description: "Create an independent copy of a Novel. Game Master only.",
4087
- inputSchema: { source_slug: z.string(), new_name: z.string(), trim_audit_sessions: z.number().min(0).optional() },
3996
+ description: "Create an independent copy of a Novel, persisting the copy (Game Master only). Use when: branching the story or testing an alternative. Do NOT use when: renaming a Novel — use rename_novel.",
3997
+ inputSchema: { source_slug: z.string().describe("The Novel to copy."), new_name: z.string().describe("The name for the copy."), trim_audit_sessions: z.number().min(0).optional().describe("Optional number of recent audit sessions to keep.") },
4088
3998
  }, async ({ source_slug, new_name }) => {
4089
3999
  requireGM();
4090
4000
  const source = state.novels.get(source_slug);
@@ -4104,11 +4014,11 @@ server.registerTool("clone_novel", {
4104
4014
  // --- Entity Management ---
4105
4015
  server.registerTool("remove_entity", {
4106
4016
  title: "Remove Entity",
4107
- description: "Remove an entity from the active Novel. Game Master only.",
4017
+ description: "Remove an entity from the active Novel, persisting the removal (Game Master only). Use when: a character permanently leaves. Do NOT use when: removing an NPC — use remove_npc.",
4108
4018
  // REQ-176 — entity removal: clears the active-entity field when the removed
4109
4019
  // entity was active; party://current excludes removed entities; roster
4110
4020
  // baseline unaffected (re-import creates a fresh copy). Player → [FORBIDDEN].
4111
- inputSchema: { entity_id: z.string() },
4021
+ inputSchema: { entity_id: z.string().describe("The entity to remove.") },
4112
4022
  }, async ({ entity_id }) => {
4113
4023
  requireGM();
4114
4024
  const novel = requireNovel();
@@ -4122,11 +4032,11 @@ server.registerTool("remove_entity", {
4122
4032
  });
4123
4033
  server.registerTool("remove_roster_character", {
4124
4034
  title: "Remove Roster Character",
4125
- description: "Remove a character from the roster. Game Master only.",
4035
+ description: "Remove a character from the roster, persisting the removal (Game Master only). Use when: discarding a staged character. Do NOT use when: listing roster characters — use list_roster_characters.",
4126
4036
  // REQ-177 — roster entity removal: removes from roster:// only; Novel copies
4127
4037
  // survive independently; Player → [FORBIDDEN]; absent → [NOT_FOUND] with
4128
4038
  // valid roster IDs enumerated.
4129
- inputSchema: { roster_id: z.string() },
4039
+ inputSchema: { roster_id: z.string().describe("The roster character to remove.") },
4130
4040
  }, async ({ roster_id }) => {
4131
4041
  requireGM();
4132
4042
  if (!state.roster.has(roster_id))
@@ -4137,7 +4047,7 @@ server.registerTool("remove_roster_character", {
4137
4047
  });
4138
4048
  server.registerTool("list_roster_characters", {
4139
4049
  title: "List Roster Characters",
4140
- description: "List all characters in the roster.",
4050
+ description: "List all characters in the persistent roster. Use when: discovering staged characters to import. Do NOT use when: importing a roster character — use import_character.",
4141
4051
  // REQ-178 — roster listing: any-badge, structured (id/name), empty-state
4142
4052
  // marker when the roster is empty; novel_setup sources its list from here.
4143
4053
  inputSchema: {},
@@ -4150,7 +4060,7 @@ server.registerTool("list_roster_characters", {
4150
4060
  // --- Special tools ---
4151
4061
  server.registerTool("toggle_action_patterns", {
4152
4062
  title: "Toggle Action Patterns",
4153
- description: "Toggle enrich-derived action patterns on or off for the active Novel. Game Master only.",
4063
+ description: "Toggle enrich-derived action patterns on or off for the active Novel (Game Master only). Use when: enabling or disabling suggested actions. Do NOT use when: setting autonomy — use set_autonomy.",
4154
4064
  // REQ-115 — action pattern activation: flips the Novel-scoped boolean; when
4155
4065
  // enabled, synthesis patterns supplement suggest_actions; pure-resolution.
4156
4066
  inputSchema: {},
@@ -4163,13 +4073,8 @@ server.registerTool("toggle_action_patterns", {
4163
4073
  });
4164
4074
  server.registerTool("present_choices", {
4165
4075
  title: "Present Choices",
4166
- description: "Present structured choice prompts to the player. Resolved via respond. Game Master only.",
4167
- inputSchema: {
4168
- prompt: z.string(),
4169
- choices: z.array(z.object({ id: z.string(), label: z.string(), description: z.string().optional() })),
4170
- allow_freeform: z.boolean().optional(),
4171
- context: z.record(z.string(), z.any()).optional(),
4172
- },
4076
+ description: "Present structured choice prompts to the player, resolved later via respond (Game Master only). Use when: offering the player a decision. Do NOT use when: resolving an open decision — use respond.",
4077
+ inputSchema: { prompt: z.string().describe("The choice prompt."), choices: z.array(z.object({ id: z.string(), label: z.string(), description: z.string().optional() })).describe("The list of choices."), allow_freeform: z.boolean().optional().describe("When true, allow a free-form response."), context: z.record(z.string(), z.any()).optional().describe("Optional context for the choice.") },
4173
4078
  }, async ({ prompt, choices, allow_freeform, context }) => {
4174
4079
  requireGM();
4175
4080
  const novel = requireNovel();
@@ -4188,8 +4093,8 @@ server.registerTool("present_choices", {
4188
4093
  });
4189
4094
  server.registerTool("ask_oracle", {
4190
4095
  title: "Ask Oracle",
4191
- description: "Resolve uncertainty with a d100 roll against the Ask-the-Oracle ladder: almost_certain (≥11), likely (≥26), 50_50 (≥51), unlikely (≥76), small_chance (≥91). Defaults to 50_50 when likelihood is omitted. Callable by Player and Game Master.",
4192
- inputSchema: { question: z.string(), likelihood: z.enum(["almost_certain", "likely", "50_50", "unlikely", "small_chance"]).optional(), seed: z.string().optional() },
4096
+ description: "Resolve uncertainty with a deterministic d100 roll against the Ask-the-Oracle ladder (almost_certain, likely, 50_50, unlikely, small_chance), callable by Player and Game Master. Use when: the rules or fiction leave an outcome open. Do NOT use when: rolling on a fixed table — use roll_on_table.",
4097
+ inputSchema: { question: z.string().describe("The question to resolve."), likelihood: z.enum(["almost_certain", "likely", "50_50", "unlikely", "small_chance"]).optional().describe("almost_certain, likely, 50_50, unlikely, or small_chance."), seed: z.string().optional().describe("Optional deterministic seed.") },
4193
4098
  }, async ({ question, likelihood, seed }) => {
4194
4099
  requireNotObserver();
4195
4100
  const novel = requireNovel();
@@ -4329,18 +4234,18 @@ function normalizeSceneTypeState(raw) {
4329
4234
  // --- Guidance (GM) ---
4330
4235
  server.registerTool("set_verbosity", {
4331
4236
  title: "Set Output Verbosity",
4332
- description: "Set the session output verbosity mode: normal (full entries) or terse (minimum mechanical content). Callable by both badges.",
4237
+ description: "Set the session output verbosity to normal (full entries) or terse (minimum mechanical content), callable by both badges. Use when: the caller wants shorter or fuller replies. Do NOT use when: setting briefing section order — use set_briefing_order.",
4333
4238
  // REQ-253 — tool-output verbosity control: session-scoped mode, discarded on
4334
4239
  // connection close; reported in spec_health.
4335
- inputSchema: { mode: z.enum(["normal", "terse"]) },
4240
+ inputSchema: { mode: z.enum(["normal", "terse"]).describe("normal or terse.") },
4336
4241
  }, async ({ mode }) => {
4337
4242
  outputVerbosity = mode;
4338
4243
  return ok(`Output verbosity set to '${mode}'.`);
4339
4244
  });
4340
4245
  server.registerTool("set_briefing_order", {
4341
4246
  title: "Set Briefing Order",
4342
- description: "Reorder sections of badge_briefing. Game Master only.",
4343
- inputSchema: { sections: z.array(z.string()) },
4247
+ description: "Reorder sections of badge_briefing, persisting the order (Game Master only). Use when: customizing what the briefing emphasizes. Do NOT use when: setting verbosity — use set_verbosity.",
4248
+ inputSchema: { sections: z.array(z.string()).describe("The ordered list of briefing sections.") },
4344
4249
  }, async ({ sections }) => {
4345
4250
  requireGM();
4346
4251
  const novel = requireNovel();
@@ -4359,11 +4264,11 @@ server.registerTool("set_briefing_order", {
4359
4264
  });
4360
4265
  server.registerTool("compress_audit", {
4361
4266
  title: "Compress Audit Log",
4362
- description: "Return a Markdown prompt compressing recent audit entries. Callable by both badges.",
4267
+ description: "Return a Markdown prompt compressing recent audit entries, callable by both badges. Use when: the caller needs a compact history. Do NOT use when: permanently compacting the audit log — use compact_audit_log.",
4363
4268
  // REQ-086 — audit compression: header line + one line per entry in
4364
4269
  // `[timestamp] [badge] tool_name — output_prefix` (or [BOUNDARY_VIOLATION]);
4365
4270
  // pure-generation, badge-filtered; max_entries ≤ 0 → [INVALID_INPUT].
4366
- inputSchema: { max_entries: z.number().optional() },
4271
+ inputSchema: { max_entries: z.number().optional().describe("Optional maximum number of entries.") },
4367
4272
  }, async ({ max_entries }) => {
4368
4273
  const novel = requireNovel();
4369
4274
  const max = max_entries ?? 20;
@@ -4384,8 +4289,8 @@ server.registerTool("compress_audit", {
4384
4289
  // compact_audit_log: legacy alias for compress_audit (REQ-086).
4385
4290
  server.registerTool("compact_audit_log", {
4386
4291
  title: "Compact Audit Log",
4387
- description: "Alias for compress_audit.",
4388
- inputSchema: { max_entries: z.number().optional() },
4292
+ description: "Alias for compress_audit: permanently compacts the audit log. Use when: reducing stored audit size. Do NOT use when: generating a compression prompt — use compress_audit.",
4293
+ inputSchema: { max_entries: z.number().optional().describe("Optional maximum number of entries.") },
4389
4294
  }, async ({ max_entries }) => {
4390
4295
  const novel = requireNovel();
4391
4296
  const max = max_entries ?? 20;
@@ -4422,14 +4327,11 @@ function assessGenerationGuard(input) {
4422
4327
  // (default otherwise), or both. No Novel is required for the codex target.
4423
4328
  server.registerTool("generate_adventure", {
4424
4329
  title: "Generate Adventure",
4425
- description: "Generate an adventure scaffold from a premise. Game Master only.",
4330
+ description: "Generate an adventure scaffold from a premise, targeting the Novel, the codex, or both (Game Master only). Use when: the GM wants a new adventure outline. Do NOT use when: generating a single scene — use generate_encounter.",
4426
4331
  // REQ-132 — adventure generation lifecycle: transient Novel-scoped artifact
4427
4332
  // surfaced at adventure://generated/<anchor>, replaced on regeneration,
4428
4333
  // discarded by end_novel, never persisted to TTRPG_ADVENTURE.
4429
- inputSchema: {
4430
- premise: z.string(),
4431
- target: z.enum(["novel", "codex", "both"]).optional(),
4432
- },
4334
+ inputSchema: { premise: z.string().describe("The adventure premise."), target: z.enum(["novel", "codex", "both"]).optional().describe("novel, codex, or both.") },
4433
4335
  }, async ({ premise, target }) => {
4434
4336
  requireGM();
4435
4337
  const novel = state.activeNovel;
@@ -4525,8 +4427,8 @@ server.registerTool("generate_adventure", {
4525
4427
  // undo target. Player badge → [FORBIDDEN].
4526
4428
  server.registerTool("generate_encounter", {
4527
4429
  title: "Generate Encounter",
4528
- description: "Generate a scene + NPC + lore entry from context. Game Master only.",
4529
- inputSchema: { context: z.string() },
4430
+ description: "Generate a scene, NPC, and lore entry from context (Game Master only). Use when: the GM wants an encounter on demand. Do NOT use when: generating a full adventure — use generate_adventure.",
4431
+ inputSchema: { context: z.string().describe("The scene context to generate from.") },
4530
4432
  }, async ({ context }) => {
4531
4433
  requireGM();
4532
4434
  const novel = requireNovel();
@@ -4590,8 +4492,8 @@ server.registerTool("generate_encounter", {
4590
4492
  });
4591
4493
  server.registerTool("load_adventure", {
4592
4494
  title: "Load Adventure",
4593
- description: "Load an adventure module. Game Master only.",
4594
- inputSchema: { slug: z.string() },
4495
+ description: "Load an adventure module, persisting it as the active adventure (Game Master only). Use when: starting a prepared adventure. Do NOT use when: listing adventures — use list_adventures.",
4496
+ inputSchema: { slug: z.string().describe("The adventure module slug to load.") },
4595
4497
  }, async ({ slug }) => {
4596
4498
  requireGM();
4597
4499
  const novel = requireNovel();
@@ -4650,8 +4552,8 @@ server.registerTool("load_adventure", {
4650
4552
  // empty-state when no modules; badge-filtered.
4651
4553
  server.registerTool("list_adventures", {
4652
4554
  title: "List Adventures",
4653
- description: "List adventure modules with metadata. Always callable.",
4654
- inputSchema: { filter: z.string().optional() },
4555
+ description: "List adventure modules with metadata, always callable. Use when: discovering available adventures. Do NOT use when: loading an adventure — use load_adventure.",
4556
+ inputSchema: { filter: z.string().optional().describe("Optional filter.") },
4655
4557
  }, async ({ filter }) => {
4656
4558
  const adventureDir = process.env.TTRPG_ADVENTURE_DIR ?? path.join(__dirname, "..", "adventures");
4657
4559
  let files = [];
@@ -4741,7 +4643,7 @@ server.registerResource("adventure-navigation", new ResourceTemplate("adventure:
4741
4643
  // REQ-072 — session_recap summarizes recent session activity.
4742
4644
  server.registerTool("session_recap", {
4743
4645
  title: "Session Recap",
4744
- description: "Summarize recent session activity.",
4646
+ description: "Summarize recent session activity into a recap. Use when: reviewing what happened this session. Do NOT use when: reading the story journal in detail — use list_stories.",
4745
4647
  inputSchema: {},
4746
4648
  }, async () => {
4747
4649
  const novel = requireNovel();
@@ -4827,8 +4729,8 @@ server.registerTool("session_recap", {
4827
4729
  // --- Novel Lifecycle ---
4828
4730
  server.registerTool("create_novel", {
4829
4731
  title: "Create Novel",
4830
- description: "Create a named novel. Novel persists to disk.",
4831
- inputSchema: { name: z.string(), ruleset: z.string().optional(), genre: z.string().optional(), description: z.string().optional(), codex_adventure: z.string().optional() },
4732
+ description: "Create a named Novel that persists to disk. Use when: starting a new campaign. Do NOT use when: resuming an existing Novel — use resume_novel.",
4733
+ inputSchema: { name: z.string().describe("The Novel name."), ruleset: z.string().optional().describe("Optional ruleset slug."), genre: z.string().optional().describe("Optional genre."), description: z.string().optional().describe("Optional description."), codex_adventure: z.string().optional().describe("Optional codex adventure to seed from.") },
4832
4734
  }, async ({ name, ruleset, genre, description, codex_adventure }) => {
4833
4735
  requireNotObserver();
4834
4736
  if (ruleset && !rulesets.isInstalled(ruleset)) {
@@ -4873,8 +4775,8 @@ Next step: run the novel_setup guide to add characters, choose a story source, a
4873
4775
  });
4874
4776
  server.registerTool("resume_novel", {
4875
4777
  title: "Resume Novel",
4876
- description: "Resume a previously created novel from disk.",
4877
- inputSchema: { slug: z.string() },
4778
+ description: "Resume a previously created Novel from disk, making it the active Novel. Use when: continuing an existing campaign. Do NOT use when: creating a new Novel — use create_novel.",
4779
+ inputSchema: { slug: z.string().describe("The Novel slug to resume.") },
4878
4780
  }, async ({ slug }) => {
4879
4781
  // REQ-402 — resuming closes the prior session window; a window with zero
4880
4782
  // state writes is surfaced as [session-no-mutations].
@@ -4900,8 +4802,8 @@ server.registerTool("resume_novel", {
4900
4802
  });
4901
4803
  server.registerTool("switch_novel", {
4902
4804
  title: "Switch Novel",
4903
- description: "Switch the active novel for this connection. Always callable.",
4904
- inputSchema: { slug: z.string() },
4805
+ description: "Switch the active Novel for this connection, always callable. Use when: changing which save file the session works on. Do NOT use when: ending a Novel — use end_novel.",
4806
+ inputSchema: { slug: z.string().describe("The Novel slug to switch to.") },
4905
4807
  }, async ({ slug }) => {
4906
4808
  // REQ-403b — TTRPG_STATE_GATE=block refuses to leave a drifting Novel.
4907
4809
  const active = state.activeNovel;
@@ -4929,7 +4831,7 @@ server.registerTool("switch_novel", {
4929
4831
  });
4930
4832
  server.registerTool("end_novel", {
4931
4833
  title: "End Novel",
4932
- description: "End the current novel. Deactivates badge, removes save file.",
4834
+ description: "End the current Novel, deactivating the badge and removing the save file after a confirmation workflow. Use when: concluding a campaign. Do NOT use when: merely switching Novels — use switch_novel.",
4933
4835
  inputSchema: {},
4934
4836
  }, async () => {
4935
4837
  requireNotObserver();
@@ -4949,8 +4851,8 @@ Options: yes, cancel`);
4949
4851
  });
4950
4852
  server.registerTool("export_novel", {
4951
4853
  title: "Export Novel",
4952
- description: "Export the active novel in interchange format. Game Master only.",
4953
- inputSchema: { format: z.string().optional(), scope: z.string().optional() },
4854
+ description: "Export the active Novel in interchange format for backup or transfer (Game Master only). Use when: moving a Novel between servers. Do NOT use when: importing a Novel — use import_novel.",
4855
+ inputSchema: { format: z.string().optional().describe("Optional output format."), scope: z.string().optional().describe("Optional export scope.") },
4954
4856
  }, async ({ format: fmt, scope }) => {
4955
4857
  requireGM();
4956
4858
  // REQ-425b — interchange surfaces accept only json/markdown; html returns
@@ -5043,12 +4945,8 @@ server.registerTool("export_novel", {
5043
4945
  });
5044
4946
  server.registerTool("import_novel", {
5045
4947
  title: "Import Novel",
5046
- description: "Import a previously exported novel. Game Master only.",
5047
- inputSchema: {
5048
- data: z.string(),
5049
- mode: z.enum(["dry-run", "merge", "replace"]).optional(),
5050
- strict: z.boolean().optional(),
5051
- },
4948
+ description: "Import a previously exported Novel in dry-run, merge, or replace mode (Game Master only). Use when: loading a Novel from interchange format. Do NOT use when: exporting a Novel — use export_novel.",
4949
+ inputSchema: { data: z.string().describe("The exported Novel JSON."), mode: z.enum(["dry-run", "merge", "replace"]).optional().describe("dry-run, merge, or replace."), strict: z.boolean().optional().describe("When true, fail on any cross-reference mismatch.") },
5052
4950
  }, async ({ data, mode, strict }) => {
5053
4951
  requireGM();
5054
4952
  const m = mode ?? "dry-run";
@@ -5159,7 +5057,7 @@ server.registerTool("import_novel", {
5159
5057
  // --- Enrichment ---
5160
5058
  server.registerTool("revert_synthesis", {
5161
5059
  title: "Revert Synthesis",
5162
- description: "Remove all synthesis state, restoring pre-synthesis server state. Game Master only.",
5060
+ description: "Remove all synthesis state, restoring pre-synthesis server state (Game Master only). Use when: discarding generated synthesis content wholesale. Do NOT use when: deactivating a single item — use deactivate_synthesis_item.",
5163
5061
  inputSchema: {},
5164
5062
  }, async () => {
5165
5063
  requireGM();
@@ -5172,8 +5070,8 @@ server.registerTool("revert_synthesis", {
5172
5070
  // --- Anchor-only tools (ruleset-free, REQ-218) ---
5173
5071
  server.registerTool("search_rules", {
5174
5072
  title: "Search Rules",
5175
- description: "Search the active ruleset's index for matching terms. Empty when no ruleset is bound.",
5176
- inputSchema: { query: z.string(), max_results: z.number().optional() },
5073
+ description: "Search the active ruleset's index for matching terms, returning empty when no ruleset is bound. Use when: looking up a rule, item, or concept in the ruleset. Do NOT use when: the Novel is ruleset-free — use suggest_actions or spec_health.",
5074
+ inputSchema: { query: z.string().describe("The search query."), max_results: z.number().optional().describe("Optional maximum number of results.") },
5177
5075
  }, async ({ query, max_results }) => {
5178
5076
  const novel = state.activeNovel;
5179
5077
  const slug = novel?.ruleset ?? null;
@@ -5194,16 +5092,8 @@ server.registerTool("search_rules", {
5194
5092
  });
5195
5093
  server.registerTool("install_ruleset", {
5196
5094
  title: "Install Ruleset",
5197
- description: "Install a ruleset package from a files bundle. Game Master or Editor only.",
5198
- inputSchema: {
5199
- slug: z.string(),
5200
- manifest: z.any(),
5201
- index: z.any().optional(),
5202
- model: z.any().optional(),
5203
- tools: z.any().optional(),
5204
- resources: z.any().optional(),
5205
- prompts: z.any().optional(),
5206
- },
5095
+ description: "Install a ruleset package from a files bundle, persisting it to the install directory (Game Master or Editor only). Use when: adding a ruleset to the host. Do NOT use when: removing a ruleset — use remove_ruleset.",
5096
+ inputSchema: { slug: z.string().describe("The ruleset slug."), manifest: z.any().describe("The package manifest."), index: z.any().optional().describe("Optional search index."), model: z.any().optional().describe("Optional extraction model."), tools: z.any().optional().describe("Optional tool schemas."), resources: z.any().optional().describe("Optional resources."), prompts: z.any().optional().describe("Optional prompts.") },
5207
5097
  }, async (args) => {
5208
5098
  requireGM();
5209
5099
  try {
@@ -5223,8 +5113,8 @@ server.registerTool("install_ruleset", {
5223
5113
  });
5224
5114
  server.registerTool("remove_ruleset", {
5225
5115
  title: "Remove Ruleset",
5226
- description: "Remove an installed ruleset package. Game Master or Editor only.",
5227
- inputSchema: { slug: z.string() },
5116
+ description: "Remove an installed ruleset package, deregistering its tools, resources, and prompts (Game Master or Editor only). Use when: uninstalling a ruleset. Do NOT use when: listing rulesets — use list_rulesets.",
5117
+ inputSchema: { slug: z.string().describe("The ruleset slug to remove.") },
5228
5118
  }, async ({ slug }) => {
5229
5119
  requireGM();
5230
5120
  const novel = state.activeNovel;
@@ -5241,7 +5131,7 @@ server.registerTool("remove_ruleset", {
5241
5131
  });
5242
5132
  server.registerTool("list_rulesets", {
5243
5133
  title: "List Rulesets",
5244
- description: "List installed ruleset packages with loaded-versus-installed state.",
5134
+ description: "List installed ruleset packages with loaded-versus-installed state. Use when: reviewing which rulesets the host knows. Do NOT use when: installing a ruleset — use install_ruleset.",
5245
5135
  // REQ-391 — scoped tool listing: default surface is active Novel's ruleset
5246
5136
  // tools plus infrastructure; list_rulesets exposes per-package state without
5247
5137
  // forcing hydration of inactive packages (REQ-390 lazy hydration).
@@ -5258,8 +5148,8 @@ server.registerTool("list_rulesets", {
5258
5148
  });
5259
5149
  server.registerTool("bind_novel_ruleset", {
5260
5150
  title: "Bind Novel Ruleset",
5261
- description: "Bind the active ruleset-free Novel to an installed ruleset. Game Master or Editor only; one-way and audited.",
5262
- inputSchema: { slug: z.string() },
5151
+ description: "Bind the active ruleset-free Novel to an installed ruleset, a one-way, audited migration (Game Master or Editor only). Use when: adding mechanical rules to a freeform Novel. Do NOT use when: removing a ruleset — use remove_ruleset.",
5152
+ inputSchema: { slug: z.string().describe("The installed ruleset slug to bind.") },
5263
5153
  }, async ({ slug }) => {
5264
5154
  requireGM();
5265
5155
  if (!rulesets.isInstalled(slug)) {
@@ -5276,8 +5166,8 @@ server.registerTool("bind_novel_ruleset", {
5276
5166
  });
5277
5167
  server.registerTool("suggest_actions", {
5278
5168
  title: "Suggest Actions",
5279
- description: "Map player intent to world-model tool invocations. No mechanical suggestions in ruleset-free mode.",
5280
- inputSchema: { intent: z.string(), entity_id: z.string().optional() },
5169
+ description: "Map player intent to world-model tool invocations, omitting mechanical suggestions in ruleset-free mode. Use when: translating natural-language intent into concrete tool calls. Do NOT use when: resolving a spatial action directly — use command or resolve_intent.",
5170
+ inputSchema: { intent: z.string().describe("The player intent to map to tool calls."), entity_id: z.string().optional().describe("Optional entity context.") },
5281
5171
  }, async ({ intent, entity_id }) => {
5282
5172
  const novel = requireNovel();
5283
5173
  const entity = entity_id ? novel.entities.get(entity_id) : state.getActiveEntity();
@@ -5374,7 +5264,7 @@ const REQ022_URI_CATALOG = [
5374
5264
  // REQ-025 — spec_health reports build health, indexed counts, and URI completeness.
5375
5265
  server.registerTool("spec_health", {
5376
5266
  title: "Spec Health",
5377
- description: "Report build health, indexed counts, and resource URI completeness.",
5267
+ description: "Report build health, indexed counts, and resource URI completeness derived from live registrations. Use when: diagnosing server or ruleset state. Do NOT use when: searching ruleset content — use search_rules.",
5378
5268
  inputSchema: {},
5379
5269
  }, async () => {
5380
5270
  const novel = state.activeNovel;
@@ -6092,7 +5982,7 @@ server.registerResource("ui-novel", "ui://novel/current", { title: "Active Novel
6092
5982
  server.registerTool("set_party_presence", {
6093
5983
  title: "Set Party Presence",
6094
5984
  description: "Declare which entities are present in the current scene. Use when: the GM needs to override party presence without altering other scene fields. Do NOT use when: setting scene description — use set_scene_state.",
6095
- inputSchema: { entity_ids: z.array(z.string()), location: z.string().optional() },
5985
+ inputSchema: { entity_ids: z.array(z.string()).describe("The entities present in the current scene."), location: z.string().optional().describe("Optional location override.") },
6096
5986
  }, async ({ entity_ids, location }) => {
6097
5987
  requireGM();
6098
5988
  const novel = requireNovel();
@@ -6104,7 +5994,7 @@ server.registerTool("set_party_presence", {
6104
5994
  server.registerTool("roll_on_table", {
6105
5995
  title: "Roll On Table",
6106
5996
  description: "Roll on a generation table from the bound ruleset. Use when: resolving a random-generation table (names, treasure, events). Do NOT use when: resolving a fixed lookup — use a lookup tool.",
6107
- inputSchema: { table: z.string(), seed: z.string().optional() },
5997
+ inputSchema: { table: z.string().describe("The generation table to roll on."), seed: z.string().optional().describe("Optional deterministic seed.") },
6108
5998
  }, async ({ table, seed }) => {
6109
5999
  const novel = state.activeNovel;
6110
6000
  const slug = novel?.ruleset ?? null;
@@ -6138,14 +6028,7 @@ server.registerTool("roll_on_table", {
6138
6028
  server.registerTool("codex_set", {
6139
6029
  title: "Set Codex Entry",
6140
6030
  description: "Create or update a typed codex entry that persists across Novels. Use when: storing reusable content (NPCs, factions, rooms, spells, etc.) for later import. Do NOT use when: storing Novel-scoped content — use set_lore_entry or set_note.",
6141
- inputSchema: {
6142
- kind: z.string(),
6143
- name: z.string(),
6144
- content: z.any(),
6145
- description: z.string().optional(),
6146
- tags: z.array(z.string()).optional(),
6147
- visibility: z.enum(["library", "shared", "private"]).optional(),
6148
- },
6031
+ inputSchema: { kind: z.string().describe("The codex entry kind."), name: z.string().describe("The entry name."), content: z.any().describe("The entry content."), description: z.string().optional().describe("Optional description."), tags: z.array(z.string()).optional().describe("Optional tags."), visibility: z.enum(["library", "shared", "private"]).optional().describe("library, shared, or private.") },
6149
6032
  }, async ({ kind, name, content, description, tags, visibility }) => {
6150
6033
  requireGM();
6151
6034
  const id = `${kind.toLowerCase()}_${name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
@@ -6166,7 +6049,7 @@ server.registerTool("codex_set", {
6166
6049
  server.registerTool("codex_list", {
6167
6050
  title: "List Codex Entries",
6168
6051
  description: "List codex entries by kind, badge-filtered by visibility. Use when: discovering reusable content to import. Do NOT use when: listing Novel entities — use list_notes or list_stories.",
6169
- inputSchema: { kind: z.string().optional() },
6052
+ inputSchema: { kind: z.string().optional().describe("Optional kind filter.") },
6170
6053
  }, async ({ kind }) => {
6171
6054
  const badge = getBadge();
6172
6055
  let entries = [...state.codex.values()];
@@ -6180,7 +6063,7 @@ server.registerTool("codex_list", {
6180
6063
  server.registerTool("codex_capture", {
6181
6064
  title: "Capture to Codex",
6182
6065
  description: "Capture an entity's voice profile to the Codex. Use when: persisting a character's corrected voice across Novels. Do NOT use when: storing Novel-scoped lore — use set_lore_entry.",
6183
- inputSchema: { kind: z.enum(["voice_profile"]), entity_id: z.string(), update_source: z.boolean().optional() },
6066
+ inputSchema: { kind: z.enum(["voice_profile"]).describe("The codex kind (voice_profile)."), entity_id: z.string().describe("The entity whose voice to capture."), update_source: z.boolean().optional().describe("When true, update the source entity too.") },
6184
6067
  }, async ({ kind, entity_id, update_source }) => {
6185
6068
  requireGM();
6186
6069
  const novel = requireNovel();
@@ -6204,7 +6087,7 @@ server.registerTool("codex_capture", {
6204
6087
  server.registerTool("codex_import", {
6205
6088
  title: "Import from Codex",
6206
6089
  description: "Import a Codex entry into the active Novel. Use when: pulling reusable content (voice profiles, adventures) in. Do NOT use when: reading the Codex — use codex_list.",
6207
- inputSchema: { entry_id: z.string() },
6090
+ inputSchema: { entry_id: z.string().describe("The codex entry identifier to import.") },
6208
6091
  }, async ({ entry_id }) => {
6209
6092
  requireGM();
6210
6093
  const novel = requireNovel();
@@ -6252,8 +6135,8 @@ server.registerTool("codex_import", {
6252
6135
  const PLAYER_SYNTH_MODULES = ["voice_examples", "action_patterns", "supplementary_guidance", "narrative_voices", "lore_templates"];
6253
6136
  server.registerTool("player_synthesize", {
6254
6137
  title: "Player Synthesize",
6255
- description: "Create a player-authored synthesis item. Player badge only.",
6256
- inputSchema: { module: z.string(), key: z.string(), content: z.string(), triggers: z.array(z.string()).optional(), badge_scope: z.enum(["shared", "player"]).optional() },
6138
+ description: "Create a player-authored synthesis item (Player badge only), persisting it to the Novel. Use when: the player wants to add their own lore or voice content. Do NOT use when: the GM is synthesizing — use synthesize.",
6139
+ inputSchema: { module: z.string().describe("The synthesis module."), key: z.string().describe("The item key."), content: z.string().describe("The item content."), triggers: z.array(z.string()).optional().describe("Optional recall triggers."), badge_scope: z.enum(["shared", "player"]).optional().describe("shared or player.") },
6257
6140
  }, async ({ module, key, content, triggers, badge_scope }) => {
6258
6141
  requirePlayer();
6259
6142
  const novel = requireNovel();
@@ -6273,8 +6156,8 @@ server.registerTool("player_synthesize", {
6273
6156
  });
6274
6157
  server.registerTool("player_remove_synthesis", {
6275
6158
  title: "Player Remove Synthesis",
6276
- description: "Remove a player-authored synthesis item. Player badge only.",
6277
- inputSchema: { module: z.string(), key: z.string() },
6159
+ description: "Remove a player-authored synthesis item (Player badge only), persisting the removal. Use when: the player discards their own synthesis content. Do NOT use when: deactivating an item without deleting it — use deactivate_synthesis_item.",
6160
+ inputSchema: { module: z.string().describe("The synthesis module."), key: z.string().describe("The item key to remove.") },
6278
6161
  }, async ({ module, key }) => {
6279
6162
  requirePlayer();
6280
6163
  const novel = requireNovel();
@@ -6289,8 +6172,8 @@ server.registerTool("player_remove_synthesis", {
6289
6172
  });
6290
6173
  server.registerTool("player_list_synthesis", {
6291
6174
  title: "Player List Synthesis",
6292
- description: "List player-authored synthesis items. Player badge only.",
6293
- inputSchema: { module: z.string().optional() },
6175
+ description: "List player-authored synthesis items (Player badge only). Use when: reviewing the player's own synthesis content. Do NOT use when: listing all synthesis items — use list_synthesis_items.",
6176
+ inputSchema: { module: z.string().optional().describe("Optional module filter.") },
6294
6177
  }, async ({ module }) => {
6295
6178
  requirePlayer();
6296
6179
  const novel = requireNovel();
@@ -6307,7 +6190,7 @@ server.registerTool("synthesize", {
6307
6190
  // items with novel:// source URIs; fingerprint staleness + force bypass.
6308
6191
  // REQ-264 — confidence model: explicit-field items carry MEDIUM, inferred
6309
6192
  // items LOW, tagged [supplementary] [MEDIUM|LOW].
6310
- inputSchema: { force: z.boolean().optional() },
6193
+ inputSchema: { force: z.boolean().optional().describe("When true, re-run synthesis even if unchanged.") },
6311
6194
  }, async ({ force }) => {
6312
6195
  requireGM();
6313
6196
  const novel = requireNovel();
@@ -6323,7 +6206,7 @@ server.registerTool("synthesize", {
6323
6206
  server.registerTool("list_synthesis_items", {
6324
6207
  title: "List Synthesis Items",
6325
6208
  description: "List synthesis items by module and tier. Use when: reviewing available synthesis content. Do NOT use when: browsing the codex — use codex_list.",
6326
- inputSchema: { module: z.string().optional(), ...detailZod },
6209
+ inputSchema: { module: z.string().optional().describe("Optional module filter."), ...detailZod },
6327
6210
  }, async ({ module, detail }) => {
6328
6211
  const manifest = state.enrichmentManifest;
6329
6212
  if (!manifest)
@@ -6344,7 +6227,7 @@ server.registerTool("list_synthesis_items", {
6344
6227
  server.registerTool("activate_synthesis_item", {
6345
6228
  title: "Activate Synthesis Item",
6346
6229
  description: "Activate a synthesis item for the active Novel. Use when: incorporating synthesis content into play. Do NOT use when: deactivating — use deactivate_synthesis_item.",
6347
- inputSchema: { module: z.string(), key: z.number() },
6230
+ inputSchema: { module: z.string().describe("The synthesis module."), key: z.number().describe("The item key to activate.") },
6348
6231
  }, async ({ module, key }) => {
6349
6232
  requireGM();
6350
6233
  const novel = requireNovel();
@@ -6359,7 +6242,7 @@ server.registerTool("activate_synthesis_item", {
6359
6242
  server.registerTool("deactivate_synthesis_item", {
6360
6243
  title: "Deactivate Synthesis Item",
6361
6244
  description: "Deactivate a synthesis item for the active Novel. Use when: removing a synthesis item from play without deleting it. Do NOT use when: removing Ruleset Wisdom — use revert_synthesis.",
6362
- inputSchema: { module: z.string() },
6245
+ inputSchema: { module: z.string().describe("The synthesis module to deactivate.") },
6363
6246
  }, async ({ module }) => {
6364
6247
  requireGM();
6365
6248
  const novel = requireNovel();
@@ -6372,7 +6255,7 @@ server.registerTool("deactivate_synthesis_item", {
6372
6255
  server.registerTool("toggle_synthesis_module", {
6373
6256
  title: "Toggle Synthesis Module",
6374
6257
  description: "Enable or disable a synthesis module for the active Novel. Use when: controlling whether a module's content appears in surfaces. Do NOT use when: activating a single item — use activate_synthesis_item.",
6375
- inputSchema: { module: z.string(), enabled: z.boolean() },
6258
+ inputSchema: { module: z.string().describe("The synthesis module."), enabled: z.boolean().describe("Whether to enable or disable it.") },
6376
6259
  }, async ({ module, enabled }) => {
6377
6260
  requireGM();
6378
6261
  const novel = requireNovel();