@hanna84/mcp-writing 1.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/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # mcp-writing
2
+
3
+ An MCP service for AI-assisted reasoning and editing on long-form fiction projects.
4
+
5
+ Designed to work with [OpenClaw](https://github.com/openclaw/openclaw) but compatible with any MCP-capable AI gateway.
6
+
7
+ ## What it does
8
+
9
+ Instead of feeding an entire manuscript to an AI and hoping it fits in the context window, `mcp-writing` builds a structured index from your scene files. The AI queries that index first — finding relevant characters, beats, and loglines — then loads only the specific prose it needs.
10
+
11
+ **Phase 1:** Read-only analysis. Ask questions about your project.
12
+ **Phase 2 (current):** Metadata write-back. Answers stay accurate as the manuscript evolves.
13
+ **Phase 3:** AI-assisted prose editing with confirmation and version history.
14
+
15
+ ## Quick start with Scrivener
16
+
17
+ If you write in [Scrivener](https://www.literatureandlatte.com/scrivener), you can seed `mcp-writing` from a Scrivener external-sync export in two steps.
18
+
19
+ ### 1. Export from Scrivener
20
+
21
+ In Scrivener: **File → Sync → With External Folder**. Set the format to **plain text** (`.txt`) and pick an output folder, for example `~/my-novel-txt/`. Your `Draft/` folder and `Notes/` folder will be exported as numbered `.txt` files.
22
+
23
+ ### 2. Import into mcp-writing
24
+
25
+ ```sh
26
+ node scripts/import.js ~/my-novel-txt /path/to/sync-dir --project my-novel
27
+ ```
28
+
29
+ The importer:
30
+ - Converts `Draft/` files to scene sidecars (`.meta.yaml`) with auto-generated `scene_id`, `title`, `part`, `chapter`, and `save_the_cat_beat` fields derived from the filename/structure.
31
+ - Routes `Notes/` files into `world/characters/` or `world/places/` based on section grouping.
32
+ - Skips beat-marker files (`-Setup-`, `-Catalyst-`, etc.), chapter-intro files, epigraphs, and trashed files.
33
+
34
+ > **Note:** The importer writes a `group` key to character sidecar files so characters stay organized by their Notes section (e.g. "Main Characters", "Mira's team"). This is preserved through lint and sync.
35
+
36
+ ### 3. Start the server
37
+
38
+ ```sh
39
+ WRITING_SYNC_DIR=/path/to/sync-dir DB_PATH=./writing.db npm start
40
+ ```
41
+
42
+ Then call the `sync` tool once to index everything.
43
+
44
+ ### 4. Lint your metadata (optional)
45
+
46
+ ```sh
47
+ node scripts/lint-metadata.mjs --sync-dir /path/to/sync-dir
48
+ ```
49
+
50
+ Exits non-zero if any errors are found. Warnings (e.g. `UNKNOWN_KEY`) are informational only.
51
+
52
+ ---
53
+
54
+ ## Native sync format
55
+
56
+ For projects not starting from a Scrivener export, place plain `.md` files in the sync folder directly. Metadata lives in a YAML frontmatter block.
57
+
58
+ ### Scene file example
59
+
60
+ ```markdown
61
+ ---
62
+ scene_id: p1-ch2-sc3
63
+ title: The Arrival
64
+ part: 1
65
+ chapter: 2
66
+ characters: [elena, marcus]
67
+ places: [harbor-district]
68
+ logline: Elena arrives at the harbor and meets Marcus for the first time.
69
+ save_the_cat_beat: Setup
70
+ pov: elena
71
+ timeline_position: 4
72
+ story_time: "Day 1, morning"
73
+ tags: [first-meeting, tension]
74
+ ---
75
+
76
+ Prose starts here...
77
+ ```
78
+
79
+ Alternatively, metadata can live in a sidecar file named `<scene-file>.meta.yaml` alongside the prose file — useful for keeping the prose file clean.
80
+
81
+ ### Project structure
82
+
83
+ ```
84
+ /sync-root/
85
+ /universes/
86
+ /my-series/
87
+ /world/
88
+ characters/elena.md
89
+ places/harbor-district.md
90
+ /book-1/
91
+ /part-1/chapter-1/scene-001.md
92
+ /projects/
93
+ /standalone-novel/
94
+ /world/
95
+ characters/
96
+ places/
97
+ /part-1/chapter-1/scene-001.md
98
+ ```
99
+
100
+ Universe-level characters and places are shared across all books in that universe. Standalone projects are fully isolated.
101
+
102
+ ---
103
+
104
+ ## Available tools
105
+
106
+ | Tool | Description |
107
+ | --- | --- |
108
+ | `sync` | Re-scan the sync folder and update the index |
109
+ | `find_scenes` | Filter scenes by character, beat, tag, part, chapter, or POV |
110
+ | `get_scene_prose` | Load the full prose for a specific scene |
111
+ | `get_chapter_prose` | Load all prose for a chapter |
112
+ | `get_arc` | Ordered scene metadata for all scenes involving a character |
113
+ | `list_characters` | All characters, optionally filtered by project or universe |
114
+ | `get_character_sheet` | Full character metadata, traits, and notes |
115
+ | `list_places` | All places |
116
+ | `search_metadata` | Full-text search across scene titles and loglines |
117
+ | `list_threads` | All subplot threads for a project |
118
+ | `get_thread_arc` | Scenes belonging to a thread, with per-thread beat |
119
+ | `upsert_thread_link` | Create/update a thread and link it to a scene |
120
+ | `enrich_scene` | Re-derive lightweight metadata from current prose and clear `metadata_stale` |
121
+ | `update_scene_metadata` | Write metadata fields back to a scene sidecar |
122
+ | `update_character_sheet` | Write fields back to a character sidecar |
123
+ | `flag_scene` | Mark a scene with a flag for AI follow-up |
124
+
125
+ Paginated tools (`find_scenes`, `get_arc`, `list_threads`, `get_thread_arc`, `search_metadata`) accept `page` and `page_size` arguments and return `total_count` / `total_pages` in the response envelope.
126
+
127
+ ---
128
+
129
+ ## Running with Docker
130
+
131
+ ```yaml
132
+ # docker-compose.yml snippet
133
+ writing-mcp:
134
+ build: .
135
+ environment:
136
+ WRITING_SYNC_DIR: /sync
137
+ DB_PATH: /data/writing.db
138
+ HTTP_PORT: "3000"
139
+ volumes:
140
+ - /path/to/sync-dir:/sync
141
+ - writing-mcp-data:/data
142
+ healthcheck:
143
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
144
+ interval: 30s
145
+ timeout: 5s
146
+ retries: 5
147
+
148
+ volumes:
149
+ writing-mcp-data:
150
+ ```
151
+
152
+ Then register in your OpenClaw config:
153
+
154
+ ```json
155
+ "mcp": {
156
+ "servers": {
157
+ "writing": { "url": "http://writing-mcp:3000/sse" }
158
+ }
159
+ }
160
+ ```
161
+
162
+ ## Running locally
163
+
164
+ ```sh
165
+ npm install
166
+ WRITING_SYNC_DIR=./my-manuscript DB_PATH=./writing.db npm start
167
+ ```
168
+
169
+ ## Development
170
+
171
+ ```sh
172
+ npm install
173
+ npm test # unit + integration tests
174
+ npm run test:unit # unit tests only (no server required)
175
+ npm run lint:metadata # lint metadata in WRITING_SYNC_DIR or ./sync
176
+ npm run lint:metadata:test # lint fixture metadata in ./test-sync
177
+ ```
178
+
179
+ Unit tests use an in-memory SQLite database and temporary directories — no server needed. Integration tests spawn a real server against `test-sync/` on port 3099 and verify all MCP tools end-to-end.
180
+
181
+ ## Environment variables
182
+
183
+ | Variable | Default | Description |
184
+ | --- | --- | --- |
185
+ | `WRITING_SYNC_DIR` | `./sync` | Path to the sync folder |
186
+ | `DB_PATH` | `./writing.db` | Path to the SQLite index database |
187
+ | `HTTP_PORT` | `3000` | Port for the MCP SSE endpoint |
188
+ | `MAX_CHAPTER_SCENES` | `10` | Maximum scenes returned by `get_chapter_prose` |
189
+ | `DEFAULT_METADATA_PAGE_SIZE` | `20` | Default page size for paginated tools |
190
+
191
+ ## License
192
+
193
+ MIT
package/db.js ADDED
@@ -0,0 +1,127 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+
3
+ export const SCHEMA = `
4
+ CREATE TABLE IF NOT EXISTS universes (
5
+ universe_id TEXT PRIMARY KEY,
6
+ name TEXT NOT NULL
7
+ );
8
+
9
+ CREATE TABLE IF NOT EXISTS projects (
10
+ project_id TEXT PRIMARY KEY,
11
+ universe_id TEXT REFERENCES universes(universe_id),
12
+ name TEXT NOT NULL
13
+ );
14
+
15
+ CREATE TABLE IF NOT EXISTS scenes (
16
+ scene_id TEXT NOT NULL,
17
+ project_id TEXT NOT NULL REFERENCES projects(project_id),
18
+ title TEXT,
19
+ part INTEGER,
20
+ chapter INTEGER,
21
+ pov TEXT,
22
+ logline TEXT,
23
+ scene_change TEXT,
24
+ causality INTEGER,
25
+ stakes INTEGER,
26
+ scene_functions TEXT,
27
+ save_the_cat_beat TEXT,
28
+ timeline_position INTEGER,
29
+ story_time TEXT,
30
+ word_count INTEGER,
31
+ file_path TEXT NOT NULL,
32
+ prose_checksum TEXT,
33
+ metadata_stale INTEGER NOT NULL DEFAULT 0,
34
+ updated_at TEXT NOT NULL,
35
+ PRIMARY KEY (scene_id, project_id)
36
+ );
37
+
38
+ CREATE TABLE IF NOT EXISTS scene_characters (
39
+ scene_id TEXT NOT NULL,
40
+ character_id TEXT NOT NULL,
41
+ PRIMARY KEY (scene_id, character_id)
42
+ );
43
+
44
+ CREATE TABLE IF NOT EXISTS scene_places (
45
+ scene_id TEXT NOT NULL,
46
+ place_id TEXT NOT NULL,
47
+ PRIMARY KEY (scene_id, place_id)
48
+ );
49
+
50
+ CREATE TABLE IF NOT EXISTS scene_tags (
51
+ scene_id TEXT NOT NULL,
52
+ tag TEXT NOT NULL,
53
+ PRIMARY KEY (scene_id, tag)
54
+ );
55
+
56
+ CREATE TABLE IF NOT EXISTS scene_threads (
57
+ scene_id TEXT NOT NULL,
58
+ thread_id TEXT NOT NULL,
59
+ beat TEXT,
60
+ PRIMARY KEY (scene_id, thread_id)
61
+ );
62
+
63
+ CREATE TABLE IF NOT EXISTS characters (
64
+ character_id TEXT NOT NULL PRIMARY KEY,
65
+ project_id TEXT,
66
+ universe_id TEXT,
67
+ name TEXT NOT NULL,
68
+ role TEXT,
69
+ arc_summary TEXT,
70
+ first_appearance TEXT,
71
+ file_path TEXT
72
+ );
73
+
74
+ CREATE TABLE IF NOT EXISTS character_traits (
75
+ character_id TEXT NOT NULL,
76
+ trait TEXT NOT NULL,
77
+ PRIMARY KEY (character_id, trait)
78
+ );
79
+
80
+ CREATE TABLE IF NOT EXISTS character_relationships (
81
+ from_character TEXT NOT NULL,
82
+ to_character TEXT NOT NULL,
83
+ relationship_type TEXT NOT NULL,
84
+ strength TEXT,
85
+ scene_id TEXT,
86
+ note TEXT
87
+ );
88
+
89
+ CREATE TABLE IF NOT EXISTS places (
90
+ place_id TEXT NOT NULL PRIMARY KEY,
91
+ project_id TEXT,
92
+ universe_id TEXT,
93
+ name TEXT NOT NULL,
94
+ file_path TEXT
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS threads (
98
+ thread_id TEXT NOT NULL PRIMARY KEY,
99
+ project_id TEXT NOT NULL,
100
+ name TEXT NOT NULL,
101
+ status TEXT NOT NULL DEFAULT 'active'
102
+ );
103
+
104
+ CREATE TABLE IF NOT EXISTS reference_docs (
105
+ doc_id TEXT NOT NULL PRIMARY KEY,
106
+ project_id TEXT,
107
+ universe_id TEXT,
108
+ title TEXT NOT NULL,
109
+ file_path TEXT NOT NULL
110
+ );
111
+
112
+ CREATE TABLE IF NOT EXISTS reference_doc_tags (
113
+ doc_id TEXT NOT NULL,
114
+ tag TEXT NOT NULL,
115
+ PRIMARY KEY (doc_id, tag)
116
+ );
117
+
118
+ CREATE VIRTUAL TABLE IF NOT EXISTS scenes_fts USING fts5(
119
+ scene_id, project_id, logline, title
120
+ );
121
+ `;
122
+
123
+ export function openDb(dbPath) {
124
+ const db = new DatabaseSync(dbPath);
125
+ db.exec(SCHEMA);
126
+ return db;
127
+ }