@hanna84/mcp-writing 2.5.1 → 2.7.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/importer.js +10 -0
- package/index.js +187 -7
- package/package.json +1 -1
- package/prose-styleguide.js +6 -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
|
+
#### [v2.7.0](https://github.com/hannasdev/mcp-writing.git
|
|
8
|
+
/compare/v2.6.0...v2.7.0)
|
|
9
|
+
|
|
10
|
+
- feat(workflows): add describe_workflows entry-point tool [`#90`](https://github.com/hannasdev/mcp-writing.git
|
|
11
|
+
/pull/90)
|
|
12
|
+
|
|
13
|
+
#### [v2.6.0](https://github.com/hannasdev/mcp-writing.git
|
|
14
|
+
/compare/v2.5.1...v2.6.0)
|
|
15
|
+
|
|
16
|
+
> 26 April 2026
|
|
17
|
+
|
|
18
|
+
- feat(styleguide): improve AI navigation with next_step hints and identifier validation [`#89`](https://github.com/hannasdev/mcp-writing.git
|
|
19
|
+
/pull/89)
|
|
20
|
+
- Release 2.6.0 [`1083d55`](https://github.com/hannasdev/mcp-writing.git
|
|
21
|
+
/commit/1083d556f1d7b23a1c1290b84735014878187b47)
|
|
22
|
+
|
|
7
23
|
#### [v2.5.1](https://github.com/hannasdev/mcp-writing.git
|
|
8
24
|
/compare/v2.5.0...v2.5.1)
|
|
9
25
|
|
|
26
|
+
> 26 April 2026
|
|
27
|
+
|
|
10
28
|
- docs(prd): update README phase status and add refactoring PRD [`#88`](https://github.com/hannasdev/mcp-writing.git
|
|
11
29
|
/pull/88)
|
|
30
|
+
- Release 2.5.1 [`4c78109`](https://github.com/hannasdev/mcp-writing.git
|
|
31
|
+
/commit/4c781092152e3ae52715bfe05dbfda143f9c37c7)
|
|
12
32
|
|
|
13
33
|
#### [v2.5.0](https://github.com/hannasdev/mcp-writing.git
|
|
14
34
|
/compare/v2.4.0...v2.5.0)
|
package/importer.js
CHANGED
|
@@ -32,6 +32,16 @@ export function validateProjectId(projectId) {
|
|
|
32
32
|
return { ok: true };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function validateUniverseId(universeId) {
|
|
36
|
+
if (typeof universeId !== "string" || universeId.trim().length === 0) {
|
|
37
|
+
return { ok: false, reason: "universe_id must be a non-empty string." };
|
|
38
|
+
}
|
|
39
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(universeId)) {
|
|
40
|
+
return { ok: false, reason: "universe_id may contain only lowercase letters, numbers, and hyphens, and must not start or end with a hyphen." };
|
|
41
|
+
}
|
|
42
|
+
return { ok: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
// Parse "NNN Title [binder_id].txt" -> { seq, rawTitle, binderId, ext } or null
|
|
36
46
|
function parseFilename(filename) {
|
|
37
47
|
const m = filename.match(/^(\d+)\s+(.+?)\s*\[(\d+)\]\.(txt|md)$/);
|
package/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { openDb } from "./db.js";
|
|
|
15
15
|
import { syncAll, isSyncDirWritable, getSyncOwnershipDiagnostics, getFileWriteDiagnostics, writeMeta, readMeta, indexSceneFile, normalizeSceneMetaForPath, sidecarPath } from "./sync.js";
|
|
16
16
|
import { isGitAvailable, isGitRepository, initGitRepository, createSnapshot, listSnapshots, getSceneProseAtCommit, getHeadCommitHash } from "./git.js";
|
|
17
17
|
import { renderCharacterArcTemplate, renderCharacterSheetTemplate, renderPlaceSheetTemplate, slugifyEntityName } from "./world-entity-templates.js";
|
|
18
|
-
import { importScrivenerSync, validateProjectId } from "./importer.js";
|
|
18
|
+
import { importScrivenerSync, validateProjectId, validateUniverseId } from "./importer.js";
|
|
19
19
|
import { ASYNC_PROGRESS_PREFIX } from "./async-progress.js";
|
|
20
20
|
import {
|
|
21
21
|
STYLEGUIDE_CONFIG_BASENAME,
|
|
@@ -845,12 +845,160 @@ async function gracefulShutdown(signal) {
|
|
|
845
845
|
process.exit(0);
|
|
846
846
|
}
|
|
847
847
|
|
|
848
|
+
function maxScenesNextStep(matchedCount) {
|
|
849
|
+
return `Re-run with max_scenes set to at least ${matchedCount}.`;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const WORKFLOW_CATALOGUE = [
|
|
853
|
+
{
|
|
854
|
+
id: "first_time_setup",
|
|
855
|
+
label: "First-time setup",
|
|
856
|
+
use_when: "Connecting to a project for the first time or verifying the runtime is correctly configured.",
|
|
857
|
+
steps: [
|
|
858
|
+
{ tool: "get_runtime_config", note: "Verify sync dir, writability, and git availability." },
|
|
859
|
+
{ tool: "sync", note: "Index scenes from disk." },
|
|
860
|
+
],
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
id: "styleguide_setup_new",
|
|
864
|
+
label: "Styleguide setup (new project)",
|
|
865
|
+
use_when: "No prose styleguide config exists and you want to create one based on the manuscript's existing conventions.",
|
|
866
|
+
steps: [
|
|
867
|
+
{ tool: "describe_workflows", note: "Check context.scene_count; use that value as max_scenes in the next call." },
|
|
868
|
+
{ tool: "bootstrap_prose_styleguide_config", note: "Detect dominant conventions. Confirm suggestions with the user before applying." },
|
|
869
|
+
{ tool: "setup_prose_styleguide_config", note: "Create config at project_root scope if context.styleguide_exists.project_root is false. Requires language (e.g. 'english_us')." },
|
|
870
|
+
{ tool: "update_prose_styleguide_config", note: "Apply the fields accepted from bootstrap suggestions." },
|
|
871
|
+
],
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
id: "styleguide_drift_check",
|
|
875
|
+
label: "Styleguide drift check",
|
|
876
|
+
use_when: "A styleguide config exists and you want to check whether recent scenes conform to it.",
|
|
877
|
+
steps: [
|
|
878
|
+
{ tool: "get_prose_styleguide_config", note: "Confirm the currently resolved config." },
|
|
879
|
+
{ tool: "check_prose_styleguide_drift", note: "Detect non-conforming scenes. Pass project_id from context.project_id and set max_scenes from context.scene_count." },
|
|
880
|
+
{ tool: "update_prose_styleguide_config", note: "If drift found and user approves, update config or note the outliers." },
|
|
881
|
+
],
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
id: "manuscript_exploration",
|
|
885
|
+
label: "Manuscript exploration",
|
|
886
|
+
use_when: "Answering questions about the manuscript, finding scenes, or getting an overview.",
|
|
887
|
+
steps: [
|
|
888
|
+
{ tool: "find_scenes", note: "Filter by character, beat, tag, part, chapter, or POV. No filters returns all scenes." },
|
|
889
|
+
{ tool: "get_scene_prose", note: "Load prose for specific scenes identified by find_scenes." },
|
|
890
|
+
{ tool: "get_chapter_prose", note: "Load all prose for a chapter. Use sparingly — large chapters can overflow context." },
|
|
891
|
+
{ tool: "search_metadata", note: "Full-text search across scene metadata fields." },
|
|
892
|
+
],
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
id: "prose_editing",
|
|
896
|
+
label: "Prose editing",
|
|
897
|
+
use_when: "Revising scene prose. All edits require explicit user confirmation before writing.",
|
|
898
|
+
steps: [
|
|
899
|
+
{ tool: "find_scenes", note: "Identify the target scene." },
|
|
900
|
+
{ tool: "get_scene_prose", note: "Load the current prose." },
|
|
901
|
+
{ tool: "propose_edit", note: "Stage a revision; returns a diff preview and a proposal_id." },
|
|
902
|
+
{ tool: "commit_edit", note: "Write the revision after the user confirms. Runs preflight checks before writing." },
|
|
903
|
+
{ tool: "discard_edit", note: "Reject the revision if the user does not approve." },
|
|
904
|
+
],
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
id: "character_management",
|
|
908
|
+
label: "Character management",
|
|
909
|
+
use_when: "Finding characters, reading their sheets, or updating character details.",
|
|
910
|
+
steps: [
|
|
911
|
+
{ tool: "list_characters", note: "Find character_id values." },
|
|
912
|
+
{ tool: "get_character_sheet", note: "Read full character details." },
|
|
913
|
+
{ tool: "create_character_sheet", note: "Create a new character. Requires exactly one of project_id or universe_id." },
|
|
914
|
+
{ tool: "update_character_sheet", note: "Edit character metadata." },
|
|
915
|
+
],
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
id: "place_management",
|
|
919
|
+
label: "Place management",
|
|
920
|
+
use_when: "Finding locations, reading place sheets, or updating place details.",
|
|
921
|
+
steps: [
|
|
922
|
+
{ tool: "list_places", note: "Find place_id values." },
|
|
923
|
+
{ tool: "get_place_sheet", note: "Read full place details." },
|
|
924
|
+
{ tool: "create_place_sheet", note: "Create a new place. Requires exactly one of project_id or universe_id." },
|
|
925
|
+
{ tool: "update_place_sheet", note: "Edit place metadata." },
|
|
926
|
+
],
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
id: "review_bundle",
|
|
930
|
+
label: "Review bundle",
|
|
931
|
+
use_when: "Preparing a formatted bundle for human review (outline, editorial, or beta read profile).",
|
|
932
|
+
steps: [
|
|
933
|
+
{ tool: "preview_review_bundle", note: "Check which scenes would be included and the estimated size. Requires project_id and profile." },
|
|
934
|
+
{ tool: "create_review_bundle", note: "Generate the bundle. Requires project_id." },
|
|
935
|
+
],
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
id: "async_job_tracking",
|
|
939
|
+
label: "Async job tracking",
|
|
940
|
+
use_when: "A tool returned a job_id instead of an immediate result (e.g. import_scrivener_sync_async).",
|
|
941
|
+
steps: [
|
|
942
|
+
{ tool: "get_async_job_status", note: "Poll with the job_id until status is 'completed' or 'failed'." },
|
|
943
|
+
{ tool: "sync", note: "Call after a completed job that modified files on disk." },
|
|
944
|
+
],
|
|
945
|
+
},
|
|
946
|
+
];
|
|
947
|
+
|
|
848
948
|
// ---------------------------------------------------------------------------
|
|
849
949
|
// MCP server factory
|
|
850
950
|
// ---------------------------------------------------------------------------
|
|
851
951
|
function createMcpServer() {
|
|
852
952
|
const s = new McpServer({ name: "mcp-writing", version: MCP_SERVER_VERSION });
|
|
853
953
|
|
|
954
|
+
// ---- describe_workflows --------------------------------------------------
|
|
955
|
+
s.tool(
|
|
956
|
+
"describe_workflows",
|
|
957
|
+
"Return a map of available task workflows and the current project context. Call this at the start of a session or whenever you are unsure what to do next. Never write scripts to invoke tools — call them directly.",
|
|
958
|
+
{},
|
|
959
|
+
async () => {
|
|
960
|
+
const projectRow = db.prepare(
|
|
961
|
+
`SELECT project_id FROM scenes GROUP BY project_id ORDER BY COUNT(*) DESC, project_id ASC LIMIT 1`
|
|
962
|
+
).get();
|
|
963
|
+
const project_id = projectRow?.project_id ?? null;
|
|
964
|
+
|
|
965
|
+
const sceneCountRow = db.prepare(`SELECT COUNT(*) as count FROM scenes`).get();
|
|
966
|
+
const scene_count = sceneCountRow?.count ?? 0;
|
|
967
|
+
|
|
968
|
+
const syncRootConfigPath = path.join(SYNC_DIR, STYLEGUIDE_CONFIG_BASENAME);
|
|
969
|
+
const projectRootConfigPath = project_id
|
|
970
|
+
? path.join(resolveProjectRoot(project_id), STYLEGUIDE_CONFIG_BASENAME)
|
|
971
|
+
: null;
|
|
972
|
+
const universeSegment = project_id?.includes("/") ? project_id.split("/")[0] : null;
|
|
973
|
+
const universeRootConfigPath = universeSegment
|
|
974
|
+
? path.join(SYNC_DIR, "universes", universeSegment, STYLEGUIDE_CONFIG_BASENAME)
|
|
975
|
+
: null;
|
|
976
|
+
|
|
977
|
+
return jsonResponse({
|
|
978
|
+
ok: true,
|
|
979
|
+
context: {
|
|
980
|
+
project_id,
|
|
981
|
+
scene_count,
|
|
982
|
+
sync_dir: SYNC_DIR_ABS,
|
|
983
|
+
styleguide_exists: {
|
|
984
|
+
sync_root: fs.existsSync(syncRootConfigPath),
|
|
985
|
+
universe_root: universeRootConfigPath !== null && fs.existsSync(universeRootConfigPath),
|
|
986
|
+
project_root: projectRootConfigPath !== null && fs.existsSync(projectRootConfigPath),
|
|
987
|
+
},
|
|
988
|
+
git_available: GIT_AVAILABLE,
|
|
989
|
+
pending_proposals: pendingProposals.size,
|
|
990
|
+
},
|
|
991
|
+
workflows: WORKFLOW_CATALOGUE,
|
|
992
|
+
notes: [
|
|
993
|
+
"Never write JavaScript or shell scripts to invoke tools. Call them directly.",
|
|
994
|
+
"If a tool returns a next_step field (in a success or error response), follow it before trying anything else.",
|
|
995
|
+
"Use find_scenes without filters to discover what project_ids are indexed.",
|
|
996
|
+
"When calling bootstrap_prose_styleguide_config or check_prose_styleguide_drift, set max_scenes to context.scene_count to avoid the default limit.",
|
|
997
|
+
],
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
);
|
|
1001
|
+
|
|
854
1002
|
// ---- sync ----------------------------------------------------------------
|
|
855
1003
|
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 () => {
|
|
856
1004
|
const result = syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
|
|
@@ -1226,6 +1374,7 @@ function createMcpServer() {
|
|
|
1226
1374
|
matched_scenes: targetScenes.length,
|
|
1227
1375
|
max_scenes,
|
|
1228
1376
|
project_id,
|
|
1377
|
+
next_step: maxScenesNextStep(targetScenes.length),
|
|
1229
1378
|
}
|
|
1230
1379
|
);
|
|
1231
1380
|
}
|
|
@@ -1485,6 +1634,7 @@ function createMcpServer() {
|
|
|
1485
1634
|
config: draft.config,
|
|
1486
1635
|
inferred_defaults: draft.inferred_defaults,
|
|
1487
1636
|
warnings: draft.warnings,
|
|
1637
|
+
next_step: "Config created. Call update_prose_styleguide_config to apply field updates.",
|
|
1488
1638
|
});
|
|
1489
1639
|
}
|
|
1490
1640
|
);
|
|
@@ -1520,7 +1670,7 @@ function createMcpServer() {
|
|
|
1520
1670
|
ok: true,
|
|
1521
1671
|
styleguide: resolved,
|
|
1522
1672
|
next_step: resolved.setup_required
|
|
1523
|
-
? "No prose-styleguide.config.yaml was found.
|
|
1673
|
+
? "No prose-styleguide.config.yaml was found. Call setup_prose_styleguide_config (with language e.g. 'en') to create one at sync root or project root."
|
|
1524
1674
|
: "Config resolved successfully.",
|
|
1525
1675
|
});
|
|
1526
1676
|
}
|
|
@@ -1632,7 +1782,12 @@ function createMcpServer() {
|
|
|
1632
1782
|
return errorResponse(
|
|
1633
1783
|
"VALIDATION_ERROR",
|
|
1634
1784
|
`Matched ${targetScenes.length} scenes, which exceeds max_scenes=${max_scenes}.`,
|
|
1635
|
-
{
|
|
1785
|
+
{
|
|
1786
|
+
matched_scenes: targetScenes.length,
|
|
1787
|
+
max_scenes,
|
|
1788
|
+
project_id,
|
|
1789
|
+
next_step: maxScenesNextStep(targetScenes.length),
|
|
1790
|
+
}
|
|
1636
1791
|
);
|
|
1637
1792
|
}
|
|
1638
1793
|
|
|
@@ -1669,7 +1824,7 @@ function createMcpServer() {
|
|
|
1669
1824
|
checked_scenes: sceneSignals.length,
|
|
1670
1825
|
unreadable_scenes: unreadableScenes,
|
|
1671
1826
|
suggested_config: suggestedConfig,
|
|
1672
|
-
next_step:
|
|
1827
|
+
next_step: `To apply: (1) If no project-scoped config exists yet, call setup_prose_styleguide_config first with scope=project_root, project_id=${project_id}, and language (e.g. 'en'). (2) Then call update_prose_styleguide_config with the fields from suggested_config you want to apply.`,
|
|
1673
1828
|
scene_signals: include_scene_signals ? sceneSignals : undefined,
|
|
1674
1829
|
});
|
|
1675
1830
|
}
|
|
@@ -1886,7 +2041,12 @@ function createMcpServer() {
|
|
|
1886
2041
|
return errorResponse(
|
|
1887
2042
|
"VALIDATION_ERROR",
|
|
1888
2043
|
`Matched ${targetScenes.length} scenes, which exceeds max_scenes=${max_scenes}.`,
|
|
1889
|
-
{
|
|
2044
|
+
{
|
|
2045
|
+
matched_scenes: targetScenes.length,
|
|
2046
|
+
max_scenes,
|
|
2047
|
+
project_id,
|
|
2048
|
+
next_step: maxScenesNextStep(targetScenes.length),
|
|
2049
|
+
}
|
|
1890
2050
|
);
|
|
1891
2051
|
}
|
|
1892
2052
|
|
|
@@ -2511,9 +2671,19 @@ function createMcpServer() {
|
|
|
2511
2671
|
if (!SYNC_DIR_WRITABLE) {
|
|
2512
2672
|
return errorResponse("READ_ONLY", "Cannot create character sheet: sync dir is read-only.");
|
|
2513
2673
|
}
|
|
2514
|
-
|
|
2674
|
+
const hasProjectId = project_id !== undefined;
|
|
2675
|
+
const hasUniverseId = universe_id !== undefined;
|
|
2676
|
+
if ((hasProjectId && hasUniverseId) || (!hasProjectId && !hasUniverseId)) {
|
|
2515
2677
|
return errorResponse("VALIDATION_ERROR", "Provide exactly one of project_id or universe_id.");
|
|
2516
2678
|
}
|
|
2679
|
+
if (hasProjectId) {
|
|
2680
|
+
const check = validateProjectId(project_id);
|
|
2681
|
+
if (!check.ok) return errorResponse("INVALID_PROJECT_ID", check.reason, { project_id });
|
|
2682
|
+
}
|
|
2683
|
+
if (hasUniverseId) {
|
|
2684
|
+
const check = validateUniverseId(universe_id);
|
|
2685
|
+
if (!check.ok) return errorResponse("INVALID_UNIVERSE_ID", check.reason, { universe_id });
|
|
2686
|
+
}
|
|
2517
2687
|
|
|
2518
2688
|
try {
|
|
2519
2689
|
const result = createCanonicalWorldEntity({
|
|
@@ -2575,9 +2745,19 @@ function createMcpServer() {
|
|
|
2575
2745
|
if (!SYNC_DIR_WRITABLE) {
|
|
2576
2746
|
return errorResponse("READ_ONLY", "Cannot create place sheet: sync dir is read-only.");
|
|
2577
2747
|
}
|
|
2578
|
-
|
|
2748
|
+
const hasProjectId = project_id !== undefined;
|
|
2749
|
+
const hasUniverseId = universe_id !== undefined;
|
|
2750
|
+
if ((hasProjectId && hasUniverseId) || (!hasProjectId && !hasUniverseId)) {
|
|
2579
2751
|
return errorResponse("VALIDATION_ERROR", "Provide exactly one of project_id or universe_id.");
|
|
2580
2752
|
}
|
|
2753
|
+
if (hasProjectId) {
|
|
2754
|
+
const check = validateProjectId(project_id);
|
|
2755
|
+
if (!check.ok) return errorResponse("INVALID_PROJECT_ID", check.reason, { project_id });
|
|
2756
|
+
}
|
|
2757
|
+
if (hasUniverseId) {
|
|
2758
|
+
const check = validateUniverseId(universe_id);
|
|
2759
|
+
if (!check.ok) return errorResponse("INVALID_UNIVERSE_ID", check.reason, { universe_id });
|
|
2760
|
+
}
|
|
2581
2761
|
|
|
2582
2762
|
try {
|
|
2583
2763
|
const result = createCanonicalWorldEntity({
|
package/package.json
CHANGED
package/prose-styleguide.js
CHANGED
|
@@ -391,7 +391,12 @@ function prepareStyleguideConfigUpdate({ syncDir, scope, projectId, updates = {}
|
|
|
391
391
|
error: {
|
|
392
392
|
code: "STYLEGUIDE_CONFIG_NOT_FOUND",
|
|
393
393
|
message: "Cannot update styleguide config because no config exists at the requested scope.",
|
|
394
|
-
details: {
|
|
394
|
+
details: {
|
|
395
|
+
file_path: filePath,
|
|
396
|
+
scope,
|
|
397
|
+
project_id: projectId ?? null,
|
|
398
|
+
next_step: `Call setup_prose_styleguide_config first (scope=${scope}${projectId ? `, project_id=${projectId}` : ""}, language=<e.g. 'en'>), then retry update_prose_styleguide_config.`,
|
|
399
|
+
},
|
|
395
400
|
},
|
|
396
401
|
};
|
|
397
402
|
}
|