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