Skip to content

Commit dc2221b

Browse files
author
Esteban Solano
committed
+ code coverage for ClientOptions/Capabilities code
1 parent 2e1b931 commit dc2221b

File tree

2 files changed

+163
-62
lines changed

2 files changed

+163
-62
lines changed

tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs

+88-60
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.AI;
22
using Microsoft.Extensions.DependencyInjection;
33
using ModelContextProtocol.Client;
4+
using ModelContextProtocol.Protocol.Messages;
45
using ModelContextProtocol.Protocol.Transport;
56
using ModelContextProtocol.Protocol.Types;
67
using ModelContextProtocol.Server;
@@ -61,29 +62,39 @@ public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperat
6162
}
6263
],
6364
Temperature = temperature,
64-
MaxTokens = maxTokens
65+
MaxTokens = maxTokens,
66+
Meta = new RequestParamsMetadata
67+
{
68+
ProgressToken = new ProgressToken(),
69+
}
6570
};
6671

6772
var cancellationToken = CancellationToken.None;
68-
var expectedResponse = new ChatResponse
69-
{
70-
Messages = { new ChatMessage { Role = ChatRole.Assistant, Contents = { new TextContent("Hi there!") } } },
71-
ModelId = "test-model",
72-
FinishReason = ChatFinishReason.Stop
73-
};
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();
7485

7586
mockChatClient
76-
.Setup(client => client.GetResponseAsync(It.IsAny<IList<ChatMessage>>(), It.IsAny<ChatOptions>(), cancellationToken))
77-
.ReturnsAsync(expectedResponse);
87+
.Setup(client => client.GetStreamingResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), cancellationToken))
88+
.Returns(expectedResponse);
7889

7990
var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object);
8091

8192
// Act
82-
var result = await handler(requestParams, cancellationToken);
93+
var result = await handler(requestParams, Mock.Of<IProgress<ProgressNotificationValue>>(), cancellationToken);
8394

8495
// Assert
8596
Assert.NotNull(result);
86-
Assert.Equal("Hi there!", result.Content.Text);
97+
Assert.Equal("Hello, World!", result.Content.Text);
8798
Assert.Equal("test-model", result.Model);
8899
Assert.Equal("assistant", result.Role);
89100
Assert.Equal("endTurn", result.StopReason);
@@ -96,42 +107,49 @@ public async Task CreateSamplingHandler_ShouldHandleImageMessages()
96107
var mockChatClient = new Mock<IChatClient>();
97108
var requestParams = new CreateMessageRequestParams
98109
{
99-
Messages = new[]
100-
{
101-
new SamplingMessage
102-
{
103-
Role = Role.User,
104-
Content = new Content
110+
Messages =
111+
[
112+
new SamplingMessage
105113
{
106-
Type = "image",
107-
MimeType = "image/png",
108-
Data = Convert.ToBase64String(new byte[] { 1, 2, 3 })
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+
}
109121
}
110-
}
111-
},
122+
],
112123
MaxTokens = 100
113124
};
114-
var cancellationToken = CancellationToken.None;
115125

116-
var expectedResponse = new ChatResponse
117-
{
118-
Messages = { new ChatMessage { Role = ChatRole.Assistant, Contents = new[] { new TextContent("Image received!") } } },
119-
ModelId = "test-model",
120-
FinishReason = ChatFinishReason.Stop
121-
};
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();
122140

123141
mockChatClient
124-
.Setup(client => client.GetResponseAsync(It.IsAny<IList<ChatMessage>>(), It.IsAny<ChatOptions>(), cancellationToken))
125-
.ReturnsAsync(expectedResponse);
142+
.Setup(client => client.GetStreamingResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), cancellationToken))
143+
.Returns(expectedResponse);
126144

127145
var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object);
128146

129147
// Act
130-
var result = await handler(requestParams, cancellationToken);
148+
var result = await handler(requestParams, Mock.Of<IProgress<ProgressNotificationValue>>(), cancellationToken);
131149

132150
// Assert
133151
Assert.NotNull(result);
134-
Assert.Equal("Image received!", result.Content.Text);
152+
Assert.Equal(expectedData, result.Content.Data);
135153
Assert.Equal("test-model", result.Model);
136154
Assert.Equal("assistant", result.Role);
137155
Assert.Equal("endTurn", result.StopReason);
@@ -141,55 +159,65 @@ public async Task CreateSamplingHandler_ShouldHandleImageMessages()
141159
public async Task CreateSamplingHandler_ShouldHandleResourceMessages()
142160
{
143161
// Arrange
162+
const string data = "SGVsbG8sIFdvcmxkIQ==";
163+
string content = $"data:application/octet-stream;base64,{data}";
144164
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+
145172
var requestParams = new CreateMessageRequestParams
146173
{
147-
Messages = new[]
148-
{
149-
new SamplingMessage
150-
{
151-
Role = Role.User,
152-
Content = new Content
174+
Messages =
175+
[
176+
new SamplingMessage
153177
{
154-
Type = "resource",
155-
Resource = new ResourceContents
178+
Role = Role.User,
179+
Content = new Content
156180
{
157-
Text = "Resource text",
158-
Blob = Convert.ToBase64String(new byte[] { 4, 5, 6 }),
159-
MimeType = "application/octet-stream"
160-
}
181+
Type = "resource",
182+
Resource = resource
183+
},
161184
}
162-
}
163-
},
185+
],
164186
MaxTokens = 100
165187
};
166-
var cancellationToken = CancellationToken.None;
167188

168-
var expectedResponse = new ChatResponse
169-
{
170-
Messages = { new ChatMessage { Role = ChatRole.Assistant, Contents = new[] { new TextContent("Resource processed!") } } },
171-
ModelId = "test-model",
172-
FinishReason = ChatFinishReason.Stop
173-
};
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();
174203

175204
mockChatClient
176-
.Setup(client => client.GetResponseAsync(It.IsAny<IList<ChatMessage>>(), It.IsAny<ChatOptions>(), cancellationToken))
177-
.ReturnsAsync(expectedResponse);
205+
.Setup(client => client.GetStreamingResponseAsync(It.IsAny<IEnumerable<ChatMessage>>(), It.IsAny<ChatOptions>(), cancellationToken))
206+
.Returns(expectedResponse);
178207

179208
var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object);
180209

181210
// Act
182-
var result = await handler(requestParams, cancellationToken);
211+
var result = await handler(requestParams, Mock.Of<IProgress<ProgressNotificationValue>>(), cancellationToken);
183212

184213
// Assert
185214
Assert.NotNull(result);
186-
Assert.Equal("Resource processed!", result.Content.Text);
187215
Assert.Equal("test-model", result.Model);
188-
Assert.Equal("assistant", result.Role);
216+
Assert.Equal(ChatRole.Assistant.ToString(), result.Role);
189217
Assert.Equal("endTurn", result.StopReason);
190218
}
191219

192-
public ValueTask DisposeAsync()
220+
public async ValueTask DisposeAsync()
193221
{
194222
await _cts.CancelAsync();
195223

tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs

+75-2
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)