@hanna84/mcp-writing 2.18.1 → 3.0.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 +10 -0
- package/README.md +53 -0
- package/package.json +1 -1
- package/src/core/db.js +214 -11
- package/src/index.js +15 -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 +77 -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
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,21 @@ 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
|
+
#### [v3.0.0](https://github.com/hannasdev/mcp-writing.git
|
|
8
|
+
/compare/v2.18.1...v3.0.0)
|
|
9
|
+
|
|
10
|
+
- feat!: improve MCP tooling usability and harden scene-id safety [`#165`](https://github.com/hannasdev/mcp-writing.git
|
|
11
|
+
/pull/165)
|
|
12
|
+
|
|
7
13
|
#### [v2.18.1](https://github.com/hannasdev/mcp-writing.git
|
|
8
14
|
/compare/v2.18.0...v2.18.1)
|
|
9
15
|
|
|
16
|
+
> 1 May 2026
|
|
17
|
+
|
|
10
18
|
- docs: finalize reference docs PRD status [`#164`](https://github.com/hannasdev/mcp-writing.git
|
|
11
19
|
/pull/164)
|
|
20
|
+
- Release 2.18.1 [`ed9d0e7`](https://github.com/hannasdev/mcp-writing.git
|
|
21
|
+
/commit/ed9d0e70b6ea17ea22f84d257d71c33d0350fe5f)
|
|
12
22
|
|
|
13
23
|
#### [v2.18.0](https://github.com/hannasdev/mcp-writing.git
|
|
14
24
|
/compare/v2.17.1...v2.18.0)
|
package/README.md
CHANGED
|
@@ -41,6 +41,47 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
|
|
|
41
41
|
| [docs/tools.md](docs/tools.md) | Full tool reference — auto-generated from source |
|
|
42
42
|
| [docs/development.md](docs/development.md) | Running locally, tests, environment variables, troubleshooting |
|
|
43
43
|
|
|
44
|
+
## Breaking changes
|
|
45
|
+
|
|
46
|
+
### `describe_workflows` surface redesign
|
|
47
|
+
|
|
48
|
+
`describe_workflows` now exposes an outcome-first, discovery-first workflow map. This is a breaking change if your prompts or automation depend on previous workflow IDs or ordering.
|
|
49
|
+
|
|
50
|
+
Update integrations using this mapping:
|
|
51
|
+
|
|
52
|
+
- `manuscript_exploration` -> `question_driven_discovery` (or `targeted_scene_reading` when the task is prose inspection)
|
|
53
|
+
- `prose_editing` -> `safe_scene_revision`
|
|
54
|
+
- `character_management` -> `character_understanding`
|
|
55
|
+
- `place_management` -> `place_understanding`
|
|
56
|
+
- `review_bundle` -> `review_preparation`
|
|
57
|
+
|
|
58
|
+
New workflow IDs added:
|
|
59
|
+
|
|
60
|
+
- `thread_understanding`
|
|
61
|
+
- `parity_recovery`
|
|
62
|
+
|
|
63
|
+
Styleguide workflows are still available, but no longer positioned as part of the primary daily workflow surface.
|
|
64
|
+
|
|
65
|
+
### `find_scenes` and `get_arc` response-shape standardization
|
|
66
|
+
|
|
67
|
+
`find_scenes` and `get_arc` now always return structured envelopes, including non-paginated calls.
|
|
68
|
+
|
|
69
|
+
- Envelope fields: `results`, `total_count`.
|
|
70
|
+
- Pagination fields are included when paging is active.
|
|
71
|
+
- `warning` / `next_step` are included when relevant.
|
|
72
|
+
|
|
73
|
+
If your integration previously handled raw arrays for non-paginated calls, update it to parse envelopes consistently.
|
|
74
|
+
|
|
75
|
+
Safe parsing pattern:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
const parsed = JSON.parse(toolText);
|
|
79
|
+
const scenes = parsed.results ?? [];
|
|
80
|
+
const totalCount = parsed.total_count ?? scenes.length;
|
|
81
|
+
const warning = parsed.warning ?? null;
|
|
82
|
+
const nextStep = parsed.next_step ?? null;
|
|
83
|
+
```
|
|
84
|
+
|
|
44
85
|
## Usage scenarios
|
|
45
86
|
|
|
46
87
|
### 1) Continuity pass before sending chapters to beta readers
|
|
@@ -99,5 +140,17 @@ Goal: rebuild scene-to-character links in a controlled way after imported prose
|
|
|
99
140
|
|
|
100
141
|
Outcome: character-link maintenance becomes a preview-first batch operation instead of a one-off regex script or manual sidecar cleanup.
|
|
101
142
|
|
|
143
|
+
### 6) Post-upgrade recovery after legacy migration warnings
|
|
144
|
+
|
|
145
|
+
Goal: recover index confidence quickly when legacy upgrade warnings indicate ambiguous rows were skipped.
|
|
146
|
+
|
|
147
|
+
1. Start by checking `get_runtime_config` (or `describe_workflows`) and confirm whether `db_migration_warnings` contains `LEGACY_JOIN_ROWS_SKIPPED`.
|
|
148
|
+
2. If present, run `sync` immediately to rebuild scene relationships from current sidecars and prose metadata.
|
|
149
|
+
3. Continue normal discovery (`find_scenes`, `get_arc`, `get_thread_arc`) and watch for stale-metadata warnings.
|
|
150
|
+
4. When you touch stale scenes, run `enrich_scene(scene_id, project_id)` to recover metadata parity incrementally.
|
|
151
|
+
5. If many scenes remain stale, switch to `enrich_scene_characters_batch` (dry-run first) for broader catch-up.
|
|
152
|
+
|
|
153
|
+
Outcome: upgrade-related data loss risk becomes an explicit, operator-visible recovery workflow instead of a silent state mismatch.
|
|
154
|
+
|
|
102
155
|
## License
|
|
103
156
|
AGPL-3.0-only
|
package/package.json
CHANGED
package/src/core/db.js
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
2
|
|
|
3
|
+
const dbStartupWarnings = [];
|
|
4
|
+
|
|
5
|
+
function cloneWarningDetails(details) {
|
|
6
|
+
if (!details) return details;
|
|
7
|
+
|
|
8
|
+
if (typeof structuredClone === "function") {
|
|
9
|
+
try {
|
|
10
|
+
return structuredClone(details);
|
|
11
|
+
} catch {
|
|
12
|
+
// Fall through to JSON clone for plain data payloads.
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(JSON.stringify(details));
|
|
18
|
+
} catch {
|
|
19
|
+
return { ...details };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDbStartupWarnings() {
|
|
24
|
+
return dbStartupWarnings.map((warning) => ({
|
|
25
|
+
...warning,
|
|
26
|
+
details: cloneWarningDetails(warning.details),
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resetDbStartupWarnings() {
|
|
31
|
+
dbStartupWarnings.length = 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
3
34
|
export const SCHEMA = `
|
|
4
35
|
CREATE TABLE IF NOT EXISTS universes (
|
|
5
36
|
universe_id TEXT PRIMARY KEY,
|
|
@@ -38,27 +69,31 @@ export const SCHEMA = `
|
|
|
38
69
|
|
|
39
70
|
CREATE TABLE IF NOT EXISTS scene_characters (
|
|
40
71
|
scene_id TEXT NOT NULL,
|
|
72
|
+
project_id TEXT NOT NULL,
|
|
41
73
|
character_id TEXT NOT NULL,
|
|
42
|
-
PRIMARY KEY (scene_id, character_id)
|
|
74
|
+
PRIMARY KEY (scene_id, project_id, character_id)
|
|
43
75
|
);
|
|
44
76
|
|
|
45
77
|
CREATE TABLE IF NOT EXISTS scene_places (
|
|
46
|
-
scene_id
|
|
47
|
-
|
|
48
|
-
|
|
78
|
+
scene_id TEXT NOT NULL,
|
|
79
|
+
project_id TEXT NOT NULL,
|
|
80
|
+
place_id TEXT NOT NULL,
|
|
81
|
+
PRIMARY KEY (scene_id, project_id, place_id)
|
|
49
82
|
);
|
|
50
83
|
|
|
51
84
|
CREATE TABLE IF NOT EXISTS scene_tags (
|
|
52
|
-
scene_id
|
|
53
|
-
|
|
54
|
-
|
|
85
|
+
scene_id TEXT NOT NULL,
|
|
86
|
+
project_id TEXT NOT NULL,
|
|
87
|
+
tag TEXT NOT NULL,
|
|
88
|
+
PRIMARY KEY (scene_id, project_id, tag)
|
|
55
89
|
);
|
|
56
90
|
|
|
57
91
|
CREATE TABLE IF NOT EXISTS scene_threads (
|
|
58
|
-
scene_id
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
92
|
+
scene_id TEXT NOT NULL,
|
|
93
|
+
project_id TEXT NOT NULL,
|
|
94
|
+
thread_id TEXT NOT NULL,
|
|
95
|
+
beat TEXT,
|
|
96
|
+
PRIMARY KEY (scene_id, project_id, thread_id)
|
|
62
97
|
);
|
|
63
98
|
|
|
64
99
|
CREATE TABLE IF NOT EXISTS characters (
|
|
@@ -160,6 +195,33 @@ export const SCHEMA = `
|
|
|
160
195
|
// Each migration runs inside a transaction with the version bump — crash-safe.
|
|
161
196
|
// Migrations must be idempotent (guard against already-applied state).
|
|
162
197
|
// Never edit existing entries — add new ones at the end.
|
|
198
|
+
function migrateSceneJoinTableToProjectScope(db, tableName, tableSql, insertSql) {
|
|
199
|
+
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
200
|
+
if (columns.some((column) => column.name === "project_id")) {
|
|
201
|
+
return {
|
|
202
|
+
migrated: false,
|
|
203
|
+
sourceCount: 0,
|
|
204
|
+
migratedCount: 0,
|
|
205
|
+
droppedCount: 0,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const sourceCount = db.prepare(`SELECT COUNT(*) AS count FROM ${tableName}`).get()?.count ?? 0;
|
|
210
|
+
db.exec(tableSql);
|
|
211
|
+
db.exec(insertSql);
|
|
212
|
+
const migratedCount = db.prepare(`SELECT COUNT(*) AS count FROM ${tableName}_migrating`).get()?.count ?? 0;
|
|
213
|
+
const droppedCount = Math.max(0, sourceCount - migratedCount);
|
|
214
|
+
db.exec(`DROP TABLE ${tableName};`);
|
|
215
|
+
db.exec(`ALTER TABLE ${tableName}_migrating RENAME TO ${tableName};`);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
migrated: true,
|
|
219
|
+
sourceCount,
|
|
220
|
+
migratedCount,
|
|
221
|
+
droppedCount,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
163
225
|
const MIGRATIONS = [
|
|
164
226
|
// 1: add chapter_title column to scenes
|
|
165
227
|
(db) => {
|
|
@@ -282,6 +344,146 @@ const MIGRATIONS = [
|
|
|
282
344
|
db.exec(`ALTER TABLE reference_links ADD COLUMN origin TEXT NOT NULL DEFAULT 'inferred';`);
|
|
283
345
|
}
|
|
284
346
|
},
|
|
347
|
+
// 7: scope scene join tables by project_id so duplicate scene IDs across projects are safe
|
|
348
|
+
(db) => {
|
|
349
|
+
const charactersMigration = migrateSceneJoinTableToProjectScope(
|
|
350
|
+
db,
|
|
351
|
+
"scene_characters",
|
|
352
|
+
`
|
|
353
|
+
CREATE TABLE scene_characters_migrating (
|
|
354
|
+
scene_id TEXT NOT NULL,
|
|
355
|
+
project_id TEXT NOT NULL,
|
|
356
|
+
character_id TEXT NOT NULL,
|
|
357
|
+
PRIMARY KEY (scene_id, project_id, character_id)
|
|
358
|
+
);
|
|
359
|
+
`,
|
|
360
|
+
`
|
|
361
|
+
INSERT OR IGNORE INTO scene_characters_migrating (scene_id, project_id, character_id)
|
|
362
|
+
SELECT sc.scene_id, s.project_id, sc.character_id
|
|
363
|
+
FROM scene_characters sc
|
|
364
|
+
JOIN scenes s ON s.scene_id = sc.scene_id
|
|
365
|
+
WHERE (
|
|
366
|
+
SELECT COUNT(*)
|
|
367
|
+
FROM scenes sx
|
|
368
|
+
WHERE sx.scene_id = sc.scene_id
|
|
369
|
+
) = 1;
|
|
370
|
+
`
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const placesMigration = migrateSceneJoinTableToProjectScope(
|
|
374
|
+
db,
|
|
375
|
+
"scene_places",
|
|
376
|
+
`
|
|
377
|
+
CREATE TABLE scene_places_migrating (
|
|
378
|
+
scene_id TEXT NOT NULL,
|
|
379
|
+
project_id TEXT NOT NULL,
|
|
380
|
+
place_id TEXT NOT NULL,
|
|
381
|
+
PRIMARY KEY (scene_id, project_id, place_id)
|
|
382
|
+
);
|
|
383
|
+
`,
|
|
384
|
+
`
|
|
385
|
+
INSERT OR IGNORE INTO scene_places_migrating (scene_id, project_id, place_id)
|
|
386
|
+
SELECT sp.scene_id, s.project_id, sp.place_id
|
|
387
|
+
FROM scene_places sp
|
|
388
|
+
JOIN scenes s ON s.scene_id = sp.scene_id
|
|
389
|
+
WHERE (
|
|
390
|
+
SELECT COUNT(*)
|
|
391
|
+
FROM scenes sx
|
|
392
|
+
WHERE sx.scene_id = sp.scene_id
|
|
393
|
+
) = 1;
|
|
394
|
+
`
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
const tagsMigration = migrateSceneJoinTableToProjectScope(
|
|
398
|
+
db,
|
|
399
|
+
"scene_tags",
|
|
400
|
+
`
|
|
401
|
+
CREATE TABLE scene_tags_migrating (
|
|
402
|
+
scene_id TEXT NOT NULL,
|
|
403
|
+
project_id TEXT NOT NULL,
|
|
404
|
+
tag TEXT NOT NULL,
|
|
405
|
+
PRIMARY KEY (scene_id, project_id, tag)
|
|
406
|
+
);
|
|
407
|
+
`,
|
|
408
|
+
`
|
|
409
|
+
INSERT OR IGNORE INTO scene_tags_migrating (scene_id, project_id, tag)
|
|
410
|
+
SELECT st.scene_id, s.project_id, st.tag
|
|
411
|
+
FROM scene_tags st
|
|
412
|
+
JOIN scenes s ON s.scene_id = st.scene_id
|
|
413
|
+
WHERE (
|
|
414
|
+
SELECT COUNT(*)
|
|
415
|
+
FROM scenes sx
|
|
416
|
+
WHERE sx.scene_id = st.scene_id
|
|
417
|
+
) = 1;
|
|
418
|
+
`
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const threadsMigration = migrateSceneJoinTableToProjectScope(
|
|
422
|
+
db,
|
|
423
|
+
"scene_threads",
|
|
424
|
+
`
|
|
425
|
+
CREATE TABLE scene_threads_migrating (
|
|
426
|
+
scene_id TEXT NOT NULL,
|
|
427
|
+
project_id TEXT NOT NULL,
|
|
428
|
+
thread_id TEXT NOT NULL,
|
|
429
|
+
beat TEXT,
|
|
430
|
+
PRIMARY KEY (scene_id, project_id, thread_id)
|
|
431
|
+
);
|
|
432
|
+
`,
|
|
433
|
+
`
|
|
434
|
+
INSERT OR IGNORE INTO scene_threads_migrating (scene_id, project_id, thread_id, beat)
|
|
435
|
+
SELECT st.scene_id, t.project_id, st.thread_id, st.beat
|
|
436
|
+
FROM scene_threads st
|
|
437
|
+
JOIN threads t ON t.thread_id = st.thread_id
|
|
438
|
+
JOIN scenes s
|
|
439
|
+
ON s.scene_id = st.scene_id
|
|
440
|
+
AND s.project_id = t.project_id;
|
|
441
|
+
|
|
442
|
+
INSERT OR IGNORE INTO scene_threads_migrating (scene_id, project_id, thread_id, beat)
|
|
443
|
+
SELECT st.scene_id, s.project_id, st.thread_id, st.beat
|
|
444
|
+
FROM scene_threads st
|
|
445
|
+
JOIN scenes s ON s.scene_id = st.scene_id
|
|
446
|
+
LEFT JOIN threads t ON t.thread_id = st.thread_id
|
|
447
|
+
WHERE t.thread_id IS NULL
|
|
448
|
+
AND (
|
|
449
|
+
SELECT COUNT(*)
|
|
450
|
+
FROM scenes sx
|
|
451
|
+
WHERE sx.scene_id = st.scene_id
|
|
452
|
+
) = 1;
|
|
453
|
+
`
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const migrationSummaries = [
|
|
457
|
+
["scene_characters", charactersMigration],
|
|
458
|
+
["scene_places", placesMigration],
|
|
459
|
+
["scene_tags", tagsMigration],
|
|
460
|
+
["scene_threads", threadsMigration],
|
|
461
|
+
];
|
|
462
|
+
const droppedByTable = {};
|
|
463
|
+
let totalDropped = 0;
|
|
464
|
+
|
|
465
|
+
for (const [tableName, summary] of migrationSummaries) {
|
|
466
|
+
if (!summary?.migrated || summary.droppedCount <= 0) continue;
|
|
467
|
+
droppedByTable[tableName] = {
|
|
468
|
+
source_rows: summary.sourceCount,
|
|
469
|
+
migrated_rows: summary.migratedCount,
|
|
470
|
+
skipped_rows: summary.droppedCount,
|
|
471
|
+
};
|
|
472
|
+
totalDropped += summary.droppedCount;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (totalDropped > 0) {
|
|
476
|
+
dbStartupWarnings.push({
|
|
477
|
+
code: "LEGACY_JOIN_ROWS_SKIPPED",
|
|
478
|
+
message: "Legacy scene relationship rows were skipped during migration because scene_id was ambiguous across projects or duplicate links could not be preserved safely.",
|
|
479
|
+
details: {
|
|
480
|
+
skipped_rows_total: totalDropped,
|
|
481
|
+
skipped_rows_by_table: droppedByTable,
|
|
482
|
+
next_step: "Run sync() immediately after upgrade, then run enrich_scene(scene_id, project_id) for any stale scenes you touch.",
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
},
|
|
285
487
|
];
|
|
286
488
|
|
|
287
489
|
// The version every database should reach after openDb. Not the current DB value —
|
|
@@ -312,6 +514,7 @@ function applyMigrations(db) {
|
|
|
312
514
|
}
|
|
313
515
|
|
|
314
516
|
export function openDb(dbPath) {
|
|
517
|
+
resetDbStartupWarnings();
|
|
315
518
|
const db = new DatabaseSync(dbPath);
|
|
316
519
|
db.exec(SCHEMA);
|
|
317
520
|
applyMigrations(db);
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import fs from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { openDb, checkpointJobFinish, loadStalledJobs, pruneJobCheckpoints } from "./core/db.js";
|
|
9
|
+
import { openDb, getDbStartupWarnings, checkpointJobFinish, loadStalledJobs, pruneJobCheckpoints } from "./core/db.js";
|
|
10
10
|
import { syncAll, isSyncDirWritable, getSyncOwnershipDiagnostics, isStructuralProjectId } from "./sync/sync.js";
|
|
11
11
|
import { isGitAvailable, isGitRepository, initGitRepository, getSceneProseAtCommit } from "./core/git.js";
|
|
12
12
|
import { createAsyncJobManager, readJsonIfExists } from "./runtime/async-jobs.js";
|
|
@@ -134,6 +134,7 @@ function errorResponse(code, message, details) {
|
|
|
134
134
|
// Database setup
|
|
135
135
|
// ---------------------------------------------------------------------------
|
|
136
136
|
const db = openDb(DB_PATH);
|
|
137
|
+
const DB_STARTUP_WARNINGS = getDbStartupWarnings();
|
|
137
138
|
|
|
138
139
|
// Recover jobs that were in-flight when the server last exited.
|
|
139
140
|
const stalledJobs = loadStalledJobs(db);
|
|
@@ -166,6 +167,12 @@ const { pruneAsyncJobs, startAsyncJob, toPublicJob } = createAsyncJobManager({
|
|
|
166
167
|
|
|
167
168
|
process.stderr.write(`[mcp-writing] Sync dir: ${SYNC_DIR_ABS}\n`);
|
|
168
169
|
process.stderr.write(`[mcp-writing] DB path: ${DB_PATH_DISPLAY}\n`);
|
|
170
|
+
if (DB_STARTUP_WARNINGS.length > 0) {
|
|
171
|
+
process.stderr.write(`[mcp-writing] WARNING: ${DB_STARTUP_WARNINGS.length} DB migration warning(s) detected.\n`);
|
|
172
|
+
for (const warning of DB_STARTUP_WARNINGS) {
|
|
173
|
+
process.stderr.write(`[mcp-writing] - ${warning.code}: ${warning.message}\n`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
169
176
|
|
|
170
177
|
// Check sync dir writability once at startup (needed for Phase 2 sidecar writes)
|
|
171
178
|
const SYNC_DIR_WRITABLE = isSyncDirWritable(SYNC_DIR);
|
|
@@ -307,7 +314,7 @@ function createMcpServer() {
|
|
|
307
314
|
// ---- describe_workflows --------------------------------------------------
|
|
308
315
|
s.tool(
|
|
309
316
|
"describe_workflows",
|
|
310
|
-
"Return
|
|
317
|
+
"Return the default workflow map and current project context for this server. Call this first in most sessions and again whenever you are unsure what to do next. Never write scripts to invoke tools — call them directly.",
|
|
311
318
|
{},
|
|
312
319
|
async () => {
|
|
313
320
|
const projectRow = db.prepare(
|
|
@@ -352,14 +359,19 @@ function createMcpServer() {
|
|
|
352
359
|
},
|
|
353
360
|
git_available: GIT_AVAILABLE,
|
|
354
361
|
pending_proposals: pendingProposals.size,
|
|
362
|
+
db_migration_warnings: DB_STARTUP_WARNINGS,
|
|
355
363
|
},
|
|
356
364
|
workflows: WORKFLOW_CATALOGUE,
|
|
357
365
|
notes: [
|
|
358
366
|
"Never write JavaScript or shell scripts to invoke tools. Call them directly.",
|
|
367
|
+
"Use describe_workflows as the default starting point for most sessions and whenever you are uncertain which tool path fits the task.",
|
|
359
368
|
"If a tool returns a next_step field (in a success or error response), follow it before trying anything else.",
|
|
360
369
|
"Use find_scenes without filters to discover what project_ids are indexed.",
|
|
361
370
|
"When calling bootstrap_prose_styleguide_config or check_prose_styleguide_drift, set max_scenes to context.scene_count to avoid the default limit.",
|
|
362
371
|
"Styleguide tools resolve config in priority order: project_root > universe_root > sync_root. If any styleguide_exists field is true, a config exists and styleguide tools will work — do not run setup_prose_styleguide_config unless ALL styleguide_exists fields are false.",
|
|
372
|
+
...(DB_STARTUP_WARNINGS.length > 0
|
|
373
|
+
? ["Database migration warnings are present in context.db_migration_warnings. Run sync() now, then run enrich_scene(scene_id, project_id) for stale scenes you touch."]
|
|
374
|
+
: []),
|
|
363
375
|
],
|
|
364
376
|
});
|
|
365
377
|
}
|
|
@@ -416,6 +428,7 @@ function createMcpServer() {
|
|
|
416
428
|
server_version: MCP_SERVER_VERSION,
|
|
417
429
|
sync_dir: SYNC_DIR_ABS,
|
|
418
430
|
db_path: DB_PATH_DISPLAY,
|
|
431
|
+
db_migration_warnings: DB_STARTUP_WARNINGS,
|
|
419
432
|
sync_dir_writable: SYNC_DIR_WRITABLE,
|
|
420
433
|
ownership_guard_mode: OWNERSHIP_GUARD_MODE,
|
|
421
434
|
permission_diagnostics: SYNC_OWNERSHIP_DIAGNOSTICS,
|
|
@@ -160,25 +160,26 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
160
160
|
|
|
161
161
|
const requestedSceneIds = resolveRequestedSceneIds(dbHandle, project_id, scene_ids);
|
|
162
162
|
const conditions = ["s.project_id = ?"];
|
|
163
|
-
const params = [project_id];
|
|
164
163
|
const joins = [];
|
|
164
|
+
const joinParams = [];
|
|
165
|
+
const conditionParams = [project_id];
|
|
165
166
|
|
|
166
167
|
if (tag) {
|
|
167
|
-
joins.push("JOIN scene_tags st ON st.scene_id = s.scene_id AND st.tag = ?");
|
|
168
|
-
|
|
168
|
+
joins.push("JOIN scene_tags st ON st.scene_id = s.scene_id AND st.project_id = s.project_id AND st.tag = ?");
|
|
169
|
+
joinParams.push(tag);
|
|
169
170
|
}
|
|
170
171
|
if (Array.isArray(scene_ids) && scene_ids.length > 0) {
|
|
171
172
|
const placeholders = scene_ids.map(() => "?").join(",");
|
|
172
173
|
conditions.push(`s.scene_id IN (${placeholders})`);
|
|
173
|
-
|
|
174
|
+
conditionParams.push(...scene_ids);
|
|
174
175
|
}
|
|
175
176
|
if (part !== undefined) {
|
|
176
177
|
conditions.push("s.part = ?");
|
|
177
|
-
|
|
178
|
+
conditionParams.push(part);
|
|
178
179
|
}
|
|
179
180
|
if (chapter !== undefined) {
|
|
180
181
|
conditions.push("s.chapter = ?");
|
|
181
|
-
|
|
182
|
+
conditionParams.push(chapter);
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
let query = `
|
|
@@ -202,7 +203,7 @@ export function buildReviewBundlePlan(dbHandle, {
|
|
|
202
203
|
}
|
|
203
204
|
query += ` WHERE ${conditions.join(" AND ")}`;
|
|
204
205
|
|
|
205
|
-
const rows = dbHandle.prepare(query).all(...
|
|
206
|
+
const rows = dbHandle.prepare(query).all(...joinParams, ...conditionParams).sort(sceneSort);
|
|
206
207
|
if (rows.length === 0) {
|
|
207
208
|
throw new ReviewBundlePlanError(
|
|
208
209
|
"NO_RESULTS",
|
|
@@ -442,7 +442,7 @@ export function renderReviewBundlePdf(dbHandle, plan, { generatedAt, syncDir: sy
|
|
|
442
442
|
doc.moveDown(0.2);
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
if (scene.logline) {
|
|
445
|
+
if (profile === "outline_discussion" && scene.logline) {
|
|
446
446
|
doc.fontSize(10).font("Helvetica-Oblique");
|
|
447
447
|
const textWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
448
448
|
doc.text(`"${scene.logline}"`, { align: "left", width: textWidth });
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Manual Script Conventions
|
|
2
|
+
|
|
3
|
+
Manual scripts under this folder should use `mcp-result.mjs` for MCP tool parsing.
|
|
4
|
+
|
|
5
|
+
## Use the shared parser
|
|
6
|
+
|
|
7
|
+
Import and call:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { callToolParsed } from "./mcp-result.mjs";
|
|
11
|
+
|
|
12
|
+
const result = await callToolParsed(client, "get_scene_prose", { scene_id: "sc-001" });
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`callToolParsed(...)` returns:
|
|
16
|
+
|
|
17
|
+
- `raw`: original MCP response object
|
|
18
|
+
- `text`: `raw.content?.[0]?.text ?? ""`
|
|
19
|
+
- `data`: parsed JSON object when `text` is valid JSON, otherwise `null`
|
|
20
|
+
- `structured`: `raw.structuredContent` (if present)
|
|
21
|
+
- `isError`: `true` when `raw.isError` is set or parsed `data.ok === false`
|
|
22
|
+
|
|
23
|
+
## Why this is required
|
|
24
|
+
|
|
25
|
+
Some tools now return advisory metadata in `structuredContent` (for example stale-metadata guidance on prose tools). Scripts that only read `content[0].text` can silently lose those warnings.
|
|
26
|
+
|
|
27
|
+
Using `callToolParsed` keeps text, JSON payloads, and structured metadata handled consistently across all manual scripts.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function parseToolResult(result) {
|
|
2
|
+
const text = result?.content?.[0]?.text ?? "";
|
|
3
|
+
let data = null;
|
|
4
|
+
if (text) {
|
|
5
|
+
try {
|
|
6
|
+
data = JSON.parse(text);
|
|
7
|
+
} catch {
|
|
8
|
+
data = null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
raw: result,
|
|
14
|
+
text,
|
|
15
|
+
data,
|
|
16
|
+
structured: result?.structuredContent,
|
|
17
|
+
isError: Boolean(
|
|
18
|
+
result?.isError
|
|
19
|
+
|| (data && typeof data === "object" && data.ok === false)
|
|
20
|
+
),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function callToolParsed(client, name, args = {}) {
|
|
25
|
+
const result = await client.callTool({ name, arguments: args });
|
|
26
|
+
return parseToolResult(result);
|
|
27
|
+
}
|
|
@@ -3,6 +3,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import process from "process";
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
+
import { callToolParsed } from "./mcp-result.mjs";
|
|
6
7
|
|
|
7
8
|
async function main() {
|
|
8
9
|
const [projectId, profile, outputDir, ...flags] = process.argv.slice(2);
|
|
@@ -64,29 +65,29 @@ async function main() {
|
|
|
64
65
|
await client.connect(transport);
|
|
65
66
|
|
|
66
67
|
if (!skipPreview) {
|
|
67
|
-
const previewResult = await client
|
|
68
|
-
|
|
69
|
-
arguments: baseArguments,
|
|
70
|
-
});
|
|
71
|
-
const previewText = previewResult.content?.[0]?.text ?? "";
|
|
68
|
+
const previewResult = await callToolParsed(client, "preview_review_bundle", baseArguments);
|
|
69
|
+
const previewText = previewResult.text;
|
|
72
70
|
console.log("\n=== preview_review_bundle ===");
|
|
73
71
|
console.log(previewText);
|
|
72
|
+
if (previewResult.structured) {
|
|
73
|
+
console.log("structuredContent:", JSON.stringify(previewResult.structured, null, 2));
|
|
74
|
+
}
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
const result = await client
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
...baseArguments,
|
|
80
|
-
output_dir: outputDir,
|
|
81
|
-
}
|
|
77
|
+
const result = await callToolParsed(client, "create_review_bundle", {
|
|
78
|
+
...baseArguments,
|
|
79
|
+
output_dir: outputDir,
|
|
82
80
|
});
|
|
83
81
|
|
|
84
|
-
const resultText = result.
|
|
82
|
+
const resultText = result.text;
|
|
85
83
|
console.log("\n=== create_review_bundle ===");
|
|
86
84
|
console.log(resultText);
|
|
85
|
+
if (result.structured) {
|
|
86
|
+
console.log("structuredContent:", JSON.stringify(result.structured, null, 2));
|
|
87
|
+
}
|
|
87
88
|
|
|
88
89
|
if (showFiles) {
|
|
89
|
-
const parsed =
|
|
90
|
+
const parsed = result.data ?? {};
|
|
90
91
|
if (parsed.ok && parsed.output_paths) {
|
|
91
92
|
printArtifactExcerpt("Bundle Markdown", parsed.output_paths.bundle_markdown);
|
|
92
93
|
printArtifactExcerpt("Manifest JSON", parsed.output_paths.manifest_json);
|
|
@@ -2,13 +2,11 @@ import path from "node:path";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
4
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
import { callToolParsed } from "./mcp-result.mjs";
|
|
5
6
|
|
|
6
7
|
async function callCreateBundle(client, args) {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
arguments: args,
|
|
10
|
-
});
|
|
11
|
-
console.log(JSON.stringify(result, null, 2));
|
|
8
|
+
const parsed = await callToolParsed(client, "create_review_bundle", args);
|
|
9
|
+
console.log(JSON.stringify(parsed.raw, null, 2));
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
async function main() {
|
|
@@ -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
|
}
|