Completion support (#15)
This commit is contained in:
parent
3e35efe79c
commit
107c7acc8f
10 changed files with 270 additions and 1 deletions
|
|
@ -21,6 +21,7 @@ This library aims to provide a clean, specification-compliant implementation of
|
||||||
- MCP client implementation for .NET applications
|
- MCP client implementation for .NET applications
|
||||||
- Support for stdio and SSE transports
|
- Support for stdio and SSE transports
|
||||||
- Support for all MCP capabilities: Tool, Resource, Prompt, Sampling, Roots
|
- Support for all MCP capabilities: Tool, Resource, Prompt, Sampling, Roots
|
||||||
|
- Support for the Completion utility capability
|
||||||
- Support for pagination and notifications
|
- Support for pagination and notifications
|
||||||
- Async/await pattern throughout
|
- Async/await pattern throughout
|
||||||
- Comprehensive logging support
|
- Comprehensive logging support
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,16 @@ public interface IMcpClient : IAsyncDisposable
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task<ReadResourceResult> ReadResourceAsync(string uri, CancellationToken cancellationToken = default);
|
Task<ReadResourceResult> ReadResourceAsync(string uri, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the completion options for a resource or prompt reference and (named) argument.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reference">A resource (uri) or prompt (name) reference</param>
|
||||||
|
/// <param name="argumentName">Name of argument. Must be non-null and non-empty.</param>
|
||||||
|
/// <param name="argumentValue">Value of argument. Must be non-null.</param>
|
||||||
|
/// <param name="cancellationToken">A token to cancel the operation.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<CompleteResult> GetCompletionAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Subscribes to a resource on the server.
|
/// Subscribes to a resource on the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,39 @@ internal class McpClient : IMcpClient
|
||||||
).ConfigureAwait(false);
|
).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<CompleteResult> 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<CompleteResult>(
|
||||||
|
new JsonRpcRequest
|
||||||
|
{
|
||||||
|
Method = "completion/complete",
|
||||||
|
Params = new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["ref"] = reference,
|
||||||
|
["argument"] = new Argument { Name = argumentName, Value = argumentValue }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken
|
||||||
|
).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public async Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default)
|
public async Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default)
|
||||||
|
|
|
||||||
|
|
@ -322,4 +322,55 @@ internal static partial class Log
|
||||||
string serverName,
|
string serverName,
|
||||||
string data,
|
string data,
|
||||||
Exception exception);
|
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);
|
||||||
}
|
}
|
||||||
15
src/mcpdotnet/Protocol/Types/Argument.cs
Normal file
15
src/mcpdotnet/Protocol/Types/Argument.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace McpDotNet.Protocol.Types;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used for completion requests to provide additional context for the completion options.
|
||||||
|
/// </summary>
|
||||||
|
public class Argument
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("value")]
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
27
src/mcpdotnet/Protocol/Types/Completion.cs
Normal file
27
src/mcpdotnet/Protocol/Types/Completion.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace McpDotNet.Protocol.Types;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a completion object in the server's response.
|
||||||
|
/// </summary>
|
||||||
|
public class Completion
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An array of completion values. Must not exceed 100 items.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("values")]
|
||||||
|
public string[] Values { get; set; } = new string[0];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total number of completion options available. This can exceed the number of values actually sent in the response.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("total")]
|
||||||
|
public int? Total { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("hasMore")]
|
||||||
|
public bool? HasMore { get; set; }
|
||||||
|
}
|
||||||
69
src/mcpdotnet/Protocol/Types/Reference.cs
Normal file
69
src/mcpdotnet/Protocol/Types/Reference.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace McpDotNet.Protocol.Types;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a reference to a resource or prompt. Umbrella type for both ResourceReference and PromptReference from the spec schema.
|
||||||
|
/// </summary>
|
||||||
|
public class Reference
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type of content. Can be ref/resource or ref/prompt.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("uri")]
|
||||||
|
public string? Uri { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a string representation of the reference.
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"\"{Type}\": \"{Uri ?? Name}\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the reference object.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server's response to a completion/complete request
|
||||||
|
/// </summary>
|
||||||
|
public class CompleteResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("completion")]
|
||||||
|
public Completion Completion { get; set; } = new Completion();
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- license and package properties -->
|
<!-- license and package properties -->
|
||||||
<PackageId>mcpdotnet</PackageId>
|
<PackageId>mcpdotnet</PackageId>
|
||||||
<Version>0.7.0.1</Version>
|
<Version>0.8.0.1</Version>
|
||||||
<Authors>PederHP</Authors>
|
<Authors>PederHP</Authors>
|
||||||
<Description>.NET client library for the Model Context Protocol (MCP)</Description>
|
<Description>.NET client library for the Model Context Protocol (MCP)</Description>
|
||||||
<PackageProjectUrl>https://github.com/PederHP/mcpdotnet</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/PederHP/mcpdotnet</PackageProjectUrl>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ public class IntegrationTestFixture : IDisposable
|
||||||
TransportOptions = new Dictionary<string, string>
|
TransportOptions = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["command"] = "npx",
|
["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",
|
["arguments"] = "-y @modelcontextprotocol/server-everything",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,68 @@ public class IntegrationTests : IClassFixture<IntegrationTestFixture>
|
||||||
Assert.NotNull(result.Contents[0].Blob);
|
Assert.NotNull(result.Contents[0].Blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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<string, string>
|
||||||
|
/// {
|
||||||
|
/// ["command"] = "npx",
|
||||||
|
/// ["arguments"] = "mcp-server-everything" // changed from "-y @modelcontextprotocol/server-everything"
|
||||||
|
/// }
|
||||||
|
/// </summary>
|
||||||
|
[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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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<string, string>
|
||||||
|
/// {
|
||||||
|
/// ["command"] = "npx",
|
||||||
|
/// ["arguments"] = "mcp-server-everything" // changed from "-y @modelcontextprotocol/server-everything"
|
||||||
|
/// }
|
||||||
|
/// </summary>
|
||||||
|
[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]
|
[Fact]
|
||||||
public async Task Sampling_Stdio_EverythingServer()
|
public async Task Sampling_Stdio_EverythingServer()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue