Skip to content
Open
36 changes: 36 additions & 0 deletions docs/concepts/completions/completions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <xref:ModelContextProtocol.Client.McpClient.CompleteAsync*>. Provide a reference to the prompt or resource template, the argument name, and the current partial value:
Expand Down
122 changes: 121 additions & 1 deletion src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -254,12 +254,62 @@ 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<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;
}

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);

ServerCapabilities.Completions = new();
Expand All @@ -271,6 +321,76 @@ private void ConfigureCompletion(McpServerOptions options)
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 ConfigureExperimental(McpServerOptions options)
{
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
Expand Down
5 changes: 5 additions & 0 deletions src/ModelContextProtocol.Core/Server/McpServerPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ namespace ModelContextProtocol.Server;
/// <para>
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
/// </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>
public abstract class McpServerPrompt : IMcpServerPrimitive
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ namespace ModelContextProtocol.Server;
/// <para>
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
/// </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>
[AttributeUsage(AttributeTargets.Method)]
public sealed class McpServerPromptAttribute : Attribute
Expand Down
5 changes: 5 additions & 0 deletions src/ModelContextProtocol.Core/Server/McpServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ namespace ModelContextProtocol.Server;
/// <para>
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
/// </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>
public abstract class McpServerResource : IMcpServerPrimitive
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ namespace ModelContextProtocol.Server;
/// <para>
/// Other returned types will result in an <see cref="InvalidOperationException"/> being thrown.
/// </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>
[AttributeUsage(AttributeTargets.Method)]
public sealed class McpServerResourceAttribute : Attribute
Expand Down
Loading