using System.Text.Json; using Microsoft.Extensions.Logging.Abstractions; using ModelContextProtocol.Protocol; using Novelly.Api.Agent; using Novelly.Api.Mcp; using Novelly.Api.Novels; namespace Novelly.Api.Tests; [TestFixture] public class NovelMcpToolsTests : ServiceTestFixture { private NovelAgentToolset _toolset = null!; protected override void OnSetUp() => _toolset = new NovelAgentToolset(Novels, Characters, Arcs, Chapters, ChapterLabels, Beats, Tags, Locations, Questions, NullLogger.Instance); private static CallToolRequestParams Params(string name, object? arguments = null) => new() { Name = name, Arguments = arguments is null ? null : JsonSerializer.Deserialize>(JsonSerializer.Serialize(arguments)) }; [Test] public void Every_agent_tool_is_advertised_over_mcp() { var tools = NovelMcpTools.Describe(_toolset.Definitions); Assert.Multiple(() => { Assert.That(tools, Has.Count.EqualTo(45)); Assert.That(tools.Select(t => t.Name), Is.Unique); Assert.That(tools, Has.All.Matches(t => !string.IsNullOrWhiteSpace(t.Description))); }); } [Test] public void Tools_that_work_on_a_whole_novel_advertise_a_required_novel_id() { var tool = NovelMcpTools.Describe(_toolset.Definitions).Single(t => t.Name == "list_tags"); Assert.Multiple(() => { Assert.That(tool.InputSchema.GetProperty("properties").TryGetProperty("novelId", out _), Is.True); Assert.That( tool.InputSchema.GetProperty("required").EnumerateArray().Select(e => e.GetString()), Has.Member("novelId")); }); } [Test] public void Tools_addressed_by_child_id_do_not_advertise_a_novel_id() { var tool = NovelMcpTools.Describe(_toolset.Definitions).Single(t => t.Name == "delete_tag"); Assert.That(tool.InputSchema.GetProperty("properties").TryGetProperty("novelId", out _), Is.False); } [Test] public void Listing_novels_over_mcp_needs_no_novel_id() { var tool = NovelMcpTools.Describe(_toolset.Definitions).Single(t => t.Name == "list_novels"); Assert.That(tool.InputSchema.GetProperty("properties").TryGetProperty("novelId", out _), Is.False); } [Test] public void Advertised_schemas_stay_valid_json_schema_objects() { Assert.Multiple(() => { foreach (var tool in NovelMcpTools.Describe(_toolset.Definitions)) { Assert.That(tool.InputSchema.GetProperty("type").GetString(), Is.EqualTo("object"), tool.Name); Assert.That(tool.InputSchema.TryGetProperty("properties", out _), Is.True, tool.Name); } }); } [Test] public async Task Calling_a_novel_scoped_tool_without_a_novel_id_comes_back_as_an_error_result() { var result = await NovelMcpTools.CallAsync(_toolset, _toolset.Definitions, Params("list_tags"), CancellationToken.None); Assert.That(result.IsError, Is.True); } [Test] public async Task Calling_a_tool_over_mcp_runs_it_against_the_novel_in_the_arguments() { var novelId = (await Novels.CreateAsync(new CreateNovelRequest("The Salt Road"))).Id; var result = await NovelMcpTools.CallAsync( _toolset, _toolset.Definitions, Params("get_novel_brief", new { novelId }), CancellationToken.None); var text = ((TextContentBlock)result.Content![0]).Text; Assert.Multiple(() => { Assert.That(result.IsError, Is.False); Assert.That(text, Does.Contain("The Salt Road")); }); } [Test] public async Task A_tool_that_finds_nothing_comes_back_as_an_error_result() { var result = await NovelMcpTools.CallAsync( _toolset, _toolset.Definitions, Params("get_character", new { characterId = Guid.NewGuid() }), CancellationToken.None); var text = ((TextContentBlock)result.Content![0]).Text; Assert.Multiple(() => { Assert.That(result.IsError, Is.True); Assert.That(text, Does.Contain("was not found")); }); } }