@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.5.4",
3
+ "version": "3.6.0",
4
4
  "description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
5
5
  "homepage": "https://hannasdev.github.io/mcp-writing/",
6
6
  "type": "module",
@@ -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.tags?.includes("epigraph");
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.tags?.includes("epigraph");
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;
@@ -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 { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
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 { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
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 nothing if no entries exist yet. Use list_characters to get character_id values.",
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