Skip to content

Commit 4af2596

Browse files
Copilotstephentoub
andauthored
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>
1 parent 06563e2 commit 4af2596

File tree

7 files changed

+415
-1
lines changed

7 files changed

+415
-1
lines changed

docs/concepts/completions/completions.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,42 @@ builder.Services.AddMcpServer()
8181
});
8282
```
8383

84+
### Automatic completions with AllowedValuesAttribute
85+
86+
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.
87+
88+
#### Prompt parameters
89+
90+
```csharp
91+
[McpServerPromptType]
92+
public class MyPrompts
93+
{
94+
[McpServerPrompt, Description("Generates a code review prompt")]
95+
public static ChatMessage CodeReview(
96+
[Description("The programming language")]
97+
[AllowedValues("csharp", "python", "javascript", "typescript", "go", "rust")]
98+
string language,
99+
[Description("The code to review")] string code)
100+
=> new(ChatRole.User, $"Please review the following {language} code:\n\n```{language}\n{code}\n```");
101+
}
102+
```
103+
104+
#### Resource template parameters
105+
106+
```csharp
107+
[McpServerResourceType]
108+
public class MyResources
109+
{
110+
[McpServerResource("config://settings/{section}"), Description("Reads a configuration section")]
111+
public static string ReadConfig(
112+
[AllowedValues("general", "network", "security", "logging")]
113+
string section)
114+
=> GetConfig(section);
115+
}
116+
```
117+
118+
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`.
119+
84120
### Requesting completions on the client
85121

86122
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:

src/ModelContextProtocol.Core/Server/McpServerImpl.cs

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,62 @@ private void ConfigureCompletion(McpServerOptions options)
245245
var completeHandler = options.Handlers.CompleteHandler;
246246
var completionsCapability = options.Capabilities?.Completions;
247247

248-
if (completeHandler is null && completionsCapability is null)
248+
// Build completion value lookups from prompt/resource collections' [AllowedValues]-attributed parameters.
249+
Dictionary<string, Dictionary<string, string[]>>? promptCompletions = BuildAllowedValueCompletions(options.PromptCollection);
250+
Dictionary<string, Dictionary<string, string[]>>? resourceCompletions = BuildAllowedValueCompletions(options.ResourceCollection);
251+
bool hasCollectionCompletions = promptCompletions is not null || resourceCompletions is not null;
252+
253+
if (completeHandler is null && completionsCapability is null && !hasCollectionCompletions)
249254
{
250255
return;
251256
}
252257

253258
completeHandler ??= (static async (_, __) => new CompleteResult());
259+
260+
// Augment the completion handler with allowed values from prompt/resource collections.
261+
if (hasCollectionCompletions)
262+
{
263+
var originalCompleteHandler = completeHandler;
264+
completeHandler = async (request, cancellationToken) =>
265+
{
266+
CompleteResult result = await originalCompleteHandler(request, cancellationToken).ConfigureAwait(false);
267+
268+
string[]? allowedValues = null;
269+
switch (request.Params?.Ref)
270+
{
271+
case PromptReference pr when promptCompletions is not null:
272+
if (promptCompletions.TryGetValue(pr.Name, out var promptParams))
273+
{
274+
promptParams.TryGetValue(request.Params.Argument.Name, out allowedValues);
275+
}
276+
break;
277+
278+
case ResourceTemplateReference rtr when resourceCompletions is not null:
279+
if (rtr.Uri is not null && resourceCompletions.TryGetValue(rtr.Uri, out var resourceParams))
280+
{
281+
resourceParams.TryGetValue(request.Params.Argument.Name, out allowedValues);
282+
}
283+
break;
284+
}
285+
286+
if (allowedValues is not null)
287+
{
288+
string partialValue = request.Params!.Argument.Value;
289+
foreach (var v in allowedValues)
290+
{
291+
if (v.StartsWith(partialValue, StringComparison.OrdinalIgnoreCase))
292+
{
293+
result.Completion.Values.Add(v);
294+
}
295+
}
296+
297+
result.Completion.Total = result.Completion.Values.Count;
298+
}
299+
300+
return result;
301+
};
302+
}
303+
254304
completeHandler = BuildFilterPipeline(completeHandler, options.Filters.Request.CompleteFilters);
255305

256306
ServerCapabilities.Completions = new();
@@ -262,6 +312,76 @@ private void ConfigureCompletion(McpServerOptions options)
262312
McpJsonUtilities.JsonContext.Default.CompleteResult);
263313
}
264314

315+
/// <summary>
316+
/// Builds a lookup of primitive name/URI → (parameter name → allowed values) from the enum values
317+
/// in the JSON schemas of AIFunction-based prompts or resources.
318+
/// </summary>
319+
private static Dictionary<string, Dictionary<string, string[]>>? BuildAllowedValueCompletions<T>(
320+
McpServerPrimitiveCollection<T>? primitives) where T : class, IMcpServerPrimitive
321+
{
322+
if (primitives is null)
323+
{
324+
return null;
325+
}
326+
327+
Dictionary<string, Dictionary<string, string[]>>? result = null;
328+
foreach (var primitive in primitives)
329+
{
330+
JsonElement schema;
331+
string id;
332+
if (primitive is AIFunctionMcpServerPrompt aiPrompt)
333+
{
334+
schema = aiPrompt.AIFunction.JsonSchema;
335+
id = aiPrompt.ProtocolPrompt.Name;
336+
}
337+
else if (primitive is AIFunctionMcpServerResource aiResource && aiResource.IsTemplated)
338+
{
339+
schema = aiResource.AIFunction.JsonSchema;
340+
id = aiResource.ProtocolResourceTemplate.UriTemplate;
341+
}
342+
else
343+
{
344+
continue;
345+
}
346+
347+
if (schema.TryGetProperty("properties", out JsonElement properties) &&
348+
properties.ValueKind is JsonValueKind.Object)
349+
{
350+
Dictionary<string, string[]>? paramValues = null;
351+
foreach (var param in properties.EnumerateObject())
352+
{
353+
if (param.Value.TryGetProperty("enum", out JsonElement enumValues) &&
354+
enumValues.ValueKind is JsonValueKind.Array)
355+
{
356+
List<string>? values = null;
357+
foreach (var item in enumValues.EnumerateArray())
358+
{
359+
if (item.ValueKind is JsonValueKind.String && item.GetString() is { } str)
360+
{
361+
values ??= [];
362+
values.Add(str);
363+
}
364+
}
365+
366+
if (values is not null)
367+
{
368+
paramValues ??= new(StringComparer.Ordinal);
369+
paramValues[param.Name] = [.. values];
370+
}
371+
}
372+
}
373+
374+
if (paramValues is not null)
375+
{
376+
result ??= new(StringComparer.Ordinal);
377+
result[id] = paramValues;
378+
}
379+
}
380+
}
381+
382+
return result;
383+
}
384+
265385
private void ConfigureExperimentalAndExtensions(McpServerOptions options)
266386
{
267387
ServerCapabilities.Experimental = options.Capabilities?.Experimental;

src/ModelContextProtocol.Core/Server/McpServerPrompt.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ namespace ModelContextProtocol.Server;
116116
/// <para>
117117
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
118118
/// </para>
119+
/// <para>
120+
/// Parameters of type <see cref="string"/> that are decorated with <c>AllowedValuesAttribute</c>
121+
/// will automatically have their allowed values surfaced as completions in response to <c>completion/complete</c> requests from clients,
122+
/// without requiring a custom <see cref="McpServerHandlers.CompleteHandler"/> to be configured.
123+
/// </para>
119124
/// </remarks>
120125
public abstract class McpServerPrompt : IMcpServerPrimitive
121126
{

src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ namespace ModelContextProtocol.Server;
103103
/// <para>
104104
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
105105
/// </para>
106+
/// <para>
107+
/// Parameters of type <see cref="string"/> that are decorated with <c>AllowedValuesAttribute</c>
108+
/// will automatically have their allowed values surfaced as completions in response to <c>completion/complete</c> requests from clients,
109+
/// without requiring a custom <see cref="McpServerHandlers.CompleteHandler"/> to be configured.
110+
/// </para>
106111
/// </remarks>
107112
[AttributeUsage(AttributeTargets.Method)]
108113
public sealed class McpServerPromptAttribute : Attribute

src/ModelContextProtocol.Core/Server/McpServerResource.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ namespace ModelContextProtocol.Server;
121121
/// <para>
122122
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
123123
/// </para>
124+
/// <para>
125+
/// Parameters of type <see cref="string"/> that are decorated with <c>AllowedValuesAttribute</c>
126+
/// will automatically have their allowed values surfaced as completions in response to <c>completion/complete</c> requests from clients,
127+
/// without requiring a custom <see cref="McpServerHandlers.CompleteHandler"/> to be configured.
128+
/// </para>
124129
/// </remarks>
125130
public abstract class McpServerResource : IMcpServerPrimitive
126131
{

src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ namespace ModelContextProtocol.Server;
105105
/// <para>
106106
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
107107
/// </para>
108+
/// <para>
109+
/// Parameters of type <see cref="string"/> that are decorated with <c>AllowedValuesAttribute</c>
110+
/// will automatically have their allowed values surfaced as completions in response to <c>completion/complete</c> requests from clients,
111+
/// without requiring a custom <see cref="McpServerHandlers.CompleteHandler"/> to be configured.
112+
/// </para>
108113
/// </remarks>
109114
[AttributeUsage(AttributeTargets.Method)]
110115
public sealed class McpServerResourceAttribute : Attribute

0 commit comments

Comments
 (0)