@hanna84/mcp-writing 3.24.0 → 3.25.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.25.0](https://github.com/hannasdev/mcp-writing/compare/v3.24.1...v3.25.0)
8
+
9
+ - feat: clarify M1 workflow and relationship responses [`#235`](https://github.com/hannasdev/mcp-writing/pull/235)
10
+
11
+ #### [v3.24.1](https://github.com/hannasdev/mcp-writing/compare/v3.24.0...v3.24.1)
12
+
13
+ > 6 June 2026
14
+
15
+ - test: add relationship boundary release readiness coverage [`#234`](https://github.com/hannasdev/mcp-writing/pull/234)
16
+ - Release 3.24.1 [`158ff88`](https://github.com/hannasdev/mcp-writing/commit/158ff8817e7bb3aefc92b2c4c05c8c2b36c83ef3)
17
+
7
18
  #### [v3.24.0](https://github.com/hannasdev/mcp-writing/compare/v3.23.2...v3.24.0)
8
19
 
20
+ > 6 June 2026
21
+
9
22
  - feat: add one-sided scene evidence workflows [`#233`](https://github.com/hannasdev/mcp-writing/pull/233)
23
+ - Release 3.24.0 [`8818d75`](https://github.com/hannasdev/mcp-writing/commit/8818d75e19ad490a3fca2fb71489c9db65eca69b)
10
24
 
11
25
  #### [v3.23.2](https://github.com/hannasdev/mcp-writing/compare/v3.23.1...v3.23.2)
12
26
 
package/README.md CHANGED
@@ -28,9 +28,8 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
28
28
 
29
29
  **Current status:**
30
30
  - **Core platform complete:** Metadata-first analysis, SQLite-canonical structural and relationship metadata, compatibility sidecar maintenance, AI-assisted prose editing with confirmation + git history, review bundles, and Scrivener Direct extraction are all implemented.
31
- - **Recently completed:** Database Backup and Recovery added project backup export, freshness diagnostics, advisory operation history, automatic backup refresh after sanctioned project-scoped canonical mutations, dry-run restore planning, transactional restore application, and backup/restore operations guidance.
32
- - **Previous milestone:** Docker, CI, and Deployment Workflow made Docker a supported way to build, run, smoke-test, and deploy Writing MCP.
33
- - **Active development:** Relationship Metadata Boundary M3, aligning workflow and generated documentation around SQLite-canonical relationship authority.
31
+ - **Recently completed:** Relationship Metadata Boundary closed the sidecar-first scene relationship mutation path, added SQLite-first paired and one-sided evidence workflows, and preserved legacy sidecar/frontmatter compatibility for sync and import.
32
+ - **Active development:** No initiative is currently selected.
34
33
  - **Deferred backlog:** OpenClaw integration, client-agnostic setup, divisions, and embeddings search.
35
34
  - **Ideas and open questions:** tracked separately so future exploration does not distort the active roadmap.
36
35
 
@@ -59,7 +58,7 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
59
58
 
60
59
  ### `describe_workflows` surface redesign
61
60
 
62
- `describe_workflows` now exposes an outcome-first, discovery-first workflow map. This is a breaking change if your prompts or automation depend on previous workflow IDs or ordering.
61
+ `describe_workflows` now exposes an outcome-first, discovery-first workflow map. This was a breaking change if your prompts or automation depend on previous workflow IDs or ordering; the newer `recommended_next_actions` tier is additive and appears before the full catalogue.
63
62
 
64
63
  Update integrations using this mapping:
65
64
 
@@ -154,7 +153,7 @@ Goal: keep indexes accurate without manually re-tagging everything.
154
153
 
155
154
  1. After rewriting scenes, call `enrich_scene` to re-derive lightweight metadata from current prose.
156
155
  2. Use `update_scene_metadata` for intentional editorial fields (for example, beat, POV, status, and tags). It rejects scene `characters` and `places`; use `connect_character_place_evidence` when a scene proves paired sheet-backed character/place evidence, `connect_scene_character_evidence` for character-only evidence, and `connect_scene_place_evidence` for place-only evidence. Use `audit_relationship_metadata` for retained sidecar/frontmatter relationship fields. Use `list_chapters` plus `assign_scene_to_chapter` or `move_scene` for chapter placement and ordering.
157
- 3. Use `search_metadata` and `find_scenes` to verify scenes are discoverable under the expected filters.
156
+ 3. Use `search_metadata` for keyword/FTS metadata searches across indexed titles, loglines, tags, characters, places, and versions, and use `find_scenes` to verify scenes are discoverable under structured filters. After identifying likely scenes, use `get_scene_prose` for prose context.
158
157
 
159
158
  Outcome: your AI assistant can reliably find the right scenes without drifting from the manuscript.
160
159
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.24.0",
3
+ "version": "3.25.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",
package/src/index.js CHANGED
@@ -327,6 +327,100 @@ function maxScenesNextStep(matchedCount) {
327
327
  return `Re-run with max_scenes set to at least ${matchedCount}.`;
328
328
  }
329
329
 
330
+ function buildRecommendedNextActions({
331
+ sceneCount,
332
+ setupContract,
333
+ dbMigrationWarnings,
334
+ }) {
335
+ const recommendations = [];
336
+
337
+ if (dbMigrationWarnings.length > 0) {
338
+ recommendations.push({
339
+ id: "refresh_index_after_migration_warning",
340
+ label: "Refresh indexed state",
341
+ tool: "sync",
342
+ workflow_id: "parity_recovery",
343
+ priority: 10,
344
+ reason: "Database migration warnings are present, so indexed metadata may need a sync before other work.",
345
+ next_step: "Call sync, then re-run describe_workflows before mutating project state.",
346
+ });
347
+ }
348
+
349
+ if (sceneCount === 0) {
350
+ recommendations.push({
351
+ id: "index_project_content",
352
+ label: "Index project content",
353
+ tool: "sync",
354
+ workflow_id: "first_time_setup",
355
+ priority: 20,
356
+ reason: "No scenes are indexed yet, so discovery and scene-reading tools have no project content to inspect.",
357
+ next_step: "Call sync to index the configured writing folder, then use find_scenes without filters to confirm project_ids.",
358
+ });
359
+ recommendations.push({
360
+ id: "inspect_runtime_configuration",
361
+ label: "Inspect runtime configuration",
362
+ tool: "get_runtime_config",
363
+ workflow_id: "first_time_setup",
364
+ priority: 30,
365
+ reason: "Runtime paths and writable state determine whether sync can index the intended manuscript folder.",
366
+ next_step: "Call get_runtime_config if sync still indexes no scenes or the configured writing folder looks wrong.",
367
+ });
368
+ recommendations.push({
369
+ id: "diagnose_empty_index",
370
+ label: "Diagnose indexed structure",
371
+ tool: "diagnose_structure",
372
+ workflow_id: "parity_recovery",
373
+ priority: 35,
374
+ reason: "If sync does not discover scenes, structure diagnostics can explain missing or ambiguous manuscript representations.",
375
+ next_step: "Call diagnose_structure after sync if the index is still empty or project structure looks unexpected.",
376
+ });
377
+ } else {
378
+ recommendations.push({
379
+ id: "discover_indexed_scenes",
380
+ label: "Discover indexed scenes",
381
+ tool: "find_scenes",
382
+ workflow_id: "question_driven_discovery",
383
+ priority: 20,
384
+ reason: "Scenes are indexed; metadata-first discovery is the safest starting point for most manuscript questions.",
385
+ next_step: "Call find_scenes with project_id or lightweight filters before loading prose.",
386
+ });
387
+ recommendations.push({
388
+ id: "keyword_metadata_search",
389
+ label: "Search metadata keywords",
390
+ tool: "search_metadata",
391
+ workflow_id: "question_driven_discovery",
392
+ priority: 30,
393
+ reason: "Use keyword/FTS metadata search when you know likely titles, logline words, tags, characters, places, or versions.",
394
+ next_step: "Search exact metadata keywords, then open likely scenes with get_scene_prose if prose context is needed.",
395
+ });
396
+ recommendations.push({
397
+ id: "read_likely_scene",
398
+ label: "Read a likely scene",
399
+ tool: "get_scene_prose",
400
+ workflow_id: "targeted_scene_reading",
401
+ priority: 35,
402
+ reason: "Prose should be loaded only after metadata has identified a likely scene.",
403
+ next_step: "Call get_scene_prose with a specific scene_id and project_id once discovery has narrowed the target.",
404
+ });
405
+ }
406
+
407
+ if (setupContract?.setup_recommended) {
408
+ recommendations.push({
409
+ id: "review_styleguide_setup",
410
+ label: "Review styleguide setup",
411
+ tool: "setup_prose_styleguide_config",
412
+ workflow_id: "styleguide_setup_new",
413
+ priority: 40,
414
+ reason: `Styleguide setup status is ${setupContract.styleguide_setup_status}.`,
415
+ next_step: "Follow context.setup_contract.plan_preview before running styleguide drift checks or enforcement workflows.",
416
+ });
417
+ }
418
+
419
+ return recommendations
420
+ .sort((a, b) => a.priority - b.priority)
421
+ .slice(0, 5);
422
+ }
423
+
330
424
  // ---------------------------------------------------------------------------
331
425
  // MCP server factory
332
426
  // ---------------------------------------------------------------------------
@@ -444,6 +538,11 @@ function createMcpServer() {
444
538
  pending_proposals: pendingProposals.size,
445
539
  db_migration_warnings: DB_STARTUP_WARNINGS,
446
540
  },
541
+ recommended_next_actions: buildRecommendedNextActions({
542
+ sceneCount: scene_count,
543
+ setupContract: setupContractContextCheck.value,
544
+ dbMigrationWarnings: DB_STARTUP_WARNINGS,
545
+ }),
447
546
  workflows: WORKFLOW_CATALOGUE,
448
547
  notes: [
449
548
  "Never write JavaScript or shell scripts to invoke tools. Call them directly.",
@@ -130,6 +130,40 @@ function buildRelationshipMetadataBoundaryDetails({ projectId, sceneId, blockedF
130
130
  };
131
131
  }
132
132
 
133
+ function relationshipEvidenceNotFoundDetails({ lookupKind, input, projectId, sceneId }) {
134
+ const details = {
135
+ lookup_kind: lookupKind,
136
+ input,
137
+ project_id: projectId,
138
+ };
139
+ if (sceneId !== undefined) {
140
+ details.scene_id = sceneId;
141
+ }
142
+
143
+ if (lookupKind === "scene") {
144
+ return {
145
+ ...details,
146
+ next_step: "Use find_scenes with the project_id to confirm the canonical scene_id, then retry the relationship evidence tool.",
147
+ };
148
+ }
149
+
150
+ if (lookupKind === "character") {
151
+ return {
152
+ ...details,
153
+ next_step: "Use list_characters to find the stable character_id for this project or universe, then retry the relationship evidence tool.",
154
+ };
155
+ }
156
+
157
+ return {
158
+ ...details,
159
+ next_step: "Use list_places to find the stable place_id for this project or universe, then retry the relationship evidence tool.",
160
+ };
161
+ }
162
+
163
+ function alreadyLinkedRelationshipNextStep({ entityKind }) {
164
+ return `This ${entityKind} was already linked to the scene, so no canonical relationship rows changed. Use the scene_relationships field in this response to inspect current links; call get_scene_prose with the scene_id and project_id if prose context is needed.`;
165
+ }
166
+
133
167
  function persistReferenceDocLink({ filePath, syncDir, targetDocId, relation }) {
134
168
  const syncDirAbs = path.resolve(syncDir);
135
169
  const syncDirReal = resolveBoundaryRootReal(syncDirAbs);
@@ -862,16 +896,42 @@ export function registerMetadataTools(s, {
862
896
  WHERE scene_id = ? AND project_id = ?
863
897
  `).get(scene_id, project_id);
864
898
  if (!scene) {
865
- return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found in project '${project_id}'.`);
899
+ return errorResponse(
900
+ "NOT_FOUND",
901
+ `Scene '${scene_id}' not found in project '${project_id}'.`,
902
+ relationshipEvidenceNotFoundDetails({
903
+ lookupKind: "scene",
904
+ input: scene_id,
905
+ projectId: project_id,
906
+ })
907
+ );
866
908
  }
867
909
 
868
910
  const character = resolveCharacterForProject(db, { characterId: character_id, projectId: project_id });
869
911
  if (!character) {
870
- return errorResponse("NOT_FOUND", `Character '${character_id}' is not indexed for project '${project_id}' or its universe.`);
912
+ return errorResponse(
913
+ "NOT_FOUND",
914
+ `Character '${character_id}' is not indexed for project '${project_id}' or its universe.`,
915
+ relationshipEvidenceNotFoundDetails({
916
+ lookupKind: "character",
917
+ input: character_id,
918
+ projectId: project_id,
919
+ sceneId: scene_id,
920
+ })
921
+ );
871
922
  }
872
923
  const place = resolvePlaceForProject(db, { placeId: place_id, projectId: project_id });
873
924
  if (!place) {
874
- return errorResponse("NOT_FOUND", `Place '${place_id}' is not indexed for project '${project_id}' or its universe.`);
925
+ return errorResponse(
926
+ "NOT_FOUND",
927
+ `Place '${place_id}' is not indexed for project '${project_id}' or its universe.`,
928
+ relationshipEvidenceNotFoundDetails({
929
+ lookupKind: "place",
930
+ input: place_id,
931
+ projectId: project_id,
932
+ sceneId: scene_id,
933
+ })
934
+ );
875
935
  }
876
936
 
877
937
  const before = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
@@ -1005,12 +1065,29 @@ export function registerMetadataTools(s, {
1005
1065
  WHERE scene_id = ? AND project_id = ?
1006
1066
  `).get(scene_id, project_id);
1007
1067
  if (!scene) {
1008
- return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found in project '${project_id}'.`);
1068
+ return errorResponse(
1069
+ "NOT_FOUND",
1070
+ `Scene '${scene_id}' not found in project '${project_id}'.`,
1071
+ relationshipEvidenceNotFoundDetails({
1072
+ lookupKind: "scene",
1073
+ input: scene_id,
1074
+ projectId: project_id,
1075
+ })
1076
+ );
1009
1077
  }
1010
1078
 
1011
1079
  if (!resolveEntity(entity_id)) {
1012
1080
  const label = entityKind[0].toUpperCase() + entityKind.slice(1);
1013
- return errorResponse("NOT_FOUND", `${label} '${entity_id}' is not indexed for project '${project_id}' or its universe.`);
1081
+ return errorResponse(
1082
+ "NOT_FOUND",
1083
+ `${label} '${entity_id}' is not indexed for project '${project_id}' or its universe.`,
1084
+ relationshipEvidenceNotFoundDetails({
1085
+ lookupKind: entityKind,
1086
+ input: entity_id,
1087
+ projectId: project_id,
1088
+ sceneId: scene_id,
1089
+ })
1090
+ );
1014
1091
  }
1015
1092
 
1016
1093
  const before = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
@@ -1095,7 +1172,11 @@ export function registerMetadataTools(s, {
1095
1172
  return jsonResponse({
1096
1173
  ok: true,
1097
1174
  action: "connected",
1175
+ outcome: alreadyLinked ? "no_op" : "connected",
1098
1176
  already_linked: alreadyLinked,
1177
+ ...(alreadyLinked
1178
+ ? { next_step: alreadyLinkedRelationshipNextStep({ entityKind }) }
1179
+ : {}),
1099
1180
  scene_id,
1100
1181
  project_id,
1101
1182
  [idField]: entity_id,
@@ -662,9 +662,9 @@ export function registerSearchTools(s, {
662
662
  // ---- search_metadata -----------------------------------------------------
663
663
  s.tool(
664
664
  "search_metadata",
665
- "Full-text search across scene titles, loglines (synopsis/logline text fields), and metadata keywords (tags/characters/places/versions). Use this when you don't know the exact scene_id or chapter but want to find scenes by topic, theme, or metadata keyword. Not a prose search use get_scene_prose to read actual text. Supports pagination via page/page_size and auto-paginates large result sets with total_count.",
665
+ "Keyword/FTS metadata search across scene titles, loglines (synopsis/logline text fields), and indexed metadata keywords (tags/characters/places/versions). Use this when you know likely words or exact metadata values but do not know the scene_id or chapter. This is not semantic search and does not search prose text; use find_scenes for structured filters and get_scene_prose after identifying likely scenes. Supports pagination via page/page_size and auto-paginates large result sets with total_count.",
666
666
  {
667
- query: z.string().describe("Search terms (e.g. 'hospital' or 'Sebastian feeding'). FTS5 syntax supported."),
667
+ query: z.string().describe("Keyword search terms from indexed metadata (e.g. 'hospital' or a quoted character/place/tag phrase). FTS5 syntax supported."),
668
668
  page: z.number().int().min(1).optional().describe("Optional page number for paginated responses (1-based)."),
669
669
  page_size: z.number().int().min(1).max(200).optional().describe("Optional page size for paginated responses (default: 20, max: 200)."),
670
670
  },
@@ -682,7 +682,11 @@ export function registerSearchTools(s, {
682
682
  }
683
683
 
684
684
  if (totalCount === 0) {
685
- return errorResponse("NO_RESULTS", "No scenes matched the search query.");
685
+ return errorResponse("NO_RESULTS", "No scenes matched the keyword metadata search query.", {
686
+ search_type: "keyword_metadata_fts",
687
+ searched_fields: ["scene.title", "scene.logline", "tags", "characters", "places", "versions"],
688
+ next_step: "Try exact metadata keywords, quoted character/place/tag names, or find_scenes structured filters. After finding likely scenes, use get_scene_prose for prose context; search_metadata is not semantic or prose search.",
689
+ });
686
690
  }
687
691
 
688
692
  const shouldPaginate = totalCount > DEFAULT_METADATA_PAGE_SIZE || page !== undefined || page_size !== undefined;
@@ -5,7 +5,7 @@ export const WORKFLOW_CATALOGUE = [
5
5
  use_when: "Start here for most sessions: when the user has a manuscript question, you need to narrow scope, or you are not yet sure which scene matters.",
6
6
  steps: [
7
7
  { tool: "find_scenes", note: "Use structured metadata filters first when the question already suggests characters, beats, tags, parts, chapters, or POV; numeric chapter values are read-scope aliases, while structure changes require canonical chapter_id workflows." },
8
- { tool: "search_metadata", note: "Use this when the question is thematic, fuzzy, or keyword-driven rather than cleanly filterable." },
8
+ { tool: "search_metadata", note: "Use this when likely title, logline, tag, character, place, or version keywords are known but structured filters are not cleanly available; it is not semantic or prose search." },
9
9
  { tool: "get_scene_prose", note: "Escalate to prose only after likely scenes have been identified and metadata is no longer enough." },
10
10
  { tool: "flag_scene", note: "Use only when the current task naturally leads to recording a follow-up note for later editorial attention." },
11
11
  ],