Auto-populate completion handlers from AllowedValuesAttribute on prompt/resource parameters (#1380)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: Stephen Toub <stoub@microsoft.com>
This commit is contained in:
parent
06563e233d
commit
4af2596019
7 changed files with 415 additions and 1 deletions
|
|
@ -81,6 +81,42 @@ builder.Services.AddMcpServer()
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Automatic completions with AllowedValuesAttribute
|
||||||
|
|
||||||
|
For parameters with a known set of valid values, you can use `System.ComponentModel.DataAnnotations.AllowedValuesAttribute` on `string` parameters of prompts or resource templates. The server will automatically surface those values as completions without needing a custom completion handler.
|
||||||
|
|
||||||
|
#### Prompt parameters
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[McpServerPromptType]
|
||||||
|
public class MyPrompts
|
||||||
|
{
|
||||||
|
[McpServerPrompt, Description("Generates a code review prompt")]
|
||||||
|
public static ChatMessage CodeReview(
|
||||||
|
[Description("The programming language")]
|
||||||
|
[AllowedValues("csharp", "python", "javascript", "typescript", "go", "rust")]
|
||||||
|
string language,
|
||||||
|
[Description("The code to review")] string code)
|
||||||
|
=> new(ChatRole.User, $"Please review the following {language} code:\n\n```{language}\n{code}\n```");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resource template parameters
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[McpServerResourceType]
|
||||||
|
public class MyResources
|
||||||
|
{
|
||||||
|
[McpServerResource("config://settings/{section}"), Description("Reads a configuration section")]
|
||||||
|
public static string ReadConfig(
|
||||||
|
[AllowedValues("general", "network", "security", "logging")]
|
||||||
|
string section)
|
||||||
|
=> GetConfig(section);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With these attributes in place, when a client sends a `completion/complete` request for the `language` or `section` argument, the server will automatically filter and return matching values based on what the user has typed so far. This approach can be combined with a custom completion handler registered via `WithCompleteHandler`; the handler's results are returned first, followed by any matching `AllowedValues`.
|
||||||
|
|
||||||
### Requesting completions on the client
|
### Requesting completions on the client
|
||||||
|
|
||||||
Clients request completions using <xref:ModelContextProtocol.Client.McpClient.CompleteAsync*>. Provide a reference to the prompt or resource template, the argument name, and the current partial value:
|
Clients request completions using <xref:ModelContextProtocol.Client.McpClient.CompleteAsync*>. Provide a reference to the prompt or resource template, the argument name, and the current partial value:
|
||||||
|
|
|
||||||
|
|
@ -245,12 +245,62 @@ internal sealed partial class McpServerImpl : McpServer
|
||||||
var completeHandler = options.Handlers.CompleteHandler;
|
var completeHandler = options.Handlers.CompleteHandler;
|
||||||
var completionsCapability = options.Capabilities?.Completions;
|
var completionsCapability = options.Capabilities?.Completions;
|
||||||
|
|
||||||
if (completeHandler is null && completionsCapability is null)
|
// Build completion value lookups from prompt/resource collections' [AllowedValues]-attributed parameters.
|
||||||
|
Dictionary<string, Dictionary<string, string[]>>? promptCompletions = BuildAllowedValueCompletions(options.PromptCollection);
|
||||||
|
Dictionary<string, Dictionary<string, string[]>>? resourceCompletions = BuildAllowedValueCompletions(options.ResourceCollection);
|
||||||
|
bool hasCollectionCompletions = promptCompletions is not null || resourceCompletions is not null;
|
||||||
|
|
||||||
|
if (completeHandler is null && completionsCapability is null && !hasCollectionCompletions)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
completeHandler ??= (static async (_, __) => new CompleteResult());
|
completeHandler ??= (static async (_, __) => new CompleteResult());
|
||||||
|
|
||||||
|
// Augment the completion handler with allowed values from prompt/resource collections.
|
||||||
|
if (hasCollectionCompletions)
|
||||||
|
{
|
||||||
|
var originalCompleteHandler = completeHandler;
|
||||||
|
completeHandler = async (request, cancellationToken) =>
|
||||||
|
{
|
||||||
|
CompleteResult result = await originalCompleteHandler(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
string[]? allowedValues = null;
|
||||||
|
switch (request.Params?.Ref)
|
||||||
|
{
|
||||||
|
case PromptReference pr when promptCompletions is not null:
|
||||||
|
if (promptCompletions.TryGetValue(pr.Name, out var promptParams))
|
||||||
|
{
|
||||||
|
promptParams.TryGetValue(request.Params.Argument.Name, out allowedValues);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ResourceTemplateReference rtr when resourceCompletions is not null:
|
||||||
|
if (rtr.Uri is not null && resourceCompletions.TryGetValue(rtr.Uri, out var resourceParams))
|
||||||
|
{
|
||||||
|
resourceParams.TryGetValue(request.Params.Argument.Name, out allowedValues);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedValues is not null)
|
||||||
|
{
|
||||||
|
string partialValue = request.Params!.Argument.Value;
|
||||||
|
foreach (var v in allowedValues)
|
||||||
|
{
|
||||||
|
if (v.StartsWith(partialValue, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.Completion.Values.Add(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Completion.Total = result.Completion.Values.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
completeHandler = BuildFilterPipeline(completeHandler, options.Filters.Request.CompleteFilters);
|
completeHandler = BuildFilterPipeline(completeHandler, options.Filters.Request.CompleteFilters);
|
||||||
|
|
||||||
ServerCapabilities.Completions = new();
|
ServerCapabilities.Completions = new();
|
||||||
|
|
@ -262,6 +312,76 @@ internal sealed partial class McpServerImpl : McpServer
|
||||||
McpJsonUtilities.JsonContext.Default.CompleteResult);
|
McpJsonUtilities.JsonContext.Default.CompleteResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a lookup of primitive name/URI → (parameter name → allowed values) from the enum values
|
||||||
|
/// in the JSON schemas of AIFunction-based prompts or resources.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, Dictionary<string, string[]>>? BuildAllowedValueCompletions<T>(
|
||||||
|
McpServerPrimitiveCollection<T>? primitives) where T : class, IMcpServerPrimitive
|
||||||
|
{
|
||||||
|
if (primitives is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, Dictionary<string, string[]>>? result = null;
|
||||||
|
foreach (var primitive in primitives)
|
||||||
|
{
|
||||||
|
JsonElement schema;
|
||||||
|
string id;
|
||||||
|
if (primitive is AIFunctionMcpServerPrompt aiPrompt)
|
||||||
|
{
|
||||||
|
schema = aiPrompt.AIFunction.JsonSchema;
|
||||||
|
id = aiPrompt.ProtocolPrompt.Name;
|
||||||
|
}
|
||||||
|
else if (primitive is AIFunctionMcpServerResource aiResource && aiResource.IsTemplated)
|
||||||
|
{
|
||||||
|
schema = aiResource.AIFunction.JsonSchema;
|
||||||
|
id = aiResource.ProtocolResourceTemplate.UriTemplate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.TryGetProperty("properties", out JsonElement properties) &&
|
||||||
|
properties.ValueKind is JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
Dictionary<string, string[]>? paramValues = null;
|
||||||
|
foreach (var param in properties.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (param.Value.TryGetProperty("enum", out JsonElement enumValues) &&
|
||||||
|
enumValues.ValueKind is JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
List<string>? values = null;
|
||||||
|
foreach (var item in enumValues.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind is JsonValueKind.String && item.GetString() is { } str)
|
||||||
|
{
|
||||||
|
values ??= [];
|
||||||
|
values.Add(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values is not null)
|
||||||
|
{
|
||||||
|
paramValues ??= new(StringComparer.Ordinal);
|
||||||
|
paramValues[param.Name] = [.. values];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paramValues is not null)
|
||||||
|
{
|
||||||
|
result ??= new(StringComparer.Ordinal);
|
||||||
|
result[id] = paramValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private void ConfigureExperimentalAndExtensions(McpServerOptions options)
|
private void ConfigureExperimentalAndExtensions(McpServerOptions options)
|
||||||
{
|
{
|
||||||
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
|
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,11 @@ namespace ModelContextProtocol.Server;
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Parameters of type <see cref="string"/> that are decorated with <c>AllowedValuesAttribute</c>
|
||||||
|
/// will automatically have their allowed values surfaced as completions in response to <c>completion/complete</c> requests from clients,
|
||||||
|
/// without requiring a custom <see cref="McpServerHandlers.CompleteHandler"/> to be configured.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public abstract class McpServerPrompt : IMcpServerPrimitive
|
public abstract class McpServerPrompt : IMcpServerPrimitive
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,11 @@ namespace ModelContextProtocol.Server;
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Parameters of type <see cref="string"/> that are decorated with <c>AllowedValuesAttribute</c>
|
||||||
|
/// will automatically have their allowed values surfaced as completions in response to <c>completion/complete</c> requests from clients,
|
||||||
|
/// without requiring a custom <see cref="McpServerHandlers.CompleteHandler"/> to be configured.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
public sealed class McpServerPromptAttribute : Attribute
|
public sealed class McpServerPromptAttribute : Attribute
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,11 @@ namespace ModelContextProtocol.Server;
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Parameters of type <see cref="string"/> that are decorated with <c>AllowedValuesAttribute</c>
|
||||||
|
/// will automatically have their allowed values surfaced as completions in response to <c>completion/complete</c> requests from clients,
|
||||||
|
/// without requiring a custom <see cref="McpServerHandlers.CompleteHandler"/> to be configured.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public abstract class McpServerResource : IMcpServerPrimitive
|
public abstract class McpServerResource : IMcpServerPrimitive
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,11 @@ namespace ModelContextProtocol.Server;
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Parameters of type <see cref="string"/> that are decorated with <c>AllowedValuesAttribute</c>
|
||||||
|
/// will automatically have their allowed values surfaced as completions in response to <c>completion/complete</c> requests from clients,
|
||||||
|
/// without requiring a custom <see cref="McpServerHandlers.CompleteHandler"/> to be configured.
|
||||||
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
public sealed class McpServerResourceAttribute : Attribute
|
public sealed class McpServerResourceAttribute : Attribute
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,244 @@ public class McpServerTests : LoggedTest
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if NET
|
||||||
|
[Fact]
|
||||||
|
public async Task Completion_AutoPopulated_FromPromptAllowedValues()
|
||||||
|
{
|
||||||
|
await using var transport = new TestServerTransport();
|
||||||
|
var options = CreateOptions();
|
||||||
|
options.PromptCollection = [McpServerPrompt.Create(
|
||||||
|
(
|
||||||
|
[System.ComponentModel.DataAnnotations.AllowedValues("dog", "cat", "fish")] string animal
|
||||||
|
) => animal,
|
||||||
|
new McpServerPromptCreateOptions { Name = "test-prompt" })];
|
||||||
|
|
||||||
|
await using var server = McpServer.Create(transport, options, LoggerFactory);
|
||||||
|
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var receivedMessage = new TaskCompletionSource<JsonRpcResponse>();
|
||||||
|
transport.OnMessageSent = (message) =>
|
||||||
|
{
|
||||||
|
if (message is JsonRpcResponse response && response.Id.ToString() == "55")
|
||||||
|
receivedMessage.SetResult(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
await transport.SendMessageAsync(new JsonRpcRequest
|
||||||
|
{
|
||||||
|
Method = RequestMethods.CompletionComplete,
|
||||||
|
Id = new RequestId(55),
|
||||||
|
Params = JsonSerializer.SerializeToNode(new CompleteRequestParams
|
||||||
|
{
|
||||||
|
Ref = new PromptReference { Name = "test-prompt" },
|
||||||
|
Argument = new Argument { Name = "animal", Value = "c" }
|
||||||
|
}, McpJsonUtilities.DefaultOptions)
|
||||||
|
}, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var response = await receivedMessage.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(response);
|
||||||
|
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||||
|
Assert.NotNull(result?.Completion);
|
||||||
|
Assert.Equal(["cat"], result.Completion.Values);
|
||||||
|
Assert.Equal(1, result.Completion.Total);
|
||||||
|
|
||||||
|
await transport.DisposeAsync();
|
||||||
|
await runTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Completion_AutoPopulated_FromPromptAllowedValues_NoMatch()
|
||||||
|
{
|
||||||
|
await using var transport = new TestServerTransport();
|
||||||
|
var options = CreateOptions();
|
||||||
|
options.PromptCollection = [McpServerPrompt.Create(
|
||||||
|
(
|
||||||
|
[System.ComponentModel.DataAnnotations.AllowedValues("dog", "cat")] string animal
|
||||||
|
) => animal,
|
||||||
|
new McpServerPromptCreateOptions { Name = "test-prompt" })];
|
||||||
|
|
||||||
|
await using var server = McpServer.Create(transport, options, LoggerFactory);
|
||||||
|
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var receivedMessage = new TaskCompletionSource<JsonRpcResponse>();
|
||||||
|
transport.OnMessageSent = (message) =>
|
||||||
|
{
|
||||||
|
if (message is JsonRpcResponse response && response.Id.ToString() == "55")
|
||||||
|
receivedMessage.SetResult(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
await transport.SendMessageAsync(new JsonRpcRequest
|
||||||
|
{
|
||||||
|
Method = RequestMethods.CompletionComplete,
|
||||||
|
Id = new RequestId(55),
|
||||||
|
Params = JsonSerializer.SerializeToNode(new CompleteRequestParams
|
||||||
|
{
|
||||||
|
Ref = new PromptReference { Name = "test-prompt" },
|
||||||
|
Argument = new Argument { Name = "animal", Value = "z" }
|
||||||
|
}, McpJsonUtilities.DefaultOptions)
|
||||||
|
}, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var response = await receivedMessage.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(response);
|
||||||
|
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||||
|
Assert.NotNull(result?.Completion);
|
||||||
|
Assert.Empty(result.Completion.Values);
|
||||||
|
|
||||||
|
await transport.DisposeAsync();
|
||||||
|
await runTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Completion_AutoPopulated_FromResourceAllowedValues()
|
||||||
|
{
|
||||||
|
await using var transport = new TestServerTransport();
|
||||||
|
var options = CreateOptions();
|
||||||
|
options.ResourceCollection =
|
||||||
|
[
|
||||||
|
McpServerResource.Create(
|
||||||
|
(
|
||||||
|
[System.ComponentModel.DataAnnotations.AllowedValues("us-east-1", "us-west-2", "eu-west-1")] string region
|
||||||
|
) => $"Resource for {region}",
|
||||||
|
new McpServerResourceCreateOptions
|
||||||
|
{
|
||||||
|
UriTemplate = "resource://regions/{region}",
|
||||||
|
Name = "regions"
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
await using var server = McpServer.Create(transport, options, LoggerFactory);
|
||||||
|
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var receivedMessage = new TaskCompletionSource<JsonRpcResponse>();
|
||||||
|
transport.OnMessageSent = (message) =>
|
||||||
|
{
|
||||||
|
if (message is JsonRpcResponse response && response.Id.ToString() == "55")
|
||||||
|
receivedMessage.SetResult(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
await transport.SendMessageAsync(new JsonRpcRequest
|
||||||
|
{
|
||||||
|
Method = RequestMethods.CompletionComplete,
|
||||||
|
Id = new RequestId(55),
|
||||||
|
Params = JsonSerializer.SerializeToNode(new CompleteRequestParams
|
||||||
|
{
|
||||||
|
Ref = new ResourceTemplateReference { Uri = "resource://regions/{region}" },
|
||||||
|
Argument = new Argument { Name = "region", Value = "us" }
|
||||||
|
}, McpJsonUtilities.DefaultOptions)
|
||||||
|
}, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var response = await receivedMessage.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(response);
|
||||||
|
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||||
|
Assert.NotNull(result?.Completion);
|
||||||
|
Assert.Equal(["us-east-1", "us-west-2"], result.Completion.Values);
|
||||||
|
Assert.Equal(2, result.Completion.Total);
|
||||||
|
|
||||||
|
await transport.DisposeAsync();
|
||||||
|
await runTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Completion_AutoPopulated_CombinedWithCustomHandler()
|
||||||
|
{
|
||||||
|
await using var transport = new TestServerTransport();
|
||||||
|
var options = CreateOptions();
|
||||||
|
options.PromptCollection = [McpServerPrompt.Create(
|
||||||
|
(
|
||||||
|
[System.ComponentModel.DataAnnotations.AllowedValues("dog", "cat")] string animal
|
||||||
|
) => animal,
|
||||||
|
new McpServerPromptCreateOptions { Name = "test-prompt" })];
|
||||||
|
|
||||||
|
// Add a custom handler that provides additional completions
|
||||||
|
options.Handlers.CompleteHandler = async (request, ct) =>
|
||||||
|
new CompleteResult
|
||||||
|
{
|
||||||
|
Completion = new()
|
||||||
|
{
|
||||||
|
Values = ["custom-value"],
|
||||||
|
Total = 1,
|
||||||
|
HasMore = false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var server = McpServer.Create(transport, options, LoggerFactory);
|
||||||
|
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var receivedMessage = new TaskCompletionSource<JsonRpcResponse>();
|
||||||
|
transport.OnMessageSent = (message) =>
|
||||||
|
{
|
||||||
|
if (message is JsonRpcResponse response && response.Id.ToString() == "55")
|
||||||
|
receivedMessage.SetResult(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
await transport.SendMessageAsync(new JsonRpcRequest
|
||||||
|
{
|
||||||
|
Method = RequestMethods.CompletionComplete,
|
||||||
|
Id = new RequestId(55),
|
||||||
|
Params = JsonSerializer.SerializeToNode(new CompleteRequestParams
|
||||||
|
{
|
||||||
|
Ref = new PromptReference { Name = "test-prompt" },
|
||||||
|
Argument = new Argument { Name = "animal", Value = "" }
|
||||||
|
}, McpJsonUtilities.DefaultOptions)
|
||||||
|
}, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var response = await receivedMessage.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(response);
|
||||||
|
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||||
|
Assert.NotNull(result?.Completion);
|
||||||
|
// Custom handler values + auto-populated values should be combined
|
||||||
|
Assert.Equal(["custom-value", "dog", "cat"], result.Completion.Values);
|
||||||
|
Assert.Equal(3, result.Completion.Total);
|
||||||
|
|
||||||
|
await transport.DisposeAsync();
|
||||||
|
await runTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Completion_AutoPopulated_EnablesCompletionsCapabilityAutomatically()
|
||||||
|
{
|
||||||
|
// When prompts with AllowedValues are registered but no explicit Completions capability is set,
|
||||||
|
// the server should still handle completion requests (i.e., the capability is auto-enabled).
|
||||||
|
// This is verified by the fact that sending a completion request succeeds rather than failing.
|
||||||
|
await using var transport = new TestServerTransport();
|
||||||
|
var options = CreateOptions();
|
||||||
|
options.PromptCollection = [McpServerPrompt.Create(
|
||||||
|
(
|
||||||
|
[System.ComponentModel.DataAnnotations.AllowedValues("a", "b")] string param
|
||||||
|
) => param,
|
||||||
|
new McpServerPromptCreateOptions { Name = "test-prompt" })];
|
||||||
|
|
||||||
|
await using var server = McpServer.Create(transport, options, LoggerFactory);
|
||||||
|
var runTask = server.RunAsync(TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var receivedMessage = new TaskCompletionSource<JsonRpcResponse>();
|
||||||
|
transport.OnMessageSent = (message) =>
|
||||||
|
{
|
||||||
|
if (message is JsonRpcResponse response && response.Id.ToString() == "55")
|
||||||
|
receivedMessage.SetResult(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
await transport.SendMessageAsync(new JsonRpcRequest
|
||||||
|
{
|
||||||
|
Method = RequestMethods.CompletionComplete,
|
||||||
|
Id = new RequestId(55),
|
||||||
|
Params = JsonSerializer.SerializeToNode(new CompleteRequestParams
|
||||||
|
{
|
||||||
|
Ref = new PromptReference { Name = "test-prompt" },
|
||||||
|
Argument = new Argument { Name = "param", Value = "" }
|
||||||
|
}, McpJsonUtilities.DefaultOptions)
|
||||||
|
}, TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
var response = await receivedMessage.Task.WaitAsync(TestConstants.DefaultTimeout, TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(response);
|
||||||
|
var result = JsonSerializer.Deserialize<CompleteResult>(response.Result, McpJsonUtilities.DefaultOptions);
|
||||||
|
Assert.NotNull(result?.Completion);
|
||||||
|
Assert.Equal(["a", "b"], result.Completion.Values);
|
||||||
|
|
||||||
|
await transport.DisposeAsync();
|
||||||
|
await runTask;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Can_Handle_ResourceTemplates_List_Requests()
|
public async Task Can_Handle_ResourceTemplates_List_Requests()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue