diff --git a/.claude/agents/outline-importer.md b/.claude/agents/outline-importer.md
index 4d226df..90bf24b 100644
--- a/.claude/agents/outline-importer.md
+++ b/.claude/agents/outline-importer.md
@@ -1,7 +1,7 @@
---
name: outline-importer
description: Imports an author's existing novel outline (chapters + character dossiers, in the Kingdom Sleeps folder format) into a Novelly project over the MCP server. Invoke explicitly with a source folder path — this agent does not run proactively.
-tools: Read, Glob, Grep, Write, mcp__novelly__list_projects, mcp__novelly__get_project_brief, mcp__novelly__create_project, mcp__novelly__update_project_brief, mcp__novelly__list_chapters, mcp__novelly__get_chapter, mcp__novelly__create_chapter, mcp__novelly__update_chapter, mcp__novelly__get_chapter_outline, mcp__novelly__create_beat, mcp__novelly__update_beat, mcp__novelly__list_characters, mcp__novelly__get_character, mcp__novelly__create_character, mcp__novelly__update_character, mcp__novelly__get_character_arc, mcp__novelly__add_arc_stage, mcp__novelly__list_tags
+tools: Read, Glob, Grep, Write, mcp__novelly__list_novels, mcp__novelly__get_novel_brief, mcp__novelly__create_novel, mcp__novelly__update_novel_brief, mcp__novelly__list_chapters, mcp__novelly__get_chapter, mcp__novelly__create_chapter, mcp__novelly__update_chapter, mcp__novelly__get_chapter_outline, mcp__novelly__create_beat, mcp__novelly__update_beat, mcp__novelly__list_characters, mcp__novelly__get_character, mcp__novelly__create_character, mcp__novelly__update_character, mcp__novelly__get_character_arc, mcp__novelly__add_arc_stage, mcp__novelly__list_tags
model: inherit
---
@@ -101,9 +101,9 @@ in your final report.
Do not skip ahead — each pass depends on ids the previous one minted. If you're picking up a
resumed run, jump straight to the first incomplete pass.
-**0. Preflight.** Call `list_projects` to confirm the API is reachable at all — if this fails, stop
-and tell the user to start the API (`ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/Novelly.Api`)
-and that `.mcp.json` must point at a published `Novelly.Mcp` binary. Glob the source root for
+**0. Preflight.** Call `list_novels` to confirm the API is reachable at all — if this fails, stop
+and tell the user to start the API (`ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/Novelly.Api`).
+Glob the source root for
`outline.md`, `outlines/*.md` or `chapters/*.md`, and `characters/*.md`. If `outline.md` is
missing, stop — that's the one file every pass depends on. Report the file counts found before
proceeding.
@@ -111,7 +111,7 @@ proceeding.
**1. Project.** Skip if `completedPasses` already has `"project"`. Read `outline.md`. Its heading is
`# Outline —
()` or similar — parse title and author out of it; if there's no
author, leave it null. The paragraph(s) before the chapter table are the blurb — pass as `notes`
-argument to `create_project` (there's no dedicated blurb field; `synopsis` may be filled in later
+argument to `create_novel` (there's no dedicated blurb field; `synopsis` may be filled in later
by the author). Record `projectId` in the ledger, mark `"project"` complete.
**2. Characters — dossier fields only, not arcs yet.** Skip files whose name (matched
diff --git a/.mcp.json.example b/.mcp.json.example
index e6ebe4d..ebde340 100644
--- a/.mcp.json.example
+++ b/.mcp.json.example
@@ -1,10 +1,10 @@
{
"mcpServers": {
"novelly": {
- "command": "./mcp-server/Novelly.Mcp",
- "env": {
- "NOVELLY_API_URL": "http://localhost:5080",
- "NOVELLY_API_KEY": ""
+ "type": "http",
+ "url": "http://localhost:5080/mcp",
+ "headers": {
+ "X-Novelly-Api-Key": ""
}
}
}
diff --git a/CLAUDE.md b/CLAUDE.md
index 02e8c67..506d1bb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -8,13 +8,12 @@ Novelly: software plan + write novel. ASP.NET Core 10, C#, TypeScript, React, .N
## Structure
-- `src/Novelly.Api/` — whole back end, organised by feature. One folder per feature holds
- entity, DTOs, service, endpoints together: `Novels/`, `Characters/`, `Chapters/`, `Beats/`,
- `Scenes/`, `Tags/`, `Agent/`. `Common/` holds what crosses features; `Data/` holds
- `DbContext` + EF migrations.
+- `src/Novelly.Api/` — whole back end, organised by feature, plus the MCP endpoint. One folder
+ per feature holds entity, DTOs, service, endpoints together: `Novels/`, `Characters/`,
+ `Chapters/`, `Beats/`, `Scenes/`, `Tags/`, `Agent/`, `Mcp/`. `Common/` holds what crosses
+ features; `Data/` holds `DbContext` + EF migrations.
- `src/Novelly.AppHost/` — .NET Aspire orchestration; run this to bring up API + web client
- `src/Novelly.ServiceDefaults/` — shared Aspire wiring: OpenTelemetry, health checks, service discovery
-- `src/Novelly.Mcp/` — MCP stdio server
- `src/Novelly.Web/` — React + Vite client
- `tests/` — test suite
- `docs/` — documentation
@@ -86,7 +85,7 @@ Build + tests passing ≠ working. Anything touching endpoint, agent loop, or MC
Vite dev server on :5173, dashboard for logs + traces
- API alone: `ASPNETCORE_URLS=http://localhost:5080 dotnet run --project src/Novelly.Api`, then exercise route with curl
- Web alone: `cd src/Novelly.Web && npm run dev` — proxies `/api` to :5080
-- MCP: build it, then drive over stdio JSON-RPC (`initialize` → `notifications/initialized` → `tools/list` → `tools/call`)
+- MCP: with the API running, drive `/mcp` over HTTP (`initialize` → `notifications/initialized` → `tools/list` → `tools/call`) — Streamable HTTP, so responses are SSE-framed and requests need `Accept: application/json, text/event-stream`
Several real bugs here — SQLite refusing ORDER BY DateTimeOffset, agent's model client throwing at construction + taking read-only endpoints down with it — passed build + test suite, only showed up when app actually ran.
diff --git a/Novelly.slnx b/Novelly.slnx
index cbee84c..84fabc4 100644
--- a/Novelly.slnx
+++ b/Novelly.slnx
@@ -8,7 +8,6 @@
-
diff --git a/README.md b/README.md
index e9f771a..4e2c08b 100644
--- a/README.md
+++ b/README.md
@@ -6,22 +6,21 @@
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.
+embedded in the app that can read and edit the same data you can, and an MCP endpoint that
+exposes that same 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.
+React UI's REST calls, the embedded agent, and MCP clients all resolve to the same
+application services in-process, 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 |
|---|---|
-| `Novelly.Api` | ASP.NET Core 10 minimal APIs, EF Core 10 + SQLite, Anthropic SDK, OpenAPI |
+| `Novelly.Api` | ASP.NET Core 10 minimal APIs, EF Core 10 + SQLite, Anthropic SDK, OpenAPI, MCP over Streamable HTTP (`ModelContextProtocol.AspNetCore`) |
| `Novelly.AppHost` | .NET Aspire orchestration for the API and the web client |
| `Novelly.ServiceDefaults` | Shared OpenTelemetry, health checks and service discovery |
-| `Novelly.Mcp` | MCP stdio server (`ModelContextProtocol`) |
| `Novelly.Web` | React 19, TypeScript, Vite, TanStack Query, Tailwind v4 |
The back end is one project organised by feature, not by layer. Each feature folder —
@@ -192,19 +191,10 @@ A few deliberate choices worth knowing about:
## The MCP server
-A stdio MCP server exposing 45 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
-./scripts/publish-mcp.sh
-```
-
-Aspire does not run or manage this process — it's a separate stdio subprocess your MCP
-client spawns directly, so nothing rebuilds it automatically. Re-run the script (and
-reconnect your MCP client) after pulling changes that touch `src/Novelly.Mcp`, or it keeps
-serving whatever was published last, including against a stale auth contract.
+The API itself serves MCP over Streamable HTTP at `POST /mcp`, exposing 45 tools that call
+the same application services the REST endpoints and the embedded web agent call — it holds
+no domain logic of its own, and there's nothing to build or publish separately. The API
+process just needs to be running; there's no separate subprocess to keep in sync with it.
Copy `.mcp.json.example` to `.mcp.json` (gitignored, since it carries your API key) and
fill in the key:
@@ -213,10 +203,10 @@ fill in the key:
{
"mcpServers": {
"novelly": {
- "command": "/absolute/path/to/mcp-server/Novelly.Mcp",
- "env": {
- "NOVELLY_API_URL": "http://localhost:5080",
- "NOVELLY_API_KEY": ""
+ "type": "http",
+ "url": "http://localhost:5080/mcp",
+ "headers": {
+ "X-Novelly-Api-Key": ""
}
}
}
@@ -225,8 +215,12 @@ fill in the key:
The API must be running, with `Auth:ServiceApiKey` set (e.g. via
`dotnet user-secrets set Auth:ServiceApiKey -p src/Novelly.Api`) to the same value
-as `NOVELLY_API_KEY` above. If the API is not running, or the key is missing or mismatched,
-the tools say so in a message the model can act on rather than failing opaquely.
+as the `X-Novelly-Api-Key` header above. If the API is not running, or the key is missing or
+mismatched, the request 401s.
+
+Tool argument names are camelCase, matching the REST API and every other MCP argument name
+this project has ever used. `create_novel` called over MCP is owned by the seeded service
+user (an Admin), not whichever person is signed into the web app.
### Importing an existing outline