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);
}
}
}
}
|