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
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,13 @@ private static McpServerPromptCreateOptions DeriveOptions(MethodInfo method, Mcp
/// <summary>Gets the <see cref="AIFunction"/> wrapped by this prompt.</summary>
internal AIFunction AIFunction { get; }

/// <summary>Backing cache for <see cref="AllowedValueCompletions"/>.</summary>
private object? _allowedValueCompletions;

/// <summary>Gets the values that each restricted parameter of the prompt allows, or <see langword="null"/> if none are restricted.</summary>
internal Dictionary<string, string[]>? AllowedValueCompletions =>
Server.AllowedValueCompletions.GetOrCreate(ref _allowedValueCompletions, AIFunction.JsonSchema);

/// <summary>Initializes a new instance of the <see cref="McpServerPrompt"/> class.</summary>
private AIFunctionMcpServerPrompt(AIFunction function, Prompt prompt, IReadOnlyList<object> metadata)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,13 @@ private static string DeriveUriTemplate(string name, AIFunction function)
/// <summary>Gets the <see cref="AIFunction"/> wrapped by this resource.</summary>
internal AIFunction AIFunction { get; }

/// <summary>Backing cache for <see cref="AllowedValueCompletions"/>.</summary>
private object? _allowedValueCompletions;

/// <summary>Gets the values that each restricted parameter of the resource allows, or <see langword="null"/> if none are restricted.</summary>
internal Dictionary<string, string[]>? AllowedValueCompletions =>
Server.AllowedValueCompletions.GetOrCreate(ref _allowedValueCompletions, AIFunction.JsonSchema);

/// <summary>Initializes a new instance of the <see cref="McpServerResource"/> class.</summary>
private AIFunctionMcpServerResource(AIFunction function, ResourceTemplate resourceTemplate, IReadOnlyList<object> metadata)
{
Expand Down
68 changes: 68 additions & 0 deletions src/ModelContextProtocol.Core/Server/AllowedValueCompletions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System.Text.Json;

namespace ModelContextProtocol.Server;

/// <summary>
/// Extracts the values that parameters of an AIFunction-based prompt or resource are restricted to, from the enum values
/// in the function's JSON schema, for use as completions.
/// </summary>
internal static class AllowedValueCompletions
{
/// <summary>Marks a cache whose function was found to have no restricted parameters.</summary>
private static readonly object s_none = new();

/// <summary>
/// Gets the parameter name → allowed values lookup for <paramref name="schema"/>, computing and caching it in
/// <paramref name="cache"/> on first use. The returned dictionary is shared and must not be modified.
/// </summary>
/// <remarks>
/// The schema of an AIFunction is immutable, so the result can be computed once per primitive rather than once per
/// session. Concurrent first calls may each compute it; the results are equivalent.
/// </remarks>
public static Dictionary<string, string[]>? GetOrCreate(ref object? cache, JsonElement schema)
{
object? cached = Volatile.Read(ref cache);
if (cached is null)
{
cached = (object?)Create(schema) ?? s_none;
Volatile.Write(ref cache, cached);
}

return cached as Dictionary<string, string[]>;
}

private static Dictionary<string, string[]>? Create(JsonElement schema)
{
if (!schema.TryGetProperty("properties", out JsonElement properties) ||
properties.ValueKind is not JsonValueKind.Object)
{
return null;
}

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];
}
}
}

return paramValues;
}
}
40 changes: 6 additions & 34 deletions src/ModelContextProtocol.Core/Server/McpServerImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1119,55 +1119,27 @@ private void ConfigureCompletion(McpServerOptions options)
Dictionary<string, Dictionary<string, string[]>>? result = null;
foreach (var primitive in primitives)
{
JsonElement schema;
Dictionary<string, string[]>? paramValues;
string id;
if (primitive is AIFunctionMcpServerPrompt aiPrompt)
{
schema = aiPrompt.AIFunction.JsonSchema;
paramValues = aiPrompt.AllowedValueCompletions;
id = aiPrompt.ProtocolPrompt.Name;
}
else if (primitive is AIFunctionMcpServerResource aiResource && aiResource.IsTemplated)
{
schema = aiResource.AIFunction.JsonSchema;
paramValues = aiResource.AllowedValueCompletions;
id = aiResource.ProtocolResourceTemplate.UriTemplate;
}
else
{
continue;
}

if (schema.TryGetProperty("properties", out JsonElement properties) &&
properties.ValueKind is JsonValueKind.Object)
if (paramValues is not null)
{
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;
}
result ??= new(StringComparer.Ordinal);
result[id] = paramValues;
}
}

Expand Down
Loading
Loading