sync and refine

This commit is contained in:
Jicheng Lu 2025-05-15 17:46:36 -05:00
parent 37bef7b8c6
commit 350c479486
15 changed files with 138 additions and 131 deletions

View file

@ -2,5 +2,5 @@ namespace BotSharp.Abstraction.MCP.Services;
public interface IMcpService
{
IEnumerable<McpServerOptionModel> GetServerConfigs() => [];
Task<IEnumerable<McpServerOptionModel>> GetServerConfigsAsync() => Task.FromResult<IEnumerable<McpServerOptionModel>>([]);
}

View file

@ -1,8 +1,7 @@
namespace BotSharp.Core.Routing.Executor;
namespace BotSharp.Abstraction.Routing.Executor;
public interface IFunctionExecutor
{
public Task<bool> ExecuteAsync(RoleDialogModel message);
public Task<string> GetIndicatorAsync(RoleDialogModel message);
}

View file

@ -18,15 +18,11 @@ public static class BotSharpMcpExtensions
{
var settings = config.GetSection("MCP").Get<McpSettings>();
services.AddScoped(provider => settings);
services.AddScoped<IMcpService, McpService>();
if (settings != null && settings.Enabled && !settings.McpServerConfigs.IsNullOrEmpty())
{
services.AddScoped<IMcpService, McpService>();
var clientManager = new McpClientManager(settings);
services.AddScoped(provider => clientManager);
// Register hooks
services.AddScoped<McpClientManager>();
services.AddScoped<IAgentHook, McpToolAgentHook>();
}
return services;

View file

@ -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<List<string>>(required.GetRawText())
Properties = JsonDocument.Parse(properties.GetRawText() ?? "{}"),
Required = JsonSerializer.Deserialize<List<string>>(required.GetRawText() ?? "[]") ?? []
}
};

View file

@ -41,18 +41,26 @@ public class McpToolAgentHook : AgentHookBase
return functionDefs;
}
var mcpClientManager = _services.GetRequiredService<McpClientManager>();
var mcps = agent.McpTools.Where(x => !x.Disabled);
var mcpClientManager = _services.GetService<McpClientManager>();
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);
}
}

View file

@ -6,45 +6,63 @@ namespace BotSharp.Core.MCP.Managers;
public class McpClientManager : IDisposable
{
private readonly McpSettings _mcpSettings;
private readonly IServiceProvider _services;
private readonly ILogger<McpClientManager> _logger;
public McpClientManager(McpSettings mcpSettings)
public McpClientManager(
IServiceProvider services,
ILogger<McpClientManager> logger)
{
_mcpSettings = mcpSettings;
_services = services;
_logger = logger;
}
public async Task<IMcpClient> GetMcpClientAsync(string serverId)
public async Task<IMcpClient?> 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<McpSettings>();
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()

View file

@ -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<McpService> _logger;
private readonly McpClientManager _mcpClientManager;
public McpService(
IServiceProvider services,
ILogger<McpService> logger,
McpClientManager mcpClient)
ILogger<McpService> logger)
{
_services = services;
_logger = logger;
_mcpClientManager = mcpClient;
}
public IEnumerable<McpServerOptionModel> GetServerConfigs()
public async Task<IEnumerable<McpServerOptionModel>> GetServerConfigsAsync()
{
var clientManager = _services.GetService<McpClientManager>();
if (clientManager == null) return [];
var options = new List<McpServerOptionModel>();
var settings = _services.GetRequiredService<McpSettings>();
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)
});
}

View file

@ -7,5 +7,4 @@ public class McpSettings
public bool Enabled { get; set; } = true;
public McpClientOptions McpClientOptions { get; set; }
public List<McpServerConfigModel> McpServerConfigs { get; set; } = [];
}

View file

@ -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<bool> ExecuteAsync(RoleDialogModel message)
{
var render = _services.GetRequiredService<ITemplateRender>();
@ -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;
}

View file

@ -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<bool> ExecuteAsync(RoleDialogModel message)
{
return await functionCallback.Execute(message);
return await _functionCallback.Execute(message);
}
public async Task<string> GetIndicatorAsync(RoleDialogModel message)
{
return await functionCallback.GetIndication(message);
return await _functionCallback.GetIndication(message);
}
}

View file

@ -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<IFunctionCallback>().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;
}
}

View file

@ -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<McpClientManager>();
_mcpServerId = mcpServerId;
_functionName = functionName;
}
public async Task<bool> ExecuteAsync(RoleDialogModel message)
@ -23,12 +22,13 @@ public class MCPToolExecutor: IFunctionExecutor
try
{
// Convert arguments to dictionary format expected by mcpdotnet
Dictionary<string, object> argDict = JsonToDictionary(message.FunctionArgs);
Dictionary<string, object> argDict = JsonToDictionary(message.FunctionArgs);
var client = await _clientManager.GetMcpClientAsync(mcpServer);
var clientManager = _services.GetRequiredService<McpClientManager>();
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;
}
}

View file

@ -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<bool> InvokeFunction(string name, RoleDialogModel message)
{
var function = _services.GetServices<IFunctionCallback>().FirstOrDefault(x => x.Name == name);
var currentAgentId = message.CurrentAgentId;
var agentService = _services.GetRequiredService<IAgentService>();
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<ConversationHookProvider>()
.HooksOrderByPriority;
var hooks = _services.GetRequiredService<ConversationHookProvider>()
.HooksOrderByPriority;
var progressService = _services.GetService<IConversationProgressService>();
clonedMessage.Indication = await funcExecutor.GetIndicatorAsync(message);
if (progressService?.OnFunctionExecuting != null)

View file

@ -13,9 +13,9 @@ public class McpController : ControllerBase
}
[HttpGet("/mcp/server-configs")]
public IEnumerable<McpServerOptionModel> GetMcpServerConfigs()
public async Task<IEnumerable<McpServerOptionModel>> GetMcpServerConfigs()
{
var mcp = _services.GetRequiredService<IMcpService>();
return mcp.GetServerConfigs();
return await mcp.GetServerConfigsAsync();
}
}

View file

@ -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",