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