parent
4b8df6638e
commit
e5abb4bb8e
11 changed files with 262 additions and 8 deletions
|
|
@ -17,7 +17,6 @@ For more information about MCP:
|
|||
This project is in early development. While functional, it may undergo significant changes as the protocol and implementation evolve.
|
||||
|
||||
### Current Limitations
|
||||
- Only supports the Tools, Resources, Prompts, Sampling capabilities (Roots not yet implemented)
|
||||
- Only stdio transport is currently supported (HTTP/SSE transport coming soon)
|
||||
- Client library only - server implementation support may be added in the future
|
||||
- Provider-agnostic implementation focused purely on the MCP protocol
|
||||
|
|
@ -29,7 +28,7 @@ The library's current focus is on implementing the core MCP protocol, with the m
|
|||
|
||||
- MCP client implementation for .NET applications
|
||||
- Support for stdio transport
|
||||
- Support for Tool, Resource, Prompt, Sampling capabilities
|
||||
- Support for all capabilities: Tool, Resource, Prompt, Sampling, Roots
|
||||
- Support for pagination and notifications
|
||||
- Async/await pattern throughout
|
||||
- Comprehensive logging support
|
||||
|
|
|
|||
13
TODO.md
Normal file
13
TODO.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# TODOs for mcpdotnet
|
||||
|
||||
## Integration Testing
|
||||
- [ ] Add comprehensive Roots feature testing once reference servers implement support
|
||||
- (Contribute PR to everything server adding `listRoots` tool)
|
||||
- Add integration tests similar to Sampling tests
|
||||
- Verify roots notification handling
|
||||
|
||||
## Future Improvements
|
||||
- [ ] Add HTTPS/SSE transport support
|
||||
|
||||
## Code Improvements
|
||||
- [ ] Consolidate notification handling in McpClient to reduce duplication between SendNotificationAsync overloads
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using McpDotNet.Client;
|
||||
using McpDotNet.Configuration;
|
||||
using McpDotNet.Protocol.Messages;
|
||||
using McpDotNet.Protocol.Types;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -215,7 +216,7 @@ public class IntegrationTests : IClassFixture<IntegrationTestFixture>
|
|||
// Set up the sampling handler
|
||||
int samplingHandlerCalls = 0;
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
client.SamplingHandler = async (_) =>
|
||||
client.SamplingHandler = async (_,_) =>
|
||||
{
|
||||
samplingHandlerCalls++;
|
||||
return new CreateMessageResult
|
||||
|
|
@ -247,4 +248,98 @@ public class IntegrationTests : IClassFixture<IntegrationTestFixture>
|
|||
Assert.Equal("text", textContent.Type);
|
||||
Assert.False(string.IsNullOrEmpty(textContent.Text));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Roots_Stdio_EverythingServer()
|
||||
{
|
||||
// arrange
|
||||
var config = new McpServerConfig
|
||||
{
|
||||
Id = "everything",
|
||||
Name = "everything",
|
||||
TransportType = "stdio",
|
||||
TransportOptions = new Dictionary<string, string>
|
||||
{
|
||||
["command"] = "npx",
|
||||
["arguments"] = "-y @modelcontextprotocol/server-everything"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new McpClientOptions
|
||||
{
|
||||
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" },
|
||||
Capabilities = new ClientCapabilities
|
||||
{
|
||||
Roots = new()
|
||||
}
|
||||
};
|
||||
|
||||
var rootsHandlerCalls = 0;
|
||||
var testRoots = new List<Root>
|
||||
{
|
||||
new() { Uri = "file:///test/root1", Name = "Test Root 1" },
|
||||
new() { Uri = "file:///test/root2", Name = "Test Root 2" }
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory(
|
||||
[config],
|
||||
options,
|
||||
_fixture.LoggerFactory
|
||||
);
|
||||
var client = await factory.GetClientAsync("everything");
|
||||
|
||||
// Set up the roots handler
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
client.RootsHandler = async (request, ct) =>
|
||||
{
|
||||
rootsHandlerCalls++;
|
||||
return new ListRootsResult
|
||||
{
|
||||
Roots = testRoots
|
||||
};
|
||||
};
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
// Connect
|
||||
await client.ConnectAsync(CancellationToken.None);
|
||||
|
||||
// assert
|
||||
// nothing to assert, no servers implement roots, so we if no exception is thrown, it's a success
|
||||
Assert.True(true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Notifications_Stdio_EverythingServer()
|
||||
{
|
||||
// arrange
|
||||
var config = new McpServerConfig
|
||||
{
|
||||
Id = "everything",
|
||||
Name = "everything",
|
||||
TransportType = "stdio",
|
||||
TransportOptions = new Dictionary<string, string>
|
||||
{
|
||||
["command"] = "npx",
|
||||
["arguments"] = "-y @modelcontextprotocol/server-everything"
|
||||
}
|
||||
};
|
||||
|
||||
var options = new McpClientOptions
|
||||
{
|
||||
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }
|
||||
};
|
||||
|
||||
var factory = new McpClientFactory([config], options, _fixture.LoggerFactory);
|
||||
var client = await factory.GetClientAsync("everything");
|
||||
|
||||
await client.ConnectAsync();
|
||||
|
||||
// Verify we can send notifications without errors
|
||||
await client.SendNotificationAsync(NotificationMethods.RootsUpdatedNotification);
|
||||
await client.SendNotificationAsync("test/notification", new { test = true });
|
||||
|
||||
// assert
|
||||
// no response to check, if no exception is thrown, it's a success
|
||||
Assert.True(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,14 @@ public interface IMcpClient : IAsyncDisposable
|
|||
Implementation? ServerInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for the server's sampling requests.
|
||||
/// Gets or sets the handler for server sampling requests.
|
||||
/// </summary>
|
||||
Func<CreateMessageRequestParams, Task<CreateMessageResult>>? SamplingHandler { get; set; }
|
||||
Func<CreateMessageRequestParams, CancellationToken, Task<CreateMessageResult>>? SamplingHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the handler for providing root URIs to servers.
|
||||
/// </summary>
|
||||
Func<ListRootsRequestParams, CancellationToken, Task<ListRootsResult>>? RootsHandler { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Establishes a connection to the server.
|
||||
|
|
@ -129,4 +134,19 @@ public interface IMcpClient : IAsyncDisposable
|
|||
/// <param name="method">The notification method to handle.</param>
|
||||
/// <param name="handler">The async handler function to process notifications.</param>
|
||||
void OnNotification(string method, Func<JsonRpcNotification, Task> handler);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a notification to the server.
|
||||
/// </summary>
|
||||
/// <param name="method">The notification method name.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
Task SendNotificationAsync(string method, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a notification to the server with parameters.
|
||||
/// </summary>
|
||||
/// <param name="method">The notification method name.</param>
|
||||
/// <param name="parameters">The parameters to send with the notification.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||
Task SendNotificationAsync<T>(string method, T parameters, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -66,7 +66,21 @@ internal class McpClient : IMcpClient
|
|||
throw new McpClientException("Sampling handler not configured");
|
||||
}
|
||||
|
||||
return await SamplingHandler(request);
|
||||
return await SamplingHandler(request, _cts?.Token ?? CancellationToken.None);
|
||||
});
|
||||
}
|
||||
if (options.Capabilities?.Roots != null)
|
||||
{
|
||||
SetRequestHandler<ListRootsRequestParams, ListRootsResult>("roots/list",
|
||||
async (request) =>
|
||||
{
|
||||
if (RootsHandler == null)
|
||||
{
|
||||
// Setting the capability, but not a handler means we have nothing to return to the server
|
||||
_logger.RootsHandlerNotConfigured(_serverConfig.Id, _serverConfig.Name);
|
||||
throw new McpClientException("Roots handler not configured");
|
||||
}
|
||||
return await RootsHandler(request, _cts?.Token ?? CancellationToken.None);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -468,6 +482,52 @@ internal class McpClient : IMcpClient
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task SendNotificationAsync(string method, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_transport.IsConnected)
|
||||
{
|
||||
_logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name);
|
||||
throw new McpClientException("Transport is not connected");
|
||||
}
|
||||
|
||||
var notification = new JsonRpcNotification { Method = method };
|
||||
|
||||
// Log if enabled
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.SendingNotificationPayload(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(notification));
|
||||
}
|
||||
|
||||
// Log basic info
|
||||
_logger.SendingNotification(_serverConfig.Id, _serverConfig.Name, method);
|
||||
|
||||
await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task SendNotificationAsync<T>(string method, T parameters, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_transport.IsConnected)
|
||||
{
|
||||
_logger.ClientNotConnected(_serverConfig.Id, _serverConfig.Name);
|
||||
throw new McpClientException("Transport is not connected");
|
||||
}
|
||||
var notification = new JsonRpcNotification
|
||||
{
|
||||
Method = method,
|
||||
Params = parameters
|
||||
};
|
||||
// Log if enabled
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.SendingNotificationPayload(_serverConfig.Id, _serverConfig.Name, JsonSerializer.Serialize(notification));
|
||||
}
|
||||
// Log basic info
|
||||
_logger.SendingNotification(_serverConfig.Id, _serverConfig.Name, method);
|
||||
await _transport.SendMessageAsync(notification, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendNotificationAsync(JsonRpcNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_transport.IsConnected)
|
||||
|
|
@ -489,7 +549,9 @@ internal class McpClient : IMcpClient
|
|||
}
|
||||
}
|
||||
|
||||
public Func<CreateMessageRequestParams, Task<CreateMessageResult>>? SamplingHandler { get; set; }
|
||||
public Func<CreateMessageRequestParams, CancellationToken, Task<CreateMessageResult>>? SamplingHandler { get; set; }
|
||||
|
||||
public Func<ListRootsRequestParams, CancellationToken, Task<ListRootsResult>>? RootsHandler { get; set; }
|
||||
|
||||
private void SetRequestHandler<TRequest, TResponse>(string method, Func<TRequest, Task<TResponse>> handler)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ internal static partial class Log
|
|||
[LoggerMessage(Level = LogLevel.Error, Message = "Sampling handler not configured for server {serverId} ({serverName}), always set a handler when using this capability")]
|
||||
internal static partial void SamplingHandlerNotConfigured(this ILogger logger, string serverId, string serverName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Roots handler not configured for server {serverId} ({serverName}), always set a handler when using this capability")]
|
||||
internal static partial void RootsHandlerNotConfigured(this ILogger logger, string serverId, string serverName);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "Client server {serverId} ({serverName}) already initializing")]
|
||||
internal static partial void ClientAlreadyInitializing(this ILogger logger, string serverId, string serverName);
|
||||
|
||||
|
|
@ -268,4 +271,10 @@ internal static partial class Log
|
|||
|
||||
[LoggerMessage(Level = LogLevel.Error, Message = "JSON-RPC message write unknown message type: {type}")]
|
||||
internal static partial void JsonRpcMessageConverterWriteUnknownMessageType(this ILogger logger, string type);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Debug, Message = "Sending notification payload for {serverId} ({serverName}): {payload}")]
|
||||
internal static partial void SendingNotificationPayload(this ILogger logger, string serverId, string serverName, string payload);
|
||||
|
||||
[LoggerMessage(Level = LogLevel.Information, Message = "Sending notification for {serverId} ({serverName}): {method}")]
|
||||
internal static partial void SendingNotification(this ILogger logger, string serverId, string serverName, string method);
|
||||
}
|
||||
|
|
@ -24,4 +24,9 @@ public static class NotificationMethods
|
|||
/// Sent by the server when a resource is updated.
|
||||
/// </summary>
|
||||
public const string ResourceUpdatedNotification = "notifications/resources/updated";
|
||||
|
||||
/// <summary>
|
||||
/// Sent by the client when roots have been updated.
|
||||
/// </summary>
|
||||
public const string RootsUpdatedNotification = "notifications/roots/list_changed";
|
||||
}
|
||||
12
src/mcpdotnet/Protocol/Types/ListRootsRequestParams.cs
Normal file
12
src/mcpdotnet/Protocol/Types/ListRootsRequestParams.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
namespace McpDotNet.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// A request from the server to get a list of root URIs from the client.
|
||||
/// </summary>
|
||||
public class ListRootsRequestParams
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional progress token for out-of-band progress notifications.
|
||||
/// </summary>
|
||||
public string? ProgressToken { get; init; }
|
||||
}
|
||||
17
src/mcpdotnet/Protocol/Types/ListRootsResult.cs
Normal file
17
src/mcpdotnet/Protocol/Types/ListRootsResult.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace McpDotNet.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// The client's response to a roots/list request from the server.
|
||||
/// </summary>
|
||||
public class ListRootsResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Additional metadata for the result. Reserved by the protocol for future use.
|
||||
/// </summary>
|
||||
public object? Meta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of root URIs provided by the client.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<Root> Roots { get; init; }
|
||||
}
|
||||
22
src/mcpdotnet/Protocol/Types/Root.cs
Normal file
22
src/mcpdotnet/Protocol/Types/Root.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace McpDotNet.Protocol.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a root URI and its metadata.
|
||||
/// </summary>
|
||||
public class Root
|
||||
{
|
||||
/// <summary>
|
||||
/// The URI of the root.
|
||||
/// </summary>
|
||||
public required string Uri { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// A human-readable name for the root.
|
||||
/// </summary>
|
||||
public string? Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional metadata for the root. Reserved by the protocol for future use.
|
||||
/// </summary>
|
||||
public object? Meta { get; init; }
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<PropertyGroup>
|
||||
<!-- license and package properties -->
|
||||
<PackageId>mcpdotnet</PackageId>
|
||||
<Version>0.4.0.1</Version>
|
||||
<Version>0.5.0.1</Version>
|
||||
<Authors>PederHP</Authors>
|
||||
<Description>.NET client library for the Model Context Protocol (MCP)</Description>
|
||||
<PackageProjectUrl>https://github.com/PederHP/mcpdotnet</PackageProjectUrl>
|
||||
|
|
|
|||
Loading…
Reference in a new issue