@hanna84/mcp-writing 1.9.3 → 1.9.4
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 +8 -629
- package/package.json +1 -1
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
|
+
#### [v1.9.4](https://github.com/hannasdev/mcp-writing.git
|
|
8
|
+
/compare/v1.9.3...v1.9.4)
|
|
9
|
+
|
|
10
|
+
- docs: split README into focused topic guides [`#56`](https://github.com/hannasdev/mcp-writing.git
|
|
11
|
+
/pull/56)
|
|
12
|
+
|
|
7
13
|
#### [v1.9.3](https://github.com/hannasdev/mcp-writing.git
|
|
8
14
|
/compare/v1.9.2...v1.9.3)
|
|
9
15
|
|
|
16
|
+
> 20 April 2026
|
|
17
|
+
|
|
10
18
|
- docs: add data ownership model section to README [`#52`](https://github.com/hannasdev/mcp-writing.git
|
|
11
19
|
/pull/52)
|
|
20
|
+
- Release 1.9.3 [`7b2b94a`](https://github.com/hannasdev/mcp-writing.git
|
|
21
|
+
/commit/7b2b94a4ad77488cab1a6215b001d925f9e7cdc5)
|
|
12
22
|
|
|
13
23
|
#### [v1.9.2](https://github.com/hannasdev/mcp-writing.git
|
|
14
24
|
/compare/v1.9.1...v1.9.2)
|
package/README.md
CHANGED
|
@@ -20,294 +20,17 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
|
|
|
20
20
|
- AI-assisted editing workflows where you want targeted context retrieval instead of full-manuscript prompting.
|
|
21
21
|
- Projects that need traceable, reversible edits with metadata that stays synchronized as drafts evolve.
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Documentation
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
- **npm 8.0.0 or later**
|
|
27
|
-
- **Git** (for edit snapshots and version history)
|
|
28
|
-
|
|
29
|
-
Verify your setup:
|
|
30
|
-
|
|
31
|
-
```sh
|
|
32
|
-
node --version # should be v22.6.0 or later
|
|
33
|
-
npm --version # should be 8.0.0 or later
|
|
34
|
-
git --version # should be installed
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Permission Contract (Recommended For All Users)
|
|
38
|
-
|
|
39
|
-
To keep MCP write tools reliable across local runs, Docker, and AI agents, use this contract:
|
|
40
|
-
|
|
41
|
-
1. The same non-root user should own and write the sync directory.
|
|
42
|
-
2. Containerized runs should use host UID/GID, not root.
|
|
43
|
-
3. If ownership drifts (for example root-owned files), repair once on host and continue.
|
|
44
|
-
|
|
45
|
-
Repair commands (host):
|
|
46
|
-
|
|
47
|
-
```sh
|
|
48
|
-
sudo chown -R "$(id -u):$(id -g)" /path/to/sync-dir
|
|
49
|
-
find /path/to/sync-dir -type d -exec chmod u+rwx {} +
|
|
50
|
-
find /path/to/sync-dir -type f -exec chmod u+rw {} +
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
You can also inspect ownership/writability status at runtime via `get_runtime_config`.
|
|
54
|
-
|
|
55
|
-
## First-time setup path (recommended)
|
|
56
|
-
|
|
57
|
-
If this is your first time, follow these steps in order:
|
|
58
|
-
|
|
59
|
-
1. Start with **Quick start with Scrivener** (or use **Running with Docker** if that is your preferred setup).
|
|
60
|
-
2. Start the server.
|
|
61
|
-
3. Verify the server (`/healthz` and `/sse`).
|
|
62
|
-
4. Run `import_scrivener_sync` with `dry_run: true` first to preview what will happen.
|
|
63
|
-
5. Run it again with `dry_run: false` to write files. Keep `auto_sync: true` (default) so your scenes are indexed immediately.
|
|
64
|
-
|
|
65
|
-
Once this is working, you can come back to:
|
|
66
|
-
|
|
67
|
-
- **Advanced: Native sync format** for custom project layouts
|
|
68
|
-
- **Reference: Available tools** for the full tool catalog
|
|
69
|
-
- **Appendix: Real-world usage scenarios** for workflow ideas
|
|
70
|
-
|
|
71
|
-
## Quick start with Scrivener
|
|
72
|
-
|
|
73
|
-
If you write in [Scrivener](https://www.literatureandlatte.com/scrivener), this gives you the smoothest path to get started.
|
|
74
|
-
|
|
75
|
-
### 1. Export from Scrivener
|
|
76
|
-
|
|
77
|
-
In Scrivener, go to **File → Sync → With External Folder**. Set the format to **plain text** (`.txt`) and choose an output folder, for example `~/my-novel-txt/`.
|
|
78
|
-
|
|
79
|
-
Only `Draft/` is imported automatically.
|
|
80
|
-
|
|
81
|
-
### 2. Start mcp-writing
|
|
82
|
-
|
|
83
|
-
```sh
|
|
84
|
-
WRITING_SYNC_DIR=/path/to/sync-dir DB_PATH=./writing.db npm start
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
You should see:
|
|
88
|
-
|
|
89
|
-
```sh
|
|
90
|
-
Listening on port 3000
|
|
91
|
-
Sync dir: /path/to/sync-dir
|
|
92
|
-
Database: ./writing.db
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### 3. Verify the server
|
|
96
|
-
|
|
97
|
-
- Open `http://localhost:3000/healthz` and confirm it returns OK.
|
|
98
|
-
- Open `http://localhost:3000/sse` and confirm it opens an SSE stream.
|
|
99
|
-
|
|
100
|
-
### 4. Import Draft scenes through MCP (recommended)
|
|
101
|
-
|
|
102
|
-
From your MCP client, call `import_scrivener_sync` with:
|
|
103
|
-
|
|
104
|
-
```json
|
|
105
|
-
{
|
|
106
|
-
"source_dir": "/Users/yourname/my-novel-txt",
|
|
107
|
-
"project_id": "my-novel",
|
|
108
|
-
"dry_run": true
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
> **Note:** use a full absolute path for `source_dir`. Shell shortcuts like `~` are not expanded by Node.js.
|
|
113
|
-
|
|
114
|
-
If the preview looks right, run it again with writes enabled:
|
|
115
|
-
|
|
116
|
-
```json
|
|
117
|
-
{
|
|
118
|
-
"source_dir": "/Users/yourname/my-novel-txt",
|
|
119
|
-
"project_id": "my-novel",
|
|
120
|
-
"dry_run": false,
|
|
121
|
-
"auto_sync": true
|
|
122
|
-
}
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
The importer:
|
|
126
|
-
|
|
127
|
-
- Converts `Draft/` files to scene sidecars (`.meta.yaml`) with generated `scene_id`, `title`, `timeline_position`, `external_source`, `external_id`, and carried `save_the_cat_beat` where applicable.
|
|
128
|
-
- Skips beat-marker files (`-Setup-`, `-Catalyst-`, etc.), chapter-intro files, epigraphs, and trashed files.
|
|
129
|
-
- Reconciles updates by stable Scrivener binder ID (`[123]` in filenames) so reorder/move operations map to existing scenes.
|
|
130
|
-
|
|
131
|
-
Non-draft content is not inferred from `Notes/`. Put it directly into the target sync dir using the `world/` folder conventions described below.
|
|
132
|
-
|
|
133
|
-
### 5. Optional: CLI fallback import
|
|
134
|
-
|
|
135
|
-
If you prefer to run the import from the command line, use:
|
|
136
|
-
|
|
137
|
-
```sh
|
|
138
|
-
node scripts/import.js ~/my-novel-txt /path/to/sync-dir --project my-novel
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Then call `sync` once.
|
|
142
|
-
|
|
143
|
-
### 6. Lint your metadata (optional)
|
|
144
|
-
|
|
145
|
-
```sh
|
|
146
|
-
node scripts/lint-metadata.mjs --sync-dir /path/to/sync-dir
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
This exits with a non-zero code if it finds errors. Warnings (for example `UNKNOWN_KEY`) are informational.
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
## Advanced: Native sync format
|
|
154
|
-
|
|
155
|
-
For projects not starting from a Scrivener export, place plain `.md` files in the sync folder directly. Metadata lives in a YAML frontmatter block.
|
|
156
|
-
|
|
157
|
-
### Scene file example
|
|
158
|
-
|
|
159
|
-
```markdown
|
|
160
|
-
---
|
|
161
|
-
scene_id: p1-ch2-sc3
|
|
162
|
-
title: The Arrival
|
|
163
|
-
part: 1
|
|
164
|
-
chapter: 2
|
|
165
|
-
characters: [elena, marcus]
|
|
166
|
-
places: [harbor-district]
|
|
167
|
-
logline: Elena arrives at the harbor and meets Marcus for the first time.
|
|
168
|
-
save_the_cat_beat: Setup
|
|
169
|
-
pov: elena
|
|
170
|
-
timeline_position: 4
|
|
171
|
-
story_time: "Day 1, morning"
|
|
172
|
-
tags: [first-meeting, tension]
|
|
173
|
-
---
|
|
174
|
-
|
|
175
|
-
Prose starts here...
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Alternatively, metadata can live in a sidecar file named `<scene-file>.meta.yaml` alongside the prose file — useful for keeping the prose file clean.
|
|
179
|
-
|
|
180
|
-
### Project structure
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
/sync-root/
|
|
184
|
-
/universes/
|
|
185
|
-
/my-series/
|
|
186
|
-
/world/
|
|
187
|
-
characters/elena/sheet.md
|
|
188
|
-
characters/elena/arc.md
|
|
189
|
-
places/harbor-district/sheet.md
|
|
190
|
-
reference/vampire-biology.md
|
|
191
|
-
/book-1/
|
|
192
|
-
/part-1/chapter-1/scene-001.md
|
|
193
|
-
/projects/
|
|
194
|
-
/standalone-novel/
|
|
195
|
-
/world/
|
|
196
|
-
characters/
|
|
197
|
-
places/
|
|
198
|
-
reference/
|
|
199
|
-
/part-1/chapter-1/scene-001.md
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
Character/place folders use one canonical sheet file for entity indexing:
|
|
203
|
-
|
|
204
|
-
- `world/characters/<slug>/sheet.md` or `sheet.txt`
|
|
205
|
-
- `world/places/<slug>/sheet.md` or `sheet.txt`
|
|
206
|
-
|
|
207
|
-
Additional files in the same folder are treated as support notes, not separate entities.
|
|
208
|
-
|
|
209
|
-
Universe-level characters and places are shared across all books in that universe. Standalone projects are fully isolated.
|
|
210
|
-
|
|
211
|
-
### Scaffolding templates
|
|
212
|
-
|
|
213
|
-
Yes, a template is helpful here. It keeps the canonical metadata fields consistent, lowers the friction of adding a new character or place, and reduces the chance that a file is created in the right folder but missing the fields needed for indexing.
|
|
214
|
-
|
|
215
|
-
Use the scaffold script to create a canonical character or place folder:
|
|
216
|
-
|
|
217
|
-
```sh
|
|
218
|
-
npm run new:entity -- --sync-dir /path/to/sync-root --kind character --scope universe --universe my-series --name "Mira Nystrom"
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
```sh
|
|
222
|
-
npm run new:entity -- --sync-dir /path/to/sync-root --kind place --scope project --project my-series/book-1 --name "University Hospital"
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
This creates:
|
|
226
|
-
|
|
227
|
-
- `world/characters/<slug>/sheet.md` plus `arc.md` for character arcs
|
|
228
|
-
- `world/places/<slug>/sheet.md` for places
|
|
229
|
-
- `sheet.meta.yaml` with the required entity ID and starter fields
|
|
230
|
-
|
|
231
|
-
Generated Markdown follows one formatting contract so scaffolded files are predictable to edit:
|
|
232
|
-
|
|
233
|
-
- the first line is a top-level title (`# Name`)
|
|
234
|
-
- every heading is followed by a blank line
|
|
235
|
-
- every generated `.md` file ends with a trailing blank line
|
|
236
|
-
|
|
237
|
-
Use `--dry-run` to preview the path without writing files.
|
|
238
|
-
|
|
239
|
-
You can also create canonical sheets directly through the MCP server with `create_character_sheet` and `create_place_sheet`, then move your existing raw notes into the generated folders.
|
|
240
|
-
|
|
241
|
-
Recommended workflow:
|
|
242
|
-
|
|
243
|
-
1. Scaffold the entity folder and canonical sheet.
|
|
244
|
-
2. Fill in the metadata fields you care about first.
|
|
245
|
-
3. For characters, fill in `arc.md` as a separate arc-analysis document.
|
|
246
|
-
4. Add any other nearby notes like `relationships.md` or `history.md` as needed.
|
|
247
|
-
5. Run `sync` to index the new entity.
|
|
248
|
-
|
|
249
|
-
---
|
|
250
|
-
|
|
251
|
-
## Data ownership model
|
|
252
|
-
|
|
253
|
-
Two separate rulesets apply depending on whether a file lives under `scenes/` (import-managed) or `world/` (human/agent-managed). Mixing writers outside these rules risks silent data loss.
|
|
254
|
-
|
|
255
|
-
### scenes/ — import-managed files
|
|
256
|
-
|
|
257
|
-
The importer is the authoritative writer for Scrivener-imported prose; any edits to `scenes/**/*.md` (manual or via tools) will be overwritten on re-import. Sidecars are shared but with clearly partitioned fields.
|
|
258
|
-
|
|
259
|
-
| File | Writer | Fields written | Behavior on re-import |
|
|
260
|
-
|---|---|---|---|
|
|
261
|
-
| `scenes/**/*.md` | **Scrivener / importer** | Full prose content | **Unconditionally overwritten.** Never edit `.md` files directly — changes will be lost on the next import. |
|
|
262
|
-
| `scenes/**/*.meta.yaml` | **Importer** (Scrivener fields) + **AI agent** (enrichment fields) | Importer writes: `scene_id`, `external_source`, `external_id` (always); `title`, `timeline_position`, `save_the_cat_beat` (from Scrivener metadata, also writable by agents via `update_scene_metadata`) | Importer spreads existing sidecar first, then overlays only its fields. All other fields (logline, status, tags, characters, notes, flags, …) are preserved across re-imports. |
|
|
263
|
-
|
|
264
|
-
**Rule:** write AI-side fields via the appropriate tool — never touch the Scrivener-controlled fields manually or the importer will overwrite them.
|
|
265
|
-
- `update_scene_metadata` supports: `logline`, `status`, `tags`, `characters`, `places`, `pov`, `part`, `chapter`, `timeline_position`, `story_time`, `save_the_cat_beat`, `title`.
|
|
266
|
-
- `flag_scene` appends accumulating continuity/review notes (free-text `flags` list).
|
|
267
|
-
- `enrich_scene` re-derives lightweight metadata from the current prose and clears staleness.
|
|
268
|
-
- `metadata_stale` is a SQLite-only flag set automatically by sync when prose changes — it is not a sidecar field and cannot be written by tools.
|
|
269
|
-
|
|
270
|
-
### sync — read-only with respect to files
|
|
271
|
-
|
|
272
|
-
`sync` reads files and writes only to SQLite. It never touches `.md` prose. The one exception is auto-migration: if a `.md` file has YAML frontmatter but no sidecar yet, sync will create the `.meta.yaml` from the frontmatter (one-time, non-destructive). After that, the sidecar is the source of truth and frontmatter is ignored.
|
|
273
|
-
|
|
274
|
-
| Operation | Reads | Writes |
|
|
275
|
-
|---|---|---|
|
|
276
|
-
| Indexing pass | `scenes/**/*.md`, `scenes/**/*.meta.yaml`, `world/**/*.md`, `world/**/*.meta.yaml` | SQLite only |
|
|
277
|
-
| Frontmatter auto-migration | Any `.md`/`.txt` file (frontmatter block) | Corresponding `.meta.yaml` (created once if missing, for any file type including `world/**`) |
|
|
278
|
-
|
|
279
|
-
`sync` never overwrites an existing sidecar and never touches a `.md` prose file.
|
|
280
|
-
|
|
281
|
-
### world/ — human/agent-managed files
|
|
282
|
-
|
|
283
|
-
The importer never reads or writes anything under `world/`. These files are fully owned by humans and the AI agent and are safe to edit at any time without import risk.
|
|
284
|
-
|
|
285
|
-
| File | Writer | Description |
|
|
286
|
-
|---|---|---|
|
|
287
|
-
| `world/characters/<slug>/sheet.md` | **Human** (after creation) | Canonical character sheet prose. `create_character_sheet` writes this file once on first setup; after that it is human-owned and no tool modifies it. |
|
|
288
|
-
| `world/characters/<slug>/*.md` | **Human or AI agent** | Arc notes, relationship docs, history. Add and edit freely. |
|
|
289
|
-
| `world/characters/<slug>/sheet.meta.yaml` | **AI agent** | Character metadata (`name`, `role`, `arc_summary`, `first_appearance`, `traits`). Written by `create_character_sheet`, `update_character_sheet`. |
|
|
290
|
-
| `world/places/<slug>/sheet.md` | **Human** (after creation) | Canonical place sheet prose. `create_place_sheet` writes this file once on first setup; after that it is human-owned and no tool modifies it. |
|
|
291
|
-
| `world/places/<slug>/sheet.meta.yaml` | **AI agent** | Place metadata (`name`, `associated_characters`, `tags`). Written by `create_place_sheet`, `update_place_sheet`. |
|
|
292
|
-
| `world/reference/**/*.md` | **Human** | Free-form reference notes (world rules, timelines, etc.). Never indexed as entities. |
|
|
293
|
-
|
|
294
|
-
**Rule:** all character and place changes that should survive forever — backstory, relationships, traits, arc notes — belong in `world/`. This content is never at risk from a Scrivener re-import.
|
|
295
|
-
|
|
296
|
-
### Summary
|
|
297
|
-
|
|
298
|
-
| What you want to change | Where to make the change |
|
|
25
|
+
| Guide | Description |
|
|
299
26
|
|---|---|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
## Appendix: Real-world usage scenarios
|
|
27
|
+
| [docs/setup.md](docs/setup.md) | Prerequisites, first-time setup, Scrivener import, native sync format |
|
|
28
|
+
| [docs/docker.md](docs/docker.md) | Docker Compose, OpenClaw integration, SSH hardening |
|
|
29
|
+
| [docs/data-ownership.md](docs/data-ownership.md) | Which tools write which files, import safety rules |
|
|
30
|
+
| [docs/tools.md](docs/tools.md) | Full tool reference — auto-generated from source |
|
|
31
|
+
| [docs/development.md](docs/development.md) | Running locally, tests, environment variables, troubleshooting |
|
|
309
32
|
|
|
310
|
-
|
|
33
|
+
## Usage scenarios
|
|
311
34
|
|
|
312
35
|
### 1) Continuity pass before sending chapters to beta readers
|
|
313
36
|
|
|
@@ -353,349 +76,5 @@ Goal: let AI propose prose edits without losing control of your draft.
|
|
|
353
76
|
|
|
354
77
|
Outcome: you get AI speed with explicit approval and recoverable history for every applied change.
|
|
355
78
|
|
|
356
|
-
---
|
|
357
|
-
|
|
358
|
-
## Reference: Available tools
|
|
359
|
-
|
|
360
|
-
The full tool reference — with every parameter, type, and description — is auto-generated from source and lives at **[docs/tools.md](docs/tools.md)**.
|
|
361
|
-
|
|
362
|
-
To regenerate after editing tool definitions in `index.js`:
|
|
363
|
-
|
|
364
|
-
```bash
|
|
365
|
-
npm run docs
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
---
|
|
369
|
-
|
|
370
|
-
## Running with Docker
|
|
371
|
-
|
|
372
|
-
```yaml
|
|
373
|
-
# docker-compose.yml snippet
|
|
374
|
-
writing-mcp:
|
|
375
|
-
build: .
|
|
376
|
-
user: "${OPENCLAW_UID:-1000}:${OPENCLAW_GID:-1000}"
|
|
377
|
-
environment:
|
|
378
|
-
WRITING_SYNC_DIR: /sync
|
|
379
|
-
DB_PATH: /data/writing.db
|
|
380
|
-
HTTP_PORT: "3000"
|
|
381
|
-
OWNERSHIP_GUARD_MODE: "${OWNERSHIP_GUARD_MODE:-warn}"
|
|
382
|
-
GIT_SSH_COMMAND: "ssh -i /ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/ssh/known_hosts"
|
|
383
|
-
volumes:
|
|
384
|
-
- ${OPENCLAW_WORKSPACE_DIR:?run scripts/setup-openclaw-env.sh first}/sync:/sync
|
|
385
|
-
- ${OPENCLAW_SSH_DIR:?run scripts/setup-openclaw-env.sh first}:/ssh:ro
|
|
386
|
-
- writing-mcp-data:/data
|
|
387
|
-
healthcheck:
|
|
388
|
-
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
389
|
-
interval: 30s
|
|
390
|
-
timeout: 5s
|
|
391
|
-
retries: 5
|
|
392
|
-
|
|
393
|
-
volumes:
|
|
394
|
-
writing-mcp-data:
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Recommended: start from `docker-compose.example.yml` and generate `.env` with machine-specific values:
|
|
398
|
-
|
|
399
|
-
```sh
|
|
400
|
-
sh scripts/setup-openclaw-env.sh
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
That script writes `OPENCLAW_UID`, `OPENCLAW_GID`, `OPENCLAW_WORKSPACE_DIR`, and `OPENCLAW_SSH_DIR` to `.env`.
|
|
404
|
-
Running Compose without these values is unsupported and may create invalid mount definitions.
|
|
405
|
-
It also normalizes `OWNERSHIP_GUARD_MODE` to `warn` or `fail` and preserves an existing valid value when rerun.
|
|
406
|
-
|
|
407
|
-
Then register in your OpenClaw config:
|
|
408
|
-
|
|
409
|
-
```json
|
|
410
|
-
"mcp": {
|
|
411
|
-
"servers": {
|
|
412
|
-
"writing": { "url": "http://writing-mcp:3000/sse" }
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
<details>
|
|
418
|
-
<summary>Advanced OpenClaw / Docker integration notes</summary>
|
|
419
|
-
|
|
420
|
-
### OpenClaw / Docker integration notes
|
|
421
|
-
|
|
422
|
-
When `mcp-writing` runs behind OpenClaw (or any Docker MCP gateway), these details prevent common runtime failures.
|
|
423
|
-
|
|
424
|
-
#### Required environment and mounts
|
|
425
|
-
|
|
426
|
-
- Set `WRITING_SYNC_DIR=/sync`
|
|
427
|
-
- Set `DB_PATH=/data/writing.db`
|
|
428
|
-
- Set `OWNERSHIP_GUARD_MODE=warn` (or `fail` to block startup on ownership drift)
|
|
429
|
-
- Mount your manuscript sync repo to `/sync`
|
|
430
|
-
- Mount a persistent path for SQLite data at `/data`
|
|
431
|
-
- Mount SSH materials read-only at `/ssh` and use `GIT_SSH_COMMAND` with `/ssh` paths
|
|
432
|
-
|
|
433
|
-
Debug/test-only runtime override knobs:
|
|
434
|
-
|
|
435
|
-
- `RUNTIME_UID_OVERRIDE` — test helper to simulate runtime UID during ownership diagnostics
|
|
436
|
-
- `ALLOW_RUNTIME_UID_OVERRIDE=1` — explicitly enables the override outside `NODE_ENV=test`
|
|
437
|
-
|
|
438
|
-
Do not set these in normal production or desktop deployments.
|
|
439
|
-
|
|
440
|
-
If `/sync` contains raw Scrivener external-sync output, run the importer once before normal `sync` usage:
|
|
441
|
-
|
|
442
|
-
```sh
|
|
443
|
-
node scripts/import.js /path/to/scrivener-export /sync --project my-novel
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
`sync` indexes files that already contain scene metadata. It does not convert Scrivener `Draft/` filenames into scene sidecars by itself.
|
|
447
|
-
|
|
448
|
-
#### Git ownership trust for mounted repos
|
|
449
|
-
|
|
450
|
-
If host and container ownership differ, git can fail with:
|
|
451
|
-
|
|
452
|
-
- `fatal: detected dubious ownership in repository`
|
|
453
|
-
|
|
454
|
-
Mark the mounted repo path as safe in the container image:
|
|
455
|
-
|
|
456
|
-
```sh
|
|
457
|
-
git config --system --add safe.directory /sync
|
|
458
|
-
```
|
|
459
|
-
|
|
460
|
-
#### SSH transport hardening
|
|
461
|
-
|
|
462
|
-
For private remotes, mount SSH materials read-only and enforce strict host checks:
|
|
463
|
-
|
|
464
|
-
- Auth key for fetch/pull/push
|
|
465
|
-
- `known_hosts` with GitHub host key
|
|
466
|
-
- `StrictHostKeyChecking=yes`
|
|
467
|
-
|
|
468
|
-
Example:
|
|
469
|
-
|
|
470
|
-
```sh
|
|
471
|
-
export GIT_SSH_COMMAND="ssh -i /ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/ssh/known_hosts"
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
#### Separate auth and signing keys
|
|
475
|
-
|
|
476
|
-
Use dedicated keys for transport and signing:
|
|
477
|
-
|
|
478
|
-
- Auth key: repository transport (`fetch` / `pull` / `push`)
|
|
479
|
-
- Signing key: commit/tag signatures
|
|
480
|
-
|
|
481
|
-
Recommended git config:
|
|
482
|
-
|
|
483
|
-
```sh
|
|
484
|
-
git config gpg.format ssh
|
|
485
|
-
git config user.signingkey /root/.ssh/id_ed25519_signing
|
|
486
|
-
git config commit.gpgsign true
|
|
487
|
-
git config pull.ff only
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
#### Git identity and GitHub email privacy
|
|
491
|
-
|
|
492
|
-
If GitHub email privacy is enabled, pushes can fail unless `user.email` is a GitHub noreply address:
|
|
493
|
-
|
|
494
|
-
```sh
|
|
495
|
-
git config user.name "Edda"
|
|
496
|
-
git config user.email "<id>+<username>@users.noreply.github.com"
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
#### Branch safety for automation
|
|
500
|
-
|
|
501
|
-
For bot-driven edits, prefer branch-per-change flow:
|
|
502
|
-
|
|
503
|
-
- Push to `edda/*` or `bot/*`
|
|
504
|
-
- Merge via pull request
|
|
505
|
-
- Protect `main` from direct automation pushes
|
|
506
|
-
|
|
507
|
-
#### Quick validation
|
|
508
|
-
|
|
509
|
-
```sh
|
|
510
|
-
ssh -T git@github.com
|
|
511
|
-
git -C /sync fetch origin
|
|
512
|
-
git -C /sync pull --ff-only
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
Then create and push a signed smoke commit on a temporary branch.
|
|
516
|
-
|
|
517
|
-
</details>
|
|
518
|
-
|
|
519
|
-
## Running locally
|
|
520
|
-
|
|
521
|
-
```sh
|
|
522
|
-
npm install
|
|
523
|
-
WRITING_SYNC_DIR=./my-manuscript DB_PATH=./writing.db npm start
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
The `npm start` script automatically includes the `--experimental-sqlite` flag needed for SQLite support in Node.js 22+.
|
|
527
|
-
|
|
528
|
-
## Verify your setup
|
|
529
|
-
|
|
530
|
-
After starting the server, test that it's working:
|
|
531
|
-
|
|
532
|
-
```sh
|
|
533
|
-
# In a new terminal
|
|
534
|
-
curl http://localhost:3000/healthz
|
|
535
|
-
# Should return: ok
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
Then test the MCP endpoint:
|
|
539
|
-
|
|
540
|
-
```sh
|
|
541
|
-
curl http://localhost:3000/sse
|
|
542
|
-
# Should return a stream endpoint: /message?sessionId=<id>
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
If both return successfully, the server is ready to use.
|
|
546
|
-
|
|
547
|
-
## Development
|
|
548
|
-
|
|
549
|
-
```sh
|
|
550
|
-
npm install
|
|
551
|
-
npm test # unit + integration tests
|
|
552
|
-
npm run test:unit # unit tests only (no server required)
|
|
553
|
-
npm run lint:metadata # lint metadata in WRITING_SYNC_DIR or ./sync
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
Unit tests use an in-memory SQLite database and temporary directories — no server needed. Integration tests generate a fixture sync tree at runtime in temporary directories, spawn a real server on port 3099, and verify all MCP tools end-to-end.
|
|
557
|
-
|
|
558
|
-
For real projects, keep your manuscript sync folder outside this tool repository and point `WRITING_SYNC_DIR` at that external path.
|
|
559
|
-
|
|
560
|
-
Maintainers: see `MAINTAINERS.md` for release and operational setup notes, and `AGENT.md` for persistent workflow conventions and release/recovery guidance.
|
|
561
|
-
|
|
562
|
-
## Troubleshooting
|
|
563
|
-
|
|
564
|
-
### "Module not found: sqlite" or "Database support not available"
|
|
565
|
-
|
|
566
|
-
Your Node.js version is too old, or SQLite support was not started with the required flag.
|
|
567
|
-
|
|
568
|
-
Fix:
|
|
569
|
-
|
|
570
|
-
1. Run `node --version` and confirm v22.6.0 or newer.
|
|
571
|
-
2. Upgrade Node.js if needed.
|
|
572
|
-
3. Restart with `npm start` (the script already includes `--experimental-sqlite`).
|
|
573
|
-
|
|
574
|
-
### "EADDRINUSE: address already in use :::3000"
|
|
575
|
-
|
|
576
|
-
Port 3000 is already in use.
|
|
577
|
-
|
|
578
|
-
Fix: start on a different port.
|
|
579
|
-
|
|
580
|
-
```sh
|
|
581
|
-
HTTP_PORT=3001 WRITING_SYNC_DIR=./my-manuscript DB_PATH=./writing.db npm start
|
|
582
|
-
```
|
|
583
|
-
|
|
584
|
-
Then update your MCP client config to use `http://localhost:3001/sse`.
|
|
585
|
-
|
|
586
|
-
### "ENOENT: no such file or directory, open './writing.db'"
|
|
587
|
-
|
|
588
|
-
The directory for `DB_PATH` does not exist.
|
|
589
|
-
|
|
590
|
-
Fix: create the directory first.
|
|
591
|
-
|
|
592
|
-
```sh
|
|
593
|
-
mkdir -p $(dirname ./writing.db) # if using a subdirectory
|
|
594
|
-
WRITING_SYNC_DIR=./my-manuscript DB_PATH=./writing.db npm start
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
Or use an absolute path:
|
|
598
|
-
|
|
599
|
-
```sh
|
|
600
|
-
WRITING_SYNC_DIR=~/my-manuscript DB_PATH=~/writing-data/writing.db npm start
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
### "Sync dir not found: ./my-manuscript"
|
|
604
|
-
|
|
605
|
-
The `WRITING_SYNC_DIR` path does not exist.
|
|
606
|
-
|
|
607
|
-
Fix: create it (or point to an existing sync folder).
|
|
608
|
-
|
|
609
|
-
```sh
|
|
610
|
-
mkdir -p ./my-manuscript/projects/my-novel
|
|
611
|
-
WRITING_SYNC_DIR=./my-manuscript DB_PATH=./writing.db npm start
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
### "Import failed: unrecognized format"
|
|
615
|
-
|
|
616
|
-
Scrivener export is not plain text (`.txt`) or folder layout is unexpected.
|
|
617
|
-
|
|
618
|
-
Fix:
|
|
619
|
-
|
|
620
|
-
1. In Scrivener, re-export with **File → Sync → With External Folder**
|
|
621
|
-
2. Ensure the format is set to **Plain text** (not RTF or .docx)
|
|
622
|
-
3. Verify the export folder has a `Draft/` subdirectory with `.txt` files
|
|
623
|
-
4. Try the import again: `node scripts/import.js ~/my-novel-txt /path/to/sync-dir --project my-novel`
|
|
624
|
-
|
|
625
|
-
### "OpenClaw can read tools, but scene indexing is empty or incomplete"
|
|
626
|
-
|
|
627
|
-
You are likely running `sync` on raw Scrivener `Draft/` output that has not been imported yet.
|
|
628
|
-
|
|
629
|
-
Fix:
|
|
630
|
-
|
|
631
|
-
1. Run importer once to create scene metadata sidecars:
|
|
632
|
-
|
|
633
|
-
```sh
|
|
634
|
-
node scripts/import.js /path/to/scrivener-export /path/to/sync-dir --project my-novel
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
2. Restart the service (if needed), then call `sync` again.
|
|
638
|
-
|
|
639
|
-
Note: importer behavior is Draft-aware (`<source>/Draft` if present, else source root), but plain `sync` only indexes already-normalized scene files.
|
|
640
|
-
|
|
641
|
-
### "Write access to repository denied" (or git push/pull fails in container)
|
|
642
|
-
|
|
643
|
-
Your container can start and read files, but cannot write metadata, create snapshots, or push branches.
|
|
644
|
-
|
|
645
|
-
Fix:
|
|
646
|
-
|
|
647
|
-
1. Check runtime diagnostics via `get_runtime_config`:
|
|
648
|
-
- `sync_dir_writable` must be `true`
|
|
649
|
-
- `runtime_warnings` should be empty for normal editing flows
|
|
650
|
-
2. Ensure `/sync` is mounted read-write (no `:ro`) and owned by the container user.
|
|
651
|
-
3. For mounted git repos with UID mismatch, mark safe directory:
|
|
652
|
-
|
|
653
|
-
```sh
|
|
654
|
-
git config --system --add safe.directory /sync
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
4. Verify SSH key has write access to the remote and `known_hosts` is mounted.
|
|
658
|
-
5. Prefer branch-per-change workflow (`bot/*` or `edda/*`) if `main` is protected.
|
|
659
|
-
|
|
660
|
-
### "Blocked: file is root-owned" (EACCES / ownership drift)
|
|
661
|
-
|
|
662
|
-
The runtime user can read but cannot overwrite prose files.
|
|
663
|
-
|
|
664
|
-
Fix:
|
|
665
|
-
|
|
666
|
-
1. Repair host ownership once:
|
|
667
|
-
|
|
668
|
-
```sh
|
|
669
|
-
sudo chown -R "$(id -u):$(id -g)" /path/to/sync-dir
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
2. Ensure container user mapping is set from `.env` (`OPENCLAW_UID` / `OPENCLAW_GID`).
|
|
673
|
-
3. Optionally set `OWNERSHIP_GUARD_MODE=fail` to catch mismatches at startup.
|
|
674
|
-
4. Re-check `get_runtime_config` and confirm ownership warnings are gone.
|
|
675
|
-
|
|
676
|
-
### Tests fail after updating Node.js
|
|
677
|
-
|
|
678
|
-
Local install state may be stale after the Node.js change.
|
|
679
|
-
|
|
680
|
-
Fix: reinstall dependencies.
|
|
681
|
-
|
|
682
|
-
```sh
|
|
683
|
-
rm -rf node_modules package-lock.json
|
|
684
|
-
npm install
|
|
685
|
-
npm test
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
## Environment variables
|
|
689
|
-
|
|
690
|
-
| Variable | Default | Description |
|
|
691
|
-
| --- | --- | --- |
|
|
692
|
-
| `WRITING_SYNC_DIR` | `./sync` | Path to the sync folder |
|
|
693
|
-
| `DB_PATH` | `./writing.db` | Path to the SQLite index database |
|
|
694
|
-
| `HTTP_PORT` | `3000` | Port for the MCP SSE endpoint |
|
|
695
|
-
| `MAX_CHAPTER_SCENES` | `10` | Maximum scenes returned by `get_chapter_prose` |
|
|
696
|
-
| `DEFAULT_METADATA_PAGE_SIZE` | `20` | Default page size for paginated tools |
|
|
697
|
-
| `OWNERSHIP_GUARD_MODE` | `warn` | Startup ownership policy: `warn` logs drift, `fail` exits when sampled files are not owned by runtime user |
|
|
698
|
-
|
|
699
79
|
## License
|
|
700
|
-
|
|
701
80
|
AGPL-3.0-only
|