diff --git a/BotSharp.sln b/BotSharp.sln
index d4890be1..d8796f8a 100644
--- a/BotSharp.sln
+++ b/BotSharp.sln
@@ -127,6 +127,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Rules", "src\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.DeepSeekAI", "src\Plugins\BotSharp.Plugin.DeepSeekAI\BotSharp.Plugin.DeepSeekAI.csproj", "{AF329442-B48E-4B48-A18A-1C869D1BA6F5}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MCP", "MCP", "{B38A04AB-F4BD-4F10-9662-EC110881533B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.Mcp", "src\Plugins\BotSharp.Plugin.Mcp\BotSharp.Plugin.Mcp.csproj", "{9475E249-6964-457C-96C2-3F917111123F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -519,6 +523,14 @@ Global
{AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|Any CPU.Build.0 = Release|Any CPU
{AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x64.ActiveCfg = Release|Any CPU
{AF329442-B48E-4B48-A18A-1C869D1BA6F5}.Release|x64.Build.0 = Release|Any CPU
+ {9475E249-6964-457C-96C2-3F917111123F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9475E249-6964-457C-96C2-3F917111123F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9475E249-6964-457C-96C2-3F917111123F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9475E249-6964-457C-96C2-3F917111123F}.Debug|x64.Build.0 = Debug|Any CPU
+ {9475E249-6964-457C-96C2-3F917111123F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9475E249-6964-457C-96C2-3F917111123F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9475E249-6964-457C-96C2-3F917111123F}.Release|x64.ActiveCfg = Release|Any CPU
+ {9475E249-6964-457C-96C2-3F917111123F}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -580,6 +592,8 @@ Global
{F812BAAE-5A7D-4DF7-8E71-70696B51C61F} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
{AFD64412-4D6A-452E-82A2-79E5D8842E29} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
{AF329442-B48E-4B48-A18A-1C869D1BA6F5} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
+ {B38A04AB-F4BD-4F10-9662-EC110881533B} = {2635EC9B-2E5F-4313-AC21-0B847F31F36C}
+ {9475E249-6964-457C-96C2-3F917111123F} = {B38A04AB-F4BD-4F10-9662-EC110881533B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}
diff --git a/src/Plugins/BotSharp.Plugin.Mcp/BotSharp.Plugin.Mcp.csproj b/src/Plugins/BotSharp.Plugin.Mcp/BotSharp.Plugin.Mcp.csproj
new file mode 100644
index 00000000..13417a59
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Mcp/BotSharp.Plugin.Mcp.csproj
@@ -0,0 +1,22 @@
+
+
+
+ $(TargetFramework)
+ enable
+ $(LangVersion)
+ $(BotSharpVersion)
+ $(GeneratePackageOnBuild)
+ $(GenerateDocumentationFile)
+ $(SolutionDir)packages
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Plugins/BotSharp.Plugin.Mcp/Functions/McpAIFunction.cs b/src/Plugins/BotSharp.Plugin.Mcp/Functions/McpAIFunction.cs
new file mode 100644
index 00000000..d19ca1e9
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Mcp/Functions/McpAIFunction.cs
@@ -0,0 +1,96 @@
+using BotSharp.Abstraction.Conversations.Models;
+using BotSharp.Abstraction.Functions;
+using McpDotNet.Client;
+using McpDotNet.Protocol.Types;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace BotSharp.Plugin.Mcp.Functions;
+
+public class McpAIFunction : IFunctionCallback
+{
+ private readonly Tool _tool;
+ private readonly IMcpClient _client;
+
+ public McpAIFunction(Tool tool, IMcpClient client)
+ {
+ _tool = tool ?? throw new ArgumentNullException(nameof(tool));
+ _client = client ?? throw new ArgumentNullException(nameof(client));
+ }
+
+ public string Name => _tool.Name;
+
+ public async Task Execute(RoleDialogModel message)
+ {
+ // Convert arguments to dictionary format expected by mcpdotnet
+ Dictionary argDict = JsonToDictionary(message.FunctionArgs);
+
+ // Call the tool through mcpdotnet
+ var result = await _client.CallToolAsync(
+ _tool.Name,
+ argDict.Count == 0 ? new() : argDict
+ );
+
+ // Extract the text content from the result
+ // For simplicity in this sample, we'll just concatenate all text content
+ message.Content = string.Join("\n", result.Content
+ .Where(c => c.Type == "text")
+ .Select(c => c.Text));
+ return true;
+ }
+
+ private static Dictionary JsonToDictionary(string? json)
+ {
+ if (string.IsNullOrEmpty(json))
+ return [];
+
+ // 使用JsonDocument解析JSON字符串
+ using JsonDocument doc = JsonDocument.Parse(json);
+ JsonElement root = doc.RootElement;
+ return JsonElementToDictionary(root);
+ }
+
+ private static Dictionary JsonElementToDictionary(JsonElement element)
+ {
+ Dictionary dictionary = [];
+
+ if (element.ValueKind == JsonValueKind.Object)
+ {
+ foreach (JsonProperty property in element.EnumerateObject())
+ {
+ dictionary[property.Name] = JsonElementToValue(property.Value);
+ }
+ }
+
+ return dictionary;
+ }
+
+ private static object? JsonElementToValue(JsonElement element) => element.ValueKind switch
+ {
+ JsonValueKind.Object => JsonElementToDictionary(element),
+ JsonValueKind.Array => element.EnumerateArray().Select(JsonElementToValue).ToList(),
+ JsonValueKind.String => element.GetString(),
+ JsonValueKind.Number when element.TryGetInt32(out int intValue) => intValue,
+ JsonValueKind.Number when element.TryGetInt64(out long longValue) => longValue,
+ JsonValueKind.Number when element.TryGetDouble(out double doubleValue) => doubleValue,
+ JsonValueKind.Number when element.TryGetDecimal(out decimal decimalValue) => decimalValue,
+ JsonValueKind.Number when element.TryGetByte(out byte byteValue) => byteValue,
+ JsonValueKind.Number when element.TryGetSByte(out sbyte sbyteValue) => sbyteValue,
+ JsonValueKind.Number when element.TryGetUInt16(out ushort uint16Value) => uint16Value,
+ JsonValueKind.Number when element.TryGetUInt32(out uint uint32Value) => uint32Value,
+ JsonValueKind.Number when element.TryGetUInt64(out ulong uint64Value) => uint64Value,
+ JsonValueKind.Number when element.TryGetDateTime(out DateTime dateTimeValue) => dateTimeValue,
+ JsonValueKind.Number when element.TryGetDateTimeOffset(out DateTimeOffset dateTimeOffsetValue) => dateTimeOffsetValue,
+ JsonValueKind.Number when element.TryGetGuid(out Guid guidValue) => guidValue,
+ JsonValueKind.Number => element.GetRawText(),
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ JsonValueKind.Null => null,
+ JsonValueKind.Undefined => string.Empty, // JsonElement is undefined (there is no value).
+ _ => throw new ArgumentOutOfRangeException(nameof(element.ValueKind), element.ValueKind, "Unexpected JsonValueKind encountered.")
+ };
+
+}
diff --git a/src/Plugins/BotSharp.Plugin.Mcp/MCPClientManager.cs b/src/Plugins/BotSharp.Plugin.Mcp/MCPClientManager.cs
new file mode 100644
index 00000000..e1f01f14
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Mcp/MCPClientManager.cs
@@ -0,0 +1,33 @@
+using BotSharp.Plugin.Mcp.Settings;
+using McpDotNet.Client;
+using Microsoft.Extensions.Logging;
+using System;
+
+namespace BotSharp.Plugin.Mcp;
+
+public class MCPClientManager : IDisposable
+{
+ public ILoggerFactory LoggerFactory { get; }
+ public McpClientFactory Factory { get; }
+
+
+ private readonly MCPSettings mcpSettings;
+
+ public MCPClientManager(MCPSettings settings, ILoggerFactory loggerFactory)
+ {
+ mcpSettings = settings;
+ LoggerFactory = loggerFactory;
+
+ // Inject the mock transport into the factory
+ Factory = new McpClientFactory(
+ settings.McpServerConfigs,
+ settings.McpClientOptions,
+ LoggerFactory
+ );
+ }
+
+ public void Dispose()
+ {
+ LoggerFactory?.Dispose();
+ }
+}
diff --git a/src/Plugins/BotSharp.Plugin.Mcp/McpPlugin.cs b/src/Plugins/BotSharp.Plugin.Mcp/McpPlugin.cs
new file mode 100644
index 00000000..2bfda270
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Mcp/McpPlugin.cs
@@ -0,0 +1,45 @@
+using BotSharp.Abstraction.Functions;
+using BotSharp.Abstraction.Plugins;
+using BotSharp.Plugin.Mcp.Functions;
+using BotSharp.Plugin.Mcp.Settings;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace BotSharp.Plugin.Mcp;
+
+public class McpPlugin : IBotSharpPlugin
+{
+ public string Id => "5d779611-0012-46cb-a754-4ca4770e88ac";
+ public string Name => "MCP Plugin";
+ public string Description => "Integrated MCP tools";
+
+ private MCPClientManager clientManager;
+
+ public void RegisterDI(IServiceCollection services, IConfiguration config)
+ {
+ var settings = config.GetSection("MCPSettings").Get();
+ services.AddScoped(provider => { return settings; });
+ services.AddSingleton(provider =>
+ {
+ var loggerFactory = provider.GetService() ?? NullLoggerFactory.Instance;
+ return new MCPClientManager(settings, loggerFactory);
+ });
+
+ foreach (var server in settings.McpServerConfigs)
+ {
+ var client = clientManager.Factory.GetClientAsync(server.Id).Result;
+ var tools = client.ListToolsAsync().Result;
+
+ foreach (var tool in tools.Tools)
+ {
+ services.AddScoped(provider =>
+ {
+ var func = new McpAIFunction(tool, client);
+ return func;
+ });
+ }
+ }
+ }
+ }
diff --git a/src/Plugins/BotSharp.Plugin.Mcp/Settings/MCPSettings.cs b/src/Plugins/BotSharp.Plugin.Mcp/Settings/MCPSettings.cs
new file mode 100644
index 00000000..a57050d5
--- /dev/null
+++ b/src/Plugins/BotSharp.Plugin.Mcp/Settings/MCPSettings.cs
@@ -0,0 +1,13 @@
+using McpDotNet.Client;
+using McpDotNet.Configuration;
+using System.Collections.Generic;
+
+namespace BotSharp.Plugin.Mcp.Settings;
+
+public class MCPSettings
+{
+ public McpClientOptions McpClientOptions { get; set; }
+
+ public List McpServerConfigs { get; set; }
+
+}
diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json
index 655fa954..7b19e043 100644
--- a/src/WebStarter/appsettings.json
+++ b/src/WebStarter/appsettings.json
@@ -175,6 +175,22 @@
}
},
+ "McpClientOptions": {
+ "ClientInfo": {
+ "Name": "SimpleToolsBotsharp",
+ "Version": "1.0.0"
+ }
+ },
+ "McpServerOptions": [
+ {
+ "Id": "everything",
+ "Name": "Everything",
+ "TransportType": "sse",
+ "TransportOptions": {},
+ "Location": "http://localhost:5000/sse"
+ }
+ ]
+ },
"Conversation": {
"DataDir": "conversations",
"ShowVerboseLog": false,