using ModelContextProtocol;
using ModelContextProtocol.Client;
using RackPeek.Mcp.Tools;
namespace Tests.Mcp;
///
/// The protocol surface itself: the handshake identifies the server, every tool is
/// advertised with enough description for an agent to use it unseen, and a bad tool
/// name is an error rather than a hang or a crash.
///
public class ProtocolTests {
/// Every tool the server ships. A rename here is a breaking change for clients.
public static readonly string[] ExpectedTools = [
"list_resources", "get_resource", "search_resources", "get_summary", "get_tree",
"list_connections", "get_subnets", "get_schema",
"upsert_resources", "delete_resource", "rename_resource", "clone_resource",
"edit_tags", "edit_labels", "add_connection", "remove_connection",
"export_ansible_inventory", "export_ssh_config", "export_hosts_file", "export_topology_mermaid",
"git_status", "git_commit",
"discover_docker", "discover_proxmox"
];
[Fact]
public async Task The_handshake_identifies_the_server_by_name_and_version() {
using var api = new McpFixture();
await using McpClient client = await api.ConnectAsync();
Assert.Equal("rackpeek", client.ServerInfo.Name);
Assert.False(string.IsNullOrWhiteSpace(client.ServerInfo.Version));
}
[Fact]
public async Task Every_expected_tool_is_advertised_and_nothing_else() {
using var api = new McpFixture();
await using McpClient client = await api.ConnectAsync();
IList tools = await client.ListToolsAsync();
Assert.Equal(
ExpectedTools.OrderBy(t => t, StringComparer.Ordinal),
tools.Select(t => t.Name).OrderBy(t => t, StringComparer.Ordinal));
}
[Fact]
public async Task Every_tool_carries_a_description_an_agent_can_act_on() {
using var api = new McpFixture();
await using McpClient client = await api.ConnectAsync();
IList tools = await client.ListToolsAsync();
foreach (McpClientTool tool in tools)
Assert.False(
string.IsNullOrWhiteSpace(tool.Description),
$"Tool '{tool.Name}' has no description.");
}
[Fact]
public async Task Calling_a_tool_that_does_not_exist_is_a_clean_error() {
using var api = new McpFixture();
await using McpClient client = await api.ConnectAsync();
await Assert.ThrowsAnyAsync(async () =>
await client.CallToolAsync("does_not_exist"));
}
[Fact]
public async Task Two_clients_can_talk_to_the_same_server_at_once() {
// Stateless streamable HTTP: no session to collide on.
using var api = new McpFixture();
await using McpClient first = await api.ConnectAsync();
await using McpClient second = await api.ConnectAsync();
await Task.WhenAll(
first.CallOkAsync("list_resources"),
second.CallOkAsync("list_resources"));
}
}