Merge branch 'SciSharp:master' into master

This commit is contained in:
evan-cao-wb 2025-03-27 15:50:55 -05:00 committed by GitHub
commit 3506179eaf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1353 additions and 189 deletions

View file

@ -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}

View file

@ -15,13 +15,16 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.0.0" />
<PackageVersion Include="System.Linq.AsyncEnumerable" Version="10.0.0-preview.1.25080.5" />
<PackageVersion Include="System.Memory.Data" Version="8.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Rougamo.Fody" Version="4.0.4" />
<PackageVersion Include="Aspects.Cache" Version="2.0.4" />
@ -106,6 +109,8 @@
<PackageVersion Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageVersion Include="MSTest.TestFramework" Version="3.1.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="ModelContextProtocol" Version="0.1.0-preview.1.25171.12" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="BotSharp.Core" Version="$(BotSharpVersion)" />

View file

@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\tests\BotSharp.PizzaBot.MCPServer\BotSharp.PizzaBot.MCPServer.csproj" />
<ProjectReference Include="..\BotSharp.ServiceDefaults\BotSharp.ServiceDefaults.csproj" />
<ProjectReference Include="..\WebStarter\WebStarter.csproj" />
</ItemGroup>

View file

@ -2,6 +2,8 @@ var builder = DistributedApplication.CreateBuilder(args);
var apiService = builder.AddProject<Projects.WebStarter>("apiservice")
.WithExternalHttpEndpoints();
var mcpService = builder.AddProject<Projects.BotSharp_PizzaBot_MCPServer>("mcpservice")
.WithExternalHttpEndpoints();
builder.AddNpmApp("BotSharpUI", "../../../BotSharp-UI")
.WithReference(apiService)

View file

@ -57,5 +57,11 @@ public abstract class AgentHookBase : IAgentHook
public virtual void OnAgentUtilityLoaded(Agent agent)
{
}
public virtual void OnAgentMCPToolLoaded(Agent agent)
{
}
}

View file

@ -19,6 +19,7 @@ public enum AgentField
Sample,
LlmConfig,
Utility,
McpTool,
KnowledgeBase,
Rule,
MaxMessageCount

View file

@ -27,6 +27,8 @@ public interface IAgentHook
void OnAgentUtilityLoaded(Agent agent);
void OnAgentMCPToolLoaded(Agent agent);
/// <summary>
/// Triggered when agent is loaded completely.
/// </summary>

View file

@ -95,15 +95,20 @@ public class Agent
public List<string> Labels { get; set; } = new();
/// <summary>
/// Merge utilities from entry agent
/// Merge Utility from entry agent
/// </summary>
public bool MergeUtility { get; set; }
/// <summary>
/// Agent utilities
/// Agent Utility
/// </summary>
public List<AgentUtility> Utilities { get; set; } = new();
/// <summary>
/// Agent MCP Tools
/// </summary>
public List<MCPTool> McpTools { get; set; } = new();
/// <summary>
/// Agent rules
/// </summary>
@ -157,6 +162,7 @@ public class 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 class Agent
LlmConfig = llmConfig;
return this;
}
public Agent SetMcps(List<MCPTool> mcps)
{
McpTools = mcps ?? [];
return this;
}
}

View file

@ -0,0 +1,37 @@
namespace BotSharp.Abstraction.Agents.Models;
public class MCPTool
{
public string ServerId { get; set; }
public bool Disabled { get; set; }
public IEnumerable<MCPFunction> Functions { get; set; } = [];
public MCPTool()
{
}
public MCPTool(
IEnumerable<MCPFunction>? 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;
}
}

View file

@ -20,12 +20,12 @@ public class RealtimeHub : IRealtimeHub
public async Task Listen(WebSocket userWebSocket,
Action<string> onUserMessageReceived)
{
var buffer = new byte[1024 * 16];
var buffer = new byte[1024 * 32];
WebSocketReceiveResult result;
do
{
Array.Clear(buffer, 0, buffer.Length);
result = await userWebSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
string receivedText = Encoding.UTF8.GetString(buffer, 0, result.Count);

View file

@ -38,6 +38,7 @@ public class BasicAgentHook : AgentHookBase
agent.SecondaryInstructions.Add(prompt);
}
}
private (IEnumerable<FunctionDef>, IEnumerable<string>) GetUtilityContent(Agent agent)
{
@ -81,5 +82,5 @@ public class BasicAgentHook : AgentHookBase
.Distinct().ToList();
return (functionNames, templateNames);
}
}
}

View file

@ -71,6 +71,11 @@ public partial class AgentService
{
hook.OnAgentUtilityLoaded(agent);
}
if(agent.McpTools != null && agent.McpTools.Count >0)
{
hook.OnAgentMCPToolLoaded(agent);
}
hook.OnAgentLoaded(agent);
}

View file

@ -40,6 +40,7 @@ public partial class AgentService
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 partial class AgentService
.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 partial class AgentService
var samples = GetSamplesFromFile(dir);
return agent.SetInstruction(defaultInstruction)
.SetChannelInstructions(channelInstructions)
.SetTemplates(templates)
.SetTemplates(templates)
.SetFunctions(functions)
.SetResponses(responses)
.SetSamples(samples);

View file

@ -60,6 +60,9 @@ namespace BotSharp.Core.Repository
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 @@ namespace BotSharp.Core.Repository
File.WriteAllText(agentFile, json);
}
private void UpdateAgentMcpTools(string agentId, List<MCPTool> 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<AgentKnowledgeBase> knowledgeBases)
{
if (knowledgeBases == null) return;
@ -360,6 +377,7 @@ namespace BotSharp.Core.Repository
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;

View file

@ -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<List<string>>(required.GetRawText())
}
};
return funDef;
}
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<OutputPath>$(SolutionDir)packages</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="System.Linq.AsyncEnumerable" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
</ItemGroup>
</Project>

View file

@ -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<bool> Execute(RoleDialogModel message)
{
// Convert arguments to dictionary format expected by mcpdotnet
Dictionary<string, object> argDict = JsonToDictionary(message.FunctionArgs);
var currentAgentId = message.CurrentAgentId;
var agentService = _serviceProvider.GetRequiredService<IAgentService>();
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<string, object> JsonToDictionary(string? json)
{
if (string.IsNullOrEmpty(json))
return [];
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
return JsonElementToDictionary(root);
}
private static Dictionary<string, object> JsonElementToDictionary(JsonElement element)
{
Dictionary<string, object> 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.")
};
}

View file

@ -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<IConversationService>();
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<IEnumerable<FunctionDef>> GetMCPContent(Agent agent)
{
List<FunctionDef> functionDefs = new List<FunctionDef>();
var mcpClientManager = _services.GetRequiredService<MCPClientManager>();
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;
}
}

View file

@ -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<IMcpClient> GetMcpClientAsync(string serverId)
{
return await McpClientFactory.CreateAsync(mcpSettings.McpServerConfigs
.Where(x=> x.Name == serverId).First(), mcpSettings.McpClientOptions);
}
public void Dispose()
{
}
}

View file

@ -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<MCPSettings>();
services.AddScoped<MCPSettings>(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<IAgentHook, MCPToolAgentHook>();
}
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<IFunctionCallback>(provider =>
{
var funcTool = new McpToolAdapter(provider, tool, clientManager);
return funcTool;
});
}
}
}

View file

@ -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<McpServerConfig> McpServerConfigs { get; set; }
}

View file

@ -55,6 +55,7 @@ public class AgentCreationModel
public int? MaxMessageCount { get; set; }
public List<AgentUtility> Utilities { get; set; } = new();
public List<MCPTool> McpTools { get; set; } = new();
public List<RoutingRuleUpdateModel> RoutingRules { get; set; } = new();
public List<AgentKnowledgeBase> KnowledgeBases { get; set; } = new();
public List<AgentRule> Rules { get; set; } = new();
@ -73,6 +74,7 @@ public class AgentCreationModel
Responses = Responses,
Samples = Samples,
Utilities = Utilities,
McpTools = McpTools,
IsPublic = IsPublic,
Type = Type,
Disabled = Disabled,

View file

@ -39,6 +39,11 @@ public class AgentUpdateModel
/// </summary>
public List<AgentUtility>? Utilities { get; set; }
/// <summary>
/// McpTools
/// </summary>
public List<MCPTool>? McpTools { get; set; }
/// <summary>
/// knowledge bases
/// </summary>
@ -103,6 +108,7 @@ public class AgentUpdateModel
Functions = Functions ?? [],
Responses = Responses ?? [],
Utilities = Utilities ?? [],
McpTools = McpTools ?? [],
KnowledgeBases = KnowledgeBases ?? [],
Rules = Rules ?? [],
LlmConfig = LlmConfig ?? new()

View file

@ -24,6 +24,7 @@ public class AgentViewModel
[JsonPropertyName("merge_utility")]
public bool MergeUtility { get; set; }
public List<AgentUtility> Utilities { get; set; }
public List<MCPTool> Acps { get; set; }
[JsonPropertyName("knowledge_bases")]
public List<AgentKnowledgeBase> KnowledgeBases { get; set; }
@ -86,6 +87,7 @@ public class AgentViewModel
Responses = agent.Responses ?? [],
Samples = agent.Samples ?? [],
Utilities = agent.Utilities ?? [],
Acps = agent.McpTools ?? [],
KnowledgeBases = agent.KnowledgeBases ?? [],
IsPublic= agent.IsPublic,
Disabled = agent.Disabled,

View file

@ -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<ChannelInstructionMongoElement> ChannelInstructions { get; set; } = [];
public List<AgentTemplateMongoElement> Templates { get; set; } = [];
public List<FunctionDefMongoElement> Functions { get; set; } = [];
public List<AgentResponseMongoElement> Responses { get; set; } = [];
public List<string> Samples { get; set; } = [];
public List<AgentUtilityMongoElement> Utilities { get; set; } = [];
public List<AgentKnowledgeBaseMongoElement> KnowledgeBases { get; set; } = [];
public List<string> Profiles { get; set; } = [];
public List<string> Labels { get; set; } = [];
public List<RoutingRuleMongoElement> RoutingRules { get; set; } = [];
public List<AgentRuleMongoElement> Rules { get; set; } = [];
public List<ChannelInstructionMongoElement> ChannelInstructions { get; set; }
public List<AgentTemplateMongoElement> Templates { get; set; }
public List<FunctionDefMongoElement> Functions { get; set; }
public List<AgentResponseMongoElement> Responses { get; set; }
public List<string> Samples { get; set; }
public List<AgentUtilityMongoElement> Utilities { get; set; }
public List<AgentMCPToolMongoElement> McpTools { get; set; }
public List<AgentKnowledgeBaseMongoElement> KnowledgeBases { get; set; }
public List<string> Profiles { get; set; }
public List<string> Labels { get; set; }
public List<RoutingRuleMongoElement> RoutingRules { get; set; }
public List<AgentRuleMongoElement> Rules { get; set; }
public AgentLlmConfigMongoElement? LlmConfig { get; set; }
public DateTime CreatedTime { get; set; }

View file

@ -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<McpFunctionMongoElement> 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;
}
}

View file

@ -61,6 +61,9 @@ public partial class MongoRepository
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 @@ public partial class MongoRepository
_dc.Agents.UpdateOne(filter, update);
}
private void UpdateAgentMcpTools(string agentId, List<MCPTool> mcps)
{
if (mcps == null) return;
var elements = mcps?.Select(x => AgentMCPToolMongoElement.ToMongoElement(x))?.ToList() ?? [];
var filter = Builders<AgentDocument>.Filter.Eq(x => x.Id, agentId);
var update = Builders<AgentDocument>.Update
.Set(x => x.McpTools, elements)
.Set(x => x.UpdatedTime, DateTime.UtcNow);
_dc.Agents.UpdateOne(filter, update);
}
private void UpdateAgentKnowledgeBases(string agentId, List<AgentKnowledgeBase> knowledgeBases)
{
if (knowledgeBases == null) return;
@ -330,6 +346,7 @@ public partial class MongoRepository
.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 partial class MongoRepository
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 @@ public partial class MongoRepository
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() ?? []
};

View file

@ -23,13 +23,14 @@ public class RealtimeSessionBody
public string[] Modalities { get; set; } = ["audio", "text"];
[JsonPropertyName("input_audio_format")]
public string InputAudioFormat { get; set; } = "pcm16";
public string InputAudioFormat { get; set; } = null!;
[JsonPropertyName("output_audio_format")]
public string OutputAudioFormat { get; set; } = "pcm16";
public string OutputAudioFormat { get; set; } = null!;
[JsonPropertyName("input_audio_transcription")]
public InputAudioTranscription InputAudioTranscription { get; set; } = new();
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public InputAudioTranscription? InputAudioTranscription { get; set; }
[JsonPropertyName("instructions")]
public string Instructions { get; set; } = "You are a friendly assistant.";

View file

@ -148,6 +148,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
do
{
Array.Clear(buffer, 0, buffer.Length);
result = await _webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), CancellationToken.None);
@ -336,12 +337,12 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
{
InputAudioFormat = "g711_ulaw",
OutputAudioFormat = "g711_ulaw",
InputAudioTranscription = new InputAudioTranscription
/*InputAudioTranscription = new InputAudioTranscription
{
Model = realtimeModelSettings.InputAudioTranscription.Model,
Language = realtimeModelSettings.InputAudioTranscription.Language,
Prompt = string.Join(", ", words.Select(x => x.ToLower().Trim()).Distinct()).SubstringMax(1024)
},
},*/
Voice = realtimeModelSettings.Voice,
Instructions = instruction,
ToolChoice = "auto",
@ -695,7 +696,7 @@ public class RealTimeCompletionProvider : IRealTimeCompletion
public async Task<RoleDialogModel> OnConversationItemCreated(RealtimeHubConnection conn, string response)
{
var item = JsonSerializer.Deserialize<ConversationItemCreated>(response).Item;
var item = response.JsonContent<ConversationItemCreated>().Item;
var message = new RoleDialogModel(item.Role, item.Content.FirstOrDefault()?.Transcript);
return message;

View file

@ -56,7 +56,6 @@ public class TwilioOutboundController : TwilioController
{
instruction.SpeechPaths.Add(request.InitAudioFile);
}
response = twilio.ReturnNoninterruptedInstructions(instruction);
}

View file

@ -1,8 +1,12 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.IO;
namespace BotSharp.Plugin.Twilio.Controllers;
@ -41,4 +45,31 @@ public class TwilioRecordController : TwilioController
return Ok();
}
[ValidateRequest]
[HttpPost("twilio/record/transcribe")]
public async Task<ActionResult> PhoneRecordingTranscribe(ConversationalVoiceRequest request)
{
if (request.Final == "true")
{
_logger.LogError($"Transcription completed for {request.CallSid}, the transcription is: {request.TranscriptionData}");
// transcription completed
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnTranscribeCompleted(request));
// Append the transcription to the dialog history
var transcript = JsonConvert.DeserializeObject<TranscriptionData>(request.TranscriptionData);
if (transcript != null && !string.IsNullOrEmpty(transcript.Transcript))
{
var storage = _services.GetRequiredService<IConversationStorage>();
var message = new RoleDialogModel(AgentRole.User, transcript.Transcript)
{
CurrentAgentId = request.AgentId
};
storage.Append(request.ConversationId, message);
}
}
return Ok();
}
}

View file

@ -53,12 +53,10 @@ public class TwilioStreamController : TwilioController
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreating(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
request.ConversationId = await InitConversation(request);
instruction.AgentId = request.AgentId;
instruction.ConversationId = request.ConversationId;
if (request.AnsweredBy == "machine_start" &&
@ -82,9 +80,6 @@ public class TwilioStreamController : TwilioController
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreated(request);
}, new HookEmitOption
{
OnlyOnce = true
});
return TwiML(response);

View file

@ -8,5 +8,6 @@ public interface ITwilioCallStatusHook
Task OnVoicemailLeft(ConversationalVoiceRequest request);
Task OnUserDisconnected(ConversationalVoiceRequest request);
Task OnRecordingCompleted(ConversationalVoiceRequest request);
Task OnTranscribeCompleted(ConversationalVoiceRequest request);
Task OnVoicemailStarting(ConversationalVoiceRequest request);
}

View file

@ -44,4 +44,32 @@ public class ConversationalVoiceRequest : VoiceRequest
[FromForm]
public int CallDuration { get; set; }
#region Transcription
[FromForm]
public string? LanguageCode { get; set; }
[FromForm]
public string? Stability { get; set; }
[FromForm]
public string? TranscriptionData { get; set; }
[FromForm]
public string? Final { get; set; }
[FromForm]
public string? Track { get; set; }
[FromForm]
public string? SequenceId { get; set; }
[FromForm]
public string? TranscriptionEvent { get; set; }
#endregion
}
public class TranscriptionData
{
public string Transcript { get; set; } = null!;
}

View file

@ -13,7 +13,6 @@ public class HangupPhoneCallFn : IFunctionCallback
private readonly TwilioSetting _twilioSetting;
public string Name => "util-twilio-hangup_phone_call";
public string Indication => "Hangup";
public HangupPhoneCallFn(
IServiceProvider services,

View file

@ -4,9 +4,6 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
public class LeaveVoicemailArgs
{
[JsonPropertyName("phone_number")]
public string PhoneNumber { get; set; } = null!;
[JsonPropertyName("voicemail_message")]
public string VoicemailMessage { get; set; } = null!;
}

View file

@ -202,8 +202,19 @@ public class TwilioService
public VoiceResponse ReturnBidirectionalMediaStreamsInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
{
var response = new VoiceResponse();
var conversationId = conversationalVoiceResponse.ConversationId;
if (_settings.TranscribeEnabled)
{
var start = new Start();
start.Transcription(
track: "inbound_track",
partialResults: false,
statusCallbackUrl: $"{_settings.CallbackHost}/twilio/record/transcribe?agent-id={conversationalVoiceResponse.AgentId}&conversation-id={conversationId}", name: conversationId);
response.Append(start);
}
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
{
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)

View file

@ -31,4 +31,5 @@ public class TwilioSetting
public int MachineDetectionSilenceTimeout { get; set; } = 2500;
public bool RecordingEnabled { get; set; } = false;
public bool TranscribeEnabled { get; set; } = false;
}

View file

@ -8,12 +8,8 @@
"voicemail_message": {
"type": "string",
"description": "User voicemail with details."
},
"phone_number": {
"type": "string",
"description": "Phone number to callback."
}
},
"required": [ "voicemail_message", "phone_number" ]
"required": [ "voicemail_message" ]
}
}

View file

@ -30,6 +30,7 @@
<ItemGroup>
<ProjectReference Include="..\..\tests\BotSharp.Plugin.PizzaBot\BotSharp.Plugin.PizzaBot.csproj" />
<ProjectReference Include="..\BotSharp.ServiceDefaults\BotSharp.ServiceDefaults.csproj" />
<ProjectReference Include="..\Infrastructure\BotSharp.MCP\BotSharp.MCP.csproj" />
</ItemGroup>
<ItemGroup Condition="$(SolutionName)==BotSharp">

View file

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

View file

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="ModelContextProtocol" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.File" />
</ItemGroup>
</Project>

View file

@ -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<ILoggerFactory>();
var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
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<IJsonRpcMessage>(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;
}
}

View file

@ -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<string> _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<string> 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<string, JsonSchemaProperty>()
// {
// ["order_number"] = new JsonSchemaProperty() { Type = "string", Description = "order number." },
// ["total_amount"] = new JsonSchemaProperty() { Type = "string", Description = "total amount." },
// },
// Required = new List<string>() { "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<string, JsonSchemaProperty>()
// {
// ["pizza_type"] = new JsonSchemaProperty() { Type = "string", Description = "The pizza type." },
// ["quantity"] = new JsonSchemaProperty() { Type = "string", Description = "quantity of pizza." },
// },
// Required = new List<string>(){ "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<string, JsonSchemaProperty>()
// {
// ["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<string>(){"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();
// });
// }
// }
//}

View file

@ -0,0 +1,12 @@
{
"profiles": {
"BotSharp.PizzaBot.MCPServer": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:58904;http://localhost:58905"
}
}
}

View file

@ -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<IJsonRpcMessage> _incomingChannel = CreateSingleItemChannel<IJsonRpcMessage>();
private readonly Channel<SseItem<IJsonRpcMessage?>> _outgoingSseChannel = CreateSingleItemChannel<SseItem<IJsonRpcMessage?>>();
private Task? _sseWriteTask;
private Utf8JsonWriter? _jsonWriter;
public bool IsConnected => _sseWriteTask?.IsCompleted == false;
public Task RunAsync(CancellationToken cancellationToken)
{
void WriteJsonRpcMessageToBuffer(SseItem<IJsonRpcMessage?> item, IBufferWriter<byte> 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<IJsonRpcMessage?>(null, "endpoint"));
var sseItems = _outgoingSseChannel.Reader.ReadAllAsync(cancellationToken);
return _sseWriteTask = SseFormatter.WriteAsync(sseItems, sseResponseStream, WriteJsonRpcMessageToBuffer, cancellationToken);
}
public ChannelReader<IJsonRpcMessage> 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<IJsonRpcMessage?>(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<T> CreateSingleItemChannel<T>() =>
Channel.CreateBounded<T>(new BoundedChannelOptions(1)
{
SingleReader = true,
SingleWriter = false,
});
private Utf8JsonWriter GetUtf8JsonWriter(IBufferWriter<byte> writer)
{
if (_jsonWriter is null)
{
_jsonWriter = new Utf8JsonWriter(writer);
}
else
{
_jsonWriter.Reset(writer);
}
return _jsonWriter;
}
}

View file

@ -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!";
}
}

View file

@ -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;
}
}

View file

@ -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\" }";
}
}

View file

@ -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<bool> 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<bool> 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;
// }
//}

View file

@ -46,7 +46,7 @@ public class GetPizzaTypesFn : IFunctionCallback
}).ToArray()
}
};
return true;
}
}

View file

@ -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<bool> 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<bool> 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;
// }
//}

View file

@ -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<bool> Execute(RoleDialogModel message)
{
message.Content = "The order number is P123-01";
var state = _service.GetRequiredService<IConversationStateService>();
state.SetState("order_number", "P123-01");
// public async Task<bool> Execute(RoleDialogModel message)
// {
// message.Content = "The order number is P123-01";
// var state = _service.GetRequiredService<IConversationStateService>();
// state.SetState("order_number", "P123-01");
return true;
}
}
// return true;
// }
//}

View file

@ -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<IAgentService>();
var state = _services.GetRequiredService<IConversationStateService>();
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);
}
}

View file

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

View file

@ -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" ]
}

View file

@ -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" ]
}
}
//{
// "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" ]
// }
//}

View file

@ -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" ]
}
}
//{
// "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" ]
// }
//}

View file

@ -7,6 +7,17 @@
"disabled": false,
"isPublic": true,
"profiles": [ "pizza" ],
"mcptools": [
{
"serverid": "PizzaServer",
"disabled": false,
"functions": [
{
"Name": "make_payment"
}
]
}
],
"labels": [ "experiment" ],
"routingRules": [
{

View file

@ -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" ]
}
}
//{
// "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" ]
// }
//}