@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 +7 -0
- package/package.json +1 -1
- package/src/structure/project-backup-restore.js +97 -3
- package/src/tools/sync.js +4 -2
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|