Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -245,12 +245,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 @@ -262,6 +312,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
Loading