diff --git a/README.MD b/README.MD index a147e2f..e25970b 100644 --- a/README.MD +++ b/README.MD @@ -21,6 +21,7 @@ This library aims to provide a clean, specification-compliant implementation of - MCP client implementation for .NET applications - Support for stdio and SSE transports - Support for all MCP capabilities: Tool, Resource, Prompt, Sampling, Roots +- Support for the Completion utility capability - Support for pagination and notifications - Async/await pattern throughout - Comprehensive logging support diff --git a/src/mcpdotnet/Client/IMcpClient.cs b/src/mcpdotnet/Client/IMcpClient.cs index fb4e470..ed6e5d5 100644 --- a/src/mcpdotnet/Client/IMcpClient.cs +++ b/src/mcpdotnet/Client/IMcpClient.cs @@ -99,6 +99,16 @@ public interface IMcpClient : IAsyncDisposable /// Task ReadResourceAsync(string uri, CancellationToken cancellationToken = default); + /// + /// Gets the completion options for a resource or prompt reference and (named) argument. + /// + /// A resource (uri) or prompt (name) reference + /// Name of argument. Must be non-null and non-empty. + /// Value of argument. Must be non-null. + /// A token to cancel the operation. + /// + Task GetCompletionAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default); + /// /// Subscribes to a resource on the server. /// diff --git a/src/mcpdotnet/Client/McpClient.cs b/src/mcpdotnet/Client/McpClient.cs index 0cd3abf..0339bd7 100644 --- a/src/mcpdotnet/Client/McpClient.cs +++ b/src/mcpdotnet/Client/McpClient.cs @@ -264,6 +264,39 @@ internal class McpClient : IMcpClient ).ConfigureAwait(false); } + /// + public async Task GetCompletionAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) + { + if (!reference.Validate(out string validationMessage)) + { + _logger.InvalidCompletionReference(_serverConfig.Id, _serverConfig.Name, reference.ToString(), validationMessage); + throw new McpClientException($"Invalid reference: {validationMessage}"); + } + if (string.IsNullOrWhiteSpace(argumentName)) + { + _logger.InvalidCompletionArgumentName(_serverConfig.Id, _serverConfig.Name, argumentName); + throw new McpClientException("Argument name cannot be null or empty"); + } + if (argumentValue is null) + { + _logger.InvalidCompletionArgumentValue(_serverConfig.Id, _serverConfig.Name, argumentValue); + throw new McpClientException("Argument value cannot be null"); + } + + _logger.GettingCompletion(_serverConfig.Id, _serverConfig.Name, reference.ToString(), argumentName, argumentValue); + return await SendRequestAsync( + new JsonRpcRequest + { + Method = "completion/complete", + Params = new Dictionary + { + ["ref"] = reference, + ["argument"] = new Argument { Name = argumentName, Value = argumentValue } + } + }, + cancellationToken + ).ConfigureAwait(false); + } /// public async Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default) diff --git a/src/mcpdotnet/Logging/Log.cs b/src/mcpdotnet/Logging/Log.cs index 9b0e01b..67251a7 100644 --- a/src/mcpdotnet/Logging/Log.cs +++ b/src/mcpdotnet/Logging/Log.cs @@ -322,4 +322,55 @@ internal static partial class Log string serverName, string data, Exception exception); + + // trace _logger.InvalidCompletionReference(_serverConfig.Id, _serverConfig.Name, reference, validationMessage); + [LoggerMessage( + EventId = 7004, + Level = LogLevel.Trace, + Message = "Invalid completion reference {reference} for server {serverId} ({serverName}): {validationMessage}" + )] + public static partial void InvalidCompletionReference( + this ILogger logger, + string serverId, + string serverName, + string reference, + string validationMessage); + + // trace _logger.InvalidACompletionArgumentName(_serverConfig.Id, _serverConfig.Name, argumentName); + [LoggerMessage( + EventId = 7005, + Level = LogLevel.Trace, + Message = "Invalid completion argument name {argumentName} for server {serverId} ({serverName})" + )] + public static partial void InvalidCompletionArgumentName( + this ILogger logger, + string serverId, + string serverName, + string argumentName); + + // trace _logger.InvalidCompletionArgumentValue(_serverConfig.Id, _serverConfig.Name, argumentValue); + [LoggerMessage( + EventId = 7006, + Level = LogLevel.Trace, + Message = "Invalid completion argument value {argumentValue} for server {serverId} ({serverName})" + )] + public static partial void InvalidCompletionArgumentValue( + this ILogger logger, + string serverId, + string serverName, + string argumentValue); + + // debug _logger.GettingCompletion(_serverConfig.Id, _serverConfig.Name, reference, argumentName, argumentValue); + [LoggerMessage( + EventId = 7007, + Level = LogLevel.Debug, + Message = "Getting completion for server {serverId} ({serverName}) with reference {reference}, argument name {argumentName}, argument value {argumentValue}" + )] + public static partial void GettingCompletion( + this ILogger logger, + string serverId, + string serverName, + string reference, + string argumentName, + string argumentValue); } \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/Argument.cs b/src/mcpdotnet/Protocol/Types/Argument.cs new file mode 100644 index 0000000..8aa6397 --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/Argument.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace McpDotNet.Protocol.Types; + +/// +/// Used for completion requests to provide additional context for the completion options. +/// +public class Argument +{ + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("value")] + public string Value { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/mcpdotnet/Protocol/Types/Completion.cs b/src/mcpdotnet/Protocol/Types/Completion.cs new file mode 100644 index 0000000..0f53706 --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/Completion.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace McpDotNet.Protocol.Types; + +/// +/// Represents a completion object in the server's response. +/// +public class Completion +{ + /// + /// An array of completion values. Must not exceed 100 items. + /// + [JsonPropertyName("values")] + public string[] Values { get; set; } = new string[0]; + + /// + /// The total number of completion options available. This can exceed the number of values actually sent in the response. + /// + [JsonPropertyName("total")] + public int? Total { get; set; } + + /// + /// Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + /// + [JsonPropertyName("hasMore")] + public bool? HasMore { get; set; } +} diff --git a/src/mcpdotnet/Protocol/Types/Reference.cs b/src/mcpdotnet/Protocol/Types/Reference.cs new file mode 100644 index 0000000..bf259c7 --- /dev/null +++ b/src/mcpdotnet/Protocol/Types/Reference.cs @@ -0,0 +1,69 @@ +using System.Text.Json.Serialization; + +namespace McpDotNet.Protocol.Types; + +/// +/// Represents a reference to a resource or prompt. Umbrella type for both ResourceReference and PromptReference from the spec schema. +/// +public class Reference +{ + /// + /// The type of content. Can be ref/resource or ref/prompt. + /// + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("uri")] + public string? Uri { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + /// + /// Returns a string representation of the reference. + /// + public override string ToString() + { + return $"\"{Type}\": \"{Uri ?? Name}\""; + } + + /// + /// Validates the reference object. + /// + public bool Validate(out string validationMessage) + { + if (Type == "ref/resource") + { + if (string.IsNullOrEmpty(Uri)) + { + validationMessage = "Uri is required for ref/resource"; + return false; + } + } + else if (Type == "ref/prompt") + { + if (string.IsNullOrEmpty(Name)) + { + validationMessage = "Name is required for ref/prompt"; + return false; + } + } + else + { + validationMessage = $"Unknown reference type: {Type}"; + return false; + } + + validationMessage = string.Empty; + return true; + } +} + +/// +/// The server's response to a completion/complete request +/// +public class CompleteResult +{ + [JsonPropertyName("completion")] + public Completion Completion { get; set; } = new Completion(); +} diff --git a/src/mcpdotnet/mcpdotnet.csproj b/src/mcpdotnet/mcpdotnet.csproj index aa119d9..7d5f86a 100644 --- a/src/mcpdotnet/mcpdotnet.csproj +++ b/src/mcpdotnet/mcpdotnet.csproj @@ -9,7 +9,7 @@ mcpdotnet - 0.7.0.1 + 0.8.0.1 PederHP .NET client library for the Model Context Protocol (MCP) https://github.com/PederHP/mcpdotnet diff --git a/tests/mcpdotnet.Tests/IntegrationTestFixture.cs b/tests/mcpdotnet.Tests/IntegrationTestFixture.cs index 2805d9f..9d95cc6 100644 --- a/tests/mcpdotnet.Tests/IntegrationTestFixture.cs +++ b/tests/mcpdotnet.Tests/IntegrationTestFixture.cs @@ -30,6 +30,7 @@ public class IntegrationTestFixture : IDisposable TransportOptions = new Dictionary { ["command"] = "npx", + // Change to ["arguments"] = "mcp-server-everything" if you want to run the server locally after creating a symlink ["arguments"] = "-y @modelcontextprotocol/server-everything", } }; diff --git a/tests/mcpdotnet.Tests/IntegrationTests.cs b/tests/mcpdotnet.Tests/IntegrationTests.cs index 854be6e..a3f651b 100644 --- a/tests/mcpdotnet.Tests/IntegrationTests.cs +++ b/tests/mcpdotnet.Tests/IntegrationTests.cs @@ -180,6 +180,68 @@ public class IntegrationTests : IClassFixture Assert.NotNull(result.Contents[0].Blob); } + /// + /// Note that as of 19th January 2025, the everything server published to npx does not support prompt completion. + /// However, the prompt completion is implemented in the github.com/modelcontextprotocol/servers repository. + /// You can clone this repo, and build locally then change the config to use a local symlink instead of npx until this is fixed, if you wish to run this test. + /// TransportOptions = new Dictionary + /// { + /// ["command"] = "npx", + /// ["arguments"] = "mcp-server-everything" // changed from "-y @modelcontextprotocol/server-everything" + /// } + /// + [Fact] + public async Task GetCompletion_Stdio_ResourceReference() + { + // arrange + + // act + var client = await _fixture.Factory.GetClientAsync("everything"); + var result = await client.GetCompletionAsync(new Reference + { + Type = "ref/resource", + Uri = "test://static/resource/1" + }, + "argument_name", "1", + CancellationToken.None + ); + + Assert.NotNull(result); + Assert.Single(result.Completion.Values); + Assert.True(result.Completion.Values[0] == "1"); + } + + /// + /// Note that as of 19th January 2025, the everything server published to npx does not support prompt completion. + /// However, the prompt completion is implemented in the github.com/modelcontextprotocol/servers repository. + /// You can clone this repo, and build locally then change the config to use a local symlink instead of npx until this is fixed, if you wish to run this test. + /// TransportOptions = new Dictionary + /// { + /// ["command"] = "npx", + /// ["arguments"] = "mcp-server-everything" // changed from "-y @modelcontextprotocol/server-everything" + /// } + /// + [Fact] + public async Task GetCompletion_Stdio_PromptReference() + { + // arrange + + // act + var client = await _fixture.Factory.GetClientAsync("everything"); + var result = await client.GetCompletionAsync(new Reference + { + Type = "ref/prompt", + Name = "irrelevant" + }, + argumentName:"style", argumentValue:"fo", + CancellationToken.None + ); + + Assert.NotNull(result); + Assert.Single(result.Completion.Values); + Assert.True(result.Completion.Values[0] == "formal"); + } + [Fact] public async Task Sampling_Stdio_EverythingServer() {