summaryrefslogtreecommitdiff
path: root/tests/Agent.Core.Tests/McpHttpTests.cs
blob: 7e3f1cff72918310c39d4b19416fc9f4f234f854 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
using System.Net;
using System.Net.Sockets;
using Agent.Core.Localization;
using Agent.Core.Ollama;
using Agent.Core.Runtime;
using Agent.Mcp;
using ModelContextProtocol;
using ModelContextProtocol.Client;

namespace Agent.Core.Tests;

public sealed class McpHttpTests
{
    [Fact(Timeout = 30_000)]
    public async Task Loopback_streamable_http_works_and_get_sse_legacy_sse_are_rejected()
    {
        int port = FreePort();
        using var stop = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken);
        LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en");
        var settings = new OllamaSettings(new Uri("http://127.0.0.1:1"), "unused", false, "0", new Dictionary<string, System.Text.Json.JsonElement>());
        Task server = McpHost.RunHttpAsync(locale, settings, new AgentRuntimeSettings("", "en", [], "sandbox"), new VisionLimits(), "127.0.0.1", port, stop.Token);
        using var http = new HttpClient();
        Uri endpoint = new($"http://127.0.0.1:{port}/mcp");
        await WaitUntilListening(http, endpoint, TestContext.Current.CancellationToken);

        var transport = new HttpClientTransport(new HttpClientTransportOptions
        {
            Name = "http-test",
            Endpoint = endpoint,
            TransportMode = HttpTransportMode.StreamableHttp
        });
        await using (McpClient client = await McpClient.CreateAsync(transport, cancellationToken: TestContext.Current.CancellationToken))
        {
            IList<McpClientTool> tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
            Assert.Equal(["agent_run", "agent_status"], tools.Select(static tool => tool.Name).Order(StringComparer.Ordinal));
        }

        using HttpResponseMessage get = await http.GetAsync(endpoint, TestContext.Current.CancellationToken);
        Assert.Equal(HttpStatusCode.MethodNotAllowed, get.StatusCode);
        using var hostileOrigin = new HttpRequestMessage(HttpMethod.Post, endpoint);
        hostileOrigin.Headers.Add("Origin", "https://example.invalid");
        using HttpResponseMessage forbidden = await http.SendAsync(hostileOrigin, TestContext.Current.CancellationToken);
        Assert.Equal(HttpStatusCode.Forbidden, forbidden.StatusCode);
        using var loopbackOrigin = new HttpRequestMessage(HttpMethod.Get, endpoint);
        loopbackOrigin.Headers.Add("Origin", $"http://127.0.0.1:{port}");
        using HttpResponseMessage loopbackAllowed = await http.SendAsync(loopbackOrigin, TestContext.Current.CancellationToken);
        Assert.Equal(HttpStatusCode.MethodNotAllowed, loopbackAllowed.StatusCode);
        Assert.Equal(HttpStatusCode.NotFound, (await http.GetAsync(new Uri($"http://127.0.0.1:{port}/sse"), TestContext.Current.CancellationToken)).StatusCode);
        Assert.Equal(HttpStatusCode.NotFound, (await http.PostAsync(new Uri($"http://127.0.0.1:{port}/message"), null, TestContext.Current.CancellationToken)).StatusCode);

        await stop.CancelAsync();
        await server;
    }

    [Fact]
    public async Task Non_loopback_bind_is_rejected_before_server_start()
    {
        LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en");
        var settings = new OllamaSettings(new Uri("http://127.0.0.1:1"), "unused", false, "0", new Dictionary<string, System.Text.Json.JsonElement>());
        await Assert.ThrowsAsync<ArgumentException>(() => McpHost.RunHttpAsync(locale, settings,
            new AgentRuntimeSettings("", "en", [], "sandbox"), new VisionLimits(), "0.0.0.0", 8080, TestContext.Current.CancellationToken));
    }

    private static int FreePort()
    {
        TcpListener listener = new(IPAddress.Loopback, 0);
        listener.Start();
        int port = ((IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();
        return port;
    }

    private static async Task WaitUntilListening(HttpClient client, Uri endpoint, CancellationToken cancellationToken)
    {
        for (int attempt = 0; attempt < 100; attempt++)
        {
            try
            {
                using HttpResponseMessage response = await client.GetAsync(endpoint, cancellationToken);
                return;
            }
            catch (HttpRequestException) when (attempt < 99)
            {
                await Task.Delay(25, cancellationToken);
            }
        }
    }
}