@hanna84/mcp-writing 2.18.1 → 3.1.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 +20 -0
- package/README.md +53 -0
- package/package.json +1 -1
- package/src/core/db.js +214 -11
- package/src/index.js +23 -2
- package/src/review-bundles/review-bundles-planner.js +8 -7
- package/src/review-bundles/review-bundles-renderer.js +1 -1
- package/src/scripts/manual/README.md +27 -0
- package/src/scripts/manual/mcp-result.mjs +27 -0
- package/src/scripts/manual/run_create_review_bundle.js +14 -13
- package/src/scripts/manual/run_mcp_and_review.js +3 -5
- package/src/scripts/manual/run_mcp_test.js +15 -15
- package/src/scripts/manual/test-scenarios.mjs +26 -14
- package/src/scripts/manual/test.mjs +11 -4
- package/src/scripts/manual-validation.mjs +11 -3
- package/src/scripts/mcp-debug-client.mjs +4 -3
- package/src/sync/sync.js +16 -19
- package/src/tools/editing.js +333 -10
- package/src/tools/metadata.js +6 -6
- package/src/tools/review-bundles.js +12 -2
- package/src/tools/search.js +140 -38
- package/src/tools/sync.js +3 -0
- package/src/workflows/workflow-catalogue.js +82 -52
|
@@ -2,6 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
2
2
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import process from "process";
|
|
5
|
+
import { callToolParsed } from "./mcp-result.mjs";
|
|
5
6
|
|
|
6
7
|
function usage() {
|
|
7
8
|
console.log("Usage: node src/scripts/manual/run_mcp_test.js <source_project_dir> [project_id] [sync_dir]");
|
|
@@ -30,17 +31,18 @@ async function runCase(env, args) {
|
|
|
30
31
|
await client.connect(transport);
|
|
31
32
|
|
|
32
33
|
// Start the async merge job
|
|
33
|
-
const startResult = await client
|
|
34
|
-
name: "merge_scrivener_project_beta",
|
|
35
|
-
arguments: args
|
|
36
|
-
});
|
|
34
|
+
const startResult = await callToolParsed(client, "merge_scrivener_project_beta", args);
|
|
37
35
|
|
|
38
36
|
if (startResult.isError) {
|
|
39
|
-
console.log(`error: ${startResult.
|
|
37
|
+
console.log(`error: ${startResult.text}`);
|
|
40
38
|
return;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
const startData =
|
|
41
|
+
const startData = startResult.data;
|
|
42
|
+
if (!startData) {
|
|
43
|
+
console.log("error: invalid start payload");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
44
46
|
if (!startData.ok) {
|
|
45
47
|
console.log(`error: ${startData.error?.code || 'unknown'}`);
|
|
46
48
|
return;
|
|
@@ -57,22 +59,20 @@ async function runCase(env, args) {
|
|
|
57
59
|
while (!settled && attempts < maxAttempts) {
|
|
58
60
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
59
61
|
|
|
60
|
-
const statusResult = await client
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
const statusResult = await callToolParsed(client, "get_async_job_status", {
|
|
63
|
+
job_id: jobId,
|
|
64
|
+
include_result: true,
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
if (statusResult.isError) {
|
|
66
|
-
console.log(`error.code/message: ${statusResult.
|
|
68
|
+
console.log(`error.code/message: ${statusResult.text || "status query failed"}`);
|
|
67
69
|
settled = true;
|
|
68
70
|
break;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} catch (parseError) {
|
|
75
|
-
console.log(`error.code/message: invalid status payload: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
|
|
73
|
+
const statusData = statusResult.data;
|
|
74
|
+
if (!statusData) {
|
|
75
|
+
console.log("error.code/message: invalid status payload");
|
|
76
76
|
settled = true;
|
|
77
77
|
break;
|
|
78
78
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
3
|
+
import { callToolParsed } from "./mcp-result.mjs";
|
|
3
4
|
|
|
4
5
|
const BASE_URL = "http://localhost:3000";
|
|
5
6
|
|
|
@@ -22,8 +23,8 @@ async function connect() {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async function callTool(client, name, args = {}) {
|
|
25
|
-
const result = await client
|
|
26
|
-
return result.
|
|
26
|
+
const result = await callToolParsed(client, name, args);
|
|
27
|
+
return result.text;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const results = [];
|
|
@@ -45,12 +46,9 @@ async function runScenarios() {
|
|
|
45
46
|
try {
|
|
46
47
|
const text = await callTool(client, "find_scenes", { project_id: "the-lamb" });
|
|
47
48
|
const parsed = JSON.parse(text);
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (isArray) {
|
|
52
|
-
log("1a", "PASS", `Got array of ${parsed.length} scenes (backward compat)`);
|
|
53
|
-
} else if (isEnvelope) {
|
|
49
|
+
const isEnvelope = parsed && parsed.results && parsed.total_count !== undefined;
|
|
50
|
+
|
|
51
|
+
if (isEnvelope) {
|
|
54
52
|
log("1b", "PASS", `Got envelope with results: ${parsed.results.length}, total_count: ${parsed.total_count}`);
|
|
55
53
|
} else {
|
|
56
54
|
log("1c", "FAIL", `Unknown shape: ${JSON.stringify(parsed).slice(0, 100)}`);
|
|
@@ -241,17 +239,23 @@ async function runScenarios() {
|
|
|
241
239
|
const ga = JSON.parse(await callTool(client, "get_arc", { character_id: "char-mira-nystrom", page_size: 1 }));
|
|
242
240
|
const sm = JSON.parse(await callTool(client, "search_metadata", { query: "scene", page_size: 1 }));
|
|
243
241
|
const lt = JSON.parse(await callTool(client, "list_threads", { project_id: "the-lamb" }));
|
|
242
|
+
const lc = JSON.parse(await callTool(client, "list_characters", { project_id: "the-lamb" }));
|
|
243
|
+
const lp = JSON.parse(await callTool(client, "list_places", { project_id: "the-lamb" }));
|
|
244
|
+
const sr = JSON.parse(await callTool(client, "search_reference", { query: "scene" }));
|
|
244
245
|
|
|
245
246
|
const commonFields = ["results", "total_count"];
|
|
246
247
|
const fsHas = commonFields.every(f => f in fs);
|
|
247
248
|
const gaHas = commonFields.every(f => f in ga);
|
|
248
249
|
const smHas = commonFields.every(f => f in sm);
|
|
249
250
|
const ltHas = commonFields.every(f => f in lt);
|
|
251
|
+
const lcHas = commonFields.every(f => f in lc);
|
|
252
|
+
const lpHas = commonFields.every(f => f in lp);
|
|
253
|
+
const srHas = commonFields.every(f => f in sr);
|
|
250
254
|
|
|
251
|
-
if (fsHas && gaHas && smHas && ltHas) {
|
|
252
|
-
log("11", "PASS", `All tools have results + total_count envelope`);
|
|
255
|
+
if (fsHas && gaHas && smHas && ltHas && lcHas && lpHas && srHas) {
|
|
256
|
+
log("11", "PASS", `All metadata-read tools have results + total_count envelope`);
|
|
253
257
|
} else {
|
|
254
|
-
log("11", "FAIL", `Missing fields: fs=${fsHas}, ga=${gaHas}, sm=${smHas}, lt=${ltHas}`);
|
|
258
|
+
log("11", "FAIL", `Missing fields: fs=${fsHas}, ga=${gaHas}, sm=${smHas}, lt=${ltHas}, lc=${lcHas}, lp=${lpHas}, sr=${srHas}`);
|
|
255
259
|
}
|
|
256
260
|
} catch (err) {
|
|
257
261
|
log("11", "FAIL", err.message);
|
|
@@ -267,14 +271,22 @@ async function runScenarios() {
|
|
|
267
271
|
ga: JSON.parse(await callTool(client, "get_arc", { character_id: "char-mira-nystrom" })),
|
|
268
272
|
sm: JSON.parse(await callTool(client, "search_metadata", { query: "scene" })),
|
|
269
273
|
lt: JSON.parse(await callTool(client, "list_threads", { project_id: "the-lamb" })),
|
|
274
|
+
lc: JSON.parse(await callTool(client, "list_characters", { project_id: "the-lamb" })),
|
|
275
|
+
lp: JSON.parse(await callTool(client, "list_places", { project_id: "the-lamb" })),
|
|
276
|
+
sr: JSON.parse(await callTool(client, "search_reference", { query: "scene" })),
|
|
270
277
|
};
|
|
271
278
|
|
|
272
|
-
const allValid = Object.values(smoke).every(obj =>
|
|
279
|
+
const allValid = Object.values(smoke).every(obj => (
|
|
280
|
+
obj !== null
|
|
281
|
+
&& typeof obj === "object"
|
|
282
|
+
&& "results" in obj
|
|
283
|
+
&& "total_count" in obj
|
|
284
|
+
));
|
|
273
285
|
|
|
274
286
|
if (allValid) {
|
|
275
|
-
log("12", "PASS", `All
|
|
287
|
+
log("12", "PASS", `All sampled metadata-read tools returned envelope responses`);
|
|
276
288
|
} else {
|
|
277
|
-
log("12", "FAIL", `Some responses did not
|
|
289
|
+
log("12", "FAIL", `Some responses did not match envelope shape`);
|
|
278
290
|
}
|
|
279
291
|
} catch (err) {
|
|
280
292
|
log("12", "FAIL", err.message);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
3
|
+
import { callToolParsed } from "./mcp-result.mjs";
|
|
3
4
|
|
|
4
5
|
const BASE_URL = "http://localhost:3000";
|
|
5
6
|
|
|
@@ -12,9 +13,10 @@ let failed = 0;
|
|
|
12
13
|
|
|
13
14
|
async function test(label, toolName, args, check) {
|
|
14
15
|
try {
|
|
15
|
-
const
|
|
16
|
-
const text =
|
|
17
|
-
const
|
|
16
|
+
const parsed = await callToolParsed(client, toolName, args);
|
|
17
|
+
const text = parsed.text;
|
|
18
|
+
const result = parsed.raw;
|
|
19
|
+
const ok = check(text, result);
|
|
18
20
|
if (ok) {
|
|
19
21
|
console.log(` ✓ ${label}`);
|
|
20
22
|
passed++;
|
|
@@ -77,7 +79,12 @@ console.log("\n── get_scene_prose ──────────────
|
|
|
77
79
|
await test(
|
|
78
80
|
"returns prose for sc-001",
|
|
79
81
|
"get_scene_prose", { scene_id: "sc-001" },
|
|
80
|
-
t
|
|
82
|
+
(t, result) => {
|
|
83
|
+
if (result.structuredContent?.warning) {
|
|
84
|
+
console.log(` warning: ${result.structuredContent.warning}`);
|
|
85
|
+
}
|
|
86
|
+
return t.includes("gangway") && t.includes("Marcus");
|
|
87
|
+
}
|
|
81
88
|
);
|
|
82
89
|
|
|
83
90
|
await test(
|
|
@@ -7,6 +7,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
|
7
7
|
import { URL as NodeURL } from "node:url";
|
|
8
8
|
import { execSync } from "node:child_process";
|
|
9
9
|
import fs from "node:fs";
|
|
10
|
+
import { callToolParsed } from "./manual/mcp-result.mjs";
|
|
10
11
|
|
|
11
12
|
const ROOT = process.cwd();
|
|
12
13
|
|
|
@@ -38,7 +39,8 @@ async function connectClient(url) {
|
|
|
38
39
|
|
|
39
40
|
async function callTool(client, name, args = {}) {
|
|
40
41
|
try {
|
|
41
|
-
|
|
42
|
+
const parsed = await callToolParsed(client, name, args);
|
|
43
|
+
return parsed.raw;
|
|
42
44
|
} catch (e) {
|
|
43
45
|
return { error: e.message };
|
|
44
46
|
}
|
|
@@ -48,12 +50,14 @@ function parseResponse(result) {
|
|
|
48
50
|
if (result.error) return { error: result.error };
|
|
49
51
|
try {
|
|
50
52
|
const text = result.content?.[0]?.text;
|
|
53
|
+
const structured = result.structuredContent;
|
|
51
54
|
if (!text) return { raw: result };
|
|
52
55
|
// Try to parse as JSON
|
|
53
56
|
try {
|
|
54
|
-
|
|
57
|
+
const parsed = JSON.parse(text);
|
|
58
|
+
return structured ? { ...parsed, _structured: structured } : parsed;
|
|
55
59
|
} catch {
|
|
56
|
-
return { text };
|
|
60
|
+
return structured ? { text, _structured: structured } : { text };
|
|
57
61
|
}
|
|
58
62
|
} catch {
|
|
59
63
|
return { raw: result };
|
|
@@ -218,7 +222,10 @@ async function runPhaseB() {
|
|
|
218
222
|
const proseRes = await callTool(client, "get_scene_prose", { scene_id: results.firstSceneId });
|
|
219
223
|
const proseData = parseResponse(proseRes);
|
|
220
224
|
results.proseExcerpt = proseData.prose?.slice(0, 200) || proseData.text?.slice(0, 200) || proseRes.content?.[0]?.text?.slice(0, 200) || "(no prose)";
|
|
225
|
+
results.proseWarning = proseData._structured?.warning || null;
|
|
226
|
+
results.proseNextStep = proseData._structured?.next_step || null;
|
|
221
227
|
console.log("get_scene_prose excerpt:", results.proseExcerpt.slice(0, 150) + "...");
|
|
228
|
+
if (results.proseWarning) console.log("get_scene_prose warning:", results.proseWarning);
|
|
222
229
|
} else {
|
|
223
230
|
results.proseExcerpt = "(no scene_id available)";
|
|
224
231
|
}
|
|
@@ -260,6 +267,7 @@ async function main() {
|
|
|
260
267
|
console.log(" Place count:", phaseB.placeCount);
|
|
261
268
|
console.log(" Airport search count:", phaseB.airportSearchCount);
|
|
262
269
|
console.log(" First scene_id:", phaseB.firstSceneId || "N/A");
|
|
270
|
+
if (phaseB.proseWarning) console.log(" Prose warning:", phaseB.proseWarning);
|
|
263
271
|
console.log(" Prose excerpt (200 chars):", phaseB.proseExcerpt?.slice(0, 200) || "N/A");
|
|
264
272
|
if (phaseB.warnings.length) console.log(" Lint warnings:", phaseB.warnings.length > 0 ? "Yes (see above)" : "None");
|
|
265
273
|
if (phaseB.errors.length) console.log(" Errors:", phaseB.errors);
|
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
3
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
4
4
|
import { URL as NodeURL } from "node:url";
|
|
5
|
+
import { callToolParsed } from "./manual/mcp-result.mjs";
|
|
5
6
|
|
|
6
7
|
const ROOT = process.cwd();
|
|
7
8
|
|
|
@@ -31,11 +32,11 @@ try {
|
|
|
31
32
|
const transport = new SSEClientTransport(new NodeURL(`${BASE}/sse`));
|
|
32
33
|
await client.connect(transport);
|
|
33
34
|
|
|
34
|
-
await client
|
|
35
|
+
await callToolParsed(client, "sync", {});
|
|
35
36
|
|
|
36
|
-
const scenes = await client
|
|
37
|
+
const scenes = await callToolParsed(client, "find_scenes", { project_id: "scrivener-export", page_size: 3, page: 1 });
|
|
37
38
|
console.log("=== find_scenes raw response ===");
|
|
38
|
-
console.log(JSON.stringify(scenes, null, 2));
|
|
39
|
+
console.log(JSON.stringify(scenes.raw, null, 2));
|
|
39
40
|
|
|
40
41
|
await client.close();
|
|
41
42
|
} finally {
|
package/src/sync/sync.js
CHANGED
|
@@ -919,13 +919,10 @@ function pruneMissingScenes(db, seenSceneKeys, syncDir) {
|
|
|
919
919
|
WHERE source_kind = 'scene' AND source_project_id = ? AND source_id = ?
|
|
920
920
|
`).run(row.project_id ?? "", row.scene_id);
|
|
921
921
|
db.prepare(`DELETE FROM scenes WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
db.prepare(`DELETE FROM scene_places WHERE scene_id = ?`).run(row.scene_id);
|
|
927
|
-
db.prepare(`DELETE FROM scene_tags WHERE scene_id = ?`).run(row.scene_id);
|
|
928
|
-
}
|
|
922
|
+
db.prepare(`DELETE FROM scene_characters WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
923
|
+
db.prepare(`DELETE FROM scene_places WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
924
|
+
db.prepare(`DELETE FROM scene_tags WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
925
|
+
db.prepare(`DELETE FROM scene_threads WHERE scene_id = ? AND project_id = ?`).run(row.scene_id, row.project_id);
|
|
929
926
|
}
|
|
930
927
|
}
|
|
931
928
|
|
|
@@ -994,14 +991,14 @@ export function indexSceneFile(db, syncDir, file, meta, prose) {
|
|
|
994
991
|
new Date().toISOString()
|
|
995
992
|
);
|
|
996
993
|
|
|
997
|
-
db.prepare(`DELETE FROM scene_characters WHERE scene_id = ?`).run(meta.scene_id);
|
|
998
|
-
db.prepare(`DELETE FROM scene_places WHERE scene_id = ?`).run(meta.scene_id);
|
|
999
|
-
db.prepare(`DELETE FROM scene_tags WHERE scene_id = ?`).run(meta.scene_id);
|
|
994
|
+
db.prepare(`DELETE FROM scene_characters WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
|
|
995
|
+
db.prepare(`DELETE FROM scene_places WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
|
|
996
|
+
db.prepare(`DELETE FROM scene_tags WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
|
|
1000
997
|
|
|
1001
998
|
for (const c of (meta.characters ?? [])) {
|
|
1002
999
|
// Version continuity markers (e.g. v7.3, v3.3b) are tracked as tags, not characters
|
|
1003
1000
|
if (/^v\d[\d.a-z]*$/i.test(c)) {
|
|
1004
|
-
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, tag) VALUES (?, ?)`).run(meta.scene_id, c);
|
|
1001
|
+
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, project_id, tag) VALUES (?, ?, ?)`).run(meta.scene_id, project_id, c);
|
|
1005
1002
|
continue;
|
|
1006
1003
|
}
|
|
1007
1004
|
let cid = c;
|
|
@@ -1021,23 +1018,23 @@ export function indexSceneFile(db, syncDir, file, meta, prose) {
|
|
|
1021
1018
|
}
|
|
1022
1019
|
if (row) cid = row.character_id;
|
|
1023
1020
|
}
|
|
1024
|
-
db.prepare(`INSERT OR IGNORE INTO scene_characters (scene_id, character_id) VALUES (?, ?)`).run(
|
|
1025
|
-
meta.scene_id, cid
|
|
1021
|
+
db.prepare(`INSERT OR IGNORE INTO scene_characters (scene_id, project_id, character_id) VALUES (?, ?, ?)`).run(
|
|
1022
|
+
meta.scene_id, project_id, cid
|
|
1026
1023
|
);
|
|
1027
1024
|
}
|
|
1028
1025
|
for (const p of (meta.places ?? [])) {
|
|
1029
|
-
db.prepare(`INSERT OR IGNORE INTO scene_places (scene_id, place_id) VALUES (?, ?)`).run(
|
|
1030
|
-
meta.scene_id, p
|
|
1026
|
+
db.prepare(`INSERT OR IGNORE INTO scene_places (scene_id, project_id, place_id) VALUES (?, ?, ?)`).run(
|
|
1027
|
+
meta.scene_id, project_id, p
|
|
1031
1028
|
);
|
|
1032
1029
|
}
|
|
1033
1030
|
for (const t of (meta.tags ?? [])) {
|
|
1034
|
-
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, tag) VALUES (?, ?)`).run(
|
|
1035
|
-
meta.scene_id, t
|
|
1031
|
+
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, project_id, tag) VALUES (?, ?, ?)`).run(
|
|
1032
|
+
meta.scene_id, project_id, t
|
|
1036
1033
|
);
|
|
1037
1034
|
}
|
|
1038
1035
|
for (const v of (meta.versions ?? [])) {
|
|
1039
|
-
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, tag) VALUES (?, ?)`).run(
|
|
1040
|
-
meta.scene_id, v
|
|
1036
|
+
db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, project_id, tag) VALUES (?, ?, ?)`).run(
|
|
1037
|
+
meta.scene_id, project_id, v
|
|
1041
1038
|
);
|
|
1042
1039
|
}
|
|
1043
1040
|
|