Files
novelly/README.md
T
James WamplerandClaude Opus 5 0d7b7a6f30 Add novel-writing app: .NET 10 API, React front end, agent and MCP server
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
2026-08-06 12:11:20 -07:00

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_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:

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-dom 7.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 — 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.