@hanna84/mcp-writing 1.12.0 → 1.13.1

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
+ #### [v1.13.1](https://github.com/hannasdev/mcp-writing.git
8
+ /compare/v1.13.0...v1.13.1)
9
+
10
+ - fix(mcp): report server version from package metadata [`#70`](https://github.com/hannasdev/mcp-writing.git
11
+ /pull/70)
12
+
13
+ #### [v1.13.0](https://github.com/hannasdev/mcp-writing.git
14
+ /compare/v1.12.0...v1.13.0)
15
+
16
+ > 24 April 2026
17
+
18
+ - feat(scrivener-direct): add ambiguity warning taxonomy for beta merge [`#69`](https://github.com/hannasdev/mcp-writing.git
19
+ /pull/69)
20
+ - Release 1.13.0 [`d46f4bd`](https://github.com/hannasdev/mcp-writing.git
21
+ /commit/d46f4bdbb7c92a1d5b43ae8ed0afdf76a7a2cb62)
22
+
7
23
  #### [v1.12.0](https://github.com/hannasdev/mcp-writing.git
8
24
  /compare/v1.11.8...v1.12.0)
9
25
 
26
+ > 24 April 2026
27
+
10
28
  - feat(scrivener-direct): ownership enforcement (PR-3a) [`#68`](https://github.com/hannasdev/mcp-writing.git
11
29
  /pull/68)
30
+ - Release 1.12.0 [`e249edd`](https://github.com/hannasdev/mcp-writing.git
31
+ /commit/e249edd1df6b1f77253200296057fe1cb468d2d0)
12
32
 
13
33
  #### [v1.11.8](https://github.com/hannasdev/mcp-writing.git
14
34
  /compare/v1.11.7...v1.11.8)
package/index.js CHANGED
@@ -78,6 +78,10 @@ const OWNERSHIP_GUARD_MODE = OWNERSHIP_GUARD_MODE_RAW === "fail" || OWNERSHIP_GU
78
78
  const OWNERSHIP_GUARD_MODE_RAW_DISPLAY = JSON.stringify(OWNERSHIP_GUARD_MODE_RAW);
79
79
  const __filename = fileURLToPath(import.meta.url);
80
80
  const __dirname = path.dirname(__filename);
81
+ const pkg = readJsonIfExists(path.join(__dirname, "package.json")) ?? {};
82
+ const MCP_SERVER_VERSION = typeof pkg.version === "string" && pkg.version.trim()
83
+ ? pkg.version
84
+ : "0.0.0";
81
85
  const asyncJobs = new Map();
82
86
 
83
87
  function pruneAsyncJobs() {
@@ -753,7 +757,7 @@ async function gracefulShutdown(signal) {
753
757
  // MCP server factory
754
758
  // ---------------------------------------------------------------------------
755
759
  function createMcpServer() {
756
- const s = new McpServer({ name: "mcp-writing", version: "0.1.0" });
760
+ const s = new McpServer({ name: "mcp-writing", version: MCP_SERVER_VERSION });
757
761
 
758
762
  // ---- sync ----------------------------------------------------------------
759
763
  s.tool("sync", "Re-scan the sync folder and update the scene/character/place index from disk. Call this after making edits in Scrivener or updating sidecar files outside the MCP.", {}, async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
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",
@@ -126,6 +126,73 @@ const KNOWN_CUSTOM_FIELD_IDS = new Set([
126
126
  ]);
127
127
 
128
128
  const MAX_RETURNED_WARNINGS = 25;
129
+ const STRUCTURE_MAPPING_FIELDS = new Set(["part", "chapter", "chapter_title"]);
130
+
131
+ function normalizeComparisonValue(key, value) {
132
+ if ((key === "tags" || key === "scene_functions") && Array.isArray(value)) {
133
+ return [...new Set(value.map(item => String(item)))].sort();
134
+ }
135
+ return value;
136
+ }
137
+
138
+ function valuesEquivalentForMerge(key, a, b) {
139
+ return JSON.stringify(normalizeComparisonValue(key, a)) === JSON.stringify(normalizeComparisonValue(key, b));
140
+ }
141
+
142
+ function collectAmbiguityWarnings(existing, mergeData, { file, uuid }) {
143
+ const out = [];
144
+
145
+ const externalSource = existing?.external_source;
146
+ if (externalSource !== undefined && externalSource !== null && externalSource !== "scrivener") {
147
+ out.push({
148
+ code: "ambiguous_identity_tie",
149
+ message: "Existing sidecar identity source conflicts with Scrivener mapping; keeping existing sidecar identity.",
150
+ reason: "external_source_conflict",
151
+ external_source: String(externalSource),
152
+ file,
153
+ uuid,
154
+ });
155
+ }
156
+ if (externalSource === "scrivener" && (existing?.external_id === undefined || existing?.external_id === null || String(existing.external_id).trim() === "")) {
157
+ out.push({
158
+ code: "ambiguous_identity_tie",
159
+ message: "Existing sidecar identity is marked as Scrivener but missing external_id; keeping existing sidecar identity.",
160
+ reason: "missing_external_id",
161
+ file,
162
+ uuid,
163
+ });
164
+ }
165
+
166
+ for (const [key, scrivenerValue] of Object.entries(mergeData)) {
167
+ if (!Object.hasOwn(existing, key)) continue;
168
+ if (valuesEquivalentForMerge(key, existing[key], scrivenerValue)) continue;
169
+
170
+ if (STRUCTURE_MAPPING_FIELDS.has(key)) {
171
+ out.push({
172
+ code: "ambiguous_structure_mapping",
173
+ message: `Existing sidecar field '${key}' conflicts with Scrivener-derived structure; keeping existing value.`,
174
+ field: key,
175
+ existing_value: existing[key],
176
+ scrivener_value: scrivenerValue,
177
+ file,
178
+ uuid,
179
+ });
180
+ continue;
181
+ }
182
+
183
+ out.push({
184
+ code: "ambiguous_metadata_mapping",
185
+ message: `Existing sidecar field '${key}' conflicts with Scrivener metadata; keeping existing value.`,
186
+ field: key,
187
+ existing_value: existing[key],
188
+ scrivener_value: scrivenerValue,
189
+ file,
190
+ uuid,
191
+ });
192
+ }
193
+
194
+ return out;
195
+ }
129
196
 
130
197
  function recordWarning(summary, warning) {
131
198
  if (!summary[warning.code]) {
@@ -137,7 +204,21 @@ function recordWarning(summary, warning) {
137
204
 
138
205
  if (entry.examples.length < 5) {
139
206
  const example = { message: warning.message };
140
- for (const key of ["file", "sync_number", "field_id", "value", "uuid", "from_path", "to_path", "moved_to"]) {
207
+ for (const key of [
208
+ "file",
209
+ "sync_number",
210
+ "field",
211
+ "field_id",
212
+ "value",
213
+ "reason",
214
+ "external_source",
215
+ "existing_value",
216
+ "scrivener_value",
217
+ "uuid",
218
+ "from_path",
219
+ "to_path",
220
+ "moved_to",
221
+ ]) {
141
222
  if (warning[key] !== undefined && warning[key] !== null) {
142
223
  example[key] = warning[key];
143
224
  }
@@ -500,6 +581,11 @@ export function mergeScrivenerProjectMetadata({
500
581
  throw new Error(`Invalid sidecar YAML mapping at ${sidecarPath}`);
501
582
  }
502
583
  const existing = existingRaw ?? {};
584
+ const ambiguityWarnings = collectAmbiguityWarnings(existing, mergeData, { file: filename, uuid });
585
+ for (const warning of ambiguityWarnings) {
586
+ warningsTruncated = pushWarning(warnings, warningSummary, warning) || warningsTruncated;
587
+ }
588
+
503
589
  const { merged, changed, newKeys } = mergeSidecarData(existing, mergeData);
504
590
  const effective = changed ? merged : existing;
505
591
  const targetDir = sceneContainerDir(