Skip to content

Commit e51c78e

Browse files
authored
SSE Transport Support (PederHP#13)
* First iteration * Tests * SSE support
1 parent e5abb4b commit e51c78e

17 files changed

+1200
-43
lines changed

README.MD

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# mcpdotnet
2-
[![NuGet version](https://img.shields.io/nuget/v/mcpdotnet.svg?style=flat)](https://www.nuget.org/packages/mcpdotnet/)
2+
[![NuGet version](https://img.shields.io/nuget/v/mcpdotnet.svg)](https://www.nuget.org/packages/mcpdotnet/)
33

44
A .NET client implementation of the Model Context Protocol (MCP), enabling .NET applications to connect to and interact with MCP servers.
55

@@ -12,31 +12,29 @@ For more information about MCP:
1212
- [Protocol Specification](https://spec.modelcontextprotocol.io/)
1313
- [GitHub Organization](https://github.com/modelcontextprotocol)
1414

15-
## Status
15+
## Design Goals
1616

17-
This project is in early development. While functional, it may undergo significant changes as the protocol and implementation evolve.
18-
19-
### Current Limitations
20-
- Only stdio transport is currently supported (HTTP/SSE transport coming soon)
21-
- Client library only - server implementation support may be added in the future
22-
- Provider-agnostic implementation focused purely on the MCP protocol
23-
- Applications using this library need to implement their own integration with LLM APIs/clients
24-
25-
The library's current focus is on implementing the core MCP protocol, with the main utility being the ability to launch and connect to MCP servers based on configuration objects.
17+
This library aims to provide a clean, specification-compliant implementation of the MCP protocol, with minimal additional abstraction. While transport implementations necessarily include additional code, they follow patterns established by the official SDKs where possible.
2618

2719
## Features
2820

2921
- MCP client implementation for .NET applications
30-
- Support for stdio transport
31-
- Support for all capabilities: Tool, Resource, Prompt, Sampling, Roots
22+
- Support for stdio and SSE transports
23+
- Support for all MCP capabilities: Tool, Resource, Prompt, Sampling, Roots
3224
- Support for pagination and notifications
3325
- Async/await pattern throughout
3426
- Comprehensive logging support
3527
- Compatible with .NET 8.0 and later
3628

3729
## Getting Started
3830

39-
To create a client you need to instantiate a factory with configuration objects:
31+
To use mcpdotnet, first install it via NuGet:
32+
33+
```powershell
34+
dotnet add package mcpdotnet
35+
```
36+
37+
Then create a client and start using tools, or other capabilities, from the servers you configure:
4038
```csharp
4139
var options = new McpClientOptions()
4240
{ ClientInfo = new() { Name = "TestClient", Version = "1.0.0" } };
@@ -58,14 +56,12 @@ var factory = new McpClientFactory(
5856
options,
5957
NullLoggerFactory.Instance
6058
);
61-
```
62-
63-
You can then retrieve an IMcpClient reference using the `id` of a server from the configuration, and use `ListToolsAsync` and `CallToolAsync` to integrate with your LLM-based code.
6459

65-
```csharp
6660
var client = await factory.GetClientAsync("everything");
61+
6762
// Get the list of tools, for passing to an LLM
6863
var tools = await client.ListToolsAsync();
64+
6965
// Execute a tool, in practice this would normally be driven by LLM tool invocations
7066
var result = await client.CallToolAsync(
7167
"echo",
@@ -75,6 +71,7 @@ var result = await client.CallToolAsync(
7571
},
7672
CancellationToken.None
7773
);
74+
7875
// echo always returns one and only one text content object
7976
Console.WriteLine(result.Content.FirstOrDefault(c => c.Type == "text").Text);
8077
```
@@ -85,6 +82,16 @@ It is also highly recommended that you pass a proper LoggerFactory instance to t
8582

8683
Additional examples and documentation will be added as in the near future.
8784

85+
## Roadmap
86+
87+
- Expand documentation with detailed guides for:
88+
- Advanced scenarios (Sampling, Resources, Prompts)
89+
- Transport configuration
90+
- Error handling and recovery
91+
- Increase test coverage
92+
- Add example applications
93+
- Performance optimization and profiling
94+
8895
## License
8996

9097
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

TODO.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
# TODOs for mcpdotnet
22

33
## Integration Testing
4-
- [ ] Add comprehensive Roots feature testing once reference servers implement support
5-
- (Contribute PR to everything server adding `listRoots` tool)
6-
- Add integration tests similar to Sampling tests
7-
- Verify roots notification handling
4+
- Add comprehensive Roots feature testing once reference servers implement support
5+
- Consider using everything server adding `listRoots` tool)
6+
- Add integration tests similar to Sampling tests
7+
- Verify roots notification handling
8+
- Expand SSE test server to support all features or use a reference SSE server if one becomes available
89

910
## Future Improvements
10-
- [ ] Add HTTPS/SSE transport support
11+
- [X] Add HTTPS/SSE transport support
12+
- [ ] Add more example applications showing different capabilities
13+
- [ ] Add comprehensive documentation for advanced scenarios
14+
- [ ] Profile and optimize performance
15+
- [ ] Linux support in stdio transport
1116

1217
## Code Improvements
1318
- [ ] Consolidate notification handling in McpClient to reduce duplication between SendNotificationAsync overloads

src/mcpdotnet.Tests/IntegrationTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using McpDotNet.Configuration;
33
using McpDotNet.Protocol.Messages;
44
using McpDotNet.Protocol.Types;
5-
using Microsoft.Extensions.Logging;
65

76
namespace McpDotNet.Tests;
87

src/mcpdotnet.Tests/McpClientFactoryTests.cs

Lines changed: 162 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
using Castle.Core.Logging;
21
using McpDotNet.Client;
32
using McpDotNet.Configuration;
43
using McpDotNet.Protocol.Messages;
54
using McpDotNet.Protocol.Transport;
65
using Microsoft.Extensions.Logging.Abstractions;
76
using Moq;
87
using System.Threading.Channels;
9-
using Xunit;
108

119
namespace McpDotNet.Tests.Client;
1210

@@ -192,23 +190,177 @@ public void Constructor_WithDuplicateServerIds_ThrowsArgumentException()
192190
}
193191

194192
[Fact]
195-
public async Task GetClientAsync_WithHttpTransport_ThrowsNotImplementedException()
193+
public async Task GetClientAsync_WithSseTransport_CanCreateClient()
196194
{
197195
// Arrange
198196
var config = new McpServerConfig
199197
{
200198
Id = "test-server",
201199
Name = "Test Server",
202-
TransportType = "http",
200+
TransportType = "sse",
203201
Location = "http://localhost:8080"
204202
};
203+
204+
// Create a mock transport
205+
var mockTransport = new Mock<IMcpTransport>();
206+
mockTransport.Setup(t => t.ConnectAsync(It.IsAny<CancellationToken>()))
207+
.Returns(Task.CompletedTask);
208+
mockTransport.Setup(t => t.IsConnected).Returns(true);
209+
mockTransport.Setup(t => t.MessageReader).Returns(Mock.Of<ChannelReader<IJsonRpcMessage>>());
205210

206-
var factory = new McpClientFactory([config], _defaultOptions,
207-
NullLoggerFactory.Instance);
211+
// Create a mock client
212+
var mockClient = new Mock<IMcpClient>();
213+
mockClient.Setup(c => c.ConnectAsync(It.IsAny<CancellationToken>()))
214+
.Returns(Task.CompletedTask);
215+
mockClient.Setup(c => c.IsInitialized).Returns(true);
208216

209-
// Act & Assert
210-
await Assert.ThrowsAsync<ArgumentException>(
211-
() => factory.GetClientAsync("test-server")
217+
// Inject the mock transport into the factory
218+
var factory = new McpClientFactory(
219+
[config],
220+
_defaultOptions,
221+
NullLoggerFactory.Instance,
222+
transportFactoryMethod: _ => mockTransport.Object,
223+
clientFactoryMethod: (_, _, _) => mockClient.Object
212224
);
225+
226+
// Act
227+
var client = await factory.GetClientAsync("test-server");
228+
229+
// Assert
230+
Assert.NotNull(client);
231+
}
232+
233+
[Fact]
234+
public void McpFactory_WithSse_CreatesCorrectTransportOptions()
235+
{
236+
// Arrange
237+
var config = new McpServerConfig
238+
{
239+
Id = "test-server",
240+
Name = "Test Server",
241+
TransportType = "sse",
242+
Location = "http://localhost:8080",
243+
TransportOptions = new Dictionary<string, string>
244+
{
245+
["connectionTimeout"] = "10",
246+
["maxReconnectAttempts"] = "2",
247+
["reconnectDelay"] = "5",
248+
["header.test"] = "the_header_value"
249+
}
250+
};
251+
252+
// Act
253+
var factory = new McpClientFactory(
254+
[config],
255+
_defaultOptions,
256+
NullLoggerFactory.Instance
257+
);
258+
259+
var transport = factory.TransportFactoryMethod(config) as SseTransport;
260+
261+
// Assert
262+
Assert.NotNull(transport);
263+
Assert.True(transport.Options.ConnectionTimeout == TimeSpan.FromSeconds(10));
264+
Assert.True(transport.Options.MaxReconnectAttempts == 2);
265+
Assert.True(transport.Options.ReconnectDelay == TimeSpan.FromSeconds(5));
266+
Assert.True(transport.Options.AdditionalHeaders["test"] == "the_header_value");
267+
}
268+
269+
[Fact]
270+
public void McpFactory_WithSseAndNoOptions_CreatesDefaultTransportOptions()
271+
{
272+
// Arrange
273+
var config = new McpServerConfig
274+
{
275+
Id = "test-server",
276+
Name = "Test Server",
277+
TransportType = "sse",
278+
Location = "http://localhost:8080"
279+
};
280+
281+
var defaultOptions = new SseTransportOptions();
282+
283+
// Act
284+
var factory = new McpClientFactory(
285+
[config],
286+
_defaultOptions,
287+
NullLoggerFactory.Instance
288+
);
289+
290+
var transport = factory.TransportFactoryMethod(config) as SseTransport;
291+
292+
// Assert
293+
Assert.NotNull(transport);
294+
Assert.True(transport.Options.ConnectionTimeout == defaultOptions.ConnectionTimeout);
295+
Assert.True(transport.Options.MaxReconnectAttempts == defaultOptions.MaxReconnectAttempts);
296+
Assert.True(transport.Options.ReconnectDelay == defaultOptions.ReconnectDelay);
297+
Assert.True(transport.Options.AdditionalHeaders == null && defaultOptions.AdditionalHeaders == null);
298+
}
299+
300+
[Fact]
301+
public void McpFactory_WithSseAndMissingOptions_CreatesCorrectTransportOptions()
302+
{
303+
// Arrange
304+
var config = new McpServerConfig
305+
{
306+
Id = "test-server",
307+
Name = "Test Server",
308+
TransportType = "sse",
309+
Location = "http://localhost:8080",
310+
TransportOptions = new Dictionary<string, string>
311+
{
312+
["connectionTimeout"] = "10",
313+
["header.test"] = "the_header_value"
314+
}
315+
};
316+
317+
var defaultOptions = new SseTransportOptions();
318+
319+
// Act
320+
var factory = new McpClientFactory(
321+
[config],
322+
_defaultOptions,
323+
NullLoggerFactory.Instance
324+
);
325+
326+
var transport = factory.TransportFactoryMethod(config) as SseTransport;
327+
328+
// Assert
329+
Assert.NotNull(transport);
330+
Assert.True(transport.Options.ConnectionTimeout == TimeSpan.FromSeconds(10));
331+
Assert.True(transport.Options.MaxReconnectAttempts == defaultOptions.MaxReconnectAttempts);
332+
Assert.True(transport.Options.ReconnectDelay == defaultOptions.ReconnectDelay);
333+
Assert.True(transport.Options.AdditionalHeaders["test"] == "the_header_value");
334+
}
335+
336+
[Theory]
337+
[InlineData("connectionTimeout", "not_a_number")]
338+
[InlineData("maxReconnectAttempts", "invalid")]
339+
[InlineData("reconnectDelay", "bad_value")]
340+
public void McpFactory_WithInvalidTransportOptions_ThrowsFormatException(string key, string value)
341+
{
342+
// arrange
343+
var config = new McpServerConfig
344+
{
345+
Id = "test-server",
346+
Name = "Test Server",
347+
TransportType = "sse",
348+
Location = "http://localhost:8080",
349+
TransportOptions = new Dictionary<string, string>
350+
{
351+
[key] = value
352+
}
353+
};
354+
355+
// Act
356+
var factory = new McpClientFactory(
357+
[config],
358+
_defaultOptions,
359+
NullLoggerFactory.Instance
360+
);
361+
362+
// act & assert
363+
Assert.Throws<FormatException>(() =>
364+
factory.TransportFactoryMethod(config));
213365
}
214-
}
366+
}

0 commit comments

Comments
 (0)