From 350c479486d052af4d2c4f36d06edc34f257e708 Mon Sep 17 00:00:00 2001 From: Jicheng Lu <103353@smsassist.com> Date: Thu, 15 May 2025 17:46:36 -0500 Subject: [PATCH] sync and refine --- .../MCP/Services/IMcpService.cs | 2 +- .../Routing/Executor/IFunctionExecutor.cs | 3 +- .../MCP/BotSharpMCPExtensions.cs | 8 +- .../MCP/Helpers/AiFunctionHelper.cs | 20 +++-- .../MCP/Hooks/MCPToolAgentHook.cs | 22 +++-- .../MCP/Managers/McpClientManager.cs | 80 ++++++++++++------- .../BotSharp.Core/MCP/Services/McpService.cs | 19 +++-- .../BotSharp.Core/MCP/Settings/MCPSettings.cs | 1 - .../Routing/Executor/DummyFunctionExecutor.cs | 10 +-- .../Executor/FunctionCallbackExecutor.cs | 9 ++- .../Executor/FunctionExecutorFactory.cs | 38 ++++----- .../Routing/Executor/MCPToolExecutor.cs | 24 +++--- .../Routing/RoutingService.InvokeFunction.cs | 14 +--- .../Controllers/McpController.cs | 4 +- src/WebStarter/appsettings.json | 15 ++-- 15 files changed, 138 insertions(+), 131 deletions(-) rename src/Infrastructure/{BotSharp.Core => BotSharp.Abstraction}/Routing/Executor/IFunctionExecutor.cs (77%) diff --git a/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpService.cs b/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpService.cs index 71952dac..32564a3f 100644 --- a/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpService.cs +++ b/src/Infrastructure/BotSharp.Abstraction/MCP/Services/IMcpService.cs @@ -2,5 +2,5 @@ namespace BotSharp.Abstraction.MCP.Services; public interface IMcpService { - IEnumerable GetServerConfigs() => []; + Task> GetServerConfigsAsync() => Task.FromResult>([]); } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/IFunctionExecutor.cs b/src/Infrastructure/BotSharp.Abstraction/Routing/Executor/IFunctionExecutor.cs similarity index 77% rename from src/Infrastructure/BotSharp.Core/Routing/Executor/IFunctionExecutor.cs rename to src/Infrastructure/BotSharp.Abstraction/Routing/Executor/IFunctionExecutor.cs index 4ba2e69f..e1c604ad 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/IFunctionExecutor.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Routing/Executor/IFunctionExecutor.cs @@ -1,8 +1,7 @@ -namespace BotSharp.Core.Routing.Executor; +namespace BotSharp.Abstraction.Routing.Executor; public interface IFunctionExecutor { public Task ExecuteAsync(RoleDialogModel message); - public Task GetIndicatorAsync(RoleDialogModel message); } diff --git a/src/Infrastructure/BotSharp.Core/MCP/BotSharpMCPExtensions.cs b/src/Infrastructure/BotSharp.Core/MCP/BotSharpMCPExtensions.cs index b3c4e6de..8eeee7b3 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/BotSharpMCPExtensions.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/BotSharpMCPExtensions.cs @@ -18,15 +18,11 @@ public static class BotSharpMcpExtensions { var settings = config.GetSection("MCP").Get(); services.AddScoped(provider => settings); + services.AddScoped(); if (settings != null && settings.Enabled && !settings.McpServerConfigs.IsNullOrEmpty()) { - services.AddScoped(); - - var clientManager = new McpClientManager(settings); - services.AddScoped(provider => clientManager); - - // Register hooks + services.AddScoped(); services.AddScoped(); } return services; diff --git a/src/Infrastructure/BotSharp.Core/MCP/Helpers/AiFunctionHelper.cs b/src/Infrastructure/BotSharp.Core/MCP/Helpers/AiFunctionHelper.cs index d6f1cb5b..b9db3ce7 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/Helpers/AiFunctionHelper.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/Helpers/AiFunctionHelper.cs @@ -1,19 +1,25 @@ -using System.Text.Json; using ModelContextProtocol.Client; namespace BotSharp.Core.MCP.Helpers; internal static class AiFunctionHelper { - public static FunctionDef MapToFunctionDef(McpClientTool tool) + public static FunctionDef? MapToFunctionDef(McpClientTool tool) { if (tool == null) { - throw new ArgumentNullException(nameof(tool)); + return null; } - var properties = tool.JsonSchema.GetProperty("properties"); - var required = tool.JsonSchema.GetProperty("required"); + if (!tool.JsonSchema.TryGetProperty("properties", out var properties)) + { + properties = JsonDocument.Parse("{}").RootElement; + } + + if (!tool.JsonSchema.TryGetProperty("required", out var required)) + { + required = JsonDocument.Parse("[]").RootElement; + } var funDef = new FunctionDef { @@ -23,8 +29,8 @@ internal static class AiFunctionHelper Parameters = new FunctionParametersDef { Type = "object", - Properties = JsonDocument.Parse(properties.GetRawText()), - Required = JsonSerializer.Deserialize>(required.GetRawText()) + Properties = JsonDocument.Parse(properties.GetRawText() ?? "{}"), + Required = JsonSerializer.Deserialize>(required.GetRawText() ?? "[]") ?? [] } }; diff --git a/src/Infrastructure/BotSharp.Core/MCP/Hooks/MCPToolAgentHook.cs b/src/Infrastructure/BotSharp.Core/MCP/Hooks/MCPToolAgentHook.cs index 08c38b6b..743bf4c0 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/Hooks/MCPToolAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/Hooks/MCPToolAgentHook.cs @@ -41,18 +41,26 @@ public class McpToolAgentHook : AgentHookBase return functionDefs; } - var mcpClientManager = _services.GetRequiredService(); - var mcps = agent.McpTools.Where(x => !x.Disabled); + var mcpClientManager = _services.GetService(); + if (mcpClientManager == null) + { + return functionDefs; + } + + var mcps = agent.McpTools?.Where(x => !x.Disabled) ?? []; foreach (var item in mcps) { var mcpClient = await mcpClientManager.GetMcpClientAsync(item.ServerId); - if (mcpClient != null) + if (mcpClient == null) continue; + + var tools = await mcpClient.ListToolsAsync(); + var toolNames = item.Functions.Select(x => x.Name).ToList(); + var targetTools = tools.Where(x => toolNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase)); + foreach (var tool in targetTools) { - var tools = await mcpClient.ListToolsAsync(); - var toolnames = item.Functions.Select(x => x.Name).ToList(); - foreach (var tool in tools.Where(x => toolnames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) + var funDef = AiFunctionHelper.MapToFunctionDef(tool); + if (funDef != null) { - var funDef = AiFunctionHelper.MapToFunctionDef(tool); functionDefs.Add(funDef); } } diff --git a/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs b/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs index a4a89d89..50b798eb 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/Managers/McpClientManager.cs @@ -6,45 +6,63 @@ namespace BotSharp.Core.MCP.Managers; public class McpClientManager : IDisposable { - private readonly McpSettings _mcpSettings; + private readonly IServiceProvider _services; + private readonly ILogger _logger; - public McpClientManager(McpSettings mcpSettings) + public McpClientManager( + IServiceProvider services, + ILogger logger) { - _mcpSettings = mcpSettings; + _services = services; + _logger = logger; } - public async Task GetMcpClientAsync(string serverId) + public async Task GetMcpClientAsync(string serverId) { - var config = _mcpSettings.McpServerConfigs.Where(x => x.Id == serverId).FirstOrDefault(); - - IClientTransport transport; - if (config.SseConfig != null) + try { - transport = new SseClientTransport(new SseClientTransportOptions + var settings = _services.GetRequiredService(); + var config = settings.McpServerConfigs.Where(x => x.Id == serverId).FirstOrDefault(); + if (config == null) { - Name = config.Name, - Endpoint = new Uri(config.SseConfig.EndPoint), - AdditionalHeaders = config.SseConfig.AdditionalHeaders, - ConnectionTimeout = config.SseConfig.ConnectionTimeout - }); - } - else if (config.StdioConfig != null) - { - transport = new StdioClientTransport(new StdioClientTransportOptions - { - Name = config.Name, - Command = config.StdioConfig.Command, - Arguments = config.StdioConfig.Arguments, - EnvironmentVariables = config.StdioConfig.EnvironmentVariables, - ShutdownTimeout = config.StdioConfig.ShutdownTimeout - }); - } - else - { - throw new ArgumentNullException("Invalid MCP server configuration!"); - } + return null; + } - return await McpClientFactory.CreateAsync(transport, _mcpSettings.McpClientOptions); + IClientTransport? transport = null; + if (config.SseConfig != null) + { + transport = new SseClientTransport(new SseClientTransportOptions + { + Name = config.Name, + Endpoint = new Uri(config.SseConfig.EndPoint), + AdditionalHeaders = config.SseConfig.AdditionalHeaders, + ConnectionTimeout = config.SseConfig.ConnectionTimeout + }); + } + else if (config.StdioConfig != null) + { + transport = new StdioClientTransport(new StdioClientTransportOptions + { + Name = config.Name, + Command = config.StdioConfig.Command, + Arguments = config.StdioConfig.Arguments, + EnvironmentVariables = config.StdioConfig.EnvironmentVariables, + ShutdownTimeout = config.StdioConfig.ShutdownTimeout + }); + } + + if (transport == null) + { + return null; + } + + return await McpClientFactory.CreateAsync(transport, settings.McpClientOptions); + } + catch (Exception ex) + { + _logger.LogWarning(ex, $"Error when loading mcp client {serverId}"); + return null; + } } public void Dispose() diff --git a/src/Infrastructure/BotSharp.Core/MCP/Services/McpService.cs b/src/Infrastructure/BotSharp.Core/MCP/Services/McpService.cs index 3bff4442..27f5326c 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/Services/McpService.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/Services/McpService.cs @@ -1,6 +1,5 @@ using BotSharp.Core.MCP.Managers; using BotSharp.Core.MCP.Settings; -using Microsoft.Extensions.Logging; using ModelContextProtocol.Client; namespace BotSharp.Core.MCP.Services; @@ -9,35 +8,35 @@ public class McpService : IMcpService { private readonly IServiceProvider _services; private readonly ILogger _logger; - private readonly McpClientManager _mcpClientManager; public McpService( IServiceProvider services, - ILogger logger, - McpClientManager mcpClient) + ILogger logger) { _services = services; _logger = logger; - _mcpClientManager = mcpClient; } - public IEnumerable GetServerConfigs() + public async Task> GetServerConfigsAsync() { + var clientManager = _services.GetService(); + if (clientManager == null) return []; + var options = new List(); var settings = _services.GetRequiredService(); var configs = settings?.McpServerConfigs ?? []; foreach (var config in configs) { - var tools = _mcpClientManager.GetMcpClientAsync(config.Id) - .Result.ListToolsAsync() - .Result.Select(x=> x.Name); + var client = await clientManager.GetMcpClientAsync(config.Id); + if (client == null) continue; + var tools = await client.ListToolsAsync(); options.Add(new McpServerOptionModel { Id = config.Id, Name = config.Name, - Tools = tools + Tools = tools.Select(x => x.Name) }); } diff --git a/src/Infrastructure/BotSharp.Core/MCP/Settings/MCPSettings.cs b/src/Infrastructure/BotSharp.Core/MCP/Settings/MCPSettings.cs index 2867712f..33723057 100644 --- a/src/Infrastructure/BotSharp.Core/MCP/Settings/MCPSettings.cs +++ b/src/Infrastructure/BotSharp.Core/MCP/Settings/MCPSettings.cs @@ -7,5 +7,4 @@ public class McpSettings public bool Enabled { get; set; } = true; public McpClientOptions McpClientOptions { get; set; } public List McpServerConfigs { get; set; } = []; - } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/DummyFunctionExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/DummyFunctionExecutor.cs index d075e185..91e0bc97 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/DummyFunctionExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/DummyFunctionExecutor.cs @@ -1,19 +1,19 @@ +using BotSharp.Abstraction.Routing.Executor; using BotSharp.Abstraction.Templating; namespace BotSharp.Core.Routing.Executor; public class DummyFunctionExecutor: IFunctionExecutor { - private FunctionDef functionDef; private readonly IServiceProvider _services; + private readonly FunctionDef _functionDef; - public DummyFunctionExecutor(FunctionDef function, IServiceProvider services) + public DummyFunctionExecutor(IServiceProvider services, FunctionDef functionDef) { - functionDef = function; _services = services; + _functionDef = functionDef; } - public async Task ExecuteAsync(RoleDialogModel message) { var render = _services.GetRequiredService(); @@ -25,7 +25,7 @@ public class DummyFunctionExecutor: IFunctionExecutor dict[item.Key] = item.Value; } - var text = render.Render(functionDef.Output, dict); + var text = render.Render(_functionDef.Output!, dict); message.Content = text; return true; } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs index 939f4fb3..4b208374 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionCallbackExecutor.cs @@ -1,23 +1,24 @@ +using BotSharp.Abstraction.Routing.Executor; using BotSharp.Abstraction.Functions; namespace BotSharp.Core.Routing.Executor; public class FunctionCallbackExecutor : IFunctionExecutor { - IFunctionCallback functionCallback; + private readonly IFunctionCallback _functionCallback; public FunctionCallbackExecutor(IFunctionCallback functionCallback) { - this.functionCallback = functionCallback; + _functionCallback = functionCallback; } public async Task ExecuteAsync(RoleDialogModel message) { - return await functionCallback.Execute(message); + return await _functionCallback.Execute(message); } public async Task GetIndicatorAsync(RoleDialogModel message) { - return await functionCallback.GetIndication(message); + return await _functionCallback.GetIndication(message); } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs index 3fb89509..8a4a5486 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/FunctionExecutorFactory.cs @@ -1,41 +1,31 @@ using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Routing.Executor; namespace BotSharp.Core.Routing.Executor; internal class FunctionExecutorFactory { - public static IFunctionExecutor Create(string functionName, Agent agent, IFunctionCallback functioncall, IServiceProvider serviceProvider) + public static IFunctionExecutor? Create(IServiceProvider services, string functionName, Agent agent) { - if(functioncall != null) + var functionCall = services.GetServices().FirstOrDefault(x => x.Name == functionName); + if (functionCall != null) { - return new FunctionCallbackExecutor(functioncall); + return new FunctionCallbackExecutor(functionCall); } - var funDef = agent?.Functions?.FirstOrDefault(x => x.Name == functionName); - if (funDef != null) + var functions = (agent?.Functions ?? []).Concat(agent?.SecondaryFunctions ?? []); + var funcDef = functions.FirstOrDefault(x => x.Name == functionName); + if (!string.IsNullOrWhiteSpace(funcDef?.Output)) { - if (!string.IsNullOrWhiteSpace(funDef?.Output)) - { - return new DummyFunctionExecutor(funDef,serviceProvider); - } + return new DummyFunctionExecutor(services, funcDef); } - else + + var mcpServerId = agent?.McpTools?.Where(x => x.Functions.Any(y => y.Name == funcDef?.Name))?.FirstOrDefault()?.ServerId; + if (!string.IsNullOrWhiteSpace(mcpServerId)) { - funDef = agent?.SecondaryFunctions?.FirstOrDefault(x => x.Name == functionName); - if (funDef != null) - { - if (!string.IsNullOrWhiteSpace(funDef?.Output)) - { - return new DummyFunctionExecutor(funDef, serviceProvider); - } - else - { - var mcpServerId = agent?.McpTools?.Where(x => x.Functions.Any(y => y.Name == funDef.Name)) - .FirstOrDefault().ServerId; - return new MCPToolExecutor(mcpServerId, functionName, serviceProvider); - } - } + return new McpToolExecutor(services, mcpServerId, functionName); } + return null; } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs index f7625b48..c452e806 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/Executor/MCPToolExecutor.cs @@ -1,21 +1,20 @@ +using BotSharp.Abstraction.Routing.Executor; using BotSharp.Core.MCP.Managers; using ModelContextProtocol.Client; namespace BotSharp.Core.Routing.Executor; -public class MCPToolExecutor: IFunctionExecutor +public class McpToolExecutor: IFunctionExecutor { - private readonly McpClientManager _clientManager; - private string mcpServer; - private string funcName; private readonly IServiceProvider _services; + private readonly string _mcpServerId; + private readonly string _functionName; - public MCPToolExecutor(string mcpserver, string functionName, IServiceProvider services) + public McpToolExecutor(IServiceProvider services, string mcpServerId, string functionName) { _services = services; - this.mcpServer = mcpserver; - this.funcName = functionName; - _clientManager = services.GetRequiredService(); + _mcpServerId = mcpServerId; + _functionName = functionName; } public async Task ExecuteAsync(RoleDialogModel message) @@ -23,12 +22,13 @@ public class MCPToolExecutor: IFunctionExecutor try { // Convert arguments to dictionary format expected by mcpdotnet - Dictionary argDict = JsonToDictionary(message.FunctionArgs); + Dictionary argDict = JsonToDictionary(message.FunctionArgs); - var client = await _clientManager.GetMcpClientAsync(mcpServer); + var clientManager = _services.GetRequiredService(); + var client = await clientManager.GetMcpClientAsync(_mcpServerId); // Call the tool through mcpdotnet - var result = await client.CallToolAsync(funcName, !argDict.IsNullOrEmpty() ? argDict : []); + var result = await client.CallToolAsync(_functionName, !argDict.IsNullOrEmpty() ? argDict : []); // Extract the text content from the result var json = string.Join("\n", result.Content.Where(c => c.Type == "text").Select(c => c.Text)); @@ -39,7 +39,7 @@ public class MCPToolExecutor: IFunctionExecutor } catch (Exception ex) { - message.Content = $"Error when calling tool {funcName} of MCP server {mcpServer}. {ex.Message}"; + message.Content = $"Error when calling tool {_functionName} of MCP server {_mcpServerId}. {ex.Message}"; return false; } } diff --git a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs index 126453e8..63bd3b20 100644 --- a/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs +++ b/src/Infrastructure/BotSharp.Core/Routing/RoutingService.InvokeFunction.cs @@ -1,5 +1,3 @@ -using BotSharp.Abstraction.Functions; -using BotSharp.Abstraction.Templating; using BotSharp.Core.Routing.Executor; namespace BotSharp.Core.Routing; @@ -8,14 +6,11 @@ public partial class RoutingService { public async Task InvokeFunction(string name, RoleDialogModel message) { - var function = _services.GetServices().FirstOrDefault(x => x.Name == name); - var currentAgentId = message.CurrentAgentId; var agentService = _services.GetRequiredService(); var agent = await agentService.GetAgent(currentAgentId); - IFunctionExecutor funcExecutor = FunctionExecutorFactory.Create(name, agent, function, _services); - + var funcExecutor = FunctionExecutorFactory.Create(_services, name, agent); if (funcExecutor == null) { message.StopCompletion = true; @@ -24,17 +19,14 @@ public partial class RoutingService return false; } - // Clone message var clonedMessage = RoleDialogModel.From(message); clonedMessage.FunctionName = name; - var hooks = _services - .GetRequiredService() - .HooksOrderByPriority; + var hooks = _services.GetRequiredService() + .HooksOrderByPriority; var progressService = _services.GetService(); - clonedMessage.Indication = await funcExecutor.GetIndicatorAsync(message); if (progressService?.OnFunctionExecuting != null) diff --git a/src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs b/src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs index 7b74a37e..6519d010 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/Controllers/McpController.cs @@ -13,9 +13,9 @@ public class McpController : ControllerBase } [HttpGet("/mcp/server-configs")] - public IEnumerable GetMcpServerConfigs() + public async Task> GetMcpServerConfigs() { var mcp = _services.GetRequiredService(); - return mcp.GetServerConfigs(); + return await mcp.GetServerConfigsAsync(); } } diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index b48d2963..a1d4ed53 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -268,13 +268,13 @@ } }, "McpServerConfigs": [ - { - "Id": "PizzaServer", - "Name": "PizzaServer", - "SseConfig": { - "Endpoint": "http://localhost:58905/sse" - } - } + //{ + // "Id": "PizzaServer", + // "Name": "PizzaServer", + // "SseConfig": { + // "Endpoint": "http://localhost:58905/sse" + // } + //} ] }, @@ -502,7 +502,6 @@ "BotSharp.Core.SideCar", "BotSharp.Core.Crontab", "BotSharp.Core.Realtime", - "BotSharp.Core.MCP", "BotSharp.Logger", "BotSharp.Plugin.MongoStorage", "BotSharp.Plugin.Dashboard",