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 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 { $"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 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 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 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 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 DeleteOpenQuestion( NovelApiClient api, [Description("The question's id.")] Guid questionId, CancellationToken ct) => api.DeleteAsync($"/api/questions/{questionId}", ct); }