diff --git a/BotSharp.sln b/BotSharp.sln index c6c75391..53eb3da9 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -127,9 +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.MCP", "src\Infrastructure\BotSharp.MCP\BotSharp.MCP.csproj", "{8ED8EEF4-06DD-45F5-AA91-BD2395E901B5}" +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.PizzaBot.MCPServer", "tests\BotSharp.PizzaBot.MCPServer\BotSharp.PizzaBot.MCPServer.csproj", "{AD91B4ED-0623-4710-913E-6D7C893E76EF}" +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 @@ -523,6 +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 + {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 @@ -584,6 +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} + {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 f49939eb..c56bade3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,7 @@ + @@ -13,13 +14,16 @@ + + + @@ -106,6 +110,8 @@ + + diff --git a/src/BotSharp.AppHost/BotSharp.AppHost.csproj b/src/BotSharp.AppHost/BotSharp.AppHost.csproj index 68673f7d..26ff84a8 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 9c371bf4..4c54ed11 100644 --- a/src/BotSharp.AppHost/Program.cs +++ b/src/BotSharp.AppHost/Program.cs @@ -2,6 +2,8 @@ var builder = DistributedApplication.CreateBuilder(args); 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/Enums/AgentField.cs b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs index 413f9eb8..2ecddf0c 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Agents/Enums/AgentField.cs @@ -19,7 +19,7 @@ public enum AgentField Sample, LlmConfig, Utility, - Acp, + McpTool, KnowledgeBase, Rule, MaxMessageCount diff --git a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs index 7e9da19c..dd8743d4 100644 --- a/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs +++ b/src/Infrastructure/BotSharp.Core/Agents/Services/AgentService.UpdateAgent.cs @@ -197,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); diff --git a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs index ae401508..4da61828 100644 --- a/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs +++ b/src/Infrastructure/BotSharp.Core/Repository/FileRepository/FileRepository.Agent.cs @@ -60,7 +60,7 @@ namespace BotSharp.Core.Repository case AgentField.Utility: UpdateAgentUtilities(agent.Id, agent.MergeUtility, agent.Utilities); break; - case AgentField.Acp: + case AgentField.McpTool: UpdateAgentMcpTools(agent.Id, agent.McpTools); break; case AgentField.KnowledgeBase: diff --git a/src/Infrastructure/BotSharp.MCP/AIFunctionUtilities.cs b/src/Infrastructure/BotSharp.MCP/AIFunctionUtilities.cs index e86cbf92..8a409f67 100644 --- a/src/Infrastructure/BotSharp.MCP/AIFunctionUtilities.cs +++ b/src/Infrastructure/BotSharp.MCP/AIFunctionUtilities.cs @@ -1,9 +1,8 @@ using BotSharp.Abstraction.Functions.Models; -using McpDotNet.Protocol.Types; +using ModelContextProtocol.Protocol.Types; using System; using System.Collections.Generic; using System.Text.Json; -using System.Text.Json.Serialization; namespace BotSharp.Core.MCP; @@ -16,19 +15,19 @@ internal static class AIFunctionUtilities throw new ArgumentNullException(nameof(tool)); } - var properties = tool.InputSchema?.Properties; - var required = tool.InputSchema?.Required ?? new List(); + var properties = tool.InputSchema.GetProperty("properties"); + var required = tool.InputSchema.GetProperty("required"); FunctionDef funDef = new FunctionDef { Name = tool.Name, - Description = tool.Description?? string.Empty, + Description = tool.Description ?? string.Empty, Type = "function", Parameters = new FunctionParametersDef { Type = "object", - Properties = properties != null ? JsonSerializer.SerializeToDocument(properties) : JsonDocument.Parse("{}"), - Required = required + Properties = JsonDocument.Parse(properties.GetRawText()), + Required = JsonSerializer.Deserialize>(required.GetRawText()) } }; diff --git a/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj b/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj index e9727f7f..94aba37c 100644 --- a/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj +++ b/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj @@ -9,7 +9,8 @@ - + + diff --git a/src/Infrastructure/BotSharp.MCP/Functions/McpToolFunction.cs b/src/Infrastructure/BotSharp.MCP/Functions/McpToolFunction.cs index 97ec724f..1f6f2c60 100644 --- a/src/Infrastructure/BotSharp.MCP/Functions/McpToolFunction.cs +++ b/src/Infrastructure/BotSharp.MCP/Functions/McpToolFunction.cs @@ -1,11 +1,10 @@ using BotSharp.Abstraction.Agents; -using BotSharp.Abstraction.Agents.Models; using BotSharp.Abstraction.Conversations.Models; using BotSharp.Abstraction.Functions; -using McpDotNet.Client; -using McpDotNet.Protocol.Types; +using BotSharp.Abstraction.Utilities; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol.Types; using System; using System.Collections.Generic; using System.Linq; @@ -38,7 +37,7 @@ public class McpToolFunction : IFunctionCallback 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.Factory.GetClientAsync(serverId); + var client = await _clientManager.GetMcpClientAsync(serverId); // Call the tool through mcpdotnet var result = await client.CallToolAsync( _tool.Name, @@ -50,7 +49,7 @@ public class McpToolFunction : IFunctionCallback .Where(c => c.Type == "text") .Select(c => c.Text)); message.Content = json; - message.Data = JsonSerializer.Deserialize(json,typeof(object)); + message.Data = json.JsonContent(); return true; } diff --git a/src/Infrastructure/BotSharp.MCP/Hooks/MCPResponseHook.cs b/src/Infrastructure/BotSharp.MCP/Hooks/MCPResponseHook.cs new file mode 100644 index 00000000..c5470582 --- /dev/null +++ b/src/Infrastructure/BotSharp.MCP/Hooks/MCPResponseHook.cs @@ -0,0 +1,36 @@ +using BotSharp.Abstraction.Agents; +using BotSharp.Abstraction.Conversations; +using BotSharp.Abstraction.Conversations.Models; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; + +namespace BotSharp.MCP.Hooks; + +public class MCPResponseHook : ConversationHookBase +{ + private readonly IServiceProvider _services; + private readonly IConversationStateService _states; + + public MCPResponseHook(IServiceProvider services, + IConversationStateService states) + { + _services = services; + _states = states; + } + 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/src/Infrastructure/BotSharp.MCP/Hooks/MCPToolAgentHook.cs b/src/Infrastructure/BotSharp.MCP/Hooks/MCPToolAgentHook.cs index 32b10d0a..e56ab68e 100644 --- a/src/Infrastructure/BotSharp.MCP/Hooks/MCPToolAgentHook.cs +++ b/src/Infrastructure/BotSharp.MCP/Hooks/MCPToolAgentHook.cs @@ -7,6 +7,7 @@ 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; @@ -51,12 +52,12 @@ public class MCPToolAgentHook : AgentHookBase var mcps = agent.McpTools; foreach (var item in mcps) { - var mcpClient = await mcpClientManager.Factory.GetClientAsync(item.ServerId); + var mcpClient = await mcpClientManager.GetMcpClientAsync(item.ServerId); if (mcpClient != null) { - var tools = await mcpClient.ListToolsAsync(); + var tools = await mcpClient.ListToolsAsync().ToListAsync(); var funcnames = item.Functions.Select(x => x.Name).ToList(); - foreach (var tool in tools.Tools.Where(x => funcnames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) + foreach (var tool in tools.Where(x => funcnames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) { var funDef = AIFunctionUtilities.MapToFunctionDef(tool); functionDefs.Add(funDef); diff --git a/src/Infrastructure/BotSharp.MCP/MCPClientManager.cs b/src/Infrastructure/BotSharp.MCP/MCPClientManager.cs index bbe70205..78d94a1a 100644 --- a/src/Infrastructure/BotSharp.MCP/MCPClientManager.cs +++ b/src/Infrastructure/BotSharp.MCP/MCPClientManager.cs @@ -1,29 +1,28 @@ using BotSharp.Core.Mcp.Settings; -using McpDotNet.Client; using Microsoft.Extensions.Logging; +using ModelContextProtocol.Client; using System; +using System.Linq; +using System.Threading.Tasks; namespace BotSharp.Core.Mcp; public class MCPClientManager : IDisposable { public ILoggerFactory LoggerFactory { get; } - public McpClientFactory Factory { get; } - - + private readonly MCPSettings mcpSettings; public MCPClientManager(MCPSettings settings, ILoggerFactory loggerFactory) { mcpSettings = settings; LoggerFactory = loggerFactory; + } - // Inject the mock transport into the factory - Factory = new McpClientFactory( - settings.McpServerConfigs, - settings.McpClientOptions, - LoggerFactory - ); + public 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 index 3d8ec0c8..1780ff56 100644 --- a/src/Infrastructure/BotSharp.MCP/McpPlugin.cs +++ b/src/Infrastructure/BotSharp.MCP/McpPlugin.cs @@ -5,11 +5,13 @@ using BotSharp.Abstraction.Plugins; using BotSharp.Core.Mcp.Functions; using BotSharp.Core.Mcp.Settings; using BotSharp.MCP.Hooks; -using McpDotNet.Protocol.Types; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using ModelContextProtocol.Client; +using ModelContextProtocol.Configuration; +using System.Linq; +using System.Threading.Tasks; namespace BotSharp.Core.Mcp; @@ -31,20 +33,30 @@ public class McpPlugin : IBotSharpPlugin foreach (var server in settings.McpServerConfigs) { - var client = clientManager.Factory.GetClientAsync(server.Id).Result; - var tools = client.ListToolsAsync().Result; - - foreach (var tool in tools.Tools) - { - services.AddScoped( provider => { return tool; }); - - services.AddScoped( provider => { - var funcTool = new McpToolFunction( provider, tool, clientManager); - return funcTool; - }); - } + RegisterFunctionCall(services, server) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); } // Register hooks services.AddScoped(); + 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 McpToolFunction(provider, tool, clientManager); + return funcTool; + }); + } + } +} diff --git a/src/Infrastructure/BotSharp.MCP/Settings/MCPSettings.cs b/src/Infrastructure/BotSharp.MCP/Settings/MCPSettings.cs index 34e408e3..1279a08d 100644 --- a/src/Infrastructure/BotSharp.MCP/Settings/MCPSettings.cs +++ b/src/Infrastructure/BotSharp.MCP/Settings/MCPSettings.cs @@ -1,5 +1,5 @@ -using McpDotNet.Client; -using McpDotNet.Configuration; +using ModelContextProtocol.Client; +using ModelContextProtocol.Configuration; using System.Collections.Generic; namespace BotSharp.Core.Mcp.Settings; diff --git a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentMCPToolMongoElement.cs b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentMCPToolMongoElement.cs index ecad71f3..614683c3 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentMCPToolMongoElement.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Models/AgentMCPToolMongoElement.cs @@ -1,9 +1,4 @@ using BotSharp.Abstraction.Agents.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace BotSharp.Plugin.MongoStorage.Models; @@ -16,7 +11,6 @@ public class AgentMCPToolMongoElement public bool Disabled { get; set; } public List Functions { get; set; } = []; - public List Templates { get; set; } = []; public static AgentMCPToolMongoElement ToMongoElement(MCPTool utility) { @@ -51,19 +45,3 @@ public class McpFunctionMongoElement Name = name; } } - -public class McpTemplateMongoElement -{ - public string Name { get; set; } - - public McpTemplateMongoElement() - { - - } - - public McpTemplateMongoElement(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 0d3a670b..ba4b3fde 100644 --- a/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs +++ b/src/Plugins/BotSharp.Plugin.MongoStorage/Repository/MongoRepository.Agent.cs @@ -61,8 +61,8 @@ public partial class MongoRepository case AgentField.Utility: UpdateAgentUtilities(agent.Id, agent.MergeUtility, agent.Utilities); break; - case AgentField.Acp: - UpdateAgentMcps(agent.Id, agent.McpTools); + case AgentField.McpTool: + UpdateAgentMcpTools(agent.Id, agent.McpTools); break; case AgentField.KnowledgeBase: UpdateAgentKnowledgeBases(agent.Id, agent.KnowledgeBases); @@ -264,7 +264,7 @@ public partial class MongoRepository _dc.Agents.UpdateOne(filter, update); } - private void UpdateAgentMcps(string agentId, List mcps) + private void UpdateAgentMcpTools(string agentId, List mcps) { if (mcps == null) return; diff --git a/src/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index 9cff5c73..c2f3f0ed 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -28,7 +28,6 @@ - diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 3c2cd567..37952b50 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -182,11 +182,9 @@ { "Id": "PizzaServer", "Name": "PizzaServer", - "TransportType": "stdio", - "TransportOptions": { - "command": "BotSharp.PizzaBot.MCPServer.exe", - "workingDirectory": "E:\\GitHub\\BotSharp\\src\\WebStarter\\bin\\Debug\\net8.0" - } + "TransportType": "sse", + "TransportOptions": [], + "Location": "http://localhost:58905/sse" } ] }, diff --git a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj index 1158e06a..319f10a1 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj +++ b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj @@ -1,16 +1,16 @@ - + - Exe 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 00000000..f2138444 --- /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 index 5e8633f8..fb39e6cd 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/Program.cs +++ b/tests/BotSharp.PizzaBot.MCPServer/Program.cs @@ -1,204 +1,236 @@ -using McpDotNet.Protocol.Transport; -using McpDotNet.Protocol.Types; -using McpDotNet.Server; -using Microsoft.Extensions.Logging; -using Serilog; -using System.Dynamic; -using System.Text; -using System.Text.Json; +using BotSharp.PizzaBot.MCPServer; +using ModelContextProtocol; -namespace BotSharp.PizzaBot.MCPServer -{ - internal class Program - { - private static async Task Main(string[] args) - { - Console.WriteLine("Starting server..."); +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddMcpServer().WithTools(); +var app = builder.Build(); - McpServerOptions options = new McpServerOptions() - { - ServerInfo = new Implementation() { Name = "PizzaServer", Version = "1.0.0" }, - Capabilities = new ServerCapabilities() - { - Tools = new(), - Resources = new(), - Prompts = new(), - }, - ProtocolVersion = "2024-11-05" - }; - var loggerFactory = CreateLoggerFactory(); - McpServerFactory factory = new McpServerFactory(new StdioServerTransport("PizzaServer", loggerFactory), options, loggerFactory, - "This is a test server with only stub functionality"); - IMcpServer server = factory.CreateServer(); +app.MapGet("/", () => "This is a test server with only stub functionality!"); +app.MapMcpSse(); - Console.WriteLine("Server object created, registering handlers."); +app.Run(); - #region Tools - server.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 prices", - 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 = "unit price" }, +//namespace BotSharp.PizzaBot.MCPServer +//{ +// internal class Program +// { +// private static HashSet _subscribedResources = new(); +// private static readonly object _subscribedResourcesLock = new(); - }, - Required = new List(){"pizza_type", "quantity", "unit_price" } - } - } - ] - }); - }; +// private static async Task Main(string[] args) +// { +// Console.WriteLine("Starting server..."); - server.CallToolHandler = async (request, cancellationToken) => - { - if (request.Name == "make_payment") - { - if (request.Arguments is null || !request.Arguments.TryGetValue("order_number", out var order_number)) - { - throw new McpServerException("Missing required argument 'order_number'"); - } - if (request.Arguments is null || !request.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"; +// 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."); - // Serialize the message to JSON - var jso = new JsonSerializerOptions() { WriteIndented = true }; - var jsonMessage = JsonSerializer.Serialize(message, jso); +// await server.StartAsync(); - return new CallToolResponse() - { - Content = [new Content() { Text = jsonMessage , Type = "text" }] - }; - } - else if(request.Name == "get_pizza_prices") - { - if (request.Arguments is null || !request.Arguments.TryGetValue("pizza_type", out var pizza_type)) - { - throw new McpServerException("Missing required argument 'pizza_type'"); - } - if (request.Arguments is null || !request.Arguments.TryGetValue("quantity", out var quantity)) - { - throw new McpServerException("Missing required argument 'quantity'"); - } - dynamic message = new ExpandoObject(); - message.pepperoni_unit_price = 3.2; - message.cheese_unit_price = 3.5; - message.margherita_unit_price = 3.8; - // Serialize the message to JSON - 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.Name == "place_an_order") - { - if (request.Arguments is null || !request.Arguments.TryGetValue("pizza_type", out var pizza_type)) - { - throw new McpServerException("Missing required argument 'pizza_type'"); - } - if (request.Arguments is null || !request.Arguments.TryGetValue("quantity", out var quantity)) - { - throw new McpServerException("Missing required argument 'quantity'"); - } - if (request.Arguments is null || !request.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 = jsonMessage, Type = "text" }] - }; - } - else - { - throw new McpServerException($"Unknown tool: {request.Name}"); - } - }; - #endregion +// Log.Logger.Information("Server started."); - Console.WriteLine("Server initialized."); +// // Run until process is stopped by the client (parent process) +// while (true) +// { +// await Task.Delay(5000); - await server.StartAsync(); +// // Snapshot the subscribed resources, rather than locking while sending notifications +// List resources; +// lock (_subscribedResourcesLock) +// { +// resources = _subscribedResources.ToList(); +// } - Console.WriteLine("Server started."); +// foreach (var resource in resources) +// { +// ResourceUpdatedNotificationParams notificationParams = new() { Uri = resource }; +// await server.SendMessageAsync(new JsonRpcNotification() +// { +// Method = NotificationMethods.ResourceUpdatedNotification, +// Params = notificationParams +// }); +// } +// } +// } - // Run until process is stopped by the client (parent process) - while (true) - { - await Task.Delay(1000); - } - } +// 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." }, - 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(); +// }, +// 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" }, - var logsPath = Path.Combine(AppContext.BaseDirectory, "testserver.log"); - return LoggerFactory.Create(builder => - { - builder.AddSerilog(); - }); - } - } -} +// }, +// 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 00000000..617e672c --- /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 00000000..7a7ce51a --- /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 00000000..a236d874 --- /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 00000000..7ca766f3 --- /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 00000000..a6237b6d --- /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/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json b/tests/BotSharp.Plugin.PizzaBot/data/agents/c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd/agent.json index 7ec18d46..2e40c4e0 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,6 +1,6 @@ { "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", @@ -20,6 +20,6 @@ } ] } - ] + ], "labels": [ "experiment" ] } \ No newline at end of file