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 — (<Author>)` 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": "<matches the API's Auth:ServiceApiKey user secret>" + "type": "http", + "url": "http://localhost:5080/mcp", + "headers": { + "X-Novelly-Api-Key": "<matches the API's Auth:ServiceApiKey user secret>" } } } 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 @@ <Folder Name="/src/"> <Project Path="src/Novelly.Api/Novelly.Api.csproj" /> <Project Path="src/Novelly.AppHost/Novelly.AppHost.csproj" /> - <Project Path="src/Novelly.Mcp/Novelly.Mcp.csproj" /> <Project Path="src/Novelly.ServiceDefaults/Novelly.ServiceDefaults.csproj" /> <Project Path="src/Novelly.Web/Novelly.Web.esproj"> <Build /> 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": "<matches the API's Auth:ServiceApiKey user secret>" + "type": "http", + "url": "http://localhost:5080/mcp", + "headers": { + "X-Novelly-Api-Key": "<matches the API's Auth:ServiceApiKey user secret>" } } } @@ -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 <key> -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