@hanna84/mcp-writing 3.5.4 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +27 -0
- package/package.json +1 -1
- package/src/review-bundles/review-bundles-renderer.js +14 -2
- package/src/tools/search.js +27 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,23 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v3.6.0](https://github.com/hannasdev/mcp-writing/compare/v3.5.5...v3.6.0)
|
|
8
|
+
|
|
9
|
+
- feat(search): normalize metadata-read tool response envelopes [`#189`](https://github.com/hannasdev/mcp-writing/pull/189)
|
|
10
|
+
|
|
11
|
+
#### [v3.5.5](https://github.com/hannasdev/mcp-writing/compare/v3.5.4...v3.5.5)
|
|
12
|
+
|
|
13
|
+
> 9 May 2026
|
|
14
|
+
|
|
15
|
+
- fix: suppress epigraph titles and refine review replies [`#187`](https://github.com/hannasdev/mcp-writing/pull/187)
|
|
16
|
+
- Release 3.5.5 [`26aa8f8`](https://github.com/hannasdev/mcp-writing/commit/26aa8f88d44dc49b0ee4c44a65b00274de73641c)
|
|
17
|
+
|
|
7
18
|
#### [v3.5.4](https://github.com/hannasdev/mcp-writing/compare/v3.5.3...v3.5.4)
|
|
8
19
|
|
|
20
|
+
> 8 May 2026
|
|
21
|
+
|
|
9
22
|
- chore(skills): add reusable post-merge cleanup skill [`#186`](https://github.com/hannasdev/mcp-writing/pull/186)
|
|
23
|
+
- Release 3.5.4 [`b17e35d`](https://github.com/hannasdev/mcp-writing/commit/b17e35d98d74a963bfca901e880e5d0965de7047)
|
|
10
24
|
|
|
11
25
|
#### [v3.5.3](https://github.com/hannasdev/mcp-writing/compare/v3.5.2...v3.5.3)
|
|
12
26
|
|
package/README.md
CHANGED
|
@@ -86,12 +86,39 @@ Safe parsing pattern:
|
|
|
86
86
|
|
|
87
87
|
```js
|
|
88
88
|
const parsed = JSON.parse(toolText);
|
|
89
|
+
if (parsed.ok === false) throw new Error(parsed.error?.message ?? "tool error");
|
|
89
90
|
const scenes = parsed.results ?? [];
|
|
90
91
|
const totalCount = parsed.total_count ?? scenes.length;
|
|
91
92
|
const warning = parsed.warning ?? null;
|
|
92
93
|
const nextStep = parsed.next_step ?? null;
|
|
93
94
|
```
|
|
94
95
|
|
|
96
|
+
### `get_character_sheet`, `get_place_sheet`, `list_scene_references`, `get_relationship_arc` response-shape standardization
|
|
97
|
+
|
|
98
|
+
These metadata-read tools now return structured envelopes instead of flat objects or raw arrays.
|
|
99
|
+
|
|
100
|
+
- `get_character_sheet` and `get_place_sheet`: previously returned a flat object of field values; now return `{ results: [row], total_count: 1, next_step }`.
|
|
101
|
+
- `list_scene_references`: previously returned `{ references, scene_id, project_id }`; now returns `{ results, total_count, scene_id, project_id }`.
|
|
102
|
+
- `get_relationship_arc`: previously returned a raw JSON array; now returns `{ results, total_count, from_character, to_character }`.
|
|
103
|
+
|
|
104
|
+
Safe parsing pattern for sheet tools:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
const parsed = JSON.parse(toolText);
|
|
108
|
+
if (parsed.ok === false) throw new Error(parsed.error?.message ?? "tool error");
|
|
109
|
+
const sheet = parsed.results?.[0] ?? {};
|
|
110
|
+
const nextStep = parsed.next_step ?? null;
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Safe parsing pattern for list/arc tools:
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
const parsed = JSON.parse(toolText);
|
|
117
|
+
if (parsed.ok === false) throw new Error(parsed.error?.message ?? "tool error");
|
|
118
|
+
const items = parsed.results ?? [];
|
|
119
|
+
const totalCount = parsed.total_count ?? items.length;
|
|
120
|
+
```
|
|
121
|
+
|
|
95
122
|
## Usage scenarios
|
|
96
123
|
|
|
97
124
|
### 1) Continuity pass before sending chapters to beta readers
|
package/package.json
CHANGED
|
@@ -374,6 +374,18 @@ function renderProseWithInlineEmphasis(doc, prose, {
|
|
|
374
374
|
doc.font(bodyFont).fontSize(fontSize);
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
+
function isEpigraphScene(scene) {
|
|
378
|
+
const tags = Array.isArray(scene?.tags)
|
|
379
|
+
? scene.tags
|
|
380
|
+
.map(tag => String(tag ?? "").trim().toLowerCase())
|
|
381
|
+
.filter(Boolean)
|
|
382
|
+
: [];
|
|
383
|
+
if (tags.includes("epigraph")) return true;
|
|
384
|
+
|
|
385
|
+
const title = String(scene?.title ?? "").trim();
|
|
386
|
+
return /^epigraph(?:\b|\s|[-:])/i.test(title);
|
|
387
|
+
}
|
|
388
|
+
|
|
377
389
|
function renderSceneBlock(scene, options) {
|
|
378
390
|
const {
|
|
379
391
|
profile,
|
|
@@ -384,7 +396,7 @@ function renderSceneBlock(scene, options) {
|
|
|
384
396
|
} = options;
|
|
385
397
|
|
|
386
398
|
const isBetaProfile = profile === "beta_reader_personalized";
|
|
387
|
-
const isEpigraph = isBetaProfile && scene
|
|
399
|
+
const isEpigraph = isBetaProfile && isEpigraphScene(scene);
|
|
388
400
|
|
|
389
401
|
const parts = [];
|
|
390
402
|
|
|
@@ -733,7 +745,7 @@ export function renderReviewBundlePdfWithMetadata(dbHandle, plan, { generatedAt,
|
|
|
733
745
|
}
|
|
734
746
|
|
|
735
747
|
// Skip title rendering for epigraphs in beta profile
|
|
736
|
-
const isEpigraph = isBetaProfile && scene
|
|
748
|
+
const isEpigraph = isBetaProfile && isEpigraphScene(scene);
|
|
737
749
|
if (!isEpigraph) {
|
|
738
750
|
doc.fontSize(isBetaProfile ? 13 : 14).font(sceneHeadingFont);
|
|
739
751
|
let heading = scene.title || scene.scene_id;
|
package/src/tools/search.js
CHANGED
|
@@ -371,7 +371,7 @@ export function registerSearchTools(s, {
|
|
|
371
371
|
// ---- get_character_sheet -------------------------------------------------
|
|
372
372
|
s.tool(
|
|
373
373
|
"get_character_sheet",
|
|
374
|
-
"Get full character details: role, arc_summary, traits, the canonical sheet content, and any adjacent support notes when the character uses a folder-based layout. Use this when the reasoning task needs the character's canonical profile rather than only their scene progression.",
|
|
374
|
+
"Get full character details: role, arc_summary, traits, the canonical sheet content, and any adjacent support notes when the character uses a folder-based layout. Use this when the reasoning task needs the character's canonical profile rather than only their scene progression. Response shape note: returns a structured envelope (`results`, `total_count`) with one result row.",
|
|
375
375
|
{
|
|
376
376
|
character_id: z.string().describe("The character_id to look up (e.g. 'char-sebastian'). Use list_characters to find valid IDs."),
|
|
377
377
|
},
|
|
@@ -402,9 +402,17 @@ export function registerSearchTools(s, {
|
|
|
402
402
|
traits,
|
|
403
403
|
notes: notes || undefined,
|
|
404
404
|
supporting_notes: supportingNotes.length ? supportingNotes : undefined,
|
|
405
|
-
next_step: "Use get_arc with this character_id to trace scene-level progression, then open specific scenes with get_scene_prose when prose evidence is needed.",
|
|
406
405
|
};
|
|
407
|
-
return {
|
|
406
|
+
return {
|
|
407
|
+
content: [{
|
|
408
|
+
type: "text",
|
|
409
|
+
text: JSON.stringify({
|
|
410
|
+
results: [result],
|
|
411
|
+
total_count: 1,
|
|
412
|
+
next_step: "Use get_arc with this character_id to trace scene-level progression, then open specific scenes with get_scene_prose when prose evidence is needed.",
|
|
413
|
+
}, null, 2),
|
|
414
|
+
}],
|
|
415
|
+
};
|
|
408
416
|
}
|
|
409
417
|
);
|
|
410
418
|
|
|
@@ -444,7 +452,7 @@ export function registerSearchTools(s, {
|
|
|
444
452
|
// ---- get_place_sheet -----------------------------------------------------
|
|
445
453
|
s.tool(
|
|
446
454
|
"get_place_sheet",
|
|
447
|
-
"Get full place details: associated_characters, tags, the canonical sheet content, and any adjacent support notes when the place uses a folder-based layout. Use this when the current scene or question makes the place itself materially relevant.",
|
|
455
|
+
"Get full place details: associated_characters, tags, the canonical sheet content, and any adjacent support notes when the place uses a folder-based layout. Use this when the current scene or question makes the place itself materially relevant. Response shape note: returns a structured envelope (`results`, `total_count`) with one result row.",
|
|
448
456
|
{
|
|
449
457
|
place_id: z.string().describe("The place_id to look up (e.g. 'place-harbor-district'). Use list_places to find valid IDs."),
|
|
450
458
|
},
|
|
@@ -480,9 +488,17 @@ export function registerSearchTools(s, {
|
|
|
480
488
|
tags: tags.length ? tags : undefined,
|
|
481
489
|
notes: notes || undefined,
|
|
482
490
|
supporting_notes: supportingNotes.length ? supportingNotes : undefined,
|
|
483
|
-
next_step: "Use find_scenes with related filters to locate scenes where this place matters, then open targeted scenes with get_scene_prose when prose context is needed.",
|
|
484
491
|
};
|
|
485
|
-
return {
|
|
492
|
+
return {
|
|
493
|
+
content: [{
|
|
494
|
+
type: "text",
|
|
495
|
+
text: JSON.stringify({
|
|
496
|
+
results: [result],
|
|
497
|
+
total_count: 1,
|
|
498
|
+
next_step: "Use find_scenes with related filters to locate scenes where this place matters, then open targeted scenes with get_scene_prose when prose context is needed.",
|
|
499
|
+
}, null, 2),
|
|
500
|
+
}],
|
|
501
|
+
};
|
|
486
502
|
}
|
|
487
503
|
);
|
|
488
504
|
|
|
@@ -635,7 +651,7 @@ export function registerSearchTools(s, {
|
|
|
635
651
|
// ---- list_scene_references -----------------------------------------------
|
|
636
652
|
s.tool(
|
|
637
653
|
"list_scene_references",
|
|
638
|
-
"List direct reference documents linked from a scene via metadata (for example, reference_ids). Returns only one-hop scene -> reference links and does not recursively traverse related references. If scene IDs are reused across projects, omitting project_id returns CONFLICT with candidate project_ids.",
|
|
654
|
+
"List direct reference documents linked from a scene via metadata (for example, reference_ids). Returns only one-hop scene -> reference links and does not recursively traverse related references. If scene IDs are reused across projects, omitting project_id returns CONFLICT with candidate project_ids. Response shape note: returns a structured envelope (`results`, `total_count`) plus the resolved `scene_id` and `project_id` context.",
|
|
639
655
|
{
|
|
640
656
|
scene_id: z.string().describe("Scene ID to inspect."),
|
|
641
657
|
project_id: z.string().optional().describe("Optional project ID to disambiguate duplicate scene IDs across projects."),
|
|
@@ -714,9 +730,10 @@ export function registerSearchTools(s, {
|
|
|
714
730
|
content: [{
|
|
715
731
|
type: "text",
|
|
716
732
|
text: JSON.stringify({
|
|
733
|
+
results: references,
|
|
734
|
+
total_count: references.length,
|
|
717
735
|
scene_id: scene.scene_id,
|
|
718
736
|
project_id: scene.project_id,
|
|
719
|
-
references,
|
|
720
737
|
}, null, 2),
|
|
721
738
|
}],
|
|
722
739
|
};
|
|
@@ -869,7 +886,7 @@ export function registerSearchTools(s, {
|
|
|
869
886
|
// ---- get_relationship_arc ------------------------------------------------
|
|
870
887
|
s.tool(
|
|
871
888
|
"get_relationship_arc",
|
|
872
|
-
"Show how the relationship between two characters evolves across scenes, in order. Uses explicitly recorded relationship entries — returns
|
|
889
|
+
"Show how the relationship between two characters evolves across scenes, in order. Uses explicitly recorded relationship entries — returns a NO_RESULTS error if no entries exist yet. Use list_characters to get character_id values. Response shape note: returns a structured envelope { results, total_count, from_character, to_character }.",
|
|
873
890
|
{
|
|
874
891
|
from_character: z.string().describe("character_id of the first character (e.g. 'char-sebastian')."),
|
|
875
892
|
to_character: z.string().describe("character_id of the second character (e.g. 'char-mira-nystrom')."),
|
|
@@ -892,7 +909,7 @@ export function registerSearchTools(s, {
|
|
|
892
909
|
if (rows.length === 0) {
|
|
893
910
|
return errorResponse("NO_RESULTS", `No relationship data found between '${from_character}' and '${to_character}'.`);
|
|
894
911
|
}
|
|
895
|
-
return { content: [{ type: "text", text: JSON.stringify(rows, null, 2) }] };
|
|
912
|
+
return { content: [{ type: "text", text: JSON.stringify({ results: rows, total_count: rows.length, from_character, to_character }, null, 2) }] };
|
|
896
913
|
}
|
|
897
914
|
);
|
|
898
915
|
|