Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Unused using directive. The namespace ConformanceServer is not used in this file - only the sub-namespaces ConformanceServer.Prompts, ConformanceServer.Resources, and ConformanceServer.Tools are used.

Copilot uses AI. Check for mistakes.
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);
Copy link

Copilot AI Nov 21, 2025

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 ensure that an entry exists for the session ID before attempting to add to it. Should use GetOrAdd to ensure the inner dictionary exists:

subscriptions.GetOrAdd(ctx.Server.SessionId, _ => new ConcurrentDictionary<string, byte>()).TryAdd(uri, 0);
Suggested change
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0);
subscriptions.GetOrAdd(ctx.Server.SessionId, _ => new ConcurrentDictionary<string, byte>()).TryAdd(uri, 0);

Copilot uses AI. Check for mistakes.

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) =>
Copy link

Copilot AI Nov 21, 2025

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 uses AI. Check for mistakes.
{
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 _);
Copy link

Copilot AI Nov 21, 2025

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 _);
}
Suggested change
subscriptions[ctx.Server.SessionId].TryRemove(uri, out _);
if (subscriptions.TryGetValue(ctx.Server.SessionId, out var sessionSubscriptions))
{
sessionSubscriptions.TryRemove(uri, out _);
}

Copilot uses AI. Check for mistakes.
}
return new EmptyResult();
})
.WithCompleteHandler(async (ctx, ct) =>
Copy link

Copilot AI Nov 21, 2025

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 uses AI. Check for mistakes.
{
// 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);
Comment on lines +83 to +88
Copy link

Copilot AI Nov 21, 2025

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);

Copilot uses AI. Check for mistakes.

return new EmptyResult();
});

var app = builder.Build();

app.MapMcp();

app.Run();
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
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing [McpServerPromptType] attribute on the class. This attribute is required to mark classes that contain MCP server prompts, similar to [McpServerToolType] used in ConformanceTools and [McpServerResourceType] used in ConformanceResources.

Copilot uses AI. Check for mistakes.
{
// 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 [
Copy link
Contributor

Choose a reason for hiding this comment

The 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";
}
}
Loading
Loading