holonovel 2026.8.30 → 2026.9.1
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 +243 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.01",
|
|
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.
|
|
@@ -1100,6 +1106,121 @@ function formatNpcSheet(npc) {
|
|
|
1100
1106
|
s += `**Location:** ${npc.location}\n`;
|
|
1101
1107
|
return s;
|
|
1102
1108
|
}
|
|
1109
|
+
// ── Output Format Catalog (REQ-425) ────────────────────────────────
|
|
1110
|
+
//
|
|
1111
|
+
// REQ-425a — every user-requestable artifact surface accepts an optional
|
|
1112
|
+
// `format` selector drawn from this catalog; the default is `markdown`.
|
|
1113
|
+
// REQ-425b — an unsupported format returns `[INVALID_INPUT]` enumerating the
|
|
1114
|
+
// surface's supported set, derived at call time (REQ-059). REQ-425c — the
|
|
1115
|
+
// same artifact in the same format renders byte-identically across surfaces
|
|
1116
|
+
// (tools and resources share the same render functions). REQ-425d — ruleset
|
|
1117
|
+
// packages may declare additional formats via the registry below.
|
|
1118
|
+
const UNIVERSAL_FORMATS = ["markdown", "json", "html"];
|
|
1119
|
+
const STATBLOCK_FORMATS = ["markdown", "json", "html", "ascii"];
|
|
1120
|
+
const SESSION_FORMATS = ["markdown", "lonelog"];
|
|
1121
|
+
const INTERCHANGE_FORMATS = ["json", "markdown"];
|
|
1122
|
+
// Ruleset-declared formats (REQ-425d). Packages register additional format
|
|
1123
|
+
// identifiers here at load time; they are surfaced in spec_health and in the
|
|
1124
|
+
// `[INVALID_INPUT]` enumeration of every surface.
|
|
1125
|
+
const declaredFormats = new Set([]);
|
|
1126
|
+
function registerDeclaredFormat(name) { declaredFormats.add(name); }
|
|
1127
|
+
function supportedFormats(surface) {
|
|
1128
|
+
return [...new Set([...surface, ...declaredFormats])];
|
|
1129
|
+
}
|
|
1130
|
+
// REQ-425b — validate a requested format against a surface's supported set;
|
|
1131
|
+
// returns the normalized format or an `[INVALID_INPUT]` result.
|
|
1132
|
+
function resolveFormat(format, surface) {
|
|
1133
|
+
const fmt = format ?? "markdown";
|
|
1134
|
+
if (!supportedFormats(surface).includes(fmt)) {
|
|
1135
|
+
const list = supportedFormats(surface).join(", ");
|
|
1136
|
+
return err("INVALID_INPUT", `Unsupported format '${fmt}'. Supported formats: ${list}.`);
|
|
1137
|
+
}
|
|
1138
|
+
return fmt;
|
|
1139
|
+
}
|
|
1140
|
+
// REQ-425c / REQ-426a — a presentational HTML render of a Markdown artifact.
|
|
1141
|
+
// Self-contained (no external origins) per REQ-426d. Minimal Markdown→HTML:
|
|
1142
|
+
// headings, bold, italics, and line breaks; everything else is escaped.
|
|
1143
|
+
function htmlEscape(s) {
|
|
1144
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1145
|
+
}
|
|
1146
|
+
function toHtml(markdown) {
|
|
1147
|
+
const body = markdown
|
|
1148
|
+
.split("\n")
|
|
1149
|
+
.map((line) => {
|
|
1150
|
+
const heading = line.match(/^(#{1,3})\s+(.+)$/);
|
|
1151
|
+
if (heading) {
|
|
1152
|
+
const level = heading[1].length;
|
|
1153
|
+
const text = htmlEscape(heading[2].replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1"));
|
|
1154
|
+
return `<h${level}>${text}</h${level}>`;
|
|
1155
|
+
}
|
|
1156
|
+
let l = htmlEscape(line);
|
|
1157
|
+
l = l.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/\*(.+?)\*/g, "<i>$1</i>");
|
|
1158
|
+
return l;
|
|
1159
|
+
})
|
|
1160
|
+
.join("<br>\n");
|
|
1161
|
+
return `<!DOCTYPE html>\n<html><body><article>${body}</article></body></html>\n`;
|
|
1162
|
+
}
|
|
1163
|
+
// REQ-426d — UI resource CSP metadata: no external network origins.
|
|
1164
|
+
function uiResourceMeta() {
|
|
1165
|
+
return { csp: { connectDomains: [], resourceDomains: [], frameDomains: [] } };
|
|
1166
|
+
}
|
|
1167
|
+
// REQ-426c — a negotiating client declares `io.modelcontextprotocol/ui` in its
|
|
1168
|
+
// capabilities.extensions. Non-negotiating clients fall back to text surfaces.
|
|
1169
|
+
function appsNegotiated() {
|
|
1170
|
+
const ext = server.server.getClientCapabilities()?.extensions;
|
|
1171
|
+
return !!ext && "io.modelcontextprotocol/ui" in ext;
|
|
1172
|
+
}
|
|
1173
|
+
// REQ-425c — the structured (`json`) render of a stat block, shared by the
|
|
1174
|
+
// character_sheet tool and the entity/npc resources so both agree byte-for-byte.
|
|
1175
|
+
function entitySheetJson(entity) {
|
|
1176
|
+
return {
|
|
1177
|
+
id: entity.id ?? null,
|
|
1178
|
+
name: entity.name ?? null,
|
|
1179
|
+
stats: entity.stats ?? null,
|
|
1180
|
+
personality: entity.personality ?? {},
|
|
1181
|
+
inventory: entity.inventory ?? [],
|
|
1182
|
+
current_room: entity.current_room ?? null,
|
|
1183
|
+
conditions: entity.conditions ?? [],
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
// REQ-425c — a Markdown render of a codex entry, shared by the codex://
|
|
1187
|
+
// resource and its `ui://` HTML view.
|
|
1188
|
+
function codexEntryMarkdown(entry) {
|
|
1189
|
+
let md = `## ${entry.name}\n`;
|
|
1190
|
+
if (entry.kind)
|
|
1191
|
+
md += `**Kind:** ${entry.kind}\n`;
|
|
1192
|
+
if (entry.description)
|
|
1193
|
+
md += `*${entry.description}*\n`;
|
|
1194
|
+
if (entry.content)
|
|
1195
|
+
md += `\n${JSON.stringify(entry.content, null, 2)}\n`;
|
|
1196
|
+
return md;
|
|
1197
|
+
}
|
|
1198
|
+
// REQ-426a/c — build a `ui://` resource result. A negotiating client receives
|
|
1199
|
+
// `text/html;profile=mcp-app` with restrictive CSP metadata (REQ-426d); a
|
|
1200
|
+
// non-negotiating client receives a plain-text fallback (REQ-426c).
|
|
1201
|
+
function uiResourceResult(uri, html, negotiated) {
|
|
1202
|
+
if (!negotiated) {
|
|
1203
|
+
return { contents: [{ uri, text: `[STATE_CONFLICT] ui:// resources require the MCP Apps extension (io.modelcontextprotocol/ui).`, mimeType: "text/plain" }] };
|
|
1204
|
+
}
|
|
1205
|
+
return { contents: [{ uri, text: html, mimeType: "text/html;profile=mcp-app", _meta: { ui: uiResourceMeta() } }] };
|
|
1206
|
+
}
|
|
1207
|
+
// REQ-426b — attach `ui://` linkage metadata to a tool result when a client has
|
|
1208
|
+
// negotiated the MCP Apps extension; non-negotiating clients get the bare result.
|
|
1209
|
+
function withUiLinkage(result, resourceUri) {
|
|
1210
|
+
if (!appsNegotiated())
|
|
1211
|
+
return result;
|
|
1212
|
+
const content = (result.content ?? []).map((c) => ({ ...c, _meta: { ui: { resourceUri } } }));
|
|
1213
|
+
return { ...result, content };
|
|
1214
|
+
}
|
|
1215
|
+
// REQ-425a — read the `format` query parameter from a resource URI (default
|
|
1216
|
+
// markdown) and extract the resource id/key, excluding the query string.
|
|
1217
|
+
function resourceFormat(uri) {
|
|
1218
|
+
return uri.searchParams.get("format") ?? "markdown";
|
|
1219
|
+
}
|
|
1220
|
+
function resourceKey(uri) {
|
|
1221
|
+
const raw = uri.href.split("?")[0].split("/").filter(Boolean).pop() ?? "";
|
|
1222
|
+
return decodeURIComponent(raw);
|
|
1223
|
+
}
|
|
1103
1224
|
// ── Tools ──────────────────────────────────────────────────────────
|
|
1104
1225
|
// --- Badge & Workflow ---
|
|
1105
1226
|
function badgeLabel(badge) {
|
|
@@ -1726,21 +1847,37 @@ server.registerTool("import_character", {
|
|
|
1726
1847
|
});
|
|
1727
1848
|
server.registerTool("character_sheet", {
|
|
1728
1849
|
title: "Character Sheet",
|
|
1729
|
-
description: "Render a character sheet for an entity. Formats: markdown (default), ascii.",
|
|
1850
|
+
description: "Render a character sheet for an entity. Formats: markdown (default), json, html, ascii.",
|
|
1730
1851
|
// REQ-120 — NPC rendering via the same sheet mechanism; REQ-124 — NPC damage
|
|
1731
1852
|
// resolution targets NPCs by identifier; REQ-129 — property group cardinality.
|
|
1853
|
+
// REQ-425a/b — the `format` selector is drawn from the output format catalog
|
|
1854
|
+
// (STATBLOCK_FORMATS) and validated at call time; REQ-426b — the result
|
|
1855
|
+
// carries `ui://` linkage metadata when the Apps extension is negotiated.
|
|
1732
1856
|
inputSchema: {
|
|
1733
1857
|
entity_id: z.string().optional(),
|
|
1734
|
-
format: z.
|
|
1858
|
+
format: z.string().optional(),
|
|
1735
1859
|
},
|
|
1736
1860
|
}, async ({ entity_id, format }) => {
|
|
1737
1861
|
const entity = resolveEntityOrNpc(entity_id);
|
|
1738
1862
|
if (!entity)
|
|
1739
1863
|
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
|
-
|
|
1864
|
+
const fmt = resolveFormat(format, STATBLOCK_FORMATS);
|
|
1865
|
+
if (typeof fmt !== "string")
|
|
1866
|
+
return fmt;
|
|
1867
|
+
let result;
|
|
1868
|
+
if (fmt === "ascii") {
|
|
1869
|
+
result = raw(`[OK] ${entity.name} Room: ${entity.current_room || "(none)"} Held: ${entity.inventory?.length || 0}`);
|
|
1870
|
+
}
|
|
1871
|
+
else if (fmt === "json") {
|
|
1872
|
+
result = raw(JSON.stringify(entitySheetJson(entity), null, 2));
|
|
1873
|
+
}
|
|
1874
|
+
else if (fmt === "html") {
|
|
1875
|
+
result = raw(toHtml(fmtEntitySheet(entity)));
|
|
1742
1876
|
}
|
|
1743
|
-
|
|
1877
|
+
else {
|
|
1878
|
+
result = ok(fmtEntitySheet(entity));
|
|
1879
|
+
}
|
|
1880
|
+
return withUiLinkage(result, `ui://character-sheet/${entity.id}`);
|
|
1744
1881
|
});
|
|
1745
1882
|
server.registerTool("set_active_entity", {
|
|
1746
1883
|
title: "Set Active Entity",
|
|
@@ -3210,9 +3347,14 @@ server.registerTool("export_lorebook", {
|
|
|
3210
3347
|
description: "Export novel lore entries in interchange format. Game Master only.",
|
|
3211
3348
|
// REQ-094 — lorebook interchange: lore-only export/import with merge, replace,
|
|
3212
3349
|
// and dry-run modes; round-trip preserves lore metadata (Appendix L).
|
|
3213
|
-
|
|
3350
|
+
// REQ-425b — interchange surfaces accept only json/markdown; html is a
|
|
3351
|
+
// presentation-only format and returns [INVALID_INPUT] enumerating the set.
|
|
3352
|
+
inputSchema: { format: z.string().optional() },
|
|
3214
3353
|
}, async ({ format: fmt }) => {
|
|
3215
3354
|
requireGM();
|
|
3355
|
+
if (fmt && !INTERCHANGE_FORMATS.includes(fmt)) {
|
|
3356
|
+
return err("INVALID_INPUT", `Unsupported format '${fmt}'. Supported formats: ${INTERCHANGE_FORMATS.join(", ")}.`);
|
|
3357
|
+
}
|
|
3216
3358
|
const novel = requireNovel();
|
|
3217
3359
|
const entries = [...novel.lore.values()];
|
|
3218
3360
|
if (fmt === "markdown") {
|
|
@@ -4808,9 +4950,14 @@ Options: yes, cancel`);
|
|
|
4808
4950
|
server.registerTool("export_novel", {
|
|
4809
4951
|
title: "Export Novel",
|
|
4810
4952
|
description: "Export the active novel in interchange format. Game Master only.",
|
|
4811
|
-
inputSchema: { format: z.
|
|
4953
|
+
inputSchema: { format: z.string().optional(), scope: z.string().optional() },
|
|
4812
4954
|
}, async ({ format: fmt, scope }) => {
|
|
4813
4955
|
requireGM();
|
|
4956
|
+
// REQ-425b — interchange surfaces accept only json/markdown; html returns
|
|
4957
|
+
// [INVALID_INPUT] enumerating the set (presentation-only, not round-trippable).
|
|
4958
|
+
if (fmt && !INTERCHANGE_FORMATS.includes(fmt)) {
|
|
4959
|
+
return err("INVALID_INPUT", `Unsupported format '${fmt}'. Supported formats: ${INTERCHANGE_FORMATS.join(", ")}.`);
|
|
4960
|
+
}
|
|
4814
4961
|
const novel = requireNovel();
|
|
4815
4962
|
if (fmt === "markdown") {
|
|
4816
4963
|
let md = `# ${novel.name}\n\n`;
|
|
@@ -5287,6 +5434,17 @@ server.registerTool("spec_health", {
|
|
|
5287
5434
|
resource_count: (server._registeredResources ? Object.keys(server._registeredResources).length : 0),
|
|
5288
5435
|
resource_uris, // REQ-139 — resource URI presence from the live resource map.
|
|
5289
5436
|
prompt_health, // REQ-138 — per-prompt presence, length, budget, stale refs.
|
|
5437
|
+
// REQ-425d — the output format catalog (universal + surface sets + any
|
|
5438
|
+
// ruleset-declared formats) and whether the MCP Apps extension (REQ-426c)
|
|
5439
|
+
// is negotiated by the current client.
|
|
5440
|
+
output_formats: {
|
|
5441
|
+
universal: [...UNIVERSAL_FORMATS],
|
|
5442
|
+
statblock: [...STATBLOCK_FORMATS],
|
|
5443
|
+
session: [...SESSION_FORMATS],
|
|
5444
|
+
interchange: [...INTERCHANGE_FORMATS],
|
|
5445
|
+
declared: [...declaredFormats],
|
|
5446
|
+
},
|
|
5447
|
+
mcp_apps: { negotiated: appsNegotiated() },
|
|
5290
5448
|
confidence: { overall: "N/A — ruleset-free", per_file: {}, per_category: {} },
|
|
5291
5449
|
indexed_counts: {
|
|
5292
5450
|
anchors: rulesets.installedSlugs().reduce((n, s) => n + (rulesets.hydrate(s)?.index.length ?? 0), 0),
|
|
@@ -5496,13 +5654,22 @@ server.registerResource("npc-single", new ResourceTemplate("npc://{id}", { list:
|
|
|
5496
5654
|
return { resources: [...novel.npcs.keys()].map(id => ({ uri: `npc://${id}`, name: id })) };
|
|
5497
5655
|
} }), { title: "NPC Record" }, async (uri) => {
|
|
5498
5656
|
const novel = state.activeNovel;
|
|
5499
|
-
const id = uri
|
|
5657
|
+
const id = resourceKey(uri);
|
|
5500
5658
|
if (!novel)
|
|
5501
5659
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "no active novel" }), mimeType: "application/json" }] };
|
|
5502
5660
|
const npc = novel.npcs.get(id);
|
|
5503
5661
|
if (!npc)
|
|
5504
5662
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
5505
|
-
|
|
5663
|
+
// REQ-425a/c — the NPC stat block honors the output format catalog; the
|
|
5664
|
+
// markdown render is byte-identical to character_sheet(npc_id) via the same
|
|
5665
|
+
// fmtEntitySheet renderer.
|
|
5666
|
+
const fmt = resourceFormat(uri);
|
|
5667
|
+
if (fmt === "json")
|
|
5668
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify(npc, null, 2), mimeType: "application/json" }] };
|
|
5669
|
+
const md = fmtEntitySheet(npc);
|
|
5670
|
+
if (fmt === "html")
|
|
5671
|
+
return { contents: [{ uri: uri.href, text: toHtml(md), mimeType: "text/html" }] };
|
|
5672
|
+
return { contents: [{ uri: uri.href, text: md, mimeType: "text/markdown" }] };
|
|
5506
5673
|
});
|
|
5507
5674
|
server.registerResource("npcs", "npcs://", { title: "All NPCs" }, async () => {
|
|
5508
5675
|
// REQ-121 — NPC resource URIs: npcs:// lists all active NPCs with summary
|
|
@@ -5554,13 +5721,21 @@ server.registerResource("lore-single", new ResourceTemplate("lore://{key}", { li
|
|
|
5554
5721
|
return { resources: [...novel.lore.keys()].map(k => ({ uri: `lore://${k}`, name: k })) };
|
|
5555
5722
|
} }), { title: "Lore Entry" }, async (uri) => {
|
|
5556
5723
|
const novel = state.activeNovel;
|
|
5557
|
-
const key = uri
|
|
5724
|
+
const key = resourceKey(uri);
|
|
5558
5725
|
if (!novel)
|
|
5559
5726
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "no active novel" }), mimeType: "application/json" }] };
|
|
5560
5727
|
const entry = novel.lore.get(key);
|
|
5561
5728
|
if (!entry)
|
|
5562
5729
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
5563
|
-
|
|
5730
|
+
// REQ-425a/c — the lore entry honors the output format catalog (markdown
|
|
5731
|
+
// default, json, html).
|
|
5732
|
+
const fmt = resourceFormat(uri);
|
|
5733
|
+
if (fmt === "json")
|
|
5734
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify(entry, null, 2), mimeType: "application/json" }] };
|
|
5735
|
+
const md = `## ${entry.key}\n\n${entry.content}`;
|
|
5736
|
+
if (fmt === "html")
|
|
5737
|
+
return { contents: [{ uri: uri.href, text: toHtml(md), mimeType: "text/html" }] };
|
|
5738
|
+
return { contents: [{ uri: uri.href, text: md, mimeType: "text/markdown" }] };
|
|
5564
5739
|
});
|
|
5565
5740
|
// Audit resource
|
|
5566
5741
|
server.registerResource("audit-novel", "audit://novel", { title: "Audit Log" }, async () => {
|
|
@@ -5741,7 +5916,7 @@ server.registerResource("server-notes-single", new ResourceTemplate("server-note
|
|
|
5741
5916
|
server.registerResource("codex-single", new ResourceTemplate("codex://{id}", { list: () => {
|
|
5742
5917
|
return { resources: [...state.codex.keys()].map(id => ({ uri: `codex://${id}`, name: state.codex.get(id)?.name ?? id })) };
|
|
5743
5918
|
} }), { title: "Codex Entry" }, async (uri) => {
|
|
5744
|
-
const id =
|
|
5919
|
+
const id = resourceKey(uri);
|
|
5745
5920
|
const entry = state.codex.get(id);
|
|
5746
5921
|
if (!entry)
|
|
5747
5922
|
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
@@ -5749,7 +5924,15 @@ server.registerResource("codex-single", new ResourceTemplate("codex://{id}", { l
|
|
|
5749
5924
|
if (entry.visibility === "private" && badge !== "game_master" && badge !== "none") {
|
|
5750
5925
|
return { contents: [{ uri: uri.href, text: "[FORBIDDEN] This codex entry is private.", mimeType: "text/plain" }] };
|
|
5751
5926
|
}
|
|
5752
|
-
|
|
5927
|
+
// REQ-425a/c — the codex entry honors the output format catalog (markdown
|
|
5928
|
+
// default, json, html).
|
|
5929
|
+
const fmt = resourceFormat(uri);
|
|
5930
|
+
if (fmt === "json")
|
|
5931
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify(entry, null, 2), mimeType: "application/json" }] };
|
|
5932
|
+
const md = codexEntryMarkdown(entry);
|
|
5933
|
+
if (fmt === "html")
|
|
5934
|
+
return { contents: [{ uri: uri.href, text: toHtml(md), mimeType: "text/html" }] };
|
|
5935
|
+
return { contents: [{ uri: uri.href, text: md, mimeType: "text/markdown" }] };
|
|
5753
5936
|
});
|
|
5754
5937
|
// Faction resources (REQ-233)
|
|
5755
5938
|
server.registerResource("factions-collection", "factions://", { title: "All Factions" }, async () => {
|
|
@@ -5860,6 +6043,51 @@ server.registerResource("synthesis-status", "synthesis://status", { title: "Synt
|
|
|
5860
6043
|
}
|
|
5861
6044
|
return { contents: [{ uri: "synthesis://status", text: md, mimeType: "text/markdown" }] };
|
|
5862
6045
|
});
|
|
6046
|
+
// ── MCP Apps UI resources (REQ-426) ────────────────────────────────
|
|
6047
|
+
//
|
|
6048
|
+
// REQ-426a — interactive HTML views of user-requestable artifacts under the
|
|
6049
|
+
// `ui://` scheme, served `text/html;profile=mcp-app` with restrictive CSP
|
|
6050
|
+
// metadata (REQ-426d). REQ-426c — the view is gated on extension negotiation.
|
|
6051
|
+
server.registerResource("ui-character-sheet", new ResourceTemplate("ui://character-sheet/{id}", { list: () => {
|
|
6052
|
+
const novel = state.activeNovel;
|
|
6053
|
+
if (!novel)
|
|
6054
|
+
return { resources: [] };
|
|
6055
|
+
return { resources: [...novel.entities.keys(), ...novel.npcs.keys()].map(id => ({ uri: `ui://character-sheet/${id}`, name: id })) };
|
|
6056
|
+
} }), { title: "Character Sheet (UI)" }, async (uri) => {
|
|
6057
|
+
const id = resourceKey(uri);
|
|
6058
|
+
const entity = resolveEntityOrNpc(id);
|
|
6059
|
+
if (!entity)
|
|
6060
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
6061
|
+
return uiResourceResult(uri.href, toHtml(fmtEntitySheet(entity)), appsNegotiated());
|
|
6062
|
+
});
|
|
6063
|
+
server.registerResource("ui-codex", new ResourceTemplate("ui://codex/{id}", { list: () => {
|
|
6064
|
+
return { resources: [...state.codex.keys()].map(id => ({ uri: `ui://codex/${id}`, name: state.codex.get(id)?.name ?? id })) };
|
|
6065
|
+
} }), { title: "Codex Entry (UI)" }, async (uri) => {
|
|
6066
|
+
const entry = state.codex.get(resourceKey(uri));
|
|
6067
|
+
if (!entry)
|
|
6068
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
6069
|
+
return uiResourceResult(uri.href, toHtml(codexEntryMarkdown(entry)), appsNegotiated());
|
|
6070
|
+
});
|
|
6071
|
+
server.registerResource("ui-lore", new ResourceTemplate("ui://lore/{key}", { list: () => {
|
|
6072
|
+
const novel = state.activeNovel;
|
|
6073
|
+
if (!novel)
|
|
6074
|
+
return { resources: [] };
|
|
6075
|
+
return { resources: [...novel.lore.keys()].map(k => ({ uri: `ui://lore/${k}`, name: k })) };
|
|
6076
|
+
} }), { title: "Lore Entry (UI)" }, async (uri) => {
|
|
6077
|
+
const novel = state.activeNovel;
|
|
6078
|
+
const key = resourceKey(uri);
|
|
6079
|
+
const entry = novel?.lore.get(key);
|
|
6080
|
+
if (!entry)
|
|
6081
|
+
return { contents: [{ uri: uri.href, text: JSON.stringify({ error: "not found" }), mimeType: "application/json" }] };
|
|
6082
|
+
return uiResourceResult(uri.href, toHtml(`## ${entry.key}\n\n${entry.content}`), appsNegotiated());
|
|
6083
|
+
});
|
|
6084
|
+
server.registerResource("ui-novel", "ui://novel/current", { title: "Active Novel (UI)" }, async () => {
|
|
6085
|
+
const novel = state.activeNovel;
|
|
6086
|
+
if (!novel)
|
|
6087
|
+
return { contents: [{ uri: "ui://novel/current", text: JSON.stringify({ error: "no active novel" }), mimeType: "application/json" }] };
|
|
6088
|
+
const md = `## ${novel.name}\n\n${novel.description ?? ""}\n`;
|
|
6089
|
+
return uiResourceResult("ui://novel/current", toHtml(md), appsNegotiated());
|
|
6090
|
+
});
|
|
5863
6091
|
// ── Additional tools (REQ-307, REQ-213/214, REQ-321, REQ-103, REQ-239) ──
|
|
5864
6092
|
server.registerTool("set_party_presence", {
|
|
5865
6093
|
title: "Set Party Presence",
|