Skip to content

Commit c70dde3

Browse files
stvansolanoEsteban Solanostephentoub
authored
[UnitTests] Add more UTs (#139)
* + Tests for McpClientExtensions code * + code coverage for ClientOptions/Capabilities code --------- Co-authored-by: Esteban Solano <[email protected]> Co-authored-by: Stephen Toub <[email protected]>
1 parent f652b1a commit c70dde3

File tree

2 files changed

+252
-2
lines changed

2 files changed

+252
-2
lines changed

tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
using Microsoft.Extensions.AI;
12
using Microsoft.Extensions.DependencyInjection;
23
using ModelContextProtocol.Client;
4+
using ModelContextProtocol.Protocol.Messages;
35
using ModelContextProtocol.Protocol.Transport;
46
using ModelContextProtocol.Protocol.Types;
57
using ModelContextProtocol.Server;
68
using ModelContextProtocol.Tests.Utils;
9+
using Moq;
710
using System.IO.Pipelines;
811
using System.Text.Json;
912
using System.Text.Json.Serialization.Metadata;
@@ -40,6 +43,180 @@ public McpClientExtensionsTests(ITestOutputHelper outputHelper)
4043
_serverTask = server.RunAsync(cancellationToken: _cts.Token);
4144
}
4245

46+
[Theory]
47+
[InlineData(null, null)]
48+
[InlineData(0.7f, 50)]
49+
[InlineData(1.0f, 100)]
50+
public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperature, int? maxTokens)
51+
{
52+
// Arrange
53+
var mockChatClient = new Mock<IChatClient>();
54+
var requestParams = new CreateMessageRequestParams
55+
{
56+
Messages =
57+
[
58+
new SamplingMessage
59+
{
60+
Role = Role.User,
61+
Content = new Content { Type = "text", Text = "Hello" }
62+
}
63+
],
64+
Temperature = temperature,
65+
MaxTokens = maxTokens,
66+
Meta = new RequestParamsMetadata
67+
{
68+
ProgressToken = new ProgressToken(),
69+
}
70+
};
71+
72+
var cancellationToken = CancellationToken.None;
73+
var expectedResponse = new[] {
74+
new ChatResponseUpdate
75+
{
76+
ModelId = "test-model",
77+
FinishReason = ChatFinishReason.Stop,
78+
Role = ChatRole.Assistant,
79+
Contents =
80+
[
81+
new TextContent("Hello, World!") { RawRepresentation = "Hello, World!" }
82+
]
83+
}
84+
}.ToAsyncEnumerable();
85+
86+
mockChatClient
87+
.Setup(client => client.GetStreamingResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), cancellationToken))
88+
.Returns(expectedResponse);
89+
90+
var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object);
91+
92+
// Act
93+
var result = await handler(requestParams, Mock.Of<IProgress<ProgressNotificationValue>>(), cancellationToken);
94+
95+
// Assert
96+
Assert.NotNull(result);
97+
Assert.Equal("Hello, World!", result.Content.Text);
98+
Assert.Equal("test-model", result.Model);
99+
Assert.Equal("assistant", result.Role);
100+
Assert.Equal("endTurn", result.StopReason);
101+
}
102+
103+
[Fact]
104+
public async Task CreateSamplingHandler_ShouldHandleImageMessages()
105+
{
106+
// Arrange
107+
var mockChatClient = new Mock<IChatClient>();
108+
var requestParams = new CreateMessageRequestParams
109+
{
110+
Messages =
111+
[
112+
new SamplingMessage
113+
{
114+
Role = Role.User,
115+
Content = new Content
116+
{
117+
Type = "image",
118+
MimeType = "image/png",
119+
Data = Convert.ToBase64String(new byte[] { 1, 2, 3 })
120+
}
121+
}
122+
],
123+
MaxTokens = 100
124+
};
125+
126+
const string expectedData = "SGVsbG8sIFdvcmxkIQ==";
127+
var cancellationToken = CancellationToken.None;
128+
var expectedResponse = new[] {
129+
new ChatResponseUpdate
130+
{
131+
ModelId = "test-model",
132+
FinishReason = ChatFinishReason.Stop,
133+
Role = ChatRole.Assistant,
134+
Contents =
135+
[
136+
new DataContent($"data:image/png;base64,{expectedData}") { RawRepresentation = "Hello, World!" }
137+
]
138+
}
139+
}.ToAsyncEnumerable();
140+
141+
mockChatClient
142+
.Setup(client => client.GetStreamingResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), cancellationToken))
143+
.Returns(expectedResponse);
144+
145+
var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object);
146+
147+
// Act
148+
var result = await handler(requestParams, Mock.Of<IProgress<ProgressNotificationValue>>(), cancellationToken);
149+
150+
// Assert
151+
Assert.NotNull(result);
152+
Assert.Equal(expectedData, result.Content.Data);
153+
Assert.Equal("test-model", result.Model);
154+
Assert.Equal("assistant", result.Role);
155+
Assert.Equal("endTurn", result.StopReason);
156+
}
157+
158+
[Fact]
159+
public async Task CreateSamplingHandler_ShouldHandleResourceMessages()
160+
{
161+
// Arrange
162+
const string data = "SGVsbG8sIFdvcmxkIQ==";
163+
string content = $"data:application/octet-stream;base64,{data}";
164+
var mockChatClient = new Mock<IChatClient>();
165+
var resource = new BlobResourceContents
166+
{
167+
Blob = data,
168+
MimeType = "application/octet-stream",
169+
Uri = "data:application/octet-stream"
170+
};
171+
172+
var requestParams = new CreateMessageRequestParams
173+
{
174+
Messages =
175+
[
176+
new SamplingMessage
177+
{
178+
Role = Role.User,
179+
Content = new Content
180+
{
181+
Type = "resource",
182+
Resource = resource
183+
},
184+
}
185+
],
186+
MaxTokens = 100
187+
};
188+
189+
var cancellationToken = CancellationToken.None;
190+
var expectedResponse = new[] {
191+
new ChatResponseUpdate
192+
{
193+
ModelId = "test-model",
194+
FinishReason = ChatFinishReason.Stop,
195+
AuthorName = "bot",
196+
Role = ChatRole.Assistant,
197+
Contents =
198+
[
199+
resource.ToAIContent()
200+
]
201+
}
202+
}.ToAsyncEnumerable();
203+
204+
mockChatClient
205+
.Setup(client => client.GetStreamingResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), cancellationToken))
206+
.Returns(expectedResponse);
207+
208+
var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object);
209+
210+
// Act
211+
var result = await handler(requestParams, Mock.Of<IProgress<ProgressNotificationValue>>(), cancellationToken);
212+
213+
// Assert
214+
Assert.NotNull(result);
215+
Assert.Equal("test-model", result.Model);
216+
Assert.Equal(ChatRole.Assistant.ToString(), result.Role);
217+
Assert.Equal("endTurn", result.StopReason);
218+
}
219+
43220
public async ValueTask DisposeAsync()
44221
{
45222
await _cts.CancelAsync();

tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
using Microsoft.Extensions.Logging;
12
using ModelContextProtocol.Client;
23
using ModelContextProtocol.Protocol.Messages;
34
using ModelContextProtocol.Protocol.Transport;
45
using ModelContextProtocol.Protocol.Types;
6+
using Moq;
57
using System.Text.Json;
68
using System.Threading.Channels;
79

@@ -187,7 +189,68 @@ public async Task McpFactory_WithInvalidTransportOptions_ThrowsFormatException(s
187189
await Assert.ThrowsAsync<ArgumentException>(() => McpClientFactory.CreateAsync(config, _defaultOptions, cancellationToken: TestContext.Current.CancellationToken));
188190
}
189191

190-
private sealed class NopTransport : ITransport, IClientTransport
192+
[Theory]
193+
[InlineData(typeof(NopTransport))]
194+
[InlineData(typeof(FailureTransport))]
195+
public async Task CreateAsync_WithCapabilitiesOptions(Type transportType)
196+
{
197+
// Arrange
198+
var serverConfig = new McpServerConfig
199+
{
200+
Id = "TestServer",
201+
Name = "TestServer",
202+
TransportType = "stdio",
203+
Location = "test-location"
204+
};
205+
206+
var clientOptions = new McpClientOptions
207+
{
208+
ClientInfo = new Implementation
209+
{
210+
Name = "TestClient",
211+
Version = "1.0.0.0"
212+
},
213+
Capabilities = new ClientCapabilities
214+
{
215+
Sampling = new SamplingCapability
216+
{
217+
SamplingHandler = (c, p, t) => Task.FromResult(
218+
new CreateMessageResult {
219+
Content = new Content { Text = "result" },
220+
Model = "test-model",
221+
Role = "test-role",
222+
StopReason = "endTurn"
223+
}),
224+
},
225+
Roots = new RootsCapability
226+
{
227+
ListChanged = true,
228+
RootsHandler = (t, r) => Task.FromResult(new ListRootsResult { Roots = [] }),
229+
}
230+
}
231+
};
232+
233+
var clientTransport = (IClientTransport?)Activator.CreateInstance(transportType);
234+
IMcpClient? client = null;
235+
236+
var actionTask = McpClientFactory.CreateAsync(serverConfig, clientOptions, (config, logger) => clientTransport ?? new NopTransport(), new Mock<ILoggerFactory>().Object, CancellationToken.None);
237+
238+
// Act
239+
if (clientTransport is FailureTransport)
240+
{
241+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async() => await actionTask);
242+
Assert.Equal(FailureTransport.ExpectedMessage, exception.Message);
243+
}
244+
else
245+
{
246+
client = await actionTask;
247+
248+
// Assert
249+
Assert.NotNull(client);
250+
}
251+
}
252+
253+
private class NopTransport : ITransport, IClientTransport
191254
{
192255
private readonly Channel<IJsonRpcMessage> _channel = Channel.CreateUnbounded<IJsonRpcMessage>();
193256

@@ -199,7 +262,7 @@ private sealed class NopTransport : ITransport, IClientTransport
199262

200263
public ValueTask DisposeAsync() => default;
201264

202-
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
265+
public virtual Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
203266
{
204267
switch (message)
205268
{
@@ -224,4 +287,14 @@ public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancella
224287
return Task.CompletedTask;
225288
}
226289
}
290+
291+
private sealed class FailureTransport : NopTransport
292+
{
293+
public const string ExpectedMessage = "Something failed";
294+
295+
public override Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default)
296+
{
297+
throw new InvalidOperationException(ExpectedMessage);
298+
}
299+
}
227300
}

0 commit comments

Comments
 (0)