diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-09 23:19:24 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-09 23:19:24 +0900 |
| commit | 788df5e1d86c1bd91571d2b9cd1559599b65c9d6 (patch) | |
| tree | 3d1d9bec9e8318bfc6749f9f8f1727436e3d6af1 /tests/Agent.Mcp.TestHost | |
| parent | b07b41cd435991cd254957af79055080c85b346e (diff) | |
feat(mcp): migrate server to MCP 2026-07-28
Replace the MCP 1.4.1 stateful HTTP design with the official C# SDK 2.0.0 stateless transport. The SDK now owns protocol negotiation, server/discover, standard MCP headers, GET and DELETE handling, error serialization, caching hints, and legacy initialize compatibility; production code no longer maintains a custom wire-protocol workaround for MCP Inspector.
Separate transport state from application conversation state. HTTP does not issue Mcp-Session-Id, while the existing session_id remains an Agent conversation identifier. Preserve agent_run and agent_status for clients without Tasks support and add the official ModelContextProtocol.Extensions.Tasks 2.0.0 path for capable clients.
Add persistent MCP Task storage with atomic JSON replacement, user-only state location, configurable TTL and poll interval, restart recovery, cancellation correlation, and public Agent-to-MCP state mapping. Keep conformance-only tools and protocol exception fixtures in a test host so they cannot enter the Release Bundle.
Run administrator, BFE, WFP transaction, and filter add/delete preflight before binding. Fail fast with exit code 2 and actionable localized diagnostics when sandbox enforcement is unavailable. Emit ready only after the listener starts and keep stdio stdout protocol-pure.
Add shared rotating application and HTTP request logs. Record real socket endpoints, connection and trace identifiers, method, path, result, timing, media metadata, User-Agent, and MCP metadata. Do not trust X-Forwarded-For and do not log authorization, cookies, query data, request or response bodies, prompts, images, or Base64 payloads.
Add typed logging and Tasks configuration, Japanese and English messages, protocol and operations documentation, design/specification deltas, traceability, and exact conformance acquisition/offline runner scripts for @modelcontextprotocol/conformance 0.2.0-alpha.11. Node remains test-only and is not a runtime dependency.
Conformance evidence: server-stateless passed 28 of 28 checks with zero unexpected failures; DNS rebinding and multiple POST stream scenarios passed. The alpha Tasks lifecycle/cancel/error/required-task checks passed, while its wire-schema validator misclassifies CreateTaskResult and its MRTR composition remains incompatible with SDK 2.0.0. The aggregate conformance Gate is therefore recorded as PARTIAL, not PASS. Test-host fixture gaps for tools-list descriptions, JSON Schema diagnostics, caching fixtures, and a header-validation/required-Task capability conflict are also recorded without expected-failure masking.
Verification: Release build passed with 0 warnings and 0 errors; Planner tests 10/10 and Core tests 47/47 passed with skip 0. The last Windows suite run passed 19/25; five WFP cases require an elevated token and one symlink case requires unavailable host privilege, so the elevated security Gate remains open. A win-x64 self-contained 1.0.0 Bundle and ZIP were regenerated under build/Release/publish; runtime check was intentionally skipped in this non-elevated session. Evidence remains under ignored artifacts/evidence/mcp-2026-07-28.
Harden release regeneration when another shell holds the previous Bundle directory as its current directory: validate the target and remove only its children instead of deleting the directory itself. build/ and artifacts/ remain excluded from Git.
Diffstat (limited to 'tests/Agent.Mcp.TestHost')
| -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 |
3 files changed, 172 insertions, 0 deletions
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); + })); + } +} |
