@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 +20 -0
- package/index.js +5 -1
- package/package.json +1 -1
- package/scrivener-direct.js +87 -1
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:
|
|
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
package/scrivener-direct.js
CHANGED
|
@@ -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 [
|
|
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(
|