Skip to content

Conversation

@mikekistler
Copy link
Contributor

Motivation and Context

This PR adds a new sample server for use with the [MCP Conformance test suite].

How Has This Been Tested?

Tested with the server conformance test suite and all pass except one which I believe is a test error.

npx @modelcontextprotocol/conformance server --url http://localhost:3001
Total: 29 passed, 1 failed

The failing scenario is tools-call-with-logging, which fails because it did not set the logging level at the start of the test.

Breaking Changes

No breaking changes.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

@mikekistler mikekistler requested a review from Copilot November 18, 2025 21:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR adds a new ConformanceServer sample that implements the MCP specification for testing against the MCP Conformance test suite. The server demonstrates all major MCP capabilities including tools, resources, prompts, sampling, elicitation, subscriptions, and logging. Testing shows 29 out of 30 conformance tests pass, with the single failure being a test configuration issue.

Key changes:

  • Implements comprehensive tool handlers for testing different content types, logging, progress, error handling, sampling, and elicitation (including SEP-1034 and SEP-1330)
  • Adds resource handlers for static text/binary content, templates with parameters, and subscribable resources
  • Provides prompt handlers demonstrating simple, parameterized, embedded resource, and image-based prompts
  • Includes subscription management and completion support

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
samples/ComplianceServer/ComplianceServer.csproj Defines the project structure targeting .NET 9.0 with references to ModelContextProtocol.AspNetCore
samples/ComplianceServer/Program.cs Configures the MCP server with HTTP transport, registers handlers for tools/prompts/resources, and implements subscription management
samples/ComplianceServer/Tools/ComplianceTools.cs Implements 12 test tools covering text/image/audio content, logging, progress, errors, sampling, and elicitation with SEP-1034/SEP-1330 support
samples/ComplianceServer/Resources/ComplianceResources.cs Provides static text/binary resources and a parameterized template resource for conformance testing
samples/ComplianceServer/Prompts/CompliancePrompts.cs Implements simple, parameterized, embedded resource, and image-based prompts
samples/ComplianceServer/appsettings.json Standard ASP.NET Core configuration for logging and allowed hosts
samples/ComplianceServer/appsettings.Development.json Development-specific logging configuration
samples/ComplianceServer/Properties/launchSettings.json Defines HTTP and HTTPS launch profiles for localhost:3001
samples/ComplianceServer/ComplianceServer.http Provides sample HTTP requests for testing the server endpoints
ModelContextProtocol.slnx Adds the ComplianceServer project to the solution
Comments suppressed due to low confidence (1)

samples/ComplianceServer/Tools/ComplianceTools.cs:1

  • Field names in result content access don't match the schema property names defined above. The code accesses 'stringField', 'numberField', and 'booleanField', but the schema defines 'name', 'age', 'score', 'status', and 'verified'.
using ModelContextProtocol;

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.

Copy link
Contributor

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

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

Neat

@mikekistler
Copy link
Contributor Author

@stephentoub I think I've addressed all your comments. Copilot wrote the test class -- I'm not sure it is right so please give that a close look.

Copilot finished reviewing on behalf of mikekistler November 21, 2025 01:36
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 15 comments.

Comment on lines +304 to +306
return $"Accepted with values: string={result.Content["stringField"].GetString()}, " +
$"number={result.Content["numberField"].GetInt32()}, " +
$"boolean={result.Content["booleanField"].GetBoolean()}";
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.

Field name mismatch in result.Content access. The schema defines properties "name", "age", and "verified", but the code attempts to access "stringField", "numberField", and "booleanField". This will cause a runtime error. Should be:

return $"Accepted with values: string={result.Content["name"].GetString()}, " +
       $"number={result.Content["age"].GetInt32()}, " +
       $"boolean={result.Content["verified"].GetBoolean()}";
Suggested change
return $"Accepted with values: string={result.Content["stringField"].GetString()}, " +
$"number={result.Content["numberField"].GetInt32()}, " +
$"boolean={result.Content["booleanField"].GetBoolean()}";
return $"Accepted with values: string={result.Content["name"].GetString()}, " +
$"number={result.Content["age"].GetInt32()}, " +
$"boolean={result.Content["verified"].GetBoolean()}";

Copilot uses AI. Check for mistakes.
}
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.
}
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.
// Report the results
Assert.True(result.Success,
$"Conformance tests failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}");
} private static Process StartConformanceServer()
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] Missing blank line between methods. The closing brace of RunConformanceTests() on line 79 should be followed by a blank line before the StartConformanceServer() method declaration to improve readability.

Suggested change
} private static Process StartConformanceServer()
}
private static Process StartConformanceServer()

Copilot uses AI. Check for mistakes.
var startInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"run --no-build --project ConformanceServer.csproj --urls {ServerUrl}",
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 --no-build flag may cause test failures if the ConformanceServer hasn't been built prior to running tests. While the ProjectReference should ensure it's built when the test project builds, it's safer to remove --no-build to ensure the server is always built with the latest changes before running tests, especially in CI/CD scenarios or when running tests individually.

Suggested change
Arguments = $"run --no-build --project ConformanceServer.csproj --urls {ServerUrl}",
Arguments = $"run --project ConformanceServer.csproj --urls {ServerUrl}",

Copilot uses AI. Check for mistakes.
var outputBuilder = new StringBuilder();
var errorBuilder = new StringBuilder();

var process = new Process { StartInfo = startInfo };
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.

Disposable 'Process' is created but not disposed.

Copilot uses AI. Check for mistakes.
try
{
// Try to connect to the MCP endpoint
var response = await httpClient.GetAsync($"{ServerUrl}/mcp");
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.

This assignment to response is useless, since its value is never read.

Copilot uses AI. Check for mistakes.
Comment on lines +236 to +243
if (result.Action == "accept" && result.Content != null)
{
return $"User responded: {result.Content["response"].GetString()}";
}
else
{
return $"Elicitation {result.Action}";
}
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.

Both branches of this 'if' statement return - consider using '?' to express intent better.

Copilot uses AI. Check for mistakes.
Comment on lines +302 to +311
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}";
}
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.

Both branches of this 'if' statement return - consider using '?' to express intent better.

Copilot uses AI. Check for mistakes.
Comment on lines +354 to +362
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}";
}
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.

Both branches of this 'if' statement return - consider using '?' to express intent better.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants