using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; using ModelContextProtocol.Protocol.Types; using ModelContextProtocol.Utils; using ModelContextProtocol.Utils.Json; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; namespace ModelContextProtocol.Server; /// Provides an that's implemented via an . internal sealed class AIFunctionMcpServerTool : McpServerTool { /// /// Creates an instance for a method, specified via a instance. /// public static new AIFunctionMcpServerTool Create( Delegate method, McpServerToolCreateOptions? 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 AIFunctionMcpServerTool Create( MethodInfo method, object? target, McpServerToolCreateOptions? 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 AIFunctionMcpServerTool Create( MethodInfo method, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type targetType, McpServerToolCreateOptions? options) { Throw.IfNull(method); options = DeriveOptions(method, options); return Create( AIFunctionFactory.Create(method, targetType, CreateAIFunctionFactoryOptions(method, options)), options); } private static AIFunctionFactoryOptions CreateAIFunctionFactoryOptions( MethodInfo method, McpServerToolCreateOptions? options) => new() { Name = options?.Name ?? method.GetCustomAttribute()?.Name, Description = options?.Description, MarshalResult = static (result, _, cancellationToken) => new ValueTask(result), 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; }, }; } // We assume that if the services used to create the tool support a particular type, // so too do the services associated with the server. This is the same basic assumption // made in ASP.NET. if (options?.Services is { } services && services.GetService() is { } ispis && ispis.IsService(pi.ParameterType)) { return new() { ExcludeFromSchema = true, BindParameter = (pi, args) => GetRequestContext(args)?.Server?.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)?.Server?.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; } }, }; /// Creates an that wraps the specified . public static new AIFunctionMcpServerTool Create(AIFunction function, McpServerToolCreateOptions? options) { Throw.IfNull(function); Tool tool = new() { Name = options?.Name ?? function.Name, Description = options?.Description ?? function.Description, InputSchema = function.JsonSchema, }; if (options is not null) { if (options.Title is not null || options.Idempotent is not null || options.Destructive is not null || options.OpenWorld is not null || options.ReadOnly is not null) { tool.Annotations = new() { Title = options?.Title, IdempotentHint = options?.Idempotent, DestructiveHint = options?.Destructive, OpenWorldHint = options?.OpenWorld, ReadOnlyHint = options?.ReadOnly, }; } } return new AIFunctionMcpServerTool(function, tool); } private static McpServerToolCreateOptions? DeriveOptions(MethodInfo method, McpServerToolCreateOptions? options) { McpServerToolCreateOptions newOptions = options?.Clone() ?? new(); if (method.GetCustomAttribute() is { } attr) { newOptions.Name ??= attr.Name; newOptions.Title ??= attr.Title; if (attr._destructive is bool destructive) { newOptions.Destructive ??= destructive; } if (attr._idempotent is bool idempotent) { newOptions.Idempotent ??= idempotent; } if (attr._openWorld is bool openWorld) { newOptions.OpenWorld ??= openWorld; } if (attr._readOnly is bool readOnly) { newOptions.ReadOnly ??= readOnly; } } return newOptions; } /// Gets the wrapped by this tool. internal AIFunction AIFunction { get; } /// Initializes a new instance of the class. private AIFunctionMcpServerTool(AIFunction function, Tool tool) { AIFunction = function; ProtocolTool = tool; } /// public override string ToString() => AIFunction.ToString(); /// public override Tool ProtocolTool { get; } /// public override async Task InvokeAsync( RequestContext request, CancellationToken cancellationToken = default) { Throw.IfNull(request); cancellationToken.ThrowIfCancellationRequested(); AIFunctionArguments arguments = new() { Services = request.Server?.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; try { result = await AIFunction.InvokeAsync(arguments, cancellationToken).ConfigureAwait(false); } catch (Exception e) when (e is not OperationCanceledException) { return new CallToolResponse() { IsError = true, Content = [new() { Text = $"An error occurred invoking '{request.Params?.Name}'.", Type = "text" }], }; } return result switch { AIContent aiContent => new() { Content = [aiContent.ToContent()] }, null => new() { Content = [] }, string text => new() { Content = [new() { Text = text, Type = "text" }] }, Content content => new() { Content = [content] }, IEnumerable texts => new() { Content = [.. texts.Select(x => new Content() { Type = "text", Text = x ?? string.Empty })] }, IEnumerable contentItems => new() { Content = [.. contentItems.Select(static item => item.ToContent())] }, IEnumerable contents => new() { Content = [.. contents] }, CallToolResponse callToolResponse => callToolResponse, _ => new() { Content = [new() { Text = JsonSerializer.Serialize(result, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), Type = "text" }] }, }; } }