ProtocolTests.cs 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. using ModelContextProtocol;
  2. using ModelContextProtocol.Client;
  3. using RackPeek.Mcp.Tools;
  4. namespace Tests.Mcp;
  5. /// <summary>
  6. /// The protocol surface itself: the handshake identifies the server, every tool is
  7. /// advertised with enough description for an agent to use it unseen, and a bad tool
  8. /// name is an error rather than a hang or a crash.
  9. /// </summary>
  10. public class ProtocolTests {
  11. /// <summary>Every tool the server ships. A rename here is a breaking change for clients.</summary>
  12. public static readonly string[] ExpectedTools = [
  13. "list_resources", "get_resource", "search_resources", "get_summary", "get_tree",
  14. "list_connections", "get_subnets", "get_schema",
  15. "upsert_resources", "delete_resource", "rename_resource", "clone_resource",
  16. "edit_tags", "edit_labels", "add_connection", "remove_connection",
  17. "export_ansible_inventory", "export_ssh_config", "export_hosts_file", "export_topology_mermaid",
  18. "git_status", "git_commit",
  19. "discover_docker", "discover_proxmox"
  20. ];
  21. [Fact]
  22. public async Task The_handshake_identifies_the_server_by_name_and_version() {
  23. using var api = new McpFixture();
  24. await using McpClient client = await api.ConnectAsync();
  25. Assert.Equal("rackpeek", client.ServerInfo.Name);
  26. Assert.False(string.IsNullOrWhiteSpace(client.ServerInfo.Version));
  27. }
  28. [Fact]
  29. public async Task Every_expected_tool_is_advertised_and_nothing_else() {
  30. using var api = new McpFixture();
  31. await using McpClient client = await api.ConnectAsync();
  32. IList<McpClientTool> tools = await client.ListToolsAsync();
  33. Assert.Equal(
  34. ExpectedTools.OrderBy(t => t, StringComparer.Ordinal),
  35. tools.Select(t => t.Name).OrderBy(t => t, StringComparer.Ordinal));
  36. }
  37. [Fact]
  38. public async Task Every_tool_carries_a_description_an_agent_can_act_on() {
  39. using var api = new McpFixture();
  40. await using McpClient client = await api.ConnectAsync();
  41. IList<McpClientTool> tools = await client.ListToolsAsync();
  42. foreach (McpClientTool tool in tools)
  43. Assert.False(
  44. string.IsNullOrWhiteSpace(tool.Description),
  45. $"Tool '{tool.Name}' has no description.");
  46. }
  47. [Fact]
  48. public async Task Calling_a_tool_that_does_not_exist_is_a_clean_error() {
  49. using var api = new McpFixture();
  50. await using McpClient client = await api.ConnectAsync();
  51. await Assert.ThrowsAnyAsync<McpException>(async () =>
  52. await client.CallToolAsync("does_not_exist"));
  53. }
  54. [Fact]
  55. public async Task Two_clients_can_talk_to_the_same_server_at_once() {
  56. // Stateless streamable HTTP: no session to collide on.
  57. using var api = new McpFixture();
  58. await using McpClient first = await api.ConnectAsync();
  59. await using McpClient second = await api.ConnectAsync();
  60. await Task.WhenAll(
  61. first.CallOkAsync<ResourceList>("list_resources"),
  62. second.CallOkAsync<ResourceList>("list_resources"));
  63. }
  64. }