diff --git a/BotSharp.sln b/BotSharp.sln index b176747f8..53eb3da99 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -127,7 +127,11 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Realtime", "src\Infrastructure\BotSharp.Core.Realtime\BotSharp.Core.Realtime.csproj", "{7ACD8C95-C66B-436A-80E7-541A57D8C3F4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.MCP", "src\Infrastructure\BotSharp.MCP\BotSharp.MCP.csproj", "{684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Realtime", "src\Infrastructure\BotSharp.Core.Realtime\BotSharp.Core.Realtime.csproj", "{781F1465-365C-0F22-1775-25025DAFA4C7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.PizzaBot.MCPServer", "tests\BotSharp.PizzaBot.MCPServer\BotSharp.PizzaBot.MCPServer.csproj", "{8D2AD45F-836A-516F-DE6A-71443CEBB18A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -521,14 +525,30 @@ 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 - {7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Debug|x64.ActiveCfg = Debug|Any CPU - {7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Debug|x64.Build.0 = Debug|Any CPU - {7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Release|Any CPU.Build.0 = Release|Any CPU - {7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Release|x64.ActiveCfg = Release|Any CPU - {7ACD8C95-C66B-436A-80E7-541A57D8C3F4}.Release|x64.Build.0 = Release|Any CPU + {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}.Debug|x64.ActiveCfg = Debug|Any CPU + {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}.Debug|x64.Build.0 = Debug|Any CPU + {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}.Release|Any CPU.Build.0 = Release|Any CPU + {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}.Release|x64.ActiveCfg = Release|Any CPU + {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}.Release|x64.Build.0 = Release|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x64.ActiveCfg = Debug|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|x64.Build.0 = Debug|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|Any CPU.Build.0 = Release|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x64.ActiveCfg = Release|Any CPU + {781F1465-365C-0F22-1775-25025DAFA4C7}.Release|x64.Build.0 = Release|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x64.ActiveCfg = Debug|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Debug|x64.Build.0 = Debug|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|Any CPU.Build.0 = Release|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x64.ActiveCfg = Release|Any CPU + {8D2AD45F-836A-516F-DE6A-71443CEBB18A}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -590,7 +610,9 @@ 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} - {7ACD8C95-C66B-436A-80E7-541A57D8C3F4} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} + {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} + {781F1465-365C-0F22-1775-25025DAFA4C7} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} + {8D2AD45F-836A-516F-DE6A-71443CEBB18A} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} diff --git a/Directory.Packages.props b/Directory.Packages.props index 5cd53104c..3354b9927 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,13 +15,16 @@ + + + @@ -106,6 +109,8 @@ + + diff --git a/src/BotSharp.AppHost/BotSharp.AppHost.csproj b/src/BotSharp.AppHost/BotSharp.AppHost.csproj index 68673f7d2..26ff84a82 100644 --- a/src/BotSharp.AppHost/BotSharp.AppHost.csproj +++ b/src/BotSharp.AppHost/BotSharp.AppHost.csproj @@ -15,6 +15,7 @@ + diff --git a/src/BotSharp.AppHost/Program.cs b/src/BotSharp.AppHost/Program.cs index 9c371bf4b..4c54ed11b 100644 --- a/src/BotSharp.AppHost/Program.cs +++ b/src/BotSharp.AppHost/Program.cs @@ -2,6 +2,8 @@ var apiService = builder.AddProject("apiservice") .WithExternalHttpEndpoints(); +var mcpService = builder.AddProject("mcpservice") + .WithExternalHttpEndpoints(); builder.AddNpmApp("BotSharpUI", "../../../BotSharp-UI") .WithReference(apiService) diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs index bec14b853..1103f2f53 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/AgentHookBase.cs @@ -57,5 +57,11 @@ public virtual void OnAgentLoaded(Agent agent) public virtual void OnAgentUtilityLoaded(Agent agent) { + + } + + public virtual void OnAgentMCPToolLoaded(Agent agent) + { + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs index 0c39a960c..2ecddf0cf 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs @@ -19,6 +19,7 @@ public enum AgentField Sample, LlmConfig, Utility, + McpTool, KnowledgeBase, Rule, MaxMessageCount diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs index a3f53fb67..624616d79 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/IAgentHook.cs @@ -27,6 +27,8 @@ public interface IAgentHook void OnAgentUtilityLoaded(Agent agent); + void OnAgentMCPToolLoaded(Agent agent); + /// /// Triggered when agent is loaded completely. /// diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs index 085b0f9df..fc28e5714 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/Agent.cs @@ -95,15 +95,20 @@ public class Agent public List Labels { get; set; } = new(); /// - /// Merge utilities from entry agent + /// Merge Utility from entry agent /// public bool MergeUtility { get; set; } /// - /// Agent utilities + /// Agent Utility /// public List Utilities { get; set; } = new(); + /// + /// Agent MCP Tools + /// + public List McpTools { get; set; } = new(); + /// /// Agent rules /// @@ -157,6 +162,7 @@ public static Agent Clone(Agent agent) Responses = agent.Responses, Samples = agent.Samples, Utilities = agent.Utilities, + McpTools = agent.McpTools, Knowledges = agent.Knowledges, IsPublic = agent.IsPublic, Disabled = agent.Disabled, @@ -298,4 +304,10 @@ public Agent SetLlmConfig(AgentLlmConfig? llmConfig) LlmConfig = llmConfig; return this; } + + public Agent SetMcps(List mcps) + { + McpTools = mcps ?? []; + return this; + } } diff --git a/src/Infrastructure/BotSharp.Abstraction/Agents/Models/MCPTool.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/MCPTool.cs new file mode 100644 index 000000000..a555a7e5d --- /dev/null +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Models/MCPTool.cs @@ -0,0 +1,37 @@ +namespace BotSharp.Abstraction.Agents.Models; + +public class MCPTool +{ + public string ServerId { get; set; } + + public bool Disabled { get; set; } + + public IEnumerable Functions { get; set; } = []; + + public MCPTool() + { + + } + + public MCPTool( + IEnumerable? functions = null) + { + Functions = functions ?? []; + } + + public override string ToString() + { + return ServerId; + } +} + + +public class MCPFunction +{ + public string Name { get; set; } + + public MCPFunction(string name) + { + this.Name = name; + } +} diff --git a/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs b/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs index 1101f1600..5ab0ea034 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Hooks/BasicAgentHook.cs @@ -38,6 +38,7 @@ public override void OnAgentUtilityLoaded(Agent agent) agent.SecondaryInstructions.Add(prompt); } } + private (IEnumerable, IEnumerable) GetUtilityContent(Agent agent) { @@ -81,5 +82,5 @@ public override void OnAgentUtilityLoaded(Agent agent) .Distinct().ToList(); return (functionNames, templateNames); - } + } } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs index 15ba26a11..c8f972061 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.LoadAgent.cs @@ -71,6 +71,11 @@ public async Task LoadAgent(string id, bool loadUtility = true) { hook.OnAgentUtilityLoaded(agent); } + + if(agent.McpTools != null && agent.McpTools.Count >0) + { + hook.OnAgentMCPToolLoaded(agent); + } hook.OnAgentLoaded(agent); } diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index a9cc58d95..dd8743d45 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -40,6 +40,7 @@ public async Task UpdateAgent(Agent agent, AgentField updateField) record.Responses = agent.Responses ?? []; record.Samples = agent.Samples ?? []; record.Utilities = agent.Utilities ?? []; + record.McpTools = agent.McpTools ?? []; record.KnowledgeBases = agent.KnowledgeBases ?? []; record.Rules = agent.Rules ?? []; if (agent.LlmConfig != null && !agent.LlmConfig.IsInherit) @@ -106,6 +107,7 @@ public async Task UpdateAgentFromFile(string id) .SetResponses(foundAgent.Responses) .SetSamples(foundAgent.Samples) .SetUtilities(foundAgent.Utilities) + .SetMcps(foundAgent.McpTools) .SetKnowledgeBases(foundAgent.KnowledgeBases) .SetRules(foundAgent.Rules) .SetLlmConfig(foundAgent.LlmConfig); @@ -195,7 +197,7 @@ public async Task PatchAgentTemplate(Agent agent) var samples = GetSamplesFromFile(dir); return agent.SetInstruction(defaultInstruction) .SetChannelInstructions(channelInstructions) - .SetTemplates(templates) + .SetTemplates(templates) .SetFunctions(functions) .SetResponses(responses) .SetSamples(samples); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index d7a6d1218..4da618285 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -60,6 +60,9 @@ public void UpdateAgent(Agent agent, AgentField field) case AgentField.Utility: UpdateAgentUtilities(agent.Id, agent.MergeUtility, agent.Utilities); break; + case AgentField.McpTool: + UpdateAgentMcpTools(agent.Id, agent.McpTools); + break; case AgentField.KnowledgeBase: UpdateAgentKnowledgeBases(agent.Id, agent.KnowledgeBases); break; @@ -191,6 +194,20 @@ private void UpdateAgentUtilities(string agentId, bool mergeUtility, List mcptools) + { + if (mcptools == null) return; + + var (agent, agentFile) = GetAgentFromFile(agentId); + if (agent == null) return; + + + agent.McpTools = mcptools; + agent.UpdatedDateTime = DateTime.UtcNow; + var json = JsonSerializer.Serialize(agent, _options); + File.WriteAllText(agentFile, json); + } + private void UpdateAgentKnowledgeBases(string agentId, List knowledgeBases) { if (knowledgeBases == null) return; @@ -360,6 +377,7 @@ private void UpdateAgentAllFields(Agent inputAgent) agent.Profiles = inputAgent.Profiles; agent.Labels = inputAgent.Labels; agent.Utilities = inputAgent.Utilities; + agent.McpTools = inputAgent.McpTools; agent.KnowledgeBases = inputAgent.KnowledgeBases; agent.RoutingRules = inputAgent.RoutingRules; agent.Rules = inputAgent.Rules; diff --git a/src/Infrastructure/BotSharp.MCP/AIFunctionUtilities.cs b/src/Infrastructure/BotSharp.MCP/AIFunctionUtilities.cs new file mode 100644 index 000000000..8a409f672 --- /dev/null +++ b/src/Infrastructure/BotSharp.MCP/AIFunctionUtilities.cs @@ -0,0 +1,36 @@ +using BotSharp.Abstraction.Functions.Models; +using ModelContextProtocol.Protocol.Types; +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace BotSharp.Core.MCP; + +internal static class AIFunctionUtilities +{ + public static FunctionDef MapToFunctionDef(Tool tool) + { + if (tool == null) + { + throw new ArgumentNullException(nameof(tool)); + } + + var properties = tool.InputSchema.GetProperty("properties"); + var required = tool.InputSchema.GetProperty("required"); + + FunctionDef funDef = new FunctionDef + { + Name = tool.Name, + Description = tool.Description ?? string.Empty, + Type = "function", + Parameters = new FunctionParametersDef + { + Type = "object", + Properties = JsonDocument.Parse(properties.GetRawText()), + Required = JsonSerializer.Deserialize>(required.GetRawText()) + } + }; + + return funDef; + } +} diff --git a/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj b/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj new file mode 100644 index 000000000..94aba37ce --- /dev/null +++ b/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj @@ -0,0 +1,19 @@ + + + + $(TargetFramework) + $(LangVersion) + $(BotSharpVersion) + $(GeneratePackageOnBuild) + $(SolutionDir)packages + + + + + + + + + + + diff --git a/src/Infrastructure/BotSharp.MCP/Functions/McpToolAdapter.cs b/src/Infrastructure/BotSharp.MCP/Functions/McpToolAdapter.cs new file mode 100644 index 000000000..60cfb5318 --- /dev/null +++ b/src/Infrastructure/BotSharp.MCP/Functions/McpToolAdapter.cs @@ -0,0 +1,106 @@ +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Conversations.Models; +using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Utilities; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace BotSharp.Core.Mcp.Functions; + +public class McpToolAdapter : IFunctionCallback +{ + private readonly Tool _tool; + private readonly MCPClientManager _clientManager; + private readonly IServiceProvider _serviceProvider; + + public McpToolAdapter(IServiceProvider provider, Tool tool, MCPClientManager client) + { + _serviceProvider = provider ?? throw new ArgumentNullException(nameof(provider)); + _tool = tool ?? throw new ArgumentNullException(nameof(tool)); + _clientManager = 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); + var currentAgentId = message.CurrentAgentId; + var agentService = _serviceProvider.GetRequiredService(); + var agent = await agentService.LoadAgent(currentAgentId); + var serverId = agent.McpTools.Where(t => t.Functions.Any(f => f.Name == Name)).FirstOrDefault().ServerId; + + var client = await _clientManager.GetMcpClientAsync(serverId); + // Call the tool through mcpdotnet + var result = await client.CallToolAsync( + _tool.Name, + argDict.Count == 0 ? new() : argDict + ); + + // Extract the text content from the result + var json = string.Join("\n", result.Content + .Where(c => c.Type == "text") + .Select(c => c.Text)); + message.Content = json; + message.Data = json.JsonContent(); + return true; + } + + private static Dictionary JsonToDictionary(string? json) + { + if (string.IsNullOrEmpty(json)) + return []; + + 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/Infrastructure/BotSharp.MCP/Hooks/MCPToolAgentHook.cs b/src/Infrastructure/BotSharp.MCP/Hooks/MCPToolAgentHook.cs new file mode 100644 index 000000000..84039cfbb --- /dev/null +++ b/src/Infrastructure/BotSharp.MCP/Hooks/MCPToolAgentHook.cs @@ -0,0 +1,70 @@ +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Agents.Enums; +using BotSharp.Abstraction.Agents.Models; +using BotSharp.Abstraction.Agents.Settings; +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Functions.Models; +using BotSharp.Core.Mcp; +using BotSharp.Core.MCP; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Client; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BotSharp.MCP.Hooks; + +public class MCPToolAgentHook : AgentHookBase +{ + public override string SelfId => string.Empty; + + public MCPToolAgentHook(IServiceProvider services, AgentSettings settings) + : base(services, settings) + { + } + + public override void OnAgentMCPToolLoaded(Agent agent) + { + if (agent.Type == AgentType.Routing) + return; + var conv = _services.GetRequiredService(); + var isConvMode = conv.IsConversationMode(); + if (!isConvMode) return; + + agent.SecondaryFunctions ??= []; + + var functions = GetMCPContent(agent).Result; + + foreach (var fn in functions) + { + if (!agent.SecondaryFunctions.Any(x => x.Name.Equals(fn.Name, StringComparison.OrdinalIgnoreCase))) + { + agent.SecondaryFunctions.Add(fn); + } + } + } + + private async Task> GetMCPContent(Agent agent) + { + List functionDefs = new List(); + var mcpClientManager = _services.GetRequiredService(); + var mcps = agent.McpTools; + foreach (var item in mcps) + { + var mcpClient = await mcpClientManager.GetMcpClientAsync(item.ServerId); + if (mcpClient != null) + { + var tools = await mcpClient.ListToolsAsync().ToListAsync(); + var toolnames = item.Functions.Select(x => x.Name).ToList(); + foreach (var tool in tools.Where(x => toolnames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) + { + var funDef = AIFunctionUtilities.MapToFunctionDef(tool); + functionDefs.Add(funDef); + } + } + } + + return functionDefs; + } +} diff --git a/src/Infrastructure/BotSharp.MCP/MCPClientManager.cs b/src/Infrastructure/BotSharp.MCP/MCPClientManager.cs new file mode 100644 index 000000000..90664c7b0 --- /dev/null +++ b/src/Infrastructure/BotSharp.MCP/MCPClientManager.cs @@ -0,0 +1,30 @@ +using BotSharp.Core.Mcp.Settings; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Client; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace BotSharp.Core.Mcp; + +public class MCPClientManager : IDisposable +{ + + private readonly MCPSettings mcpSettings; + + public MCPClientManager(MCPSettings settings) + { + mcpSettings = settings; + } + + public async Task GetMcpClientAsync(string serverId) + { + return await McpClientFactory.CreateAsync(mcpSettings.McpServerConfigs + .Where(x=> x.Name == serverId).First(), mcpSettings.McpClientOptions); + } + + public void Dispose() + { + + } +} diff --git a/src/Infrastructure/BotSharp.MCP/McpPlugin.cs b/src/Infrastructure/BotSharp.MCP/McpPlugin.cs new file mode 100644 index 000000000..f95965ac3 --- /dev/null +++ b/src/Infrastructure/BotSharp.MCP/McpPlugin.cs @@ -0,0 +1,61 @@ +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Functions; +using BotSharp.Abstraction.Plugins; +using BotSharp.Core.Mcp.Functions; +using BotSharp.Core.Mcp.Settings; +using BotSharp.MCP.Hooks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using ModelContextProtocol.Client; +using ModelContextProtocol.Configuration; +using System.Linq; +using System.Threading.Tasks; + +namespace BotSharp.Core.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; }); + + clientManager = new MCPClientManager(settings); + services.AddSingleton(clientManager); + + foreach (var server in settings.McpServerConfigs) + { + RegisterFunctionCall(services, server) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + } + // Register hooks + services.AddScoped(); + } + + private async Task RegisterFunctionCall(IServiceCollection services, McpServerConfig server) + { + var client = await clientManager.GetMcpClientAsync(server.Id); + var tools = await client.ListToolsAsync().ToListAsync(); + + foreach (var tool in tools) + { + services.AddScoped(provider => { return tool; }); + + services.AddScoped(provider => + { + var funcTool = new McpToolAdapter(provider, tool, clientManager); + return funcTool; + }); + } + } +} diff --git a/src/Infrastructure/BotSharp.MCP/Settings/MCPSettings.cs b/src/Infrastructure/BotSharp.MCP/Settings/MCPSettings.cs new file mode 100644 index 000000000..1279a08d8 --- /dev/null +++ b/src/Infrastructure/BotSharp.MCP/Settings/MCPSettings.cs @@ -0,0 +1,13 @@ +using ModelContextProtocol.Client; +using ModelContextProtocol.Configuration; +using System.Collections.Generic; + +namespace BotSharp.Core.Mcp.Settings; + +public class MCPSettings +{ + public McpClientOptions McpClientOptions { get; set; } + + public List McpServerConfigs { get; set; } + +} diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs index 8e4d915d4..81ec1bc7b 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentCreationModel.cs @@ -55,6 +55,7 @@ public class AgentCreationModel public int? MaxMessageCount { get; set; } public List Utilities { get; set; } = new(); + public List McpTools { get; set; } = new(); public List RoutingRules { get; set; } = new(); public List KnowledgeBases { get; set; } = new(); public List Rules { get; set; } = new(); @@ -73,6 +74,7 @@ public Agent ToAgent() Responses = Responses, Samples = Samples, Utilities = Utilities, + McpTools = McpTools, IsPublic = IsPublic, Type = Type, Disabled = Disabled, diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs index ac2749b55..9f2925e5d 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/Request/AgentUpdateModel.cs @@ -39,6 +39,11 @@ public class AgentUpdateModel /// public List? Utilities { get; set; } + /// + /// McpTools + /// + public List? McpTools { get; set; } + /// /// knowledge bases /// @@ -103,6 +108,7 @@ public Agent ToAgent() Functions = Functions ?? [], Responses = Responses ?? [], Utilities = Utilities ?? [], + McpTools = McpTools ?? [], KnowledgeBases = KnowledgeBases ?? [], Rules = Rules ?? [], LlmConfig = LlmConfig ?? new() diff --git a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs index b4ba1e088..db3af34ca 100644 --- a/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs +++ b/src/Infrastructure/BotSharp.OpenAPI/ViewModels/Agents/View/AgentViewModel.cs @@ -24,6 +24,7 @@ public class AgentViewModel [JsonPropertyName("merge_utility")] public bool MergeUtility { get; set; } public List Utilities { get; set; } + public List Acps { get; set; } [JsonPropertyName("knowledge_bases")] public List KnowledgeBases { get; set; } @@ -86,6 +87,7 @@ public static AgentViewModel FromAgent(Agent agent) Responses = agent.Responses ?? [], Samples = agent.Samples ?? [], Utilities = agent.Utilities ?? [], + Acps = agent.McpTools ?? [], KnowledgeBases = agent.KnowledgeBases ?? [], IsPublic= agent.IsPublic, Disabled = agent.Disabled, diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs index bdfdbe805..a0d99e0d9 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Collections/AgentDocument.cs @@ -12,17 +12,18 @@ public class AgentDocument : MongoBase public bool Disabled { get; set; } public bool MergeUtility { get; set; } public int? MaxMessageCount { get; set; } - public List ChannelInstructions { get; set; } = []; - public List Templates { get; set; } = []; - public List Functions { get; set; } = []; - public List Responses { get; set; } = []; - public List Samples { get; set; } = []; - public List Utilities { get; set; } = []; - public List KnowledgeBases { get; set; } = []; - public List Profiles { get; set; } = []; - public List Labels { get; set; } = []; - public List RoutingRules { get; set; } = []; - public List Rules { get; set; } = []; + public List ChannelInstructions { get; set; } + public List Templates { get; set; } + public List Functions { get; set; } + public List Responses { get; set; } + public List Samples { get; set; } + public List Utilities { get; set; } + public List McpTools { get; set; } + public List KnowledgeBases { get; set; } + public List Profiles { get; set; } + public List Labels { get; set; } + public List RoutingRules { get; set; } + public List Rules { get; set; } public AgentLlmConfigMongoElement? LlmConfig { get; set; } public DateTime CreatedTime { get; set; } diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentMCPToolMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentMCPToolMongoElement.cs new file mode 100644 index 000000000..614683c37 --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentMCPToolMongoElement.cs @@ -0,0 +1,47 @@ +using BotSharp.Abstraction.Agents.Models; + +namespace BotSharp.Plugin.MongoStorage.Models; + +[BsonIgnoreExtraElements(Inherited = true)] +public class AgentMCPToolMongoElement +{ + public string Name { get; set; } + + public string ServerId { get; set; } + + public bool Disabled { get; set; } + public List Functions { get; set; } = []; + + public static AgentMCPToolMongoElement ToMongoElement(MCPTool utility) + { + return new AgentMCPToolMongoElement + { + Disabled = utility.Disabled, + Functions = utility.Functions?.Select(x => new McpFunctionMongoElement(x.Name))?.ToList() ?? [], + }; + } + + public static MCPTool ToDomainElement(AgentMCPToolMongoElement utility) + { + return new MCPTool + { + Disabled = utility.Disabled, + Functions = utility.Functions?.Select(x => new MCPFunction(x.Name))?.ToList() ?? [], + }; + } +} + +public class McpFunctionMongoElement +{ + public string Name { get; set; } + + public McpFunctionMongoElement() + { + + } + + public McpFunctionMongoElement(string name) + { + Name = name; + } +} diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs index f433ee77e..ba4b3fdec 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -61,6 +61,9 @@ public void UpdateAgent(Agent agent, AgentField field) case AgentField.Utility: UpdateAgentUtilities(agent.Id, agent.MergeUtility, agent.Utilities); break; + case AgentField.McpTool: + UpdateAgentMcpTools(agent.Id, agent.McpTools); + break; case AgentField.KnowledgeBase: UpdateAgentKnowledgeBases(agent.Id, agent.KnowledgeBases); break; @@ -261,6 +264,19 @@ private void UpdateAgentUtilities(string agentId, bool mergeUtility, List mcps) + { + if (mcps == null) return; + + var elements = mcps?.Select(x => AgentMCPToolMongoElement.ToMongoElement(x))?.ToList() ?? []; + + var filter = Builders.Filter.Eq(x => x.Id, agentId); + var update = Builders.Update + .Set(x => x.McpTools, elements) + .Set(x => x.UpdatedTime, DateTime.UtcNow); + + _dc.Agents.UpdateOne(filter, update); + } private void UpdateAgentKnowledgeBases(string agentId, List knowledgeBases) { if (knowledgeBases == null) return; @@ -330,6 +346,7 @@ private void UpdateAgentAllFields(Agent agent) .Set(x => x.Responses, agent.Responses.Select(r => AgentResponseMongoElement.ToMongoElement(r)).ToList()) .Set(x => x.Samples, agent.Samples) .Set(x => x.Utilities, agent.Utilities.Select(u => AgentUtilityMongoElement.ToMongoElement(u)).ToList()) + .Set(x => x.McpTools, agent.McpTools.Select(u => AgentMCPToolMongoElement.ToMongoElement(u)).ToList()) .Set(x => x.KnowledgeBases, agent.KnowledgeBases.Select(u => AgentKnowledgeBaseMongoElement.ToMongoElement(u)).ToList()) .Set(x => x.Rules, agent.Rules.Select(e => AgentRuleMongoElement.ToMongoElement(e)).ToList()) .Set(x => x.LlmConfig, AgentLlmConfigMongoElement.ToMongoElement(agent.LlmConfig)) @@ -510,6 +527,7 @@ public void BulkInsertAgents(List agents) Responses = x.Responses?.Select(r => AgentResponseMongoElement.ToMongoElement(r))?.ToList() ?? [], RoutingRules = x.RoutingRules?.Select(r => RoutingRuleMongoElement.ToMongoElement(r))?.ToList() ?? [], Utilities = x.Utilities?.Select(u => AgentUtilityMongoElement.ToMongoElement(u))?.ToList() ?? [], + McpTools = x.McpTools?.Select(u => AgentMCPToolMongoElement.ToMongoElement(u))?.ToList() ?? [], KnowledgeBases = x.KnowledgeBases?.Select(k => AgentKnowledgeBaseMongoElement.ToMongoElement(k))?.ToList() ?? [], Rules = x.Rules?.Select(e => AgentRuleMongoElement.ToMongoElement(e))?.ToList() ?? [], CreatedTime = x.CreatedDateTime, @@ -604,6 +622,7 @@ private Agent TransformAgentDocument(AgentDocument? agentDoc) Responses = agentDoc.Responses?.Select(r => AgentResponseMongoElement.ToDomainElement(r))?.ToList() ?? [], RoutingRules = agentDoc.RoutingRules?.Select(r => RoutingRuleMongoElement.ToDomainElement(agentDoc.Id, agentDoc.Name, r))?.ToList() ?? [], Utilities = agentDoc.Utilities?.Select(u => AgentUtilityMongoElement.ToDomainElement(u))?.ToList() ?? [], + McpTools = agentDoc.McpTools?.Select(u => AgentMCPToolMongoElement.ToDomainElement(u))?.ToList() ?? [], KnowledgeBases = agentDoc.KnowledgeBases?.Select(x => AgentKnowledgeBaseMongoElement.ToDomainElement(x))?.ToList() ?? [], Rules = agentDoc.Rules?.Select(e => AgentRuleMongoElement.ToDomainElement(e))?.ToList() ?? [] }; diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 7817e1679..c2f3f0ed4 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -30,6 +30,7 @@ + diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index f3b271493..37952b50d 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -50,10 +50,7 @@ "Name": "gpt-35-turbo", "Version": "1106", "ApiKey": "", - "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/", - "Type": "chat", - "PromptCost": 0.0015, - "CompletionCost": 0.002 + "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/" }, { "Name": "gpt-35-turbo-instruct", @@ -170,11 +167,27 @@ "HostAgentId": "01e2fc5c-2c89-4ec7-8470-7688608b496c", "EnableTranslator": false, "LlmConfig": { - "Provider": "openai", + "Provider": "azure-openai", "Model": "gpt-4o-mini" } }, - + "MCPSettings": { + "McpClientOptions": { + "ClientInfo": { + "Name": "SimpleToolsBotsharp", + "Version": "1.0.0" + } + }, + "McpServerConfigs": [ + { + "Id": "PizzaServer", + "Name": "PizzaServer", + "TransportType": "sse", + "TransportOptions": [], + "Location": "http://localhost:58905/sse" + } + ] + }, "Conversation": { "DataDir": "conversations", "ShowVerboseLog": false, @@ -400,6 +413,7 @@ "BotSharp.Core.Crontab", "BotSharp.Core.Realtime", "BotSharp.Logger", + "BotSharp.MCP", "BotSharp.Plugin.MongoStorage", "BotSharp.Plugin.Dashboard", "BotSharp.Plugin.OpenAI", diff --git a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj new file mode 100644 index 000000000..319f10a10 --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + 12.0 + enable + + + + + + + + + diff --git a/tests/BotSharp.PizzaBot.MCPServer/McpEndpointRouteBuilderExtensions.cs b/tests/BotSharp.PizzaBot.MCPServer/McpEndpointRouteBuilderExtensions.cs new file mode 100644 index 000000000..f21384443 --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/McpEndpointRouteBuilderExtensions.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Options; +using ModelContextProtocol.Protocol.Messages; +using ModelContextProtocol.Server; +using ModelContextProtocol.Utils.Json; + +namespace BotSharp.PizzaBot.MCPServer; + +public static class McpEndpointRouteBuilderExtensions +{ + public static IEndpointConventionBuilder MapMcpSse(this IEndpointRouteBuilder endpoints) + { + IMcpServer? server = null; + SseServerStreamTransport? transport = null; + var loggerFactory = endpoints.ServiceProvider.GetRequiredService(); + var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService>(); + + var routeGroup = endpoints.MapGroup(""); + + routeGroup.MapGet("/sse", async (HttpResponse response, CancellationToken requestAborted) => + { + await using var localTransport = transport = new SseServerStreamTransport(response.Body); + await using var localServer = server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider); + + await localServer.StartAsync(requestAborted); + + response.Headers.ContentType = "text/event-stream"; + response.Headers.CacheControl = "no-cache"; + + try + { + await transport.RunAsync(requestAborted); + } + catch (OperationCanceledException) when (requestAborted.IsCancellationRequested) + { + // RequestAborted always triggers when the client disconnects before a complete response body is written, + // but this is how SSE connections are typically closed. + } + }); + + routeGroup.MapPost("/message", async (HttpContext context) => + { + if (transport is null) + { + await Results.BadRequest("Connect to the /sse endpoint before sending messages.").ExecuteAsync(context); + return; + } + + var message = await context.Request.ReadFromJsonAsync(McpJsonUtilities.DefaultOptions, context.RequestAborted); + if (message is null) + { + await Results.BadRequest("No message in request body.").ExecuteAsync(context); + return; + } + + await transport.OnMessageReceivedAsync(message, context.RequestAborted); + context.Response.StatusCode = StatusCodes.Status202Accepted; + await context.Response.WriteAsync("Accepted"); + }); + + return routeGroup; + } +} diff --git a/tests/BotSharp.PizzaBot.MCPServer/Program.cs b/tests/BotSharp.PizzaBot.MCPServer/Program.cs new file mode 100644 index 000000000..fb39e6cd7 --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/Program.cs @@ -0,0 +1,236 @@ +using BotSharp.PizzaBot.MCPServer; +using ModelContextProtocol; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddMcpServer().WithTools(); +var app = builder.Build(); + +app.MapGet("/", () => "This is a test server with only stub functionality!"); +app.MapMcpSse(); + +app.Run(); + + +//namespace BotSharp.PizzaBot.MCPServer +//{ +// internal class Program +// { +// private static HashSet _subscribedResources = new(); +// private static readonly object _subscribedResourcesLock = new(); + +// private static async Task Main(string[] args) +// { +// Console.WriteLine("Starting server..."); + +// McpServerOptions options = new McpServerOptions() +// { +// ServerInfo = new Implementation() { Name = "PizzaServer", Version = "1.0.0" }, +// Capabilities = new ServerCapabilities() +// { +// Tools = ConfigureTools(), +// }, +// ProtocolVersion = "2024-11-05", +// ServerInstructions = "This is a test server with only stub functionality" +// }; +// var loggerFactory = CreateLoggerFactory(); +// await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("TestServer", loggerFactory), options, loggerFactory); +// Log.Logger.Information("Server initialized."); + +// await server.StartAsync(); + +// Log.Logger.Information("Server started."); + +// // Run until process is stopped by the client (parent process) +// while (true) +// { +// await Task.Delay(5000); + +// // Snapshot the subscribed resources, rather than locking while sending notifications +// List resources; +// lock (_subscribedResourcesLock) +// { +// resources = _subscribedResources.ToList(); +// } + +// foreach (var resource in resources) +// { +// ResourceUpdatedNotificationParams notificationParams = new() { Uri = resource }; +// await server.SendMessageAsync(new JsonRpcNotification() +// { +// Method = NotificationMethods.ResourceUpdatedNotification, +// Params = notificationParams +// }); +// } +// } +// } + +// private static ToolsCapability ConfigureTools() +// { +// return new() +// { +// ListToolsHandler = (request, cancellationToken) => +// { +// return Task.FromResult(new ListToolsResult() +// { +// Tools = [ +// new Tool() +// { +// Name = "make_payment", +// Description = "call this function to make payment", +// InputSchema = new JsonSchema() +// { +// Type = "object", +// Properties = new Dictionary() +// { +// ["order_number"] = new JsonSchemaProperty() { Type = "string", Description = "order number." }, +// ["total_amount"] = new JsonSchemaProperty() { Type = "string", Description = "total amount." }, +// }, +// Required = new List() { "order_number", "total_amount" } +// }, +// }, +// new Tool() +// { +// Name = "get_pizza_prices", +// Description = "call this function to get pizza unit price", +// InputSchema = new JsonSchema() +// { +// Type = "object", +// Properties = new Dictionary() +// { +// ["pizza_type"] = new JsonSchemaProperty() { Type = "string", Description = "The pizza type." }, +// ["quantity"] = new JsonSchemaProperty() { Type = "string", Description = "quantity of pizza." }, + +// }, +// Required = new List(){ "pizza_type", "quantity" } +// } +// }, +// new Tool() +// { +// Name = "place_an_order", +// Description = "Place an order when user has confirmed the pizza type and quantity.", +// InputSchema = new JsonSchema() +// { +// Type = "object", +// Properties = new Dictionary() +// { +// ["pizza_type"] = new JsonSchemaProperty() { Type = "string", Description = "The pizza type." }, +// ["quantity"] = new JsonSchemaProperty() { Type = "number", Description = "quantity of pizza." }, +// ["unit_price"] = new JsonSchemaProperty() { Type = "number", Description = "pizza unit price" }, + +// }, +// Required = new List(){"pizza_type", "quantity", "unit_price" } +// } +// } +// ] +// }); +// }, + +// CallToolHandler = async (request, cancellationToken) => +// { +// if (request.Params.Name == "make_payment") +// { +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("order_number", out var order_number)) +// { +// throw new McpServerException("Missing required argument 'order_number'"); +// } +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("total_amount", out var total_amount)) +// { +// throw new McpServerException("Missing required argument 'total_amount'"); +// } +// //dynamic message = new ExpandoObject(); +// //message.Transaction = Guid.NewGuid().ToString(); +// //message.Status = "Success"; + +// //// Serialize the message to JSON +// //var jso = new JsonSerializerOptions() { WriteIndented = true }; +// //var jsonMessage = JsonSerializer.Serialize(message, jso); + +// return new CallToolResponse() +// { +// Content = [new Content() { Text = "Payment proceed successfully. Thank you for your business. Have a great day!", Type = "text" }] +// }; +// } +// else if (request.Params.Name == "get_pizza_prices") +// { +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("pizza_type", out var pizza_type)) +// { +// throw new McpServerException("Missing required argument 'pizza_type'"); +// } +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("quantity", out var quantity)) +// { +// throw new McpServerException("Missing required argument 'quantity'"); +// } +// double unit_price = 0; +// if(pizza_type.ToString() == "Pepperoni Pizza") +// { +// unit_price = 3.2 * (int)quantity; +// } +// else if(pizza_type.ToString() == "Cheese Pizza") +// { +// unit_price = 3.5 * (int)quantity; ; +// } +// else if(pizza_type.ToString() == "Margherita Pizza") +// { +// unit_price = 3.8 * (int)quantity; ; +// } +// dynamic message = new ExpandoObject(); +// message.unit_price = unit_price; +// var jso = new JsonSerializerOptions() { WriteIndented = true }; +// var jsonMessage = JsonSerializer.Serialize(message, jso); +// return new CallToolResponse() +// { +// Content = [new Content() { Text = jsonMessage, Type = "text" }] +// }; +// } +// else if (request.Params.Name == "place_an_order") +// { +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("pizza_type", out var pizza_type)) +// { +// throw new McpServerException("Missing required argument 'pizza_type'"); +// } +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("quantity", out var quantity)) +// { +// throw new McpServerException("Missing required argument 'quantity'"); +// } +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("unit_price", out var unit_price)) +// { +// throw new McpServerException("Missing required argument 'unit_price'"); +// } +// //dynamic message = new ExpandoObject(); +// //message.order_number = "P123-01"; +// //message.Content = "The order number is P123-01"; +// //// Serialize the message to JSON +// //var jso = new JsonSerializerOptions() { WriteIndented = true }; +// //var jsonMessage = JsonSerializer.Serialize(message, jso); +// return new CallToolResponse() +// { +// Content = [new Content() { Text = "The order number is P123-01: {order_number = \"P123-01\" }", Type = "text" }] +// }; +// } +// else +// { +// throw new McpServerException($"Unknown tool: {request.Params.Name}"); +// } +// } +// }; +// } + + +// private static ILoggerFactory CreateLoggerFactory() +// { +// // Use serilog +// Log.Logger = new LoggerConfiguration() +// .MinimumLevel.Verbose() // Capture all log levels +// .WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "TestServer_.log"), +// rollingInterval: RollingInterval.Day, +// outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") +// .CreateLogger(); + +// var logsPath = Path.Combine(AppContext.BaseDirectory, "testserver.log"); +// return LoggerFactory.Create(builder => +// { +// builder.AddSerilog(); +// }); +// } +// } +//} diff --git a/tests/BotSharp.PizzaBot.MCPServer/Properties/launchSettings.json b/tests/BotSharp.PizzaBot.MCPServer/Properties/launchSettings.json new file mode 100644 index 000000000..617e672ca --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "BotSharp.PizzaBot.MCPServer": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:58904;http://localhost:58905" + } + } +} \ No newline at end of file diff --git a/tests/BotSharp.PizzaBot.MCPServer/SseServerStreamTransport.cs b/tests/BotSharp.PizzaBot.MCPServer/SseServerStreamTransport.cs new file mode 100644 index 000000000..7a7ce51a6 --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/SseServerStreamTransport.cs @@ -0,0 +1,84 @@ +using ModelContextProtocol.Protocol.Messages; +using ModelContextProtocol.Protocol.Transport; +using ModelContextProtocol.Utils.Json; +using System.Buffers; +using System.Net.ServerSentEvents; +using System.Text.Json; +using System.Threading.Channels; + +namespace BotSharp.PizzaBot.MCPServer; + +public class SseServerStreamTransport(Stream sseResponseStream) : ITransport +{ + private readonly Channel _incomingChannel = CreateSingleItemChannel(); + private readonly Channel> _outgoingSseChannel = CreateSingleItemChannel>(); + + private Task? _sseWriteTask; + private Utf8JsonWriter? _jsonWriter; + + public bool IsConnected => _sseWriteTask?.IsCompleted == false; + + public Task RunAsync(CancellationToken cancellationToken) + { + void WriteJsonRpcMessageToBuffer(SseItem item, IBufferWriter writer) + { + if (item.EventType == "endpoint") + { + writer.Write("/message"u8); + return; + } + + JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, McpJsonUtilities.DefaultOptions); + } + + // The very first SSE event isn't really an IJsonRpcMessage, but there's no API to write a single item of a different type, + // so we fib and special-case the "endpoint" event type in the formatter. + _outgoingSseChannel.Writer.TryWrite(new SseItem(null, "endpoint")); + + var sseItems = _outgoingSseChannel.Reader.ReadAllAsync(cancellationToken); + return _sseWriteTask = SseFormatter.WriteAsync(sseItems, sseResponseStream, WriteJsonRpcMessageToBuffer, cancellationToken); + } + + public ChannelReader MessageReader => _incomingChannel.Reader; + + public ValueTask DisposeAsync() + { + _incomingChannel.Writer.TryComplete(); + _outgoingSseChannel.Writer.TryComplete(); + return new ValueTask(_sseWriteTask ?? Task.CompletedTask); + } + + public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) => + _outgoingSseChannel.Writer.WriteAsync(new SseItem(message), cancellationToken).AsTask(); + + public Task OnMessageReceivedAsync(IJsonRpcMessage message, CancellationToken cancellationToken) + { + if (!IsConnected) + { + throw new McpTransportException("Transport is not connected"); + } + + return _incomingChannel.Writer.WriteAsync(message, cancellationToken).AsTask(); + } + + private static Channel CreateSingleItemChannel() => + Channel.CreateBounded(new BoundedChannelOptions(1) + { + SingleReader = true, + SingleWriter = false, + }); + + private Utf8JsonWriter GetUtf8JsonWriter(IBufferWriter writer) + { + if (_jsonWriter is null) + { + _jsonWriter = new Utf8JsonWriter(writer); + } + else + { + _jsonWriter.Reset(writer); + } + + return _jsonWriter; + } +} diff --git a/tests/BotSharp.PizzaBot.MCPServer/Tools/MakePayment.cs b/tests/BotSharp.PizzaBot.MCPServer/Tools/MakePayment.cs new file mode 100644 index 000000000..a236d8749 --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/Tools/MakePayment.cs @@ -0,0 +1,25 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace BotSharp.PizzaBot.MCPServer.Tools; + +[McpToolType] +public static class MakePayment +{ + [McpTool(name: "make_payment"), Description("call this function to make payment.")] + public static string Make_Payment( + [Description("order number"),Required] string order_number, + [Description("total amount"),Required] int total_amount) + { + if (order_number is null) + { + throw new McpServerException("Missing required argument 'order_number'"); + } + if (order_number is null) + { + throw new McpServerException("Missing required argument 'total_amount'"); + } + return "Payment proceed successfully. Thank you for your business. Have a great day!"; + } +} diff --git a/tests/BotSharp.PizzaBot.MCPServer/Tools/PizzaPrices.cs b/tests/BotSharp.PizzaBot.MCPServer/Tools/PizzaPrices.cs new file mode 100644 index 000000000..7ca766f3d --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/Tools/PizzaPrices.cs @@ -0,0 +1,46 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Dynamic; +using System.Text.Json; + +namespace BotSharp.PizzaBot.MCPServer.Tools; + +[McpToolType] +public static class PizzaPrices +{ + [McpTool(name: "get_pizza_prices"), Description("call this function to get pizza unit price.")] + public static string GetPizzaPrices( + [Description("The pizza type."), Required] string pizza_type, + [Description("quantity of pizza"), Required] int quantity) + { + if (pizza_type is null) + { + throw new McpServerException("Missing required argument 'pizza_type'"); + } + if (quantity <= 0) + { + throw new McpServerException("Missing required argument 'quantity'"); + } + double unit_price = 0; + if (pizza_type.ToString() == "Pepperoni Pizza") + { + unit_price = 3.2 * (int)quantity; + } + else if (pizza_type.ToString() == "Cheese Pizza") + { + unit_price = 3.5 * (int)quantity; ; + } + else if (pizza_type.ToString() == "Margherita Pizza") + { + unit_price = 3.8 * (int)quantity; ; + } + + dynamic message = new ExpandoObject(); + message.unit_price = unit_price; + var jso = new JsonSerializerOptions() { WriteIndented = true }; + var jsonMessage = JsonSerializer.Serialize(message, jso); + + return jsonMessage; + } +} diff --git a/tests/BotSharp.PizzaBot.MCPServer/Tools/PlaceOrder.cs b/tests/BotSharp.PizzaBot.MCPServer/Tools/PlaceOrder.cs new file mode 100644 index 000000000..a6237b6dd --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/Tools/PlaceOrder.cs @@ -0,0 +1,31 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace BotSharp.PizzaBot.MCPServer.Tools; + +[McpToolType] +public static class PlaceOrder +{ + [McpTool(name: "place_an_order"), Description("Place an order when user has confirmed the pizza type and quantity.")] + public static string PlaceAnOrder( + [Description("The pizza type."), Required] string pizza_type, + [Description("quantity of pizza"), Required] int quantity, + [Description("pizza unit price"),Required] double unit_price) + { + if (pizza_type is null) + { + throw new McpServerException("Missing required argument 'pizza_type'"); + } + if (quantity <= 0) + { + throw new McpServerException("Missing required argument 'quantity'"); + } + if (unit_price <= 0) + { + throw new McpServerException("Missing required argument 'unit_price'"); + } + + return "The order number is P123-01: {order_number = \"P123-01\" }"; + } +} diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaPricesFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaPricesFn.cs index 4fa39c7c8..9433c6a81 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaPricesFn.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaPricesFn.cs @@ -1,21 +1,21 @@ -using BotSharp.Abstraction.Conversations.Models; -using System.Text.Json; +//using BotSharp.Abstraction.Conversations.Models; +//using System.Text.Json; -namespace BotSharp.Plugin.PizzaBot.Functions; +//namespace BotSharp.Plugin.PizzaBot.Functions; -public class GetPizzaPricesFn : IFunctionCallback -{ - public string Name => "get_pizza_price"; +//public class GetPizzaPricesFn : IFunctionCallback +//{ +// public string Name => "get_pizza_price"; - public async Task Execute(RoleDialogModel message) - { - message.Data = new - { - pepperoni_unit_price = 3.2, - cheese_unit_price = 3.5, - margherita_unit_price = 3.8, - }; - message.Content = JsonSerializer.Serialize(message.Data); - return true; - } -} +// public async Task Execute(RoleDialogModel message) +// { +// message.Data = new +// { +// pepperoni_unit_price = 3.2, +// cheese_unit_price = 3.5, +// margherita_unit_price = 3.8, +// }; +// message.Content = JsonSerializer.Serialize(message.Data); +// return true; +// } +//} diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs index 9a239fd23..1ce3aac3b 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Functions/GetPizzaTypesFn.cs @@ -46,7 +46,7 @@ public async Task Execute(RoleDialogModel message) }).ToArray() } }; - + return true; } } diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/MakePaymentFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/MakePaymentFn.cs index 144eb05a7..5a34a6ce5 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Functions/MakePaymentFn.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Functions/MakePaymentFn.cs @@ -1,19 +1,19 @@ -using BotSharp.Abstraction.Conversations.Models; +//using BotSharp.Abstraction.Conversations.Models; -namespace BotSharp.Plugin.PizzaBot.Functions; +//namespace BotSharp.Plugin.PizzaBot.Functions; -public class MakePaymentFn : IFunctionCallback -{ - public string Name => "make_payment"; +//public class MakePaymentFn : IFunctionCallback +//{ +// public string Name => "make_payment"; - public async Task Execute(RoleDialogModel message) - { - message.Content = "Payment proceed successfully. Thank you for your business. Have a great day!"; - message.Data = new - { - Transaction = Guid.NewGuid().ToString(), - Status = "Success" - }; - return true; - } -} +// public async Task Execute(RoleDialogModel message) +// { +// message.Content = "Payment proceed successfully. Thank you for your business. Have a great day!"; +// message.Data = new +// { +// Transaction = Guid.NewGuid().ToString(), +// Status = "Success" +// }; +// return true; +// } +//} diff --git a/tests/BotSharp.Plugin.PizzaBot/Functions/PlaceOrderFn.cs b/tests/BotSharp.Plugin.PizzaBot/Functions/PlaceOrderFn.cs index 87466956f..47d273635 100644 --- a/tests/BotSharp.Plugin.PizzaBot/Functions/PlaceOrderFn.cs +++ b/tests/BotSharp.Plugin.PizzaBot/Functions/PlaceOrderFn.cs @@ -1,24 +1,24 @@ -using BotSharp.Abstraction.Conversations; -using BotSharp.Abstraction.Conversations.Models; +//using BotSharp.Abstraction.Conversations; +//using BotSharp.Abstraction.Conversations.Models; -namespace BotSharp.Plugin.PizzaBot.Functions; +//namespace BotSharp.Plugin.PizzaBot.Functions; -public class PlaceOrderFn : IFunctionCallback -{ - public string Name => "place_an_order"; +//public class PlaceOrderFn : IFunctionCallback +//{ +// public string Name => "place_an_order"; - private readonly IServiceProvider _service; - public PlaceOrderFn(IServiceProvider service) - { - _service = service; - } +// private readonly IServiceProvider _service; +// public PlaceOrderFn(IServiceProvider service) +// { +// _service = service; +// } - public async Task Execute(RoleDialogModel message) - { - message.Content = "The order number is P123-01"; - var state = _service.GetRequiredService(); - state.SetState("order_number", "P123-01"); +// public async Task Execute(RoleDialogModel message) +// { +// message.Content = "The order number is P123-01"; +// var state = _service.GetRequiredService(); +// state.SetState("order_number", "P123-01"); - return true; - } -} +// return true; +// } +//} diff --git a/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaBotConversationHook.cs b/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaBotConversationHook.cs new file mode 100644 index 000000000..92083ee7e --- /dev/null +++ b/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaBotConversationHook.cs @@ -0,0 +1,47 @@ +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Conversations.Models; +using System.Linq; +using System.Text.Json; + +namespace BotSharp.Plugin.PizzaBot.Hooks; + +public class PizzaBotConversationHook : ConversationHookBase +{ + private readonly IServiceProvider _services; + private readonly IConversationStateService _states; + + public PizzaBotConversationHook(IServiceProvider services, + IConversationStateService states) + { + _services = services; + _states = states; + } + + public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg) + { + if (replyMsg.FunctionName == "get_pizza_types") + { + // message.StopCompletion = true; + } + return; + } + + public override Task OnTaskCompleted(RoleDialogModel message) + { + return base.OnTaskCompleted(message); + } + + public override async Task OnResponseGenerated(RoleDialogModel message) + { + var agentService = _services.GetRequiredService(); + var state = _services.GetRequiredService(); + var agent = await agentService.LoadAgent(message.CurrentAgentId); + if (agent.McpTools.Any(item => item.Functions.Any(x => x.Name == message.FunctionName))) + { + var data = JsonDocument.Parse(JsonSerializer.Serialize(message.Data)); + state.SaveStateByArgs(data); + } + await base.OnResponseGenerated(message); + } +} diff --git a/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaTypeConversationHook.cs b/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaTypeConversationHook.cs deleted file mode 100644 index 6fb34772b..000000000 --- a/tests/BotSharp.Plugin.PizzaBot/Hooks/PizzaTypeConversationHook.cs +++ /dev/null @@ -1,21 +0,0 @@ -using BotSharp.Abstraction.Conversations; -using BotSharp.Abstraction.Conversations.Models; - -namespace BotSharp.Plugin.PizzaBot.Hooks; - -public class PizzaTypeConversationHook : ConversationHookBase -{ - public override async Task OnPostbackMessageReceived(RoleDialogModel message, PostbackMessageModel replyMsg) - { - if (replyMsg.FunctionName == "get_pizza_types") - { - // message.StopCompletion = true; - } - return; - } - - public override Task OnTaskCompleted(RoleDialogModel message) - { - return base.OnTaskCompleted(message); - } -} diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json index 40988cb23..2e40c4e03 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json @@ -1,11 +1,25 @@ { "name": "Ordering", - "description": "Provide types of pizza available, unit price, total cost and place the order.", + "description": "Provide types of pizza available, pizza unit price, total cost and place the order.", "createdDateTime": "2023-07-26T02:29:25.123224Z", "updatedDateTime": "2023-07-26T02:29:25.123274Z", "id": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd", "disabled": false, "isPublic": true, "profiles": [ "pizza" ], + "mcptools": [ + { + "serverid": "PizzaServer", + "disabled": false, + "functions": [ + { + "Name": "get_pizza_price" + }, + { + "Name": "place_an_order" + } + ] + } + ], "labels": [ "experiment" ] } \ No newline at end of file diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/get_pizza_price.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/get_pizza_price.json index e2eaac315..1ecdbb659 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/get_pizza_price.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/get_pizza_price.json @@ -1,18 +1,18 @@ -{ - "name": "get_pizza_price", - "description": "call this function to get the pizza price", - "parameters": { - "type": "object", - "properties": { - "pizza_type": { - "type": "string", - "description": "The pizza type." - }, - "quantity": { - "type": "string", - "description": "quantity of pizza." - } - }, - "required": [ "pizza_type", "quantity" ] - } -} \ No newline at end of file +//{ +// "name": "get_pizza_price", +// "description": "call this function to get the pizza price", +// "parameters": { +// "type": "object", +// "properties": { +// "pizza_type": { +// "type": "string", +// "description": "The pizza type." +// }, +// "quantity": { +// "type": "string", +// "description": "quantity of pizza." +// } +// }, +// "required": [ "pizza_type", "quantity" ] +// } +//} \ No newline at end of file diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_an_order.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_an_order.json index d5514fba0..a01f0005b 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_an_order.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/functions/place_an_order.json @@ -1,22 +1,22 @@ -{ - "name": "place_an_order", - "description": "Place an order when user has confirmed the pizza type and quantity.", - "parameters": { - "type": "object", - "properties": { - "pizza_type": { - "type": "string", - "description": "The pizza type." - }, - "quantity": { - "type": "number", - "description": "quantity of pizza." - }, - "unit_price": { - "type": "number", - "description": "unit price" - } - }, - "required": [ "pizza_type", "quantity", "unit_price" ] - } -} \ No newline at end of file +//{ +// "name": "place_an_order", +// "description": "Place an order when user has confirmed the pizza type and quantity.", +// "parameters": { +// "type": "object", +// "properties": { +// "pizza_type": { +// "type": "string", +// "description": "The pizza type." +// }, +// "quantity": { +// "type": "number", +// "description": "quantity of pizza." +// }, +// "unit_price": { +// "type": "number", +// "description": "unit price" +// } +// }, +// "required": [ "pizza_type", "quantity", "unit_price" ] +// } +//} \ No newline at end of file diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json index ef47f34d1..f34ffb767 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/agent.json @@ -7,6 +7,17 @@ "disabled": false, "isPublic": true, "profiles": [ "pizza" ], + "mcptools": [ + { + "serverid": "PizzaServer", + "disabled": false, + "functions": [ + { + "Name": "make_payment" + } + ] + } + ], "labels": [ "experiment" ], "routingRules": [ { diff --git a/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/functions/make_payment.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/functions/make_payment.json index 82782c941..914f215e2 100644 --- a/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/functions/make_payment.json +++ b/tests/BotSharp.Plugin.PizzaBot/data/agents/fe8c60aa-b114-4ef3-93cb-a8efeac80f75/functions/make_payment.json @@ -1,18 +1,18 @@ -{ - "name": "make_payment", - "description": "call this function to make payment", - "parameters": { - "type": "object", - "properties": { - "order_number": { - "type": "string", - "description": "order number." - }, - "total_amount": { - "type": "string", - "description": "total amount." - } - }, - "required": [ "order_number", "total_amount" ] - } -} \ No newline at end of file +//{ +// "name": "make_payment", +// "description": "call this function to make payment", +// "parameters": { +// "type": "object", +// "properties": { +// "order_number": { +// "type": "string", +// "description": "order number." +// }, +// "total_amount": { +// "type": "string", +// "description": "total amount." +// } +// }, +// "required": [ "order_number", "total_amount" ] +// } +//} \ No newline at end of file