Delete the stdio MCP server now that the API serves /mcp directly
src/Novelly.Mcp was a separate stdio process, unbuilt by CI, that looped back over HTTP to the same REST API the previous commit's /mcp endpoint now calls in-process. Nothing else referenced it (not CI, not Docker, not the AppHost), so removal is just the project, its solution entry, and scripts/publish-mcp.sh. Updates .mcp.json / .mcp.json.example to the type: http form, fixes .claude/agents/outline-importer.md's already-stale tool references (list_projects/create_project/etc. never existed; the real names are list_novels/create_novel/etc.), and rewrites README + CLAUDE.md's description of the MCP surface and how to verify it at runtime.
This commit is contained in:
@@ -1,15 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Rebuilds the standalone Novelly.Mcp binary that Claude Code (or Claude Desktop) spawns
|
|
||||||
# per .mcp.json. Aspire does not run or manage this process, so nothing else rebuilds it —
|
|
||||||
# run this after pulling changes that touch src/Novelly.Mcp, or the MCP server silently
|
|
||||||
# keeps serving whatever was published last.
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")/.." && source ./scripts/ci/lib.sh
|
|
||||||
cd "$CI_ROOT"
|
|
||||||
|
|
||||||
ensure_dotnet
|
|
||||||
|
|
||||||
log "Publishing Novelly.Mcp to ./mcp-server"
|
|
||||||
dotnet publish src/Novelly.Mcp -c Release -o ./mcp-server
|
|
||||||
|
|
||||||
log "Done. Reconnect the MCP server (e.g. /mcp in Claude Code) to pick up the new build."
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using ModelContextProtocol.Protocol;
|
|
||||||
|
|
||||||
namespace Novelly.Mcp;
|
|
||||||
|
|
||||||
public class NovelApiClient(HttpClient http, ILogger<NovelApiClient> logger)
|
|
||||||
{
|
|
||||||
private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
};
|
|
||||||
|
|
||||||
public Task<CallToolResult> GetAsync(string path, CancellationToken ct = default) => SendAsync(new HttpRequestMessage(HttpMethod.Get, path), ct);
|
|
||||||
|
|
||||||
public Task<CallToolResult> PostAsync(string path, object body, CancellationToken ct = default) =>
|
|
||||||
SendAsync(new HttpRequestMessage(HttpMethod.Post, path)
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(body, options: Options)
|
|
||||||
}, ct);
|
|
||||||
|
|
||||||
public Task<CallToolResult> PatchAsync(string path, object body, CancellationToken ct = default) =>
|
|
||||||
SendAsync(new HttpRequestMessage(HttpMethod.Patch, path)
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(body, options: Options)
|
|
||||||
}, ct);
|
|
||||||
|
|
||||||
public Task<CallToolResult> PutAsync(string path, object body, CancellationToken ct = default) =>
|
|
||||||
SendAsync(new HttpRequestMessage(HttpMethod.Put, path)
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(body, options: Options)
|
|
||||||
}, ct);
|
|
||||||
|
|
||||||
public Task<CallToolResult> DeleteAsync(string path, CancellationToken ct = default) => SendAsync(new HttpRequestMessage(HttpMethod.Delete, path), ct);
|
|
||||||
|
|
||||||
private async Task<CallToolResult> SendAsync(HttpRequestMessage request, CancellationToken ct)
|
|
||||||
{
|
|
||||||
HttpResponseMessage response;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
response = await http.SendAsync(request, ct);
|
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Could not reach the Novelly API at {BaseAddress}", http.BaseAddress);
|
|
||||||
return Error($"Could not reach the Novelly API at {http.BaseAddress}. Is it running? ({ex.Message})");
|
|
||||||
}
|
|
||||||
|
|
||||||
var body = await response.Content.ReadAsStringAsync(ct);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return Ok(string.IsNullOrWhiteSpace(body) ? "{\"ok\":true}" : Prettify(body));
|
|
||||||
}
|
|
||||||
|
|
||||||
var detail = TryReadProblemDetail(body) ?? body;
|
|
||||||
return Error(response.StatusCode switch
|
|
||||||
{
|
|
||||||
HttpStatusCode.Unauthorized => $"Not permitted: the Novelly API rejected the service api key. Set NOVELLY_API_KEY to match the API's Auth:ServiceApiKey. ({detail})",
|
|
||||||
HttpStatusCode.Forbidden => $"Not permitted: {detail}",
|
|
||||||
HttpStatusCode.NotFound => $"Not found: {detail}",
|
|
||||||
HttpStatusCode.BadRequest => $"Rejected: {detail}",
|
|
||||||
_ => $"API returned {(int)response.StatusCode}: {detail}"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CallToolResult Ok(string text) =>
|
|
||||||
new() { Content = [new TextContentBlock { Text = text }] };
|
|
||||||
|
|
||||||
private static CallToolResult Error(string message) =>
|
|
||||||
new() { Content = [new TextContentBlock { Text = message }], IsError = true };
|
|
||||||
|
|
||||||
private string Prettify(string json)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return JsonSerializer.Serialize(JsonSerializer.Deserialize<JsonElement>(json), Options);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
logger.LogWarning(ex, "Response body was not valid JSON; returning it unformatted");
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? TryReadProblemDetail(string body)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var problem = JsonSerializer.Deserialize<JsonElement>(body);
|
|
||||||
return problem.TryGetProperty("detail", out var detail) ? detail.GetString() : null;
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
logger.LogWarning(ex, "Error response body was not valid JSON problem details");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.10" />
|
|
||||||
<PackageReference Include="ModelContextProtocol" Version="2.1.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Novelly.Mcp;
|
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
|
||||||
|
|
||||||
builder.Logging.ClearProviders();
|
|
||||||
builder.Logging.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace);
|
|
||||||
builder.Logging.SetMinimumLevel(LogLevel.Warning);
|
|
||||||
|
|
||||||
var apiBaseUrl = builder.Configuration["NOVELLY_API_URL"] ?? "http://localhost:5080";
|
|
||||||
var apiKey = builder.Configuration["NOVELLY_API_KEY"];
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient<NovelApiClient>(client =>
|
|
||||||
{
|
|
||||||
client.BaseAddress = new Uri(apiBaseUrl);
|
|
||||||
client.Timeout = TimeSpan.FromSeconds(30);
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(apiKey))
|
|
||||||
{
|
|
||||||
client.DefaultRequestHeaders.Add("X-Novelly-Api-Key", apiKey);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services
|
|
||||||
.AddMcpServer()
|
|
||||||
.WithStdioServerTransport()
|
|
||||||
.WithToolsFromAssembly();
|
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using ModelContextProtocol.Protocol;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace Novelly.Mcp.Tools;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public static class LocationTools
|
|
||||||
{
|
|
||||||
[McpServerTool(Name = "list_locations")]
|
|
||||||
[Description("List a novel's locations with how many chapters are set there. "
|
|
||||||
+ "Read this before inventing a new location so you reuse the writer's vocabulary.")]
|
|
||||||
public static Task<CallToolResult> ListLocations(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.GetAsync($"/api/novels/{novelId}/locations", ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "get_location_references")]
|
|
||||||
[Description("Cross-reference a location: every chapter set there.")]
|
|
||||||
public static Task<CallToolResult> GetLocationReferences(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The location's id.")] Guid locationId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.GetAsync($"/api/locations/{locationId}/references", ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "create_location")]
|
|
||||||
[Description("Create a location explicitly. Applying an unknown location by name to a chapter "
|
|
||||||
+ "also creates it, so this is only needed to set one up ahead of time.")]
|
|
||||||
public static Task<CallToolResult> CreateLocation(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
[Description("The location's name. Unique within the novel, matched case-insensitively.")] string name,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.PostAsync($"/api/novels/{novelId}/locations", new { name }, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "update_location")]
|
|
||||||
[Description("Rename a location. Renaming updates it everywhere it is applied.")]
|
|
||||||
public static Task<CallToolResult> UpdateLocation(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The location's id.")] Guid locationId,
|
|
||||||
[Description("New name.")] string name,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.PatchAsync($"/api/locations/{locationId}", new { name }, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "delete_location")]
|
|
||||||
[Description("Move a location to the trash. Whatever carried it is left alone — only the label goes. "
|
|
||||||
+ "It can be restored from the Trash page within the retention window.")]
|
|
||||||
public static Task<CallToolResult> DeleteLocation(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The location's id.")] Guid locationId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.DeleteAsync($"/api/locations/{locationId}", ct);
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using ModelContextProtocol.Protocol;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace Novelly.Mcp.Tools;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public static class ManuscriptTools
|
|
||||||
{
|
|
||||||
[McpServerTool(Name = "list_chapters")]
|
|
||||||
[Description("List a novel's chapters in manuscript order, with beat and word counts.")]
|
|
||||||
public static Task<CallToolResult> ListChapters(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.GetAsync($"/api/novels/{novelId}/chapters", ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "get_chapter")]
|
|
||||||
[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,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.GetAsync($"/api/chapters/{chapterId}", ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "create_chapter")]
|
|
||||||
[Description("Add a chapter to a novel. It goes at the end of the manuscript unless you supply a number. "
|
|
||||||
+ "Use 'kind' for a foreword, prologue, afterword, or other unnumbered front/back matter.")]
|
|
||||||
public static Task<CallToolResult> CreateChapter(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
[Description("Chapter title.")] string title,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("Manuscript position, 1-based, counting front and back matter.")] int? number = null,
|
|
||||||
[Description("FrontMatter, Body, or BackMatter. Defaults to Body.")] string? kind = null,
|
|
||||||
[Description("The chapter's outline summary paragraph.")] string? summary = null,
|
|
||||||
[Description("Where and when the chapter takes place. Unknown locations are created.")] string[]? locations = 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/novels/{novelId}/chapters", new
|
|
||||||
{
|
|
||||||
title,
|
|
||||||
number,
|
|
||||||
kind = kind ?? "Body",
|
|
||||||
summary,
|
|
||||||
locations,
|
|
||||||
status = status ?? "Planned",
|
|
||||||
targetWordCount,
|
|
||||||
prose,
|
|
||||||
tags
|
|
||||||
}, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "update_chapter")]
|
|
||||||
[Description("Revise a chapter's title, number, kind, summary, locations, 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,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("New title.")] string? title = null,
|
|
||||||
[Description("Manuscript position, 1-based, counting front and back matter.")] int? number = null,
|
|
||||||
[Description("FrontMatter, Body, or BackMatter.")] string? kind = null,
|
|
||||||
[Description("The chapter's outline summary paragraph.")] string? summary = null,
|
|
||||||
[Description("Where and when the chapter takes place. Replaces the existing locations. Unknown locations are created.")] string[]? locations = null,
|
|
||||||
[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, kind, summary, locations, notes, status, targetWordCount, prose, tags }, ct);
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using ModelContextProtocol.Protocol;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace Novelly.Mcp.Tools;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public static class NovelTools
|
|
||||||
{
|
|
||||||
[McpServerTool(Name = "list_novels")]
|
|
||||||
[Description("List every novel, with counts of characters, chapters and drafted words. "
|
|
||||||
+ "Start here to find the novel id everything else needs.")]
|
|
||||||
public static Task<CallToolResult> ListNovels(NovelApiClient api, CancellationToken ct) =>
|
|
||||||
api.GetAsync("/api/novels", ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "get_novel_brief")]
|
|
||||||
[Description("Read a novel's title, author, genre, logline, synopsis, notes and word-count target.")]
|
|
||||||
public static Task<CallToolResult> GetNovel(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.GetAsync($"/api/novels/{novelId}", ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "create_novel")]
|
|
||||||
[Description("Create a new novel.")]
|
|
||||||
public static Task<CallToolResult> CreateNovel(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("Working title.")] string title,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("Author name.")] string? author = null,
|
|
||||||
[Description("Genre or category.")] string? genre = null,
|
|
||||||
[Description("One-sentence pitch.")] string? logline = null,
|
|
||||||
[Description("Paragraph-length summary of the whole book.")] string? synopsis = null,
|
|
||||||
[Description("Free-form notes on theme, tone, comparable titles.")] string? notes = null,
|
|
||||||
[Description("Target manuscript length in words.")] int? targetWordCount = null) =>
|
|
||||||
api.PostAsync("/api/novels", new { title, author, genre, logline, synopsis, notes, targetWordCount }, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "update_novel_brief")]
|
|
||||||
[Description("Revise a novel's top-level fields. Only the fields you supply change; "
|
|
||||||
+ "pass an empty string to clear one.")]
|
|
||||||
public static Task<CallToolResult> UpdateNovel(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("New title.")] string? title = null,
|
|
||||||
[Description("Author name.")] string? author = null,
|
|
||||||
[Description("Genre or category.")] string? genre = null,
|
|
||||||
[Description("One-sentence pitch.")] string? logline = null,
|
|
||||||
[Description("Paragraph-length summary of the whole book.")] string? synopsis = null,
|
|
||||||
[Description("Free-form notes on theme, tone, comparable titles.")] string? notes = null,
|
|
||||||
[Description("Target manuscript length in words.")] int? targetWordCount = null) =>
|
|
||||||
api.PatchAsync($"/api/novels/{novelId}",
|
|
||||||
new { title, author, genre, logline, synopsis, notes, targetWordCount }, ct);
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using ModelContextProtocol.Protocol;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace Novelly.Mcp.Tools;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public static class QuestionTools
|
|
||||||
{
|
|
||||||
[McpServerTool(Name = "list_open_questions")]
|
|
||||||
[Description("The decisions the writer has not made yet, newest first. Read this before "
|
|
||||||
+ "proposing changes — an open question marks somewhere the writer is still "
|
|
||||||
+ "thinking, not a gap to fill in for them.")]
|
|
||||||
public static Task<CallToolResult> ListOpenQuestions(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("Narrow to questions about one chapter outline.")] Guid? chapterId = null,
|
|
||||||
[Description("Narrow to questions about one character.")] Guid? characterId = null,
|
|
||||||
[Description("Include questions already settled. Defaults to false.")] bool includeResolved = false)
|
|
||||||
{
|
|
||||||
var query = new List<string> { $"includeResolved={includeResolved.ToString().ToLowerInvariant()}" };
|
|
||||||
|
|
||||||
if (chapterId is { } chapter)
|
|
||||||
{
|
|
||||||
query.Add($"chapterId={chapter}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (characterId is { } character)
|
|
||||||
{
|
|
||||||
query.Add($"characterId={character}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.GetAsync($"/api/novels/{novelId}/questions?{string.Join('&', query)}", ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
[McpServerTool(Name = "raise_open_question")]
|
|
||||||
[Description("Record a question the writer has not settled, attached to the chapter outline "
|
|
||||||
+ "and/or the character it is about. Prefer raising a question over guessing.")]
|
|
||||||
public static Task<CallToolResult> RaiseOpenQuestion(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
[Description("The question, in one line.")] string question,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("The thinking around it — options considered, and what each costs.")] string? detail = null,
|
|
||||||
[Description("Id of the chapter outline this is about, if any.")] Guid? chapterId = null,
|
|
||||||
[Description("Id of the character this is about, if any.")] Guid? characterId = null) =>
|
|
||||||
api.PostAsync($"/api/novels/{novelId}/questions",
|
|
||||||
new { question, detail, chapterId, characterId }, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "update_open_question")]
|
|
||||||
[Description("Revise a question or change what it is attached to. Only the fields you supply change.")]
|
|
||||||
public static Task<CallToolResult> UpdateOpenQuestion(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The question's id.")] Guid questionId,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("New wording for the question.")] string? question = null,
|
|
||||||
[Description("New detail. Pass an empty string to clear it.")] string? detail = null,
|
|
||||||
[Description("Attach to this chapter outline.")] Guid? chapterId = null,
|
|
||||||
[Description("Attach to this character.")] Guid? characterId = null,
|
|
||||||
[Description("Detach from its chapter.")] bool clearChapter = false,
|
|
||||||
[Description("Detach from its character.")] bool clearCharacter = false) =>
|
|
||||||
api.PatchAsync($"/api/questions/{questionId}",
|
|
||||||
new { question, detail, chapterId, characterId, clearChapter, clearCharacter }, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "resolve_open_question")]
|
|
||||||
[Description("Settle a question with what the writer decided. Set appendToNotes to also write "
|
|
||||||
+ "the resolution into the notes of the chapter and character it hangs off.")]
|
|
||||||
public static Task<CallToolResult> ResolveOpenQuestion(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The question's id.")] Guid questionId,
|
|
||||||
[Description("What was decided.")] string resolution,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("Also append the resolution to the associated notes.")] bool appendToNotes = false) =>
|
|
||||||
api.PostAsync($"/api/questions/{questionId}/resolve", new { resolution, appendToNotes }, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "reopen_question")]
|
|
||||||
[Description("Put a resolved question back on the list. Anything already appended to notes stays.")]
|
|
||||||
public static Task<CallToolResult> ReopenQuestion(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The question's id.")] Guid questionId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.PostAsync($"/api/questions/{questionId}/reopen", new { }, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "delete_open_question")]
|
|
||||||
[Description("Delete a question outright. Resolving is usually better — it keeps the decision.")]
|
|
||||||
public static Task<CallToolResult> DeleteOpenQuestion(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The question's id.")] Guid questionId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.DeleteAsync($"/api/questions/{questionId}", ct);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using ModelContextProtocol.Protocol;
|
|
||||||
using ModelContextProtocol.Server;
|
|
||||||
|
|
||||||
namespace Novelly.Mcp.Tools;
|
|
||||||
|
|
||||||
[McpServerToolType]
|
|
||||||
public static class TagTools
|
|
||||||
{
|
|
||||||
[McpServerTool(Name = "list_tags")]
|
|
||||||
[Description("List a novel's tags with how many characters, chapters and beats carry each. "
|
|
||||||
+ "Read this before inventing a new tag so you reuse the writer's vocabulary.")]
|
|
||||||
public static Task<CallToolResult> ListTags(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.GetAsync($"/api/novels/{novelId}/tags", ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "get_tag_references")]
|
|
||||||
[Description("Cross-reference a tag: every character, chapter and beat carrying it. Use this "
|
|
||||||
+ "to trace a motif, a thread, or a piece of setup through the book.")]
|
|
||||||
public static Task<CallToolResult> GetTagReferences(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The tag's id.")] Guid tagId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.GetAsync($"/api/tags/{tagId}/references", ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "create_tag")]
|
|
||||||
[Description("Create a tag explicitly. Applying an unknown tag by name to a character, "
|
|
||||||
+ "chapter or beat also creates it, so this is only needed to set a colour up front.")]
|
|
||||||
public static Task<CallToolResult> CreateTag(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The novel's id.")] Guid novelId,
|
|
||||||
[Description("The tag's name. Unique within the novel, matched case-insensitively.")] string name,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("Optional hex colour for the UI, e.g. \"#9a4a2f\".")] string? color = null) =>
|
|
||||||
api.PostAsync($"/api/novels/{novelId}/tags", new { name, color }, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "update_tag")]
|
|
||||||
[Description("Rename or recolour a tag. Renaming updates it everywhere it is applied.")]
|
|
||||||
public static Task<CallToolResult> UpdateTag(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The tag's id.")] Guid tagId,
|
|
||||||
CancellationToken ct,
|
|
||||||
[Description("New name.")] string? name = null,
|
|
||||||
[Description("Hex colour, e.g. \"#9a4a2f\".")] string? color = null) =>
|
|
||||||
api.PatchAsync($"/api/tags/{tagId}", new { name, color }, ct);
|
|
||||||
|
|
||||||
[McpServerTool(Name = "delete_tag")]
|
|
||||||
[Description("Delete a tag. Whatever carried it is left alone — only the label goes.")]
|
|
||||||
public static Task<CallToolResult> DeleteTag(
|
|
||||||
NovelApiClient api,
|
|
||||||
[Description("The tag's id.")] Guid tagId,
|
|
||||||
CancellationToken ct) =>
|
|
||||||
api.DeleteAsync($"/api/tags/{tagId}", ct);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user