-
Notifications
You must be signed in to change notification settings - Fork 571
Add ConformanceServer sample #983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
67f0e38
c9a1601
31412c4
4e61590
012460f
805ea7f
550cdc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="../../../src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,97 @@ | ||||||||||||
| 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); | ||||||||||||
|
|
||||||||||||
| // 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<string, ConcurrentDictionary<string, byte>> subscriptions = new(); | ||||||||||||
|
|
||||||||||||
| builder.Services | ||||||||||||
| .AddMcpServer() | ||||||||||||
| .WithHttpTransport() | ||||||||||||
| .WithTools<ConformanceTools>() | ||||||||||||
| .WithPrompts<ConformancePrompts>() | ||||||||||||
| .WithResources<ConformanceResources>() | ||||||||||||
| .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); | ||||||||||||
|
||||||||||||
| subscriptions[ctx.Server.SessionId].TryAdd(uri, 0); | |
| subscriptions.GetOrAdd(ctx.Server.SessionId, _ => new ConcurrentDictionary<string, byte>()).TryAdd(uri, 0); |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lambda is marked as async but contains no await expressions. The async keyword should be removed since the handler doesn't perform any asynchronous operations.
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential KeyNotFoundException when accessing subscriptions[ctx.Server.SessionId]. The code doesn't check if an entry exists for the session ID before attempting to remove from it. Should use TryGetValue:
if (subscriptions.TryGetValue(ctx.Server.SessionId, out var sessionSubscriptions))
{
sessionSubscriptions.TryRemove(uri, out _);
}| subscriptions[ctx.Server.SessionId].TryRemove(uri, out _); | |
| if (subscriptions.TryGetValue(ctx.Server.SessionId, out var sessionSubscriptions)) | |
| { | |
| sessionSubscriptions.TryRemove(uri, out _); | |
| } |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lambda is marked as async but contains no await expressions. The async keyword should be removed since the handler doesn't perform any asynchronous operations.
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incorrect notification payload type. Should use LoggingMessageNotificationParams instead of an anonymous object, and Level should be of type LoggingLevel (enum) rather than a string. Should be:
await ctx.Server.SendNotificationAsync("notifications/message", new LoggingMessageNotificationParams
{
Level = LoggingLevel.Info,
Logger = "conformance-test-server",
Data = JsonSerializer.SerializeToElement($"Log level set to: {ctx.Params.Level}", McpJsonUtilities.JsonContext.Default.String),
}, cancellationToken: ct);| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| using ModelContextProtocol.Server; | ||
| using ModelContextProtocol.Protocol; | ||
| using Microsoft.Extensions.AI; | ||
| using System.ComponentModel; | ||
|
|
||
| namespace ConformanceServer.Prompts; | ||
|
|
||
| public class ConformancePrompts | ||
|
||
| { | ||
| // Sample base64 encoded 1x1 red PNG pixel for testing | ||
| 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 string ParameterizedPrompt( | ||
| [Description("First test argument")] string arg1, | ||
| [Description("Second test argument")] string arg2) | ||
| { | ||
| return $"Prompt with arguments: arg1={arg1}, arg2={arg2}"; | ||
| } | ||
|
|
||
| [McpServerPrompt(Name = "test_prompt_with_embedded_resource"), Description("Prompt with embedded resource")] | ||
| public static IEnumerable<PromptMessage> PromptWithEmbeddedResource( | ||
| [Description("URI of the resource to embed")] string resourceUri) | ||
| { | ||
| return [ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similarly here, you could return a PromptMessage instead of an enumerable of prompt messages, if you want. |
||
| 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<PromptMessage> PromptWithImage() | ||
| { | ||
| return [ | ||
| new PromptMessage | ||
| { | ||
| Role = Role.User, | ||
| Content = new ImageContentBlock | ||
| { | ||
| MimeType = "image/png", | ||
| Data = TestImageBase64 | ||
| } | ||
| }, | ||
| new PromptMessage | ||
| { | ||
| Role = Role.User, | ||
| Content = new TextContentBlock { Text = "Please analyze the image above." } | ||
| }, | ||
| ]; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| using ModelContextProtocol.Protocol; | ||
| using ModelContextProtocol.Server; | ||
| using System.ComponentModel; | ||
| using System.Text.Json; | ||
|
|
||
| namespace ConformanceServer.Resources; | ||
|
|
||
| [McpServerResourceType] | ||
| public class ConformanceResources | ||
| { | ||
| // Sample base64 encoded 1x1 red PNG pixel for testing | ||
| private const string TestImageBase64 = | ||
| "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; | ||
|
|
||
| /// <summary> | ||
| /// Static text resource for testing | ||
| /// </summary> | ||
| [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."; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Static binary resource (image) for testing | ||
| /// </summary> | ||
| [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 = TestImageBase64 | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resource template with parameter substitution | ||
| /// </summary> | ||
| [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) | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Subscribable resource that can send updates | ||
| /// </summary> | ||
| [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"; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.