using PostFundManagement.Application.Abstractions.Mcp; namespace PostFundManagement.Application.Mcp; public class InAppMcpClient : IInAppMcpClient { private static bool initialized; private readonly IServiceProvider serviceProvider; private readonly ILoggerFactory loggerFactory; private static readonly ConcurrentDictionary ToolRegistry = new(StringComparer.OrdinalIgnoreCase); private static readonly List ToolDefinitions = []; private static readonly Lock InitLock = new(); private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; public static readonly Type[] ToolTypes = [ typeof(GrantsMcpTools), typeof(ProjectsMcpTools), typeof(ExecutionMcpTools), typeof(GovernanceMcpTools), typeof(EvidenceMcpTools), typeof(CommunicationsMcpTools), typeof(UsersMcpTools) ]; public InAppMcpClient(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) { this.serviceProvider = serviceProvider; this.loggerFactory = loggerFactory; EnsureInitialized(); } private static void EnsureInitialized() { if (initialized) return; lock (InitLock) { if (initialized) return; foreach (var toolType in ToolTypes) { var groupName = toolType.Name.Replace("McpTools", string.Empty); foreach (var method in toolType.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { var toolAttr = method.GetCustomAttribute(); if (toolAttr == null) continue; var toolName = !string.IsNullOrWhiteSpace(toolAttr.Name) ? toolAttr.Name : method.Name; var title = !string.IsNullOrWhiteSpace(toolAttr.Title) ? toolAttr.Title : toolName; var descAttr = method.GetCustomAttribute(); var description = descAttr?.Description ?? title; var parameters = new List(); var methodParams = method.GetParameters(); foreach (var p in methodParams) { if (p.ParameterType == typeof(CancellationToken)) continue; var pDescAttr = p.GetCustomAttribute(); parameters.Add(new McpToolParameter(Name: p.Name ?? "param", Type: p.ParameterType.Name, Description: pDescAttr?.Description ?? string.Empty, IsRequired: !p.HasDefaultValue && !IsNullable(p.ParameterType), DefaultValue: p.HasDefaultValue ? p.DefaultValue : null)); } var toolDef = new McpToolDefinition(Name: toolName, Title: title, Description: description, Group: groupName, ReadOnly: toolAttr.ReadOnly, Parameters: parameters); ToolDefinitions.Add(toolDef); ToolRegistry[toolName] = new ToolMethodInfo(toolType, method, methodParams); } } initialized = true; } } public IReadOnlyList GetAvailableTools() => ToolDefinitions; public async Task ExecuteToolAsync(string toolName, IReadOnlyDictionary? arguments = null, CancellationToken cancellationToken = default) { if (!ToolRegistry.TryGetValue(toolName, out var toolMethod)) return McpToolExecutionResult.Fail($"MCP tool '{toolName}' is not recognized."); try { var targetInstance = serviceProvider.GetRequiredService(toolMethod.TargetType); var args = BindArguments(toolMethod.Parameters, arguments, cancellationToken); var invokeResult = toolMethod.Method.Invoke(targetInstance, args); if (invokeResult is Task task) { await task.ConfigureAwait(false); var resultProperty = task.GetType().GetProperty("Result"); var value = resultProperty?.GetValue(task); return McpToolExecutionResult.Ok(value); } return McpToolExecutionResult.Ok(invokeResult); } catch (TargetInvocationException ex) { var inner = ex.InnerException ?? ex; return McpToolExecutionResult.Fail($"Execution of tool '{toolName}' failed: {inner.Message}"); } catch (Exception ex) { return McpToolExecutionResult.Fail($"Execution of tool '{toolName}' failed: {ex.Message}"); } } public async Task CreateProtocolClientAsync(CancellationToken cancellationToken = default) { var serverToClient = new Pipe(); var clientToServer = new Pipe(); var serverIn = clientToServer.Reader.AsStream(); var serverOut = serverToClient.Writer.AsStream(); var clientIn = serverToClient.Reader.AsStream(); var clientOut = clientToServer.Writer.AsStream(); var inMemoryServices = new ServiceCollection(); inMemoryServices.AddLogging(); foreach (var t in ToolTypes) inMemoryServices.AddScoped(t, _ => serviceProvider.GetRequiredService(t)); var mcpBuilder = inMemoryServices.AddMcpServer(); foreach (var t in ToolTypes) mcpBuilder.WithTools(t, (JsonSerializerOptions?)null); mcpBuilder.WithStreamServerTransport(serverIn, serverOut); var inMemoryProvider = inMemoryServices.BuildServiceProvider(); var hostedServices = inMemoryProvider.GetServices(); foreach (var hs in hostedServices) _ = hs.StartAsync(cancellationToken); var clientTransport = new StreamClientTransport(serverInput: clientOut, serverOutput: clientIn, loggerFactory); return await McpClient.CreateAsync(clientTransport, cancellationToken: cancellationToken); } private static object?[] BindArguments(ParameterInfo[] parameters, IReadOnlyDictionary? arguments, CancellationToken cancellationToken) { var result = new object?[parameters.Length]; var args = arguments ?? new Dictionary(StringComparer.OrdinalIgnoreCase); for (var i = 0; i < parameters.Length; i++) { var p = parameters[i]; if (p.ParameterType == typeof(CancellationToken)) { result[i] = cancellationToken; continue; } var matchKey = args.Keys.FirstOrDefault(k => string.Equals(k, p.Name, StringComparison.OrdinalIgnoreCase)); if (matchKey != null && args.TryGetValue(matchKey, out var val) && val != null) result[i] = ConvertValue(val, p.ParameterType); else if (p.HasDefaultValue) result[i] = p.DefaultValue; else if (p.ParameterType == typeof(Guid) && p.Name?.Contains("userId", StringComparison.OrdinalIgnoreCase) == true) result[i] = SystemUser; else if (IsNullable(p.ParameterType)) result[i] = null; else result[i] = p.ParameterType.IsValueType ? Activator.CreateInstance(p.ParameterType) : null; } return result; } private static object? ConvertValue(object val, Type targetType) { if (val == null) return null; var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; if (underlying.IsInstanceOfType(val)) return val; if (val is JsonElement jsonElement) return JsonSerializer.Deserialize(jsonElement.GetRawText(), targetType, JsonOptions); var str = val.ToString(); if (str == null) return null; if (underlying == typeof(Guid)) return Guid.Parse(str); if (underlying.IsEnum) return Enum.Parse(underlying, str, true); if (underlying.IsPrimitive || underlying == typeof(decimal) || underlying == typeof(string) || underlying == typeof(DateTime)) return Convert.ChangeType(val, underlying, CultureInfo.InvariantCulture); return JsonSerializer.Deserialize(JsonSerializer.Serialize(val), targetType, JsonOptions); } private static bool IsNullable(Type type) => !type.IsValueType || Nullable.GetUnderlyingType(type) != null; }