@hanna84/mcp-writing 3.29.2 → 3.29.3

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,16 @@ 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.29.3](https://github.com/hannasdev/mcp-writing/compare/v3.29.2...v3.29.3)
8
+
9
+ - fix: tighten MCP response contracts [`#242`](https://github.com/hannasdev/mcp-writing/pull/242)
10
+
7
11
  #### [v3.29.2](https://github.com/hannasdev/mcp-writing/compare/v3.29.1...v3.29.2)
8
12
 
13
+ > 11 June 2026
14
+
9
15
  - ci: skip PR template check for Dependabot [`#241`](https://github.com/hannasdev/mcp-writing/pull/241)
16
+ - Release 3.29.2 [`a26b7cc`](https://github.com/hannasdev/mcp-writing/commit/a26b7cc3d66d250fd9f30bd6d9ddcad49187008c)
10
17
 
11
18
  #### [v3.29.1](https://github.com/hannasdev/mcp-writing/compare/v3.29.0...v3.29.1)
12
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.29.2",
3
+ "version": "3.29.3",
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",
@@ -114,6 +114,7 @@ function buildResolutionFailure({ targetKind, input, projectId, universeId, cand
114
114
  input,
115
115
  project_id: projectId,
116
116
  ...(universeId !== undefined ? { universe_id: universeId } : {}),
117
+ suggestions_available: cappedCandidates.length > 0,
117
118
  candidate_matches: cappedCandidates,
118
119
  next_step: nextStepForTargetKind(targetKind),
119
120
  };
package/src/index.js CHANGED
@@ -134,7 +134,10 @@ function paginateRows(rows, { page, pageSize, forcePagination = false }) {
134
134
  }
135
135
 
136
136
  function jsonResponse(payload) {
137
- return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
137
+ return {
138
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
139
+ ...(payload?.ok === false ? { isError: true } : {}),
140
+ };
138
141
  }
139
142
 
140
143
  function errorResponse(code, message, details) {
@@ -612,6 +615,7 @@ function createMcpServer() {
612
615
  async () => {
613
616
  return jsonResponse({
614
617
  server_version: MCP_SERVER_VERSION,
618
+ transport: MCP_TRANSPORT === "stdio" ? "stdio" : "http",
615
619
  sync_dir: SYNC_DIR_ABS,
616
620
  db_path: DB_PATH_DISPLAY,
617
621
  db_migration_warnings: DB_STARTUP_WARNINGS,
package/src/sync/sync.js CHANGED
@@ -1688,7 +1688,7 @@ export function observeOrphanedSidecars(syncDir, { indexedSceneIds = new Set() }
1688
1688
  }
1689
1689
 
1690
1690
  const WARNING_TYPE_LABELS = {
1691
- no_scene_id: "Skipped (no scene_id)",
1691
+ no_scene_id: "Ignored non-manuscript files",
1692
1692
  duplicate_scene_id: "Duplicate scene_id",
1693
1693
  path_metadata_mismatch: "Path/metadata mismatch",
1694
1694
  chapter_structure: "Chapter structure",
@@ -1703,7 +1703,7 @@ const WARNING_TYPE_LABELS = {
1703
1703
  };
1704
1704
 
1705
1705
  const WARNING_PATTERNS = [
1706
- { type: "no_scene_id", re: /^Skipped \(no scene_id\):/ },
1706
+ { type: "no_scene_id", re: /^Ignored non-manuscript file \(no scene_id\):/ },
1707
1707
  { type: "duplicate_scene_id", re: /^Duplicate scene_id/ },
1708
1708
  { type: "path_metadata_mismatch", re: /^Path\/metadata mismatch/ },
1709
1709
  { type: "chapter_structure", re: /^(Chapter structure warning|Epigraph requires explicit chapter linkage|Epigraph references unknown chapter_id|Scene references unknown chapter_id|Ambiguous chapter linkage|Epigraph identity conflict|Managed structure sync ignored|Managed structure sync preserved canonical epigraph)/ },
@@ -1718,6 +1718,7 @@ const WARNING_PATTERNS = [
1718
1718
  ];
1719
1719
 
1720
1720
  const MAX_WARNING_EXAMPLES = 5;
1721
+ const INFO_WARNING_TYPES = new Set(["no_scene_id"]);
1721
1722
 
1722
1723
  export function buildWarningSummary(warnings) {
1723
1724
  const summary = {};
@@ -1725,7 +1726,14 @@ export function buildWarningSummary(warnings) {
1725
1726
  const firstLine = w.split("\n")[0];
1726
1727
  const match = WARNING_PATTERNS.find(p => p.re.test(firstLine));
1727
1728
  const type = match?.type ?? "other";
1728
- if (!summary[type]) summary[type] = { count: 0, examples: [] };
1729
+ if (!summary[type]) {
1730
+ summary[type] = {
1731
+ count: 0,
1732
+ examples: [],
1733
+ severity: INFO_WARNING_TYPES.has(type) ? "info" : "warning",
1734
+ label: WARNING_TYPE_LABELS[type] ?? type,
1735
+ };
1736
+ }
1729
1737
  summary[type].count++;
1730
1738
  if (summary[type].examples.length < MAX_WARNING_EXAMPLES) {
1731
1739
  summary[type].examples.push(firstLine);
@@ -1799,7 +1807,7 @@ export function syncAll(db, syncDir, { quiet = false, writable = false, relation
1799
1807
 
1800
1808
  if (!meta.scene_id && !chapterStructure.isEpigraph) {
1801
1809
  skipped++;
1802
- if (!quiet) warnings.push(`Skipped (no scene_id): ${structureObservation.relativePath}`);
1810
+ if (!quiet) warnings.push(`Ignored non-manuscript file (no scene_id): ${structureObservation.relativePath}`);
1803
1811
  continue;
1804
1812
  }
1805
1813
 
@@ -1921,12 +1929,13 @@ export function syncAll(db, syncDir, { quiet = false, writable = false, relation
1921
1929
  `[mcp-writing] Sync complete: ${indexed} scenes indexed, ${staleMarked} scenes marked stale` +
1922
1930
  (epigraphsIndexed ? `, ${epigraphsIndexed} epigraphs indexed, ${epigraphsStaleMarked} epigraphs marked stale` : "") +
1923
1931
  (sidecarsMigrated ? `, ${sidecarsMigrated} sidecars auto-generated` : "") +
1924
- (skipped ? `, ${skipped} files skipped` : "") + "\n"
1932
+ (skipped ? `, ${skipped} non-manuscript file(s) ignored` : "") + "\n"
1925
1933
  );
1926
1934
  for (const [type, entry] of Object.entries(warningSummary)) {
1927
1935
  const count = entry.count;
1928
1936
  const label = WARNING_TYPE_LABELS[type] ?? type;
1929
- process.stderr.write(`[mcp-writing] WARNING: ${label}: ${count} file(s). First example: ${entry.examples[0]}\n`);
1937
+ const level = entry.severity === "info" ? "INFO" : "WARNING";
1938
+ process.stderr.write(`[mcp-writing] ${level}: ${label}: ${count} file(s). First example: ${entry.examples[0]}\n`);
1930
1939
  }
1931
1940
  }
1932
1941
  return {
@@ -485,13 +485,15 @@ export function registerSearchTools(s, {
485
485
  pageSize: page_size,
486
486
  forcePagination: rows.length > DEFAULT_METADATA_PAGE_SIZE,
487
487
  });
488
+ const resolvedFrom = mergeResolvedFilters(resolvedFilters);
488
489
 
489
490
  const payload = paged.paginated
490
491
  ? {
491
492
  results: paged.rows,
492
493
  ...paged.meta,
493
494
  warning,
494
- resolved_filters: mergeResolvedFilters(resolvedFilters),
495
+ resolved_filters: resolvedFrom,
496
+ resolved_from: resolvedFrom,
495
497
  next_step: staleCount > 0
496
498
  ? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
497
499
  : undefined,
@@ -500,7 +502,8 @@ export function registerSearchTools(s, {
500
502
  results: rows,
501
503
  total_count: rows.length,
502
504
  warning,
503
- resolved_filters: mergeResolvedFilters(resolvedFilters),
505
+ resolved_filters: resolvedFrom,
506
+ resolved_from: resolvedFrom,
504
507
  next_step: staleCount > 0
505
508
  ? "Touch stale scenes as you work and run enrich_scene(scene_id, project_id) to recover metadata parity incrementally."
506
509
  : undefined,
package/src/tools/sync.js CHANGED
@@ -60,14 +60,22 @@ export function registerSyncTools(s, {
60
60
  }
61
61
  if (result.sidecarsMigrated) parts.push(`${result.sidecarsMigrated} sidecar(s) auto-generated from frontmatter.`);
62
62
  if (result.skipped) {
63
- parts.push(`${result.skipped} file(s) skipped (no scene_id).`);
64
- parts.push(`Tip: for raw Scrivener Draft exports, run src/scripts/import.js first, then run sync again.`);
63
+ parts.push(`${result.skipped} non-manuscript file(s) ignored because they did not declare scene_id.`);
64
+ parts.push(`Tip: if these are raw Scrivener Draft scene exports, run src/scripts/import.js first, then run sync again.`);
65
65
  }
66
66
  const summary = result.warningSummary;
67
67
  const summaryEntries = Object.entries(summary);
68
68
  if (summaryEntries.length) {
69
- const lines = summaryEntries.map(([type, entry]) => `- ${type}: ${entry.count} (e.g. ${entry.examples[0]})`);
70
- parts.push(`\n⚠️ Warning summary:\n` + lines.join("\n"));
69
+ const infoEntries = summaryEntries.filter(([, entry]) => entry.severity === "info");
70
+ const warningEntries = summaryEntries.filter(([, entry]) => entry.severity !== "info");
71
+ if (infoEntries.length) {
72
+ const lines = infoEntries.map(([type, entry]) => `- ${entry.label ?? type}: ${entry.count} (e.g. ${entry.examples[0]})`);
73
+ parts.push(`\nIgnored file summary:\n` + lines.join("\n"));
74
+ }
75
+ if (warningEntries.length) {
76
+ const lines = warningEntries.map(([type, entry]) => `- ${entry.label ?? type}: ${entry.count} (e.g. ${entry.examples[0]})`);
77
+ parts.push(`\nWarning summary:\n` + lines.join("\n"));
78
+ }
71
79
  }
72
80
  return { content: [{ type: "text", text: parts.join(" ") }] };
73
81
  });