- Characters can carry aliases and be linked as the same underlying person (canonical SameCharacterAsId, optional reveal chapter/note), surfaced through the API, MCP tools, agent toolset, and web UI. - Characters page redesigned as a filterable/sortable table (name+ aliases, role, importance, occupation, tags) instead of a sidebar list, to stay usable as the cast grows. - Beats can be moved between chapters (BeatService.MoveAsync + MCP/ agent tool + endpoint). - Add a keyboard-shortcuts help overlay (HelpButton/HelpOverlayContext) wired into the project layout. - CLAUDE.md: require every frontend component to carry a unique id attribute; apply it to CharacterMultiSelect and MarkdownEditor.
93 lines
5.2 KiB
C#
93 lines
5.2 KiB
C#
using System.ComponentModel;
|
|
using ModelContextProtocol.Protocol;
|
|
using ModelContextProtocol.Server;
|
|
|
|
namespace Novelly.Mcp.Tools;
|
|
|
|
[McpServerToolType]
|
|
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, 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,
|
|
CancellationToken ct) =>
|
|
api.GetAsync($"/api/chapters/{chapterId}/beats", ct);
|
|
|
|
[McpServerTool(Name = "create_beat")]
|
|
[Description("Add a beat to a chapter's outline. Keep the title to three to five words — it "
|
|
+ "is a handle, not a sentence; detail belongs in whatHappened and whatsNext.")]
|
|
public static Task<CallToolResult> CreateBeat(
|
|
NovelApiClient api,
|
|
[Description("The chapter's id.")] Guid chapterId,
|
|
[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("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("Tags for cross-referencing. Unknown tags are created.")] string[]? tags = null) =>
|
|
api.PostAsync($"/api/chapters/{chapterId}/beats",
|
|
new { title, sortOrder, characterIds, whatHappened, whatsNext, tags }, ct);
|
|
|
|
[McpServerTool(Name = "update_beat")]
|
|
[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("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("Tags for cross-referencing. Replaces the existing tags.")] string[]? tags = null) =>
|
|
api.PatchAsync($"/api/beats/{beatId}",
|
|
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.")]
|
|
public static Task<CallToolResult> DeleteBeat(
|
|
NovelApiClient api,
|
|
[Description("The beat's id.")] Guid beatId,
|
|
CancellationToken ct) =>
|
|
api.DeleteAsync($"/api/beats/{beatId}", ct);
|
|
|
|
[McpServerTool(Name = "assign_character_to_beats")]
|
|
[Description("Add a character to several beats at once. Leaves each beat's existing characters "
|
|
+ "and other fields alone — this only adds, it never removes.")]
|
|
public static Task<CallToolResult> AssignCharacterToBeats(
|
|
NovelApiClient api,
|
|
[Description("The chapter's id.")] Guid chapterId,
|
|
[Description("Id of the character to add.")] Guid characterId,
|
|
[Description("Ids of the beats to add the character to.")] Guid[] beatIds,
|
|
CancellationToken ct) =>
|
|
api.PostAsync($"/api/chapters/{chapterId}/beats/assign-character", new { characterId, beatIds }, ct);
|
|
|
|
[McpServerTool(Name = "reorder_beats")]
|
|
[Description("Renumber a chapter's beats to match the order given. List every beat id in the "
|
|
+ "order wanted; any left out keep their relative position at the end.")]
|
|
public static Task<CallToolResult> ReorderBeats(
|
|
NovelApiClient api,
|
|
[Description("The chapter's id.")] Guid chapterId,
|
|
[Description("Beat ids in their new order.")] Guid[] beatIds,
|
|
CancellationToken ct) =>
|
|
api.PostAsync($"/api/chapters/{chapterId}/beats/reorder", new { beatIds }, ct);
|
|
|
|
[McpServerTool(Name = "move_beats")]
|
|
[Description("Move one or more beats from one chapter to another, appending them to the "
|
|
+ "target chapter's end in the order given.")]
|
|
public static Task<CallToolResult> MoveBeats(
|
|
NovelApiClient api,
|
|
[Description("The beats' current chapter id.")] Guid chapterId,
|
|
[Description("Id of the chapter to move the beats into.")] Guid targetChapterId,
|
|
[Description("Ids of the beats to move.")] Guid[] beatIds,
|
|
CancellationToken ct) =>
|
|
api.PostAsync($"/api/chapters/{chapterId}/beats/move", new { targetChapterId, beatIds }, ct);
|
|
}
|