diff --git a/BotSharp.sln b/BotSharp.sln index 92feb007..17352903 100644 --- a/BotSharp.sln +++ b/BotSharp.sln @@ -129,12 +129,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.DeepSeekAI" EndProject 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", "{8D2AD45F-836A-516F-DE6A-71443CEBB18A}" -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 +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 Debug|Any CPU = Debug|Any CPU @@ -535,14 +535,6 @@ Global {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 - {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 {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 @@ -559,6 +551,14 @@ Global {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 + {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 @@ -621,9 +621,9 @@ Global {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} - {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} + {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 c32e26ac..d5b26f7e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -111,6 +111,7 @@ + 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/WebStarter/WebStarter.csproj b/src/WebStarter/WebStarter.csproj index d06fa30a..b1b461d5 100644 --- a/src/WebStarter/WebStarter.csproj +++ b/src/WebStarter/WebStarter.csproj @@ -27,7 +27,6 @@ - diff --git a/src/WebStarter/appsettings.json b/src/WebStarter/appsettings.json index 2582a8ae..15896cd6 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 5d0ed45d..880099e5 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj +++ b/tests/BotSharp.PizzaBot.MCPServer/BotSharp.PizzaBot.MCPServer.csproj @@ -1,7 +1,6 @@ - + - Exe net8.0 enable 12.0 @@ -12,6 +11,7 @@ + diff --git a/tests/BotSharp.PizzaBot.MCPServer/McpEndpointRouteBuilderExtensions.cs b/tests/BotSharp.PizzaBot.MCPServer/McpEndpointRouteBuilderExtensions.cs new file mode 100644 index 00000000..25553962 --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/McpEndpointRouteBuilderExtensions.cs @@ -0,0 +1,62 @@ +using McpDotNet.Protocol.Messages; +using McpDotNet.Server; +using McpDotNet.Utils.Json; +using Microsoft.Extensions.Options; + +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(JsonSerializerOptionsExtensions.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 534054b2..c82745a6 100644 --- a/tests/BotSharp.PizzaBot.MCPServer/Program.cs +++ b/tests/BotSharp.PizzaBot.MCPServer/Program.cs @@ -1,233 +1,236 @@ -using McpDotNet.Protocol.Messages; -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 McpDotNet; -namespace BotSharp.PizzaBot.MCPServer -{ - internal class Program - { - private static HashSet _subscribedResources = new(); - private static readonly object _subscribedResourcesLock = new(); +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddMcpServer().WithTools(); +var app = builder.Build(); - private static async Task Main(string[] args) - { - Console.WriteLine("Starting server..."); +app.MapGet("/", () => "This is a test server with only stub functionality!"); +app.MapMcpSse(); - 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."); +app.Run(); - await server.StartAsync(); - Log.Logger.Information("Server started."); +//namespace BotSharp.PizzaBot.MCPServer +//{ +// internal class Program +// { +// private static HashSet _subscribedResources = new(); +// private static readonly object _subscribedResourcesLock = new(); - // Run until process is stopped by the client (parent process) - while (true) - { - await Task.Delay(5000); +// private static async Task Main(string[] args) +// { +// Console.WriteLine("Starting server..."); - // Snapshot the subscribed resources, rather than locking while sending notifications - List resources; - lock (_subscribedResourcesLock) - { - resources = _subscribedResources.ToList(); - } +// 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."); - foreach (var resource in resources) - { - ResourceUpdatedNotificationParams notificationParams = new() { Uri = resource }; - await server.SendMessageAsync(new JsonRpcNotification() - { - Method = NotificationMethods.ResourceUpdatedNotification, - Params = notificationParams - }); - } - } - } +// await server.StartAsync(); - 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." }, +// Log.Logger.Information("Server started."); - }, - 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" }, +// // Run until process is stopped by the client (parent process) +// while (true) +// { +// await Task.Delay(5000); - }, - Required = new List(){"pizza_type", "quantity", "unit_price" } - } - } - ] - }); - }, +// // Snapshot the subscribed resources, rather than locking while sending notifications +// List resources; +// lock (_subscribedResourcesLock) +// { +// resources = _subscribedResources.ToList(); +// } - 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"; +// foreach (var resource in resources) +// { +// ResourceUpdatedNotificationParams notificationParams = new() { Uri = resource }; +// await server.SendMessageAsync(new JsonRpcNotification() +// { +// Method = NotificationMethods.ResourceUpdatedNotification, +// Params = notificationParams +// }); +// } +// } +// } - //// Serialize the message to JSON - //var jso = new JsonSerializerOptions() { WriteIndented = true }; - //var jsonMessage = JsonSerializer.Serialize(message, jso); +// 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." }, - 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}"); - } - } - }; - } +// }, +// Required = new List(){ "pizza_type", "quantity" } +// } +// }, +// new Tool() +// { +// Name = "place_an_order", +// Description = "Place an order when user has confirmed the pizza type and quantity.", +// InputSchema = new JsonSchema() +// { +// Type = "object", +// Properties = new Dictionary() +// { +// ["pizza_type"] = new JsonSchemaProperty() { Type = "string", Description = "The pizza type." }, +// ["quantity"] = new JsonSchemaProperty() { Type = "number", Description = "quantity of pizza." }, +// ["unit_price"] = new JsonSchemaProperty() { Type = "number", Description = "pizza unit price" }, + +// }, +// Required = new List(){"pizza_type", "quantity", "unit_price" } +// } +// } +// ] +// }); +// }, + +// CallToolHandler = async (request, cancellationToken) => +// { +// if (request.Params.Name == "make_payment") +// { +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("order_number", out var order_number)) +// { +// throw new McpServerException("Missing required argument 'order_number'"); +// } +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("total_amount", out var total_amount)) +// { +// throw new McpServerException("Missing required argument 'total_amount'"); +// } +// //dynamic message = new ExpandoObject(); +// //message.Transaction = Guid.NewGuid().ToString(); +// //message.Status = "Success"; + +// //// Serialize the message to JSON +// //var jso = new JsonSerializerOptions() { WriteIndented = true }; +// //var jsonMessage = JsonSerializer.Serialize(message, jso); + +// return new CallToolResponse() +// { +// Content = [new Content() { Text = "Payment proceed successfully. Thank you for your business. Have a great day!", Type = "text" }] +// }; +// } +// else if (request.Params.Name == "get_pizza_prices") +// { +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("pizza_type", out var pizza_type)) +// { +// throw new McpServerException("Missing required argument 'pizza_type'"); +// } +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("quantity", out var quantity)) +// { +// throw new McpServerException("Missing required argument 'quantity'"); +// } +// double unit_price = 0; +// if(pizza_type.ToString() == "Pepperoni Pizza") +// { +// unit_price = 3.2 * (int)quantity; +// } +// else if(pizza_type.ToString() == "Cheese Pizza") +// { +// unit_price = 3.5 * (int)quantity; ; +// } +// else if(pizza_type.ToString() == "Margherita Pizza") +// { +// unit_price = 3.8 * (int)quantity; ; +// } +// dynamic message = new ExpandoObject(); +// message.unit_price = unit_price; +// var jso = new JsonSerializerOptions() { WriteIndented = true }; +// var jsonMessage = JsonSerializer.Serialize(message, jso); +// return new CallToolResponse() +// { +// Content = [new Content() { Text = jsonMessage, Type = "text" }] +// }; +// } +// else if (request.Params.Name == "place_an_order") +// { +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("pizza_type", out var pizza_type)) +// { +// throw new McpServerException("Missing required argument 'pizza_type'"); +// } +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("quantity", out var quantity)) +// { +// throw new McpServerException("Missing required argument 'quantity'"); +// } +// if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("unit_price", out var unit_price)) +// { +// throw new McpServerException("Missing required argument 'unit_price'"); +// } +// //dynamic message = new ExpandoObject(); +// //message.order_number = "P123-01"; +// //message.Content = "The order number is P123-01"; +// //// Serialize the message to JSON +// //var jso = new JsonSerializerOptions() { WriteIndented = true }; +// //var jsonMessage = JsonSerializer.Serialize(message, jso); +// return new CallToolResponse() +// { +// Content = [new Content() { Text = "The order number is P123-01: {order_number = \"P123-01\" }", Type = "text" }] +// }; +// } +// else +// { +// throw new McpServerException($"Unknown tool: {request.Params.Name}"); +// } +// } +// }; +// } - private static ILoggerFactory CreateLoggerFactory() - { - // Use serilog - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Verbose() // Capture all log levels - .WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "TestServer_.log"), - rollingInterval: RollingInterval.Day, - outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") - .CreateLogger(); +// 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(); - }); - } - } -} +// 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..47e0d09a --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/SseServerStreamTransport.cs @@ -0,0 +1,84 @@ +using McpDotNet.Protocol.Messages; +using McpDotNet.Protocol.Transport; +using McpDotNet.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, JsonSerializerOptionsExtensions.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..c6870aa8 --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/Tools/MakePayment.cs @@ -0,0 +1,25 @@ +using McpDotNet.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..db0892d2 --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/Tools/PizzaPrices.cs @@ -0,0 +1,46 @@ +using McpDotNet.Server; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +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..7b38327a --- /dev/null +++ b/tests/BotSharp.PizzaBot.MCPServer/Tools/PlaceOrder.cs @@ -0,0 +1,31 @@ +using McpDotNet.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\" }"; + } +}