Files
novelly/src/Novelly.Mcp/Tools/CharacterTools.cs
T
James Wampler 45afc980d3
CI / build-and-push (push) Successful in 51s
CI / deploy (push) Successful in 9s
Simplify character dossier fields, add ShowPronouns setting
Collapse Want/Need into Motivation and Internal/External Conflict into
Conflict, drop Arc summary field, and move tags below open questions on
the character page. Gate Pronouns display behind new UiSettings:ShowPronouns
config (default off).
2026-08-19 15:33:14 -07:00

227 lines
12 KiB
C#

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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> 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<CallToolResult> UnlinkCharacterIdentity(
NovelApiClient api,
[Description("The character's id.")] Guid characterId,
CancellationToken ct) =>
api.DeleteAsync($"/api/characters/{characterId}/identity", ct);
}