holonovel 2026.8.30 → 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 +506 -395
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -53,8 +53,14 @@ state.buildFingerprint.lastSpecReview = new Date().toISOString();
|
|
|
53
53
|
// ── Server ─────────────────────────────────────────────────────────
|
|
54
54
|
const server = new McpServer({
|
|
55
55
|
name: "inform-holonovel",
|
|
56
|
-
version: "2026.
|
|
57
|
-
});
|
|
56
|
+
version: "2026.09.02",
|
|
57
|
+
});
|
|
58
|
+
// REQ-426c — MCP Apps capability negotiation: the server declares the
|
|
59
|
+
// `io.modelcontextprotocol/ui` extension in its capabilities; a client that
|
|
60
|
+
// does not negotiate the extension sees no `ui://` surface (see
|
|
61
|
+
// appsNegotiated() below). REQ-426a — UI resources are served under the
|
|
62
|
+
// `ui://` scheme as `text/html;profile=mcp-app`.
|
|
63
|
+
server.server.registerCapabilities({ extensions: { "io.modelcontextprotocol/ui": {} } });
|
|
58
64
|
// REQ-133 — forbidden-call audit: every tool handler is wrapped so that a
|
|
59
65
|
// thrown `[FORBIDDEN]` records the call (badge, tool name, arguments,
|
|
60
66
|
// violation_type: boundary) in the Novel audit log before propagating.
|
|
@@ -446,7 +452,10 @@ function terseOutput(tool, args, normal, terse) {
|
|
|
446
452
|
}
|
|
447
453
|
// REQ-409 — normalize the per-call detail request: absent → summary (lean
|
|
448
454
|
// default); explicit `true` → full entries; explicit `false` → summary.
|
|
449
|
-
|
|
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.") };
|
|
450
459
|
function wantsDetail(detail) {
|
|
451
460
|
return detail === true;
|
|
452
461
|
}
|
|
@@ -1100,6 +1109,121 @@ function formatNpcSheet(npc) {
|
|
|
1100
1109
|
s += `**Location:** ${npc.location}\n`;
|
|
1101
1110
|
return s;
|
|
1102
1111
|
}
|
|
1112
|
+
// ── Output Format Catalog (REQ-425) ────────────────────────────────
|
|
1113
|
+
//
|
|
1114
|
+
// REQ-425a — every user-requestable artifact surface accepts an optional
|
|
1115
|
+
// `format` selector drawn from this catalog; the default is `markdown`.
|
|
1116
|
+
// REQ-425b — an unsupported format returns `[INVALID_INPUT]` enumerating the
|
|
1117
|
+
// surface's supported set, derived at call time (REQ-059). REQ-425c — the
|
|
1118
|
+
// same artifact in the same format renders byte-identically across surfaces
|
|
1119
|
+
// (tools and resources share the same render functions). REQ-425d — ruleset
|
|
1120
|
+
// packages may declare additional formats via the registry below.
|
|
1121
|
+
const UNIVERSAL_FORMATS = ["markdown", "json", "html"];
|
|
1122
|
+
const STATBLOCK_FORMATS = ["markdown", "json", "html", "ascii"];
|
|
1123
|
+
const SESSION_FORMATS = ["markdown", "lonelog"];
|
|
1124
|
+
const INTERCHANGE_FORMATS = ["json", "markdown"];
|
|
1125
|
+
// Ruleset-declared formats (REQ-425d). Packages register additional format
|
|
1126
|
+
// identifiers here at load time; they are surfaced in spec_health and in the
|
|
1127
|
+
// `[INVALID_INPUT]` enumeration of every surface.
|
|
1128
|
+
const declaredFormats = new Set([]);
|
|
1129
|
+
function registerDeclaredFormat(name) { declaredFormats.add(name); }
|
|
1130
|
+
function supportedFormats(surface) {
|
|
1131
|
+
return [...new Set([...surface, ...declaredFormats])];
|
|
1132
|
+
}
|
|
1133
|
+
// REQ-425b — validate a requested format against a surface's supported set;
|
|
1134
|
+
// returns the normalized format or an `[INVALID_INPUT]` result.
|
|
1135
|
+
function resolveFormat(format, surface) {
|
|
1136
|
+
const fmt = format ?? "markdown";
|
|
1137
|
+
if (!supportedFormats(surface).includes(fmt)) {
|
|
1138
|
+
const list = supportedFormats(surface).join(", ");
|
|
1139
|
+
return err("INVALID_INPUT", `Unsupported format '${fmt}'. Supported formats: ${list}.`);
|
|
1140
|
+
}
|
|
1141
|
+
return fmt;
|
|
1142
|
+
}
|
|
1143
|
+
// REQ-425c / REQ-426a — a presentational HTML render of a Markdown artifact.
|
|
1144
|
+
// Self-contained (no external origins) per REQ-426d. Minimal Markdown→HTML:
|
|
1145
|
+
// headings, bold, italics, and line breaks; everything else is escaped.
|
|
1146
|
+
function htmlEscape(s) {
|
|
1147
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1148
|
+
}
|
|
1149
|
+
function toHtml(markdown) {
|
|
1150
|
+
const body = markdown
|
|
1151
|
+
.split("\n")
|
|
1152
|
+
.map((line) => {
|
|
1153
|
+
const heading = line.match(/^(#{1,3})\s+(.+)$/);
|
|
1154
|
+
if (heading) {
|
|
1155
|
+
const level = heading[1].length;
|
|
1156
|
+
const text = htmlEscape(heading[2].replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1"));
|
|
1157
|
+
return `<h${level}>${text}</h${level}>`;
|
|
1158
|
+
}
|
|
1159
|
+
let l = htmlEscape(line);
|
|
1160
|
+
l = l.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/\*(.+?)\*/g, "<i>$1</i>");
|
|
1161
|
+
return l;
|
|
1162
|
+
})
|
|
1163
|
+
.join("<br>\n");
|
|
1164
|
+
return `<!DOCTYPE html>\n<html><body><article>${body}</article></body></html>\n`;
|
|
1165
|
+
}
|
|
1166
|
+
// REQ-426d — UI resource CSP metadata: no external network origins.
|
|
1167
|
+
function uiResourceMeta() {
|
|
1168
|
+
return { csp: { connectDomains: [], resourceDomains: [], frameDomains: [] } };
|
|
1169
|
+
}
|
|
1170
|
+
// REQ-426c — a negotiating client declares `io.modelcontextprotocol/ui` in its
|
|
1171
|
+
// capabilities.extensions. Non-negotiating clients fall back to text surfaces.
|
|
1172
|
+
function appsNegotiated() {
|
|
1173
|
+
const ext = server.server.getClientCapabilities()?.extensions;
|
|
1174
|
+
return !!ext && "io.modelcontextprotocol/ui" in ext;
|
|
1175
|
+
}
|
|
1176
|
+
// REQ-425c — the structured (`json`) render of a stat block, shared by the
|
|
1177
|
+
// character_sheet tool and the entity/npc resources so both agree byte-for-byte.
|
|
1178
|
+
function entitySheetJson(entity) {
|
|
1179
|
+
return {
|
|
1180
|
+
id: entity.id ?? null,
|
|
1181
|
+
name: entity.name ?? null,
|
|
1182
|
+
stats: entity.stats ?? null,
|
|
1183
|
+
personality: entity.personality ?? {},
|
|
1184
|
+
inventory: entity.inventory ?? [],
|
|
1185
|
+
current_room: entity.current_room ?? null,
|
|
1186
|
+
conditions: entity.conditions ?? [],
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
// REQ-425c — a Markdown render of a codex entry, shared by the codex://
|
|
1190
|
+
// resource and its `ui://` HTML view.
|
|
1191
|
+
function codexEntryMarkdown(entry) {
|
|
1192
|
+
let md = `## ${entry.name}\n`;
|
|
1193
|
+
if (entry.kind)
|
|
1194
|
+
md += `**Kind:** ${entry.kind}\n`;
|
|
1195
|
+
if (entry.description)
|
|
1196
|
+
md += `*${entry.description}*\n`;
|
|
1197
|
+
if (entry.content)
|
|
1198
|
+
md += `\n${JSON.stringify(entry.content, null, 2)}\n`;
|
|
1199
|
+
return md;
|
|
1200
|
+
}
|
|
1201
|
+
// REQ-426a/c — build a `ui://` resource result. A negotiating client receives
|
|
1202
|
+
// `text/html;profile=mcp-app` with restrictive CSP metadata (REQ-426d); a
|
|
1203
|
+
// non-negotiating client receives a plain-text fallback (REQ-426c).
|
|
1204
|
+
function uiResourceResult(uri, html, negotiated) {
|
|
1205
|
+
if (!negotiated) {
|
|
1206
|
+
return { contents: [{ uri, text: `[STATE_CONFLICT] ui:// resources require the MCP Apps extension (io.modelcontextprotocol/ui).`, mimeType: "text/plain" }] };
|
|
1207
|
+
}
|
|
1208
|
+
return { contents: [{ uri, text: html, mimeType: "text/html;profile=mcp-app", _meta: { ui: uiResourceMeta() } }] };
|
|
1209
|
+
}
|
|
1210
|
+
// REQ-426b — attach `ui://` linkage metadata to a tool result when a client has
|
|
1211
|
+
// negotiated the MCP Apps extension; non-negotiating clients get the bare result.
|
|
1212
|
+
function withUiLinkage(result, resourceUri) {
|
|
1213
|
+
if (!appsNegotiated())
|
|
1214
|
+
return result;
|
|
1215
|
+
const content = (result.content ?? []).map((c) => ({ ...c, _meta: { ui: { resourceUri } } }));
|
|
1216
|
+
return { ...result, content };
|
|
1217
|
+
}
|
|
1218
|
+
// REQ-425a — read the `format` query parameter from a resource URI (default
|
|
1219
|
+
// markdown) and extract the resource id/key, excluding the query string.
|
|
1220
|
+
function resourceFormat(uri) {
|
|
1221
|
+
return uri.searchParams.get("format") ?? "markdown";
|
|
1222
|
+
}
|
|
1223
|
+
function resourceKey(uri) {
|
|
1224
|
+
const raw = uri.href.split("?")[0].split("/").filter(Boolean).pop() ?? "";
|
|
1225
|
+
return decodeURIComponent(raw);
|
|
1226
|
+
}
|
|
1103
1227
|
// ── Tools ──────────────────────────────────────────────────────────
|
|
1104
1228
|
// --- Badge & Workflow ---
|
|
1105
1229
|
function badgeLabel(badge) {
|
|
@@ -1112,8 +1236,8 @@ function badgeLabel(badge) {
|
|
|
1112
1236
|
// context; REQ-305 — observer mode: read-only spectator, AI plays both roles.
|
|
1113
1237
|
server.registerTool("set_badge", {
|
|
1114
1238
|
title: "Set Active Badge",
|
|
1115
|
-
description: "Switch active badge
|
|
1116
|
-
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).") },
|
|
1117
1241
|
}, async ({ badge }) => {
|
|
1118
1242
|
const novel = state.activeNovel;
|
|
1119
1243
|
if (novel) {
|
|
@@ -1137,8 +1261,8 @@ function canon(text) {
|
|
|
1137
1261
|
}
|
|
1138
1262
|
server.registerTool("respond", {
|
|
1139
1263
|
title: "Respond to Workflow Decision",
|
|
1140
|
-
description: "
|
|
1141
|
-
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.") },
|
|
1142
1266
|
}, async ({ decision, option }) => {
|
|
1143
1267
|
requireNotObserver();
|
|
1144
1268
|
const novel = requireNovel();
|
|
@@ -1372,7 +1496,7 @@ function kwMatch(canonicalDecision, keywords) {
|
|
|
1372
1496
|
}
|
|
1373
1497
|
server.registerTool("undo", {
|
|
1374
1498
|
title: "Undo",
|
|
1375
|
-
description: "Undo the most recent mutation
|
|
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.",
|
|
1376
1500
|
inputSchema: {},
|
|
1377
1501
|
}, async () => {
|
|
1378
1502
|
requireNotObserver();
|
|
@@ -1385,7 +1509,7 @@ server.registerTool("undo", {
|
|
|
1385
1509
|
});
|
|
1386
1510
|
server.registerTool("redo", {
|
|
1387
1511
|
title: "Redo",
|
|
1388
|
-
description: "
|
|
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.",
|
|
1389
1513
|
inputSchema: {},
|
|
1390
1514
|
}, async () => {
|
|
1391
1515
|
requireNotObserver();
|
|
@@ -1398,8 +1522,8 @@ server.registerTool("redo", {
|
|
|
1398
1522
|
});
|
|
1399
1523
|
server.registerTool("help", {
|
|
1400
1524
|
title: "Help and Tool Discovery",
|
|
1401
|
-
description: "Show available
|
|
1402
|
-
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.") },
|
|
1403
1527
|
}, async ({ query }) => {
|
|
1404
1528
|
// REQ-024 — tool documentation: tools carry a human title and descriptions
|
|
1405
1529
|
// using the ruleset's own terms; full descriptions remain at resources/read.
|
|
@@ -1476,8 +1600,8 @@ server.registerTool("help", {
|
|
|
1476
1600
|
});
|
|
1477
1601
|
server.registerTool("set_help_category", {
|
|
1478
1602
|
title: "Set Help Category Override",
|
|
1479
|
-
description: "Override the builder-assigned category
|
|
1480
|
-
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.") },
|
|
1481
1605
|
}, async ({ tool_name, category }) => {
|
|
1482
1606
|
requireGM();
|
|
1483
1607
|
const novel = requireNovel();
|
|
@@ -1567,30 +1691,10 @@ function buildCharacterStats(build, rules) {
|
|
|
1567
1691
|
}
|
|
1568
1692
|
server.registerTool("create_character", {
|
|
1569
1693
|
title: "Create Character",
|
|
1570
|
-
description: "Create a character
|
|
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.",
|
|
1571
1695
|
// REQ-408 — parameter ceiling (8): compact entry (name + identity + source)
|
|
1572
1696
|
// with mechanical and personality detail grouped into refinement objects.
|
|
1573
|
-
inputSchema: {
|
|
1574
|
-
name: z.string().optional(),
|
|
1575
|
-
species: z.string().optional(),
|
|
1576
|
-
classes: z.union([z.string(), z.array(z.object({ className: z.string(), levels: z.number().optional() }))]).optional(),
|
|
1577
|
-
stat_method: z.string().optional(),
|
|
1578
|
-
seed: z.string().optional(),
|
|
1579
|
-
stage_to_roster: z.boolean().optional(),
|
|
1580
|
-
personality: z.object({
|
|
1581
|
-
description: z.string().optional(),
|
|
1582
|
-
voice: z.string().optional(),
|
|
1583
|
-
background: z.string().optional(),
|
|
1584
|
-
goals: z.string().optional(),
|
|
1585
|
-
}).optional(),
|
|
1586
|
-
details: z.object({
|
|
1587
|
-
ability_scores: z.union([z.string(), z.array(z.number())]).optional(),
|
|
1588
|
-
skills: z.union([z.string(), z.array(z.string())]).optional(),
|
|
1589
|
-
feats: z.union([z.string(), z.array(z.string())]).optional(),
|
|
1590
|
-
talents: z.union([z.string(), z.array(z.string())]).optional(),
|
|
1591
|
-
equipment: z.union([z.string(), z.array(z.string())]).optional(),
|
|
1592
|
-
}).optional(),
|
|
1593
|
-
},
|
|
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.") },
|
|
1594
1698
|
}, async ({ name, species, classes, stat_method, seed, stage_to_roster, personality: personalityObj, details, description, voice, background, goals, ability_scores, skills, feats, talents, equipment }) => {
|
|
1595
1699
|
// Legacy-tolerant normalization: accept the grouped objects or their
|
|
1596
1700
|
// top-level spellings interchangeably.
|
|
@@ -1698,8 +1802,8 @@ ${stage_to_roster ? `Staged to roster as ${entity.id}.` : `Character '${name}' c
|
|
|
1698
1802
|
});
|
|
1699
1803
|
server.registerTool("stage_character", {
|
|
1700
1804
|
title: "Stage Character to Roster",
|
|
1701
|
-
description: "Stage an existing
|
|
1702
|
-
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.") },
|
|
1703
1807
|
}, async ({ entity_id }) => {
|
|
1704
1808
|
requireNotObserver();
|
|
1705
1809
|
const novel = requireNovel();
|
|
@@ -1712,8 +1816,8 @@ server.registerTool("stage_character", {
|
|
|
1712
1816
|
});
|
|
1713
1817
|
server.registerTool("import_character", {
|
|
1714
1818
|
title: "Import Character",
|
|
1715
|
-
description: "Import a roster character into the active
|
|
1716
|
-
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.") },
|
|
1717
1821
|
}, async ({ roster_id }) => {
|
|
1718
1822
|
requireNotObserver();
|
|
1719
1823
|
const novel = requireNovel();
|
|
@@ -1726,26 +1830,39 @@ server.registerTool("import_character", {
|
|
|
1726
1830
|
});
|
|
1727
1831
|
server.registerTool("character_sheet", {
|
|
1728
1832
|
title: "Character Sheet",
|
|
1729
|
-
description: "Render a character sheet for an entity
|
|
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.",
|
|
1730
1834
|
// REQ-120 — NPC rendering via the same sheet mechanism; REQ-124 — NPC damage
|
|
1731
1835
|
// resolution targets NPCs by identifier; REQ-129 — property group cardinality.
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
},
|
|
1836
|
+
// REQ-425a/b — the `format` selector is drawn from the output format catalog
|
|
1837
|
+
// (STATBLOCK_FORMATS) and validated at call time; REQ-426b — the result
|
|
1838
|
+
// carries `ui://` linkage metadata when the Apps extension is negotiated.
|
|
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.") },
|
|
1736
1840
|
}, async ({ entity_id, format }) => {
|
|
1737
1841
|
const entity = resolveEntityOrNpc(entity_id);
|
|
1738
1842
|
if (!entity)
|
|
1739
1843
|
return err("NOT_FOUND", `Entity or NPC '${entity_id || "none"}' not found. Corrective action: list entities with party://current or NPCs with npcs://.`);
|
|
1740
|
-
|
|
1741
|
-
|
|
1844
|
+
const fmt = resolveFormat(format, STATBLOCK_FORMATS);
|
|
1845
|
+
if (typeof fmt !== "string")
|
|
1846
|
+
return fmt;
|
|
1847
|
+
let result;
|
|
1848
|
+
if (fmt === "ascii") {
|
|
1849
|
+
result = raw(`[OK] ${entity.name} Room: ${entity.current_room || "(none)"} Held: ${entity.inventory?.length || 0}`);
|
|
1850
|
+
}
|
|
1851
|
+
else if (fmt === "json") {
|
|
1852
|
+
result = raw(JSON.stringify(entitySheetJson(entity), null, 2));
|
|
1742
1853
|
}
|
|
1743
|
-
|
|
1854
|
+
else if (fmt === "html") {
|
|
1855
|
+
result = raw(toHtml(fmtEntitySheet(entity)));
|
|
1856
|
+
}
|
|
1857
|
+
else {
|
|
1858
|
+
result = ok(fmtEntitySheet(entity));
|
|
1859
|
+
}
|
|
1860
|
+
return withUiLinkage(result, `ui://character-sheet/${entity.id}`);
|
|
1744
1861
|
});
|
|
1745
1862
|
server.registerTool("set_active_entity", {
|
|
1746
1863
|
title: "Set Active Entity",
|
|
1747
|
-
description: "Set the currently active entity.",
|
|
1748
|
-
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.") },
|
|
1749
1866
|
}, async ({ entity_id, pov }) => {
|
|
1750
1867
|
requireNotObserver();
|
|
1751
1868
|
const novel = requireNovel();
|
|
@@ -1759,19 +1876,13 @@ server.registerTool("set_active_entity", {
|
|
|
1759
1876
|
});
|
|
1760
1877
|
server.registerTool("set_personality", {
|
|
1761
1878
|
title: "Set Entity or NPC Personality",
|
|
1762
|
-
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.",
|
|
1763
1880
|
// REQ-127 — ruleset-native personality mapping (set_personality description
|
|
1764
1881
|
// references ruleset-native construct names when a ruleset defines them);
|
|
1765
1882
|
// REQ-165 — entity ownership gating (Player for own entities, GM for all);
|
|
1766
1883
|
// REQ-166 — personality briefing rendering (fields surfaced in badge_briefing
|
|
1767
1884
|
// alongside stats); REQ-122 — NPC narrative fields (NPC identifiers accepted).
|
|
1768
|
-
inputSchema: {
|
|
1769
|
-
entity_id: z.string(),
|
|
1770
|
-
description: z.string().optional(),
|
|
1771
|
-
voice: z.string().optional(),
|
|
1772
|
-
background: z.string().optional(),
|
|
1773
|
-
goals: z.string().optional(),
|
|
1774
|
-
},
|
|
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.") },
|
|
1775
1886
|
}, async ({ entity_id, description, voice, background, goals }) => {
|
|
1776
1887
|
requireNotObserver();
|
|
1777
1888
|
const novel = requireNovel();
|
|
@@ -1801,13 +1912,10 @@ server.registerTool("set_personality", {
|
|
|
1801
1912
|
});
|
|
1802
1913
|
server.registerTool("set_voice_examples", {
|
|
1803
1914
|
title: "Set Voice Examples",
|
|
1804
|
-
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.",
|
|
1805
1916
|
// REQ-126 — voice examples render ahead of trait descriptions in prompts
|
|
1806
1917
|
// (show-don't-tell); REQ-077f — the primary dialogue-consistency mechanism.
|
|
1807
|
-
inputSchema: {
|
|
1808
|
-
entity_id: z.string(),
|
|
1809
|
-
examples: z.array(z.object({ context: z.string(), dialogue: z.string(), tag: z.string().optional() })),
|
|
1810
|
-
},
|
|
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.") },
|
|
1811
1919
|
}, async ({ entity_id, examples }) => {
|
|
1812
1920
|
requireNotObserver();
|
|
1813
1921
|
const novel = requireNovel();
|
|
@@ -1822,11 +1930,8 @@ server.registerTool("set_voice_examples", {
|
|
|
1822
1930
|
// REQ-069 — player feedback signal to the GM.
|
|
1823
1931
|
server.registerTool("player_signal", {
|
|
1824
1932
|
title: "Player Signal",
|
|
1825
|
-
description: "Send a narrative signal from the
|
|
1826
|
-
inputSchema: {
|
|
1827
|
-
signal: z.enum(["pace", "difficulty", "tone", "focus", "boundary", "voice_feedback"]),
|
|
1828
|
-
value: z.string(),
|
|
1829
|
-
},
|
|
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.") },
|
|
1830
1935
|
}, async ({ signal, value }) => {
|
|
1831
1936
|
requirePlayer();
|
|
1832
1937
|
const novel = requireNovel();
|
|
@@ -1855,13 +1960,8 @@ server.registerTool("player_signal", {
|
|
|
1855
1960
|
// ── Autonomy (REQ-306) ────────────────────────────────────────────
|
|
1856
1961
|
server.registerTool("set_autonomy", {
|
|
1857
1962
|
title: "Adjustable Autonomy",
|
|
1858
|
-
description: "Set the AI autonomy sliders for the active Novel
|
|
1859
|
-
inputSchema: {
|
|
1860
|
-
level: z.enum(["full", "mechanical_prompt", "manual"]).optional(),
|
|
1861
|
-
confirmation: z.enum(["auto", "confirm", "prompt"]).optional(),
|
|
1862
|
-
safety: z.enum(["safe", "moderate", "hardcore"]).optional(),
|
|
1863
|
-
creativity: z.enum(["predictable", "standard", "chaotic"]).optional(),
|
|
1864
|
-
},
|
|
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.") },
|
|
1865
1965
|
}, async ({ level, confirmation, safety, creativity }) => {
|
|
1866
1966
|
requireGM();
|
|
1867
1967
|
const novel = requireNovel();
|
|
@@ -1990,8 +2090,8 @@ function recordExplorationKnowledge(novel, entity, type, name) {
|
|
|
1990
2090
|
}
|
|
1991
2091
|
server.registerTool("command", {
|
|
1992
2092
|
title: "Parser Command",
|
|
1993
|
-
description: "Execute a natural-language parser command against the world model
|
|
1994
|
-
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'.") },
|
|
1995
2095
|
}, async ({ command }) => {
|
|
1996
2096
|
const novel = requireNovel();
|
|
1997
2097
|
// REQ-197 — description mode commands are always recognized verbs.
|
|
@@ -2156,11 +2256,8 @@ function findMatchingThing(name, world, roomName) {
|
|
|
2156
2256
|
// --- World-Model CRUD (GM-only) ---
|
|
2157
2257
|
server.registerTool("create_room", {
|
|
2158
2258
|
title: "Create Room",
|
|
2159
|
-
description: "Create a new room in the world model
|
|
2160
|
-
inputSchema: {
|
|
2161
|
-
name: z.string(),
|
|
2162
|
-
description: z.string().optional(),
|
|
2163
|
-
},
|
|
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.") },
|
|
2164
2261
|
}, async ({ name, description }) => {
|
|
2165
2262
|
requireGM();
|
|
2166
2263
|
const novel = requireNovel();
|
|
@@ -2182,8 +2279,8 @@ server.registerTool("create_room", {
|
|
|
2182
2279
|
});
|
|
2183
2280
|
server.registerTool("remove_room", {
|
|
2184
2281
|
title: "Remove Room",
|
|
2185
|
-
description: "Remove a room
|
|
2186
|
-
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.") },
|
|
2187
2284
|
}, async ({ name }) => {
|
|
2188
2285
|
requireGM();
|
|
2189
2286
|
const novel = requireNovel();
|
|
@@ -2211,29 +2308,8 @@ server.registerTool("remove_room", {
|
|
|
2211
2308
|
});
|
|
2212
2309
|
server.registerTool("create_thing", {
|
|
2213
2310
|
title: "Create Thing",
|
|
2214
|
-
description: "Create a new thing in the world model
|
|
2215
|
-
inputSchema: {
|
|
2216
|
-
name: z.string(),
|
|
2217
|
-
kind: z.string().optional(),
|
|
2218
|
-
description: z.string().optional(),
|
|
2219
|
-
location: z.string().optional(),
|
|
2220
|
-
location_type: z.enum(["room", "container", "supporter"]).optional(),
|
|
2221
|
-
fixed: z.boolean().optional(),
|
|
2222
|
-
openable: z.boolean().optional(),
|
|
2223
|
-
lockable: z.boolean().optional(),
|
|
2224
|
-
locked: z.boolean().optional(),
|
|
2225
|
-
lit: z.boolean().optional(),
|
|
2226
|
-
switched_on: z.boolean().optional(),
|
|
2227
|
-
switchable: z.boolean().optional(),
|
|
2228
|
-
transparent: z.boolean().optional(),
|
|
2229
|
-
readable: z.boolean().optional(),
|
|
2230
|
-
read_text: z.string().optional(),
|
|
2231
|
-
wearable: z.boolean().optional(),
|
|
2232
|
-
edible: z.boolean().optional(),
|
|
2233
|
-
drinkable: z.boolean().optional(),
|
|
2234
|
-
enterable: z.boolean().optional(),
|
|
2235
|
-
climbable: z.boolean().optional(),
|
|
2236
|
-
},
|
|
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.") },
|
|
2237
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 }) => {
|
|
2238
2314
|
requireGM();
|
|
2239
2315
|
const novel = requireNovel();
|
|
@@ -2287,8 +2363,8 @@ server.registerTool("create_thing", {
|
|
|
2287
2363
|
});
|
|
2288
2364
|
server.registerTool("remove_thing", {
|
|
2289
2365
|
title: "Remove Thing",
|
|
2290
|
-
description: "Remove a thing from the world model
|
|
2291
|
-
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.") },
|
|
2292
2368
|
}, async ({ name }) => {
|
|
2293
2369
|
requireGM();
|
|
2294
2370
|
const novel = requireNovel();
|
|
@@ -2303,12 +2379,8 @@ server.registerTool("remove_thing", {
|
|
|
2303
2379
|
});
|
|
2304
2380
|
server.registerTool("create_exit", {
|
|
2305
2381
|
title: "Create Exit",
|
|
2306
|
-
description: "Create a directional exit between two rooms
|
|
2307
|
-
inputSchema: {
|
|
2308
|
-
direction: z.string(),
|
|
2309
|
-
room_a: z.string(),
|
|
2310
|
-
room_b: z.string(),
|
|
2311
|
-
},
|
|
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.") },
|
|
2312
2384
|
}, async ({ direction, room_a, room_b }) => {
|
|
2313
2385
|
requireGM();
|
|
2314
2386
|
const novel = requireNovel();
|
|
@@ -2330,11 +2402,8 @@ server.registerTool("create_exit", {
|
|
|
2330
2402
|
});
|
|
2331
2403
|
server.registerTool("remove_exit", {
|
|
2332
2404
|
title: "Remove Exit",
|
|
2333
|
-
description: "Remove a directional exit from a room
|
|
2334
|
-
inputSchema: {
|
|
2335
|
-
direction: z.string(),
|
|
2336
|
-
room: z.string(),
|
|
2337
|
-
},
|
|
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.") },
|
|
2338
2407
|
}, async ({ direction, room: roomName }) => {
|
|
2339
2408
|
requireGM();
|
|
2340
2409
|
const novel = requireNovel();
|
|
@@ -2354,8 +2423,8 @@ server.registerTool("remove_exit", {
|
|
|
2354
2423
|
});
|
|
2355
2424
|
server.registerTool("convert_source", {
|
|
2356
2425
|
title: "Convert Source",
|
|
2357
|
-
description: "Parse hybrid world-model assertions and populate the Novel's world model
|
|
2358
|
-
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.") },
|
|
2359
2428
|
}, async ({ source }) => {
|
|
2360
2429
|
requireGM();
|
|
2361
2430
|
const novel = requireNovel();
|
|
@@ -2471,7 +2540,7 @@ function composeRoomContext(room, novel, world) {
|
|
|
2471
2540
|
server.registerTool("resolve_intent", {
|
|
2472
2541
|
title: "Resolve Intent",
|
|
2473
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.",
|
|
2474
|
-
inputSchema: { intent: z.string() },
|
|
2543
|
+
inputSchema: { intent: z.string().describe("The natural-language spatial intent to resolve (e.g. 'go north', 'open the door').") },
|
|
2475
2544
|
}, async ({ intent }) => {
|
|
2476
2545
|
const badge = getBadge();
|
|
2477
2546
|
if (badge === "player") {
|
|
@@ -2485,12 +2554,8 @@ server.registerTool("resolve_intent", {
|
|
|
2485
2554
|
// --- Combat (GM, auto-advance in ruleset-free mode) ---
|
|
2486
2555
|
server.registerTool("init_combat", {
|
|
2487
2556
|
title: "Initiate Combat",
|
|
2488
|
-
description: "Start a combat encounter
|
|
2489
|
-
inputSchema: {
|
|
2490
|
-
participants: z.array(z.string()),
|
|
2491
|
-
dangers: z.array(z.object({ name: z.string(), ac: z.number().optional(), hp: z.number().optional(), initiative_bonus: z.number().optional() })).optional(),
|
|
2492
|
-
seed: z.string().optional(),
|
|
2493
|
-
},
|
|
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.") },
|
|
2494
2559
|
}, async ({ participants, dangers, seed }) => {
|
|
2495
2560
|
requireGM();
|
|
2496
2561
|
const novel = requireNovel();
|
|
@@ -2513,7 +2578,7 @@ server.registerTool("init_combat", {
|
|
|
2513
2578
|
});
|
|
2514
2579
|
server.registerTool("advance_combat", {
|
|
2515
2580
|
title: "Advance Combat",
|
|
2516
|
-
description: "Advance to the next turn
|
|
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.",
|
|
2517
2582
|
inputSchema: {},
|
|
2518
2583
|
}, async () => {
|
|
2519
2584
|
requireGM();
|
|
@@ -2533,8 +2598,8 @@ server.registerTool("advance_combat", {
|
|
|
2533
2598
|
});
|
|
2534
2599
|
server.registerTool("end_combat", {
|
|
2535
2600
|
title: "End Combat",
|
|
2536
|
-
description: "End the active combat encounter
|
|
2537
|
-
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.") },
|
|
2538
2603
|
}, async ({ outcome }) => {
|
|
2539
2604
|
requireGM();
|
|
2540
2605
|
const novel = requireNovel();
|
|
@@ -2546,8 +2611,8 @@ server.registerTool("end_combat", {
|
|
|
2546
2611
|
});
|
|
2547
2612
|
server.registerTool("add_combat_participant", {
|
|
2548
2613
|
title: "Add Combat Participant",
|
|
2549
|
-
description: "Add a participant to active combat
|
|
2550
|
-
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.") },
|
|
2551
2616
|
}, async ({ participant_id }) => {
|
|
2552
2617
|
requireGM();
|
|
2553
2618
|
const novel = requireNovel();
|
|
@@ -2558,8 +2623,8 @@ server.registerTool("add_combat_participant", {
|
|
|
2558
2623
|
});
|
|
2559
2624
|
server.registerTool("remove_combat_participant", {
|
|
2560
2625
|
title: "Remove Combat Participant",
|
|
2561
|
-
description: "Remove a participant from active combat
|
|
2562
|
-
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.") },
|
|
2563
2628
|
}, async ({ participant_id }) => {
|
|
2564
2629
|
requireGM();
|
|
2565
2630
|
const novel = requireNovel();
|
|
@@ -2709,28 +2774,28 @@ function advanceSceneTransitionCountdowns(novel) {
|
|
|
2709
2774
|
}
|
|
2710
2775
|
server.registerTool("set_scene_state", {
|
|
2711
2776
|
title: "Set Scene State",
|
|
2712
|
-
description: "Set the scene description and
|
|
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.",
|
|
2713
2778
|
inputSchema: {
|
|
2714
|
-
description: z.string(),
|
|
2715
|
-
location: z.string().optional(),
|
|
2716
|
-
time_of_day: z.string().optional(),
|
|
2717
|
-
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."),
|
|
2718
2783
|
// REQ-087 — scene type tagging: scene_type accepts a single tag or an array
|
|
2719
2784
|
// from the canonical catalog (social/exploration/neutral; combat is a
|
|
2720
2785
|
// resolution mode, added automatically on init_combat).
|
|
2721
|
-
scene_type: z.union([z.enum(["combat", "social", "exploration", "neutral"]), z.array(z.enum(["combat", "social", "exploration", "neutral"]))]).optional(),
|
|
2722
|
-
beat: z.enum(BEAT_VALUES).optional(),
|
|
2723
|
-
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."),
|
|
2724
2789
|
// REQ-250 — adventure scene waypoint: heading anchor from the adventure
|
|
2725
2790
|
// structural index; empty/null clears; unknown anchors → [NOT_FOUND].
|
|
2726
|
-
adventure_scene: z.string().nullable().optional(),
|
|
2791
|
+
adventure_scene: z.string().nullable().optional().describe("Optional adventure-scene waypoint anchor; empty or null clears it."),
|
|
2727
2792
|
// REQ-252 — narrative fast-forward: skip intervening time with a bridging
|
|
2728
2793
|
// summary, countdown adjustments, and NPC changes.
|
|
2729
2794
|
fast_forward: z.object({
|
|
2730
|
-
interval: z.string(),
|
|
2731
|
-
changes: z.array(z.object({ npc_id: z.string(), location: z.string().optional(), disposition: z.string().optional(), condition: z.string().optional() })).optional(),
|
|
2732
|
-
skip_countdowns: z.boolean().optional(),
|
|
2733
|
-
}).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."),
|
|
2734
2799
|
},
|
|
2735
2800
|
}, async ({ description, location, time_of_day, atmosphere, scene_type, beat, skip_transition_hook, adventure_scene, fast_forward }) => {
|
|
2736
2801
|
requireGM();
|
|
@@ -2861,8 +2926,8 @@ server.registerTool("set_scene_state", {
|
|
|
2861
2926
|
});
|
|
2862
2927
|
server.registerTool("set_narrative_directive", {
|
|
2863
2928
|
title: "Set Narrative Directive",
|
|
2864
|
-
description: "Set overarching narrative directive for the current scene
|
|
2865
|
-
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.") },
|
|
2866
2931
|
}, async ({ directive }) => {
|
|
2867
2932
|
requireGM();
|
|
2868
2933
|
const novel = requireNovel();
|
|
@@ -2874,15 +2939,15 @@ server.registerTool("set_narrative_directive", {
|
|
|
2874
2939
|
// --- NPCs (GM) ---
|
|
2875
2940
|
server.registerTool("create_npc", {
|
|
2876
2941
|
title: "Create NPC",
|
|
2877
|
-
description: "Create a named NPC with optional description and
|
|
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.",
|
|
2878
2943
|
inputSchema: {
|
|
2879
|
-
name: z.string(),
|
|
2880
|
-
description: z.string().optional(),
|
|
2881
|
-
disposition: z.string().optional(),
|
|
2882
|
-
location: z.string().optional(),
|
|
2883
|
-
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."),
|
|
2884
2949
|
// REQ-119 — optional ruleset stat-block reference.
|
|
2885
|
-
ruleset_reference: z.string().optional(),
|
|
2950
|
+
ruleset_reference: z.string().optional().describe("Optional ruleset stat-block reference."),
|
|
2886
2951
|
},
|
|
2887
2952
|
}, async ({ name, description, disposition, location, goals, ruleset_reference }) => {
|
|
2888
2953
|
requireGM();
|
|
@@ -2932,17 +2997,10 @@ server.registerTool("create_npc", {
|
|
|
2932
2997
|
});
|
|
2933
2998
|
server.registerTool("update_npc", {
|
|
2934
2999
|
title: "Update NPC",
|
|
2935
|
-
description: "Update an existing NPC's
|
|
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.",
|
|
2936
3001
|
// REQ-123 — builder-defined NPC stat fields: fields are builder-determined
|
|
2937
3002
|
// from the ruleset's stat conventions; every field optional except name.
|
|
2938
|
-
inputSchema: {
|
|
2939
|
-
npc_id: z.string(),
|
|
2940
|
-
name: z.string().optional(),
|
|
2941
|
-
description: z.string().optional(),
|
|
2942
|
-
disposition: z.string().optional(),
|
|
2943
|
-
location: z.string().optional(),
|
|
2944
|
-
goals: z.string().optional(),
|
|
2945
|
-
},
|
|
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.") },
|
|
2946
3004
|
}, async ({ npc_id, name, description, disposition, location, goals }) => {
|
|
2947
3005
|
requireGM();
|
|
2948
3006
|
const novel = requireNovel();
|
|
@@ -2966,8 +3024,8 @@ server.registerTool("update_npc", {
|
|
|
2966
3024
|
});
|
|
2967
3025
|
server.registerTool("remove_npc", {
|
|
2968
3026
|
title: "Remove NPC",
|
|
2969
|
-
description: "Remove an NPC from the
|
|
2970
|
-
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.") },
|
|
2971
3029
|
}, async ({ npc_id }) => {
|
|
2972
3030
|
requireGM();
|
|
2973
3031
|
const novel = requireNovel();
|
|
@@ -2982,26 +3040,26 @@ server.registerTool("remove_npc", {
|
|
|
2982
3040
|
// --- Countdowns (GM) ---
|
|
2983
3041
|
server.registerTool("set_countdown", {
|
|
2984
3042
|
title: "Set Countdown",
|
|
2985
|
-
description: "Set a countdown timer
|
|
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.",
|
|
2986
3044
|
inputSchema: {
|
|
2987
|
-
name: z.string(),
|
|
2988
|
-
ticks: z.number().min(1),
|
|
2989
|
-
type: z.enum(["round", "narrative"]).optional(),
|
|
2990
|
-
scope: z.string().optional(),
|
|
2991
|
-
direction: z.string().optional(),
|
|
2992
|
-
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."),
|
|
2993
3051
|
// REQ-329 — world-model coupling triggers (on_room_enter/on_thing_take/
|
|
2994
3052
|
// on_door_open). Any match advances one tick; supplements normal advancement.
|
|
2995
|
-
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)."),
|
|
2996
3054
|
// REQ-368 — world-model effect coupling applied when the countdown fires.
|
|
2997
3055
|
world_effect: z.object({
|
|
2998
|
-
type: z.enum(["describe", "property", "exit"]),
|
|
2999
|
-
target: z.string(),
|
|
3000
|
-
direction: z.string().optional(),
|
|
3001
|
-
destination: z.string().optional(),
|
|
3002
|
-
property: z.string().optional(),
|
|
3003
|
-
value: z.string().optional(),
|
|
3004
|
-
}).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."),
|
|
3005
3063
|
},
|
|
3006
3064
|
}, async ({ name, ticks, type, scope, direction, on_scene_transition, triggers, world_effect }) => {
|
|
3007
3065
|
requireGM();
|
|
@@ -3015,8 +3073,8 @@ server.registerTool("set_countdown", {
|
|
|
3015
3073
|
});
|
|
3016
3074
|
server.registerTool("advance_countdown", {
|
|
3017
3075
|
title: "Advance Countdown",
|
|
3018
|
-
description: "Advance a countdown timer by one tick
|
|
3019
|
-
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.") },
|
|
3020
3078
|
}, async ({ name }) => {
|
|
3021
3079
|
requireGM();
|
|
3022
3080
|
const novel = requireNovel();
|
|
@@ -3056,8 +3114,8 @@ server.registerTool("advance_countdown", {
|
|
|
3056
3114
|
});
|
|
3057
3115
|
server.registerTool("remove_countdown", {
|
|
3058
3116
|
title: "Remove Countdown",
|
|
3059
|
-
description: "Remove a countdown timer
|
|
3060
|
-
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.") },
|
|
3061
3119
|
}, async ({ name }) => {
|
|
3062
3120
|
requireGM();
|
|
3063
3121
|
const novel = requireNovel();
|
|
@@ -3072,17 +3130,17 @@ server.registerTool("remove_countdown", {
|
|
|
3072
3130
|
// --- Lore (GM) ---
|
|
3073
3131
|
server.registerTool("set_lore_entry", {
|
|
3074
3132
|
title: "Set Lore Entry",
|
|
3075
|
-
description: "
|
|
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.",
|
|
3076
3134
|
inputSchema: {
|
|
3077
|
-
key: z.string(),
|
|
3078
|
-
content: z.string(),
|
|
3079
|
-
triggers: z.array(z.string()).optional(),
|
|
3080
|
-
badge_scope: z.enum(["game_master", "shared"]).optional(),
|
|
3081
|
-
priority: z.number().optional(),
|
|
3082
|
-
sticky: z.number().optional(),
|
|
3083
|
-
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."),
|
|
3084
3142
|
// REQ-328 — lore-world coupling: world-model target (room/thing/exit ref).
|
|
3085
|
-
world_target: z.string().optional(),
|
|
3143
|
+
world_target: z.string().optional().describe("Optional world-model target reference."),
|
|
3086
3144
|
},
|
|
3087
3145
|
}, async ({ key, content, triggers, badge_scope, priority, sticky, group, world_target }) => {
|
|
3088
3146
|
requireGM();
|
|
@@ -3107,16 +3165,8 @@ server.registerTool("set_lore_entry", {
|
|
|
3107
3165
|
});
|
|
3108
3166
|
server.registerTool("update_lore_entry", {
|
|
3109
3167
|
title: "Update Lore Entry",
|
|
3110
|
-
description: "Update fields of an existing lore entry
|
|
3111
|
-
inputSchema: {
|
|
3112
|
-
key: z.string(),
|
|
3113
|
-
content: z.string().optional(),
|
|
3114
|
-
triggers: z.array(z.string()).optional(),
|
|
3115
|
-
badge_scope: z.enum(["game_master", "shared"]).optional(),
|
|
3116
|
-
priority: z.number().optional(),
|
|
3117
|
-
sticky: z.number().optional(),
|
|
3118
|
-
group: z.string().nullable().optional(),
|
|
3119
|
-
},
|
|
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.") },
|
|
3120
3170
|
}, async ({ key, content, triggers, badge_scope, priority, sticky, group }) => {
|
|
3121
3171
|
requireGM();
|
|
3122
3172
|
const novel = requireNovel();
|
|
@@ -3147,8 +3197,8 @@ server.registerTool("update_lore_entry", {
|
|
|
3147
3197
|
});
|
|
3148
3198
|
server.registerTool("remove_lore_entry", {
|
|
3149
3199
|
title: "Remove Lore Entry",
|
|
3150
|
-
description: "Remove a lore entry
|
|
3151
|
-
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.") },
|
|
3152
3202
|
}, async ({ key }) => {
|
|
3153
3203
|
requireGM();
|
|
3154
3204
|
const novel = requireNovel();
|
|
@@ -3161,8 +3211,8 @@ server.registerTool("remove_lore_entry", {
|
|
|
3161
3211
|
});
|
|
3162
3212
|
server.registerTool("toggle_lore_entry", {
|
|
3163
3213
|
title: "Toggle Lore Entry",
|
|
3164
|
-
description: "Enable or disable a lore entry
|
|
3165
|
-
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.") },
|
|
3166
3216
|
}, async ({ key }) => {
|
|
3167
3217
|
requireGM();
|
|
3168
3218
|
const novel = requireNovel();
|
|
@@ -3176,8 +3226,8 @@ server.registerTool("toggle_lore_entry", {
|
|
|
3176
3226
|
});
|
|
3177
3227
|
server.registerTool("set_lore_group", {
|
|
3178
3228
|
title: "Set Lore Group",
|
|
3179
|
-
description: "Assign or remove a lore entry from a named group
|
|
3180
|
-
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.") },
|
|
3181
3231
|
}, async ({ key, group }) => {
|
|
3182
3232
|
requireGM();
|
|
3183
3233
|
const novel = requireNovel();
|
|
@@ -3194,7 +3244,7 @@ server.registerTool("set_lore_group", {
|
|
|
3194
3244
|
});
|
|
3195
3245
|
server.registerTool("suggest_lore", {
|
|
3196
3246
|
title: "Suggest Lore",
|
|
3197
|
-
description: "Suggest lore entries from enrichment templates based on current scene
|
|
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.",
|
|
3198
3248
|
inputSchema: {},
|
|
3199
3249
|
}, async () => {
|
|
3200
3250
|
requireGM();
|
|
@@ -3207,12 +3257,17 @@ server.registerTool("suggest_lore", {
|
|
|
3207
3257
|
});
|
|
3208
3258
|
server.registerTool("export_lorebook", {
|
|
3209
3259
|
title: "Export Lorebook",
|
|
3210
|
-
description: "Export
|
|
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.",
|
|
3211
3261
|
// REQ-094 — lorebook interchange: lore-only export/import with merge, replace,
|
|
3212
3262
|
// and dry-run modes; round-trip preserves lore metadata (Appendix L).
|
|
3213
|
-
|
|
3263
|
+
// REQ-425b — interchange surfaces accept only json/markdown; html is a
|
|
3264
|
+
// presentation-only format and returns [INVALID_INPUT] enumerating the set.
|
|
3265
|
+
inputSchema: { format: z.string().optional().describe("Optional output format.") },
|
|
3214
3266
|
}, async ({ format: fmt }) => {
|
|
3215
3267
|
requireGM();
|
|
3268
|
+
if (fmt && !INTERCHANGE_FORMATS.includes(fmt)) {
|
|
3269
|
+
return err("INVALID_INPUT", `Unsupported format '${fmt}'. Supported formats: ${INTERCHANGE_FORMATS.join(", ")}.`);
|
|
3270
|
+
}
|
|
3216
3271
|
const novel = requireNovel();
|
|
3217
3272
|
const entries = [...novel.lore.values()];
|
|
3218
3273
|
if (fmt === "markdown") {
|
|
@@ -3230,11 +3285,8 @@ server.registerTool("export_lorebook", {
|
|
|
3230
3285
|
});
|
|
3231
3286
|
server.registerTool("import_lorebook", {
|
|
3232
3287
|
title: "Import Lorebook",
|
|
3233
|
-
description: "Import lore entries from JSON or Markdown
|
|
3234
|
-
inputSchema: {
|
|
3235
|
-
data: z.string(),
|
|
3236
|
-
mode: z.enum(["dry-run", "merge", "replace"]).optional(),
|
|
3237
|
-
},
|
|
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.") },
|
|
3238
3290
|
}, async ({ data, mode }) => {
|
|
3239
3291
|
requireGM();
|
|
3240
3292
|
const novel = requireNovel();
|
|
@@ -3280,8 +3332,8 @@ function conditionCatalogue(novel) {
|
|
|
3280
3332
|
}
|
|
3281
3333
|
server.registerTool("apply_condition", {
|
|
3282
3334
|
title: "Apply Condition",
|
|
3283
|
-
description: "Apply a condition to an entity.",
|
|
3284
|
-
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.") },
|
|
3285
3337
|
}, async ({ entity_id, condition, rounds }) => {
|
|
3286
3338
|
requireGM();
|
|
3287
3339
|
const novel = requireNovel();
|
|
@@ -3310,8 +3362,8 @@ server.registerTool("apply_condition", {
|
|
|
3310
3362
|
});
|
|
3311
3363
|
server.registerTool("remove_condition", {
|
|
3312
3364
|
title: "Remove Condition",
|
|
3313
|
-
description: "Remove a condition from an entity.",
|
|
3314
|
-
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.") },
|
|
3315
3367
|
}, async ({ entity_id, condition }) => {
|
|
3316
3368
|
requireGM();
|
|
3317
3369
|
const novel = requireNovel();
|
|
@@ -3331,8 +3383,8 @@ server.registerTool("remove_condition", {
|
|
|
3331
3383
|
// --- Factions (GM) ---
|
|
3332
3384
|
server.registerTool("create_faction", {
|
|
3333
3385
|
title: "Create Faction",
|
|
3334
|
-
description: "Create a named faction with goals, resources, and a progress clock
|
|
3335
|
-
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.") },
|
|
3336
3388
|
}, async ({ name, description, goals, resources, territory }) => {
|
|
3337
3389
|
requireGM();
|
|
3338
3390
|
const novel = requireNovel();
|
|
@@ -3350,8 +3402,8 @@ server.registerTool("create_faction", {
|
|
|
3350
3402
|
});
|
|
3351
3403
|
server.registerTool("update_faction", {
|
|
3352
3404
|
title: "Update Faction",
|
|
3353
|
-
description: "Update a faction's fields
|
|
3354
|
-
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.") },
|
|
3355
3407
|
}, async ({ faction_id, ...fields }) => {
|
|
3356
3408
|
requireGM();
|
|
3357
3409
|
const novel = requireNovel();
|
|
@@ -3364,8 +3416,8 @@ server.registerTool("update_faction", {
|
|
|
3364
3416
|
});
|
|
3365
3417
|
server.registerTool("remove_faction", {
|
|
3366
3418
|
title: "Remove Faction",
|
|
3367
|
-
description: "Remove a faction and its clock
|
|
3368
|
-
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.") },
|
|
3369
3421
|
}, async ({ faction_id }) => {
|
|
3370
3422
|
requireGM();
|
|
3371
3423
|
const novel = requireNovel();
|
|
@@ -3381,8 +3433,8 @@ server.registerTool("remove_faction", {
|
|
|
3381
3433
|
// --- Secrets (GM) ---
|
|
3382
3434
|
server.registerTool("set_secret", {
|
|
3383
3435
|
title: "Set Secret",
|
|
3384
|
-
description: "Create a secret lore entry
|
|
3385
|
-
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.") },
|
|
3386
3438
|
}, async ({ key, content, triggers, badge_scope, world_target }) => {
|
|
3387
3439
|
requireGM();
|
|
3388
3440
|
const novel = requireNovel();
|
|
@@ -3394,8 +3446,8 @@ server.registerTool("set_secret", {
|
|
|
3394
3446
|
});
|
|
3395
3447
|
server.registerTool("reveal_secret", {
|
|
3396
3448
|
title: "Reveal Secret",
|
|
3397
|
-
description: "
|
|
3398
|
-
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.") },
|
|
3399
3451
|
}, async ({ key, entity_id }) => {
|
|
3400
3452
|
requireGM();
|
|
3401
3453
|
const novel = requireNovel();
|
|
@@ -3411,8 +3463,8 @@ server.registerTool("reveal_secret", {
|
|
|
3411
3463
|
});
|
|
3412
3464
|
server.registerTool("get_knowledge", {
|
|
3413
3465
|
title: "Get Knowledge",
|
|
3414
|
-
description: "Return
|
|
3415
|
-
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.") },
|
|
3416
3468
|
}, async ({ entity_id, key }) => {
|
|
3417
3469
|
requireGM();
|
|
3418
3470
|
const novel = requireNovel();
|
|
@@ -3428,8 +3480,8 @@ server.registerTool("get_knowledge", {
|
|
|
3428
3480
|
// --- Relationships (GM) ---
|
|
3429
3481
|
server.registerTool("set_relationship", {
|
|
3430
3482
|
title: "Set Relationship",
|
|
3431
|
-
description: "Set a directed relationship between entities, NPCs, or factions.
|
|
3432
|
-
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.") },
|
|
3433
3485
|
}, async ({ entity_a, entity_b, type, value, description }) => {
|
|
3434
3486
|
requireGM();
|
|
3435
3487
|
const novel = requireNovel();
|
|
@@ -3439,8 +3491,8 @@ server.registerTool("set_relationship", {
|
|
|
3439
3491
|
});
|
|
3440
3492
|
server.registerTool("get_relationships", {
|
|
3441
3493
|
title: "Get Relationships",
|
|
3442
|
-
description: "Return all
|
|
3443
|
-
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.") },
|
|
3444
3496
|
}, async ({ entity_id }) => {
|
|
3445
3497
|
requireGM();
|
|
3446
3498
|
const novel = requireNovel();
|
|
@@ -3451,8 +3503,8 @@ server.registerTool("get_relationships", {
|
|
|
3451
3503
|
// --- Vows (GM) ---
|
|
3452
3504
|
server.registerTool("set_vow", {
|
|
3453
3505
|
title: "Set Vow",
|
|
3454
|
-
description: "Track a narrative vow, quest, or obligation
|
|
3455
|
-
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.") },
|
|
3456
3508
|
}, async ({ name, description, parties, difficulty, scope }) => {
|
|
3457
3509
|
requireGM();
|
|
3458
3510
|
const novel = requireNovel();
|
|
@@ -3479,8 +3531,8 @@ server.registerTool("set_vow", {
|
|
|
3479
3531
|
});
|
|
3480
3532
|
server.registerTool("mark_milestone", {
|
|
3481
3533
|
title: "Mark Milestone",
|
|
3482
|
-
description: "Advance a vow's progress by one milestone
|
|
3483
|
-
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.") },
|
|
3484
3536
|
}, async ({ vow_name }) => {
|
|
3485
3537
|
requireGM();
|
|
3486
3538
|
const novel = requireNovel();
|
|
@@ -3504,8 +3556,8 @@ server.registerTool("mark_milestone", {
|
|
|
3504
3556
|
});
|
|
3505
3557
|
server.registerTool("resolve_vow", {
|
|
3506
3558
|
title: "Resolve Vow",
|
|
3507
|
-
description: "Close a completed vow with outcome and consequences
|
|
3508
|
-
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.") },
|
|
3509
3561
|
}, async ({ vow_name, outcome, consequences }) => {
|
|
3510
3562
|
requireGM();
|
|
3511
3563
|
const novel = requireNovel();
|
|
@@ -3520,8 +3572,8 @@ server.registerTool("resolve_vow", {
|
|
|
3520
3572
|
});
|
|
3521
3573
|
server.registerTool("forsake_vow", {
|
|
3522
3574
|
title: "Forsake Vow",
|
|
3523
|
-
description: "Abandon a vow with a reason
|
|
3524
|
-
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.") },
|
|
3525
3577
|
}, async ({ vow_name, reason }) => {
|
|
3526
3578
|
requireGM();
|
|
3527
3579
|
const novel = requireNovel();
|
|
@@ -3538,8 +3590,8 @@ server.registerTool("forsake_vow", {
|
|
|
3538
3590
|
// immutable → [RULE_VIOLATION]; existing key → [STATE_CONFLICT]).
|
|
3539
3591
|
server.registerTool("promote_story_to_lore", {
|
|
3540
3592
|
title: "Promote Story to Lore",
|
|
3541
|
-
description: "Promote a story journal entry into a lore entry
|
|
3542
|
-
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.") },
|
|
3543
3595
|
}, async ({ index, key }) => {
|
|
3544
3596
|
requireGM();
|
|
3545
3597
|
const novel = requireNovel();
|
|
@@ -3568,8 +3620,8 @@ server.registerTool("promote_story_to_lore", {
|
|
|
3568
3620
|
// --- Story Journal (GM) ---
|
|
3569
3621
|
server.registerTool("record_story", {
|
|
3570
3622
|
title: "Record Story",
|
|
3571
|
-
description: "Record a narrative memory
|
|
3572
|
-
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.") },
|
|
3573
3625
|
}, async ({ type, entry }) => {
|
|
3574
3626
|
requireGM();
|
|
3575
3627
|
const novel = requireNovel();
|
|
@@ -3593,8 +3645,8 @@ server.registerTool("record_story", {
|
|
|
3593
3645
|
});
|
|
3594
3646
|
server.registerTool("update_story", {
|
|
3595
3647
|
title: "Update Story",
|
|
3596
|
-
description: "Edit a story journal entry by index
|
|
3597
|
-
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.") },
|
|
3598
3650
|
}, async ({ index, type, entry }) => {
|
|
3599
3651
|
requireGM();
|
|
3600
3652
|
const novel = requireNovel();
|
|
@@ -3612,8 +3664,8 @@ server.registerTool("update_story", {
|
|
|
3612
3664
|
});
|
|
3613
3665
|
server.registerTool("remove_story", {
|
|
3614
3666
|
title: "Remove Story",
|
|
3615
|
-
description: "Delete a story journal entry by index
|
|
3616
|
-
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.") },
|
|
3617
3669
|
}, async ({ index }) => {
|
|
3618
3670
|
requireGM();
|
|
3619
3671
|
const novel = requireNovel();
|
|
@@ -3625,8 +3677,8 @@ server.registerTool("remove_story", {
|
|
|
3625
3677
|
});
|
|
3626
3678
|
server.registerTool("list_stories", {
|
|
3627
3679
|
title: "List Stories",
|
|
3628
|
-
description: "List story journal entries with optional type filter and pagination
|
|
3629
|
-
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 },
|
|
3630
3682
|
}, async ({ filter, offset, limit, detail }) => {
|
|
3631
3683
|
requireGM();
|
|
3632
3684
|
const novel = requireNovel();
|
|
@@ -3644,8 +3696,8 @@ server.registerTool("list_stories", {
|
|
|
3644
3696
|
// --- Notes ---
|
|
3645
3697
|
server.registerTool("set_note", {
|
|
3646
3698
|
title: "Set Note",
|
|
3647
|
-
description: "Create or update a key-value note
|
|
3648
|
-
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.") },
|
|
3649
3701
|
}, async ({ key, content, badge_scope }) => {
|
|
3650
3702
|
requireNotObserver();
|
|
3651
3703
|
const novel = requireNovel();
|
|
@@ -3671,8 +3723,8 @@ server.registerTool("set_note", {
|
|
|
3671
3723
|
});
|
|
3672
3724
|
server.registerTool("remove_note", {
|
|
3673
3725
|
title: "Remove Note",
|
|
3674
|
-
description: "Remove a note by key
|
|
3675
|
-
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.") },
|
|
3676
3728
|
}, async ({ key }) => {
|
|
3677
3729
|
requireNotObserver();
|
|
3678
3730
|
const novel = requireNovel();
|
|
@@ -3689,7 +3741,7 @@ server.registerTool("remove_note", {
|
|
|
3689
3741
|
});
|
|
3690
3742
|
server.registerTool("list_notes", {
|
|
3691
3743
|
title: "List Notes",
|
|
3692
|
-
description: "List all notes
|
|
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.",
|
|
3693
3745
|
inputSchema: {},
|
|
3694
3746
|
}, async () => {
|
|
3695
3747
|
const novel = requireNovel();
|
|
@@ -3701,8 +3753,8 @@ server.registerTool("list_notes", {
|
|
|
3701
3753
|
// --- Server Notes (GM) ---
|
|
3702
3754
|
server.registerTool("set_server_note", {
|
|
3703
3755
|
title: "Set Server Note",
|
|
3704
|
-
description: "Create or update a server-level note
|
|
3705
|
-
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.") },
|
|
3706
3758
|
}, async ({ key, content, narrative_tag }) => {
|
|
3707
3759
|
requireGM();
|
|
3708
3760
|
state.serverNotes.set(key, { content, narrative_tag });
|
|
@@ -3711,8 +3763,8 @@ server.registerTool("set_server_note", {
|
|
|
3711
3763
|
});
|
|
3712
3764
|
server.registerTool("remove_server_note", {
|
|
3713
3765
|
title: "Remove Server Note",
|
|
3714
|
-
description: "Remove a server-level note
|
|
3715
|
-
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.") },
|
|
3716
3768
|
}, async ({ key }) => {
|
|
3717
3769
|
requireGM();
|
|
3718
3770
|
if (!state.serverNotes.has(key))
|
|
@@ -3723,7 +3775,7 @@ server.registerTool("remove_server_note", {
|
|
|
3723
3775
|
});
|
|
3724
3776
|
server.registerTool("list_server_notes", {
|
|
3725
3777
|
title: "List Server Notes",
|
|
3726
|
-
description: "List all server-level notes
|
|
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.",
|
|
3727
3779
|
inputSchema: {},
|
|
3728
3780
|
}, async () => {
|
|
3729
3781
|
requireGM();
|
|
@@ -3733,8 +3785,8 @@ server.registerTool("list_server_notes", {
|
|
|
3733
3785
|
// --- Pause/Resume (GM) ---
|
|
3734
3786
|
server.registerTool("set_pause_context", {
|
|
3735
3787
|
title: "Set Pause Context",
|
|
3736
|
-
description: "Save GM context for session resumption
|
|
3737
|
-
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.") },
|
|
3738
3790
|
}, async (fields) => {
|
|
3739
3791
|
requireGM();
|
|
3740
3792
|
const novel = requireNovel();
|
|
@@ -3757,7 +3809,7 @@ server.registerTool("set_pause_context", {
|
|
|
3757
3809
|
});
|
|
3758
3810
|
server.registerTool("get_pause_context", {
|
|
3759
3811
|
title: "Get Pause Context",
|
|
3760
|
-
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.",
|
|
3761
3813
|
inputSchema: {},
|
|
3762
3814
|
}, async () => {
|
|
3763
3815
|
const novel = requireNovel();
|
|
@@ -3773,8 +3825,8 @@ server.registerTool("get_pause_context", {
|
|
|
3773
3825
|
// --- Checkpoints (GM) ---
|
|
3774
3826
|
server.registerTool("set_checkpoint", {
|
|
3775
3827
|
title: "Set Checkpoint",
|
|
3776
|
-
description: "Save a named checkpoint of the full Novel state
|
|
3777
|
-
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.") },
|
|
3778
3830
|
}, async ({ label }) => {
|
|
3779
3831
|
requireGM();
|
|
3780
3832
|
const novel = requireNovel();
|
|
@@ -3784,7 +3836,7 @@ server.registerTool("set_checkpoint", {
|
|
|
3784
3836
|
});
|
|
3785
3837
|
server.registerTool("list_checkpoints", {
|
|
3786
3838
|
title: "List Checkpoints",
|
|
3787
|
-
description: "List all checkpoints for the active Novel
|
|
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.",
|
|
3788
3840
|
inputSchema: {},
|
|
3789
3841
|
}, async () => {
|
|
3790
3842
|
requireGM();
|
|
@@ -3793,8 +3845,8 @@ server.registerTool("list_checkpoints", {
|
|
|
3793
3845
|
});
|
|
3794
3846
|
server.registerTool("restore_checkpoint", {
|
|
3795
3847
|
title: "Restore Checkpoint",
|
|
3796
|
-
description: "Restore a checkpoint
|
|
3797
|
-
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.") },
|
|
3798
3850
|
}, async ({ label }) => {
|
|
3799
3851
|
requireGM();
|
|
3800
3852
|
const novel = requireNovel();
|
|
@@ -3815,8 +3867,8 @@ Options: yes, cancel`);
|
|
|
3815
3867
|
});
|
|
3816
3868
|
server.registerTool("remove_checkpoint", {
|
|
3817
3869
|
title: "Remove Checkpoint",
|
|
3818
|
-
description: "Remove a named checkpoint
|
|
3819
|
-
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.") },
|
|
3820
3872
|
}, async ({ label }) => {
|
|
3821
3873
|
requireGM();
|
|
3822
3874
|
const novel = requireNovel();
|
|
@@ -3830,8 +3882,8 @@ server.registerTool("remove_checkpoint", {
|
|
|
3830
3882
|
// --- Novel Lifecycle additions (GM) ---
|
|
3831
3883
|
server.registerTool("rename_novel", {
|
|
3832
3884
|
title: "Rename Novel",
|
|
3833
|
-
description: "Rename the active Novel on disk
|
|
3834
|
-
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.") },
|
|
3835
3887
|
}, async ({ new_slug }) => {
|
|
3836
3888
|
requireGM();
|
|
3837
3889
|
const novel = requireNovel();
|
|
@@ -3851,8 +3903,8 @@ server.registerTool("rename_novel", {
|
|
|
3851
3903
|
// description, surfaced in novel_info and badge_briefing.
|
|
3852
3904
|
server.registerTool("update_novel_description", {
|
|
3853
3905
|
title: "Update Novel Description",
|
|
3854
|
-
description: "Set or replace the active Novel's description
|
|
3855
|
-
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.") },
|
|
3856
3908
|
}, async ({ description }) => {
|
|
3857
3909
|
requireGM();
|
|
3858
3910
|
const novel = requireNovel();
|
|
@@ -3863,8 +3915,8 @@ server.registerTool("update_novel_description", {
|
|
|
3863
3915
|
});
|
|
3864
3916
|
server.registerTool("list_novels", {
|
|
3865
3917
|
title: "List Novels",
|
|
3866
|
-
description: "List all Novels on disk with metadata
|
|
3867
|
-
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.") },
|
|
3868
3920
|
}, async ({ detail, filter }) => {
|
|
3869
3921
|
const arch = state.archivedNovels();
|
|
3870
3922
|
const archivedSlugs = new Set(arch.map((a) => a.slug));
|
|
@@ -3883,8 +3935,8 @@ server.registerTool("list_novels", {
|
|
|
3883
3935
|
// REQ-334 — archive/unarchive Novel (Game Master only).
|
|
3884
3936
|
server.registerTool("archive_novel", {
|
|
3885
3937
|
title: "Archive Novel",
|
|
3886
|
-
description: "Move a Novel to the long-term
|
|
3887
|
-
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.") },
|
|
3888
3940
|
}, async ({ slug }) => {
|
|
3889
3941
|
requireGM();
|
|
3890
3942
|
if (state.activeNovelId === slug)
|
|
@@ -3895,8 +3947,8 @@ server.registerTool("archive_novel", {
|
|
|
3895
3947
|
});
|
|
3896
3948
|
server.registerTool("unarchive_novel", {
|
|
3897
3949
|
title: "Unarchive Novel",
|
|
3898
|
-
description: "Restore an archived Novel to active status
|
|
3899
|
-
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.") },
|
|
3900
3952
|
}, async ({ slug }) => {
|
|
3901
3953
|
requireGM();
|
|
3902
3954
|
const novel = state.unarchiveNovel(slug);
|
|
@@ -3905,8 +3957,8 @@ server.registerTool("unarchive_novel", {
|
|
|
3905
3957
|
});
|
|
3906
3958
|
server.registerTool("novel_info", {
|
|
3907
3959
|
title: "Novel Info",
|
|
3908
|
-
description: "Return extended metadata for a Novel
|
|
3909
|
-
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.") },
|
|
3910
3962
|
}, async ({ slug }) => {
|
|
3911
3963
|
const novel = slug ? state.novels.get(slug) : state.activeNovel;
|
|
3912
3964
|
if (!novel)
|
|
@@ -3926,8 +3978,8 @@ server.registerTool("novel_info", {
|
|
|
3926
3978
|
const GENRE_CATALOG = ["noir", "high_fantasy", "sword_and_sorcery", "sci_fi_horror", "cosmic_horror", "historical", "western", "modern", "cyberpunk"];
|
|
3927
3979
|
server.registerTool("set_genre", {
|
|
3928
3980
|
title: "Set Genre",
|
|
3929
|
-
description: "Set the active Novel's genre tag
|
|
3930
|
-
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.") },
|
|
3931
3983
|
}, async ({ genre }) => {
|
|
3932
3984
|
requireGM();
|
|
3933
3985
|
const novel = requireNovel();
|
|
@@ -3941,8 +3993,8 @@ server.registerTool("set_genre", {
|
|
|
3941
3993
|
});
|
|
3942
3994
|
server.registerTool("clone_novel", {
|
|
3943
3995
|
title: "Clone Novel",
|
|
3944
|
-
description: "Create an independent copy of a Novel
|
|
3945
|
-
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.") },
|
|
3946
3998
|
}, async ({ source_slug, new_name }) => {
|
|
3947
3999
|
requireGM();
|
|
3948
4000
|
const source = state.novels.get(source_slug);
|
|
@@ -3962,11 +4014,11 @@ server.registerTool("clone_novel", {
|
|
|
3962
4014
|
// --- Entity Management ---
|
|
3963
4015
|
server.registerTool("remove_entity", {
|
|
3964
4016
|
title: "Remove Entity",
|
|
3965
|
-
description: "Remove an entity from the active Novel
|
|
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.",
|
|
3966
4018
|
// REQ-176 — entity removal: clears the active-entity field when the removed
|
|
3967
4019
|
// entity was active; party://current excludes removed entities; roster
|
|
3968
4020
|
// baseline unaffected (re-import creates a fresh copy). Player → [FORBIDDEN].
|
|
3969
|
-
inputSchema: { entity_id: z.string() },
|
|
4021
|
+
inputSchema: { entity_id: z.string().describe("The entity to remove.") },
|
|
3970
4022
|
}, async ({ entity_id }) => {
|
|
3971
4023
|
requireGM();
|
|
3972
4024
|
const novel = requireNovel();
|
|
@@ -3980,11 +4032,11 @@ server.registerTool("remove_entity", {
|
|
|
3980
4032
|
});
|
|
3981
4033
|
server.registerTool("remove_roster_character", {
|
|
3982
4034
|
title: "Remove Roster Character",
|
|
3983
|
-
description: "Remove a character from the roster
|
|
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.",
|
|
3984
4036
|
// REQ-177 — roster entity removal: removes from roster:// only; Novel copies
|
|
3985
4037
|
// survive independently; Player → [FORBIDDEN]; absent → [NOT_FOUND] with
|
|
3986
4038
|
// valid roster IDs enumerated.
|
|
3987
|
-
inputSchema: { roster_id: z.string() },
|
|
4039
|
+
inputSchema: { roster_id: z.string().describe("The roster character to remove.") },
|
|
3988
4040
|
}, async ({ roster_id }) => {
|
|
3989
4041
|
requireGM();
|
|
3990
4042
|
if (!state.roster.has(roster_id))
|
|
@@ -3995,7 +4047,7 @@ server.registerTool("remove_roster_character", {
|
|
|
3995
4047
|
});
|
|
3996
4048
|
server.registerTool("list_roster_characters", {
|
|
3997
4049
|
title: "List Roster Characters",
|
|
3998
|
-
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.",
|
|
3999
4051
|
// REQ-178 — roster listing: any-badge, structured (id/name), empty-state
|
|
4000
4052
|
// marker when the roster is empty; novel_setup sources its list from here.
|
|
4001
4053
|
inputSchema: {},
|
|
@@ -4008,7 +4060,7 @@ server.registerTool("list_roster_characters", {
|
|
|
4008
4060
|
// --- Special tools ---
|
|
4009
4061
|
server.registerTool("toggle_action_patterns", {
|
|
4010
4062
|
title: "Toggle Action Patterns",
|
|
4011
|
-
description: "Toggle enrich-derived action patterns on or off for the active Novel
|
|
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.",
|
|
4012
4064
|
// REQ-115 — action pattern activation: flips the Novel-scoped boolean; when
|
|
4013
4065
|
// enabled, synthesis patterns supplement suggest_actions; pure-resolution.
|
|
4014
4066
|
inputSchema: {},
|
|
@@ -4021,13 +4073,8 @@ server.registerTool("toggle_action_patterns", {
|
|
|
4021
4073
|
});
|
|
4022
4074
|
server.registerTool("present_choices", {
|
|
4023
4075
|
title: "Present Choices",
|
|
4024
|
-
description: "Present structured choice prompts to the player
|
|
4025
|
-
inputSchema: {
|
|
4026
|
-
prompt: z.string(),
|
|
4027
|
-
choices: z.array(z.object({ id: z.string(), label: z.string(), description: z.string().optional() })),
|
|
4028
|
-
allow_freeform: z.boolean().optional(),
|
|
4029
|
-
context: z.record(z.string(), z.any()).optional(),
|
|
4030
|
-
},
|
|
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.") },
|
|
4031
4078
|
}, async ({ prompt, choices, allow_freeform, context }) => {
|
|
4032
4079
|
requireGM();
|
|
4033
4080
|
const novel = requireNovel();
|
|
@@ -4046,8 +4093,8 @@ server.registerTool("present_choices", {
|
|
|
4046
4093
|
});
|
|
4047
4094
|
server.registerTool("ask_oracle", {
|
|
4048
4095
|
title: "Ask Oracle",
|
|
4049
|
-
description: "Resolve uncertainty with a d100 roll against the Ask-the-Oracle ladder
|
|
4050
|
-
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.") },
|
|
4051
4098
|
}, async ({ question, likelihood, seed }) => {
|
|
4052
4099
|
requireNotObserver();
|
|
4053
4100
|
const novel = requireNovel();
|
|
@@ -4187,18 +4234,18 @@ function normalizeSceneTypeState(raw) {
|
|
|
4187
4234
|
// --- Guidance (GM) ---
|
|
4188
4235
|
server.registerTool("set_verbosity", {
|
|
4189
4236
|
title: "Set Output Verbosity",
|
|
4190
|
-
description: "Set the session output verbosity
|
|
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.",
|
|
4191
4238
|
// REQ-253 — tool-output verbosity control: session-scoped mode, discarded on
|
|
4192
4239
|
// connection close; reported in spec_health.
|
|
4193
|
-
inputSchema: { mode: z.enum(["normal", "terse"]) },
|
|
4240
|
+
inputSchema: { mode: z.enum(["normal", "terse"]).describe("normal or terse.") },
|
|
4194
4241
|
}, async ({ mode }) => {
|
|
4195
4242
|
outputVerbosity = mode;
|
|
4196
4243
|
return ok(`Output verbosity set to '${mode}'.`);
|
|
4197
4244
|
});
|
|
4198
4245
|
server.registerTool("set_briefing_order", {
|
|
4199
4246
|
title: "Set Briefing Order",
|
|
4200
|
-
description: "Reorder sections of badge_briefing
|
|
4201
|
-
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.") },
|
|
4202
4249
|
}, async ({ sections }) => {
|
|
4203
4250
|
requireGM();
|
|
4204
4251
|
const novel = requireNovel();
|
|
@@ -4217,11 +4264,11 @@ server.registerTool("set_briefing_order", {
|
|
|
4217
4264
|
});
|
|
4218
4265
|
server.registerTool("compress_audit", {
|
|
4219
4266
|
title: "Compress Audit Log",
|
|
4220
|
-
description: "Return a Markdown prompt compressing recent audit entries
|
|
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.",
|
|
4221
4268
|
// REQ-086 — audit compression: header line + one line per entry in
|
|
4222
4269
|
// `[timestamp] [badge] tool_name — output_prefix` (or [BOUNDARY_VIOLATION]);
|
|
4223
4270
|
// pure-generation, badge-filtered; max_entries ≤ 0 → [INVALID_INPUT].
|
|
4224
|
-
inputSchema: { max_entries: z.number().optional() },
|
|
4271
|
+
inputSchema: { max_entries: z.number().optional().describe("Optional maximum number of entries.") },
|
|
4225
4272
|
}, async ({ max_entries }) => {
|
|
4226
4273
|
const novel = requireNovel();
|
|
4227
4274
|
const max = max_entries ?? 20;
|
|
@@ -4242,8 +4289,8 @@ server.registerTool("compress_audit", {
|
|
|
4242
4289
|
// compact_audit_log: legacy alias for compress_audit (REQ-086).
|
|
4243
4290
|
server.registerTool("compact_audit_log", {
|
|
4244
4291
|
title: "Compact Audit Log",
|
|
4245
|
-
description: "Alias for compress_audit.",
|
|
4246
|
-
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.") },
|
|
4247
4294
|
}, async ({ max_entries }) => {
|
|
4248
4295
|
const novel = requireNovel();
|
|
4249
4296
|
const max = max_entries ?? 20;
|
|
@@ -4280,14 +4327,11 @@ function assessGenerationGuard(input) {
|
|
|
4280
4327
|
// (default otherwise), or both. No Novel is required for the codex target.
|
|
4281
4328
|
server.registerTool("generate_adventure", {
|
|
4282
4329
|
title: "Generate Adventure",
|
|
4283
|
-
description: "Generate an adventure scaffold from a premise
|
|
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.",
|
|
4284
4331
|
// REQ-132 — adventure generation lifecycle: transient Novel-scoped artifact
|
|
4285
4332
|
// surfaced at adventure://generated/<anchor>, replaced on regeneration,
|
|
4286
4333
|
// discarded by end_novel, never persisted to TTRPG_ADVENTURE.
|
|
4287
|
-
inputSchema: {
|
|
4288
|
-
premise: z.string(),
|
|
4289
|
-
target: z.enum(["novel", "codex", "both"]).optional(),
|
|
4290
|
-
},
|
|
4334
|
+
inputSchema: { premise: z.string().describe("The adventure premise."), target: z.enum(["novel", "codex", "both"]).optional().describe("novel, codex, or both.") },
|
|
4291
4335
|
}, async ({ premise, target }) => {
|
|
4292
4336
|
requireGM();
|
|
4293
4337
|
const novel = state.activeNovel;
|
|
@@ -4383,8 +4427,8 @@ server.registerTool("generate_adventure", {
|
|
|
4383
4427
|
// undo target. Player badge → [FORBIDDEN].
|
|
4384
4428
|
server.registerTool("generate_encounter", {
|
|
4385
4429
|
title: "Generate Encounter",
|
|
4386
|
-
description: "Generate a scene
|
|
4387
|
-
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.") },
|
|
4388
4432
|
}, async ({ context }) => {
|
|
4389
4433
|
requireGM();
|
|
4390
4434
|
const novel = requireNovel();
|
|
@@ -4448,8 +4492,8 @@ server.registerTool("generate_encounter", {
|
|
|
4448
4492
|
});
|
|
4449
4493
|
server.registerTool("load_adventure", {
|
|
4450
4494
|
title: "Load Adventure",
|
|
4451
|
-
description: "Load an adventure module
|
|
4452
|
-
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.") },
|
|
4453
4497
|
}, async ({ slug }) => {
|
|
4454
4498
|
requireGM();
|
|
4455
4499
|
const novel = requireNovel();
|
|
@@ -4508,8 +4552,8 @@ server.registerTool("load_adventure", {
|
|
|
4508
4552
|
// empty-state when no modules; badge-filtered.
|
|
4509
4553
|
server.registerTool("list_adventures", {
|
|
4510
4554
|
title: "List Adventures",
|
|
4511
|
-
description: "List adventure modules with metadata
|
|
4512
|
-
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.") },
|
|
4513
4557
|
}, async ({ filter }) => {
|
|
4514
4558
|
const adventureDir = process.env.TTRPG_ADVENTURE_DIR ?? path.join(__dirname, "..", "adventures");
|
|
4515
4559
|
let files = [];
|
|
@@ -4599,7 +4643,7 @@ server.registerResource("adventure-navigation", new ResourceTemplate("adventure:
|
|
|
4599
4643
|
// REQ-072 — session_recap summarizes recent session activity.
|
|
4600
4644
|
server.registerTool("session_recap", {
|
|
4601
4645
|
title: "Session Recap",
|
|
4602
|
-
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.",
|
|
4603
4647
|
inputSchema: {},
|
|
4604
4648
|
}, async () => {
|
|
4605
4649
|
const novel = requireNovel();
|
|
@@ -4685,8 +4729,8 @@ server.registerTool("session_recap", {
|
|
|
4685
4729
|
// --- Novel Lifecycle ---
|
|
4686
4730
|
server.registerTool("create_novel", {
|
|
4687
4731
|
title: "Create Novel",
|
|
4688
|
-
description: "Create a named
|
|
4689
|
-
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.") },
|
|
4690
4734
|
}, async ({ name, ruleset, genre, description, codex_adventure }) => {
|
|
4691
4735
|
requireNotObserver();
|
|
4692
4736
|
if (ruleset && !rulesets.isInstalled(ruleset)) {
|
|
@@ -4731,8 +4775,8 @@ Next step: run the novel_setup guide to add characters, choose a story source, a
|
|
|
4731
4775
|
});
|
|
4732
4776
|
server.registerTool("resume_novel", {
|
|
4733
4777
|
title: "Resume Novel",
|
|
4734
|
-
description: "Resume a previously created
|
|
4735
|
-
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.") },
|
|
4736
4780
|
}, async ({ slug }) => {
|
|
4737
4781
|
// REQ-402 — resuming closes the prior session window; a window with zero
|
|
4738
4782
|
// state writes is surfaced as [session-no-mutations].
|
|
@@ -4758,8 +4802,8 @@ server.registerTool("resume_novel", {
|
|
|
4758
4802
|
});
|
|
4759
4803
|
server.registerTool("switch_novel", {
|
|
4760
4804
|
title: "Switch Novel",
|
|
4761
|
-
description: "Switch the active
|
|
4762
|
-
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.") },
|
|
4763
4807
|
}, async ({ slug }) => {
|
|
4764
4808
|
// REQ-403b — TTRPG_STATE_GATE=block refuses to leave a drifting Novel.
|
|
4765
4809
|
const active = state.activeNovel;
|
|
@@ -4787,7 +4831,7 @@ server.registerTool("switch_novel", {
|
|
|
4787
4831
|
});
|
|
4788
4832
|
server.registerTool("end_novel", {
|
|
4789
4833
|
title: "End Novel",
|
|
4790
|
-
description: "End the current
|
|
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.",
|
|
4791
4835
|
inputSchema: {},
|
|
4792
4836
|
}, async () => {
|
|
4793
4837
|
requireNotObserver();
|
|
@@ -4807,10 +4851,15 @@ Options: yes, cancel`);
|
|
|
4807
4851
|
});
|
|
4808
4852
|
server.registerTool("export_novel", {
|
|
4809
4853
|
title: "Export Novel",
|
|
4810
|
-
description: "Export the active
|
|
4811
|
-
inputSchema: { format: z.
|
|
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.") },
|
|
4812
4856
|
}, async ({ format: fmt, scope }) => {
|
|
4813
4857
|
requireGM();
|
|
4858
|
+
// REQ-425b — interchange surfaces accept only json/markdown; html returns
|
|
4859
|
+
// [INVALID_INPUT] enumerating the set (presentation-only, not round-trippable).
|
|
4860
|
+
if (fmt && !INTERCHANGE_FORMATS.includes(fmt)) {
|
|
4861
|
+
return err("INVALID_INPUT", `Unsupported format '${fmt}'. Supported formats: ${INTERCHANGE_FORMATS.join(", ")}.`);
|
|
4862
|
+
}
|
|
4814
4863
|
const novel = requireNovel();
|
|
4815
4864
|
if (fmt === "markdown") {
|
|
4816
4865
|
let md = `# ${novel.name}\n\n`;
|
|
@@ -4896,12 +4945,8 @@ server.registerTool("export_novel", {
|
|
|
4896
4945
|
});
|
|
4897
4946
|
server.registerTool("import_novel", {
|
|
4898
4947
|
title: "Import Novel",
|
|
4899
|
-
description: "Import a previously exported
|
|
4900
|
-
inputSchema: {
|
|
4901
|
-
data: z.string(),
|
|
4902
|
-
mode: z.enum(["dry-run", "merge", "replace"]).optional(),
|
|
4903
|
-
strict: z.boolean().optional(),
|
|
4904
|
-
},
|
|
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.") },
|
|
4905
4950
|
}, async ({ data, mode, strict }) => {
|
|
4906
4951
|
requireGM();
|
|
4907
4952
|
const m = mode ?? "dry-run";
|
|
@@ -5012,7 +5057,7 @@ server.registerTool("import_novel", {
|
|
|
5012
5057
|
// --- Enrichment ---
|
|
5013
5058
|
server.registerTool("revert_synthesis", {
|
|
5014
5059
|
title: "Revert Synthesis",
|
|
5015
|
-
description: "Remove all synthesis state, restoring pre-synthesis server state
|
|
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.",
|
|
5016
5061
|
inputSchema: {},
|
|
5017
5062
|
}, async () => {
|
|
5018
5063
|
requireGM();
|
|
@@ -5025,8 +5070,8 @@ server.registerTool("revert_synthesis", {
|
|
|
5025
5070
|
// --- Anchor-only tools (ruleset-free, REQ-218) ---
|
|
5026
5071
|
server.registerTool("search_rules", {
|
|
5027
5072
|
title: "Search Rules",
|
|
5028
|
-
description: "Search the active ruleset's index for matching terms
|
|
5029
|
-
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.") },
|
|
5030
5075
|
}, async ({ query, max_results }) => {
|
|
5031
5076
|
const novel = state.activeNovel;
|
|
5032
5077
|
const slug = novel?.ruleset ?? null;
|
|
@@ -5047,16 +5092,8 @@ server.registerTool("search_rules", {
|
|
|
5047
5092
|
});
|
|
5048
5093
|
server.registerTool("install_ruleset", {
|
|
5049
5094
|
title: "Install Ruleset",
|
|
5050
|
-
description: "Install a ruleset package from a files bundle
|
|
5051
|
-
inputSchema: {
|
|
5052
|
-
slug: z.string(),
|
|
5053
|
-
manifest: z.any(),
|
|
5054
|
-
index: z.any().optional(),
|
|
5055
|
-
model: z.any().optional(),
|
|
5056
|
-
tools: z.any().optional(),
|
|
5057
|
-
resources: z.any().optional(),
|
|
5058
|
-
prompts: z.any().optional(),
|
|
5059
|
-
},
|
|
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.") },
|
|
5060
5097
|
}, async (args) => {
|
|
5061
5098
|
requireGM();
|
|
5062
5099
|
try {
|
|
@@ -5076,8 +5113,8 @@ server.registerTool("install_ruleset", {
|
|
|
5076
5113
|
});
|
|
5077
5114
|
server.registerTool("remove_ruleset", {
|
|
5078
5115
|
title: "Remove Ruleset",
|
|
5079
|
-
description: "Remove an installed ruleset package
|
|
5080
|
-
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.") },
|
|
5081
5118
|
}, async ({ slug }) => {
|
|
5082
5119
|
requireGM();
|
|
5083
5120
|
const novel = state.activeNovel;
|
|
@@ -5094,7 +5131,7 @@ server.registerTool("remove_ruleset", {
|
|
|
5094
5131
|
});
|
|
5095
5132
|
server.registerTool("list_rulesets", {
|
|
5096
5133
|
title: "List Rulesets",
|
|
5097
|
-
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.",
|
|
5098
5135
|
// REQ-391 — scoped tool listing: default surface is active Novel's ruleset
|
|
5099
5136
|
// tools plus infrastructure; list_rulesets exposes per-package state without
|
|
5100
5137
|
// forcing hydration of inactive packages (REQ-390 lazy hydration).
|
|
@@ -5111,8 +5148,8 @@ server.registerTool("list_rulesets", {
|
|
|
5111
5148
|
});
|
|
5112
5149
|
server.registerTool("bind_novel_ruleset", {
|
|
5113
5150
|
title: "Bind Novel Ruleset",
|
|
5114
|
-
description: "Bind the active ruleset-free Novel to an installed ruleset
|
|
5115
|
-
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.") },
|
|
5116
5153
|
}, async ({ slug }) => {
|
|
5117
5154
|
requireGM();
|
|
5118
5155
|
if (!rulesets.isInstalled(slug)) {
|
|
@@ -5129,8 +5166,8 @@ server.registerTool("bind_novel_ruleset", {
|
|
|
5129
5166
|
});
|
|
5130
5167
|
server.registerTool("suggest_actions", {
|
|
5131
5168
|
title: "Suggest Actions",
|
|
5132
|
-
description: "Map player intent to world-model tool invocations
|
|
5133
|
-
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.") },
|
|
5134
5171
|
}, async ({ intent, entity_id }) => {
|
|
5135
5172
|
const novel = requireNovel();
|
|
5136
5173
|
const entity = entity_id ? novel.entities.get(entity_id) : state.getActiveEntity();
|
|
@@ -5227,7 +5264,7 @@ const REQ022_URI_CATALOG = [
|
|
|
5227
5264
|
// REQ-025 — spec_health reports build health, indexed counts, and URI completeness.
|
|
5228
5265
|
server.registerTool("spec_health", {
|
|
5229
5266
|
title: "Spec Health",
|
|
5230
|
-
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.",
|
|
5231
5268
|
inputSchema: {},
|
|
5232
5269
|
}, async () => {
|
|
5233
5270
|
const novel = state.activeNovel;
|
|
@@ -5287,6 +5324,17 @@ server.registerTool("spec_health", {
|
|
|
5287
5324
|
resource_count: (server._registeredResources ? Object.keys(server._registeredResources).length : 0),
|
|
5288
5325
|
resource_uris, // REQ-139 — resource URI presence from the live resource map.
|
|
5289
5326
|
prompt_health, // REQ-138 — per-prompt presence, length, budget, stale refs.
|
|
5327
|
+
// REQ-425d — the output format catalog (universal + surface sets + any
|
|
5328
|
+
// ruleset-declared formats) and whether the MCP Apps extension (REQ-426c)
|
|
5329
|
+
// is negotiated by the current client.
|
|
5330
|
+
output_formats: {
|
|
5331
|
+
universal: [...UNIVERSAL_FORMATS],
|
|
5332
|
+
statblock: [...STATBLOCK_FORMATS],
|
|
5333
|
+
session: [...SESSION_FORMATS],
|
|
5334
|
+
interchange: [...INTERCHANGE_FORMATS],
|
|
5335
|
+
declared: [...declaredFormats],
|
|
5336
|
+
},
|
|
5337
|
+
mcp_apps: { negotiated: appsNegotiated() },
|
|
5290
5338
|
confidence: { overall: "N/A — ruleset-free", per_file: {}, per_category: {} },
|
|
5291
5339
|
indexed_counts: {
|
|
5292
5340
|
anchors: rulesets.installedSlugs().reduce((n, s) => n + (rulesets.hydrate(s)?.index.length ?? 0), 0),
|
|
@@ -5496,13 +5544,22 @@ server.registerResource("npc-single", new ResourceTemplate("npc://{id}", { list:
|
|
|
5496
5544
|
return { resources: [...novel.npcs.keys()].map(id => ({ uri: `npc://${id}`, name: id })) };
|
|
5497
5545
|
} }), { title: "NPC Record" }, async (uri) => {
|
|
5498
5546
|
const novel = state.activeNovel;
|
|
5499
|
-
const id = uri
|
|
5547
|
+
const id = resourceKey(uri);
|
|
5500
5548
|
if (!novel)
|
|
5501
5549
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "no active novel" }), mimeType: "application/json" }] };
|
|
5502
5550
|
const npc = novel.npcs.get(id);
|
|
5503
5551
|
if (!npc)
|
|
5504
5552
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
5505
|
-
|
|
5553
|
+
// REQ-425a/c — the NPC stat block honors the output format catalog; the
|
|
5554
|
+
// markdown render is byte-identical to character_sheet(npc_id) via the same
|
|
5555
|
+
// fmtEntitySheet renderer.
|
|
5556
|
+
const fmt = resourceFormat(uri);
|
|
5557
|
+
if (fmt === "json")
|
|
5558
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify(npc, null, 2), mimeType: "application/json" }] };
|
|
5559
|
+
const md = fmtEntitySheet(npc);
|
|
5560
|
+
if (fmt === "html")
|
|
5561
|
+
return { contents: [{ uri: uri.href, text: toHtml(md), mimeType: "text/html" }] };
|
|
5562
|
+
return { contents: [{ uri: uri.href, text: md, mimeType: "text/markdown" }] };
|
|
5506
5563
|
});
|
|
5507
5564
|
server.registerResource("npcs", "npcs://", { title: "All NPCs" }, async () => {
|
|
5508
5565
|
// REQ-121 — NPC resource URIs: npcs:// lists all active NPCs with summary
|
|
@@ -5554,13 +5611,21 @@ server.registerResource("lore-single", new ResourceTemplate("lore://{key}", { li
|
|
|
5554
5611
|
return { resources: [...novel.lore.keys()].map(k => ({ uri: `lore://${k}`, name: k })) };
|
|
5555
5612
|
} }), { title: "Lore Entry" }, async (uri) => {
|
|
5556
5613
|
const novel = state.activeNovel;
|
|
5557
|
-
const key = uri
|
|
5614
|
+
const key = resourceKey(uri);
|
|
5558
5615
|
if (!novel)
|
|
5559
5616
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "no active novel" }), mimeType: "application/json" }] };
|
|
5560
5617
|
const entry = novel.lore.get(key);
|
|
5561
5618
|
if (!entry)
|
|
5562
5619
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
5563
|
-
|
|
5620
|
+
// REQ-425a/c — the lore entry honors the output format catalog (markdown
|
|
5621
|
+
// default, json, html).
|
|
5622
|
+
const fmt = resourceFormat(uri);
|
|
5623
|
+
if (fmt === "json")
|
|
5624
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify(entry, null, 2), mimeType: "application/json" }] };
|
|
5625
|
+
const md = `## ${entry.key}\n\n${entry.content}`;
|
|
5626
|
+
if (fmt === "html")
|
|
5627
|
+
return { contents: [{ uri: uri.href, text: toHtml(md), mimeType: "text/html" }] };
|
|
5628
|
+
return { contents: [{ uri: uri.href, text: md, mimeType: "text/markdown" }] };
|
|
5564
5629
|
});
|
|
5565
5630
|
// Audit resource
|
|
5566
5631
|
server.registerResource("audit-novel", "audit://novel", { title: "Audit Log" }, async () => {
|
|
@@ -5741,7 +5806,7 @@ server.registerResource("server-notes-single", new ResourceTemplate("server-note
|
|
|
5741
5806
|
server.registerResource("codex-single", new ResourceTemplate("codex://{id}", { list: () => {
|
|
5742
5807
|
return { resources: [...state.codex.keys()].map(id => ({ uri: `codex://${id}`, name: state.codex.get(id)?.name ?? id })) };
|
|
5743
5808
|
} }), { title: "Codex Entry" }, async (uri) => {
|
|
5744
|
-
const id =
|
|
5809
|
+
const id = resourceKey(uri);
|
|
5745
5810
|
const entry = state.codex.get(id);
|
|
5746
5811
|
if (!entry)
|
|
5747
5812
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
@@ -5749,7 +5814,15 @@ server.registerResource("codex-single", new ResourceTemplate("codex://{id}", { l
|
|
|
5749
5814
|
if (entry.visibility === "private" && badge !== "game_master" && badge !== "none") {
|
|
5750
5815
|
return { contents: [{ uri: uri.href, text: "[FORBIDDEN] This codex entry is private.", mimeType: "text/plain" }] };
|
|
5751
5816
|
}
|
|
5752
|
-
|
|
5817
|
+
// REQ-425a/c — the codex entry honors the output format catalog (markdown
|
|
5818
|
+
// default, json, html).
|
|
5819
|
+
const fmt = resourceFormat(uri);
|
|
5820
|
+
if (fmt === "json")
|
|
5821
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify(entry, null, 2), mimeType: "application/json" }] };
|
|
5822
|
+
const md = codexEntryMarkdown(entry);
|
|
5823
|
+
if (fmt === "html")
|
|
5824
|
+
return { contents: [{ uri: uri.href, text: toHtml(md), mimeType: "text/html" }] };
|
|
5825
|
+
return { contents: [{ uri: uri.href, text: md, mimeType: "text/markdown" }] };
|
|
5753
5826
|
});
|
|
5754
5827
|
// Faction resources (REQ-233)
|
|
5755
5828
|
server.registerResource("factions-collection", "factions://", { title: "All Factions" }, async () => {
|
|
@@ -5860,11 +5933,56 @@ server.registerResource("synthesis-status", "synthesis://status", { title: "Synt
|
|
|
5860
5933
|
}
|
|
5861
5934
|
return { contents: [{ uri: "synthesis://status", text: md, mimeType: "text/markdown" }] };
|
|
5862
5935
|
});
|
|
5936
|
+
// ── MCP Apps UI resources (REQ-426) ────────────────────────────────
|
|
5937
|
+
//
|
|
5938
|
+
// REQ-426a — interactive HTML views of user-requestable artifacts under the
|
|
5939
|
+
// `ui://` scheme, served `text/html;profile=mcp-app` with restrictive CSP
|
|
5940
|
+
// metadata (REQ-426d). REQ-426c — the view is gated on extension negotiation.
|
|
5941
|
+
server.registerResource("ui-character-sheet", new ResourceTemplate("ui://character-sheet/{id}", { list: () => {
|
|
5942
|
+
const novel = state.activeNovel;
|
|
5943
|
+
if (!novel)
|
|
5944
|
+
return { resources: [] };
|
|
5945
|
+
return { resources: [...novel.entities.keys(), ...novel.npcs.keys()].map(id => ({ uri: `ui://character-sheet/${id}`, name: id })) };
|
|
5946
|
+
} }), { title: "Character Sheet (UI)" }, async (uri) => {
|
|
5947
|
+
const id = resourceKey(uri);
|
|
5948
|
+
const entity = resolveEntityOrNpc(id);
|
|
5949
|
+
if (!entity)
|
|
5950
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
5951
|
+
return uiResourceResult(uri.href, toHtml(fmtEntitySheet(entity)), appsNegotiated());
|
|
5952
|
+
});
|
|
5953
|
+
server.registerResource("ui-codex", new ResourceTemplate("ui://codex/{id}", { list: () => {
|
|
5954
|
+
return { resources: [...state.codex.keys()].map(id => ({ uri: `ui://codex/${id}`, name: state.codex.get(id)?.name ?? id })) };
|
|
5955
|
+
} }), { title: "Codex Entry (UI)" }, async (uri) => {
|
|
5956
|
+
const entry = state.codex.get(resourceKey(uri));
|
|
5957
|
+
if (!entry)
|
|
5958
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
5959
|
+
return uiResourceResult(uri.href, toHtml(codexEntryMarkdown(entry)), appsNegotiated());
|
|
5960
|
+
});
|
|
5961
|
+
server.registerResource("ui-lore", new ResourceTemplate("ui://lore/{key}", { list: () => {
|
|
5962
|
+
const novel = state.activeNovel;
|
|
5963
|
+
if (!novel)
|
|
5964
|
+
return { resources: [] };
|
|
5965
|
+
return { resources: [...novel.lore.keys()].map(k => ({ uri: `ui://lore/${k}`, name: k })) };
|
|
5966
|
+
} }), { title: "Lore Entry (UI)" }, async (uri) => {
|
|
5967
|
+
const novel = state.activeNovel;
|
|
5968
|
+
const key = resourceKey(uri);
|
|
5969
|
+
const entry = novel?.lore.get(key);
|
|
5970
|
+
if (!entry)
|
|
5971
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
5972
|
+
return uiResourceResult(uri.href, toHtml(`## ${entry.key}\n\n${entry.content}`), appsNegotiated());
|
|
5973
|
+
});
|
|
5974
|
+
server.registerResource("ui-novel", "ui://novel/current", { title: "Active Novel (UI)" }, async () => {
|
|
5975
|
+
const novel = state.activeNovel;
|
|
5976
|
+
if (!novel)
|
|
5977
|
+
return { contents: [{ uri: "ui://novel/current", text: JSON.stringify({ error: "no active novel" }), mimeType: "application/json" }] };
|
|
5978
|
+
const md = `## ${novel.name}\n\n${novel.description ?? ""}\n`;
|
|
5979
|
+
return uiResourceResult("ui://novel/current", toHtml(md), appsNegotiated());
|
|
5980
|
+
});
|
|
5863
5981
|
// ── Additional tools (REQ-307, REQ-213/214, REQ-321, REQ-103, REQ-239) ──
|
|
5864
5982
|
server.registerTool("set_party_presence", {
|
|
5865
5983
|
title: "Set Party Presence",
|
|
5866
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.",
|
|
5867
|
-
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.") },
|
|
5868
5986
|
}, async ({ entity_ids, location }) => {
|
|
5869
5987
|
requireGM();
|
|
5870
5988
|
const novel = requireNovel();
|
|
@@ -5876,7 +5994,7 @@ server.registerTool("set_party_presence", {
|
|
|
5876
5994
|
server.registerTool("roll_on_table", {
|
|
5877
5995
|
title: "Roll On Table",
|
|
5878
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.",
|
|
5879
|
-
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.") },
|
|
5880
5998
|
}, async ({ table, seed }) => {
|
|
5881
5999
|
const novel = state.activeNovel;
|
|
5882
6000
|
const slug = novel?.ruleset ?? null;
|
|
@@ -5910,14 +6028,7 @@ server.registerTool("roll_on_table", {
|
|
|
5910
6028
|
server.registerTool("codex_set", {
|
|
5911
6029
|
title: "Set Codex Entry",
|
|
5912
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.",
|
|
5913
|
-
inputSchema: {
|
|
5914
|
-
kind: z.string(),
|
|
5915
|
-
name: z.string(),
|
|
5916
|
-
content: z.any(),
|
|
5917
|
-
description: z.string().optional(),
|
|
5918
|
-
tags: z.array(z.string()).optional(),
|
|
5919
|
-
visibility: z.enum(["library", "shared", "private"]).optional(),
|
|
5920
|
-
},
|
|
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.") },
|
|
5921
6032
|
}, async ({ kind, name, content, description, tags, visibility }) => {
|
|
5922
6033
|
requireGM();
|
|
5923
6034
|
const id = `${kind.toLowerCase()}_${name.toLowerCase().replace(/[^a-z0-9]+/g, "_")}`;
|
|
@@ -5938,7 +6049,7 @@ server.registerTool("codex_set", {
|
|
|
5938
6049
|
server.registerTool("codex_list", {
|
|
5939
6050
|
title: "List Codex Entries",
|
|
5940
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.",
|
|
5941
|
-
inputSchema: { kind: z.string().optional() },
|
|
6052
|
+
inputSchema: { kind: z.string().optional().describe("Optional kind filter.") },
|
|
5942
6053
|
}, async ({ kind }) => {
|
|
5943
6054
|
const badge = getBadge();
|
|
5944
6055
|
let entries = [...state.codex.values()];
|
|
@@ -5952,7 +6063,7 @@ server.registerTool("codex_list", {
|
|
|
5952
6063
|
server.registerTool("codex_capture", {
|
|
5953
6064
|
title: "Capture to Codex",
|
|
5954
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.",
|
|
5955
|
-
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.") },
|
|
5956
6067
|
}, async ({ kind, entity_id, update_source }) => {
|
|
5957
6068
|
requireGM();
|
|
5958
6069
|
const novel = requireNovel();
|
|
@@ -5976,7 +6087,7 @@ server.registerTool("codex_capture", {
|
|
|
5976
6087
|
server.registerTool("codex_import", {
|
|
5977
6088
|
title: "Import from Codex",
|
|
5978
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.",
|
|
5979
|
-
inputSchema: { entry_id: z.string() },
|
|
6090
|
+
inputSchema: { entry_id: z.string().describe("The codex entry identifier to import.") },
|
|
5980
6091
|
}, async ({ entry_id }) => {
|
|
5981
6092
|
requireGM();
|
|
5982
6093
|
const novel = requireNovel();
|
|
@@ -6024,8 +6135,8 @@ server.registerTool("codex_import", {
|
|
|
6024
6135
|
const PLAYER_SYNTH_MODULES = ["voice_examples", "action_patterns", "supplementary_guidance", "narrative_voices", "lore_templates"];
|
|
6025
6136
|
server.registerTool("player_synthesize", {
|
|
6026
6137
|
title: "Player Synthesize",
|
|
6027
|
-
description: "Create a player-authored synthesis item
|
|
6028
|
-
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.") },
|
|
6029
6140
|
}, async ({ module, key, content, triggers, badge_scope }) => {
|
|
6030
6141
|
requirePlayer();
|
|
6031
6142
|
const novel = requireNovel();
|
|
@@ -6045,8 +6156,8 @@ server.registerTool("player_synthesize", {
|
|
|
6045
6156
|
});
|
|
6046
6157
|
server.registerTool("player_remove_synthesis", {
|
|
6047
6158
|
title: "Player Remove Synthesis",
|
|
6048
|
-
description: "Remove a player-authored synthesis item
|
|
6049
|
-
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.") },
|
|
6050
6161
|
}, async ({ module, key }) => {
|
|
6051
6162
|
requirePlayer();
|
|
6052
6163
|
const novel = requireNovel();
|
|
@@ -6061,8 +6172,8 @@ server.registerTool("player_remove_synthesis", {
|
|
|
6061
6172
|
});
|
|
6062
6173
|
server.registerTool("player_list_synthesis", {
|
|
6063
6174
|
title: "Player List Synthesis",
|
|
6064
|
-
description: "List player-authored synthesis items
|
|
6065
|
-
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.") },
|
|
6066
6177
|
}, async ({ module }) => {
|
|
6067
6178
|
requirePlayer();
|
|
6068
6179
|
const novel = requireNovel();
|
|
@@ -6079,7 +6190,7 @@ server.registerTool("synthesize", {
|
|
|
6079
6190
|
// items with novel:// source URIs; fingerprint staleness + force bypass.
|
|
6080
6191
|
// REQ-264 — confidence model: explicit-field items carry MEDIUM, inferred
|
|
6081
6192
|
// items LOW, tagged [supplementary] [MEDIUM|LOW].
|
|
6082
|
-
inputSchema: { force: z.boolean().optional() },
|
|
6193
|
+
inputSchema: { force: z.boolean().optional().describe("When true, re-run synthesis even if unchanged.") },
|
|
6083
6194
|
}, async ({ force }) => {
|
|
6084
6195
|
requireGM();
|
|
6085
6196
|
const novel = requireNovel();
|
|
@@ -6095,7 +6206,7 @@ server.registerTool("synthesize", {
|
|
|
6095
6206
|
server.registerTool("list_synthesis_items", {
|
|
6096
6207
|
title: "List Synthesis Items",
|
|
6097
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.",
|
|
6098
|
-
inputSchema: { module: z.string().optional(), ...detailZod },
|
|
6209
|
+
inputSchema: { module: z.string().optional().describe("Optional module filter."), ...detailZod },
|
|
6099
6210
|
}, async ({ module, detail }) => {
|
|
6100
6211
|
const manifest = state.enrichmentManifest;
|
|
6101
6212
|
if (!manifest)
|
|
@@ -6116,7 +6227,7 @@ server.registerTool("list_synthesis_items", {
|
|
|
6116
6227
|
server.registerTool("activate_synthesis_item", {
|
|
6117
6228
|
title: "Activate Synthesis Item",
|
|
6118
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.",
|
|
6119
|
-
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.") },
|
|
6120
6231
|
}, async ({ module, key }) => {
|
|
6121
6232
|
requireGM();
|
|
6122
6233
|
const novel = requireNovel();
|
|
@@ -6131,7 +6242,7 @@ server.registerTool("activate_synthesis_item", {
|
|
|
6131
6242
|
server.registerTool("deactivate_synthesis_item", {
|
|
6132
6243
|
title: "Deactivate Synthesis Item",
|
|
6133
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.",
|
|
6134
|
-
inputSchema: { module: z.string() },
|
|
6245
|
+
inputSchema: { module: z.string().describe("The synthesis module to deactivate.") },
|
|
6135
6246
|
}, async ({ module }) => {
|
|
6136
6247
|
requireGM();
|
|
6137
6248
|
const novel = requireNovel();
|
|
@@ -6144,7 +6255,7 @@ server.registerTool("deactivate_synthesis_item", {
|
|
|
6144
6255
|
server.registerTool("toggle_synthesis_module", {
|
|
6145
6256
|
title: "Toggle Synthesis Module",
|
|
6146
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.",
|
|
6147
|
-
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.") },
|
|
6148
6259
|
}, async ({ module, enabled }) => {
|
|
6149
6260
|
requireGM();
|
|
6150
6261
|
const novel = requireNovel();
|