diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/Agent.Core.Tests/Agent.Core.Tests.csproj | 1 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/McpHttpTests.cs | 28 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/McpStdioTests.cs | 35 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/ObservabilityTests.cs | 46 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/PersistentMcpTaskStoreTests.cs | 36 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/SharedFakeOllamaServer.cs | 55 | ||||
| -rw-r--r-- | tests/Agent.Core.Tests/TaskRegistryTests.cs | 13 | ||||
| -rw-r--r-- | tests/Agent.Mcp.TestHost/Agent.Mcp.TestHost.csproj | 8 | ||||
| -rw-r--r-- | tests/Agent.Mcp.TestHost/ConformanceTools.cs | 92 | ||||
| -rw-r--r-- | tests/Agent.Mcp.TestHost/Program.cs | 72 |
10 files changed, 358 insertions, 28 deletions
diff --git a/tests/Agent.Core.Tests/Agent.Core.Tests.csproj b/tests/Agent.Core.Tests/Agent.Core.Tests.csproj index 1326733..337bde9 100644 --- a/tests/Agent.Core.Tests/Agent.Core.Tests.csproj +++ b/tests/Agent.Core.Tests/Agent.Core.Tests.csproj @@ -8,6 +8,7 @@ <ItemGroup> <ProjectReference Include="..\..\src\Agent.Core\Agent.Core.csproj" /> <ProjectReference Include="..\..\src\Agent.App\Agent.App.csproj" /> + <ProjectReference Include="..\Agent.Mcp.TestHost\Agent.Mcp.TestHost.csproj" /> </ItemGroup> <ItemGroup> <PackageReference Include="Microsoft.NET.Test.Sdk" /> diff --git a/tests/Agent.Core.Tests/McpHttpTests.cs b/tests/Agent.Core.Tests/McpHttpTests.cs index 7e3f1cf..9edb133 100644 --- a/tests/Agent.Core.Tests/McpHttpTests.cs +++ b/tests/Agent.Core.Tests/McpHttpTests.cs @@ -6,6 +6,9 @@ using Agent.Core.Runtime; using Agent.Mcp; using ModelContextProtocol; using ModelContextProtocol.Client; +using ModelContextProtocol.Extensions.Tasks; +using ModelContextProtocol.Protocol; +using System.Text.Json; namespace Agent.Core.Tests; @@ -15,9 +18,10 @@ public sealed class McpHttpTests public async Task Loopback_streamable_http_works_and_get_sse_legacy_sse_are_rejected() { int port = FreePort(); + await using var ollamaServer = new SharedFakeOllamaServer(); 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>()); + var settings = new OllamaSettings(ollamaServer.Endpoint, "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"); @@ -33,10 +37,32 @@ public sealed class McpHttpTests { 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)); + + ResultOrCreatedTask<CallToolResult> taskCall = await client.CallToolAsTaskAsync(new CallToolRequestParams + { + Name = "agent_run", + Arguments = new Dictionary<string, JsonElement> { ["request"] = JsonSerializer.SerializeToElement("task extension test") } + }, TestContext.Current.CancellationToken); + Assert.True(taskCall.IsTask); + CreateTaskResult created = taskCall.TaskCreated!; + Assert.True(Guid.TryParse(created.TaskId, out _)); + GetTaskResult? task = null; + for (int attempt = 0; attempt < 400; attempt++) + { + task = await client.GetTaskAsync(created.TaskId, TestContext.Current.CancellationToken); + if (task.Status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled) break; + await Task.Delay(25, TestContext.Current.CancellationToken); + } + Assert.NotNull(task); + Assert.Equal(McpTaskStatus.Completed, task.Status); + Assert.Contains(created.TaskId, JsonSerializer.Serialize(task), StringComparison.OrdinalIgnoreCase); } using HttpResponseMessage get = await http.GetAsync(endpoint, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.MethodNotAllowed, get.StatusCode); + Assert.False(get.Headers.Contains("Mcp-Session-Id")); + using HttpResponseMessage delete = await http.DeleteAsync(endpoint, TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.MethodNotAllowed, delete.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); diff --git a/tests/Agent.Core.Tests/McpStdioTests.cs b/tests/Agent.Core.Tests/McpStdioTests.cs index 66f4851..fcef80e 100644 --- a/tests/Agent.Core.Tests/McpStdioTests.cs +++ b/tests/Agent.Core.Tests/McpStdioTests.cs @@ -1,4 +1,4 @@ -using Agent.App.Configuration; +using Agent.Mcp.TestHost; using ModelContextProtocol; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; @@ -15,17 +15,18 @@ public sealed class McpStdioTests { using var directory = new TestDirectory(); await using var ollama = new FakeOllamaServer(); - string configPath = CreateBundleConfiguration(directory, ollama.Endpoint); - string application = typeof(ConfigurationLoader).Assembly.Location; + string localeDirectory = CreateLocale(directory); + string application = typeof(Marker).Assembly.Location; var transport = new StdioClientTransport(new StdioClientTransportOptions { Name = "agent-mcp-test", Command = "dotnet", - Arguments = [application, "serve", "--config", configPath, "--lang", "en"], + Arguments = [application, localeDirectory, System.IO.Path.Combine(directory.Path, "mcp-tasks")], WorkingDirectory = directory.Path }); - await using McpClient client = await McpClient.CreateAsync(transport, cancellationToken: TestContext.Current.CancellationToken); + await using McpClient client = await McpClient.CreateAsync(transport, + new McpClientOptions { ProtocolVersion = "2025-11-25" }, 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)); foreach (McpClientTool tool in tools) @@ -71,30 +72,10 @@ public sealed class McpStdioTests return Guid.Parse(line[(key.Length + 1)..].Trim()); } - private static string CreateBundleConfiguration(TestDirectory directory, Uri endpoint) + private static string CreateLocale(TestDirectory directory) { - directory.File("prompts/user-system.md", string.Empty); directory.File("config/locales/en.json", """{"app.starting":"Starting","check.success":"OK","check.failure":"Failed {message}","check.wfp_admin_required":"Administrator required","config.invalid":"Invalid {message}","locale.invalid":"Locale {message}","mcp.task_queued":"Task {task} queued in {session}","mcp.task_not_found":"Task {task} not found","sandbox.access_denied":"Denied {path}","ollama.vision_unsupported":"No vision"}"""); - Directory.CreateDirectory(System.IO.Path.Combine(directory.Path, "skills")); - return directory.File("agent.yaml", $$""" - schema_version: 1 - application: - language: en - mcp: - transport: stdio - agent: - user_system_prompt: prompts/user-system.md - ollama: - endpoint: {{endpoint}} - model: model - localization: - directory: config/locales - fallback: en - skills: - directory: skills - logging: - directory: logs - """); + return System.IO.Path.Combine(directory.Path, "config", "locales"); } private sealed class FakeOllamaServer : IAsyncDisposable diff --git a/tests/Agent.Core.Tests/ObservabilityTests.cs b/tests/Agent.Core.Tests/ObservabilityTests.cs new file mode 100644 index 0000000..cd86bc2 --- /dev/null +++ b/tests/Agent.Core.Tests/ObservabilityTests.cs @@ -0,0 +1,46 @@ +using Agent.App.Logging; +using Agent.Core.Localization; +using Agent.Mcp; + +namespace Agent.Core.Tests; + +public sealed class ObservabilityTests +{ + [Fact] + public void Ready_is_always_visible_even_when_log_threshold_is_fatal() + { + using var directory = new TestDirectory(); + LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en"); + using (var application = new RollingFileLog(directory.Path, 1024 * 1024, 2, "fatal")) + using (var observer = new McpHostLogObserver(application, locale, directory.Path, false, 1024 * 1024, 2)) + observer.Ready(new McpReadyInfo("http", "http://127.0.0.1:8080/mcp", 42, "2026-07-28", true, true)); + + Assert.Contains("Agent is ready", File.ReadAllText(Path.Combine(directory.Path, "application.log")), StringComparison.Ordinal); + } + + [Fact] + public void Ready_and_http_request_are_logged_without_sensitive_payloads() + { + using var directory = new TestDirectory(); + LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en"); + using (var application = new RollingFileLog(directory.Path, 1024 * 1024, 2)) + { + using var observer = new McpHostLogObserver(application, locale, directory.Path, true, 1024 * 1024, 2); + observer.Ready(new McpReadyInfo("http", "http://127.0.0.1:8080/mcp", 42, "2026-07-28", true, true)); + observer.HttpRequestStarted(new McpHttpRequestStartInfo("connection", "trace", "127.0.0.1", 50123, "POST", "/mcp")); + observer.HttpRequest(new McpHttpRequestInfo(DateTimeOffset.UtcNow, "connection", "trace", "127.0.0.1", 50123, + "127.0.0.1", 8080, "POST", "/mcp", 200, 12.5, "application/json", 100, "test-client", + "2026-07-28", "tools/list", "agent_run")); + } + + string applicationText = File.ReadAllText(Path.Combine(directory.Path, "application.log")); + Assert.Contains("Agent is ready", applicationText, StringComparison.Ordinal); + Assert.Contains("remote=127.0.0.1:50123", applicationText, StringComparison.Ordinal); + string httpText = File.ReadAllText(Path.Combine(directory.Path, "http.jsonl")); + Assert.Contains("\"method\":\"POST\"", httpText, StringComparison.Ordinal); + Assert.Contains("\"remote_ip\":\"127.0.0.1\"", httpText, StringComparison.Ordinal); + Assert.DoesNotContain("Authorization", httpText, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("Cookie", httpText, StringComparison.OrdinalIgnoreCase); + Assert.DoesNotContain("prompt", httpText, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/tests/Agent.Core.Tests/PersistentMcpTaskStoreTests.cs b/tests/Agent.Core.Tests/PersistentMcpTaskStoreTests.cs new file mode 100644 index 0000000..2b43459 --- /dev/null +++ b/tests/Agent.Core.Tests/PersistentMcpTaskStoreTests.cs @@ -0,0 +1,36 @@ +using System.Text.Json; +using Agent.Mcp; +using ModelContextProtocol.Extensions.Tasks; + +namespace Agent.Core.Tests; + +public sealed class PersistentMcpTaskStoreTests +{ + [Fact] + public async Task Persists_terminal_task_and_marks_incomplete_task_failed_after_restart() + { + using var directory = new TestDirectory(); + var settings = new McpTaskStoreSettings(directory.Path, TimeSpan.FromHours(24), 1000); + var first = new PersistentMcpTaskStore(settings); + McpTaskInfo completed = await first.CreateTaskAsync(TestContext.Current.CancellationToken); + await first.SetCompletedAsync(completed.TaskId, JsonSerializer.SerializeToElement(new { ok = true }), TestContext.Current.CancellationToken); + McpTaskInfo working = await first.CreateTaskAsync(TestContext.Current.CancellationToken); + + var recovered = new PersistentMcpTaskStore(settings); + Assert.Equal(McpTaskStatus.Completed, (await recovered.GetTaskAsync(completed.TaskId, TestContext.Current.CancellationToken))!.Status); + McpTaskInfo failed = (await recovered.GetTaskAsync(working.TaskId, TestContext.Current.CancellationToken))!; + Assert.Equal(McpTaskStatus.Failed, failed.Status); + Assert.Equal("server_restarted", failed.StatusMessage); + } + + [Fact] + public async Task Expired_task_is_removed() + { + using var directory = new TestDirectory(); + var store = new PersistentMcpTaskStore(new McpTaskStoreSettings(directory.Path, TimeSpan.FromMilliseconds(1), 1000)); + McpTaskInfo task = await store.CreateTaskAsync(TestContext.Current.CancellationToken); + await Task.Delay(20, TestContext.Current.CancellationToken); + Assert.Null(await store.GetTaskAsync(task.TaskId, TestContext.Current.CancellationToken)); + Assert.Empty(Directory.EnumerateFiles(directory.Path, "*.json")); + } +} diff --git a/tests/Agent.Core.Tests/SharedFakeOllamaServer.cs b/tests/Agent.Core.Tests/SharedFakeOllamaServer.cs new file mode 100644 index 0000000..3264c77 --- /dev/null +++ b/tests/Agent.Core.Tests/SharedFakeOllamaServer.cs @@ -0,0 +1,55 @@ +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Agent.Core.Tests; + +internal sealed class SharedFakeOllamaServer : IAsyncDisposable +{ + private readonly TcpListener _listener = new(IPAddress.Loopback, 0); + private readonly CancellationTokenSource _stop = new(); + private readonly Task _loop; + + public SharedFakeOllamaServer() + { + _listener.Start(); + Endpoint = new Uri($"http://127.0.0.1:{((IPEndPoint)_listener.LocalEndpoint).Port}/"); + _loop = RunAsync(); + } + + public Uri Endpoint { get; } + + private async Task RunAsync() + { + try + { + while (!_stop.IsCancellationRequested) + { + using TcpClient client = await _listener.AcceptTcpClientAsync(_stop.Token).ConfigureAwait(false); + NetworkStream stream = client.GetStream(); + using var reader = new StreamReader(stream, Encoding.ASCII, false, 1024, true); + int contentLength = 0; + while (await reader.ReadLineAsync(_stop.Token).ConfigureAwait(false) is { } line && line.Length > 0) + if (line.StartsWith("Content-Length:", StringComparison.OrdinalIgnoreCase)) contentLength = int.Parse(line[15..].Trim(), System.Globalization.CultureInfo.InvariantCulture); + if (contentLength > 0) + { + char[] body = new char[contentLength]; + await reader.ReadBlockAsync(body.AsMemory(), _stop.Token).ConfigureAwait(false); + } + byte[] payload = Encoding.UTF8.GetBytes("""{"message":{"role":"assistant","content":"done"},"prompt_eval_count":3,"eval_count":2}"""); + byte[] header = Encoding.ASCII.GetBytes($"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {payload.Length}\r\nConnection: close\r\n\r\n"); + await stream.WriteAsync(header, _stop.Token).ConfigureAwait(false); + await stream.WriteAsync(payload, _stop.Token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) when (_stop.IsCancellationRequested) { } + } + + public async ValueTask DisposeAsync() + { + await _stop.CancelAsync().ConfigureAwait(false); + _listener.Stop(); + await _loop.ConfigureAwait(false); + _stop.Dispose(); + } +} diff --git a/tests/Agent.Core.Tests/TaskRegistryTests.cs b/tests/Agent.Core.Tests/TaskRegistryTests.cs index 33e16ab..28a2844 100644 --- a/tests/Agent.Core.Tests/TaskRegistryTests.cs +++ b/tests/Agent.Core.Tests/TaskRegistryTests.cs @@ -5,6 +5,19 @@ namespace Agent.Core.Tests; public sealed class TaskRegistryTests { [Fact] + public async Task Explicit_task_id_can_be_cancelled_and_observed_as_terminal() + { + var registry = new TaskRegistry(); + Guid taskId = Guid.NewGuid(); + TaskSnapshot queued = registry.Enqueue("work", null, null, requestedTaskId: taskId); + Assert.Equal(taskId, queued.TaskId); + Assert.True(registry.Cancel(taskId)); + TaskSnapshot terminal = await registry.WaitForTerminalAsync(taskId, TestContext.Current.CancellationToken); + Assert.Equal(AgentTaskState.Cancelled, terminal.State); + Assert.True(registry.GetCancellationToken(taskId).IsCancellationRequested); + } + + [Fact] public void NewTaskGetsUniqueTaskAndSessionIds() { var registry = new TaskRegistry(); diff --git a/tests/Agent.Mcp.TestHost/Agent.Mcp.TestHost.csproj b/tests/Agent.Mcp.TestHost/Agent.Mcp.TestHost.csproj new file mode 100644 index 0000000..ff1c533 --- /dev/null +++ b/tests/Agent.Mcp.TestHost/Agent.Mcp.TestHost.csproj @@ -0,0 +1,8 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\src\Agent.Mcp\Agent.Mcp.csproj" /> + </ItemGroup> +</Project> diff --git a/tests/Agent.Mcp.TestHost/ConformanceTools.cs b/tests/Agent.Mcp.TestHost/ConformanceTools.cs new file mode 100644 index 0000000..3d8879d --- /dev/null +++ b/tests/Agent.Mcp.TestHost/ConformanceTools.cs @@ -0,0 +1,92 @@ +using System.ComponentModel; +using ModelContextProtocol; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +#pragma warning disable MCP9005 // The frozen alpha conformance fixture deliberately probes the legacy sampling capability. + +namespace Agent.Mcp.TestHost; + +[McpServerToolType] +public sealed class ConformanceTools +{ + [McpServerTool(Name = "test_missing_capability")] + [Description("Conformance-only sampling capability probe.")] + public async Task<string> TestMissingCapability(McpServer server, CancellationToken cancellationToken) + { + await server.SampleAsync(new CreateMessageRequestParams + { + MaxTokens = 1, + Messages = [new SamplingMessage { Role = Role.User, Content = [new TextContentBlock { Text = "probe" }] }] + }, cancellationToken).ConfigureAwait(false); + return "sampled"; + } + + [McpServerTool(Name = "test_streaming_elicitation")] + [Description("Conformance-only response-stream framing probe.")] + public string TestStreamingElicitation() => "stream complete"; + + [McpServerTool(Name = "test_logging_tool")] + [Description("Conformance-only request log-level probe.")] + public string TestLoggingTool() => "no request-scoped log emitted"; + + [McpServerTool(Name = "confirm_delete")] + [Description("Conformance-only elicitation task fixture.")] + public string ConfirmDelete(RequestContext<CallToolRequestParams> context, string filename) + { + if (context.Params?.InputResponses?.Count > 0) return $"delete {filename}: accepted"; + throw new InputRequiredException(new Dictionary<string, InputRequest> + { + ["confirm-delete"] = Elicitation($"Confirm deletion of {filename}", "confirm", new ElicitRequestParams.BooleanSchema()) + }, "confirm-delete"); + } + + [McpServerTool(Name = "multi_input")] + [Description("Conformance-only multiple elicitation task fixture.")] + public string MultiInput(RequestContext<CallToolRequestParams> context) + { + if (context.Params?.InputResponses?.Count >= 2) return "both inputs received"; + throw new InputRequiredException(new Dictionary<string, InputRequest> + { + ["first"] = Elicitation("First confirmation", "confirm", new ElicitRequestParams.BooleanSchema()), + ["second"] = Elicitation("Second confirmation", "confirm", new ElicitRequestParams.BooleanSchema()) + }, "multi-input"); + } + + [McpServerTool(Name = "test_tool_with_task")] + [Description("Conformance-only MRTR and Tasks composition fixture.")] + public string TestToolWithTask(RequestContext<CallToolRequestParams> context) + { + if (context.Params?.InputResponses is { Count: > 0 } responses) + { + string response = System.Text.Json.JsonSerializer.Serialize(responses); + return response.Contains("Alice", StringComparison.Ordinal) ? "Hello, Alice" : "Hello, user"; + } + throw new InputRequiredException(new Dictionary<string, InputRequest> + { + ["user-name"] = Elicitation("Provide a user name", "name", new ElicitRequestParams.StringSchema()) + }, "user-name"); + } + + private static InputRequest Elicitation(string message, string propertyName, ElicitRequestParams.PrimitiveSchemaDefinition schema) => + InputRequest.ForElicitation(new ElicitRequestParams + { + Mode = "form", + Message = message, + RequestedSchema = new ElicitRequestParams.RequestSchema + { + Properties = new Dictionary<string, ElicitRequestParams.PrimitiveSchemaDefinition> { [propertyName] = schema }, + Required = [propertyName] + } + }); + + public sealed class Confirmation + { + public bool Confirm { get; set; } + } + + public sealed class UserName + { + public string? Name { get; set; } + } +} diff --git a/tests/Agent.Mcp.TestHost/Program.cs b/tests/Agent.Mcp.TestHost/Program.cs new file mode 100644 index 0000000..3447beb --- /dev/null +++ b/tests/Agent.Mcp.TestHost/Program.cs @@ -0,0 +1,72 @@ +using Agent.Core.Localization; +using Agent.Core.Ollama; +using Agent.Core.Runtime; +using Agent.Mcp; +using ModelContextProtocol.Server; +using ModelContextProtocol.Protocol; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol; + +namespace Agent.Mcp.TestHost; + +public static class Marker; + +internal static class Program +{ + public static async Task Main(string[] args) + { + if (args.Length is not (2 or 4)) throw new ArgumentException("Expected <locale> <task-state> or http <port> <locale> <task-state>."); + bool http = args.Length == 4; + int offset = http ? 2 : 0; + LocaleCatalog locale = LocaleCatalog.Load(args[offset], "en"); + var ollama = new OllamaSettings(new Uri("http://127.0.0.1:1"), "unused", false, "0", new Dictionary<string, System.Text.Json.JsonElement>()); + var store = new McpTaskStoreSettings(args[offset + 1], TimeSpan.FromHours(24), 1000); + var runtime = new AgentRuntimeSettings("", "en", [], "sandbox"); + McpServerTool[] fixtures = + [ + McpServerTool.Create((string name) => $"Hello, {name}!", new McpServerToolCreateOptions { Name = "greet" }), + McpServerTool.Create(async (int seconds, CancellationToken cancellationToken) => + { + await Task.Delay(TimeSpan.FromSeconds(seconds), cancellationToken).ConfigureAwait(false); + return $"Computed after {seconds} seconds"; + }, new McpServerToolCreateOptions { Name = "slow_compute" }), + McpServerTool.Create(async () => + { + await Task.Delay(1000).ConfigureAwait(false); + return new CallToolResult { IsError = true, Content = [new TextContentBlock { Text = "intentional tool error" }] }; + }, new McpServerToolCreateOptions { Name = "failing_job" }), + McpServerTool.Create((Func<string>)(() => throw new InvalidOperationException("intentional protocol error")), + new McpServerToolCreateOptions { Name = "protocol_error_job" }), + .. typeof(ConformanceTools).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Where(static method => method.GetCustomAttribute<McpServerToolAttribute>() is not null) + .Select(method => McpServerTool.Create(method, new ConformanceTools())) + ]; + var taskTools = new HashSet<string>(["agent_run", "slow_compute", "confirm_delete", "multi_input", "protocol_error_job"], StringComparer.Ordinal); + var requiredTaskTools = new HashSet<string>(["failing_job", "test_tool_with_task"], StringComparer.Ordinal); + if (http) + await McpHost.RunHttpAsync(locale, ollama, runtime, new VisionLimits(), "127.0.0.1", int.Parse(args[1], System.Globalization.CultureInfo.InvariantCulture), CancellationToken.None, + taskStoreSettings: store, additionalTools: fixtures, taskCapableTools: taskTools, requiredTaskTools: requiredTaskTools, + configureBuilder: ConfigureConformanceFilters).ConfigureAwait(false); + else + await McpHost.RunStdioAsync(locale, ollama, runtime, new VisionLimits(), CancellationToken.None, taskStoreSettings: store).ConfigureAwait(false); + } + + private static void ConfigureConformanceFilters(IMcpServerBuilder builder) + { + builder.WithRequestFilters(filters => filters.AddCallToolFilter(next => async (context, cancellationToken) => + { + if (context.Params?.Name == "test_missing_capability") + { +#pragma warning disable MCP9005 // Frozen alpha conformance probes the legacy sampling capability shape. + throw new MissingRequiredClientCapabilityException( + new ClientCapabilities { Sampling = new SamplingCapability() }, + "The conformance diagnostic requires the sampling capability."); +#pragma warning restore MCP9005 + } + if (context.Params?.Name == "protocol_error_job") + throw new McpProtocolException("intentional protocol error", McpErrorCode.InternalError); + return await next(context, cancellationToken).ConfigureAwait(false); + })); + } +} |
