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).
227 lines
12 KiB
C#
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);
|
|
}
|