@hanna84/mcp-writing 3.24.1 → 3.25.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 +7 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/index.js +99 -0
- package/src/tools/metadata.js +86 -5
- package/src/tools/search.js +7 -3
- package/src/workflows/workflow-catalogue.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,16 @@ 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.25.0](https://github.com/hannasdev/mcp-writing/compare/v3.24.1...v3.25.0)
|
|
8
|
+
|
|
9
|
+
- feat: clarify M1 workflow and relationship responses [`#235`](https://github.com/hannasdev/mcp-writing/pull/235)
|
|
10
|
+
|
|
7
11
|
#### [v3.24.1](https://github.com/hannasdev/mcp-writing/compare/v3.24.0...v3.24.1)
|
|
8
12
|
|
|
13
|
+
> 6 June 2026
|
|
14
|
+
|
|
9
15
|
- test: add relationship boundary release readiness coverage [`#234`](https://github.com/hannasdev/mcp-writing/pull/234)
|
|
16
|
+
- Release 3.24.1 [`158ff88`](https://github.com/hannasdev/mcp-writing/commit/158ff8817e7bb3aefc92b2c4c05c8c2b36c83ef3)
|
|
10
17
|
|
|
11
18
|
#### [v3.24.0](https://github.com/hannasdev/mcp-writing/compare/v3.23.2...v3.24.0)
|
|
12
19
|
|
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
|
|
|
58
58
|
|
|
59
59
|
### `describe_workflows` surface redesign
|
|
60
60
|
|
|
61
|
-
`describe_workflows` now exposes an outcome-first, discovery-first workflow map. This
|
|
61
|
+
`describe_workflows` now exposes an outcome-first, discovery-first workflow map. This was a breaking change if your prompts or automation depend on previous workflow IDs or ordering; the newer `recommended_next_actions` tier is additive and appears before the full catalogue.
|
|
62
62
|
|
|
63
63
|
Update integrations using this mapping:
|
|
64
64
|
|
|
@@ -153,7 +153,7 @@ Goal: keep indexes accurate without manually re-tagging everything.
|
|
|
153
153
|
|
|
154
154
|
1. After rewriting scenes, call `enrich_scene` to re-derive lightweight metadata from current prose.
|
|
155
155
|
2. Use `update_scene_metadata` for intentional editorial fields (for example, beat, POV, status, and tags). It rejects scene `characters` and `places`; use `connect_character_place_evidence` when a scene proves paired sheet-backed character/place evidence, `connect_scene_character_evidence` for character-only evidence, and `connect_scene_place_evidence` for place-only evidence. Use `audit_relationship_metadata` for retained sidecar/frontmatter relationship fields. Use `list_chapters` plus `assign_scene_to_chapter` or `move_scene` for chapter placement and ordering.
|
|
156
|
-
3. Use `search_metadata` and `find_scenes` to verify scenes are discoverable under
|
|
156
|
+
3. Use `search_metadata` for keyword/FTS metadata searches across indexed titles, loglines, tags, characters, places, and versions, and use `find_scenes` to verify scenes are discoverable under structured filters. After identifying likely scenes, use `get_scene_prose` for prose context.
|
|
157
157
|
|
|
158
158
|
Outcome: your AI assistant can reliably find the right scenes without drifting from the manuscript.
|
|
159
159
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -327,6 +327,100 @@ function maxScenesNextStep(matchedCount) {
|
|
|
327
327
|
return `Re-run with max_scenes set to at least ${matchedCount}.`;
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
function buildRecommendedNextActions({
|
|
331
|
+
sceneCount,
|
|
332
|
+
setupContract,
|
|
333
|
+
dbMigrationWarnings,
|
|
334
|
+
}) {
|
|
335
|
+
const recommendations = [];
|
|
336
|
+
|
|
337
|
+
if (dbMigrationWarnings.length > 0) {
|
|
338
|
+
recommendations.push({
|
|
339
|
+
id: "refresh_index_after_migration_warning",
|
|
340
|
+
label: "Refresh indexed state",
|
|
341
|
+
tool: "sync",
|
|
342
|
+
workflow_id: "parity_recovery",
|
|
343
|
+
priority: 10,
|
|
344
|
+
reason: "Database migration warnings are present, so indexed metadata may need a sync before other work.",
|
|
345
|
+
next_step: "Call sync, then re-run describe_workflows before mutating project state.",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (sceneCount === 0) {
|
|
350
|
+
recommendations.push({
|
|
351
|
+
id: "index_project_content",
|
|
352
|
+
label: "Index project content",
|
|
353
|
+
tool: "sync",
|
|
354
|
+
workflow_id: "first_time_setup",
|
|
355
|
+
priority: 20,
|
|
356
|
+
reason: "No scenes are indexed yet, so discovery and scene-reading tools have no project content to inspect.",
|
|
357
|
+
next_step: "Call sync to index the configured writing folder, then use find_scenes without filters to confirm project_ids.",
|
|
358
|
+
});
|
|
359
|
+
recommendations.push({
|
|
360
|
+
id: "inspect_runtime_configuration",
|
|
361
|
+
label: "Inspect runtime configuration",
|
|
362
|
+
tool: "get_runtime_config",
|
|
363
|
+
workflow_id: "first_time_setup",
|
|
364
|
+
priority: 30,
|
|
365
|
+
reason: "Runtime paths and writable state determine whether sync can index the intended manuscript folder.",
|
|
366
|
+
next_step: "Call get_runtime_config if sync still indexes no scenes or the configured writing folder looks wrong.",
|
|
367
|
+
});
|
|
368
|
+
recommendations.push({
|
|
369
|
+
id: "diagnose_empty_index",
|
|
370
|
+
label: "Diagnose indexed structure",
|
|
371
|
+
tool: "diagnose_structure",
|
|
372
|
+
workflow_id: "parity_recovery",
|
|
373
|
+
priority: 35,
|
|
374
|
+
reason: "If sync does not discover scenes, structure diagnostics can explain missing or ambiguous manuscript representations.",
|
|
375
|
+
next_step: "Call diagnose_structure after sync if the index is still empty or project structure looks unexpected.",
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
recommendations.push({
|
|
379
|
+
id: "discover_indexed_scenes",
|
|
380
|
+
label: "Discover indexed scenes",
|
|
381
|
+
tool: "find_scenes",
|
|
382
|
+
workflow_id: "question_driven_discovery",
|
|
383
|
+
priority: 20,
|
|
384
|
+
reason: "Scenes are indexed; metadata-first discovery is the safest starting point for most manuscript questions.",
|
|
385
|
+
next_step: "Call find_scenes with project_id or lightweight filters before loading prose.",
|
|
386
|
+
});
|
|
387
|
+
recommendations.push({
|
|
388
|
+
id: "keyword_metadata_search",
|
|
389
|
+
label: "Search metadata keywords",
|
|
390
|
+
tool: "search_metadata",
|
|
391
|
+
workflow_id: "question_driven_discovery",
|
|
392
|
+
priority: 30,
|
|
393
|
+
reason: "Use keyword/FTS metadata search when you know likely titles, logline words, tags, characters, places, or versions.",
|
|
394
|
+
next_step: "Search exact metadata keywords, then open likely scenes with get_scene_prose if prose context is needed.",
|
|
395
|
+
});
|
|
396
|
+
recommendations.push({
|
|
397
|
+
id: "read_likely_scene",
|
|
398
|
+
label: "Read a likely scene",
|
|
399
|
+
tool: "get_scene_prose",
|
|
400
|
+
workflow_id: "targeted_scene_reading",
|
|
401
|
+
priority: 35,
|
|
402
|
+
reason: "Prose should be loaded only after metadata has identified a likely scene.",
|
|
403
|
+
next_step: "Call get_scene_prose with a specific scene_id and project_id once discovery has narrowed the target.",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (setupContract?.setup_recommended) {
|
|
408
|
+
recommendations.push({
|
|
409
|
+
id: "review_styleguide_setup",
|
|
410
|
+
label: "Review styleguide setup",
|
|
411
|
+
tool: "setup_prose_styleguide_config",
|
|
412
|
+
workflow_id: "styleguide_setup_new",
|
|
413
|
+
priority: 40,
|
|
414
|
+
reason: `Styleguide setup status is ${setupContract.styleguide_setup_status}.`,
|
|
415
|
+
next_step: "Follow context.setup_contract.plan_preview before running styleguide drift checks or enforcement workflows.",
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return recommendations
|
|
420
|
+
.sort((a, b) => a.priority - b.priority)
|
|
421
|
+
.slice(0, 5);
|
|
422
|
+
}
|
|
423
|
+
|
|
330
424
|
// ---------------------------------------------------------------------------
|
|
331
425
|
// MCP server factory
|
|
332
426
|
// ---------------------------------------------------------------------------
|
|
@@ -444,6 +538,11 @@ function createMcpServer() {
|
|
|
444
538
|
pending_proposals: pendingProposals.size,
|
|
445
539
|
db_migration_warnings: DB_STARTUP_WARNINGS,
|
|
446
540
|
},
|
|
541
|
+
recommended_next_actions: buildRecommendedNextActions({
|
|
542
|
+
sceneCount: scene_count,
|
|
543
|
+
setupContract: setupContractContextCheck.value,
|
|
544
|
+
dbMigrationWarnings: DB_STARTUP_WARNINGS,
|
|
545
|
+
}),
|
|
447
546
|
workflows: WORKFLOW_CATALOGUE,
|
|
448
547
|
notes: [
|
|
449
548
|
"Never write JavaScript or shell scripts to invoke tools. Call them directly.",
|
package/src/tools/metadata.js
CHANGED
|
@@ -130,6 +130,40 @@ function buildRelationshipMetadataBoundaryDetails({ projectId, sceneId, blockedF
|
|
|
130
130
|
};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function relationshipEvidenceNotFoundDetails({ lookupKind, input, projectId, sceneId }) {
|
|
134
|
+
const details = {
|
|
135
|
+
lookup_kind: lookupKind,
|
|
136
|
+
input,
|
|
137
|
+
project_id: projectId,
|
|
138
|
+
};
|
|
139
|
+
if (sceneId !== undefined) {
|
|
140
|
+
details.scene_id = sceneId;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (lookupKind === "scene") {
|
|
144
|
+
return {
|
|
145
|
+
...details,
|
|
146
|
+
next_step: "Use find_scenes with the project_id to confirm the canonical scene_id, then retry the relationship evidence tool.",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (lookupKind === "character") {
|
|
151
|
+
return {
|
|
152
|
+
...details,
|
|
153
|
+
next_step: "Use list_characters to find the stable character_id for this project or universe, then retry the relationship evidence tool.",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
...details,
|
|
159
|
+
next_step: "Use list_places to find the stable place_id for this project or universe, then retry the relationship evidence tool.",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function alreadyLinkedRelationshipNextStep({ entityKind }) {
|
|
164
|
+
return `This ${entityKind} was already linked to the scene, so no canonical relationship rows changed. Use the scene_relationships field in this response to inspect current links; call get_scene_prose with the scene_id and project_id if prose context is needed.`;
|
|
165
|
+
}
|
|
166
|
+
|
|
133
167
|
function persistReferenceDocLink({ filePath, syncDir, targetDocId, relation }) {
|
|
134
168
|
const syncDirAbs = path.resolve(syncDir);
|
|
135
169
|
const syncDirReal = resolveBoundaryRootReal(syncDirAbs);
|
|
@@ -862,16 +896,42 @@ export function registerMetadataTools(s, {
|
|
|
862
896
|
WHERE scene_id = ? AND project_id = ?
|
|
863
897
|
`).get(scene_id, project_id);
|
|
864
898
|
if (!scene) {
|
|
865
|
-
return errorResponse(
|
|
899
|
+
return errorResponse(
|
|
900
|
+
"NOT_FOUND",
|
|
901
|
+
`Scene '${scene_id}' not found in project '${project_id}'.`,
|
|
902
|
+
relationshipEvidenceNotFoundDetails({
|
|
903
|
+
lookupKind: "scene",
|
|
904
|
+
input: scene_id,
|
|
905
|
+
projectId: project_id,
|
|
906
|
+
})
|
|
907
|
+
);
|
|
866
908
|
}
|
|
867
909
|
|
|
868
910
|
const character = resolveCharacterForProject(db, { characterId: character_id, projectId: project_id });
|
|
869
911
|
if (!character) {
|
|
870
|
-
return errorResponse(
|
|
912
|
+
return errorResponse(
|
|
913
|
+
"NOT_FOUND",
|
|
914
|
+
`Character '${character_id}' is not indexed for project '${project_id}' or its universe.`,
|
|
915
|
+
relationshipEvidenceNotFoundDetails({
|
|
916
|
+
lookupKind: "character",
|
|
917
|
+
input: character_id,
|
|
918
|
+
projectId: project_id,
|
|
919
|
+
sceneId: scene_id,
|
|
920
|
+
})
|
|
921
|
+
);
|
|
871
922
|
}
|
|
872
923
|
const place = resolvePlaceForProject(db, { placeId: place_id, projectId: project_id });
|
|
873
924
|
if (!place) {
|
|
874
|
-
return errorResponse(
|
|
925
|
+
return errorResponse(
|
|
926
|
+
"NOT_FOUND",
|
|
927
|
+
`Place '${place_id}' is not indexed for project '${project_id}' or its universe.`,
|
|
928
|
+
relationshipEvidenceNotFoundDetails({
|
|
929
|
+
lookupKind: "place",
|
|
930
|
+
input: place_id,
|
|
931
|
+
projectId: project_id,
|
|
932
|
+
sceneId: scene_id,
|
|
933
|
+
})
|
|
934
|
+
);
|
|
875
935
|
}
|
|
876
936
|
|
|
877
937
|
const before = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
|
|
@@ -1005,12 +1065,29 @@ export function registerMetadataTools(s, {
|
|
|
1005
1065
|
WHERE scene_id = ? AND project_id = ?
|
|
1006
1066
|
`).get(scene_id, project_id);
|
|
1007
1067
|
if (!scene) {
|
|
1008
|
-
return errorResponse(
|
|
1068
|
+
return errorResponse(
|
|
1069
|
+
"NOT_FOUND",
|
|
1070
|
+
`Scene '${scene_id}' not found in project '${project_id}'.`,
|
|
1071
|
+
relationshipEvidenceNotFoundDetails({
|
|
1072
|
+
lookupKind: "scene",
|
|
1073
|
+
input: scene_id,
|
|
1074
|
+
projectId: project_id,
|
|
1075
|
+
})
|
|
1076
|
+
);
|
|
1009
1077
|
}
|
|
1010
1078
|
|
|
1011
1079
|
if (!resolveEntity(entity_id)) {
|
|
1012
1080
|
const label = entityKind[0].toUpperCase() + entityKind.slice(1);
|
|
1013
|
-
return errorResponse(
|
|
1081
|
+
return errorResponse(
|
|
1082
|
+
"NOT_FOUND",
|
|
1083
|
+
`${label} '${entity_id}' is not indexed for project '${project_id}' or its universe.`,
|
|
1084
|
+
relationshipEvidenceNotFoundDetails({
|
|
1085
|
+
lookupKind: entityKind,
|
|
1086
|
+
input: entity_id,
|
|
1087
|
+
projectId: project_id,
|
|
1088
|
+
sceneId: scene_id,
|
|
1089
|
+
})
|
|
1090
|
+
);
|
|
1014
1091
|
}
|
|
1015
1092
|
|
|
1016
1093
|
const before = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
|
|
@@ -1095,7 +1172,11 @@ export function registerMetadataTools(s, {
|
|
|
1095
1172
|
return jsonResponse({
|
|
1096
1173
|
ok: true,
|
|
1097
1174
|
action: "connected",
|
|
1175
|
+
outcome: alreadyLinked ? "no_op" : "connected",
|
|
1098
1176
|
already_linked: alreadyLinked,
|
|
1177
|
+
...(alreadyLinked
|
|
1178
|
+
? { next_step: alreadyLinkedRelationshipNextStep({ entityKind }) }
|
|
1179
|
+
: {}),
|
|
1099
1180
|
scene_id,
|
|
1100
1181
|
project_id,
|
|
1101
1182
|
[idField]: entity_id,
|
package/src/tools/search.js
CHANGED
|
@@ -662,9 +662,9 @@ export function registerSearchTools(s, {
|
|
|
662
662
|
// ---- search_metadata -----------------------------------------------------
|
|
663
663
|
s.tool(
|
|
664
664
|
"search_metadata",
|
|
665
|
-
"
|
|
665
|
+
"Keyword/FTS metadata search across scene titles, loglines (synopsis/logline text fields), and indexed metadata keywords (tags/characters/places/versions). Use this when you know likely words or exact metadata values but do not know the scene_id or chapter. This is not semantic search and does not search prose text; use find_scenes for structured filters and get_scene_prose after identifying likely scenes. Supports pagination via page/page_size and auto-paginates large result sets with total_count.",
|
|
666
666
|
{
|
|
667
|
-
query: z.string().describe("
|
|
667
|
+
query: z.string().describe("Keyword search terms from indexed metadata (e.g. 'hospital' or a quoted character/place/tag phrase). FTS5 syntax supported."),
|
|
668
668
|
page: z.number().int().min(1).optional().describe("Optional page number for paginated responses (1-based)."),
|
|
669
669
|
page_size: z.number().int().min(1).max(200).optional().describe("Optional page size for paginated responses (default: 20, max: 200)."),
|
|
670
670
|
},
|
|
@@ -682,7 +682,11 @@ export function registerSearchTools(s, {
|
|
|
682
682
|
}
|
|
683
683
|
|
|
684
684
|
if (totalCount === 0) {
|
|
685
|
-
return errorResponse("NO_RESULTS", "No scenes matched the search query."
|
|
685
|
+
return errorResponse("NO_RESULTS", "No scenes matched the keyword metadata search query.", {
|
|
686
|
+
search_type: "keyword_metadata_fts",
|
|
687
|
+
searched_fields: ["scene.title", "scene.logline", "tags", "characters", "places", "versions"],
|
|
688
|
+
next_step: "Try exact metadata keywords, quoted character/place/tag names, or find_scenes structured filters. After finding likely scenes, use get_scene_prose for prose context; search_metadata is not semantic or prose search.",
|
|
689
|
+
});
|
|
686
690
|
}
|
|
687
691
|
|
|
688
692
|
const shouldPaginate = totalCount > DEFAULT_METADATA_PAGE_SIZE || page !== undefined || page_size !== undefined;
|
|
@@ -5,7 +5,7 @@ export const WORKFLOW_CATALOGUE = [
|
|
|
5
5
|
use_when: "Start here for most sessions: when the user has a manuscript question, you need to narrow scope, or you are not yet sure which scene matters.",
|
|
6
6
|
steps: [
|
|
7
7
|
{ tool: "find_scenes", note: "Use structured metadata filters first when the question already suggests characters, beats, tags, parts, chapters, or POV; numeric chapter values are read-scope aliases, while structure changes require canonical chapter_id workflows." },
|
|
8
|
-
{ tool: "search_metadata", note: "Use this when
|
|
8
|
+
{ tool: "search_metadata", note: "Use this when likely title, logline, tag, character, place, or version keywords are known but structured filters are not cleanly available; it is not semantic or prose search." },
|
|
9
9
|
{ tool: "get_scene_prose", note: "Escalate to prose only after likely scenes have been identified and metadata is no longer enough." },
|
|
10
10
|
{ tool: "flag_scene", note: "Use only when the current task naturally leads to recording a follow-up note for later editorial attention." },
|
|
11
11
|
],
|