@hanna84/mcp-writing 3.18.0 → 3.19.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.
@@ -28,9 +28,58 @@ import {
28
28
  upsertExplicitReferenceLinkRow,
29
29
  upsertSerializedReferenceLinks,
30
30
  } from "./reference-link-persistence.js";
31
+ import {
32
+ createToolActor,
33
+ refreshProjectBackupAfterMutation,
34
+ } from "../structure/project-backup-refresh.js";
31
35
 
32
36
  const STRUCTURAL_SCENE_METADATA_FIELDS = ["part", "chapter", "chapter_id", "timeline_position"];
33
37
 
38
+ function emptyBackupMutationResult() {
39
+ return {
40
+ operation_history: null,
41
+ backup_refresh: null,
42
+ backup_warnings: [],
43
+ };
44
+ }
45
+
46
+ function refreshProjectScopedBackupAfterMutation(db, {
47
+ syncDir,
48
+ projectId,
49
+ applicationVersion,
50
+ operation,
51
+ actor,
52
+ affected,
53
+ before,
54
+ after,
55
+ summary,
56
+ metadata,
57
+ }) {
58
+ if (!projectId) {
59
+ return emptyBackupMutationResult();
60
+ }
61
+ return refreshProjectBackupAfterMutation(db, {
62
+ syncDir,
63
+ projectId,
64
+ applicationVersion,
65
+ operation,
66
+ actor,
67
+ affected,
68
+ before,
69
+ after,
70
+ summary,
71
+ metadata,
72
+ });
73
+ }
74
+
75
+ function backupMutationFields(backupResult) {
76
+ return {
77
+ operation_history: backupResult.operation_history,
78
+ backup_refresh: backupResult.backup_refresh,
79
+ backup_warnings: backupResult.backup_warnings,
80
+ };
81
+ }
82
+
34
83
  function getProvidedStructuralSceneMetadataFields(fields) {
35
84
  return STRUCTURAL_SCENE_METADATA_FIELDS.filter((field) => Object.hasOwn(fields, field));
36
85
  }
@@ -317,6 +366,7 @@ export function registerMetadataTools(s, {
317
366
  db,
318
367
  SYNC_DIR,
319
368
  SYNC_DIR_WRITABLE,
369
+ MCP_SERVER_VERSION = "0.0.0",
320
370
  errorResponse,
321
371
  jsonResponse,
322
372
  createCanonicalWorldEntity,
@@ -364,8 +414,34 @@ export function registerMetadataTools(s, {
364
414
  universeId: universe_id,
365
415
  meta: fields ?? {},
366
416
  });
417
+ const backupResult = refreshProjectScopedBackupAfterMutation(db, {
418
+ syncDir: SYNC_DIR,
419
+ projectId: project_id,
420
+ applicationVersion: MCP_SERVER_VERSION,
421
+ operation: "create_character_sheet",
422
+ actor: createToolActor("create_character_sheet"),
423
+ affected: {
424
+ characters: [result.id],
425
+ },
426
+ summary: `${result.created ? "Created" : "Reused"} character sheet "${result.id}".`,
427
+ before: null,
428
+ after: {
429
+ character: {
430
+ character_id: result.id,
431
+ project_id: result.project_id,
432
+ universe_id: result.universe_id,
433
+ name,
434
+ },
435
+ },
436
+ });
367
437
 
368
- return jsonResponse({ ok: true, action: result.created ? "created" : "exists", kind: "character", ...result });
438
+ return jsonResponse({
439
+ ok: true,
440
+ action: result.created ? "created" : "exists",
441
+ kind: "character",
442
+ ...result,
443
+ ...backupMutationFields(backupResult),
444
+ });
369
445
  } catch (err) {
370
446
  if (err?.name === "CoreValidationError") {
371
447
  return errorResponse(err.code, err.message, err.details);
@@ -416,8 +492,34 @@ export function registerMetadataTools(s, {
416
492
  universeId: universe_id,
417
493
  meta: fields ?? {},
418
494
  });
495
+ const backupResult = refreshProjectScopedBackupAfterMutation(db, {
496
+ syncDir: SYNC_DIR,
497
+ projectId: project_id,
498
+ applicationVersion: MCP_SERVER_VERSION,
499
+ operation: "create_place_sheet",
500
+ actor: createToolActor("create_place_sheet"),
501
+ affected: {
502
+ places: [result.id],
503
+ },
504
+ summary: `${result.created ? "Created" : "Reused"} place sheet "${result.id}".`,
505
+ before: null,
506
+ after: {
507
+ place: {
508
+ place_id: result.id,
509
+ project_id: result.project_id,
510
+ universe_id: result.universe_id,
511
+ name,
512
+ },
513
+ },
514
+ });
419
515
 
420
- return jsonResponse({ ok: true, action: result.created ? "created" : "exists", kind: "place", ...result });
516
+ return jsonResponse({
517
+ ok: true,
518
+ action: result.created ? "created" : "exists",
519
+ kind: "place",
520
+ ...result,
521
+ ...backupMutationFields(backupResult),
522
+ });
421
523
  } catch (err) {
422
524
  if (err?.name === "CoreValidationError") {
423
525
  return errorResponse(err.code, err.message, err.details);
@@ -443,6 +545,10 @@ export function registerMetadataTools(s, {
443
545
  if (!SYNC_DIR_WRITABLE) {
444
546
  return errorResponse("READ_ONLY", "Cannot write thread links: sync dir is read-only.");
445
547
  }
548
+ const projectIdCheck = validateProjectId(project_id);
549
+ if (!projectIdCheck.ok) {
550
+ return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
551
+ }
446
552
 
447
553
  const existingThread = db.prepare(`SELECT thread_id, project_id FROM threads WHERE thread_id = ?`).get(thread_id);
448
554
  if (existingThread && existingThread.project_id !== project_id) {
@@ -475,12 +581,30 @@ export function registerMetadataTools(s, {
475
581
  const thread = db.prepare(`SELECT * FROM threads WHERE thread_id = ?`).get(thread_id);
476
582
  const link = db.prepare(`SELECT scene_id, project_id, thread_id, beat FROM scene_threads WHERE scene_id = ? AND project_id = ? AND thread_id = ?`)
477
583
  .get(scene_id, project_id, thread_id);
584
+ const backupResult = refreshProjectScopedBackupAfterMutation(db, {
585
+ syncDir: SYNC_DIR,
586
+ projectId: project_id,
587
+ applicationVersion: MCP_SERVER_VERSION,
588
+ operation: "upsert_thread_link",
589
+ actor: createToolActor("upsert_thread_link"),
590
+ affected: {
591
+ threads: [thread_id],
592
+ scenes: [scene_id],
593
+ },
594
+ summary: `Upserted thread "${thread_id}" link for scene "${scene_id}".`,
595
+ before: null,
596
+ after: {
597
+ thread,
598
+ link,
599
+ },
600
+ });
478
601
 
479
602
  return jsonResponse({
480
603
  ok: true,
481
604
  action: "upserted",
482
605
  thread,
483
606
  link,
607
+ ...backupMutationFields(backupResult),
484
608
  });
485
609
  }
486
610
  );
@@ -511,7 +635,7 @@ export function registerMetadataTools(s, {
511
635
  }
512
636
 
513
637
  const targetDoc = db.prepare(`
514
- SELECT doc_id
638
+ SELECT doc_id, project_id
515
639
  FROM reference_docs
516
640
  WHERE doc_id = ?
517
641
  `).get(target_doc_id);
@@ -623,11 +747,29 @@ export function registerMetadataTools(s, {
623
747
  FROM reference_links
624
748
  WHERE source_kind = ? AND source_project_id = ? AND source_id = ? AND target_doc_id = ? AND relation = ?
625
749
  `).get(source_kind, resolvedSourceProjectId, source_id, target_doc_id, normalizedRelation);
750
+ const backupProjectId = resolvedSourceProjectId || targetDoc.project_id || null;
751
+ const backupResult = refreshProjectScopedBackupAfterMutation(db, {
752
+ syncDir: SYNC_DIR,
753
+ projectId: backupProjectId,
754
+ applicationVersion: MCP_SERVER_VERSION,
755
+ operation: "upsert_reference_link",
756
+ actor: createToolActor("upsert_reference_link"),
757
+ affected: {
758
+ reference_docs: [target_doc_id],
759
+ sources: [`${source_kind}:${source_id}`],
760
+ },
761
+ summary: `Upserted ${source_kind} reference link from "${source_id}" to "${target_doc_id}".`,
762
+ before: null,
763
+ after: {
764
+ link,
765
+ },
766
+ });
626
767
 
627
768
  return jsonResponse({
628
769
  ok: true,
629
770
  action: "upserted",
630
771
  link,
772
+ ...backupMutationFields(backupResult),
631
773
  });
632
774
  }
633
775
  );
@@ -683,6 +825,29 @@ export function registerMetadataTools(s, {
683
825
  return errorResponse("IO_ERROR", `Failed to create chapter '${plan.chapter.chapter_id}': ${err.message}`);
684
826
  }
685
827
 
828
+ const backupResult = refreshProjectBackupAfterMutation(db, {
829
+ syncDir: SYNC_DIR,
830
+ projectId: project_id,
831
+ applicationVersion: MCP_SERVER_VERSION,
832
+ operation: "create_chapter",
833
+ actor: createToolActor("create_chapter"),
834
+ affected: {
835
+ chapters: [plan.chapter.chapter_id],
836
+ },
837
+ summary: `Created chapter "${plan.chapter.title}" at sort_index ${plan.chapter.sort_index}.`,
838
+ before: null,
839
+ after: {
840
+ chapter: {
841
+ chapter_id: plan.chapter.chapter_id,
842
+ project_id: plan.chapter.project_id,
843
+ title: plan.chapter.title,
844
+ sort_index: plan.chapter.sort_index,
845
+ logline: plan.chapter.logline,
846
+ metadata_stale: plan.chapter.metadata_stale,
847
+ },
848
+ },
849
+ });
850
+
686
851
  return jsonResponse({
687
852
  ok: true,
688
853
  action: "created",
@@ -695,6 +860,9 @@ export function registerMetadataTools(s, {
695
860
  metadata_stale: plan.chapter.metadata_stale,
696
861
  },
697
862
  diagnostics: plan.diagnostics,
863
+ operation_history: backupResult.operation_history,
864
+ backup_refresh: backupResult.backup_refresh,
865
+ backup_warnings: backupResult.backup_warnings,
698
866
  next_steps: [
699
867
  "Use assign_scene_to_chapter to place unchaptered scenes in this chapter.",
700
868
  "Run diagnose_structure if existing folders or sidecars may imply conflicting structure.",
@@ -781,6 +949,39 @@ export function registerMetadataTools(s, {
781
949
  failureCode: "SCENE_SIDECAR_UPDATE_FAILED",
782
950
  syncDir: SYNC_DIR,
783
951
  });
952
+ const backupResult = refreshProjectBackupAfterMutation(db, {
953
+ syncDir: SYNC_DIR,
954
+ projectId: project_id,
955
+ applicationVersion: MCP_SERVER_VERSION,
956
+ operation: "rename_chapter",
957
+ actor: createToolActor("rename_chapter"),
958
+ affected: {
959
+ chapters: [plan.chapter.chapter_id],
960
+ scenes: linkedScenes.map(scene => scene.scene_id),
961
+ },
962
+ summary: `Renamed chapter "${plan.previousChapter.title}" to "${plan.chapter.title}".`,
963
+ before: {
964
+ chapter: {
965
+ chapter_id: plan.previousChapter.chapter_id,
966
+ title: plan.previousChapter.title,
967
+ sort_index: plan.previousChapter.sort_index,
968
+ },
969
+ },
970
+ after: {
971
+ chapter: {
972
+ chapter_id: plan.chapter.chapter_id,
973
+ project_id: plan.chapter.project_id,
974
+ title: plan.chapter.title,
975
+ sort_index: plan.chapter.sort_index,
976
+ logline: plan.chapter.logline,
977
+ metadata_stale: plan.chapter.metadata_stale,
978
+ },
979
+ },
980
+ metadata: {
981
+ updated_scene_count: linkedScenes.length,
982
+ updated_sidecar_count: sidecarWriteResult.updatedCount,
983
+ },
984
+ });
784
985
 
785
986
  return jsonResponse({
786
987
  ok: true,
@@ -800,6 +1001,9 @@ export function registerMetadataTools(s, {
800
1001
  ...plan.diagnostics,
801
1002
  ...sidecarWriteResult.diagnostics,
802
1003
  ],
1004
+ operation_history: backupResult.operation_history,
1005
+ backup_refresh: backupResult.backup_refresh,
1006
+ backup_warnings: backupResult.backup_warnings,
803
1007
  next_steps: [
804
1008
  "Use list_chapters to confirm the canonical title.",
805
1009
  "Run diagnose_structure if folder-derived structure may still use the previous chapter title.",
@@ -887,6 +1091,39 @@ export function registerMetadataTools(s, {
887
1091
  failureCode: "SCENE_SIDECAR_UPDATE_FAILED",
888
1092
  syncDir: SYNC_DIR,
889
1093
  });
1094
+ const backupResult = refreshProjectBackupAfterMutation(db, {
1095
+ syncDir: SYNC_DIR,
1096
+ projectId: project_id,
1097
+ applicationVersion: MCP_SERVER_VERSION,
1098
+ operation: "reorder_chapter",
1099
+ actor: createToolActor("reorder_chapter"),
1100
+ affected: {
1101
+ chapters: [plan.chapter.chapter_id],
1102
+ scenes: linkedScenes.map(scene => scene.scene_id),
1103
+ },
1104
+ summary: `Reordered chapter "${plan.chapter.title}" from ${plan.previousChapter.sort_index} to ${plan.chapter.sort_index}.`,
1105
+ before: {
1106
+ chapter: {
1107
+ chapter_id: plan.previousChapter.chapter_id,
1108
+ title: plan.previousChapter.title,
1109
+ sort_index: plan.previousChapter.sort_index,
1110
+ },
1111
+ },
1112
+ after: {
1113
+ chapter: {
1114
+ chapter_id: plan.chapter.chapter_id,
1115
+ project_id: plan.chapter.project_id,
1116
+ title: plan.chapter.title,
1117
+ sort_index: plan.chapter.sort_index,
1118
+ logline: plan.chapter.logline,
1119
+ metadata_stale: plan.chapter.metadata_stale,
1120
+ },
1121
+ },
1122
+ metadata: {
1123
+ updated_scene_count: linkedScenes.length,
1124
+ updated_sidecar_count: sidecarWriteResult.updatedCount,
1125
+ },
1126
+ });
890
1127
 
891
1128
  return jsonResponse({
892
1129
  ok: true,
@@ -906,6 +1143,9 @@ export function registerMetadataTools(s, {
906
1143
  ...plan.diagnostics,
907
1144
  ...sidecarWriteResult.diagnostics,
908
1145
  ],
1146
+ operation_history: backupResult.operation_history,
1147
+ backup_refresh: backupResult.backup_refresh,
1148
+ backup_warnings: backupResult.backup_warnings,
909
1149
  next_steps: [
910
1150
  "Use list_chapters to confirm canonical order.",
911
1151
  "Run diagnose_structure if folder-derived structure may still use the previous order.",
@@ -969,6 +1209,40 @@ export function registerMetadataTools(s, {
969
1209
  failureCode: "EPIGRAPH_SIDECAR_UPDATE_FAILED",
970
1210
  syncDir: SYNC_DIR,
971
1211
  });
1212
+ const backupResult = refreshProjectBackupAfterMutation(db, {
1213
+ syncDir: SYNC_DIR,
1214
+ projectId: project_id,
1215
+ applicationVersion: MCP_SERVER_VERSION,
1216
+ operation: "attach_epigraph",
1217
+ actor: createToolActor("attach_epigraph"),
1218
+ affected: {
1219
+ epigraphs: [plan.epigraph.epigraph_id],
1220
+ chapters: [plan.chapter.chapter_id],
1221
+ },
1222
+ summary: `Attached epigraph "${plan.epigraph.epigraph_id}" to chapter "${plan.chapter.title}".`,
1223
+ before: {
1224
+ epigraph: {
1225
+ epigraph_id: plan.epigraph.epigraph_id,
1226
+ chapter_id: plan.previousChapter?.chapter_id ?? null,
1227
+ },
1228
+ },
1229
+ after: {
1230
+ epigraph: {
1231
+ epigraph_id: plan.epigraph.epigraph_id,
1232
+ project_id: plan.epigraph.project_id,
1233
+ chapter_id: plan.epigraph.chapter_id,
1234
+ metadata_stale: plan.epigraph.metadata_stale,
1235
+ },
1236
+ chapter: {
1237
+ chapter_id: plan.chapter.chapter_id,
1238
+ title: plan.chapter.title,
1239
+ sort_index: plan.chapter.sort_index,
1240
+ },
1241
+ },
1242
+ metadata: {
1243
+ updated_sidecar_count: sidecarWriteResult.updatedCount,
1244
+ },
1245
+ });
972
1246
 
973
1247
  return jsonResponse({
974
1248
  ok: true,
@@ -996,6 +1270,9 @@ export function registerMetadataTools(s, {
996
1270
  ...plan.diagnostics,
997
1271
  ...sidecarWriteResult.diagnostics,
998
1272
  ],
1273
+ operation_history: backupResult.operation_history,
1274
+ backup_refresh: backupResult.backup_refresh,
1275
+ backup_warnings: backupResult.backup_warnings,
999
1276
  next_steps: [
1000
1277
  "Use find_epigraphs to confirm the canonical epigraph attachment.",
1001
1278
  "Run diagnose_structure if folder-derived structure may still imply the previous chapter.",
@@ -1146,6 +1423,40 @@ export function registerMetadataTools(s, {
1146
1423
  managedStructure: isManagedStructureProject(db, project_id),
1147
1424
  });
1148
1425
  }
1426
+ const backupResult = refreshProjectBackupAfterMutation(db, {
1427
+ syncDir: SYNC_DIR,
1428
+ projectId: project_id,
1429
+ applicationVersion: MCP_SERVER_VERSION,
1430
+ operation: "move_scene",
1431
+ actor: createToolActor("move_scene"),
1432
+ affected: {
1433
+ scenes: [scene_id],
1434
+ chapters: [
1435
+ plan.previousChapterId ?? null,
1436
+ plan.assignedChapter?.chapter_id ?? null,
1437
+ ].filter(Boolean),
1438
+ },
1439
+ summary: `Moved scene "${scene_id}" to ${plan.assignedChapter?.chapter_id ?? "no chapter"} at timeline_position ${plan.timelinePosition ?? "unchanged"}.`,
1440
+ before: {
1441
+ scene: {
1442
+ scene_id,
1443
+ project_id,
1444
+ chapter_id: plan.previousChapterId ?? null,
1445
+ timeline_position: plan.previousTimelinePosition ?? null,
1446
+ },
1447
+ },
1448
+ after: {
1449
+ scene: {
1450
+ scene_id,
1451
+ project_id,
1452
+ chapter_id: plan.assignedChapter?.chapter_id ?? null,
1453
+ timeline_position: plan.timelinePosition ?? null,
1454
+ },
1455
+ },
1456
+ metadata: {
1457
+ updated_sidecar_count: sidecarMirror.updatedCount,
1458
+ },
1459
+ });
1149
1460
 
1150
1461
  return jsonResponse({
1151
1462
  ok: true,
@@ -1169,6 +1480,9 @@ export function registerMetadataTools(s, {
1169
1480
  },
1170
1481
  ...sidecarMirror.diagnostics,
1171
1482
  ],
1483
+ operation_history: backupResult.operation_history,
1484
+ backup_refresh: backupResult.backup_refresh,
1485
+ backup_warnings: backupResult.backup_warnings,
1172
1486
  next_steps: [
1173
1487
  "Use find_scenes to confirm the scene's canonical chapter and timeline_position.",
1174
1488
  "Run diagnose_structure if folder-derived structure may still imply the previous placement.",
@@ -1264,6 +1578,40 @@ export function registerMetadataTools(s, {
1264
1578
  managedStructure: isManagedStructureProject(db, project_id),
1265
1579
  });
1266
1580
  }
1581
+ const backupResult = refreshProjectBackupAfterMutation(db, {
1582
+ syncDir: SYNC_DIR,
1583
+ projectId: project_id,
1584
+ applicationVersion: MCP_SERVER_VERSION,
1585
+ operation: "assign_scene_to_chapter",
1586
+ actor: createToolActor("assign_scene_to_chapter"),
1587
+ affected: {
1588
+ scenes: [scene_id],
1589
+ chapters: [
1590
+ plan.previousChapterId ?? scene.chapter_id ?? null,
1591
+ plan.assignedChapter?.chapter_id ?? null,
1592
+ ].filter(Boolean),
1593
+ },
1594
+ summary: chapter === null
1595
+ ? `Cleared chapter assignment for scene "${scene_id}".`
1596
+ : `Assigned scene "${scene_id}" to chapter "${plan.assignedChapter.chapter_id}".`,
1597
+ before: {
1598
+ scene: {
1599
+ scene_id,
1600
+ project_id,
1601
+ chapter_id: plan.previousChapterId ?? scene.chapter_id ?? null,
1602
+ },
1603
+ },
1604
+ after: {
1605
+ scene: {
1606
+ scene_id,
1607
+ project_id,
1608
+ chapter_id: plan.assignedChapter?.chapter_id ?? null,
1609
+ },
1610
+ },
1611
+ metadata: {
1612
+ updated_sidecar_count: sidecarMirror.updatedCount,
1613
+ },
1614
+ });
1267
1615
 
1268
1616
  return jsonResponse({
1269
1617
  ok: true,
@@ -1274,6 +1622,9 @@ export function registerMetadataTools(s, {
1274
1622
  chapter: plan.assignedChapter,
1275
1623
  updated_sidecar_count: sidecarMirror.updatedCount,
1276
1624
  diagnostics: sidecarMirror.diagnostics,
1625
+ operation_history: backupResult.operation_history,
1626
+ backup_refresh: backupResult.backup_refresh,
1627
+ backup_warnings: backupResult.backup_warnings,
1277
1628
  });
1278
1629
  } catch (err) {
1279
1630
  if (err?.name === "CoreValidationError") {
@@ -1341,8 +1692,46 @@ export function registerMetadataTools(s, {
1341
1692
  indexSceneFile(db, SYNC_DIR, scene.file_path, updated, prose, {
1342
1693
  managedStructure: isManagedStructureProject(db, project_id),
1343
1694
  });
1695
+ const backupResult = refreshProjectScopedBackupAfterMutation(db, {
1696
+ syncDir: SYNC_DIR,
1697
+ projectId: project_id,
1698
+ applicationVersion: MCP_SERVER_VERSION,
1699
+ operation: "update_scene_metadata",
1700
+ actor: createToolActor("update_scene_metadata"),
1701
+ affected: {
1702
+ scenes: [scene_id],
1703
+ },
1704
+ summary: `Updated metadata for scene "${scene_id}".`,
1705
+ before: {
1706
+ scene: {
1707
+ scene_id,
1708
+ project_id,
1709
+ fields: Object.keys(fields).sort().reduce((acc, key) => {
1710
+ acc[key] = meta[key] ?? null;
1711
+ return acc;
1712
+ }, {}),
1713
+ },
1714
+ },
1715
+ after: {
1716
+ scene: {
1717
+ scene_id,
1718
+ project_id,
1719
+ fields: Object.keys(fields).sort().reduce((acc, key) => {
1720
+ acc[key] = updated[key] ?? null;
1721
+ return acc;
1722
+ }, {}),
1723
+ },
1724
+ },
1725
+ });
1344
1726
 
1345
- return { content: [{ type: "text", text: `Updated metadata for scene '${scene_id}'.` }] };
1727
+ return jsonResponse({
1728
+ ok: true,
1729
+ action: "updated",
1730
+ message: `Updated metadata for scene '${scene_id}'.`,
1731
+ scene_id,
1732
+ project_id,
1733
+ ...backupMutationFields(backupResult),
1734
+ });
1346
1735
  } catch (err) {
1347
1736
  if (err.code === "ENOENT") {
1348
1737
  return errorResponse("STALE_PATH", `Prose file for scene '${scene_id}' not found at indexed path — the file may have moved. Run sync() to refresh.`, { indexed_path: scene.file_path });
@@ -1370,7 +1759,7 @@ export function registerMetadataTools(s, {
1370
1759
  if (!SYNC_DIR_WRITABLE) {
1371
1760
  return errorResponse("READ_ONLY", "Cannot update character: sync dir is read-only.");
1372
1761
  }
1373
- const char = db.prepare(`SELECT file_path FROM characters WHERE character_id = ?`).get(character_id);
1762
+ const char = db.prepare(`SELECT character_id, project_id, universe_id, file_path, name, role, arc_summary, first_appearance FROM characters WHERE character_id = ?`).get(character_id);
1374
1763
  if (!char) {
1375
1764
  return errorResponse("NOT_FOUND", `Character '${character_id}' not found.`);
1376
1765
  }
@@ -1393,8 +1782,47 @@ export function registerMetadataTools(s, {
1393
1782
  db.prepare(`INSERT OR IGNORE INTO character_traits (character_id, trait) VALUES (?, ?)`).run(character_id, t);
1394
1783
  }
1395
1784
  }
1785
+ const backupResult = refreshProjectScopedBackupAfterMutation(db, {
1786
+ syncDir: SYNC_DIR,
1787
+ projectId: char.project_id,
1788
+ applicationVersion: MCP_SERVER_VERSION,
1789
+ operation: "update_character_sheet",
1790
+ actor: createToolActor("update_character_sheet"),
1791
+ affected: {
1792
+ characters: [character_id],
1793
+ },
1794
+ summary: `Updated character sheet "${character_id}".`,
1795
+ before: {
1796
+ character: {
1797
+ character_id,
1798
+ project_id: char.project_id,
1799
+ universe_id: char.universe_id,
1800
+ name: char.name,
1801
+ role: char.role,
1802
+ arc_summary: char.arc_summary,
1803
+ first_appearance: char.first_appearance,
1804
+ },
1805
+ },
1806
+ after: {
1807
+ character: {
1808
+ character_id,
1809
+ project_id: char.project_id,
1810
+ universe_id: char.universe_id,
1811
+ name: updated.name ?? meta.name ?? null,
1812
+ role: updated.role ?? null,
1813
+ arc_summary: updated.arc_summary ?? null,
1814
+ first_appearance: updated.first_appearance ?? null,
1815
+ },
1816
+ },
1817
+ });
1396
1818
 
1397
- return { content: [{ type: "text", text: `Updated character sheet for '${character_id}'.` }] };
1819
+ return jsonResponse({
1820
+ ok: true,
1821
+ action: "updated",
1822
+ message: `Updated character sheet for '${character_id}'.`,
1823
+ character_id,
1824
+ ...backupMutationFields(backupResult),
1825
+ });
1398
1826
  } catch (err) {
1399
1827
  if (err?.name === "CoreValidationError") {
1400
1828
  return errorResponse(err.code, err.message, err.details);
@@ -1423,7 +1851,7 @@ export function registerMetadataTools(s, {
1423
1851
  if (!SYNC_DIR_WRITABLE) {
1424
1852
  return errorResponse("READ_ONLY", "Cannot update place: sync dir is read-only.");
1425
1853
  }
1426
- const place = db.prepare(`SELECT file_path FROM places WHERE place_id = ?`).get(place_id);
1854
+ const place = db.prepare(`SELECT place_id, project_id, universe_id, file_path, name FROM places WHERE place_id = ?`).get(place_id);
1427
1855
  if (!place) {
1428
1856
  return errorResponse("NOT_FOUND", `Place '${place_id}' not found.`);
1429
1857
  }
@@ -1434,8 +1862,41 @@ export function registerMetadataTools(s, {
1434
1862
 
1435
1863
  db.prepare(`UPDATE places SET name = ? WHERE place_id = ?`)
1436
1864
  .run(updated.name ?? meta.name ?? place_id, place_id);
1865
+ const backupResult = refreshProjectScopedBackupAfterMutation(db, {
1866
+ syncDir: SYNC_DIR,
1867
+ projectId: place.project_id,
1868
+ applicationVersion: MCP_SERVER_VERSION,
1869
+ operation: "update_place_sheet",
1870
+ actor: createToolActor("update_place_sheet"),
1871
+ affected: {
1872
+ places: [place_id],
1873
+ },
1874
+ summary: `Updated place sheet "${place_id}".`,
1875
+ before: {
1876
+ place: {
1877
+ place_id,
1878
+ project_id: place.project_id,
1879
+ universe_id: place.universe_id,
1880
+ name: place.name,
1881
+ },
1882
+ },
1883
+ after: {
1884
+ place: {
1885
+ place_id,
1886
+ project_id: place.project_id,
1887
+ universe_id: place.universe_id,
1888
+ name: updated.name ?? meta.name ?? place_id,
1889
+ },
1890
+ },
1891
+ });
1437
1892
 
1438
- return { content: [{ type: "text", text: `Updated place sheet for '${place_id}'.` }] };
1893
+ return jsonResponse({
1894
+ ok: true,
1895
+ action: "updated",
1896
+ message: `Updated place sheet for '${place_id}'.`,
1897
+ place_id,
1898
+ ...backupMutationFields(backupResult),
1899
+ });
1439
1900
  } catch (err) {
1440
1901
  if (err?.name === "CoreValidationError") {
1441
1902
  return errorResponse(err.code, err.message, err.details);