using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Utils; using ModelContextProtocol.Utils.Json; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; namespace ModelContextProtocol.Server; /// Provides an that's implemented via an . internal sealed class AIFunctionMcpServerPrompt : McpServerPrompt { /// /// Creates an instance for a method, specified via a instance. /// public static new AIFunctionMcpServerPrompt Create( Delegate method, McpServerPromptCreateOptions? options) { Throw.IfNull(method); options = DeriveOptions(method.Method, options); return Create(method.Method, method.Target, options); } /// /// Creates an instance for a method, specified via a instance. /// public static new AIFunctionMcpServerPrompt Create( MethodInfo method, object? target, McpServerPromptCreateOptions? options) { Throw.IfNull(method); options = DeriveOptions(method, options); return Create( AIFunctionFactory.Create(method, target, CreateAIFunctionFactoryOptions(method, options)), options); } /// /// Creates an instance for a method, specified via a instance. /// public static new AIFunctionMcpServerPrompt Create( MethodInfo method, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, McpServerPromptCreateOptions? options) { Throw.IfNull(method); options = DeriveOptions(method, options); return Create( AIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, options)), options); } private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MethodInfo method, McpServerPromptCreateOptions? options) => new() { Name = options?.Name ?? method.GetCustomAttribute()?.Name, Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), SerializerOptions = options?.SerializerOptions ?? McpJsonUtilities.DefaultOptions, CreateInstance = AIFunctionMcpServerTool.GetCreateInstanceFunc(), ConfigureParameterBinding = pi => { if (pi.ParameterType == typeof(RequestContext)) { return new() { ExcludeFromSchema = true, BindParameter = (pi, args) => GetRequestContext(args), }; } if (pi.ParameterType == typeof(IMcpServer)) { return new() { ExcludeFromSchema = true, BindParameter = (pi, args) => GetRequestContext(args)?.Server, }; } if (pi.ParameterType == typeof(IProgress)) { // Bind IProgress to the progress token in the request, // if there is one. If we can't get one, return a nop progress. return new() { ExcludeFromSchema = true, BindParameter = (pi, args) => { var requestContent = GetRequestContext(args); if (requestContent?.Server is { } server && requestContent?.Params?.Meta?.ProgressToken is { } progressToken) { return new TokenProgress(server, progressToken); } return NullProgress.Instance; }, }; } if (options?.Services is { } services && services.GetService() is { } ispis && ispis.IsService(pi.ParameterType)) { return new() { ExcludeFromSchema = true, BindParameter = (pi, args) => GetRequestContext(args)?.Services?.GetService(pi.ParameterType) ?? (pi.HasDefaultValue ? null : throw new ArgumentException("No service of the requested type was found.")), }; } if (pi.GetCustomAttribute() is { } keyedAttr) { return new() { ExcludeFromSchema = true, BindParameter = (pi, args) => (GetRequestContext(args)?.Services as IKeyedServiceProvider)?.GetKeyedService(pi.ParameterType, keyedAttr.Key) ?? (pi.HasDefaultValue ? null : throw new ArgumentException("No service of the requested type was found.")), }; } return default; static RequestContext? GetRequestContext(AIFunctionArguments args) { if (args.Context?.TryGetValue(typeof(RequestContext), out var orc) is true && orc is RequestContext requestContext) { return requestContext; } return null; } }, JsonSchemaCreateOptions = options?.SchemaCreateOptions, }; /// Creates an that wraps the specified . public static new AIFunctionMcpServerPrompt Create(AIFunction function, McpServerPromptCreateOptions? options) { Throw.IfNull(function); List args = []; HashSet? requiredProps = function.JsonSchema.TryGetProperty("required", out JsonElement required) ? new(required.EnumerateArray().Select(p => p.GetString()!), StringComparer.Ordinal) : null; if (function.JsonSchema.TryGetProperty("properties", out JsonElement properties)) { foreach (var param in properties.EnumerateObject()) { args.Add(new() { Name = param.Name, Description = param.Value.TryGetProperty("description", out JsonElement description) ? description.GetString() : null, Required = requiredProps?.Contains(param.Name) ?? false, }); } } Prompt prompt = new() { Name = options?.Name ?? function.Name, Description = options?.Description ?? function.Description, Arguments = args, }; return new AIFunctionMcpServerPrompt(function, prompt); } private static McpServerPromptCreateOptions DeriveOptions(MethodInfo method, McpServerPromptCreateOptions? options) { McpServerPromptCreateOptions newOptions = options?.Clone() ?? new(); if (method.GetCustomAttribute() is { } promptAttr) { newOptions.Name ??= promptAttr.Name; } if (method.GetCustomAttribute() is { } descAttr) { newOptions.Description ??= descAttr.Description; } return newOptions; } /// Gets the wrapped by this prompt. internal AIFunction AIFunction { get; } /// Initializes a new instance of the class. private AIFunctionMcpServerPrompt(AIFunction function, Prompt prompt) { AIFunction = function; ProtocolPrompt = prompt; } /// public override string ToString() => AIFunction.ToString(); /// public override Prompt ProtocolPrompt { get; } /// public override async ValueTask GetAsync( RequestContext request, CancellationToken cancellationToken = default) { Throw.IfNull(request); cancellationToken.ThrowIfCancellationRequested(); AIFunctionArguments arguments = new() { Services = request.Services, Context = new Dictionary() { [typeof(RequestContext)] = request } }; var argDict = request.Params?.Arguments; if (argDict is not null) { foreach (var kvp in argDict) { arguments[kvp.Key] = kvp.Value; } } object? result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false); return result switch { GetPromptResult getPromptResult => getPromptResult, string text => new() { Description = ProtocolPrompt.Description, Messages = [new() { Role = Role.User, Content = new() { Text = text, Type = "text" } }], }, PromptMessage promptMessage => new() { Description = ProtocolPrompt.Description, Messages = [promptMessage], }, IEnumerable promptMessages => new() { Description = ProtocolPrompt.Description, Messages = [.. promptMessages], }, ChatMessage chatMessage => new() { Description = ProtocolPrompt.Description, Messages = [.. chatMessage.ToPromptMessages()], }, IEnumerable chatMessages => new() { Description = ProtocolPrompt.Description, Messages = [.. chatMessages.SelectMany(chatMessage => chatMessage.ToPromptMessages())], }, null => throw new InvalidOperationException("Null result returned from prompt function."), _ => throw new InvalidOperationException($"Unknown result type '{result.GetType()}' returned from prompt function."), }; } }