Builds out the vertical slice for planning and writing a novel. Three front ends — the React UI, an embedded Claude agent, and an MCP stdio server — all go through one REST API, so an edit made from Claude Code and one made in the browser are the same edit. Layout: Domain entities and enums, no dependencies Application services, DTOs, the agent tool-use loop and its 15 tools Infrastructure EF Core 10 + SQLite, Anthropic SDK client Api ASP.NET Core 10 minimal APIs, OpenAPI, ProblemDetails Mcp MCP stdio server, 21 tools over the same REST API Web React 19 + Vite + TanStack Query + Tailwind v4 Data model is Project > Characters / OutlineNodes / Chapters > Scenes, plus agent conversations. The outline is a self-nesting tree so acts, sequences and beats can be arranged however the book wants; scenes carry goal/conflict/outcome because that is what the agent drafts prose from. Notes on a few choices: - Conversation history replays to the model as text only. The agent re-reads current state through its tools rather than trusting a record of edits that may since have changed in the UI. - The user's turn is persisted before the tool loop runs, so a question is recorded even when the model call fails. Turn order uses an explicit sequence column; timestamps tie when a turn completes inside one tick. - Tool failures return is_error results rather than throwing, so the model can read the message and correct itself. MCP tools do the same via CallToolResult, which keeps the API's own message instead of a generic SDK error. - The Anthropic client is constructed lazily. It is injected into the agent service, which also serves read-only endpoints, and those should keep working on an install with no key. Sending without one returns 503, not 400. - DateTimeOffset is stored as UTC ticks. SQLite refuses to ORDER BY the default text form, which every "recently updated first" listing depends on. Tests run against real in-memory SQLite rather than the EF in-memory provider so they exercise the cascade deletes and query translation that actually ship. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S56bfZMGe1hnhpWP4CjjNw
175 lines
6.8 KiB
Markdown
175 lines
6.8 KiB
Markdown
# Novel Software
|
|
|
|
Software for planning and writing a novel. You outline the book, keep character
|
|
dossiers, break chapters into scenes, and draft prose — with a Claude-powered agent
|
|
embedded in the app that can read and edit the same data you can, and an MCP server that
|
|
exposes that data to Claude Code, Claude Desktop, or any other MCP client.
|
|
|
|
The point of the three-way arrangement is that there is exactly one source of truth. The
|
|
React UI, the embedded agent, and the MCP server all go through the same REST API, so an
|
|
edit made from a chat in Claude Code and an edit made by typing in the browser are the
|
|
same edit.
|
|
|
|
## Stack
|
|
|
|
| Piece | Built with |
|
|
|---|---|
|
|
| `NovelSoftware.Api` | ASP.NET Core 10 minimal APIs, OpenAPI |
|
|
| `NovelSoftware.Application` | Services, DTOs, the agent tool-use loop |
|
|
| `NovelSoftware.Domain` | Entities and enums, no dependencies |
|
|
| `NovelSoftware.Infrastructure` | EF Core 10 + SQLite, Anthropic SDK client |
|
|
| `NovelSoftware.Mcp` | MCP stdio server (`ModelContextProtocol`) |
|
|
| `NovelSoftware.Web` | React 19, TypeScript, Vite, TanStack Query, Tailwind v4 |
|
|
|
|
## Running it
|
|
|
|
Prerequisites: .NET 10 SDK and Node 20+.
|
|
|
|
```bash
|
|
# 1. API — creates and migrates novel.db on first run, listens on :5080
|
|
ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/NovelSoftware.Api
|
|
|
|
# 2. Web — dev server on :5173, proxies /api to :5080
|
|
cd src/NovelSoftware.Web && npm install && npm run dev
|
|
```
|
|
|
|
Open http://localhost:5173.
|
|
|
|
The app is fully usable without an Anthropic key — only the Agent tab needs one. To turn
|
|
the agent on:
|
|
|
|
```bash
|
|
export ANTHROPIC_API_KEY=sk-ant-...
|
|
```
|
|
|
|
Without it, agent endpoints return `503 Agent unavailable` with an explanatory message
|
|
and everything else keeps working.
|
|
|
|
### Tests
|
|
|
|
```bash
|
|
dotnet test # 31 tests
|
|
cd src/NovelSoftware.Web && npm run build # typecheck + bundle
|
|
```
|
|
|
|
Tests run against real in-memory SQLite rather than the EF in-memory provider, so they
|
|
exercise the cascade deletes and query translation the app actually ships with.
|
|
|
|
## Configuration
|
|
|
|
`src/NovelSoftware.Api/appsettings.json`:
|
|
|
|
```jsonc
|
|
{
|
|
"ConnectionStrings": { "Novel": "Data Source=novel.db" },
|
|
"Cors": { "Origins": [ "http://localhost:5173" ] },
|
|
"Agent": {
|
|
"Model": "claude-opus-5",
|
|
"MaxTokens": 16000,
|
|
"Effort": "high", // low | medium | high | max
|
|
"MaxIterations": 12 // tool-call ceiling per user turn
|
|
}
|
|
}
|
|
```
|
|
|
|
The API key is read from `ANTHROPIC_API_KEY` or, if you prefer, `Agent:ApiKey` — keep it
|
|
out of `appsettings.json` and use user-secrets or the environment.
|
|
|
|
## The data model
|
|
|
|
```
|
|
Project ──┬── Character ── CharacterRelationship
|
|
├── OutlineNode (self-nesting: Part > Act > Sequence > Beat)
|
|
├── Chapter ── Scene (goal / conflict / outcome, prose, word count)
|
|
└── AgentConversation ── AgentMessage
|
|
```
|
|
|
|
The outline tree is deliberately loose — nest acts under parts, beats under sequences, or
|
|
keep a flat list of beats. An outline node can link to the chapter that realises it.
|
|
|
|
Scenes carry the goal/conflict/outcome trio because that is the unit the agent works from
|
|
when turning an outline into prose. Word counts are recomputed on every save.
|
|
|
|
## The embedded agent
|
|
|
|
`NovelAgentService` runs the tool-use loop: it calls the Messages API, executes any tools
|
|
Claude asks for, feeds every result back in a single user turn, and repeats until Claude
|
|
stops asking. It has 15 tools covering the brief, characters, the outline tree, chapters
|
|
and scenes — all of them going through the same application services the REST API uses.
|
|
|
|
A few deliberate choices worth knowing about:
|
|
|
|
- **Conversation history replays as text only.** Tool calls are not replayed into the
|
|
transcript. The agent re-reads current state through its tools instead, which is more
|
|
reliable than trusting a record of edits that may since have changed in the UI.
|
|
- **The user's turn is persisted before the loop runs**, so a question is recorded even if
|
|
the model call fails.
|
|
- **Tool failures come back as `is_error` results**, not exceptions — the model reads the
|
|
message and corrects itself.
|
|
- **`MaxIterations` caps tool calls per turn.** On hitting it the agent says so rather
|
|
than silently truncating.
|
|
- **The system prompt is cached** (`cache_control: ephemeral`), so every turn after the
|
|
first reads it back at a fraction of the input price.
|
|
|
|
## The MCP server
|
|
|
|
A stdio MCP server exposing 21 tools over the same REST API. It holds no domain logic of
|
|
its own — it is a second front end, not a second implementation.
|
|
|
|
Build it, then point your MCP client at the produced binary:
|
|
|
|
```bash
|
|
dotnet publish src/NovelSoftware.Mcp -c Release -o ./mcp-server
|
|
```
|
|
|
|
`.mcp.json` (or Claude Desktop's config):
|
|
|
|
```jsonc
|
|
{
|
|
"mcpServers": {
|
|
"novel-software": {
|
|
"command": "/absolute/path/to/mcp-server/NovelSoftware.Mcp",
|
|
"env": { "NOVELSOFTWARE_API_URL": "http://localhost:5080" }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The API must be running. If it is not, the tools say so in a message the model can act on
|
|
rather than failing opaquely.
|
|
|
|
## API surface
|
|
|
|
`GET /api/health`, plus:
|
|
|
|
| Resource | Routes |
|
|
|---|---|
|
|
| Projects | `GET\|POST /api/projects`, `GET\|PATCH\|DELETE /api/projects/{id}` |
|
|
| Characters | `GET\|POST /api/projects/{id}/characters`, `GET\|PATCH\|DELETE /api/characters/{id}`, `POST /api/characters/{id}/relationships` |
|
|
| Outline | `GET\|POST /api/projects/{id}/outline`, `GET\|PATCH\|DELETE /api/outline/{id}`, `POST /api/outline/{id}/move` |
|
|
| Chapters | `GET\|POST /api/projects/{id}/chapters`, `GET\|PATCH\|DELETE /api/chapters/{id}` |
|
|
| Scenes | `GET\|POST /api/chapters/{id}/scenes`, `GET\|PATCH\|DELETE /api/scenes/{id}` |
|
|
| Agent | `GET /api/projects/{id}/agent/conversations`, `POST /api/projects/{id}/agent/messages`, `GET\|DELETE /api/conversations/{id}` |
|
|
|
|
`PATCH` bodies are partial: an omitted field is left alone, an empty string clears it.
|
|
Enums travel as names (`"Protagonist"`, `"Drafted"`), never ordinals. In development the
|
|
OpenAPI document is at `/openapi/v1.json`.
|
|
|
|
## Known issues
|
|
|
|
- `react-router-dom` 7.18.2 carries [GHSA-qwww-vcr4-c8h2](https://github.com/advisories/GHSA-qwww-vcr4-c8h2)
|
|
(CSRF bypass in RSC mode). No patched release exists yet, and every version below the
|
|
affected range carries 14 worse advisories. This app is a client-only SPA and does not
|
|
use RSC mode, so the advisory does not apply — but `npm audit` will flag it until a fix
|
|
ships. Upgrade when one does.
|
|
|
|
## Where this could go next
|
|
|
|
The vertical slice is complete but thin in places. The obvious next steps:
|
|
|
|
- Stream agent responses over SSE instead of returning the finished turn.
|
|
- Drag-and-drop reordering in the outline (the `move` endpoint is already there).
|
|
- A manuscript export (Markdown, DOCX) built from chapters and scenes in order.
|
|
- Revision history for scene prose.
|
|
- Authentication, if this is ever going to run anywhere but localhost.
|