Allow JsonSerializerOptions parameters for user-defined and inputs and hardcode everything else to the source generator. (#182)

* Allow user-defined JsonSerializerOptions for user-defined and inputs and hardcode everything else to the source generator.

* Add validation for user-defined JsonSerializerOptions.

* Remove out of date comment.

* Address feedback and add tests
This commit is contained in:
Eirik Tsarpalis 2025-04-02 18:43:54 +01:00 • committed by GitHub
commit f1af251c32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 632 additions and 372 deletions

View file

@ -1,9 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using ModelContextProtocol.Tests.Utils;
using System.IO.Pipelines;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
namespace ModelContextProtocol.Tests.Client;
@ -72,7 +75,7 @@ public class McpClientExtensionsTests : LoggedTest
{
IMcpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(12, tools.Count);
var echo = tools.Single(t => t.Name == "Method4");
var result = await echo.InvokeAsync(new Dictionary<string, object?>() { ["i"] = 42 }, TestContext.Current.CancellationToken);
@ -98,7 +101,7 @@ public class McpClientExtensionsTests : LoggedTest
{
IMcpClient client = await CreateMcpClientForServer();
await foreach (var tool in client.EnumerateToolsAsync(TestContext.Current.CancellationToken))
await foreach (var tool in client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken))
{
if (tool.Name == "Method4")
{
@ -110,4 +113,62 @@ public class McpClientExtensionsTests : LoggedTest
Assert.Fail("Couldn't find target method");
}
[Fact]
public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions()
{
JsonSerializerOptions options = new(JsonSerializerOptions.Default);
IMcpClient client = await CreateMcpClientForServer();
bool hasTools = false;
await foreach (var tool in client.EnumerateToolsAsync(options, TestContext.Current.CancellationToken))
{
Assert.Same(options, tool.JsonSerializerOptions);
hasTools = true;
}
foreach (var tool in await client.ListToolsAsync(options, TestContext.Current.CancellationToken))
{
Assert.Same(options, tool.JsonSerializerOptions);
}
Assert.True(hasTools);
}
[Fact]
public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions()
{
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
IMcpClient client = await CreateMcpClientForServer();
var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First();
await Assert.ThrowsAsync<NotSupportedException>(() => tool.InvokeAsync(new Dictionary<string, object?> { ["i"] = 42 }, TestContext.Current.CancellationToken));
}
[Fact]
public async Task SendRequestAsync_HonorsJsonSerializerOptions()
{
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
IMcpClient client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<NotSupportedException>(() => client.SendRequestAsync<CallToolRequestParams, CallToolResponse>("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
public async Task SendNotificationAsync_HonorsJsonSerializerOptions()
{
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
IMcpClient client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<NotSupportedException>(() => client.SendNotificationAsync("Method4", new { Value = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
public async Task GetPromptsAsync_HonorsJsonSerializerOptions()
{
JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() };
IMcpClient client = await CreateMcpClientForServer();
await Assert.ThrowsAsync<NotSupportedException>(() => client.GetPromptAsync("Prompt", new Dictionary<string, object?> { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken));
}
}

View file

@ -1,8 +1,9 @@
using System.Threading.Channels;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using System.Text.Json;
using System.Threading.Channels;
namespace ModelContextProtocol.Tests.Client;
@ -206,16 +207,16 @@ public class McpClientFactoryTests
_channel.Writer.TryWrite(new JsonRpcResponse
{
Id = ((JsonRpcRequest)message).Id,
Result = new InitializeResult()
Result = JsonSerializer.SerializeToNode(new InitializeResult
{
Capabilities = new ServerCapabilities(),
ProtocolVersion = "2024-11-05",
ServerInfo = new Implementation()
ServerInfo = new Implementation
{
Name = "NopTransport",
Version = "1.0.0"
},
}
}),
});
break;
}

View file

@ -68,7 +68,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
// act
await using var client = await _fixture.CreateClientAsync(clientId);
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotEmpty(tools);
@ -88,7 +88,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
{
["message"] = "Hello MCP!"
},
TestContext.Current.CancellationToken
cancellationToken: TestContext.Current.CancellationToken
);
// assert
@ -106,7 +106,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
// act
await using var client = await _fixture.CreateClientAsync(clientId);
var aiFunctions = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var aiFunctions = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
var echo = aiFunctions.Single(t => t.Name == "echo");
var result = await echo.InvokeAsync([new KeyValuePair<string, object?>("message", "Hello MCP!")], TestContext.Current.CancellationToken);
@ -140,7 +140,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
// act
await using var client = await _fixture.CreateClientAsync(clientId);
var result = await client.GetPromptAsync("simple_prompt", null, TestContext.Current.CancellationToken);
var result = await client.GetPromptAsync("simple_prompt", null, cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result);
@ -160,7 +160,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
{ "temperature", "0.7" },
{ "style", "formal" }
};
var result = await client.GetPromptAsync("complex_prompt", arguments, TestContext.Current.CancellationToken);
var result = await client.GetPromptAsync("complex_prompt", arguments, cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result);
@ -176,7 +176,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
// act
await using var client = await _fixture.CreateClientAsync(clientId);
await Assert.ThrowsAsync<McpClientException>(() =>
client.GetPromptAsync("non_existent_prompt", null, TestContext.Current.CancellationToken));
client.GetPromptAsync("non_existent_prompt", null, cancellationToken: TestContext.Current.CancellationToken));
}
[Theory]
@ -259,7 +259,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
await using var client = await _fixture.CreateClientAsync(clientId);
client.AddNotificationHandler(NotificationMethods.ResourceUpdatedNotification, (notification) =>
{
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params!.ToString() ?? string.Empty);
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params);
tcs.TrySetResult(true);
return Task.CompletedTask;
});
@ -280,7 +280,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
await using var client = await _fixture.CreateClientAsync(clientId);
client.AddNotificationHandler(NotificationMethods.ResourceUpdatedNotification, (notification) =>
{
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params!.ToString() ?? string.Empty);
var notificationParams = JsonSerializer.Deserialize<ResourceUpdatedNotificationParams>(notification.Params);
receivedNotification.TrySetResult(true);
return Task.CompletedTask;
});
@ -381,7 +381,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
["prompt"] = "Test prompt",
["maxTokens"] = 100
},
TestContext.Current.CancellationToken);
cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result);
@ -429,7 +429,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
// Verify we can send notifications without errors
await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification, cancellationToken: TestContext.Current.CancellationToken);
await client.SendNotificationAsync("test/notification", new { test = true }, TestContext.Current.CancellationToken);
await client.SendNotificationAsync("test/notification", new { test = true }, cancellationToken: TestContext.Current.CancellationToken);
// assert
// no response to check, if no exception is thrown, it's a success
@ -467,7 +467,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
var result = await client.CallToolAsync(
"read_graph",
new Dictionary<string, object?>(),
TestContext.Current.CancellationToken);
cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result);
@ -485,7 +485,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
_fixture.EverythingServerConfig,
_fixture.DefaultOptions,
cancellationToken: TestContext.Current.CancellationToken);
var mappedTools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var mappedTools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
// Create the chat client.
using IChatClient chatClient = new OpenAIClient(s_openAIKey).AsChatClient("gpt-4o-mini")
@ -529,7 +529,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
var result = await client.CallToolAsync("sampleLLM", new Dictionary<string, object?>()
{
["prompt"] = "In just a few words, what is the most famous tower in Paris?",
}, TestContext.Current.CancellationToken);
}, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.NotEmpty(result.Content);
@ -556,8 +556,7 @@ public class ClientIntegrationTests : LoggedTest, IClassFixture<ClientIntegratio
await using var client = await _fixture.CreateClientAsync(clientId);
client.AddNotificationHandler(NotificationMethods.LoggingMessageNotification, (notification) =>
{
var loggingMessageNotificationParameters = JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params!.ToString() ?? string.Empty,
jsonSerializerOptions);
var loggingMessageNotificationParameters = JsonSerializer.Deserialize<LoggingMessageNotificationParams>(notification.Params, jsonSerializerOptions);
if (loggingMessageNotificationParameters is not null)
{
receivedNotification.TrySetResult(true);

View file

@ -154,7 +154,7 @@ public class McpServerBuilderExtensionsPromptsTests : LoggedTest, IAsyncDisposab
var prompt = prompts.First(t => t.Name == nameof(SimplePrompts.ReturnsChatMessages));
Assert.Equal("Returns chat messages", prompt.Description);
var result = await prompt.GetAsync(new Dictionary<string, object?>() { ["message"] = "hello" }, TestContext.Current.CancellationToken);
var result = await prompt.GetAsync(new Dictionary<string, object?>() { ["message"] = "hello" }, cancellationToken: TestContext.Current.CancellationToken);
var chatMessages = result.ToChatMessages();
Assert.NotNull(chatMessages);

View file

@ -172,7 +172,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
{
IMcpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(16, tools.Count);
McpClientTool echoTool = tools.First(t => t.Name == "Echo");
@ -217,7 +217,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
loggerFactory: LoggerFactory,
cancellationToken: TestContext.Current.CancellationToken))
{
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(16, tools.Count);
McpClientTool echoTool = tools.First(t => t.Name == "Echo");
@ -244,7 +244,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
{
IMcpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(16, tools.Count);
Channel<JsonRpcNotification> listChanged = Channel.CreateUnbounded<JsonRpcNotification>();
@ -265,7 +265,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
serverTools.Add(newTool);
await notificationRead;
tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(17, tools.Count);
Assert.Contains(tools, t => t.Name == "NewTool");
@ -274,7 +274,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
serverTools.Remove(newTool);
await notificationRead;
tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(16, tools.Count);
Assert.DoesNotContain(tools, t => t.Name == "NewTool");
}
@ -287,7 +287,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
var result = await client.CallToolAsync(
"Echo",
new Dictionary<string, object?>() { ["message"] = "Peter" },
TestContext.Current.CancellationToken);
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result);
Assert.NotNull(result.Content);
@ -305,7 +305,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
var result = await client.CallToolAsync(
"EchoArray",
new Dictionary<string, object?>() { ["message"] = "Peter" },
TestContext.Current.CancellationToken);
cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(result.Content);
Assert.NotEmpty(result.Content);
@ -536,7 +536,7 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
{
IMcpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(tools);
Assert.NotEmpty(tools);
@ -612,26 +612,25 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
IMcpClient client = await CreateMcpClientForServer();
client.AddNotificationHandler(NotificationMethods.ProgressNotification, notification =>
{
ProgressNotification pn = JsonSerializer.Deserialize<ProgressNotification>((JsonElement)notification.Params!)!;
ProgressNotification pn = JsonSerializer.Deserialize<ProgressNotification>(notification.Params)!;
notifications.Enqueue(pn);
return Task.CompletedTask;
});
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(tools);
Assert.NotEmpty(tools);
McpClientTool progressTool = tools.First(t => t.Name == nameof(EchoTool.SendsProgressNotifications));
var result = await client.SendRequestAsync<CallToolResponse>(new JsonRpcRequest()
{
Method = RequestMethods.ToolsCall,
Params = new CallToolRequestParams()
var result = await client.SendRequestAsync<CallToolRequestParams, CallToolResponse>(
RequestMethods.ToolsCall,
new CallToolRequestParams
{
Name = progressTool.ProtocolTool.Name,
Meta = new() { ProgressToken = new("abc123") },
},
}, TestContext.Current.CancellationToken);
cancellationToken: TestContext.Current.CancellationToken);
Assert.Contains("done", JsonSerializer.Serialize(result));
SpinWait.SpinUntil(() => notifications.Count == 10, TimeSpan.FromSeconds(10));
@ -652,18 +651,17 @@ public class McpServerBuilderExtensionsToolsTests : LoggedTest, IAsyncDisposable
{
IMcpClient client = await CreateMcpClientForServer();
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(tools);
Assert.NotEmpty(tools);
McpClientTool cancelableTool = tools.First(t => t.Name == nameof(EchoTool.InfiniteCancelableOperation));
var requestId = new RequestId(Guid.NewGuid().ToString());
var invokeTask = client.SendRequestAsync<CallToolResponse>(new JsonRpcRequest()
{
Method = RequestMethods.ToolsCall,
Id = requestId,
Params = new CallToolRequestParams() { Name = cancelableTool.ProtocolTool.Name },
}, TestContext.Current.CancellationToken);
var invokeTask = client.SendRequestAsync<CallToolRequestParams, CallToolResponse>(
RequestMethods.ToolsCall,
new CallToolRequestParams { Name = cancelableTool.ProtocolTool.Name },
requestId: requestId,
cancellationToken: TestContext.Current.CancellationToken);
await client.SendNotificationAsync(
NotificationMethods.CancelledNotification,

View file

@ -23,7 +23,7 @@ public class DiagnosticTests
{
await RunConnected(async (client, server) =>
{
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(tools);
Assert.NotEmpty(tools);

View file

@ -5,6 +5,8 @@ using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Server;
using ModelContextProtocol.Tests.Utils;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Server;
@ -171,7 +173,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<PingResult>(response);
JsonObject jObj = Assert.IsType<JsonObject>(response);
Assert.Empty(jObj);
});
}
@ -184,9 +187,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<InitializeResult>(response);
var result = (InitializeResult)response;
var result = JsonSerializer.Deserialize<InitializeResult>(response);
Assert.NotNull(result);
Assert.Equal("TestServer", result.ServerInfo.Name);
Assert.Equal("1.0", result.ServerInfo.Version);
Assert.Equal("2024", result.ProtocolVersion);
@ -202,10 +204,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<CompleteResult>(response);
var result = (CompleteResult)response;
Assert.NotNull(result.Completion);
var result = JsonSerializer.Deserialize<CompleteResult>(response);
Assert.NotNull(result?.Completion);
Assert.Empty(result.Completion.Values);
Assert.Equal(0, result.Completion.Total);
Assert.False(result.Completion.HasMore);
@ -233,10 +233,8 @@ public class McpServerTests : LoggedTest
},
assertResult: response =>
{
Assert.IsType<CompleteResult>(response);
var result = (CompleteResult)response;
Assert.NotNull(result.Completion);
CompleteResult? result = JsonSerializer.Deserialize<CompleteResult>(response);
Assert.NotNull(result?.Completion);
Assert.NotEmpty(result.Completion.Values);
Assert.Equal("test", result.Completion.Values[0]);
Assert.Equal(2, result.Completion.Total);
@ -273,10 +271,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ListResourceTemplatesResult>(response);
var result = (ListResourceTemplatesResult)response;
Assert.NotNull(result.ResourceTemplates);
var result = JsonSerializer.Deserialize<ListResourceTemplatesResult>(response);
Assert.NotNull(result?.ResourceTemplates);
Assert.NotEmpty(result.ResourceTemplates);
Assert.Equal("test", result.ResourceTemplates[0].UriTemplate);
});
@ -304,10 +300,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ListResourcesResult>(response);
var result = (ListResourcesResult)response;
Assert.NotNull(result.Resources);
var result = JsonSerializer.Deserialize<ListResourcesResult>(response);
Assert.NotNull(result?.Resources);
Assert.NotEmpty(result.Resources);
Assert.Equal("test", result.Resources[0].Uri);
});
@ -331,7 +325,7 @@ public class McpServerTests : LoggedTest
{
return Task.FromResult(new ReadResourceResult
{
Contents = [new TextResourceContents() { Text = "test" }]
Contents = [new TextResourceContents { Text = "test" }]
});
},
ListResourcesHandler = (request, ct) => throw new NotImplementedException(),
@ -341,10 +335,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ReadResourceResult>(response);
var result = (ReadResourceResult)response;
Assert.NotNull(result.Contents);
var result = JsonSerializer.Deserialize<ReadResourceResult>(response);
Assert.NotNull(result?.Contents);
Assert.NotEmpty(result.Contents);
TextResourceContents textResource = Assert.IsType<TextResourceContents>(result.Contents[0]);
@ -380,10 +372,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ListPromptsResult>(response);
var result = (ListPromptsResult)response;
Assert.NotNull(result.Prompts);
var result = JsonSerializer.Deserialize<ListPromptsResult>(response);
Assert.NotNull(result?.Prompts);
Assert.NotEmpty(result.Prompts);
Assert.Equal("test", result.Prompts[0].Name);
});
@ -411,9 +401,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<GetPromptResult>(response);
var result = (GetPromptResult)response;
var result = JsonSerializer.Deserialize<GetPromptResult>(response);
Assert.NotNull(result);
Assert.Equal("test", result.Description);
});
}
@ -446,9 +435,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<ListToolsResult>(response);
var result = (ListToolsResult)response;
var result = JsonSerializer.Deserialize<ListToolsResult>(response);
Assert.NotNull(result);
Assert.NotEmpty(result.Tools);
Assert.Equal("test", result.Tools[0].Name);
});
@ -482,9 +470,8 @@ public class McpServerTests : LoggedTest
configureOptions: null,
assertResult: response =>
{
Assert.IsType<CallToolResponse>(response);
var result = (CallToolResponse)response;
var result = JsonSerializer.Deserialize<CallToolResponse>(response);
Assert.NotNull(result);
Assert.NotEmpty(result.Content);
Assert.Equal("test", result.Content[0].Text);
});
@ -496,7 +483,7 @@ public class McpServerTests : LoggedTest
await Throws_Exception_If_No_Handler_Assigned(new ServerCapabilities { Tools = new() }, RequestMethods.ToolsCall, "CallTool handler not configured");
}
private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action<McpServerOptions>? configureOptions, Action<object> assertResult)
private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, string method, Action<McpServerOptions>? configureOptions, Action<JsonNode?> assertResult)
{
await using var transport = new TestServerTransport();
var options = CreateOptions(serverCapabilities);
@ -524,7 +511,6 @@ public class McpServerTests : LoggedTest
var response = await receivedMessage.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.NotNull(response);
Assert.NotNull(response.Result);
assertResult(response.Result);
@ -610,10 +596,11 @@ public class McpServerTests : LoggedTest
supportsSampling ? new ClientCapabilities { Sampling = new SamplingCapability() } :
null;
public Task<T> SendRequestAsync<T>(JsonRpcRequest request, CancellationToken cancellationToken) where T : class
public Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken)
{
CreateMessageRequestParams rp = Assert.IsType<CreateMessageRequestParams>(request.Params);
CreateMessageRequestParams? rp = JsonSerializer.Deserialize<CreateMessageRequestParams>(request.Params);
Assert.NotNull(rp);
Assert.Equal(0.75f, rp.Temperature);
Assert.Equal(42, rp.MaxTokens);
Assert.Equal(["."], rp.StopSequences);
@ -634,7 +621,12 @@ public class McpServerTests : LoggedTest
Role = "assistant",
StopReason = "endTurn",
};
return Task.FromResult((T)(object)result);
return Task.FromResult(new JsonRpcResponse
{
Id = new RequestId("0"),
Result = JsonSerializer.SerializeToNode(result),
});
}
public ValueTask DisposeAsync() => default;
@ -670,7 +662,7 @@ public class McpServerTests : LoggedTest
await transport.SendMessageAsync(new JsonRpcNotification
{
Method = NotificationMethods.ProgressNotification,
Params = new ProgressNotification()
Params = JsonSerializer.SerializeToNode(new ProgressNotification
{
ProgressToken = new("abc"),
Progress = new()
@ -679,11 +671,12 @@ public class McpServerTests : LoggedTest
Total = 100,
Message = "Progress message",
},
},
}),
}, TestContext.Current.CancellationToken);
var notification = await notificationReceived.Task;
var progress = (ProgressNotification)notification.Params!;
var progress = JsonSerializer.Deserialize<ProgressNotification>(notification.Params);
Assert.NotNull(progress);
Assert.Equal("abc", progress.ProgressToken.ToString());
Assert.Equal(50, progress.Progress.Progress);
Assert.Equal(100, progress.Progress.Total);

View file

@ -3,7 +3,6 @@ using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using ModelContextProtocol.Tests.Utils;
using System.Text.Json;
namespace ModelContextProtocol.Tests;
@ -41,7 +40,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10));
// Send a test message through POST endpoint
await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, TestContext.Current.CancellationToken);
await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, cancellationToken: TestContext.Current.CancellationToken);
// Assert
Assert.True(true);
@ -78,7 +77,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
defaultOptions,
loggerFactory: LoggerFactory,
cancellationToken: TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotEmpty(tools);
@ -145,7 +144,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
{
["prompt"] = "Test prompt",
["maxTokens"] = 100
}, TestContext.Current.CancellationToken);
}, cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result);
@ -188,7 +187,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
await server.WaitForConnectionAsync(TimeSpan.FromSeconds(10));
// Send a test message through POST endpoint
await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, TestContext.Current.CancellationToken);
await client.SendNotificationAsync("test/message", new { message = "Hello, SSE!" }, cancellationToken: TestContext.Current.CancellationToken);
// Assert
Assert.True(true);
@ -229,7 +228,7 @@ public class SseIntegrationTests(ITestOutputHelper outputHelper) : LoggedTest(ou
var receivedNotification = new TaskCompletionSource<string?>();
client.AddNotificationHandler("test/notification", (args) =>
{
var msg = ((JsonElement?)args.Params)?.GetProperty("message").GetString();
var msg = args.Params?["message"]?.GetValue<string>();
receivedNotification.SetResult(msg);
return Task.CompletedTask;

View file

@ -63,7 +63,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// act
await using var client = await GetClientAsync();
var tools = await client.ListToolsAsync(TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(tools);
@ -82,7 +82,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
{
["message"] = "Hello MCP!"
},
TestContext.Current.CancellationToken
cancellationToken: TestContext.Current.CancellationToken
);
// assert
@ -168,7 +168,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// act
await using var client = await GetClientAsync();
var result = await client.GetPromptAsync("simple_prompt", null, TestContext.Current.CancellationToken);
var result = await client.GetPromptAsync("simple_prompt", null, cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result);
@ -187,7 +187,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
{ "temperature", "0.7" },
{ "style", "formal" }
};
var result = await client.GetPromptAsync("complex_prompt", arguments, TestContext.Current.CancellationToken);
var result = await client.GetPromptAsync("complex_prompt", arguments, cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result);
@ -202,7 +202,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
// act
await using var client = await GetClientAsync();
await Assert.ThrowsAsync<McpClientException>(() =>
client.GetPromptAsync("non_existent_prompt", null, TestContext.Current.CancellationToken));
client.GetPromptAsync("non_existent_prompt", null, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
@ -238,7 +238,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
["prompt"] = "Test prompt",
["maxTokens"] = 100
},
TestContext.Current.CancellationToken);
cancellationToken: TestContext.Current.CancellationToken);
// assert
Assert.NotNull(result);
@ -262,7 +262,7 @@ public class SseServerIntegrationTests : LoggedTest, IClassFixture<SseServerInte
{
["message"] = $"Hello MCP! {i}"
},
TestContext.Current.CancellationToken
cancellationToken: TestContext.Current.CancellationToken
);
Assert.NotNull(result);

View file

@ -8,6 +8,7 @@ using ModelContextProtocol.Utils.Json;
using System.IO.Pipelines;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace ModelContextProtocol.Tests.Transport;
@ -153,10 +154,10 @@ public class StdioServerTransportTests : LoggedTest
{
Method = "test",
Id = new RequestId(44),
Params = new Dictionary<string, JsonElement>
Params = new JsonObject
{
["text"] = JsonSerializer.SerializeToElement(chineseText)
}
["text"] = chineseText
},
};
// Clear output and send message
@ -175,10 +176,10 @@ public class StdioServerTransportTests : LoggedTest
{
Method = "test",
Id = new RequestId(45),
Params = new Dictionary<string, JsonElement>
Params = new JsonObject
{
["text"] = JsonSerializer.SerializeToElement(emojiText)
}
["text"] = emojiText
},
};
// Clear output and send message

View file

@ -289,7 +289,7 @@ public sealed class InMemoryTestSseServer : IAsyncDisposable
var jsonRpcResponse = new JsonRpcResponse()
{
Id = jsonRpcRequest.Id,
Result = new InitializeResult()
Result = JsonSerializer.SerializeToNode(new InitializeResult
{
ProtocolVersion = "2024-11-05",
Capabilities = new(),
@ -298,7 +298,7 @@ public sealed class InMemoryTestSseServer : IAsyncDisposable
Name = "ExampleServer",
Version = "1.0.0"
}
}
})
};
// Echo back to the HTTP response
@ -369,7 +369,7 @@ public sealed class InMemoryTestSseServer : IAsyncDisposable
{
JsonRpc = "2.0",
Method = "test/notification",
Params = new { message = content }
Params = JsonSerializer.SerializeToNode(new { message = content }),
};
var serialized = JsonSerializer.Serialize(notification);

View file

@ -1,7 +1,8 @@
using System.Threading.Channels;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Messages;
using ModelContextProtocol.Protocol.Transport;
using ModelContextProtocol.Protocol.Types;
using System.Text.Json;
using System.Threading.Channels;
namespace ModelContextProtocol.Tests.Utils;
@ -59,10 +60,10 @@ public class TestServerTransport : ITransport
await WriteMessageAsync(new JsonRpcResponse
{
Id = request.Id,
Result = new ModelContextProtocol.Protocol.Types.ListRootsResult
Result = JsonSerializer.SerializeToNode(new ListRootsResult
{
Roots = []
}
}),
}, cancellationToken);
}
@ -71,7 +72,7 @@ public class TestServerTransport : ITransport
await WriteMessageAsync(new JsonRpcResponse
{
Id = request.Id,
Result = new CreateMessageResult { Content = new(), Model = "model", Role = "role" }
Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = new(), Model = "model", Role = "role" }),
}, cancellationToken);
}