@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.
@@ -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.callTool({
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.content[0].text}`);
37
+ console.log(`error: ${startResult.text}`);
40
38
  return;
41
39
  }
42
40
 
43
- const startData = JSON.parse(startResult.content[0].text);
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.callTool({
61
- name: "get_async_job_status",
62
- arguments: { job_id: jobId, include_result: true }
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.content?.[0]?.text || "status query failed"}`);
68
+ console.log(`error.code/message: ${statusResult.text || "status query failed"}`);
67
69
  settled = true;
68
70
  break;
69
71
  }
70
72
 
71
- let statusData;
72
- try {
73
- statusData = JSON.parse(statusResult.content[0].text);
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.callTool({ name, arguments: args });
26
- return result.content?.[0]?.text ?? "";
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 isArray = Array.isArray(parsed);
49
- const isEnvelope = !isArray && parsed.results && parsed.total_count !== undefined;
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 => obj !== null && typeof obj === "object");
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 4 tools parsed as valid JSON objects`);
287
+ log("12", "PASS", `All sampled metadata-read tools returned envelope responses`);
276
288
  } else {
277
- log("12", "FAIL", `Some responses did not parse as JSON`);
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 result = await client.callTool({ name: toolName, arguments: args });
16
- const text = result.content?.[0]?.text ?? "";
17
- const ok = check(text);
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 => t.includes("gangway") && t.includes("Marcus")
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
- return await client.callTool({ name, arguments: args });
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
- return JSON.parse(text);
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.callTool({ name: "sync", arguments: {} });
35
+ await callToolParsed(client, "sync", {});
35
36
 
36
- const scenes = await client.callTool({ name: "find_scenes", arguments: { project_id: "scrivener-export", page_size: 3, page: 1 } });
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
- const remainingScene = db.prepare(`SELECT 1 FROM scenes WHERE scene_id = ? LIMIT 1`).get(row.scene_id);
924
- if (!remainingScene) {
925
- db.prepare(`DELETE FROM scene_characters WHERE scene_id = ?`).run(row.scene_id);
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