using System.ComponentModel; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; namespace Novelly.Mcp.Tools; [McpServerToolType] public static class CharacterTools { [McpServerTool(Name = "list_characters")] [Description("List a novel's character dossiers in full, including their relationships.")] public static Task ListCharacters( NovelApiClient api, [Description("The novel's id.")] Guid novelId, CancellationToken ct) => api.GetAsync($"/api/novels/{novelId}/characters", ct); [McpServerTool(Name = "get_character")] [Description("Read one character's dossier.")] public static Task GetCharacter( NovelApiClient api, [Description("The character's id.")] Guid characterId, CancellationToken ct) => api.GetAsync($"/api/characters/{characterId}", ct); [McpServerTool(Name = "create_character")] [Description("Add a character dossier to a novel. Name is the only requirement — leave a field " + "blank when the writer has not decided it yet rather than inventing detail.")] public static Task CreateCharacter( NovelApiClient api, [Description("The novel's id.")] Guid novelId, [Description("The character's name.")] string name, CancellationToken ct, [Description("Protagonist, Antagonist, Deuteragonist, Supporting, Minor, Mentor, LoveInterest or Foil.")] string? role = null, [Description("Main or Supporting. Main characters are the few the story is about and are worth tracking an arc for.")] string? importance = null, [Description("Age, exact or approximate.")] string? age = null, [Description("The pronouns this character uses.")] string? pronouns = null, [Description("What they do.")] string? occupation = null, [Description("How they look.")] string? appearance = null, [Description("Temperament, habits, how they treat people.")] string? personality = null, [Description("History that shapes who they are now.")] string? backstory = null, [Description("What they consciously pursue, weighed against what they actually need.")] string? motivation = null, [Description("The war inside them and what in the world opposes them.")] string? conflict = null, [Description("Speech patterns and register that make their dialogue theirs.")] string? voice = null, [Description("Anything else worth recording.")] string? notes = null, [Description("Tags for cross-referencing. Unknown tags are created.")] string[]? tags = null, [Description("Other names this character is known by.")] string[]? aliases = null) => api.PostAsync($"/api/novels/{novelId}/characters", new { name, role = role ?? "Supporting", importance = importance ?? "Supporting", age, pronouns, occupation, appearance, personality, backstory, motivation, conflict, voice, notes, tags, aliases }, ct); [McpServerTool(Name = "update_character")] [Description("Revise an existing character dossier. Only the fields you supply change.")] public static Task UpdateCharacter( NovelApiClient api, [Description("The character's id.")] Guid characterId, CancellationToken ct, [Description("New name.")] string? name = null, [Description("Protagonist, Antagonist, Deuteragonist, Supporting, Minor, Mentor, LoveInterest or Foil.")] string? role = null, [Description("Main or Supporting. Main characters are the few the story is about and are worth tracking an arc for.")] string? importance = null, [Description("Age, exact or approximate.")] string? age = null, [Description("The pronouns this character uses.")] string? pronouns = null, [Description("What they do.")] string? occupation = null, [Description("How they look.")] string? appearance = null, [Description("Temperament, habits, how they treat people.")] string? personality = null, [Description("History that shapes who they are now.")] string? backstory = null, [Description("What they consciously pursue, weighed against what they actually need.")] string? motivation = null, [Description("The war inside them and what in the world opposes them.")] string? conflict = null, [Description("Speech patterns and register.")] string? voice = null, [Description("Anything else worth recording.")] string? notes = null, [Description("Tags for cross-referencing. Replaces the existing tags.")] string[]? tags = null, [Description("Other names this character is known by. Replaces the existing aliases.")] string[]? aliases = null) => api.PatchAsync($"/api/characters/{characterId}", new { name, role, importance, age, pronouns, occupation, appearance, personality, backstory, motivation, conflict, voice, notes, tags, aliases }, ct); [McpServerTool(Name = "get_character_beats")] [Description("Every beat this character appears in, across the whole book, in manuscript order. " + "This is what the character actually does on the page, as opposed to what the " + "dossier claims about them — read it before revising a character.")] public static Task GetCharacterBeats( NovelApiClient api, [Description("The character's id.")] Guid characterId, CancellationToken ct) => api.GetAsync($"/api/characters/{characterId}/beats", ct); [McpServerTool(Name = "get_character_arc")] [Description("Read a main character's arc: the ordered stages of how they change, each " + "optionally pinned to the chapter where it lands.")] public static Task GetCharacterArc( NovelApiClient api, [Description("The character's id.")] Guid characterId, CancellationToken ct) => api.GetAsync($"/api/characters/{characterId}/arc", ct); [McpServerTool(Name = "add_arc_stage")] [Description("Add a stage to a character's arc. Arcs are kept for main characters — promote " + "the character with update_character first if they are still Supporting.")] public static Task AddArcStage( NovelApiClient api, [Description("Id of the character whose arc to add to.")] Guid characterId, [Description("A short handle for the change, three to five words.")] string title, CancellationToken ct, [Description("What this stage of the arc results in for the character — what shifts, and what it costs them.")] string? result = null, [Description("Id of the chapter where this stage lands, if it is pinned to one.")] Guid? chapterId = null, [Description("Position in the arc. Appended to the end when omitted.")] int? sortOrder = null) => api.PostAsync($"/api/characters/{characterId}/arc", new { title, sortOrder, result, chapterId }, ct); [McpServerTool(Name = "update_arc_stage")] [Description("Revise a stage of a character's arc. Only the fields you supply change.")] public static Task UpdateArcStage( NovelApiClient api, [Description("The arc stage's id.")] Guid arcStageId, CancellationToken ct, [Description("New title for the stage.")] string? title = null, [Description("What this stage of the arc results in for the character.")] string? result = null, [Description("Id of the chapter where this stage lands.")] Guid? chapterId = null, [Description("Position in the arc.")] int? sortOrder = null) => api.PatchAsync($"/api/arc-stages/{arcStageId}", new { title, sortOrder, result, chapterId }, ct); [McpServerTool(Name = "delete_arc_stage")] [Description("Remove a stage from a character's arc.")] public static Task DeleteArcStage( NovelApiClient api, [Description("The arc stage's id.")] Guid arcStageId, CancellationToken ct) => api.DeleteAsync($"/api/arc-stages/{arcStageId}", ct); [McpServerTool(Name = "reorder_arc_stages")] [Description("Renumber a character's arc to match the order given. Stages left out keep their " + "relative position after the ones listed.")] public static Task ReorderArcStages( NovelApiClient api, [Description("Id of the character whose arc to reorder.")] Guid characterId, [Description("Arc stage ids in the order wanted.")] string[] stageIds, CancellationToken ct) => api.PostAsync($"/api/characters/{characterId}/arc/reorder", new { stageIds }, ct); [McpServerTool(Name = "set_arc_stage_beats")] [Description("Set which beats belong to an arc stage, replacing its current set. This groups the " + "chapter-level beats that establish or pay off this stage of the character's arc. A " + "beat moved into this stage leaves any other stage of the same character it was in. " + "Each beat must already include this character.")] public static Task SetArcStageBeats( NovelApiClient api, [Description("The arc stage's id.")] Guid arcStageId, [Description("Beat ids that belong to this stage, replacing whatever was there before.")] string[] beatIds, CancellationToken ct) => api.PostAsync($"/api/arc-stages/{arcStageId}/beats", new { beatIds }, ct); [McpServerTool(Name = "relate_characters")] [Description("Record a relationship between two characters in the same novel. Creates both directions " + "at once — characterId's side and relatedCharacterId's side — so the pair always shows up " + "on both dossiers.")] public static Task RelateCharacters( NovelApiClient api, [Description("Id of the character the relationship belongs to.")] Guid characterId, [Description("Id of the character they are related to.")] Guid relatedCharacterId, [Description("How characterId is related to relatedCharacterId, e.g. 'sister', 'rival', 'former mentor'.")] string relationshipType, CancellationToken ct, [Description("How relatedCharacterId is related back to characterId, if different — e.g. 'brother' for " + "'sister'. Defaults to relationshipType when the relation is symmetric, like 'rival'.")] string? reciprocalRelationshipType = null, [Description("What the relationship is like, and where it is headed.")] string? description = null) => api.PostAsync($"/api/characters/{characterId}/relationships", new { relatedCharacterId, relationshipType, reciprocalRelationshipType, description }, ct); [McpServerTool(Name = "link_character_identity")] [Description("Record that this character is really another character — e.g. a character introduced " + "under one name who is later revealed to be a character already in the novel under " + "another name. Both characters keep their own dossier and beats; the canonical identity " + "is whichever character you link to.")] public static Task LinkCharacterIdentity( NovelApiClient api, [Description("Id of the character being revealed as someone else.")] Guid characterId, [Description("Id of the character this one really is.")] Guid sameCharacterAsId, CancellationToken ct, [Description("Id of the chapter where the reveal happens, if any.")] Guid? revealedInChapterId = null, [Description("Context on the reveal, e.g. how and why the disguise held.")] string? note = null) => api.PutAsync($"/api/characters/{characterId}/identity", new { sameCharacterAsId, revealedInChapterId, note }, ct); [McpServerTool(Name = "unlink_character_identity")] [Description("Remove a character's identity link, restoring it to its own separate identity.")] public static Task UnlinkCharacterIdentity( NovelApiClient api, [Description("The character's id.")] Guid characterId, CancellationToken ct) => api.DeleteAsync($"/api/characters/{characterId}/identity", ct); }