using System.Net; using System.Net.Http.Json; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; namespace Nalu.Tests; public class McpServerTests : IClassFixture> { private readonly HttpClient _client; public McpServerTests(WebApplicationFactory factory) { _client = factory.CreateClient(); _client.DefaultRequestHeaders.Add("Authorization", "Bearer nalu-test-key-001"); } private async Task PostMcp(object body) { var json = JsonSerializer.Serialize(body); var response = await _client.PostAsync("/mcp", new StringContent(json, Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); var text = await response.Content.ReadAsStringAsync(); return JsonNode.Parse(text); } // ── Auth ────────────────────────────────────────────────────────────────── [Fact] public async Task Mcp_NoApiKey_Returns401() { using var client = new HttpClient { BaseAddress = _client.BaseAddress }; var response = await client.PostAsync("/mcp", new StringContent("{}", Encoding.UTF8, "application/json")); response.StatusCode.Should().Be(HttpStatusCode.Unauthorized); } // ── initialize ──────────────────────────────────────────────────────────── [Fact] public async Task Mcp_Initialize_ReturnsServerInfo() { var result = await PostMcp(new { jsonrpc = "2.0", method = "initialize", @params = new { protocolVersion = "2024-11-05", capabilities = new { } }, id = 1 }); result.Should().NotBeNull(); result!["result"]!["serverInfo"]!["name"]!.GetValue().Should().Be("nalu-ai"); result["result"]!["protocolVersion"]!.GetValue().Should().Be("2024-11-05"); result["result"]!["capabilities"]!["tools"].Should().NotBeNull(); } // ── tools/list ──────────────────────────────────────────────────────────── [Fact] public async Task Mcp_ToolsList_ContainsAllValidators() { var result = await PostMcp(new { jsonrpc = "2.0", method = "tools/list", @params = new { }, id = 2 }); var tools = result!["result"]!["tools"]!.AsArray(); tools.Should().NotBeEmpty(); var toolNames = tools.Select(t => t!["name"]!.GetValue()).ToList(); toolNames.Should().Contain("nalu_extract_name"); toolNames.Should().Contain("nalu_extract_cpf"); toolNames.Should().Contain("nalu_extract_cep"); toolNames.Should().Contain("nalu_extract_phone"); toolNames.Should().Contain("nalu_extract_email"); toolNames.Should().Contain("nalu_extract_yes_no"); toolNames.Should().Contain("nalu_extract_postal_code"); // Universal — new toolNames.Should().Contain("nalu_extract_birthdate"); toolNames.Should().Contain("nalu_extract_handoff"); toolNames.Should().Contain("nalu_extract_cancel_intent"); // Brasil — new toolNames.Should().Contain("nalu_extract_cnpj"); toolNames.Should().Contain("nalu_extract_plate_br"); toolNames.Should().Contain("nalu_extract_company_name"); } [Fact] public async Task Mcp_ToolsList_EachToolHasInputSchema() { var result = await PostMcp(new { jsonrpc = "2.0", method = "tools/list", @params = new { }, id = 3 }); var tools = result!["result"]!["tools"]!.AsArray(); foreach (var tool in tools) { var schema = tool!["inputSchema"]; schema.Should().NotBeNull($"tool {tool["name"]} should have inputSchema"); schema!["properties"]!["agent_input"].Should().NotBeNull(); schema["properties"]!["user_input"].Should().NotBeNull(); var required = schema["required"]!.AsArray().Select(r => r!.GetValue()).ToList(); required.Should().Contain("agent_input"); required.Should().Contain("user_input"); } } // ── tools/call ──────────────────────────────────────────────────────────── [Fact] public async Task Mcp_ToolsCall_ExtractName_WithGreeting_ObtainedFalse() { var result = await PostMcp(new { jsonrpc = "2.0", method = "tools/call", @params = new { name = "nalu_extract_name", arguments = new { agent_input = "Qual seu nome completo?", user_input = "Bom dia!" } }, id = 4 }); result!["result"]!["isError"]!.GetValue().Should().BeFalse(); var content = result["result"]!["content"]!.AsArray(); content.Should().HaveCount(1); content[0]!["type"]!.GetValue().Should().Be("text"); var text = content[0]!["text"]!.GetValue(); var extracted = JsonSerializer.Deserialize>(text)!; extracted["obtained"].GetBoolean().Should().BeFalse(); } [Fact] public async Task Mcp_ToolsCall_UnknownTool_ReturnsError() { var result = await PostMcp(new { jsonrpc = "2.0", method = "tools/call", @params = new { name = "nalu_extract_unknown", arguments = new { } }, id = 5 }); result!["error"].Should().NotBeNull(); result["error"]!["code"]!.GetValue().Should().Be(-32602); } // ── New .md file → auto-appears in tools/list ───────────────────────────── [Fact] public async Task Mcp_ToolsList_AllLoadedValidatorsHaveMcpTool() { // Any validator without mcp_tool should not appear in tools/list var result = await PostMcp(new { jsonrpc = "2.0", method = "tools/list", @params = new { }, id = 6 }); var tools = result!["result"]!["tools"]!.AsArray(); foreach (var tool in tools) { tool!["name"]!.GetValue().Should().NotBeNullOrWhiteSpace(); tool!["description"]!.GetValue().Should().NotBeNullOrWhiteSpace(); } } // ── notifications ───────────────────────────────────────────────────────── [Fact] public async Task Mcp_Initialized_Notification_Returns202() { var json = JsonSerializer.Serialize(new { jsonrpc = "2.0", method = "initialized" }); var response = await _client.PostAsync("/mcp", new StringContent(json, Encoding.UTF8, "application/json")); response.StatusCode.Should().Be(HttpStatusCode.Accepted); } }