Decouple Handlers and Collections from protocol types (#765)

* Decouple Handlers and Collections (e.g. ToolsHandler and ToolsCollection) from Capabilities by promoting them to the options types.

Also, group all handlers in container classes, for server, I decided to move McpServerHandlers down to Mcp.Core and move the NotificationHandlers there too.

Also, Fix ListResourceTemplatesHandler issue.

* Add remarks for empty protocol capability types

* Keep JsonIgnore properties and mark them as Obsolete

* Continue supporting Obsolete members

* Update Obsolete messages to indicate the member will be removed

* Add EditorBrowsable Never to the remaining obsolete members
This commit is contained in:
David Cantú 2025-09-24 09:42:03 -05:00 • committed by GitHub
commit 5f992994c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1588 additions and 1161 deletions

View file

@ -63,21 +63,20 @@ public class McpClientCreationTests
{
Capabilities = new ClientCapabilities
{
Sampling = new SamplingCapability
{
SamplingHandler = async (c, p, t) =>
new CreateMessageResult
{
Content = new TextContentBlock { Text = "result" },
Model = "test-model",
Role = Role.User,
StopReason = "endTurn"
},
},
Roots = new RootsCapability
{
ListChanged = true,
RootsHandler = async (t, r) => new ListRootsResult { Roots = [] },
}
},
Handlers = new()
{
RootsHandler = async (t, r) => new ListRootsResult { Roots = [] },
SamplingHandler = async (c, p, t) => new CreateMessageResult
{
Content = new TextContentBlock { Text = "result" },
Model = "test-model",
Role = Role.User,
StopReason = "endTurn"
}
}
};

View file

@ -331,7 +331,7 @@ public class McpClientTests : ClientServerTestBase
int remainingProgress = TotalNotifications;
TaskCompletionSource<bool> allProgressReceived = new(TaskCreationOptions.RunContinuationsAsynchronously);
Server.ServerOptions.Capabilities?.Tools?.ToolCollection?.Add(McpServerTool.Create(async (IProgress<ProgressNotificationValue> progress) =>
Server.ServerOptions.ToolCollection?.Add(McpServerTool.Create(async (IProgress<ProgressNotificationValue> progress) =>
{
for (int i = 0; i < TotalNotifications; i++)
{

View file

@ -276,7 +276,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
TaskCompletionSource<bool> tcs = new();
await using var client = await _fixture.CreateClientAsync(clientId, new()
{
Capabilities = new()
Handlers = new()
{
NotificationHandlers =
[
@ -306,7 +306,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
TaskCompletionSource<bool> receivedNotification = new();
await using var client = await _fixture.CreateClientAsync(clientId, new()
{
Capabilities = new()
Handlers = new()
{
NotificationHandlers =
[
@ -374,22 +374,19 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
int samplingHandlerCalls = 0;
await using var client = await _fixture.CreateClientAsync(clientId, new()
{
Capabilities = new()
Handlers = new()
{
Sampling = new()
SamplingHandler = async (_, _, _) =>
{
SamplingHandler = async (_, _, _) =>
samplingHandlerCalls++;
return new CreateMessageResult
{
samplingHandlerCalls++;
return new CreateMessageResult
{
Model = "test-model",
Role = Role.Assistant,
Content = new TextContentBlock { Text = "Test response" },
};
},
},
},
Model = "test-model",
Role = Role.Assistant,
Content = new TextContentBlock { Text = "Test response" },
};
}
}
});
// Call the server's sampleLLM tool which should trigger our sampling handler
@ -477,8 +474,8 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
await using var client = await McpClient.CreateAsync(
new StdioClientTransport(stdioOptions),
clientOptions,
loggerFactory: LoggerFactory,
clientOptions,
loggerFactory: LoggerFactory,
cancellationToken: TestContext.Current.CancellationToken);
// act
@ -533,13 +530,10 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
.CreateSamplingHandler();
await using var client = await McpClient.CreateAsync(new StdioClientTransport(_fixture.EverythingServerTransportOptions), new()
{
Capabilities = new()
Handlers = new()
{
Sampling = new()
{
SamplingHandler = samplingHandler,
},
},
SamplingHandler = samplingHandler
}
}, cancellationToken: TestContext.Current.CancellationToken);
var result = await client.CallToolAsync("sampleLLM", new Dictionary<string, object?>()
@ -561,7 +555,7 @@ public partial class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIn
TaskCompletionSource<bool> receivedNotification = new();
await using var client = await _fixture.CreateClientAsync(clientId, new()
{
Capabilities = new()
Handlers = new()
{
NotificationHandlers =
[

View file

@ -90,7 +90,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
public void Adds_Prompts_To_Server()
{
var serverOptions = ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;
var prompts = serverOptions?.Capabilities?.Prompts?.PromptCollection;
var prompts = serverOptions.PromptCollection;
Assert.NotNull(prompts);
Assert.NotEmpty(prompts);
}
@ -137,7 +137,7 @@ public partial class McpServerBuilderExtensionsPromptsTests : ClientServerTestBa
Assert.False(notificationRead.IsCompleted);
var serverOptions = ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;
var serverPrompts = serverOptions.Capabilities?.Prompts?.PromptCollection;
var serverPrompts = serverOptions.PromptCollection;
Assert.NotNull(serverPrompts);
var newPrompt = McpServerPrompt.Create([McpServerPrompt(Name = "NewPrompt")] () => "42");

View file

@ -118,7 +118,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
public void Adds_Resources_To_Server()
{
var serverOptions = ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;
var resources = serverOptions?.Capabilities?.Resources?.ResourceCollection;
var resources = serverOptions.ResourceCollection;
Assert.NotNull(resources);
Assert.NotEmpty(resources);
}
@ -172,7 +172,7 @@ public partial class McpServerBuilderExtensionsResourcesTests : ClientServerTest
Assert.False(notificationRead.IsCompleted);
var serverOptions = ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;
var serverResources = serverOptions.Capabilities?.Resources?.ResourceCollection;
var serverResources = serverOptions.ResourceCollection;
Assert.NotNull(serverResources);
var newResource = McpServerResource.Create([McpServerResource(Name = "NewResource")] () => "42");

View file

@ -119,7 +119,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
public void Adds_Tools_To_Server()
{
var serverOptions = ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;
var tools = serverOptions.Capabilities?.Tools?.ToolCollection;
var tools = serverOptions.ToolCollection;
Assert.NotNull(tools);
Assert.NotEmpty(tools);
}
@ -201,7 +201,7 @@ public partial class McpServerBuilderExtensionsToolsTests : ClientServerTestBase
Assert.False(notificationRead.IsCompleted);
var serverOptions = ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;
var serverTools = serverOptions.Capabilities?.Tools?.ToolCollection;
var serverTools = serverOptions.ToolCollection;
Assert.NotNull(serverTools);
var newTool = McpServerTool.Create([McpServerTool(Name = "NewTool")] () => "42");

View file

@ -0,0 +1,194 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
namespace ModelContextProtocol.Tests.Configuration;
public class McpServerOptionsSetupTests
{
#region Prompt Handler Tests
[Fact]
public void Configure_WithListPromptsHandler_CreatesPromptsCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithListPromptsHandler(async (request, ct) => new ListPromptsResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.ListPromptsHandler);
Assert.NotNull(options.Capabilities?.Prompts);
}
[Fact]
public void Configure_WithGetPromptHandler_CreatesPromptsCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithGetPromptHandler(async (request, ct) => new GetPromptResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.GetPromptHandler);
Assert.NotNull(options.Capabilities?.Prompts);
}
#endregion
#region Resource Handler Tests
[Fact]
public void Configure_WithListResourceTemplatesHandler_CreatesResourcesCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithListResourceTemplatesHandler(async (request, ct) => new ListResourceTemplatesResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.ListResourceTemplatesHandler);
Assert.NotNull(options.Capabilities?.Resources);
}
[Fact]
public void Configure_WithListResourcesHandler_CreatesResourcesCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithListResourcesHandler(async (request, ct) => new ListResourcesResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.ListResourcesHandler);
Assert.NotNull(options.Capabilities?.Resources);
}
[Fact]
public void Configure_WithReadResourceHandler_CreatesResourcesCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithReadResourceHandler(async (request, ct) => new ReadResourceResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.ReadResourceHandler);
Assert.NotNull(options.Capabilities?.Resources);
}
[Fact]
public void Configure_WithSubscribeToResourcesHandler_And_WithOtherResourcesHandler_EnablesSubscription()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithListResourcesHandler(async (request, ct) => new ListResourcesResult())
.WithSubscribeToResourcesHandler(async (request, ct) => new EmptyResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.ListResourcesHandler);
Assert.NotNull(options.Handlers.SubscribeToResourcesHandler);
Assert.NotNull(options.Capabilities?.Resources);
Assert.True(options.Capabilities.Resources.Subscribe);
}
[Fact]
public void Configure_WithUnsubscribeFromResourcesHandler_And_WithOtherResourcesHandler_EnablesSubscription()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithListResourcesHandler(async (request, ct) => new ListResourcesResult())
.WithUnsubscribeFromResourcesHandler(async (request, ct) => new EmptyResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.ListResourcesHandler);
Assert.NotNull(options.Handlers.UnsubscribeFromResourcesHandler);
Assert.NotNull(options.Capabilities?.Resources);
Assert.True(options.Capabilities.Resources.Subscribe);
}
[Fact]
public void Configure_WithSubscribeToResourcesHandler_WithoutOtherResourcesHandler_DoesNotCreateResourcesCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithSubscribeToResourcesHandler(async (request, ct) => new EmptyResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Null(options.Handlers.SubscribeToResourcesHandler);
Assert.Null(options.Capabilities?.Resources);
}
[Fact]
public void Configure_WithUnsubscribeFromResourcesHandler_WithoutOtherResourcesHandler_DoesNotCreateResourcesCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithUnsubscribeFromResourcesHandler(async (request, ct) => new EmptyResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Null(options.Handlers.UnsubscribeFromResourcesHandler);
Assert.Null(options.Capabilities?.Resources);
}
#endregion
#region Tool Handler Tests
[Fact]
public void Configure_WithListToolsHandler_CreatesToolsCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithListToolsHandler(async (request, ct) => new ListToolsResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.ListToolsHandler);
Assert.NotNull(options.Capabilities?.Tools);
}
[Fact]
public void Configure_WithCallToolHandler_CreatesToolsCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithCallToolHandler(async (request, ct) => new CallToolResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.CallToolHandler);
Assert.NotNull(options.Capabilities?.Tools);
}
#endregion
#region Logging Handler Tests
[Fact]
public void Configure_WithSetLoggingLevelHandler_CreatesLoggingCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithSetLoggingLevelHandler(async (request, ct) => new EmptyResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.SetLoggingLevelHandler);
Assert.NotNull(options.Capabilities?.Logging);
}
#endregion
#region Completion Handler Tests
[Fact]
public void Configure_WithCompleteHandler_CreatesCompletionsCapability()
{
var services = new ServiceCollection();
services.AddMcpServer()
.WithCompleteHandler(async (request, ct) => new CompleteResult());
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.NotNull(options.Handlers.CompleteHandler);
Assert.NotNull(options.Capabilities?.Completions);
}
#endregion
}

View file

@ -139,16 +139,10 @@ public class DiagnosticTests
await using (McpServer server = McpServer.Create(serverTransport, new()
{
Capabilities = new()
{
Tools = new()
{
ToolCollection = [
McpServerTool.Create((int amount) => amount * 2, new() { Name = "DoubleValue", Description = "Doubles the value." }),
McpServerTool.Create(() => { throw new Exception("boom"); }, new() { Name = "Throw", Description = "Throws error." }),
],
}
}
ToolCollection = [
McpServerTool.Create((int amount) => amount * 2, new() { Name = "DoubleValue", Description = "Doubles the value." }),
McpServerTool.Create(() => { throw new Exception("boom"); }, new() { Name = "Throw", Description = "Throws error." }),
]
}))
{
serverTask = server.RunAsync(TestContext.Current.CancellationToken);

View file

@ -70,24 +70,21 @@ public class DockerEverythingServerTests(ITestOutputHelper testOutputHelper) : L
};
int samplingHandlerCalls = 0;
var defaultOptions = new McpClientOptions
var defaultOptions = new McpClientOptions()
{
Capabilities = new()
Handlers = new()
{
Sampling = new()
SamplingHandler = async (_, _, _) =>
{
SamplingHandler = async (_, _, _) =>
samplingHandlerCalls++;
return new CreateMessageResult
{
samplingHandlerCalls++;
return new CreateMessageResult
{
Model = "test-model",
Role = Role.Assistant,
Content = new TextContentBlock { Text = "Test response" },
};
},
},
},
Model = "test-model",
Role = Role.Assistant,
Content = new TextContentBlock { Text = "Test response" },
};
}
}
};
await using var client = await McpClient.CreateAsync(

View file

@ -69,75 +69,72 @@ public partial class ElicitationTests : ClientServerTestBase
{
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
{
Capabilities = new()
Handlers = new McpClientHandlers()
{
Elicitation = new()
ElicitationHandler = async (request, cancellationtoken) =>
{
ElicitationHandler = async (request, cancellationtoken) =>
Assert.NotNull(request);
Assert.Equal("Please provide more information.", request.Message);
Assert.Equal(4, request.RequestedSchema.Properties.Count);
foreach (var entry in request.RequestedSchema.Properties)
{
Assert.NotNull(request);
Assert.Equal("Please provide more information.", request.Message);
Assert.Equal(4, request.RequestedSchema.Properties.Count);
foreach (var entry in request.RequestedSchema.Properties)
switch (entry.Key)
{
switch (entry.Key)
{
case "prop1":
var primitiveString = Assert.IsType<ElicitRequestParams.StringSchema>(entry.Value);
Assert.Equal("title1", primitiveString.Title);
Assert.Equal(1, primitiveString.MinLength);
Assert.Equal(100, primitiveString.MaxLength);
break;
case "prop1":
var primitiveString = Assert.IsType<ElicitRequestParams.StringSchema>(entry.Value);
Assert.Equal("title1", primitiveString.Title);
Assert.Equal(1, primitiveString.MinLength);
Assert.Equal(100, primitiveString.MaxLength);
break;
case "prop2":
var primitiveNumber = Assert.IsType<ElicitRequestParams.NumberSchema>(entry.Value);
Assert.Equal("description2", primitiveNumber.Description);
Assert.Equal(0, primitiveNumber.Minimum);
Assert.Equal(1000, primitiveNumber.Maximum);
break;
case "prop2":
var primitiveNumber = Assert.IsType<ElicitRequestParams.NumberSchema>(entry.Value);
Assert.Equal("description2", primitiveNumber.Description);
Assert.Equal(0, primitiveNumber.Minimum);
Assert.Equal(1000, primitiveNumber.Maximum);
break;
case "prop3":
var primitiveBool = Assert.IsType<ElicitRequestParams.BooleanSchema>(entry.Value);
Assert.Equal("title3", primitiveBool.Title);
Assert.Equal("description4", primitiveBool.Description);
Assert.True(primitiveBool.Default);
break;
case "prop3":
var primitiveBool = Assert.IsType<ElicitRequestParams.BooleanSchema>(entry.Value);
Assert.Equal("title3", primitiveBool.Title);
Assert.Equal("description4", primitiveBool.Description);
Assert.True(primitiveBool.Default);
break;
case "prop4":
var primitiveEnum = Assert.IsType<ElicitRequestParams.EnumSchema>(entry.Value);
Assert.Equal(["option1", "option2", "option3"], primitiveEnum.Enum);
Assert.Equal(["Name1", "Name2", "Name3"], primitiveEnum.EnumNames);
break;
case "prop4":
var primitiveEnum = Assert.IsType<ElicitRequestParams.EnumSchema>(entry.Value);
Assert.Equal(["option1", "option2", "option3"], primitiveEnum.Enum);
Assert.Equal(["Name1", "Name2", "Name3"], primitiveEnum.EnumNames);
break;
default:
Assert.Fail($"Unknown property: {entry.Key}");
break;
}
default:
Assert.Fail($"Unknown property: {entry.Key}");
break;
}
}
return new ElicitResult
return new ElicitResult
{
Action = "accept",
Content = new Dictionary<string, JsonElement>
{
Action = "accept",
Content = new Dictionary<string, JsonElement>
{
["prop1"] = (JsonElement)JsonSerializer.Deserialize("""
"string result"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["prop2"] = (JsonElement)JsonSerializer.Deserialize("""
42
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["prop3"] = (JsonElement)JsonSerializer.Deserialize("""
true
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["prop4"] = (JsonElement)JsonSerializer.Deserialize("""
"option2"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
},
};
},
},
},
["prop1"] = (JsonElement)JsonSerializer.Deserialize("""
"string result"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["prop2"] = (JsonElement)JsonSerializer.Deserialize("""
42
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["prop3"] = (JsonElement)JsonSerializer.Deserialize("""
true
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["prop4"] = (JsonElement)JsonSerializer.Deserialize("""
"option2"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
},
};
}
}
});
var result = await client.CallToolAsync("TestElicitation", cancellationToken: TestContext.Current.CancellationToken);

View file

@ -103,90 +103,87 @@ public partial class ElicitationTypedTests : ClientServerTestBase
{
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
{
Capabilities = new()
Handlers = new()
{
Elicitation = new()
ElicitationHandler = async (request, cancellationToken) =>
{
ElicitationHandler = async (request, cancellationToken) =>
Assert.NotNull(request);
Assert.Equal("Please provide more information.", request.Message);
Assert.Equal(6, request.RequestedSchema.Properties.Count);
foreach (var entry in request.RequestedSchema.Properties)
{
Assert.NotNull(request);
Assert.Equal("Please provide more information.", request.Message);
Assert.Equal(6, request.RequestedSchema.Properties.Count);
foreach (var entry in request.RequestedSchema.Properties)
var key = entry.Key;
var value = entry.Value;
switch (key)
{
var key = entry.Key;
var value = entry.Value;
switch (key)
{
case nameof(SampleForm.Name):
var stringSchema = Assert.IsType<ElicitRequestParams.StringSchema>(value);
Assert.Equal("string", stringSchema.Type);
break;
case nameof(SampleForm.Name):
var stringSchema = Assert.IsType<ElicitRequestParams.StringSchema>(value);
Assert.Equal("string", stringSchema.Type);
break;
case nameof(SampleForm.Age):
var intSchema = Assert.IsType<ElicitRequestParams.NumberSchema>(value);
Assert.Equal("integer", intSchema.Type);
break;
case nameof(SampleForm.Age):
var intSchema = Assert.IsType<ElicitRequestParams.NumberSchema>(value);
Assert.Equal("integer", intSchema.Type);
break;
case nameof(SampleForm.Active):
var boolSchema = Assert.IsType<ElicitRequestParams.BooleanSchema>(value);
Assert.Equal("boolean", boolSchema.Type);
break;
case nameof(SampleForm.Active):
var boolSchema = Assert.IsType<ElicitRequestParams.BooleanSchema>(value);
Assert.Equal("boolean", boolSchema.Type);
break;
case nameof(SampleForm.Role):
var enumSchema = Assert.IsType<ElicitRequestParams.EnumSchema>(value);
Assert.Equal("string", enumSchema.Type);
Assert.Equal([nameof(SampleRole.User), nameof(SampleRole.Admin)], enumSchema.Enum);
break;
case nameof(SampleForm.Role):
var enumSchema = Assert.IsType<ElicitRequestParams.EnumSchema>(value);
Assert.Equal("string", enumSchema.Type);
Assert.Equal([nameof(SampleRole.User), nameof(SampleRole.Admin)], enumSchema.Enum);
break;
case nameof(SampleForm.Score):
var numSchema = Assert.IsType<ElicitRequestParams.NumberSchema>(value);
Assert.Equal("number", numSchema.Type);
break;
case nameof(SampleForm.Score):
var numSchema = Assert.IsType<ElicitRequestParams.NumberSchema>(value);
Assert.Equal("number", numSchema.Type);
break;
case nameof(SampleForm.Created):
var dateTimeSchema = Assert.IsType<ElicitRequestParams.StringSchema>(value);
Assert.Equal("string", dateTimeSchema.Type);
Assert.Equal("date-time", dateTimeSchema.Format);
case nameof(SampleForm.Created):
var dateTimeSchema = Assert.IsType<ElicitRequestParams.StringSchema>(value);
Assert.Equal("string", dateTimeSchema.Type);
Assert.Equal("date-time", dateTimeSchema.Format);
break;
break;
default:
Assert.Fail($"Unexpected property in schema: {key}");
break;
}
default:
Assert.Fail($"Unexpected property in schema: {key}");
break;
}
}
return new ElicitResult
return new ElicitResult
{
Action = "accept",
Content = new Dictionary<string, JsonElement>
{
Action = "accept",
Content = new Dictionary<string, JsonElement>
{
[nameof(SampleForm.Name)] = (JsonElement)JsonSerializer.Deserialize("""
"Alice"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Age)] = (JsonElement)JsonSerializer.Deserialize("""
30
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Active)] = (JsonElement)JsonSerializer.Deserialize("""
true
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Role)] = (JsonElement)JsonSerializer.Deserialize("""
"Admin"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Score)] = (JsonElement)JsonSerializer.Deserialize("""
99.5
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Created)] = (JsonElement)JsonSerializer.Deserialize("""
"2023-08-27T03:05:00"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
},
};
},
[nameof(SampleForm.Name)] = (JsonElement)JsonSerializer.Deserialize("""
"Alice"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Age)] = (JsonElement)JsonSerializer.Deserialize("""
30
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Active)] = (JsonElement)JsonSerializer.Deserialize("""
true
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Role)] = (JsonElement)JsonSerializer.Deserialize("""
"Admin"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Score)] = (JsonElement)JsonSerializer.Deserialize("""
99.5
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
[nameof(SampleForm.Created)] = (JsonElement)JsonSerializer.Deserialize("""
"2023-08-27T03:05:00"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
},
};
},
},
}
});
var result = await client.CallToolAsync("TestElicitationTyped", cancellationToken: TestContext.Current.CancellationToken);
@ -199,37 +196,34 @@ public partial class ElicitationTypedTests : ClientServerTestBase
{
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
{
Capabilities = new()
Handlers = new()
{
Elicitation = new()
ElicitationHandler = async (request, cancellationToken) =>
{
ElicitationHandler = async (request, cancellationToken) =>
Assert.NotNull(request);
Assert.Equal("Please provide more information.", request.Message);
// Expect camelCase names based on serializer options
Assert.Contains("firstName", request.RequestedSchema.Properties.Keys);
Assert.Contains("zipCode", request.RequestedSchema.Properties.Keys);
Assert.Contains("isAdmin", request.RequestedSchema.Properties.Keys);
return new ElicitResult
{
Assert.NotNull(request);
Assert.Equal("Please provide more information.", request.Message);
// Expect camelCase names based on serializer options
Assert.Contains("firstName", request.RequestedSchema.Properties.Keys);
Assert.Contains("zipCode", request.RequestedSchema.Properties.Keys);
Assert.Contains("isAdmin", request.RequestedSchema.Properties.Keys);
return new ElicitResult
Action = "accept",
Content = new Dictionary<string, JsonElement>
{
Action = "accept",
Content = new Dictionary<string, JsonElement>
{
["firstName"] = (JsonElement)JsonSerializer.Deserialize("""
"Bob"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["zipCode"] = (JsonElement)JsonSerializer.Deserialize("""
90210
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["isAdmin"] = (JsonElement)JsonSerializer.Deserialize("""
false
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
},
};
},
["firstName"] = (JsonElement)JsonSerializer.Deserialize("""
"Bob"
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["zipCode"] = (JsonElement)JsonSerializer.Deserialize("""
90210
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
["isAdmin"] = (JsonElement)JsonSerializer.Deserialize("""
false
""", McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(JsonElement)))!,
},
};
},
},
});
@ -243,16 +237,13 @@ public partial class ElicitationTypedTests : ClientServerTestBase
{
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
{
Capabilities = new()
Handlers = new()
{
Elicitation = new()
// Handler should never be invoked because the exception occurs before the request is sent.
ElicitationHandler = async (req, ct) =>
{
// Handler should never be invoked because the exception occurs before the request is sent.
ElicitationHandler = async (req, ct) =>
{
Assert.Fail("Elicitation handler should not be called for unsupported schema test.");
return new ElicitResult { Action = "cancel" };
},
Assert.Fail("Elicitation handler should not be called for unsupported schema test.");
return new ElicitResult { Action = "cancel" };
},
},
});
@ -268,18 +259,15 @@ public partial class ElicitationTypedTests : ClientServerTestBase
{
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
{
Capabilities = new()
Handlers = new()
{
Elicitation = new()
// Handler should never be invoked because the exception occurs before the request is sent.
ElicitationHandler = async (req, ct) =>
{
// Handler should never be invoked because the exception occurs before the request is sent.
ElicitationHandler = async (req, ct) =>
{
Assert.Fail("Elicitation handler should not be called for unsupported schema test.");
return new ElicitResult { Action = "cancel" };
},
Assert.Fail("Elicitation handler should not be called for unsupported schema test.");
return new ElicitResult { Action = "cancel" };
},
},
}
});
var ex = await Assert.ThrowsAsync<McpException>(async () =>
@ -291,18 +279,15 @@ public partial class ElicitationTypedTests : ClientServerTestBase
{
await using McpClient client = await CreateMcpClientForServer(new McpClientOptions
{
Capabilities = new()
Handlers = new()
{
Elicitation = new()
// Should not be invoked
ElicitationHandler = async (req, ct) =>
{
// Should not be invoked
ElicitationHandler = async (req, ct) =>
{
Assert.Fail("Elicitation handler should not be called for non-object generic type test.");
return new ElicitResult { Action = "cancel" };
},
Assert.Fail("Elicitation handler should not be called for non-object generic type test.");
return new ElicitResult { Action = "cancel" };
},
},
}
});
var ex = await Assert.ThrowsAsync<McpException>(async () =>

View file

@ -42,7 +42,7 @@ public class McpServerLoggingLevelTests
var server = provider.GetRequiredService<McpServer>();
Assert.NotNull(server.ServerOptions.Capabilities?.Logging);
Assert.NotNull(server.ServerOptions.Capabilities.Logging.SetLoggingLevelHandler);
Assert.NotNull(server.ServerOptions.Handlers.SetLoggingLevelHandler);
}
[Fact]

View file

@ -263,24 +263,24 @@ public class McpServerTests : LoggedTest
public async Task Can_Handle_Completion_Requests()
{
await Can_Handle_Requests(
new()
new ServerCapabilities
{
Completions = new()
{
CompleteHandler = async (request, ct) =>
new CompleteResult
{
Completion = new()
{
Values = ["test"],
Total = 2,
HasMore = true
}
}
}
},
method: RequestMethods.CompletionComplete,
configureOptions: null,
configureOptions: options =>
{
options.Handlers.CompleteHandler = async (request, ct) =>
new CompleteResult
{
Completion = new()
{
Values = ["test"],
Total = 2,
HasMore = true
}
};
},
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<CompleteResult>(response, McpJsonUtilities.DefaultOptions);
@ -298,26 +298,26 @@ public class McpServerTests : LoggedTest
new ServerCapabilities
{
Resources = new()
{
ListResourceTemplatesHandler = async (request, ct) =>
{
return new ListResourceTemplatesResult
{
ResourceTemplates = [new() { UriTemplate = "test", Name = "Test Resource" }]
};
},
ListResourcesHandler = async (request, ct) =>
{
return new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
};
},
ReadResourceHandler = (request, ct) => throw new NotImplementedException(),
}
},
RequestMethods.ResourcesTemplatesList,
configureOptions: null,
configureOptions: options =>
{
options.Handlers.ListResourceTemplatesHandler = async (request, ct) =>
{
return new ListResourceTemplatesResult
{
ResourceTemplates = [new() { UriTemplate = "test", Name = "Test Resource" }]
};
};
options.Handlers.ListResourcesHandler = async (request, ct) =>
{
return new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
};
};
options.Handlers.ReadResourceHandler = (request, ct) => throw new NotImplementedException();
},
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response, McpJsonUtilities.DefaultOptions);
@ -334,19 +334,19 @@ public class McpServerTests : LoggedTest
new ServerCapabilities
{
Resources = new()
{
ListResourcesHandler = async (request, ct) =>
{
return new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
};
},
ReadResourceHandler = (request, ct) => throw new NotImplementedException(),
}
},
RequestMethods.ResourcesList,
configureOptions: null,
configureOptions: options =>
{
options.Handlers.ListResourcesHandler = async (request, ct) =>
{
return new ListResourcesResult
{
Resources = [new() { Uri = "test", Name = "Test Resource" }]
};
};
options.Handlers.ReadResourceHandler = (request, ct) => throw new NotImplementedException();
},
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ListResourcesResult>(response, McpJsonUtilities.DefaultOptions);
@ -369,19 +369,19 @@ public class McpServerTests : LoggedTest
new ServerCapabilities
{
Resources = new()
{
ReadResourceHandler = async (request, ct) =>
{
return new ReadResourceResult
{
Contents = [new TextResourceContents { Text = "test" }]
};
},
ListResourcesHandler = (request, ct) => throw new NotImplementedException(),
}
},
method: RequestMethods.ResourcesRead,
configureOptions: null,
configureOptions: options =>
{
options.Handlers.ReadResourceHandler = async (request, ct) =>
{
return new ReadResourceResult
{
Contents = [new TextResourceContents { Text = "test" }]
};
};
options.Handlers.ListResourcesHandler = (request, ct) => throw new NotImplementedException();
},
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ReadResourceResult>(response, McpJsonUtilities.DefaultOptions);
@ -406,19 +406,19 @@ public class McpServerTests : LoggedTest
new ServerCapabilities
{
Prompts = new()
{
ListPromptsHandler = async (request, ct) =>
{
return new ListPromptsResult
{
Prompts = [new() { Name = "test" }]
};
},
GetPromptHandler = (request, ct) => throw new NotImplementedException(),
},
},
method: RequestMethods.PromptsList,
configureOptions: null,
configureOptions: options =>
{
options.Handlers.ListPromptsHandler = async (request, ct) =>
{
return new ListPromptsResult
{
Prompts = [new() { Name = "test" }]
};
};
options.Handlers.GetPromptHandler = (request, ct) => throw new NotImplementedException();
},
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ListPromptsResult>(response, McpJsonUtilities.DefaultOptions);
@ -441,13 +441,13 @@ public class McpServerTests : LoggedTest
new ServerCapabilities
{
Prompts = new()
{
GetPromptHandler = async (request, ct) => new GetPromptResult { Description = "test" },
ListPromptsHandler = (request, ct) => throw new NotImplementedException(),
}
},
method: RequestMethods.PromptsGet,
configureOptions: null,
configureOptions: options =>
{
options.Handlers.GetPromptHandler = async (request, ct) => new GetPromptResult { Description = "test" };
options.Handlers.ListPromptsHandler = (request, ct) => throw new NotImplementedException();
},
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<GetPromptResult>(response, McpJsonUtilities.DefaultOptions);
@ -469,19 +469,19 @@ public class McpServerTests : LoggedTest
new ServerCapabilities
{
Tools = new()
{
ListToolsHandler = async (request, ct) =>
{
return new ListToolsResult
{
Tools = [new() { Name = "test" }]
};
},
CallToolHandler = (request, ct) => throw new NotImplementedException(),
}
},
method: RequestMethods.ToolsList,
configureOptions: null,
configureOptions: options =>
{
options.Handlers.ListToolsHandler = async (request, ct) =>
{
return new ListToolsResult
{
Tools = [new() { Name = "test" }]
};
};
options.Handlers.CallToolHandler = (request, ct) => throw new NotImplementedException();
},
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<ListToolsResult>(response, McpJsonUtilities.DefaultOptions);
@ -504,19 +504,19 @@ public class McpServerTests : LoggedTest
new ServerCapabilities
{
Tools = new()
{
CallToolHandler = async (request, ct) =>
{
return new CallToolResult
{
Content = [new TextContentBlock { Text = "test" }]
};
},
ListToolsHandler = (request, ct) => throw new NotImplementedException(),
}
},
method: RequestMethods.ToolsCall,
configureOptions: null,
configureOptions: options =>
{
options.Handlers.CallToolHandler = async (request, ct) =>
{
return new CallToolResult
{
Content = [new TextContentBlock { Text = "test" }]
};
};
options.Handlers.ListToolsHandler = (request, ct) => throw new NotImplementedException();
},
assertResult: (_, response) =>
{
var result = JsonSerializer.Deserialize<CallToolResult>(response, McpJsonUtilities.DefaultOptions);
@ -703,14 +703,12 @@ public class McpServerTests : LoggedTest
var options = CreateOptions();
var notificationReceived = new TaskCompletionSource<JsonRpcNotification>();
options.Capabilities = new()
{
NotificationHandlers = [new(NotificationMethods.ProgressNotification, (notification, cancellationToken) =>
options.Handlers.NotificationHandlers =
[new(NotificationMethods.ProgressNotification, (notification, cancellationToken) =>
{
notificationReceived.TrySetResult(notification);
return default;
})],
};
})];
var server = McpServer.Create(transport, options, LoggerFactory);