summaryrefslogtreecommitdiff
path: root/tests/Agent.Core.Tests/McpHttpTests.cs
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
commitb07b41cd435991cd254957af79055080c85b346e (patch)
tree27720d3ce14e4d0b84670a55ac26a2f877ce3a20 /tests/Agent.Core.Tests/McpHttpTests.cs
parent2fefd29e86c3b926d0cf4a362251d3f119688612 (diff)
feat: AI Agent Sandbox MCP Server v1を実装
Specifications、Design、ImplementationPlanの優先順位を整理し、単一Task実行モデル、typed option優先、設定可能なresource上限、local Ollamaを必須とするRelease Gateを文書化。 .NET 10/C# 14の5 production project、3 test project、E2E Clientを構築。typed YAML、en/ja locale、fixed/user prompt、stdioおよびloopback限定Streamable HTTP、agent_run/agent_status、Session、Ollama text/tool/Vision、TaskGraphとtoken accountingを実装。 Windows sandboxとしてfixed AppContainer、canonical/reparse検査、能力別ACL、path+SHA-256 executable approval、child-process restriction、Job Object、task journal、atomic file transaction、crash recoveryを実装。WFPのIPv4/IPv6 default denyとexact allow、dynamic session、temporary loopback exemption、管理者/BFE診断を追加。 Security/Recovery test、MCP/Ollama integration test、stdio/HTTP Operational E2E、Release Bundle生成・checksum・manifest・fresh extraction検査を追加。Requirement TraceabilityとPhase 0〜11の進捗、初回セットアップ可能なREADMEを整備。 Debug/Release/publish/objをrepository rootのbuild/へ集約し、旧project-local bin/objへの依存を除去。artifacts/とbuild/をGit管理外に設定し、clean checkoutでもsolution buildからE2E Clientまで生成可能にした。
Diffstat (limited to 'tests/Agent.Core.Tests/McpHttpTests.cs')
-rw-r--r--tests/Agent.Core.Tests/McpHttpTests.cs88
1 files changed, 88 insertions, 0 deletions
diff --git a/tests/Agent.Core.Tests/McpHttpTests.cs b/tests/Agent.Core.Tests/McpHttpTests.cs
new file mode 100644
index 0000000..7e3f1cf
--- /dev/null
+++ b/tests/Agent.Core.Tests/McpHttpTests.cs
@@ -0,0 +1,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);
+ }
+ }
+ }
+}