Unify MCP and agent tool surfaces onto one registry in NovelAgentToolset

Fixes NovelAgentService continuing a conversation under the wrong
novel's route, since FindConversationAsync matched by id alone. Then
extends NovelAgentToolset to all 45 tools the stdio MCP server offered
(tag/location CRUD, character relationships, arc-stage beat pinning,
question editing, cross-novel novel listing/creation), tagging each
with whether it needs an explicit novel scope so a later MCP adapter
can inject it. Renames the toolset's 33 existing schemas from
snake_case to camelCase to match .NET/REST convention, since nothing
external consumes them.

Lays the groundwork to serve this same registry over MCP at /mcp and
retire the separate stdio Novelly.Mcp project (docs/plans/api/mcp_http_merge_plan.md).
This commit is contained in:
James Wampler
2026-08-21 10:52:50 -07:00
parent bb2a499569
commit 897fb442a1
6 changed files with 784 additions and 117 deletions
@@ -100,7 +100,7 @@ public class NovelAgentServiceTests : ServiceTestFixture
var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var model = new ScriptedModelClient([
[ToolUse("t1", "update_character", new { character_id = Guid.NewGuid().ToString(), name = "Ines" })],
[ToolUse("t1", "update_character", new { characterId = Guid.NewGuid().ToString(), name = "Ines" })],
[new AgentTextBlock("That character does not exist yet — shall I create her?")]
]);
@@ -185,6 +185,42 @@ public class NovelAgentServiceTests : ServiceTestFixture
});
}
[Test]
public async Task Continuing_a_conversation_under_a_different_novel_is_rejected()
{
var novelAId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var novelBId = (await Novels.CreateAsync(new CreateNovelRequest("A Different Book"))).Id;
var model = new ScriptedModelClient([
[new AgentTextBlock("First answer.")]
]);
var agent = BuildAgent(model);
var first = await agent.SendMessageAsync(novelAId, new SendAgentMessageRequest("Question one."));
var second = await agent.SendMessageAsync(
novelBId, new SendAgentMessageRequest("Question two.", first.ConversationId));
Assert.That(second, Is.Null);
}
[Test]
public async Task Continuing_a_conversation_under_its_own_novel_still_works()
{
var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id;
var model = new ScriptedModelClient([
[new AgentTextBlock("First answer.")],
[new AgentTextBlock("Second answer.")]
]);
var agent = BuildAgent(model);
var first = await agent.SendMessageAsync(novelId, new SendAgentMessageRequest("Question one."));
var second = await agent.SendMessageAsync(
novelId, new SendAgentMessageRequest("Question two.", first.ConversationId));
Assert.That(second, Is.Not.Null);
Assert.That(second.ConversationId, Is.EqualTo(first.ConversationId));
}
[Test]
public void Every_tool_declares_an_object_schema_and_a_description()
{