diff --git a/BotSharp.sln b/BotSharp.sln index 1e8ac13d..92feb007 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -133,6 +133,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.PizzaBot.MCPServer 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}") = "mcpdotnet", "..\mcpdotnet\src\mcpdotnet\mcpdotnet.csproj", "{32D455C9-C7CF-928B-F878-8EAF0198328E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -549,6 +551,14 @@ Global {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 + {32D455C9-C7CF-928B-F878-8EAF0198328E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32D455C9-C7CF-928B-F878-8EAF0198328E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32D455C9-C7CF-928B-F878-8EAF0198328E}.Debug|x64.ActiveCfg = Debug|Any CPU + {32D455C9-C7CF-928B-F878-8EAF0198328E}.Debug|x64.Build.0 = Debug|Any CPU + {32D455C9-C7CF-928B-F878-8EAF0198328E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32D455C9-C7CF-928B-F878-8EAF0198328E}.Release|Any CPU.Build.0 = Release|Any CPU + {32D455C9-C7CF-928B-F878-8EAF0198328E}.Release|x64.ActiveCfg = Release|Any CPU + {32D455C9-C7CF-928B-F878-8EAF0198328E}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -613,6 +623,7 @@ Global {684781D5-3DD4-6A0B-B53F-0A362CD6BB0C} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {8D2AD45F-836A-516F-DE6A-71443CEBB18A} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {781F1465-365C-0F22-1775-25025DAFA4C7} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} + {32D455C9-C7CF-928B-F878-8EAF0198328E} = {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 80231ce1..c32e26ac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,7 +1,7 @@ 8.0.0 - 2.3.0 + 2.3.0 true @@ -15,10 +15,11 @@ - + + @@ -131,7 +132,6 @@ - diff --git a/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj b/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj index 037ed776..2980894f 100644 --- a/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj +++ b/src/Infrastructure/BotSharp.MCP/BotSharp.MCP.csproj @@ -9,10 +9,11 @@ - + + diff --git a/src/Infrastructure/BotSharp.MCP/Functions/McpToolFunction.cs b/src/Infrastructure/BotSharp.MCP/Functions/McpToolFunction.cs index 0e755df5..07397db4 100644 --- a/src/Infrastructure/BotSharp.MCP/Functions/McpToolFunction.cs +++ b/src/Infrastructure/BotSharp.MCP/Functions/McpToolFunction.cs @@ -39,7 +39,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,8 +50,8 @@ public class McpToolFunction : IFunctionCallback var json = string.Join("\n", result.Content .Where(c => c.Type == "text") .Select(c => c.Text)); - message.Content = json.JsonContent(); - + message.Content = json; + message.Data = json.JsonContent(); return true; } diff --git a/src/Infrastructure/BotSharp.MCP/Hooks/MCPResponseHook.cs b/src/Infrastructure/BotSharp.MCP/Hooks/MCPResponseHook.cs index 0b939f2e..9658eae6 100644 --- a/src/Infrastructure/BotSharp.MCP/Hooks/MCPResponseHook.cs +++ b/src/Infrastructure/BotSharp.MCP/Hooks/MCPResponseHook.cs @@ -30,10 +30,12 @@ public class MCPResponseHook : ConversationHookBase 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))) { - message.Data = JsonSerializer.Deserialize(message.Content, typeof(object)); + 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..e4cc3e58 100644 --- a/src/Infrastructure/BotSharp.MCP/Hooks/MCPToolAgentHook.cs +++ b/src/Infrastructure/BotSharp.MCP/Hooks/MCPToolAgentHook.cs @@ -6,6 +6,7 @@ using BotSharp.Abstraction.Conversations; using BotSharp.Abstraction.Functions.Models; using BotSharp.Core.Mcp; using BotSharp.Core.MCP; +using McpDotNet.Client; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; @@ -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..7e833503 100644 --- a/src/Infrastructure/BotSharp.MCP/MCPClientManager.cs +++ b/src/Infrastructure/BotSharp.MCP/MCPClientManager.cs @@ -2,28 +2,27 @@ using BotSharp.Core.Mcp.Settings; using McpDotNet.Client; using Microsoft.Extensions.Logging; 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..d161984a 100644 --- a/src/Infrastructure/BotSharp.MCP/McpPlugin.cs +++ b/src/Infrastructure/BotSharp.MCP/McpPlugin.cs @@ -5,11 +5,14 @@ using BotSharp.Abstraction.Plugins; using BotSharp.Core.Mcp.Functions; using BotSharp.Core.Mcp.Settings; using BotSharp.MCP.Hooks; +using McpDotNet.Client; using McpDotNet.Protocol.Types; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using System.Linq; +using System.Threading.Tasks; namespace BotSharp.Core.Mcp; @@ -31,20 +34,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, McpDotNet.Configuration.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/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 2582a8ae..a0e88d0d 100644 --- a/src/WebStarter/appsettings.json +++ b/src/WebStarter/appsettings.json @@ -46,11 +46,15 @@ "Provider": "azure-openai", "Models": [ { - "Id": "gpt-3.5-turbo", - "Name": "gpt-35-turbo", - "Version": "1106", - "ApiKey": "", - "Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/" + "Id": "gpt-4o-mini", + "Name": "gpt-4o-mini", + "ApiKey": "c1cee6ddfb3d4f469587789efe7a2ba9", + "Endpoint": "https://ai4c-demo.openai.azure.com/", + "Type": "chat", + "PromptCost": 0.0015, + "CompletionCost": 0.002, + "MaxTokens": null, + "Temperature": 1.0 }, { "Name": "gpt-35-turbo-instruct", diff --git a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj index 03b092b1..5d0ed45d 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj +++ b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj @@ -8,10 +8,12 @@ enable - - - - - + + + + + + + diff --git a/tests/BotSharp.PizzaBot.MCPServer/Program.cs b/tests/BotSharp.PizzaBot.MCPServer/Program.cs index b2477c99..534054b2 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/Program.cs +++ b/tests/BotSharp.PizzaBot.MCPServer/Program.cs @@ -12,6 +12,9 @@ namespace BotSharp.PizzaBot.MCPServer { internal class Program { + private static HashSet _subscribedResources = new(); + private static readonly object _subscribedResourcesLock = new(); + private static async Task Main(string[] args) { Console.WriteLine("Starting server..."); @@ -21,34 +24,48 @@ namespace BotSharp.PizzaBot.MCPServer ServerInfo = new Implementation() { Name = "PizzaServer", Version = "1.0.0" }, Capabilities = new ServerCapabilities() { - //Tools = ConfigureTools(), + Tools = ConfigureTools(), }, ProtocolVersion = "2024-11-05", ServerInstructions = "This is a test server with only stub functionality" }; var loggerFactory = CreateLoggerFactory(); - McpServerFactory factory = new McpServerFactory(new StdioServerTransport("PizzaServer", loggerFactory), options, loggerFactory); - IMcpServer server = factory.CreateServer(); - ConfigureTools(server); - - Console.WriteLine("Server object created, registering handlers."); - - Console.WriteLine("Server initialized."); + await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("TestServer", loggerFactory), options, loggerFactory); + Log.Logger.Information("Server initialized."); await server.StartAsync(); - Console.WriteLine("Server started."); + Log.Logger.Information("Server started."); // Run until process is stopped by the client (parent process) while (true) { - await Task.Delay(1000); + await Task.Delay(5000); + + // Snapshot the subscribed resources, rather than locking while sending notifications + List resources; + lock (_subscribedResourcesLock) + { + resources = _subscribedResources.ToList(); + } + + foreach (var resource in resources) + { + ResourceUpdatedNotificationParams notificationParams = new() { Uri = resource }; + await server.SendMessageAsync(new JsonRpcNotification() + { + Method = NotificationMethods.ResourceUpdatedNotification, + Params = notificationParams + }); + } } } - private static void ConfigureTools(IMcpServer server) + private static ToolsCapability ConfigureTools() { - server.ListToolsHandler = (request, cancellationToken) => + return new() + { + ListToolsHandler = (request, cancellationToken) => { return Task.FromResult(new ListToolsResult() { @@ -71,7 +88,7 @@ namespace BotSharp.PizzaBot.MCPServer new Tool() { Name = "get_pizza_prices", - Description = "call this function to get pizza prices", + Description = "call this function to get pizza unit price", InputSchema = new JsonSchema() { Type = "object", @@ -95,7 +112,7 @@ namespace BotSharp.PizzaBot.MCPServer { ["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" }, + ["unit_price"] = new JsonSchemaProperty() { Type = "number", Description = "pizza unit price" }, }, Required = new List(){"pizza_type", "quantity", "unit_price" } @@ -103,86 +120,98 @@ namespace BotSharp.PizzaBot.MCPServer } ] }); - }; + }, - server.CallToolHandler = async (request, cancellationToken) => - { - if (request.Params.Name == "make_payment") + CallToolHandler = async (request, cancellationToken) => { - if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("order_number", out var order_number)) + if (request.Params.Name == "make_payment") { - 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"; + 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); + //// Serialize the message to JSON + //var jso = new JsonSerializerOptions() { WriteIndented = true }; + //var jsonMessage = JsonSerializer.Serialize(message, jso); - return new CallToolResponse() + 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") { - Content = [new Content() { Text = jsonMessage, Type = "text" }] - }; + 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}"); + } } - 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'"); - } - 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.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 = jsonMessage, Type = "text" }] - }; - } - else - { - throw new McpServerException($"Unknown tool: {request.Params.Name}"); - } - }; + }; } + private static ILoggerFactory CreateLoggerFactory() { 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 9699d5ab..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",