From a80f30db6cb0b23c4eed542ca990b44b9f22eaca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 14:46:16 +0000 Subject: [PATCH 1/7] Initial plan From 5e7064e4f3dfa237169a1c8ca67045a380890e3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:02:19 +0000 Subject: [PATCH 2/7] Augment ConfigureCompletion to extract enum values from prompt/resource JSON schemas and provide automatic completions Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Server/McpServerImpl.cs | 128 +++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 39feae5d6..c08fa9b7b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -254,12 +254,69 @@ private void ConfigureCompletion(McpServerOptions options) var completeHandler = options.Handlers.CompleteHandler; var completionsCapability = options.Capabilities?.Completions; - if (completeHandler is null && completionsCapability is null) + // Build completion value lookups from prompt/resource collections' [AllowedValues]-attributed parameters. + Dictionary>? promptCompletions = BuildAllowedValueCompletions(options.PromptCollection); + Dictionary>? resourceCompletions = BuildAllowedValueCompletions(options.ResourceCollection); + bool hasCollectionCompletions = promptCompletions is not null || resourceCompletions is not null; + + if (completeHandler is null && completionsCapability is null && !hasCollectionCompletions) { return; } 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; + var filtered = Array.FindAll(allowedValues, v => v.StartsWith(partialValue, StringComparison.OrdinalIgnoreCase)); + + if (result.Completion.Values.Count > 0) + { + foreach (var v in filtered) + { + result.Completion.Values.Add(v); + } + } + else + { + result.Completion.Values = filtered; + } + + result.Completion.Total = result.Completion.Values.Count; + result.Completion.HasMore = false; + } + + return result; + }; + } + completeHandler = BuildFilterPipeline(completeHandler, options.Filters.Request.CompleteFilters); ServerCapabilities.Completions = new(); @@ -271,6 +328,75 @@ private void ConfigureCompletion(McpServerOptions options) McpJsonUtilities.JsonContext.Default.CompleteResult); } + /// + /// 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. + /// + private static Dictionary>? BuildAllowedValueCompletions( + McpServerPrimitiveCollection? primitives) where T : class, IMcpServerPrimitive + { + if (primitives is null) + { + return null; + } + + Dictionary>? 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)) + { + Dictionary? paramValues = null; + foreach (var param in properties.EnumerateObject()) + { + if (param.Value.TryGetProperty("enum", out JsonElement enumValues) && + enumValues.ValueKind == JsonValueKind.Array) + { + List? values = null; + foreach (var item in enumValues.EnumerateArray()) + { + if (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 ConfigureExperimental(McpServerOptions options) { ServerCapabilities.Experimental = options.Capabilities?.Experimental; From 41b1c5c0a45bb5212725dceef764ba55fd824e09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:08:44 +0000 Subject: [PATCH 3/7] Add tests for automatic completion from AllowedValuesAttribute on prompt and resource parameters Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Server/McpServerTests.cs | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 53fd50685..01018e4a5 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -322,6 +322,242 @@ await Can_Handle_Requests( }); } + [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(); + 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(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(); + 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(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(); + 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(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(); + 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(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(); + 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(response.Result, McpJsonUtilities.DefaultOptions); + Assert.NotNull(result?.Completion); + Assert.Equal(["a", "b"], result.Completion.Values); + + await transport.DisposeAsync(); + await runTask; + } + [Fact] public async Task Can_Handle_ResourceTemplates_List_Requests() { From 36939661e5fe48a8a1d1fac18f68a7c657c931ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:13:35 +0000 Subject: [PATCH 4/7] Simplify combining logic: always add values and preserve original HasMore state Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Server/McpServerImpl.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index c08fa9b7b..d7d049701 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -297,20 +297,12 @@ private void ConfigureCompletion(McpServerOptions options) string partialValue = request.Params!.Argument.Value; var filtered = Array.FindAll(allowedValues, v => v.StartsWith(partialValue, StringComparison.OrdinalIgnoreCase)); - if (result.Completion.Values.Count > 0) + foreach (var v in filtered) { - foreach (var v in filtered) - { - result.Completion.Values.Add(v); - } - } - else - { - result.Completion.Values = filtered; + result.Completion.Values.Add(v); } result.Completion.Total = result.Completion.Values.Count; - result.Completion.HasMore = false; } return result; From e634cc7ae1d235dada7dc81aaa53f7f73cdbdeb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:04:16 +0000 Subject: [PATCH 5/7] Address PR review comments: fix completion filtering, JSON guards, and add AllowedValues docs Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Server/McpServerImpl.cs | 16 +++++++++------- .../Server/McpServerPrompt.cs | 5 +++++ .../Server/McpServerPromptAttribute.cs | 5 +++++ .../Server/McpServerResource.cs | 5 +++++ .../Server/McpServerResourceAttribute.cs | 5 +++++ 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index d7d049701..6a36f9539 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -295,11 +295,12 @@ private void ConfigureCompletion(McpServerOptions options) if (allowedValues is not null) { string partialValue = request.Params!.Argument.Value; - var filtered = Array.FindAll(allowedValues, v => v.StartsWith(partialValue, StringComparison.OrdinalIgnoreCase)); - - foreach (var v in filtered) + foreach (var v in allowedValues) { - result.Completion.Values.Add(v); + if (v.StartsWith(partialValue, StringComparison.OrdinalIgnoreCase)) + { + result.Completion.Values.Add(v); + } } result.Completion.Total = result.Completion.Values.Count; @@ -352,18 +353,19 @@ private void ConfigureCompletion(McpServerOptions options) continue; } - if (schema.TryGetProperty("properties", out JsonElement properties)) + if (schema.TryGetProperty("properties", out JsonElement properties) && + properties.ValueKind is JsonValueKind.Object) { Dictionary? paramValues = null; foreach (var param in properties.EnumerateObject()) { if (param.Value.TryGetProperty("enum", out JsonElement enumValues) && - enumValues.ValueKind == JsonValueKind.Array) + enumValues.ValueKind is JsonValueKind.Array) { List? values = null; foreach (var item in enumValues.EnumerateArray()) { - if (item.GetString() is { } str) + if (item.ValueKind is JsonValueKind.String && item.GetString() is { } str) { values ??= []; values.Add(str); diff --git a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs index 35fd2b80a..333dbdf15 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs @@ -116,6 +116,11 @@ namespace ModelContextProtocol.Server; /// /// Other returned types will result in an being thrown. /// +/// +/// Parameters of type that are decorated with AllowedValuesAttribute +/// will automatically have their allowed values surfaced as completions in response to completion/complete requests from clients, +/// without requiring a custom to be configured. +/// /// public abstract class McpServerPrompt : IMcpServerPrimitive { diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs index 3b5e061ed..c2e95dd9a 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs @@ -103,6 +103,11 @@ namespace ModelContextProtocol.Server; /// /// Other returned types will result in an being thrown. /// +/// +/// Parameters of type that are decorated with AllowedValuesAttribute +/// will automatically have their allowed values surfaced as completions in response to completion/complete requests from clients, +/// without requiring a custom to be configured. +/// /// [AttributeUsage(AttributeTargets.Method)] public sealed class McpServerPromptAttribute : Attribute diff --git a/src/ModelContextProtocol.Core/Server/McpServerResource.cs b/src/ModelContextProtocol.Core/Server/McpServerResource.cs index a7f4780d4..b1f21076d 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResource.cs @@ -121,6 +121,11 @@ namespace ModelContextProtocol.Server; /// /// Other returned types will result in an being thrown. /// +/// +/// Parameters of type that are decorated with AllowedValuesAttribute +/// will automatically have their allowed values surfaced as completions in response to completion/complete requests from clients, +/// without requiring a custom to be configured. +/// /// public abstract class McpServerResource : IMcpServerPrimitive { diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs index 7d1054507..6ed59485c 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs @@ -105,6 +105,11 @@ namespace ModelContextProtocol.Server; /// /// Other returned types will result in an being thrown. /// +/// +/// Parameters of type that are decorated with AllowedValuesAttribute +/// will automatically have their allowed values surfaced as completions in response to completion/complete requests from clients, +/// without requiring a custom to be configured. +/// /// [AttributeUsage(AttributeTargets.Method)] public sealed class McpServerResourceAttribute : Attribute From 90dd295e8167e9a5719a6834a9744bcd6bc95843 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:51:23 +0000 Subject: [PATCH 6/7] Guard AllowedValues test methods with #if NET for net472 compatibility Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- tests/ModelContextProtocol.Tests/Server/McpServerTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 01018e4a5..9f7dcb099 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -322,6 +322,7 @@ await Can_Handle_Requests( }); } +#if NET [Fact] public async Task Completion_AutoPopulated_FromPromptAllowedValues() { @@ -557,6 +558,7 @@ await transport.SendMessageAsync(new JsonRpcRequest await transport.DisposeAsync(); await runTask; } +#endif [Fact] public async Task Can_Handle_ResourceTemplates_List_Requests() From 10b47485f1f2a0a17877be0e0c2e97fc9940b23e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 00:22:36 +0000 Subject: [PATCH 7/7] Add AllowedValuesAttribute section with code samples to completions conceptual docs Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- docs/concepts/completions/completions.md | 36 ++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/concepts/completions/completions.md b/docs/concepts/completions/completions.md index 0bc0b21aa..10996882c 100644 --- a/docs/concepts/completions/completions.md +++ b/docs/concepts/completions/completions.md @@ -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 Clients request completions using . Provide a reference to the prompt or resource template, the argument name, and the current partial value: