From 51a3ac6b79658c60aa67019bed846d7d5fac6a51 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Tue, 18 Nov 2025 10:09:32 -0800 Subject: [PATCH 01/14] Add ComplianceServer sample --- ModelContextProtocol.slnx | 1 + .../ComplianceServer/ComplianceServer.csproj | 13 ++ .../ComplianceServer/ComplianceServer.http | 75 ++++++ samples/ComplianceServer/Program.cs | 133 +++++++++++ .../Prompts/CompliancePrompts.cs | 57 +++++ .../Properties/launchSettings.json | 23 ++ .../Resources/ComplianceResources.cs | 71 ++++++ .../ComplianceServer/Tools/ComplianceTools.cs | 216 ++++++++++++++++++ .../appsettings.Development.json | 8 + samples/ComplianceServer/appsettings.json | 9 + 10 files changed, 606 insertions(+) create mode 100644 samples/ComplianceServer/ComplianceServer.csproj create mode 100644 samples/ComplianceServer/ComplianceServer.http create mode 100644 samples/ComplianceServer/Program.cs create mode 100644 samples/ComplianceServer/Prompts/CompliancePrompts.cs create mode 100644 samples/ComplianceServer/Properties/launchSettings.json create mode 100644 samples/ComplianceServer/Resources/ComplianceResources.cs create mode 100644 samples/ComplianceServer/Tools/ComplianceTools.cs create mode 100644 samples/ComplianceServer/appsettings.Development.json create mode 100644 samples/ComplianceServer/appsettings.json diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index 1f6dce1ed..b2b74e84f 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -42,6 +42,7 @@ + diff --git a/samples/ComplianceServer/ComplianceServer.csproj b/samples/ComplianceServer/ComplianceServer.csproj new file mode 100644 index 000000000..755bb5d81 --- /dev/null +++ b/samples/ComplianceServer/ComplianceServer.csproj @@ -0,0 +1,13 @@ + + + + net9.0 + enable + enable + + + + + + + diff --git a/samples/ComplianceServer/ComplianceServer.http b/samples/ComplianceServer/ComplianceServer.http new file mode 100644 index 000000000..27173de10 --- /dev/null +++ b/samples/ComplianceServer/ComplianceServer.http @@ -0,0 +1,75 @@ +@HostAddress = http://localhost:3001 + +POST {{HostAddress}}/ +Accept: application/json, text/event-stream +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 1, + "method": "ping" +} + +### + +POST {{HostAddress}}/ +Accept: application/json, text/event-stream +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 2, + "method": "initialize", + "params": { + "clientInfo": { + "name": "RestClient", + "version": "0.1.0" + }, + "capabilities": {}, + "protocolVersion": "2025-06-18" + } +} + +### + +@SessionId = XxIXkrK210aKVnZxD8Iu_g + +POST {{HostAddress}}/ +Accept: application/json, text/event-stream +Content-Type: application/json +MCP-Protocol-Version: 2025-06-18 +Mcp-Session-Id: {{SessionId}} + +{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/list" +} + +### + +POST {{HostAddress}}/ +Accept: application/json, text/event-stream +Content-Type: application/json +MCP-Protocol-Version: 2025-06-18 +Mcp-Session-Id: {{SessionId}} + +{ + "jsonrpc": "2.0", + "id": 4, + "method": "resources/list" +} + +### + +POST {{HostAddress}}/ +Accept: application/json, text/event-stream +Content-Type: application/json +MCP-Protocol-Version: 2025-06-18 +Mcp-Session-Id: {{SessionId}} + +{ + "jsonrpc": "2.0", + "id": 5, + "method": "prompts/list" +} diff --git a/samples/ComplianceServer/Program.cs b/samples/ComplianceServer/Program.cs new file mode 100644 index 000000000..35b1c5773 --- /dev/null +++ b/samples/ComplianceServer/Program.cs @@ -0,0 +1,133 @@ +using ComplianceServer; +using ComplianceServer.Prompts; +using ComplianceServer.Resources; +using ComplianceServer.Tools; +using Microsoft.Extensions.AI; +using ModelContextProtocol; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.Collections.Concurrent; + +var builder = WebApplication.CreateBuilder(args); + +// Dictionary of session IDs to a set of resource URIs they are subscribed to +// The value is a ConcurrentDictionary used as a thread-safe HashSet +// because .NET does not have a built-in concurrent HashSet +ConcurrentDictionary> subscriptions = new(); + +builder.Services + .AddMcpServer() + .WithHttpTransport() + .WithTools() + .WithPrompts() + .WithResources() + .WithSubscribeToResourcesHandler(async (ctx, ct) => + { + if (ctx.Server.SessionId == null) + { + throw new McpException("Cannot add subscription for server with null SessionId"); + } + if (ctx.Params?.Uri is { } uri) + { + subscriptions[ctx.Server.SessionId].TryAdd(uri, 0); + + await ctx.Server.SampleAsync([ + new ChatMessage(ChatRole.System, "You are a helpful test server"), + new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"), + ], + options: new ChatOptions + { + MaxOutputTokens = 100, + Temperature = 0.7f, + }, + cancellationToken: ct); + } + + return new EmptyResult(); + }) + .WithUnsubscribeFromResourcesHandler(async (ctx, ct) => + { + if (ctx.Server.SessionId == null) + { + throw new McpException("Cannot remove subscription for server with null SessionId"); + } + if (ctx.Params?.Uri is { } uri) + { + subscriptions[ctx.Server.SessionId].TryRemove(uri, out _); + } + return new EmptyResult(); + }) + .WithCompleteHandler(async (ctx, ct) => + { + var exampleCompletions = new Dictionary> + { + { "style", ["casual", "formal", "technical", "friendly"] }, + { "temperature", ["0", "0.5", "0.7", "1.0"] }, + { "resourceId", ["1", "2", "3", "4", "5"] } + }; + + if (ctx.Params is not { } @params) + { + throw new NotSupportedException($"Params are required."); + } + + var @ref = @params.Ref; + var argument = @params.Argument; + + if (@ref is ResourceTemplateReference rtr) + { + var resourceId = rtr.Uri?.Split("/").Last(); + + if (resourceId is null) + { + return new CompleteResult(); + } + + var values = exampleCompletions["resourceId"].Where(id => id.StartsWith(argument.Value)); + + return new CompleteResult + { + Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() } + }; + } + + if (@ref is PromptReference pr) + { + if (!exampleCompletions.TryGetValue(argument.Name, out IEnumerable? value)) + { + throw new NotSupportedException($"Unknown argument name: {argument.Name}"); + } + + var values = value.Where(value => value.StartsWith(argument.Value)); + return new CompleteResult + { + Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() } + }; + } + + throw new NotSupportedException($"Unknown reference type: {@ref.Type}"); + }) + .WithSetLoggingLevelHandler(async (ctx, ct) => + { + if (ctx.Params?.Level is null) + { + throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams); + } + + // The SDK updates the LoggingLevel field of the IMcpServer + + await ctx.Server.SendNotificationAsync("notifications/message", new + { + Level = "debug", + Logger = "test-server", + Data = $"Logging level set to {ctx.Params.Level}", + }, cancellationToken: ct); + + return new EmptyResult(); + }); + +var app = builder.Build(); + +app.MapMcp(); + +app.Run(); diff --git a/samples/ComplianceServer/Prompts/CompliancePrompts.cs b/samples/ComplianceServer/Prompts/CompliancePrompts.cs new file mode 100644 index 000000000..11ea71f7c --- /dev/null +++ b/samples/ComplianceServer/Prompts/CompliancePrompts.cs @@ -0,0 +1,57 @@ +using ModelContextProtocol.Server; +using ModelContextProtocol.Protocol; +using Microsoft.Extensions.AI; +using System.ComponentModel; + +namespace ComplianceServer.Prompts; + +public class CompliancePrompts +{ + // Sample base64 encoded 1x1 red PNG pixel for testing + const string TEST_IMAGE_BASE64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; + + [McpServerPrompt(Name = "test_simple_prompt"), Description("Simple prompt without arguments")] + public static string SimplePrompt() => "This is a simple prompt without arguments"; + + [McpServerPrompt(Name = "test_prompt_with_arguments"), Description("Parameterized prompt")] + public static IEnumerable ParameterizedPrompt( + [Description("First test argument")] string arg1, + [Description("Second test argument")] string arg2) + { + return [ + new ChatMessage(ChatRole.User,$"Prompt with arguments: arg1={arg1}, arg2={arg2}"), + ]; + } + + [McpServerPrompt(Name = "test_prompt_with_embedded_resource"), Description("Prompt with embedded resource")] + public static IEnumerable PromptWithEmbeddedResource( + [Description("URI of the resource to embed")] string resourceUri) + { + return [ + new PromptMessage + { + Role = Role.User, + Content = new EmbeddedResourceBlock + { + Resource = new TextResourceContents + { + Uri = resourceUri, + Text = "Embedded resource content for testing.", + MimeType = "text/plain" + } + } + }, + new PromptMessage { Role = Role.User, Content = new TextContentBlock { Text = "Please process the embedded resource above." } }, + ]; + } + + [McpServerPrompt(Name = "test_prompt_with_image"), Description("Prompt with image")] + public static IEnumerable PromptWithImage() + { + return [ + new ChatMessage(ChatRole.User, [new DataContent(TEST_IMAGE_BASE64)]), + new ChatMessage(ChatRole.User, "Please analyze the image above."), + ]; + } +} diff --git a/samples/ComplianceServer/Properties/launchSettings.json b/samples/ComplianceServer/Properties/launchSettings.json new file mode 100644 index 000000000..cf9924175 --- /dev/null +++ b/samples/ComplianceServer/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:3001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7292;http://localhost:3001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ComplianceServer/Resources/ComplianceResources.cs b/samples/ComplianceServer/Resources/ComplianceResources.cs new file mode 100644 index 000000000..7cc17f3c2 --- /dev/null +++ b/samples/ComplianceServer/Resources/ComplianceResources.cs @@ -0,0 +1,71 @@ +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Text.Json; + +namespace ComplianceServer.Resources; + +[McpServerResourceType] +public class ComplianceResources +{ + // Sample base64 encoded 1x1 red PNG pixel for testing + private const string TEST_IMAGE_BASE64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; + + /// + /// Static text resource for testing + /// + [McpServerResource(UriTemplate = "test://static-text", Name = "Static Text Resource", MimeType = "text/plain")] + [Description("A static text resource for testing")] + public static string StaticText() + { + return "This is the content of the static text resource."; + } + + /// + /// Static binary resource (image) for testing + /// + [McpServerResource(UriTemplate = "test://static-binary", Name = "Static Binary Resource", MimeType = "image/png")] + [Description("A static binary resource (image) for testing")] + public static BlobResourceContents StaticBinary() + { + return new BlobResourceContents + { + Uri = "test://static-binary", + MimeType = "image/png", + Blob = TEST_IMAGE_BASE64 + }; + } + + /// + /// Resource template with parameter substitution + /// + [McpServerResource(UriTemplate = "test://template/{id}/data", Name = "Resource Template", MimeType = "application/json")] + [Description("A resource template with parameter substitution")] + public static TextResourceContents TemplateResource(string id) + { + var data = new + { + id = id, + templateTest = true, + data = $"Data for ID: {id}" + }; + + return new TextResourceContents + { + Uri = $"test://template/{id}/data", + MimeType = "application/json", + Text = JsonSerializer.Serialize(data) + }; + } + + /// + /// Subscribable resource that can send updates + /// + [McpServerResource(UriTemplate = "test://watched-resource", Name = "Watched Resource", MimeType = "text/plain")] + [Description("A resource that auto-updates every 3 seconds")] + public static string WatchedResource() + { + return "Watched resource content"; + } +} diff --git a/samples/ComplianceServer/Tools/ComplianceTools.cs b/samples/ComplianceServer/Tools/ComplianceTools.cs new file mode 100644 index 000000000..2e049d55e --- /dev/null +++ b/samples/ComplianceServer/Tools/ComplianceTools.cs @@ -0,0 +1,216 @@ +using ModelContextProtocol; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Text.Json; + +namespace ComplianceServer.Tools; + +[McpServerToolType] +public class ComplianceTools +{ + // Sample base64 encoded 1x1 red PNG pixel for testing + private const string TEST_IMAGE_BASE64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; + + // Sample base64 encoded minimal WAV file for testing + private const string TEST_AUDIO_BASE64 = + "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA="; + + /// + /// Simple text tool - returns simple text content for testing + /// + [McpServerTool(Name = "test_simple_text")] + [Description("Tests simple text content response")] + public static string SimpleText() + { + return "This is a simple text response for testing."; + } + + /// + /// Image content tool - returns base64-encoded image content + /// + [McpServerTool(Name = "test_image_content")] + [Description("Tests image content response")] + public static ImageContentBlock ImageContent() + { + return new ImageContentBlock + { + Data = TEST_IMAGE_BASE64, + MimeType = "image/png" + }; + } + + /// + /// Audio content tool - returns base64-encoded audio content + /// + [McpServerTool(Name = "test_audio_content")] + [Description("Tests audio content response")] + public static AudioContentBlock AudioContent() + { + return new AudioContentBlock + { + Data = TEST_AUDIO_BASE64, + MimeType = "audio/wav" + }; + } + + /// + /// Embedded resource tool - returns embedded resource content + /// + [McpServerTool(Name = "test_embedded_resource")] + [Description("Tests embedded resource content response")] + public static EmbeddedResourceBlock EmbeddedResource() + { + return new EmbeddedResourceBlock + { + Resource = new TextResourceContents + { + Uri = "test://embedded-resource", + MimeType = "text/plain", + Text = "This is an embedded resource content." + } + }; + } + + /// + /// Multiple content types tool - returns mixed content types (text, image, resource) + /// + [McpServerTool(Name = "test_multiple_content_types")] + [Description("Tests response with multiple content types (text, image, resource)")] + public static ContentBlock[] MultipleContentTypes() + { + return + [ + new TextContentBlock { Text = "Multiple content types test:" }, + new ImageContentBlock { Data = TEST_IMAGE_BASE64, MimeType = "image/png" }, + new EmbeddedResourceBlock + { + Resource = new TextResourceContents + { + Uri = "test://mixed-content-resource", + MimeType = "application/json", + Text = JsonSerializer.Serialize(new { test = "data", value = 123 }) + } + } + ]; + } + + /// + /// Tool with logging - emits log messages during execution + /// + [McpServerTool(Name = "test_tool_with_logging")] + [Description("Tests tool that emits log messages during execution")] + public static async Task ToolWithLogging( + RequestContext context, + CancellationToken cancellationToken) + { + var server = context.Server; + + // Use ILogger for logging (will be forwarded to client if supported) + ILoggerProvider loggerProvider = server.AsClientLoggerProvider(); + ILogger logger = loggerProvider.CreateLogger("ComplianceTools"); + + logger.LogInformation("Tool execution started"); + await Task.Delay(50, cancellationToken); + + logger.LogInformation("Tool processing data"); + await Task.Delay(50, cancellationToken); + + logger.LogInformation("Tool execution completed"); + + return "Tool with logging executed successfully"; + } + + /// + /// Tool with progress - reports progress notifications + /// + [McpServerTool(Name = "test_tool_with_progress")] + [Description("Tests tool that reports progress notifications")] + public static async Task ToolWithProgress( + McpServer server, + RequestContext context, + CancellationToken cancellationToken) + { + var progressToken = context.Params?.ProgressToken; + + if (progressToken is not null) + { + await server.NotifyProgressAsync(progressToken.Value, new ProgressNotificationValue + { + Progress = 0, + Total = 100, + }, cancellationToken); + + await Task.Delay(50, cancellationToken); + + await server.NotifyProgressAsync(progressToken.Value, new ProgressNotificationValue + { + Progress = 50, + Total = 100, + }, cancellationToken); + + await Task.Delay(50, cancellationToken); + + await server.NotifyProgressAsync(progressToken.Value, new ProgressNotificationValue + { + Progress = 100, + Total = 100, + }, cancellationToken); + } + + return progressToken?.ToString() ?? "No progress token provided"; + } + + /// + /// Error handling tool - intentionally throws an error for testing + /// + [McpServerTool(Name = "test_error_handling")] + [Description("Tests error response handling")] + public static string ErrorHandling() + { + throw new Exception("This tool intentionally returns an error for testing"); + } + + /// + /// Sampling tool - requests LLM completion from client (not implemented in this version) + /// + [McpServerTool(Name = "test_sampling")] + [Description("Tests server-initiated sampling (LLM completion request)")] + public static string Sampling([Description("The prompt to send to the LLM")] string prompt) + { + // Note: Client-requested sampling is not yet implemented in the C# SDK + return "Sampling not supported or error: Sampling capability not implemented in C# SDK yet"; + } + + /// + /// Elicitation tool - requests user input from client (not implemented in this version) + /// + [McpServerTool(Name = "test_elicitation")] + [Description("Tests elicitation (user input request from client)")] + public static string Elicitation([Description("Message to show to the user")] string message) + { + // Note: Elicitation is not yet implemented in the C# SDK + return "Elicitation not supported or error: Elicitation capability not implemented in C# SDK yet"; + } + + /// + /// SEP-1034: Elicitation with default values for all primitive types (not implemented) + /// + [McpServerTool(Name = "test_elicitation_sep1034_defaults")] + [Description("Tests elicitation with default values per SEP-1034")] + public static string ElicitationSep1034Defaults() + { + return "Elicitation not supported or error: Elicitation capability not implemented in C# SDK yet"; + } + + /// + /// SEP-1330: Elicitation with enum schema improvements (not implemented) + /// + [McpServerTool(Name = "test_elicitation_sep1330_enums")] + [Description("Tests elicitation with enum schema improvements per SEP-1330")] + public static string ElicitationSep1330Enums() + { + return "Elicitation not supported or error: Elicitation capability not implemented in C# SDK yet"; + } +} \ No newline at end of file diff --git a/samples/ComplianceServer/appsettings.Development.json b/samples/ComplianceServer/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/samples/ComplianceServer/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/ComplianceServer/appsettings.json b/samples/ComplianceServer/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/samples/ComplianceServer/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 03b678f11380365bed365569a0705ded9fb23707 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Tue, 18 Nov 2025 13:19:16 -0800 Subject: [PATCH 02/14] ComplianceServer now passing all tests (with one fix) --- samples/ComplianceServer/Program.cs | 62 ++---- .../Prompts/CompliancePrompts.cs | 18 +- .../ComplianceServer/Tools/ComplianceTools.cs | 181 ++++++++++++++++-- 3 files changed, 195 insertions(+), 66 deletions(-) diff --git a/samples/ComplianceServer/Program.cs b/samples/ComplianceServer/Program.cs index 35b1c5773..34f321dad 100644 --- a/samples/ComplianceServer/Program.cs +++ b/samples/ComplianceServer/Program.cs @@ -59,53 +59,17 @@ await ctx.Server.SampleAsync([ }) .WithCompleteHandler(async (ctx, ct) => { - var exampleCompletions = new Dictionary> + // Basic completion support - returns empty array for conformance + // Real implementations would provide contextual suggestions + return new CompleteResult { - { "style", ["casual", "formal", "technical", "friendly"] }, - { "temperature", ["0", "0.5", "0.7", "1.0"] }, - { "resourceId", ["1", "2", "3", "4", "5"] } - }; - - if (ctx.Params is not { } @params) - { - throw new NotSupportedException($"Params are required."); - } - - var @ref = @params.Ref; - var argument = @params.Argument; - - if (@ref is ResourceTemplateReference rtr) - { - var resourceId = rtr.Uri?.Split("/").Last(); - - if (resourceId is null) - { - return new CompleteResult(); - } - - var values = exampleCompletions["resourceId"].Where(id => id.StartsWith(argument.Value)); - - return new CompleteResult - { - Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() } - }; - } - - if (@ref is PromptReference pr) - { - if (!exampleCompletions.TryGetValue(argument.Name, out IEnumerable? value)) + Completion = new Completion { - throw new NotSupportedException($"Unknown argument name: {argument.Name}"); + Values = [], + HasMore = false, + Total = 0 } - - var values = value.Where(value => value.StartsWith(argument.Value)); - return new CompleteResult - { - Completion = new Completion { Values = [.. values], HasMore = false, Total = values.Count() } - }; - } - - throw new NotSupportedException($"Unknown reference type: {@ref.Type}"); + }; }) .WithSetLoggingLevelHandler(async (ctx, ct) => { @@ -114,13 +78,13 @@ await ctx.Server.SampleAsync([ throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams); } - // The SDK updates the LoggingLevel field of the IMcpServer - + // The SDK updates the LoggingLevel field of the McpServer + // Send a log notification to confirm the level was set await ctx.Server.SendNotificationAsync("notifications/message", new { - Level = "debug", - Logger = "test-server", - Data = $"Logging level set to {ctx.Params.Level}", + Level = "info", + Logger = "conformance-test-server", + Data = $"Log level set to: {ctx.Params.Level}", }, cancellationToken: ct); return new EmptyResult(); diff --git a/samples/ComplianceServer/Prompts/CompliancePrompts.cs b/samples/ComplianceServer/Prompts/CompliancePrompts.cs index 11ea71f7c..e6950fd82 100644 --- a/samples/ComplianceServer/Prompts/CompliancePrompts.cs +++ b/samples/ComplianceServer/Prompts/CompliancePrompts.cs @@ -47,11 +47,23 @@ public static IEnumerable PromptWithEmbeddedResource( } [McpServerPrompt(Name = "test_prompt_with_image"), Description("Prompt with image")] - public static IEnumerable PromptWithImage() + public static IEnumerable PromptWithImage() { return [ - new ChatMessage(ChatRole.User, [new DataContent(TEST_IMAGE_BASE64)]), - new ChatMessage(ChatRole.User, "Please analyze the image above."), + new PromptMessage + { + Role = Role.User, + Content = new ImageContentBlock + { + MimeType = "image/png", + Data = TEST_IMAGE_BASE64 + } + }, + new PromptMessage + { + Role = Role.User, + Content = new TextContentBlock { Text = "Please analyze the image above." } + }, ]; } } diff --git a/samples/ComplianceServer/Tools/ComplianceTools.cs b/samples/ComplianceServer/Tools/ComplianceTools.cs index 2e049d55e..2618247c9 100644 --- a/samples/ComplianceServer/Tools/ComplianceTools.cs +++ b/samples/ComplianceServer/Tools/ComplianceTools.cs @@ -173,44 +173,197 @@ public static string ErrorHandling() } /// - /// Sampling tool - requests LLM completion from client (not implemented in this version) + /// Sampling tool - requests LLM completion from client /// [McpServerTool(Name = "test_sampling")] [Description("Tests server-initiated sampling (LLM completion request)")] - public static string Sampling([Description("The prompt to send to the LLM")] string prompt) + public static async Task Sampling( + McpServer server, + [Description("The prompt to send to the LLM")] string prompt, + CancellationToken cancellationToken) { - // Note: Client-requested sampling is not yet implemented in the C# SDK - return "Sampling not supported or error: Sampling capability not implemented in C# SDK yet"; + try + { + var samplingParams = new CreateMessageRequestParams + { + Messages = [new SamplingMessage + { + Role = Role.User, + Content = new TextContentBlock { Text = prompt }, + }], + MaxTokens = 100, + Temperature = 0.7f + }; + + var result = await server.SampleAsync(samplingParams, cancellationToken); + return $"Sampling result: {(result.Content as TextContentBlock)?.Text ?? "No text content"}"; + } + catch (Exception ex) + { + return $"Sampling not supported or error: {ex.Message}"; + } } /// - /// Elicitation tool - requests user input from client (not implemented in this version) + /// Elicitation tool - requests user input from client /// [McpServerTool(Name = "test_elicitation")] [Description("Tests elicitation (user input request from client)")] - public static string Elicitation([Description("Message to show to the user")] string message) + public static async Task Elicitation( + McpServer server, + [Description("Message to show to the user")] string message, + CancellationToken cancellationToken) { - // Note: Elicitation is not yet implemented in the C# SDK - return "Elicitation not supported or error: Elicitation capability not implemented in C# SDK yet"; + try + { + var schema = new ElicitRequestParams.RequestSchema + { + Properties = + { + ["response"] = new ElicitRequestParams.StringSchema() + { + Description = "User's response to the message" + } + } + }; + + var result = await server.ElicitAsync(new ElicitRequestParams + { + Message = message, + RequestedSchema = schema + }, cancellationToken); + + if (result.Action == "accept" && result.Content != null) + { + return $"User responded: {result.Content["response"].GetString()}"; + } + else + { + return $"Elicitation {result.Action}"; + } + } + catch (Exception ex) + { + return $"Elicitation not supported or error: {ex.Message}"; + } } /// - /// SEP-1034: Elicitation with default values for all primitive types (not implemented) + /// SEP-1034: Elicitation with default values for all primitive types /// [McpServerTool(Name = "test_elicitation_sep1034_defaults")] [Description("Tests elicitation with default values per SEP-1034")] - public static string ElicitationSep1034Defaults() + public static async Task ElicitationSep1034Defaults( + McpServer server, + CancellationToken cancellationToken) { - return "Elicitation not supported or error: Elicitation capability not implemented in C# SDK yet"; + try + { + var schema = new ElicitRequestParams.RequestSchema + { + Properties = + { + ["name"] = new ElicitRequestParams.StringSchema() + { + Description = "Name", + Default = "John Doe" + }, + ["age"] = new ElicitRequestParams.NumberSchema() + { + Type = "integer", + Description = "Age", + Default = 30 + }, + ["score"] = new ElicitRequestParams.NumberSchema() + { + Description = "Score", + Default = 95.5 + }, + ["status"] = new ElicitRequestParams.EnumSchema() + { + Description = "Status", + Enum = ["active", "inactive", "pending"], + Default = "active" + }, + ["verified"] = new ElicitRequestParams.BooleanSchema() + { + Description = "Verified", + Default = true + } + } + }; + + var result = await server.ElicitAsync(new ElicitRequestParams + { + Message = "Test elicitation with default values for primitive types", + RequestedSchema = schema + }, cancellationToken); + + if (result.Action == "accept" && result.Content != null) + { + return $"Accepted with values: string={result.Content["stringField"].GetString()}, " + + $"number={result.Content["numberField"].GetInt32()}, " + + $"boolean={result.Content["booleanField"].GetBoolean()}"; + } + else + { + return $"Elicitation {result.Action}"; + } + } + catch (Exception ex) + { + return $"Elicitation not supported or error: {ex.Message}"; + } } /// - /// SEP-1330: Elicitation with enum schema improvements (not implemented) + /// SEP-1330: Elicitation with enum schema improvements /// [McpServerTool(Name = "test_elicitation_sep1330_enums")] [Description("Tests elicitation with enum schema improvements per SEP-1330")] - public static string ElicitationSep1330Enums() + public static async Task ElicitationSep1330Enums( + McpServer server, + CancellationToken cancellationToken) { - return "Elicitation not supported or error: Elicitation capability not implemented in C# SDK yet"; + try + { + var schema = new ElicitRequestParams.RequestSchema + { + Properties = + { + ["color"] = new ElicitRequestParams.EnumSchema() + { + Description = "Choose a color", + Enum = ["red", "green", "blue"] + }, + ["size"] = new ElicitRequestParams.EnumSchema() + { + Description = "Choose a size", + Enum = ["small", "medium", "large"], + Default = "medium" + } + } + }; + + var result = await server.ElicitAsync(new ElicitRequestParams + { + Message = "Test elicitation with enum schema", + RequestedSchema = schema + }, cancellationToken); + + if (result.Action == "accept" && result.Content != null) + { + return $"Accepted with values: color={result.Content["color"].GetString()}, " + + $"size={result.Content["size"].GetString()}"; + } + else + { + return $"Elicitation {result.Action}"; + } + } + catch (Exception ex) + { + return $"Elicitation not supported or error: {ex.Message}"; + } } } \ No newline at end of file From 73cbe0f7488846e64091d85a52735f925e0441fe Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 19 Nov 2025 07:16:01 -0800 Subject: [PATCH 03/14] Address PR review comments --- samples/ComplianceServer/Prompts/CompliancePrompts.cs | 10 ++++------ .../ComplianceServer/Resources/ComplianceResources.cs | 4 ++-- samples/ComplianceServer/Tools/ComplianceTools.cs | 10 +++++----- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/samples/ComplianceServer/Prompts/CompliancePrompts.cs b/samples/ComplianceServer/Prompts/CompliancePrompts.cs index e6950fd82..467e0923a 100644 --- a/samples/ComplianceServer/Prompts/CompliancePrompts.cs +++ b/samples/ComplianceServer/Prompts/CompliancePrompts.cs @@ -8,20 +8,18 @@ namespace ComplianceServer.Prompts; public class CompliancePrompts { // Sample base64 encoded 1x1 red PNG pixel for testing - const string TEST_IMAGE_BASE64 = + private const string TestImageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; [McpServerPrompt(Name = "test_simple_prompt"), Description("Simple prompt without arguments")] public static string SimplePrompt() => "This is a simple prompt without arguments"; [McpServerPrompt(Name = "test_prompt_with_arguments"), Description("Parameterized prompt")] - public static IEnumerable ParameterizedPrompt( + public static string ParameterizedPrompt( [Description("First test argument")] string arg1, [Description("Second test argument")] string arg2) { - return [ - new ChatMessage(ChatRole.User,$"Prompt with arguments: arg1={arg1}, arg2={arg2}"), - ]; + return $"Prompt with arguments: arg1={arg1}, arg2={arg2}"; } [McpServerPrompt(Name = "test_prompt_with_embedded_resource"), Description("Prompt with embedded resource")] @@ -56,7 +54,7 @@ public static IEnumerable PromptWithImage() Content = new ImageContentBlock { MimeType = "image/png", - Data = TEST_IMAGE_BASE64 + Data = TestImageBase64 } }, new PromptMessage diff --git a/samples/ComplianceServer/Resources/ComplianceResources.cs b/samples/ComplianceServer/Resources/ComplianceResources.cs index 7cc17f3c2..89deb5655 100644 --- a/samples/ComplianceServer/Resources/ComplianceResources.cs +++ b/samples/ComplianceServer/Resources/ComplianceResources.cs @@ -9,7 +9,7 @@ namespace ComplianceServer.Resources; public class ComplianceResources { // Sample base64 encoded 1x1 red PNG pixel for testing - private const string TEST_IMAGE_BASE64 = + private const string TestImageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; /// @@ -33,7 +33,7 @@ public static BlobResourceContents StaticBinary() { Uri = "test://static-binary", MimeType = "image/png", - Blob = TEST_IMAGE_BASE64 + Blob = TestImageBase64 }; } diff --git a/samples/ComplianceServer/Tools/ComplianceTools.cs b/samples/ComplianceServer/Tools/ComplianceTools.cs index 2618247c9..85a4a3f89 100644 --- a/samples/ComplianceServer/Tools/ComplianceTools.cs +++ b/samples/ComplianceServer/Tools/ComplianceTools.cs @@ -10,11 +10,11 @@ namespace ComplianceServer.Tools; public class ComplianceTools { // Sample base64 encoded 1x1 red PNG pixel for testing - private const string TEST_IMAGE_BASE64 = + private const string TestImageBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; // Sample base64 encoded minimal WAV file for testing - private const string TEST_AUDIO_BASE64 = + private const string TestAudioBase64 = "UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA="; /// @@ -36,7 +36,7 @@ public static ImageContentBlock ImageContent() { return new ImageContentBlock { - Data = TEST_IMAGE_BASE64, + Data = TestImageBase64, MimeType = "image/png" }; } @@ -50,7 +50,7 @@ public static AudioContentBlock AudioContent() { return new AudioContentBlock { - Data = TEST_AUDIO_BASE64, + Data = TestAudioBase64, MimeType = "audio/wav" }; } @@ -83,7 +83,7 @@ public static ContentBlock[] MultipleContentTypes() return [ new TextContentBlock { Text = "Multiple content types test:" }, - new ImageContentBlock { Data = TEST_IMAGE_BASE64, MimeType = "image/png" }, + new ImageContentBlock { Data = TestImageBase64, MimeType = "image/png" }, new EmbeddedResourceBlock { Resource = new TextResourceContents From dc4757f20d027a501a7a5657ae70e268d8caaf6c Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 19 Nov 2025 07:35:21 -0800 Subject: [PATCH 04/14] Move ConformanceServer to tests and fix naming --- ModelContextProtocol.slnx | 1 - .../ConformanceServer/ConformanceServer.csproj | 4 ++-- .../ConformanceServer/ConformanceServer.http | 0 .../Conformance/ConformanceServer}/Program.cs | 14 +++++++------- .../Prompts/ConformancePrompts.cs | 4 ++-- .../Properties/launchSettings.json | 0 .../Resources/ConformanceResources.cs | 4 ++-- .../ConformanceServer/Tools/ConformanceTools.cs | 6 +++--- .../appsettings.Development.json | 0 .../ConformanceServer}/appsettings.json | 0 10 files changed, 16 insertions(+), 17 deletions(-) rename samples/ComplianceServer/ComplianceServer.csproj => tests/Conformance/ConformanceServer/ConformanceServer.csproj (53%) rename samples/ComplianceServer/ComplianceServer.http => tests/Conformance/ConformanceServer/ConformanceServer.http (100%) rename {samples/ComplianceServer => tests/Conformance/ConformanceServer}/Program.cs (92%) rename samples/ComplianceServer/Prompts/CompliancePrompts.cs => tests/Conformance/ConformanceServer/Prompts/ConformancePrompts.cs (97%) rename {samples/ComplianceServer => tests/Conformance/ConformanceServer}/Properties/launchSettings.json (100%) rename samples/ComplianceServer/Resources/ComplianceResources.cs => tests/Conformance/ConformanceServer/Resources/ConformanceResources.cs (96%) rename samples/ComplianceServer/Tools/ComplianceTools.cs => tests/Conformance/ConformanceServer/Tools/ConformanceTools.cs (98%) rename {samples/ComplianceServer => tests/Conformance/ConformanceServer}/appsettings.Development.json (100%) rename {samples/ComplianceServer => tests/Conformance/ConformanceServer}/appsettings.json (100%) diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index b2b74e84f..1f6dce1ed 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -42,7 +42,6 @@ - diff --git a/samples/ComplianceServer/ComplianceServer.csproj b/tests/Conformance/ConformanceServer/ConformanceServer.csproj similarity index 53% rename from samples/ComplianceServer/ComplianceServer.csproj rename to tests/Conformance/ConformanceServer/ConformanceServer.csproj index 755bb5d81..2fa29eef0 100644 --- a/samples/ComplianceServer/ComplianceServer.csproj +++ b/tests/Conformance/ConformanceServer/ConformanceServer.csproj @@ -1,13 +1,13 @@ - net9.0 + net10.0 enable enable - + diff --git a/samples/ComplianceServer/ComplianceServer.http b/tests/Conformance/ConformanceServer/ConformanceServer.http similarity index 100% rename from samples/ComplianceServer/ComplianceServer.http rename to tests/Conformance/ConformanceServer/ConformanceServer.http diff --git a/samples/ComplianceServer/Program.cs b/tests/Conformance/ConformanceServer/Program.cs similarity index 92% rename from samples/ComplianceServer/Program.cs rename to tests/Conformance/ConformanceServer/Program.cs index 34f321dad..2b2d6b258 100644 --- a/samples/ComplianceServer/Program.cs +++ b/tests/Conformance/ConformanceServer/Program.cs @@ -1,7 +1,7 @@ -using ComplianceServer; -using ComplianceServer.Prompts; -using ComplianceServer.Resources; -using ComplianceServer.Tools; +using ConformanceServer; +using ConformanceServer.Prompts; +using ConformanceServer.Resources; +using ConformanceServer.Tools; using Microsoft.Extensions.AI; using ModelContextProtocol; using ModelContextProtocol.Protocol; @@ -18,9 +18,9 @@ builder.Services .AddMcpServer() .WithHttpTransport() - .WithTools() - .WithPrompts() - .WithResources() + .WithTools() + .WithPrompts() + .WithResources() .WithSubscribeToResourcesHandler(async (ctx, ct) => { if (ctx.Server.SessionId == null) diff --git a/samples/ComplianceServer/Prompts/CompliancePrompts.cs b/tests/Conformance/ConformanceServer/Prompts/ConformancePrompts.cs similarity index 97% rename from samples/ComplianceServer/Prompts/CompliancePrompts.cs rename to tests/Conformance/ConformanceServer/Prompts/ConformancePrompts.cs index 467e0923a..345e215b2 100644 --- a/samples/ComplianceServer/Prompts/CompliancePrompts.cs +++ b/tests/Conformance/ConformanceServer/Prompts/ConformancePrompts.cs @@ -3,9 +3,9 @@ using Microsoft.Extensions.AI; using System.ComponentModel; -namespace ComplianceServer.Prompts; +namespace ConformanceServer.Prompts; -public class CompliancePrompts +public class ConformancePrompts { // Sample base64 encoded 1x1 red PNG pixel for testing private const string TestImageBase64 = diff --git a/samples/ComplianceServer/Properties/launchSettings.json b/tests/Conformance/ConformanceServer/Properties/launchSettings.json similarity index 100% rename from samples/ComplianceServer/Properties/launchSettings.json rename to tests/Conformance/ConformanceServer/Properties/launchSettings.json diff --git a/samples/ComplianceServer/Resources/ComplianceResources.cs b/tests/Conformance/ConformanceServer/Resources/ConformanceResources.cs similarity index 96% rename from samples/ComplianceServer/Resources/ComplianceResources.cs rename to tests/Conformance/ConformanceServer/Resources/ConformanceResources.cs index 89deb5655..9e48a91c0 100644 --- a/samples/ComplianceServer/Resources/ComplianceResources.cs +++ b/tests/Conformance/ConformanceServer/Resources/ConformanceResources.cs @@ -3,10 +3,10 @@ using System.ComponentModel; using System.Text.Json; -namespace ComplianceServer.Resources; +namespace ConformanceServer.Resources; [McpServerResourceType] -public class ComplianceResources +public class ConformanceResources { // Sample base64 encoded 1x1 red PNG pixel for testing private const string TestImageBase64 = diff --git a/samples/ComplianceServer/Tools/ComplianceTools.cs b/tests/Conformance/ConformanceServer/Tools/ConformanceTools.cs similarity index 98% rename from samples/ComplianceServer/Tools/ComplianceTools.cs rename to tests/Conformance/ConformanceServer/Tools/ConformanceTools.cs index 85a4a3f89..a96d9c2be 100644 --- a/samples/ComplianceServer/Tools/ComplianceTools.cs +++ b/tests/Conformance/ConformanceServer/Tools/ConformanceTools.cs @@ -4,10 +4,10 @@ using System.ComponentModel; using System.Text.Json; -namespace ComplianceServer.Tools; +namespace ConformanceServer.Tools; [McpServerToolType] -public class ComplianceTools +public class ConformanceTools { // Sample base64 encoded 1x1 red PNG pixel for testing private const string TestImageBase64 = @@ -109,7 +109,7 @@ public static async Task ToolWithLogging( // Use ILogger for logging (will be forwarded to client if supported) ILoggerProvider loggerProvider = server.AsClientLoggerProvider(); - ILogger logger = loggerProvider.CreateLogger("ComplianceTools"); + ILogger logger = loggerProvider.CreateLogger("ConformanceTools"); logger.LogInformation("Tool execution started"); await Task.Delay(50, cancellationToken); diff --git a/samples/ComplianceServer/appsettings.Development.json b/tests/Conformance/ConformanceServer/appsettings.Development.json similarity index 100% rename from samples/ComplianceServer/appsettings.Development.json rename to tests/Conformance/ConformanceServer/appsettings.Development.json diff --git a/samples/ComplianceServer/appsettings.json b/tests/Conformance/ConformanceServer/appsettings.json similarity index 100% rename from samples/ComplianceServer/appsettings.json rename to tests/Conformance/ConformanceServer/appsettings.json From 6689ec0cfcea5a9edfe2f22e18cdc32e81133088 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 19 Nov 2025 11:07:43 -0800 Subject: [PATCH 05/14] Add test project for conformance tests --- .../ConformanceServer.csproj | 0 .../ConformanceServer/ConformanceServer.http | 0 .../ConformanceServer/Program.cs | 0 .../Prompts/ConformancePrompts.cs | 0 .../Properties/launchSettings.json | 0 .../Resources/ConformanceResources.cs | 0 .../Tools/ConformanceTools.cs | 0 .../appsettings.Development.json | 0 .../ConformanceServer/appsettings.json | 0 .../ConformanceTests.cs | 158 ++++++++++++++++++ ...delContextProtocol.ConformanceTests.csproj | 21 +++ .../README.md | 59 +++++++ 12 files changed, 238 insertions(+) rename tests/{Conformance => ModelContextProtocol.ConformanceTests}/ConformanceServer/ConformanceServer.csproj (100%) rename tests/{Conformance => ModelContextProtocol.ConformanceTests}/ConformanceServer/ConformanceServer.http (100%) rename tests/{Conformance => ModelContextProtocol.ConformanceTests}/ConformanceServer/Program.cs (100%) rename tests/{Conformance => ModelContextProtocol.ConformanceTests}/ConformanceServer/Prompts/ConformancePrompts.cs (100%) rename tests/{Conformance => ModelContextProtocol.ConformanceTests}/ConformanceServer/Properties/launchSettings.json (100%) rename tests/{Conformance => ModelContextProtocol.ConformanceTests}/ConformanceServer/Resources/ConformanceResources.cs (100%) rename tests/{Conformance => ModelContextProtocol.ConformanceTests}/ConformanceServer/Tools/ConformanceTools.cs (100%) rename tests/{Conformance => ModelContextProtocol.ConformanceTests}/ConformanceServer/appsettings.Development.json (100%) rename tests/{Conformance => ModelContextProtocol.ConformanceTests}/ConformanceServer/appsettings.json (100%) create mode 100644 tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs create mode 100644 tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj create mode 100644 tests/ModelContextProtocol.ConformanceTests/README.md diff --git a/tests/Conformance/ConformanceServer/ConformanceServer.csproj b/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.csproj similarity index 100% rename from tests/Conformance/ConformanceServer/ConformanceServer.csproj rename to tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.csproj diff --git a/tests/Conformance/ConformanceServer/ConformanceServer.http b/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.http similarity index 100% rename from tests/Conformance/ConformanceServer/ConformanceServer.http rename to tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.http diff --git a/tests/Conformance/ConformanceServer/Program.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Program.cs similarity index 100% rename from tests/Conformance/ConformanceServer/Program.cs rename to tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Program.cs diff --git a/tests/Conformance/ConformanceServer/Prompts/ConformancePrompts.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Prompts/ConformancePrompts.cs similarity index 100% rename from tests/Conformance/ConformanceServer/Prompts/ConformancePrompts.cs rename to tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Prompts/ConformancePrompts.cs diff --git a/tests/Conformance/ConformanceServer/Properties/launchSettings.json b/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Properties/launchSettings.json similarity index 100% rename from tests/Conformance/ConformanceServer/Properties/launchSettings.json rename to tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Properties/launchSettings.json diff --git a/tests/Conformance/ConformanceServer/Resources/ConformanceResources.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Resources/ConformanceResources.cs similarity index 100% rename from tests/Conformance/ConformanceServer/Resources/ConformanceResources.cs rename to tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Resources/ConformanceResources.cs diff --git a/tests/Conformance/ConformanceServer/Tools/ConformanceTools.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Tools/ConformanceTools.cs similarity index 100% rename from tests/Conformance/ConformanceServer/Tools/ConformanceTools.cs rename to tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Tools/ConformanceTools.cs diff --git a/tests/Conformance/ConformanceServer/appsettings.Development.json b/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/appsettings.Development.json similarity index 100% rename from tests/Conformance/ConformanceServer/appsettings.Development.json rename to tests/ModelContextProtocol.ConformanceTests/ConformanceServer/appsettings.Development.json diff --git a/tests/Conformance/ConformanceServer/appsettings.json b/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/appsettings.json similarity index 100% rename from tests/Conformance/ConformanceServer/appsettings.json rename to tests/ModelContextProtocol.ConformanceTests/ConformanceServer/appsettings.json diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs new file mode 100644 index 000000000..c73860090 --- /dev/null +++ b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs @@ -0,0 +1,158 @@ +using System.Diagnostics; +using System.Text; +using Xunit; + +namespace ModelContextProtocol.ConformanceTests; + +/// +/// Runs the official MCP conformance tests against the ConformanceServer. +/// This test starts the ConformanceServer, runs the Node.js-based conformance test suite, +/// and reports the results. +/// +public class ConformanceTests : IAsyncLifetime +{ + private const int ServerPort = 3001; + private static readonly string ServerUrl = $"http://localhost:{ServerPort}"; + private Process? _serverProcess; + + public async ValueTask InitializeAsync() + { + // Start the ConformanceServer + _serverProcess = StartConformanceServer(); + + // Wait for server to be ready (retry for up to 30 seconds) + var timeout = TimeSpan.FromSeconds(30); + var stopwatch = Stopwatch.StartNew(); + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; + + while (stopwatch.Elapsed < timeout) + { + try + { + // Try to connect to the MCP endpoint + var response = await httpClient.GetAsync($"{ServerUrl}/mcp"); + // Any response (even an error) means the server is up + return; + } + catch (HttpRequestException) + { + // Connection refused means server not ready yet + } + catch (TaskCanceledException) + { + // Timeout means server might be processing, give it more time + } + + await Task.Delay(500); + } + + throw new InvalidOperationException("ConformanceServer failed to start within the timeout period"); + } + + public ValueTask DisposeAsync() + { + // Stop the server + if (_serverProcess != null && !_serverProcess.HasExited) + { + _serverProcess.Kill(entireProcessTree: true); + _serverProcess.WaitForExit(5000); + _serverProcess.Dispose(); + } + return ValueTask.CompletedTask; + } + + [Fact] + [Trait("Execution", "Manual")] // Requires Node.js/npm to be installed + public async Task RunConformanceTests() + { + // Run the conformance test suite + var result = await RunNpxConformanceTests(); + + // Report the results + Assert.True(result.Success, + $"Conformance tests failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}"); + } private static Process StartConformanceServer() + { + // Find the ConformanceServer project directory + var testProjectDir = AppContext.BaseDirectory; + var conformanceServerDir = Path.GetFullPath( + Path.Combine(testProjectDir, "..", "..", "..", "..", "ConformanceServer")); + + if (!Directory.Exists(conformanceServerDir)) + { + throw new DirectoryNotFoundException( + $"ConformanceServer directory not found at: {conformanceServerDir}"); + } + + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"run --no-build --project ConformanceServer.csproj --urls {ServerUrl}", + WorkingDirectory = conformanceServerDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + Environment = + { + ["ASPNETCORE_ENVIRONMENT"] = "Development" + } + }; + + var process = Process.Start(startInfo); + if (process == null) + { + throw new InvalidOperationException("Failed to start ConformanceServer process"); + } + + return process; + } + + private static async Task<(bool Success, string Output, string Error)> RunNpxConformanceTests() + { + var startInfo = new ProcessStartInfo + { + FileName = "npx", + Arguments = $"@modelcontextprotocol/conformance server --url {ServerUrl}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + var outputBuilder = new StringBuilder(); + var errorBuilder = new StringBuilder(); + + var process = new Process { StartInfo = startInfo }; + + process.OutputDataReceived += (sender, e) => + { + if (e.Data != null) + { + Console.WriteLine(e.Data); // Echo to test output + outputBuilder.AppendLine(e.Data); + } + }; + + process.ErrorDataReceived += (sender, e) => + { + if (e.Data != null) + { + Console.Error.WriteLine(e.Data); // Echo to test output + errorBuilder.AppendLine(e.Data); + } + }; + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(); + + return ( + Success: process.ExitCode == 0, + Output: outputBuilder.ToString(), + Error: errorBuilder.ToString() + ); + } +} diff --git a/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj b/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj new file mode 100644 index 000000000..1485d63cf --- /dev/null +++ b/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + ModelContextProtocol.ConformanceTests + + + + + + + + + + + + + diff --git a/tests/ModelContextProtocol.ConformanceTests/README.md b/tests/ModelContextProtocol.ConformanceTests/README.md new file mode 100644 index 000000000..18649203a --- /dev/null +++ b/tests/ModelContextProtocol.ConformanceTests/README.md @@ -0,0 +1,59 @@ +# MCP Conformance Tests + +This project contains integration tests that run the official Model Context Protocol (MCP) conformance test suite against the C# SDK's ConformanceServer implementation. + +## Overview + +The conformance tests verify that the C# MCP server implementation adheres to the MCP specification by running the official Node.js-based conformance test suite. + +## Prerequisites + +- .NET 10.0 SDK or later +- Node.js and npm (required to run the `@modelcontextprotocol/conformance` package) + +## Running the Tests + +Since these tests require Node.js/npm to be installed, they are marked as manual tests and excluded from the default test run. + +### Run conformance tests explicitly + +```bash +# Run only conformance tests +dotnet test tests/Conformance/ModelContextProtocol.ConformanceTests --filter 'Execution=Manual' + +# Or run all manual tests across the solution +dotnet test --filter 'Execution=Manual' +``` + +### Skip conformance tests (default behavior) + +```bash +# Normal test run excludes Manual tests +dotnet test --filter '(Execution!=Manual)' + +# Or simply +dotnet test +``` + +## How It Works + +1. **ClassInitialize** - Starts the ConformanceServer on port 3001 and waits for it to be ready +2. **Test Execution** - Runs `npx @modelcontextprotocol/conformance server --url http://localhost:3001` +3. **Result Reporting** - Parses the conformance test output and reports pass/fail to MSTest +4. **ClassCleanup** - Shuts down the ConformanceServer + +## Troubleshooting + +If the tests fail: + +1. Ensure Node.js and npm are installed: `node --version && npm --version` +2. Check that port 3001 is not already in use +3. Review the test output for specific conformance test failures +4. The ConformanceServer logs are captured in the test output + +## Implementation Details + +- **Test Framework**: xUnit v3 with Microsoft.Testing.Platform +- **Server**: ASP.NET Core-based ConformanceServer with HTTP transport +- **Test Runner**: Uses `npx` to run the official MCP conformance test suite +- **Lifecycle**: Uses xUnit's `IAsyncLifetime` to manage server startup/shutdown per test class From 7f757065e70fa282293cb5f3353c6f72116da5bd Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 19 Nov 2025 17:21:04 -0800 Subject: [PATCH 06/14] Fix conformance tests --- .../ConformanceTests.cs | 6 ++-- ...delContextProtocol.ConformanceTests.csproj | 33 ++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs index c73860090..e9f00baa5 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs +++ b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs @@ -73,10 +73,12 @@ public async Task RunConformanceTests() $"Conformance tests failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}"); } private static Process StartConformanceServer() { - // Find the ConformanceServer project directory + // The ConformanceServer is in a subdirectory of the test project + // Test binary is in: artifacts/bin/ModelContextProtocol.ConformanceTests/net10.0/ + // ConformanceServer project is in: tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ var testProjectDir = AppContext.BaseDirectory; var conformanceServerDir = Path.GetFullPath( - Path.Combine(testProjectDir, "..", "..", "..", "..", "ConformanceServer")); + Path.Combine(testProjectDir, "..", "..", "..", "..", "tests", "ModelContextProtocol.ConformanceTests", "ConformanceServer")); if (!Directory.Exists(conformanceServerDir)) { diff --git a/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj b/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj index 1485d63cf..306cccb8a 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj +++ b/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj @@ -5,17 +5,42 @@ net10.0 enable enable + false + true ModelContextProtocol.ConformanceTests + + + true + + + + + + + - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + From effa3ce33cbc991ef8a916cca0f8a645870a91e2 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Thu, 20 Nov 2025 16:54:04 -0800 Subject: [PATCH 07/14] Skip the Conformance tests if Node is not installed --- .../ConformanceTests.cs | 36 ++++++++++++++- .../README.md | 46 +------------------ 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs index e9f00baa5..029e53848 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs +++ b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs @@ -62,9 +62,14 @@ public ValueTask DisposeAsync() } [Fact] - [Trait("Execution", "Manual")] // Requires Node.js/npm to be installed public async Task RunConformanceTests() { + // Check if Node.js is installed + if (!IsNodeInstalled()) + { + throw new SkipException("Node.js is not installed. Skipping conformance tests."); + } + // Run the conformance test suite var result = await RunNpxConformanceTests(); @@ -157,4 +162,33 @@ public async Task RunConformanceTests() Error: errorBuilder.ToString() ); } + + private static bool IsNodeInstalled() + { + try + { + var startInfo = new ProcessStartInfo + { + FileName = "node", + Arguments = "--version", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using var process = Process.Start(startInfo); + if (process == null) + { + return false; + } + + process.WaitForExit(5000); + return process.ExitCode == 0; + } + catch + { + return false; + } + } } diff --git a/tests/ModelContextProtocol.ConformanceTests/README.md b/tests/ModelContextProtocol.ConformanceTests/README.md index 18649203a..732335a5b 100644 --- a/tests/ModelContextProtocol.ConformanceTests/README.md +++ b/tests/ModelContextProtocol.ConformanceTests/README.md @@ -13,47 +13,5 @@ The conformance tests verify that the C# MCP server implementation adheres to th ## Running the Tests -Since these tests require Node.js/npm to be installed, they are marked as manual tests and excluded from the default test run. - -### Run conformance tests explicitly - -```bash -# Run only conformance tests -dotnet test tests/Conformance/ModelContextProtocol.ConformanceTests --filter 'Execution=Manual' - -# Or run all manual tests across the solution -dotnet test --filter 'Execution=Manual' -``` - -### Skip conformance tests (default behavior) - -```bash -# Normal test run excludes Manual tests -dotnet test --filter '(Execution!=Manual)' - -# Or simply -dotnet test -``` - -## How It Works - -1. **ClassInitialize** - Starts the ConformanceServer on port 3001 and waits for it to be ready -2. **Test Execution** - Runs `npx @modelcontextprotocol/conformance server --url http://localhost:3001` -3. **Result Reporting** - Parses the conformance test output and reports pass/fail to MSTest -4. **ClassCleanup** - Shuts down the ConformanceServer - -## Troubleshooting - -If the tests fail: - -1. Ensure Node.js and npm are installed: `node --version && npm --version` -2. Check that port 3001 is not already in use -3. Review the test output for specific conformance test failures -4. The ConformanceServer logs are captured in the test output - -## Implementation Details - -- **Test Framework**: xUnit v3 with Microsoft.Testing.Platform -- **Server**: ASP.NET Core-based ConformanceServer with HTTP transport -- **Test Runner**: Uses `npx` to run the official MCP conformance test suite -- **Lifecycle**: Uses xUnit's `IAsyncLifetime` to manage server startup/shutdown per test class +These tests will run as part of the standard `dotnet test` command if Node.js is installed +but will be skipped if Node.js is not detected. From ed0adcf15c139cb66e479b9385e851c299cee743 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sat, 22 Nov 2025 10:14:06 -0800 Subject: [PATCH 08/14] Restucture and add testing for .net8 and .net9 --- ModelContextProtocol.slnx | 2 + ...lContextProtocol.ConformanceServer.csproj} | 4 +- ...delContextProtocol.ConformanceServer.http} | 0 .../Program.cs | 2 + .../Prompts/ConformancePrompts.cs | 0 .../Properties/launchSettings.json | 0 .../Resources/ConformanceResources.cs | 0 .../Tools/ConformanceTools.cs | 10 +-- .../appsettings.Development.json | 0 .../appsettings.json | 0 .../ConformanceTests.cs | 87 ++++++++++++++----- .../GlobalUsings.cs | 1 + ...delContextProtocol.ConformanceTests.csproj | 14 +-- 13 files changed, 81 insertions(+), 39 deletions(-) rename tests/{ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.csproj => ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj} (52%) rename tests/{ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.http => ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.http} (100%) rename tests/{ModelContextProtocol.ConformanceTests/ConformanceServer => ModelContextProtocol.ConformanceServer}/Program.cs (98%) rename tests/{ModelContextProtocol.ConformanceTests/ConformanceServer => ModelContextProtocol.ConformanceServer}/Prompts/ConformancePrompts.cs (100%) rename tests/{ModelContextProtocol.ConformanceTests/ConformanceServer => ModelContextProtocol.ConformanceServer}/Properties/launchSettings.json (100%) rename tests/{ModelContextProtocol.ConformanceTests/ConformanceServer => ModelContextProtocol.ConformanceServer}/Resources/ConformanceResources.cs (100%) rename tests/{ModelContextProtocol.ConformanceTests/ConformanceServer => ModelContextProtocol.ConformanceServer}/Tools/ConformanceTools.cs (96%) rename tests/{ModelContextProtocol.ConformanceTests/ConformanceServer => ModelContextProtocol.ConformanceServer}/appsettings.Development.json (100%) rename tests/{ModelContextProtocol.ConformanceTests/ConformanceServer => ModelContextProtocol.ConformanceServer}/appsettings.json (100%) create mode 100644 tests/ModelContextProtocol.ConformanceTests/GlobalUsings.cs diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index 1f6dce1ed..2456d2590 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -70,6 +70,8 @@ + + diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.csproj b/tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj similarity index 52% rename from tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.csproj rename to tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj index 2fa29eef0..7dae8630a 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.csproj +++ b/tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj @@ -1,13 +1,13 @@ - net10.0 + net10.0;net9.0;net8.0 enable enable - + diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.http b/tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.http similarity index 100% rename from tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ConformanceServer.http rename to tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.http diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Program.cs b/tests/ModelContextProtocol.ConformanceServer/Program.cs similarity index 98% rename from tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Program.cs rename to tests/ModelContextProtocol.ConformanceServer/Program.cs index 2b2d6b258..090403b15 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Program.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Program.cs @@ -94,4 +94,6 @@ await ctx.Server.SampleAsync([ app.MapMcp(); +app.MapGet("/health", () => TypedResults.Ok("Healthy")); + app.Run(); diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Prompts/ConformancePrompts.cs b/tests/ModelContextProtocol.ConformanceServer/Prompts/ConformancePrompts.cs similarity index 100% rename from tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Prompts/ConformancePrompts.cs rename to tests/ModelContextProtocol.ConformanceServer/Prompts/ConformancePrompts.cs diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Properties/launchSettings.json b/tests/ModelContextProtocol.ConformanceServer/Properties/launchSettings.json similarity index 100% rename from tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Properties/launchSettings.json rename to tests/ModelContextProtocol.ConformanceServer/Properties/launchSettings.json diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Resources/ConformanceResources.cs b/tests/ModelContextProtocol.ConformanceServer/Resources/ConformanceResources.cs similarity index 100% rename from tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Resources/ConformanceResources.cs rename to tests/ModelContextProtocol.ConformanceServer/Resources/ConformanceResources.cs diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Tools/ConformanceTools.cs b/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs similarity index 96% rename from tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Tools/ConformanceTools.cs rename to tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs index a96d9c2be..9079bcd98 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/Tools/ConformanceTools.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs @@ -189,14 +189,14 @@ public static async Task Sampling( Messages = [new SamplingMessage { Role = Role.User, - Content = new TextContentBlock { Text = prompt }, + Content = [new TextContentBlock { Text = prompt }], }], MaxTokens = 100, Temperature = 0.7f }; var result = await server.SampleAsync(samplingParams, cancellationToken); - return $"Sampling result: {(result.Content as TextContentBlock)?.Text ?? "No text content"}"; + return $"Sampling result: {(result.Content.FirstOrDefault() as TextContentBlock)?.Text ?? "No text content"}"; } catch (Exception ex) { @@ -279,7 +279,7 @@ public static async Task ElicitationSep1034Defaults( Description = "Score", Default = 95.5 }, - ["status"] = new ElicitRequestParams.EnumSchema() + ["status"] = new ElicitRequestParams.UntitledSingleSelectEnumSchema() { Description = "Status", Enum = ["active", "inactive", "pending"], @@ -331,12 +331,12 @@ public static async Task ElicitationSep1330Enums( { Properties = { - ["color"] = new ElicitRequestParams.EnumSchema() + ["color"] = new ElicitRequestParams.UntitledSingleSelectEnumSchema() { Description = "Choose a color", Enum = ["red", "green", "blue"] }, - ["size"] = new ElicitRequestParams.EnumSchema() + ["size"] = new ElicitRequestParams.UntitledSingleSelectEnumSchema() { Description = "Choose a size", Enum = ["small", "medium", "large"], diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/appsettings.Development.json b/tests/ModelContextProtocol.ConformanceServer/appsettings.Development.json similarity index 100% rename from tests/ModelContextProtocol.ConformanceTests/ConformanceServer/appsettings.Development.json rename to tests/ModelContextProtocol.ConformanceServer/appsettings.Development.json diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceServer/appsettings.json b/tests/ModelContextProtocol.ConformanceServer/appsettings.json similarity index 100% rename from tests/ModelContextProtocol.ConformanceTests/ConformanceServer/appsettings.json rename to tests/ModelContextProtocol.ConformanceServer/appsettings.json diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs index 029e53848..09a615148 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs +++ b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Text; -using Xunit; namespace ModelContextProtocol.ConformanceTests; @@ -11,10 +10,33 @@ namespace ModelContextProtocol.ConformanceTests; /// public class ConformanceTests : IAsyncLifetime { - private const int ServerPort = 3001; - private static readonly string ServerUrl = $"http://localhost:{ServerPort}"; + // Use different ports for each target framework to allow parallel execution + // net10.0 -> 3001, net9.0 -> 3002, net8.0 -> 3003 + private static int GetPortForTargetFramework() + { + var testBinaryDir = AppContext.BaseDirectory; + var targetFramework = Path.GetFileName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar)); + + return targetFramework switch + { + "net10.0" => 3001, + "net9.0" => 3002, + "net8.0" => 3003, + _ => 3001 // Default fallback + }; + } + + private readonly int _serverPort = GetPortForTargetFramework(); + private readonly string _serverUrl; + private readonly ITestOutputHelper _output; private Process? _serverProcess; + public ConformanceTests(ITestOutputHelper output) + { + _output = output; + _serverUrl = $"http://localhost:{_serverPort}"; + } + public async ValueTask InitializeAsync() { // Start the ConformanceServer @@ -29,8 +51,8 @@ public async ValueTask InitializeAsync() { try { - // Try to connect to the MCP endpoint - var response = await httpClient.GetAsync($"{ServerUrl}/mcp"); + // Try to connect to the health endpoint + var response = await httpClient.GetAsync($"{_serverUrl}/health"); // Any response (even an error) means the server is up return; } @@ -65,10 +87,7 @@ public ValueTask DisposeAsync() public async Task RunConformanceTests() { // Check if Node.js is installed - if (!IsNodeInstalled()) - { - throw new SkipException("Node.js is not installed. Skipping conformance tests."); - } + Assert.SkipWhen(!IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests."); // Run the conformance test suite var result = await RunNpxConformanceTests(); @@ -76,14 +95,15 @@ public async Task RunConformanceTests() // Report the results Assert.True(result.Success, $"Conformance tests failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}"); - } private static Process StartConformanceServer() + } private Process StartConformanceServer() { - // The ConformanceServer is in a subdirectory of the test project - // Test binary is in: artifacts/bin/ModelContextProtocol.ConformanceTests/net10.0/ - // ConformanceServer project is in: tests/ModelContextProtocol.ConformanceTests/ConformanceServer/ - var testProjectDir = AppContext.BaseDirectory; + // The ConformanceServer binary is in a parallel directory to the test binary + // Test binary is in: artifacts/bin/ModelContextProtocol.ConformanceTests/Debug/{tfm}/ + // ConformanceServer binary is in: artifacts/bin/ModelContextProtocol.ConformanceServer/Debug/{tfm}/ + var testBinaryDir = AppContext.BaseDirectory; // e.g., .../net10.0/ + var targetFramework = Path.GetFileName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar)); var conformanceServerDir = Path.GetFullPath( - Path.Combine(testProjectDir, "..", "..", "..", "..", "tests", "ModelContextProtocol.ConformanceTests", "ConformanceServer")); + Path.Combine(testBinaryDir, "..", "..", "..", "ModelContextProtocol.ConformanceServer", "Debug", targetFramework)); if (!Directory.Exists(conformanceServerDir)) { @@ -94,7 +114,7 @@ public async Task RunConformanceTests() var startInfo = new ProcessStartInfo { FileName = "dotnet", - Arguments = $"run --no-build --project ConformanceServer.csproj --urls {ServerUrl}", + Arguments = $"ModelContextProtocol.ConformanceServer.dll --urls {_serverUrl}", WorkingDirectory = conformanceServerDir, RedirectStandardOutput = true, RedirectStandardError = true, @@ -112,15 +132,42 @@ public async Task RunConformanceTests() throw new InvalidOperationException("Failed to start ConformanceServer process"); } + // Asynchronously read output to prevent buffer overflow + _ = Task.Run(async () => + { + try + { + string? line; + while ((line = await process.StandardOutput.ReadLineAsync()) != null) + { + _output.WriteLine($"[Server {_serverPort}] {line}"); + } + } + catch { /* Process may exit */ } + }); + + _ = Task.Run(async () => + { + try + { + string? line; + while ((line = await process.StandardError.ReadLineAsync()) != null) + { + _output.WriteLine($"[Server {_serverPort} ERROR] {line}"); + } + } + catch { /* Process may exit */ } + }); + return process; } - private static async Task<(bool Success, string Output, string Error)> RunNpxConformanceTests() + private async Task<(bool Success, string Output, string Error)> RunNpxConformanceTests() { var startInfo = new ProcessStartInfo { FileName = "npx", - Arguments = $"@modelcontextprotocol/conformance server --url {ServerUrl}", + Arguments = $"@modelcontextprotocol/conformance server --url {_serverUrl}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, @@ -136,7 +183,7 @@ public async Task RunConformanceTests() { if (e.Data != null) { - Console.WriteLine(e.Data); // Echo to test output + _output.WriteLine(e.Data); outputBuilder.AppendLine(e.Data); } }; @@ -145,7 +192,7 @@ public async Task RunConformanceTests() { if (e.Data != null) { - Console.Error.WriteLine(e.Data); // Echo to test output + _output.WriteLine(e.Data); errorBuilder.AppendLine(e.Data); } }; diff --git a/tests/ModelContextProtocol.ConformanceTests/GlobalUsings.cs b/tests/ModelContextProtocol.ConformanceTests/GlobalUsings.cs new file mode 100644 index 000000000..c802f4480 --- /dev/null +++ b/tests/ModelContextProtocol.ConformanceTests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj b/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj index 306cccb8a..7e8214970 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj +++ b/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj @@ -1,8 +1,7 @@ - Exe - net10.0 + net10.0;net9.0;net8.0 enable enable false @@ -17,20 +16,11 @@ true - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive all - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - @@ -40,7 +30,7 @@ - + From 33059b5dc842bc9c7f3460db8bfcbafcc0cb9241 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sat, 22 Nov 2025 11:03:44 -0800 Subject: [PATCH 09/14] A few more little fixes --- tests/ModelContextProtocol.ConformanceServer/Program.cs | 2 -- .../ConformanceTests.cs | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ModelContextProtocol.ConformanceServer/Program.cs b/tests/ModelContextProtocol.ConformanceServer/Program.cs index 090403b15..a663b6d15 100644 --- a/tests/ModelContextProtocol.ConformanceServer/Program.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Program.cs @@ -1,11 +1,9 @@ -using ConformanceServer; using ConformanceServer.Prompts; using ConformanceServer.Resources; using ConformanceServer.Tools; using Microsoft.Extensions.AI; using ModelContextProtocol; using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; using System.Collections.Concurrent; var builder = WebApplication.CreateBuilder(args); diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs index 09a615148..53d919da1 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs +++ b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs @@ -52,7 +52,7 @@ public async ValueTask InitializeAsync() try { // Try to connect to the health endpoint - var response = await httpClient.GetAsync($"{_serverUrl}/health"); + await httpClient.GetAsync($"{_serverUrl}/health"); // Any response (even an error) means the server is up return; } @@ -95,7 +95,9 @@ public async Task RunConformanceTests() // Report the results Assert.True(result.Success, $"Conformance tests failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}"); - } private Process StartConformanceServer() + } + + private Process StartConformanceServer() { // The ConformanceServer binary is in a parallel directory to the test binary // Test binary is in: artifacts/bin/ModelContextProtocol.ConformanceTests/Debug/{tfm}/ From fbbc8075faa912bee2785e4e8f3e81a57be082bd Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sat, 22 Nov 2025 16:50:10 -0800 Subject: [PATCH 10/14] Fix Conformance tests in Release configuration --- .../ModelContextProtocol.ConformanceTests/ConformanceTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs index 53d919da1..99f769c3c 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs +++ b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs @@ -103,9 +103,10 @@ private Process StartConformanceServer() // Test binary is in: artifacts/bin/ModelContextProtocol.ConformanceTests/Debug/{tfm}/ // ConformanceServer binary is in: artifacts/bin/ModelContextProtocol.ConformanceServer/Debug/{tfm}/ var testBinaryDir = AppContext.BaseDirectory; // e.g., .../net10.0/ + var configuration = Path.GetFileName(Path.GetDirectoryName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar))!); var targetFramework = Path.GetFileName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar)); var conformanceServerDir = Path.GetFullPath( - Path.Combine(testBinaryDir, "..", "..", "..", "ModelContextProtocol.ConformanceServer", "Debug", targetFramework)); + Path.Combine(testBinaryDir, "..", "..", "..", "ModelContextProtocol.ConformanceServer", configuration, targetFramework)); if (!Directory.Exists(conformanceServerDir)) { From f2ba02d700ede50adb5864902aeb21abb8ff47b7 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sun, 23 Nov 2025 11:47:22 -0800 Subject: [PATCH 11/14] Attempt fix for server conformance tests in CI --- .github/workflows/ci-build-test.yml | 3 +++ .../ModelContextProtocol.ConformanceTests/ConformanceTests.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-build-test.yml b/.github/workflows/ci-build-test.yml index 5731f8365..e4e4be37d 100644 --- a/.github/workflows/ci-build-test.yml +++ b/.github/workflows/ci-build-test.yml @@ -67,6 +67,9 @@ jobs: - name: 📦 Install dependencies for tests run: npm install @modelcontextprotocol/server-memory + - name: 📦 Install dependencies for tests + run: npm install @modelcontextprotocol/conformance + - name: 🏗️ Build run: make build CONFIGURATION=${{ matrix.configuration }} diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs index 99f769c3c..e8ad358b4 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs +++ b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs @@ -170,7 +170,7 @@ private Process StartConformanceServer() var startInfo = new ProcessStartInfo { FileName = "npx", - Arguments = $"@modelcontextprotocol/conformance server --url {_serverUrl}", + Arguments = $"-y @modelcontextprotocol/conformance server --url {_serverUrl}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, From cd3d3fb37c0c52cbb0eba125264b4f613dc151bf Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sun, 23 Nov 2025 17:15:36 -0800 Subject: [PATCH 12/14] Another fix attempt for server conformance tests in CI --- tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs index e8ad358b4..14b44ceaa 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs +++ b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs @@ -219,7 +219,7 @@ private static bool IsNodeInstalled() { var startInfo = new ProcessStartInfo { - FileName = "node", + FileName = "npx", // Check specifically for npx because windows seems unable to find it Arguments = "--version", RedirectStandardOutput = true, RedirectStandardError = true, From 769fb69c54c2bd3898479efa79fad64f396a44cb Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Mon, 24 Nov 2025 14:29:07 -0800 Subject: [PATCH 13/14] Launch Conformance Server in-process for better debugging --- .../Program.cs | 170 ++++++++++-------- .../ConformanceTests.cs | 81 +++------ ...delContextProtocol.ConformanceTests.csproj | 4 + 3 files changed, 121 insertions(+), 134 deletions(-) diff --git a/tests/ModelContextProtocol.ConformanceServer/Program.cs b/tests/ModelContextProtocol.ConformanceServer/Program.cs index a663b6d15..d7120847b 100644 --- a/tests/ModelContextProtocol.ConformanceServer/Program.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Program.cs @@ -2,96 +2,114 @@ using ConformanceServer.Resources; using ConformanceServer.Tools; using Microsoft.Extensions.AI; -using ModelContextProtocol; using ModelContextProtocol.Protocol; using System.Collections.Concurrent; -var builder = WebApplication.CreateBuilder(args); +namespace ModelContextProtocol.ConformanceServer; -// Dictionary of session IDs to a set of resource URIs they are subscribed to -// The value is a ConcurrentDictionary used as a thread-safe HashSet -// because .NET does not have a built-in concurrent HashSet -ConcurrentDictionary> subscriptions = new(); - -builder.Services - .AddMcpServer() - .WithHttpTransport() - .WithTools() - .WithPrompts() - .WithResources() - .WithSubscribeToResourcesHandler(async (ctx, ct) => +public class Program +{ + public static async Task MainAsync(string[] args, ILoggerProvider? loggerProvider = null, CancellationToken cancellationToken = default) { - if (ctx.Server.SessionId == null) + var builder = WebApplication.CreateBuilder(args); + + if (loggerProvider != null) { - throw new McpException("Cannot add subscription for server with null SessionId"); + builder.Logging.ClearProviders(); + builder.Logging.AddProvider(loggerProvider); } - if (ctx.Params?.Uri is { } uri) - { - subscriptions[ctx.Server.SessionId].TryAdd(uri, 0); - await ctx.Server.SampleAsync([ - new ChatMessage(ChatRole.System, "You are a helpful test server"), - new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"), - ], - options: new ChatOptions + // Dictionary of session IDs to a set of resource URIs they are subscribed to + // The value is a ConcurrentDictionary used as a thread-safe HashSet + // because .NET does not have a built-in concurrent HashSet + ConcurrentDictionary> subscriptions = new(); + + builder.Services + .AddMcpServer() + .WithHttpTransport() + .WithTools() + .WithPrompts() + .WithResources() + .WithSubscribeToResourcesHandler(async (ctx, ct) => { - MaxOutputTokens = 100, - Temperature = 0.7f, - }, - cancellationToken: ct); - } + if (ctx.Server.SessionId == null) + { + throw new McpException("Cannot add subscription for server with null SessionId"); + } + if (ctx.Params?.Uri is { } uri) + { + subscriptions[ctx.Server.SessionId].TryAdd(uri, 0); - return new EmptyResult(); - }) - .WithUnsubscribeFromResourcesHandler(async (ctx, ct) => - { - if (ctx.Server.SessionId == null) - { - throw new McpException("Cannot remove subscription for server with null SessionId"); - } - if (ctx.Params?.Uri is { } uri) - { - subscriptions[ctx.Server.SessionId].TryRemove(uri, out _); - } - return new EmptyResult(); - }) - .WithCompleteHandler(async (ctx, ct) => - { - // Basic completion support - returns empty array for conformance - // Real implementations would provide contextual suggestions - return new CompleteResult - { - Completion = new Completion + await ctx.Server.SampleAsync([ + new ChatMessage(ChatRole.System, "You are a helpful test server"), + new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"), + ], + options: new ChatOptions + { + MaxOutputTokens = 100, + Temperature = 0.7f, + }, + cancellationToken: ct); + } + + return new EmptyResult(); + }) + .WithUnsubscribeFromResourcesHandler(async (ctx, ct) => { - Values = [], - HasMore = false, - Total = 0 - } - }; - }) - .WithSetLoggingLevelHandler(async (ctx, ct) => - { - if (ctx.Params?.Level is null) - { - throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams); - } + if (ctx.Server.SessionId == null) + { + throw new McpException("Cannot remove subscription for server with null SessionId"); + } + if (ctx.Params?.Uri is { } uri) + { + subscriptions[ctx.Server.SessionId].TryRemove(uri, out _); + } + return new EmptyResult(); + }) + .WithCompleteHandler(async (ctx, ct) => + { + // Basic completion support - returns empty array for conformance + // Real implementations would provide contextual suggestions + return new CompleteResult + { + Completion = new Completion + { + Values = [], + HasMore = false, + Total = 0 + } + }; + }) + .WithSetLoggingLevelHandler(async (ctx, ct) => + { + if (ctx.Params?.Level is null) + { + throw new McpProtocolException("Missing required argument 'level'", McpErrorCode.InvalidParams); + } - // The SDK updates the LoggingLevel field of the McpServer - // Send a log notification to confirm the level was set - await ctx.Server.SendNotificationAsync("notifications/message", new - { - Level = "info", - Logger = "conformance-test-server", - Data = $"Log level set to: {ctx.Params.Level}", - }, cancellationToken: ct); + // The SDK updates the LoggingLevel field of the McpServer + // Send a log notification to confirm the level was set + await ctx.Server.SendNotificationAsync("notifications/message", new + { + Level = "info", + Logger = "conformance-test-server", + Data = $"Log level set to: {ctx.Params.Level}", + }, cancellationToken: ct); - return new EmptyResult(); - }); + return new EmptyResult(); + }); -var app = builder.Build(); + var app = builder.Build(); -app.MapMcp(); + app.MapMcp(); -app.MapGet("/health", () => TypedResults.Ok("Healthy")); + app.MapGet("/health", () => TypedResults.Ok("Healthy")); -app.Run(); + await app.RunAsync(cancellationToken); + } + + public static async Task Main(string[] args) + { + await MainAsync(args); + } +} diff --git a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs index 14b44ceaa..9859da729 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs +++ b/tests/ModelContextProtocol.ConformanceTests/ConformanceTests.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Text; +using ModelContextProtocol.Tests.Utils; namespace ModelContextProtocol.ConformanceTests; @@ -29,7 +30,8 @@ private static int GetPortForTargetFramework() private readonly int _serverPort = GetPortForTargetFramework(); private readonly string _serverUrl; private readonly ITestOutputHelper _output; - private Process? _serverProcess; + private Task? _serverTask; + private CancellationTokenSource? _serverCts; public ConformanceTests(ITestOutputHelper output) { @@ -40,7 +42,7 @@ public ConformanceTests(ITestOutputHelper output) public async ValueTask InitializeAsync() { // Start the ConformanceServer - _serverProcess = StartConformanceServer(); + StartConformanceServer(); // Wait for server to be ready (retry for up to 30 seconds) var timeout = TimeSpan.FromSeconds(30); @@ -71,16 +73,25 @@ public async ValueTask InitializeAsync() throw new InvalidOperationException("ConformanceServer failed to start within the timeout period"); } - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { // Stop the server - if (_serverProcess != null && !_serverProcess.HasExited) + if (_serverCts != null) { - _serverProcess.Kill(entireProcessTree: true); - _serverProcess.WaitForExit(5000); - _serverProcess.Dispose(); + _serverCts.Cancel(); + if (_serverTask != null) + { + try + { + await _serverTask.WaitAsync(TimeSpan.FromSeconds(5)); + } + catch + { + // Ignore exceptions during shutdown + } + } + _serverCts.Dispose(); } - return ValueTask.CompletedTask; } [Fact] @@ -97,7 +108,7 @@ public async Task RunConformanceTests() $"Conformance tests failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}"); } - private Process StartConformanceServer() + private void StartConformanceServer() { // The ConformanceServer binary is in a parallel directory to the test binary // Test binary is in: artifacts/bin/ModelContextProtocol.ConformanceTests/Debug/{tfm}/ @@ -114,55 +125,9 @@ private Process StartConformanceServer() $"ConformanceServer directory not found at: {conformanceServerDir}"); } - var startInfo = new ProcessStartInfo - { - FileName = "dotnet", - Arguments = $"ModelContextProtocol.ConformanceServer.dll --urls {_serverUrl}", - WorkingDirectory = conformanceServerDir, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true, - Environment = - { - ["ASPNETCORE_ENVIRONMENT"] = "Development" - } - }; - - var process = Process.Start(startInfo); - if (process == null) - { - throw new InvalidOperationException("Failed to start ConformanceServer process"); - } - - // Asynchronously read output to prevent buffer overflow - _ = Task.Run(async () => - { - try - { - string? line; - while ((line = await process.StandardOutput.ReadLineAsync()) != null) - { - _output.WriteLine($"[Server {_serverPort}] {line}"); - } - } - catch { /* Process may exit */ } - }); - - _ = Task.Run(async () => - { - try - { - string? line; - while ((line = await process.StandardError.ReadLineAsync()) != null) - { - _output.WriteLine($"[Server {_serverPort} ERROR] {line}"); - } - } - catch { /* Process may exit */ } - }); - - return process; + // Start the server in a background task + _serverCts = new CancellationTokenSource(); + _serverTask = Task.Run(() => ConformanceServer.Program.MainAsync(["--urls", _serverUrl], new XunitLoggerProvider(_output), cancellationToken: _serverCts.Token)); } private async Task<(bool Success, string Output, string Error)> RunNpxConformanceTests() diff --git a/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj b/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj index 7e8214970..cdc7dce69 100644 --- a/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj +++ b/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj @@ -9,6 +9,10 @@ ModelContextProtocol.ConformanceTests + + + + + false diff --git a/tests/ModelContextProtocol.ConformanceServer/Program.cs b/tests/ModelContextProtocol.ConformanceServer/Program.cs index d7120847b..419937511 100644 --- a/tests/ModelContextProtocol.ConformanceServer/Program.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Program.cs @@ -4,6 +4,9 @@ using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; using System.Collections.Concurrent; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace ModelContextProtocol.ConformanceServer; @@ -89,11 +92,11 @@ await ctx.Server.SampleAsync([ // The SDK updates the LoggingLevel field of the McpServer // Send a log notification to confirm the level was set - await ctx.Server.SendNotificationAsync("notifications/message", new + await ctx.Server.SendNotificationAsync("notifications/message", new LoggingMessageNotificationParams { - Level = "info", + Level = LoggingLevel.Info, Logger = "conformance-test-server", - Data = $"Log level set to: {ctx.Params.Level}", + Data = JsonElement.Parse($"\"Log level set to: {ctx.Params.Level}\""), }, cancellationToken: ct); return new EmptyResult(); diff --git a/tests/ModelContextProtocol.ConformanceServer/Resources/ConformanceResources.cs b/tests/ModelContextProtocol.ConformanceServer/Resources/ConformanceResources.cs index 9e48a91c0..1e36cb646 100644 --- a/tests/ModelContextProtocol.ConformanceServer/Resources/ConformanceResources.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Resources/ConformanceResources.cs @@ -2,6 +2,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; using System.Text.Json; +using System.Text.Json.Serialization; namespace ConformanceServer.Resources; @@ -44,18 +45,13 @@ public static BlobResourceContents StaticBinary() [Description("A resource template with parameter substitution")] public static TextResourceContents TemplateResource(string id) { - var data = new - { - id = id, - templateTest = true, - data = $"Data for ID: {id}" - }; + var data = new ResourceData(id, true, $"Data for ID: {id}"); return new TextResourceContents { Uri = $"test://template/{id}/data", MimeType = "application/json", - Text = JsonSerializer.Serialize(data) + Text = JsonSerializer.Serialize(data, JsonContext.Default.ResourceData) }; } @@ -69,3 +65,8 @@ public static string WatchedResource() return "Watched resource content"; } } + +record ResourceData(string Id, bool TemplateTest, string Data); + +[JsonSerializable(typeof(ResourceData))] +internal partial class JsonContext : JsonSerializerContext; diff --git a/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs b/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs index 9079bcd98..f624e5ca3 100644 --- a/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs +++ b/tests/ModelContextProtocol.ConformanceServer/Tools/ConformanceTools.cs @@ -3,6 +3,7 @@ using ModelContextProtocol.Server; using System.ComponentModel; using System.Text.Json; +using System.Text.Json.Serialization; namespace ConformanceServer.Tools; @@ -90,7 +91,7 @@ public static ContentBlock[] MultipleContentTypes() { Uri = "test://mixed-content-resource", MimeType = "application/json", - Text = JsonSerializer.Serialize(new { test = "data", value = 123 }) + Text = "{ \"test\" = \"data\", \"value\" = 123 }" } } ]; diff --git a/tests/ModelContextProtocol.ConformanceTests/GlobalUsings.cs b/tests/ModelContextProtocol.ConformanceTests/GlobalUsings.cs deleted file mode 100644 index c802f4480..000000000 --- a/tests/ModelContextProtocol.ConformanceTests/GlobalUsings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; diff --git a/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj b/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj deleted file mode 100644 index cdc7dce69..000000000 --- a/tests/ModelContextProtocol.ConformanceTests/ModelContextProtocol.ConformanceTests.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - net10.0;net9.0;net8.0 - enable - enable - false - true - ModelContextProtocol.ConformanceTests - - - - - - - - - true - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/tests/ModelContextProtocol.ConformanceTests/README.md b/tests/ModelContextProtocol.ConformanceTests/README.md deleted file mode 100644 index 732335a5b..000000000 --- a/tests/ModelContextProtocol.ConformanceTests/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# MCP Conformance Tests - -This project contains integration tests that run the official Model Context Protocol (MCP) conformance test suite against the C# SDK's ConformanceServer implementation. - -## Overview - -The conformance tests verify that the C# MCP server implementation adheres to the MCP specification by running the official Node.js-based conformance test suite. - -## Prerequisites - -- .NET 10.0 SDK or later -- Node.js and npm (required to run the `@modelcontextprotocol/conformance` package) - -## Running the Tests - -These tests will run as part of the standard `dotnet test` command if Node.js is installed -but will be skipped if Node.js is not detected.