Remove Scenes, group beats by multiple characters; strip comments repo-wide

Drop the Scene entity/grouping in favor of chapters carrying prose directly
and beats belonging to many characters. Add markdown editor + character
multi-select components to the web client. Remove all XML doc and inline
comments across the touched C#/TS/CSS files in favor of self-documenting
names, and record that convention in CLAUDE.md. Add .mcp.json (local MCP
server config, no secrets) and ignore .idea/.
This commit is contained in:
James Wampler
2026-08-11 21:05:13 -07:00
parent 1ce526019f
commit 23348327a9
57 changed files with 2600 additions and 1421 deletions
+10 -15
View File
@@ -9,8 +9,8 @@ public static class BeatTools
{
[McpServerTool(Name = "get_chapter_outline")]
[Description("Read a chapter's outline: its beats in order. Each beat is one row — a short "
+ "title, whose beat it is, what happened, and what it sets up. The chapter's "
+ "summary paragraph sits on the chapter itself, via get_chapter.")]
+ "title, who it belongs to, what happened, and what it sets up. The chapter's "
+ "summary paragraph and drafted prose sit on the chapter itself, via get_chapter.")]
public static Task<CallToolResult> GetChapterOutline(
NovelApiClient api,
[Description("The chapter's id.")] Guid chapterId,
@@ -26,34 +26,29 @@ public static class BeatTools
[Description("Three to five words naming the beat.")] string title,
CancellationToken ct,
[Description("Position in the chapter. Appended to the end when omitted.")] int? sortOrder = null,
[Description("Id of the character whose beat this is.")] Guid? characterId = null,
[Description("Ids of the characters whose beat this is.")] Guid[]? characterIds = null,
[Description("The event itself.")] string? whatHappened = null,
[Description("What it sets in motion — the hook into the next beat.")] string? whatsNext = null,
[Description("Id of the scene this beat will be written into, if decided.")] Guid? sceneId = null,
[Description("Tags for cross-referencing. Unknown tags are created.")] string[]? tags = null) =>
api.PostAsync($"/api/chapters/{chapterId}/beats",
new { title, sortOrder, characterId, whatHappened, whatsNext, sceneId, tags }, ct);
new { title, sortOrder, characterIds, whatHappened, whatsNext, tags }, ct);
[McpServerTool(Name = "update_beat")]
[Description("Revise a beat. Only the fields you supply change. Supplying a tag list replaces "
+ "the beat's tags outright, so include the ones you want to keep. A character or "
+ "scene id already set stays put unless you pass clearCharacter/clearScene — "
+ "leaving the id null means 'don't touch it', not 'remove it'.")]
[Description("Revise a beat. Only the fields you supply change. Supplying a characterIds or "
+ "tag list replaces the beat's characters or tags outright — pass an empty list "
+ "to clear one, and include everything you want to keep.")]
public static Task<CallToolResult> UpdateBeat(
NovelApiClient api,
[Description("The beat's id.")] Guid beatId,
CancellationToken ct,
[Description("Three to five words naming the beat.")] string? title = null,
[Description("Position in the chapter.")] int? sortOrder = null,
[Description("Id of the character whose beat this is.")] Guid? characterId = null,
[Description("Ids of the characters whose beat this is. Replaces the existing list.")] Guid[]? characterIds = null,
[Description("The event itself.")] string? whatHappened = null,
[Description("What it sets in motion.")] string? whatsNext = null,
[Description("Id of the scene this beat will be written into.")] Guid? sceneId = null,
[Description("Tags for cross-referencing. Replaces the existing tags.")] string[]? tags = null,
[Description("Detach this beat's character, leaving it unassigned.")] bool clearCharacter = false,
[Description("Detach this beat's scene, leaving it ungrouped.")] bool clearScene = false) =>
[Description("Tags for cross-referencing. Replaces the existing tags.")] string[]? tags = null) =>
api.PatchAsync($"/api/beats/{beatId}",
new { title, sortOrder, characterId, whatHappened, whatsNext, sceneId, tags, clearCharacter, clearScene }, ct);
new { title, sortOrder, characterIds, whatHappened, whatsNext, tags }, ct);
[McpServerTool(Name = "delete_beat")]
[Description("Remove a beat from a chapter's outline. Confirm with the writer first.")]
+9 -74
View File
@@ -8,7 +8,7 @@ namespace Novelly.Mcp.Tools;
public static class ManuscriptTools
{
[McpServerTool(Name = "list_chapters")]
[Description("List a project's chapters in manuscript order, with scene and word counts.")]
[Description("List a project's chapters in manuscript order, with beat and word counts.")]
public static Task<CallToolResult> ListChapters(
NovelApiClient api,
[Description("The project's id.")] Guid projectId,
@@ -16,7 +16,7 @@ public static class ManuscriptTools
api.GetAsync($"/api/projects/{projectId}/chapters", ct);
[McpServerTool(Name = "get_chapter")]
[Description("Read one chapter in full, including every scene and any drafted prose.")]
[Description("Read one chapter in full: its outline (beats) and its drafted prose.")]
public static Task<CallToolResult> GetChapter(
NovelApiClient api,
[Description("The chapter's id.")] Guid chapterId,
@@ -36,6 +36,7 @@ public static class ManuscriptTools
[Description("Where and when the chapter takes place.")] string? setting = null,
[Description("Planned, Outlined, Drafted, Revised or Final.")] string? status = null,
[Description("Target length in words.")] int? targetWordCount = null,
[Description("The chapter's drafted text, in markdown, if you are writing it now.")] string? prose = null,
[Description("Tags for cross-referencing. Unknown tags are created.")] string[]? tags = null) =>
api.PostAsync($"/api/projects/{projectId}/chapters", new
{
@@ -46,11 +47,14 @@ public static class ManuscriptTools
setting,
status = status ?? "Planned",
targetWordCount,
prose,
tags
}, ct);
[McpServerTool(Name = "update_chapter")]
[Description("Revise a chapter's title, number, summary, POV character, setting, notes or status.")]
[Description("Revise a chapter's title, number, summary, POV character, setting, notes, status "
+ "or drafted prose. Use 'prose' to write or replace the chapter's draft text in "
+ "markdown; the word count is recomputed automatically.")]
public static Task<CallToolResult> UpdateChapter(
NovelApiClient api,
[Description("The chapter's id.")] Guid chapterId,
@@ -63,77 +67,8 @@ public static class ManuscriptTools
[Description("Anything else worth recording.")] string? notes = null,
[Description("Planned, Outlined, Drafted, Revised or Final.")] string? status = null,
[Description("Target length in words.")] int? targetWordCount = null,
[Description("The chapter's drafted text, in markdown.")] string? prose = null,
[Description("Tags for cross-referencing. Replaces the existing tags.")] string[]? tags = null) =>
api.PatchAsync($"/api/chapters/{chapterId}",
new { title, number, summary, povCharacterId, setting, notes, status, targetWordCount, tags }, ct);
[McpServerTool(Name = "list_scenes")]
[Description("List a chapter's scenes in order.")]
public static Task<CallToolResult> ListScenes(
NovelApiClient api,
[Description("The chapter's id.")] Guid chapterId,
CancellationToken ct) =>
api.GetAsync($"/api/chapters/{chapterId}/scenes", ct);
[McpServerTool(Name = "create_scene")]
[Description("Add a scene to a chapter. The goal/conflict/outcome trio is what makes a scene "
+ "draftable later, so fill those in when there is enough to work with.")]
public static Task<CallToolResult> CreateScene(
NovelApiClient api,
[Description("The chapter's id.")] Guid chapterId,
[Description("Scene title.")] string title,
CancellationToken ct,
[Description("Position within the chapter. Appended to the end when omitted.")] int? sortOrder = null,
[Description("What happens in the scene.")] string? summary = null,
[Description("What the POV character is trying to achieve.")] string? goal = null,
[Description("What stands in the way.")] string? conflict = null,
[Description("How it lands, and what it costs.")] string? outcome = null,
[Description("Id of the point-of-view character.")] Guid? povCharacterId = null,
[Description("Where the scene takes place.")] string? location = null,
[Description("Drafted prose for the scene, if you are writing it now.")] string? prose = null,
[Description("Planned, Outlined, Drafted, Revised or Final.")] string? status = null) =>
api.PostAsync($"/api/chapters/{chapterId}/scenes", new
{
title,
sortOrder,
summary,
goal,
conflict,
outcome,
povCharacterId,
location,
prose,
status = status ?? "Planned"
}, ct);
[McpServerTool(Name = "update_scene")]
[Description("Revise a scene. Supplying 'prose' writes or replaces the scene's draft text and "
+ "recomputes its word count.")]
public static Task<CallToolResult> UpdateScene(
NovelApiClient api,
[Description("The scene's id.")] Guid sceneId,
CancellationToken ct,
[Description("New title.")] string? title = null,
[Description("Position within the chapter.")] int? sortOrder = null,
[Description("What happens in the scene.")] string? summary = null,
[Description("What the POV character is trying to achieve.")] string? goal = null,
[Description("What stands in the way.")] string? conflict = null,
[Description("How it lands, and what it costs.")] string? outcome = null,
[Description("Id of the point-of-view character.")] Guid? povCharacterId = null,
[Description("Where the scene takes place.")] string? location = null,
[Description("Drafted prose for the scene.")] string? prose = null,
[Description("Planned, Outlined, Drafted, Revised or Final.")] string? status = null) =>
api.PatchAsync($"/api/scenes/{sceneId}", new
{
title,
sortOrder,
summary,
goal,
conflict,
outcome,
povCharacterId,
location,
prose,
status
}, ct);
new { title, number, summary, povCharacterId, setting, notes, status, targetWordCount, prose, tags }, ct);
}