Skip to content

Commit d555fba

Browse files
Copilotstephentoub
andcommitted
Add private protected constructors to McpClient and McpServer to prevent external derivation
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent 220a02d commit d555fba

4 files changed

Lines changed: 55 additions & 59 deletions

File tree

src/ModelContextProtocol.Core/Client/McpClient.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ namespace ModelContextProtocol.Client;
77
/// </summary>
88
public abstract partial class McpClient : McpSession
99
{
10+
/// <summary>Initializes a new instance of the <see cref="McpClient"/> class.</summary>
11+
private protected McpClient()
12+
{
13+
}
1014
/// <summary>
1115
/// Gets the capabilities supported by the connected server.
1216
/// </summary>

src/ModelContextProtocol.Core/Server/McpServer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ namespace ModelContextProtocol.Server;
77
/// </summary>
88
public abstract partial class McpServer : McpSession
99
{
10+
/// <summary>Initializes a new instance of the <see cref="McpServer"/> class.</summary>
11+
private protected McpServer()
12+
{
13+
}
1014
/// <summary>
1115
/// Gets the capabilities supported by the client.
1216
/// </summary>

tests/Common/Utils/TestServerTransport.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,11 @@ await WriteMessageAsync(new JsonRpcResponse
9393
else
9494
{
9595
// Return a normal sampling response
96+
var result = MockSamplingResult ?? new CreateMessageResult { Content = [new TextContentBlock { Text = "" }], Model = "model" };
9697
await WriteMessageAsync(new JsonRpcResponse
9798
{
9899
Id = request.Id,
99-
Result = JsonSerializer.SerializeToNode(new CreateMessageResult { Content = [new TextContentBlock { Text = "" }], Model = "model" }, McpJsonUtilities.DefaultOptions),
100+
Result = JsonSerializer.SerializeToNode(result, McpJsonUtilities.DefaultOptions),
100101
}, cancellationToken);
101102
}
102103
}
@@ -125,6 +126,12 @@ await WriteMessageAsync(new JsonRpcResponse
125126
}
126127
}
127128

129+
/// <summary>
130+
/// Gets or sets the sampling result to return from sampling/createMessage requests.
131+
/// When null, a default sampling response is returned.
132+
/// </summary>
133+
public CreateMessageResult? MockSamplingResult { get; set; }
134+
128135
/// <summary>
129136
/// Gets or sets the task to return from tasks/get requests.
130137
/// </summary>

tests/ModelContextProtocol.Tests/Server/McpServerTests.cs

Lines changed: 39 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -807,15 +807,36 @@ private async Task Succeeds_Even_If_No_Handler_Assigned(ServerCapabilities serve
807807
[Fact]
808808
public async Task AsSamplingChatClient_NoSamplingSupport_Throws()
809809
{
810-
await using var server = new TestServerForIChatClient(supportsSampling: false);
810+
await using var transport = new TestServerTransport();
811+
await using var server = McpServer.Create(transport, _options, LoggerFactory);
811812

812813
Assert.Throws<InvalidOperationException>(() => server.AsSamplingChatClient());
813814
}
814815

815816
[Fact]
816817
public async Task AsSamplingChatClient_HandlesRequestResponse()
817818
{
818-
await using var server = new TestServerForIChatClient(supportsSampling: true);
819+
await using var transport = new TestServerTransport();
820+
transport.MockSamplingResult = new CreateMessageResult
821+
{
822+
Content = [new TextContentBlock { Text = "The Eiffel Tower." }],
823+
Model = "amazingmodel",
824+
Role = Role.Assistant,
825+
StopReason = "endTurn",
826+
};
827+
await using var server = McpServer.Create(transport, _options, LoggerFactory);
828+
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
829+
await InitializeServerAsync(transport, new ClientCapabilities { Sampling = new SamplingCapability() }, TestContext.Current.CancellationToken);
830+
831+
// Capture the sampling request for validation
832+
CreateMessageRequestParams? capturedParams = null;
833+
transport.OnMessageSent = (message) =>
834+
{
835+
if (message is JsonRpcRequest request && request.Method == RequestMethods.SamplingCreateMessage)
836+
{
837+
capturedParams = JsonSerializer.Deserialize<CreateMessageRequestParams>(request.Params, McpJsonUtilities.DefaultOptions);
838+
}
839+
};
819840

820841
IChatClient client = server.AsSamplingChatClient();
821842

@@ -839,6 +860,22 @@ public async Task AsSamplingChatClient_HandlesRequestResponse()
839860
Assert.Single(response.Messages);
840861
Assert.Equal("The Eiffel Tower.", response.Text);
841862
Assert.Equal(ChatRole.Assistant, response.Messages[0].Role);
863+
864+
// Validate the request parameters
865+
Assert.NotNull(capturedParams);
866+
Assert.Equal(0.75f, capturedParams.Temperature);
867+
Assert.Equal(42, capturedParams.MaxTokens);
868+
Assert.Equal(["."], capturedParams.StopSequences);
869+
Assert.Null(capturedParams.IncludeContext);
870+
Assert.Null(capturedParams.Metadata);
871+
Assert.Null(capturedParams.ModelPreferences);
872+
Assert.Equal($"You are a helpful assistant.{Environment.NewLine}More system stuff.", capturedParams.SystemPrompt);
873+
Assert.Equal(2, capturedParams.Messages.Count);
874+
Assert.Equal("I am going to France.", Assert.IsType<TextContentBlock>(Assert.Single(capturedParams.Messages[0].Content)).Text);
875+
Assert.Equal("What is the most famous tower in Paris?", Assert.IsType<TextContentBlock>(Assert.Single(capturedParams.Messages[1].Content)).Text);
876+
877+
await transport.DisposeAsync();
878+
await runTask;
842879
}
843880

844881
[Fact]
@@ -890,62 +927,6 @@ private static async Task InitializeServerAsync(TestServerTransport transport, C
890927
await tcs.Task.WaitAsync(TestConstants.DefaultTimeout, cancellationToken);
891928
}
892929

893-
private sealed class TestServerForIChatClient(bool supportsSampling) : McpServer
894-
{
895-
public override ClientCapabilities? ClientCapabilities =>
896-
supportsSampling ? new ClientCapabilities { Sampling = new SamplingCapability() } :
897-
null;
898-
899-
public override McpServerOptions ServerOptions => new();
900-
901-
public override Task<JsonRpcResponse> SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken)
902-
{
903-
CreateMessageRequestParams? rp = JsonSerializer.Deserialize<CreateMessageRequestParams>(request.Params, McpJsonUtilities.DefaultOptions);
904-
905-
Assert.NotNull(rp);
906-
Assert.Equal(0.75f, rp.Temperature);
907-
Assert.Equal(42, rp.MaxTokens);
908-
Assert.Equal(["."], rp.StopSequences);
909-
Assert.Null(rp.IncludeContext);
910-
Assert.Null(rp.Metadata);
911-
Assert.Null(rp.ModelPreferences);
912-
913-
Assert.Equal($"You are a helpful assistant.{Environment.NewLine}More system stuff.", rp.SystemPrompt);
914-
915-
Assert.Equal(2, rp.Messages.Count);
916-
Assert.Equal("I am going to France.", Assert.IsType<TextContentBlock>(Assert.Single(rp.Messages[0].Content)).Text);
917-
Assert.Equal("What is the most famous tower in Paris?", Assert.IsType<TextContentBlock>(Assert.Single(rp.Messages[1].Content)).Text);
918-
919-
CreateMessageResult result = new()
920-
{
921-
Content = [new TextContentBlock { Text = "The Eiffel Tower." }],
922-
Model = "amazingmodel",
923-
Role = Role.Assistant,
924-
StopReason = "endTurn",
925-
};
926-
927-
return Task.FromResult(new JsonRpcResponse
928-
{
929-
Id = new RequestId("0"),
930-
Result = JsonSerializer.SerializeToNode(result, McpJsonUtilities.DefaultOptions),
931-
});
932-
}
933-
934-
public override ValueTask DisposeAsync() => default;
935-
936-
public override string? SessionId => throw new NotImplementedException();
937-
public override string? NegotiatedProtocolVersion => throw new NotImplementedException();
938-
public override Implementation? ClientInfo => throw new NotImplementedException();
939-
public override IServiceProvider? Services => throw new NotImplementedException();
940-
public override LoggingLevel? LoggingLevel => throw new NotImplementedException();
941-
public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) =>
942-
throw new NotImplementedException();
943-
public override Task RunAsync(CancellationToken cancellationToken = default) =>
944-
throw new NotImplementedException();
945-
public override IAsyncDisposable RegisterNotificationHandler(string method, Func<JsonRpcNotification, CancellationToken, ValueTask> handler) =>
946-
throw new NotImplementedException();
947-
}
948-
949930
[Fact]
950931
public async Task NotifyProgress_Should_Be_Handled()
951932
{

0 commit comments

Comments
 (0)