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
6.8 KiB
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+.
# 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:
export ANTHROPIC_API_KEY=sk-ant-...
Without it, agent endpoints return 503 Agent unavailable with an explanatory message
and everything else keeps working.
Tests
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:
{
"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_errorresults, not exceptions — the model reads the message and corrects itself. MaxIterationscaps 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:
dotnet publish src/NovelSoftware.Mcp -c Release -o ./mcp-server
.mcp.json (or Claude Desktop's config):
{
"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-dom7.18.2 carries 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 — butnpm auditwill 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
moveendpoint 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.