@hanna84/mcp-writing 3.27.0 → 3.28.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,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.28.0](https://github.com/hannasdev/mcp-writing/compare/v3.27.0...v3.28.0)
8
+
9
+ - feat: make restore plans easier to scan [`#238`](https://github.com/hannasdev/mcp-writing/pull/238)
10
+
7
11
  #### [v3.27.0](https://github.com/hannasdev/mcp-writing/compare/v3.26.0...v3.27.0)
8
12
 
13
+ > 6 June 2026
14
+
9
15
  - feat(metadata): accept forgiving relationship evidence inputs [`#237`](https://github.com/hannasdev/mcp-writing/pull/237)
16
+ - Release 3.27.0 [`ef5423b`](https://github.com/hannasdev/mcp-writing/commit/ef5423b044274b75bf60327772c9a944b94123c1)
10
17
 
11
18
  #### [v3.26.0](https://github.com/hannasdev/mcp-writing/compare/v3.25.0...v3.26.0)
12
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.27.0",
3
+ "version": "3.28.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",
@@ -873,6 +873,88 @@ function buildRestorePlan(currentSnapshot, backupSnapshot, { projectId }) {
873
873
  };
874
874
  }
875
875
 
876
+ function buildPlanSummary(plan) {
877
+ return {
878
+ totals: { ...plan.totals },
879
+ by_domain: plan.by_domain,
880
+ destructive_change_count: plan.destructive_change_count,
881
+ cross_scope_change_count: plan.cross_scope_change_count,
882
+ has_destructive_changes: plan.destructive_change_count > 0,
883
+ has_cross_scope_changes: plan.cross_scope_change_count > 0,
884
+ requires_destructive_confirmation: plan.destructive_change_count > 0,
885
+ requires_cross_scope_confirmation: plan.cross_scope_change_count > 0,
886
+ };
887
+ }
888
+
889
+ function buildPlanDetailPolicy({ includeUnchanged, omittedUnchangedChangeCount }) {
890
+ return {
891
+ include_unchanged: includeUnchanged,
892
+ unchanged_rows_included: includeUnchanged,
893
+ omitted_unchanged_change_count: omittedUnchangedChangeCount,
894
+ full_plan_available: true,
895
+ full_plan_next_step: includeUnchanged
896
+ ? "This response includes unchanged rows. Pass include_unchanged=false to suppress unchanged row details while keeping plan_summary counts."
897
+ : "Rerun the restore plan with include_unchanged=true or omit include_unchanged to retrieve unchanged row details.",
898
+ };
899
+ }
900
+
901
+ function applyPlanDetailPolicy(plan, { includeUnchanged }) {
902
+ if (includeUnchanged) {
903
+ return {
904
+ plan,
905
+ planDetailPolicy: buildPlanDetailPolicy({
906
+ includeUnchanged,
907
+ omittedUnchangedChangeCount: 0,
908
+ }),
909
+ };
910
+ }
911
+
912
+ const visibleChanges = [];
913
+ let omittedUnchangedChangeCount = 0;
914
+ for (const change of plan.changes) {
915
+ if (change.action === "unchanged") {
916
+ omittedUnchangedChangeCount += 1;
917
+ } else {
918
+ visibleChanges.push(change);
919
+ }
920
+ }
921
+
922
+ return {
923
+ plan: {
924
+ ...plan,
925
+ changes: visibleChanges,
926
+ },
927
+ planDetailPolicy: buildPlanDetailPolicy({
928
+ includeUnchanged,
929
+ omittedUnchangedChangeCount,
930
+ }),
931
+ };
932
+ }
933
+
934
+ const BLOCKING_REQUIREMENT_PRIORITY = new Map([
935
+ ["project_restore_current_snapshot_confirmation_required", 0],
936
+ ["project_restore_current_snapshot_changed", 1],
937
+ ["project_restore_destructive_confirmation_required", 2],
938
+ ["project_restore_cross_scope_confirmation_required", 3],
939
+ ]);
940
+
941
+ function buildBlockingRequirements(diagnostics) {
942
+ return [...diagnostics]
943
+ .sort((left, right) => {
944
+ const priorityDelta =
945
+ (BLOCKING_REQUIREMENT_PRIORITY.get(left.type) ?? 99) -
946
+ (BLOCKING_REQUIREMENT_PRIORITY.get(right.type) ?? 99);
947
+ if (priorityDelta !== 0) return priorityDelta;
948
+ return left.type.localeCompare(right.type);
949
+ })
950
+ .map(diagnostic => ({
951
+ type: diagnostic.type,
952
+ message: diagnostic.message,
953
+ next_step: diagnostic.next_step ?? null,
954
+ details: diagnostic.details,
955
+ }));
956
+ }
957
+
876
958
  function placeholders(values) {
877
959
  return values.map(() => "?").join(",") || "NULL";
878
960
  }
@@ -1059,6 +1141,7 @@ export function restoreProjectFromBackup(db, {
1059
1141
  confirmDestructive = false,
1060
1142
  confirmCrossScope = false,
1061
1143
  expectedCurrentSnapshotChecksum = null,
1144
+ includeUnchanged = true,
1062
1145
  applicationVersion = "0.0.0",
1063
1146
  } = {}) {
1064
1147
  const resolvedBackupDir = resolveBackupDir(backupPath ?? path.join(syncDir, "project-backups", projectId));
@@ -1150,6 +1233,10 @@ export function restoreProjectFromBackup(db, {
1150
1233
  }
1151
1234
 
1152
1235
  const plan = buildRestorePlan(current.snapshot, snapshot, { projectId });
1236
+ const planSummary = buildPlanSummary(plan);
1237
+ const { plan: responsePlan, planDetailPolicy } = applyPlanDetailPolicy(plan, {
1238
+ includeUnchanged: includeUnchanged !== false,
1239
+ });
1153
1240
  const applyDiagnostics = [];
1154
1241
  if (dryRun === false) {
1155
1242
  if (typeof expectedCurrentSnapshotChecksum !== "string" || expectedCurrentSnapshotChecksum === "") {
@@ -1222,8 +1309,11 @@ export function restoreProjectFromBackup(db, {
1222
1309
  dry_run: Boolean(dryRun),
1223
1310
  project_id: projectId,
1224
1311
  backup_dir: resolvedBackupDir,
1312
+ blocking_requirements: buildBlockingRequirements(applyDiagnostics),
1225
1313
  diagnostics: applyDiagnostics,
1226
- plan,
1314
+ plan_summary: planSummary,
1315
+ plan_detail_policy: planDetailPolicy,
1316
+ plan: responsePlan,
1227
1317
  next_step: "Resolve confirmation requirements before applying this trusted backup.",
1228
1318
  };
1229
1319
  }
@@ -1259,7 +1349,9 @@ export function restoreProjectFromBackup(db, {
1259
1349
  },
1260
1350
  { severity: "error", nextStep: "Review the database error and retry after resolving conflicts." }
1261
1351
  )],
1262
- plan,
1352
+ plan_summary: planSummary,
1353
+ plan_detail_policy: planDetailPolicy,
1354
+ plan: responsePlan,
1263
1355
  next_step: "Resolve restore write diagnostics before retrying.",
1264
1356
  };
1265
1357
  }
@@ -1279,7 +1371,9 @@ export function restoreProjectFromBackup(db, {
1279
1371
  },
1280
1372
  current_snapshot_checksum: current.checksum,
1281
1373
  backup_snapshot_checksum: manifest.checksums.canonical_snapshot_sha256,
1282
- plan,
1374
+ plan_summary: planSummary,
1375
+ plan_detail_policy: planDetailPolicy,
1376
+ plan: responsePlan,
1283
1377
  applied: dryRun ? null : {
1284
1378
  restored: true,
1285
1379
  destructive_confirmed: Boolean(confirmDestructive),
package/src/tools/sync.js CHANGED
@@ -338,7 +338,7 @@ export function registerSyncTools(s, {
338
338
 
339
339
  s.tool(
340
340
  "restore_project_from_backup",
341
- "Explicitly restore a project from a trusted generated project backup bundle. Defaults to dry-run planning; dry_run=false applies canonical SQLite changes transactionally after the reviewed current snapshot checksum and required destructive or cross-scope confirmations are provided.",
341
+ "Explicitly restore a project from a trusted generated project backup bundle. Defaults to dry-run planning; dry_run=false applies canonical SQLite changes transactionally after the reviewed current snapshot checksum and required destructive or cross-scope confirmations are provided. Restore plan and confirmation-refusal responses include compact plan_summary details; confirmation refusals include blocking_requirements. include_unchanged=false suppresses unchanged row details while preserving summary counts.",
342
342
  {
343
343
  project_id: z.string().describe("Project ID to restore (e.g. 'test-novel' or 'universe-1/book-1-the-lamb')."),
344
344
  backup_path: z.string().optional().describe("Path under WRITING_SYNC_DIR to a project backup directory, manifest.json, or canonical.snapshot.json. Defaults to project-backups/<project_id>."),
@@ -346,8 +346,9 @@ export function registerSyncTools(s, {
346
346
  confirm_destructive: z.boolean().optional().describe("Required with dry_run=false when the restore plan includes delete candidates."),
347
347
  confirm_cross_scope: z.boolean().optional().describe("Required with dry_run=false when the restore plan changes universe-scoped records."),
348
348
  expected_current_snapshot_checksum: z.string().optional().describe("Required with dry_run=false; pass the current_snapshot_checksum returned by the reviewed dry-run plan to guard against state changes before apply."),
349
+ include_unchanged: z.boolean().optional().describe("If false, suppress unchanged rows from plan.changes while preserving plan_summary counts. Defaults to true for compatibility."),
349
350
  },
350
- async ({ project_id, backup_path, dry_run = true, confirm_destructive = false, confirm_cross_scope = false, expected_current_snapshot_checksum = null } = {}) => {
351
+ async ({ project_id, backup_path, dry_run = true, confirm_destructive = false, confirm_cross_scope = false, expected_current_snapshot_checksum = null, include_unchanged = true } = {}) => {
351
352
  if (!SYNC_DIR_WRITABLE && dry_run === false) {
352
353
  return errorResponse("READ_ONLY", "Cannot restore project from backup: server is in read-only mode for canonical structure mutations.");
353
354
  }
@@ -374,6 +375,7 @@ export function registerSyncTools(s, {
374
375
  confirmDestructive: confirm_destructive,
375
376
  confirmCrossScope: confirm_cross_scope,
376
377
  expectedCurrentSnapshotChecksum: expected_current_snapshot_checksum,
378
+ includeUnchanged: include_unchanged,
377
379
  applicationVersion: MCP_SERVER_VERSION,
378
380
  }));
379
381
  } catch (error) {