@hanna84/mcp-writing 2.5.0 → 2.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,11 +4,31 @@ 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
+ #### [v2.6.0](https://github.com/hannasdev/mcp-writing.git
8
+ /compare/v2.5.1...v2.6.0)
9
+
10
+ - feat(styleguide): improve AI navigation with next_step hints and identifier validation [`#89`](https://github.com/hannasdev/mcp-writing.git
11
+ /pull/89)
12
+
13
+ #### [v2.5.1](https://github.com/hannasdev/mcp-writing.git
14
+ /compare/v2.5.0...v2.5.1)
15
+
16
+ > 26 April 2026
17
+
18
+ - docs(prd): update README phase status and add refactoring PRD [`#88`](https://github.com/hannasdev/mcp-writing.git
19
+ /pull/88)
20
+ - Release 2.5.1 [`4c78109`](https://github.com/hannasdev/mcp-writing.git
21
+ /commit/4c781092152e3ae52715bfe05dbfda143f9c37c7)
22
+
7
23
  #### [v2.5.0](https://github.com/hannasdev/mcp-writing.git
8
24
  /compare/v2.4.0...v2.5.0)
9
25
 
26
+ > 26 April 2026
27
+
10
28
  - feat(styleguide): add prose styleguide bootstrap suggestions [`#87`](https://github.com/hannasdev/mcp-writing.git
11
29
  /pull/87)
30
+ - Release 2.5.0 [`3e93d31`](https://github.com/hannasdev/mcp-writing.git
31
+ /commit/3e93d319212a9e8182bc4a272a8c97d806d605f8)
12
32
 
13
33
  #### [v2.4.0](https://github.com/hannasdev/mcp-writing.git
14
34
  /compare/v2.3.0...v2.4.0)
package/README.md CHANGED
@@ -10,9 +10,10 @@ Designed to work with [OpenClaw](https://github.com/openclaw/openclaw) but compa
10
10
 
11
11
  Instead of feeding an entire manuscript to an AI and hoping it fits in the context window, `mcp-writing` builds a structured index from your scene files. The AI queries that index first — finding relevant characters, beats, and loglines — then loads only the specific prose it needs.
12
12
 
13
- **Phase 1:** Read-only analysis. Ask questions about your project.
14
- **Phase 2:** Metadata write-back. Answers stay accurate as the manuscript evolves.
15
- **Phase 3 (current):** AI-assisted prose editing with confirmation and version history.
13
+ **Current status:**
14
+ - **Phase 1-3 completed:** Metadata-first analysis, sidecar-backed metadata maintenance, and AI-assisted prose editing with confirmation + git history.
15
+ - **Phase 4 delivered in part:** Review bundles and Scrivener Direct extraction are complete; embedding search and reference-doc querying are intentionally deferred.
16
+ - **Phase 5 in progress:** OpenClaw runtime integration is active.
16
17
 
17
18
  ## Who it is for
18
19
 
package/importer.js CHANGED
@@ -32,6 +32,16 @@ export function validateProjectId(projectId) {
32
32
  return { ok: true };
33
33
  }
34
34
 
35
+ export function validateUniverseId(universeId) {
36
+ if (typeof universeId !== "string" || universeId.trim().length === 0) {
37
+ return { ok: false, reason: "universe_id must be a non-empty string." };
38
+ }
39
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(universeId)) {
40
+ return { ok: false, reason: "universe_id may contain only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen." };
41
+ }
42
+ return { ok: true };
43
+ }
44
+
35
45
  // Parse "NNN Title [binder_id].txt" -> { seq, rawTitle, binderId, ext } or null
36
46
  function parseFilename(filename) {
37
47
  const m = filename.match(/^(\d+)\s+(.+?)\s*\[(\d+)\]\.(txt|md)$/);
package/index.js CHANGED
@@ -15,7 +15,7 @@ import { openDb } from "./db.js";
15
15
  import { syncAll, isSyncDirWritable, getSyncOwnershipDiagnostics, getFileWriteDiagnostics, writeMeta, readMeta, indexSceneFile, normalizeSceneMetaForPath, sidecarPath } from "./sync.js";
16
16
  import { isGitAvailable, isGitRepository, initGitRepository, createSnapshot, listSnapshots, getSceneProseAtCommit, getHeadCommitHash } from "./git.js";
17
17
  import { renderCharacterArcTemplate, renderCharacterSheetTemplate, renderPlaceSheetTemplate, slugifyEntityName } from "./world-entity-templates.js";
18
- import { importScrivenerSync, validateProjectId } from "./importer.js";
18
+ import { importScrivenerSync, validateProjectId, validateUniverseId } from "./importer.js";
19
19
  import { ASYNC_PROGRESS_PREFIX } from "./async-progress.js";
20
20
  import {
21
21
  STYLEGUIDE_CONFIG_BASENAME,
@@ -845,6 +845,10 @@ async function gracefulShutdown(signal) {
845
845
  process.exit(0);
846
846
  }
847
847
 
848
+ function maxScenesNextStep(matchedCount) {
849
+ return `Re-run with max_scenes set to at least ${matchedCount}.`;
850
+ }
851
+
848
852
  // ---------------------------------------------------------------------------
849
853
  // MCP server factory
850
854
  // ---------------------------------------------------------------------------
@@ -1226,6 +1230,7 @@ function createMcpServer() {
1226
1230
  matched_scenes: targetScenes.length,
1227
1231
  max_scenes,
1228
1232
  project_id,
1233
+ next_step: maxScenesNextStep(targetScenes.length),
1229
1234
  }
1230
1235
  );
1231
1236
  }
@@ -1485,6 +1490,7 @@ function createMcpServer() {
1485
1490
  config: draft.config,
1486
1491
  inferred_defaults: draft.inferred_defaults,
1487
1492
  warnings: draft.warnings,
1493
+ next_step: "Config created. Call update_prose_styleguide_config to apply field updates.",
1488
1494
  });
1489
1495
  }
1490
1496
  );
@@ -1520,7 +1526,7 @@ function createMcpServer() {
1520
1526
  ok: true,
1521
1527
  styleguide: resolved,
1522
1528
  next_step: resolved.setup_required
1523
- ? "No prose-styleguide.config.yaml was found. Run setup to create one at sync root or project root."
1529
+ ? "No prose-styleguide.config.yaml was found. Call setup_prose_styleguide_config (with language e.g. 'en') to create one at sync root or project root."
1524
1530
  : "Config resolved successfully.",
1525
1531
  });
1526
1532
  }
@@ -1632,7 +1638,12 @@ function createMcpServer() {
1632
1638
  return errorResponse(
1633
1639
  "VALIDATION_ERROR",
1634
1640
  `Matched ${targetScenes.length} scenes, which exceeds max_scenes=${max_scenes}.`,
1635
- { matched_scenes: targetScenes.length, max_scenes, project_id }
1641
+ {
1642
+ matched_scenes: targetScenes.length,
1643
+ max_scenes,
1644
+ project_id,
1645
+ next_step: maxScenesNextStep(targetScenes.length),
1646
+ }
1636
1647
  );
1637
1648
  }
1638
1649
 
@@ -1669,7 +1680,7 @@ function createMcpServer() {
1669
1680
  checked_scenes: sceneSignals.length,
1670
1681
  unreadable_scenes: unreadableScenes,
1671
1682
  suggested_config: suggestedConfig,
1672
- next_step: "Review suggested_config, then write accepted fields via update_prose_styleguide_config.",
1683
+ next_step: `To apply: (1) If no project-scoped config exists yet, call setup_prose_styleguide_config first with scope=project_root, project_id=${project_id}, and language (e.g. 'en'). (2) Then call update_prose_styleguide_config with the fields from suggested_config you want to apply.`,
1673
1684
  scene_signals: include_scene_signals ? sceneSignals : undefined,
1674
1685
  });
1675
1686
  }
@@ -1886,7 +1897,12 @@ function createMcpServer() {
1886
1897
  return errorResponse(
1887
1898
  "VALIDATION_ERROR",
1888
1899
  `Matched ${targetScenes.length} scenes, which exceeds max_scenes=${max_scenes}.`,
1889
- { matched_scenes: targetScenes.length, max_scenes, project_id }
1900
+ {
1901
+ matched_scenes: targetScenes.length,
1902
+ max_scenes,
1903
+ project_id,
1904
+ next_step: maxScenesNextStep(targetScenes.length),
1905
+ }
1890
1906
  );
1891
1907
  }
1892
1908
 
@@ -2511,9 +2527,19 @@ function createMcpServer() {
2511
2527
  if (!SYNC_DIR_WRITABLE) {
2512
2528
  return errorResponse("READ_ONLY", "Cannot create character sheet: sync dir is read-only.");
2513
2529
  }
2514
- if ((project_id && universe_id) || (!project_id && !universe_id)) {
2530
+ const hasProjectId = project_id !== undefined;
2531
+ const hasUniverseId = universe_id !== undefined;
2532
+ if ((hasProjectId && hasUniverseId) || (!hasProjectId && !hasUniverseId)) {
2515
2533
  return errorResponse("VALIDATION_ERROR", "Provide exactly one of project_id or universe_id.");
2516
2534
  }
2535
+ if (hasProjectId) {
2536
+ const check = validateProjectId(project_id);
2537
+ if (!check.ok) return errorResponse("INVALID_PROJECT_ID", check.reason, { project_id });
2538
+ }
2539
+ if (hasUniverseId) {
2540
+ const check = validateUniverseId(universe_id);
2541
+ if (!check.ok) return errorResponse("INVALID_UNIVERSE_ID", check.reason, { universe_id });
2542
+ }
2517
2543
 
2518
2544
  try {
2519
2545
  const result = createCanonicalWorldEntity({
@@ -2575,9 +2601,19 @@ function createMcpServer() {
2575
2601
  if (!SYNC_DIR_WRITABLE) {
2576
2602
  return errorResponse("READ_ONLY", "Cannot create place sheet: sync dir is read-only.");
2577
2603
  }
2578
- if ((project_id && universe_id) || (!project_id && !universe_id)) {
2604
+ const hasProjectId = project_id !== undefined;
2605
+ const hasUniverseId = universe_id !== undefined;
2606
+ if ((hasProjectId && hasUniverseId) || (!hasProjectId && !hasUniverseId)) {
2579
2607
  return errorResponse("VALIDATION_ERROR", "Provide exactly one of project_id or universe_id.");
2580
2608
  }
2609
+ if (hasProjectId) {
2610
+ const check = validateProjectId(project_id);
2611
+ if (!check.ok) return errorResponse("INVALID_PROJECT_ID", check.reason, { project_id });
2612
+ }
2613
+ if (hasUniverseId) {
2614
+ const check = validateUniverseId(universe_id);
2615
+ if (!check.ok) return errorResponse("INVALID_UNIVERSE_ID", check.reason, { universe_id });
2616
+ }
2581
2617
 
2582
2618
  try {
2583
2619
  const result = createCanonicalWorldEntity({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -391,7 +391,12 @@ function prepareStyleguideConfigUpdate({ syncDir, scope, projectId, updates = {}
391
391
  error: {
392
392
  code: "STYLEGUIDE_CONFIG_NOT_FOUND",
393
393
  message: "Cannot update styleguide config because no config exists at the requested scope.",
394
- details: { file_path: filePath, scope, project_id: projectId ?? null },
394
+ details: {
395
+ file_path: filePath,
396
+ scope,
397
+ project_id: projectId ?? null,
398
+ next_step: `Call setup_prose_styleguide_config first (scope=${scope}${projectId ? `, project_id=${projectId}` : ""}, language=<e.g. 'en'>), then retry update_prose_styleguide_config.`,
399
+ },
395
400
  },
396
401
  };
397
402
  }