feat: Using MCP's SSE transport

This commit is contained in:
geffzhang 2025-03-19 11:07:31 +08:00
parent c2de647b7a
commit 58e3e7fba7
14 changed files with 499 additions and 235 deletions

View file

@ -129,12 +129,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Plugin.DeepSeekAI"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.MCP", "src\Infrastructure\BotSharp.MCP\BotSharp.MCP.csproj", "{684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.MCP", "src\Infrastructure\BotSharp.MCP\BotSharp.MCP.csproj", "{684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}"
EndProject 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}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.Core.Realtime", "src\Infrastructure\BotSharp.Core.Realtime\BotSharp.Core.Realtime.csproj", "{781F1465-365C-0F22-1775-25025DAFA4C7}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcpdotnet", "..\mcpdotnet\src\mcpdotnet\mcpdotnet.csproj", "{32D455C9-C7CF-928B-F878-8EAF0198328E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcpdotnet", "..\mcpdotnet\src\mcpdotnet\mcpdotnet.csproj", "{32D455C9-C7CF-928B-F878-8EAF0198328E}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|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.ActiveCfg = Release|Any CPU
{684781D5-3DD4-6A0B-B53F-0A362CD6BB0C}.Release|x64.Build.0 = 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.ActiveCfg = Debug|Any CPU
{781F1465-365C-0F22-1775-25025DAFA4C7}.Debug|Any CPU.Build.0 = 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.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|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.ActiveCfg = Release|Any CPU
{32D455C9-C7CF-928B-F878-8EAF0198328E}.Release|x64.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -621,9 +621,9 @@ Global
{AFD64412-4D6A-452E-82A2-79E5D8842E29} = {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} {AF329442-B48E-4B48-A18A-1C869D1BA6F5} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{684781D5-3DD4-6A0B-B53F-0A362CD6BB0C} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749} {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} {781F1465-365C-0F22-1775-25025DAFA4C7} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
{32D455C9-C7CF-928B-F878-8EAF0198328E} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC} {32D455C9-C7CF-928B-F878-8EAF0198328E} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
{8D2AD45F-836A-516F-DE6A-71443CEBB18A} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19} SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}

View file

@ -111,6 +111,7 @@
<PackageVersion Include="MSTest.TestFramework" Version="3.1.1" /> <PackageVersion Include="MSTest.TestFramework" Version="3.1.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="System.Net.ServerSentEvents" Version="10.0.0-preview.1.25080.5" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="BotSharp.Logger" Version="$(BotSharpVersion)" /> <PackageVersion Include="BotSharp.Logger" Version="$(BotSharpVersion)" />

View file

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

View file

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

View file

@ -27,7 +27,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\tests\BotSharp.PizzaBot.MCPServer\BotSharp.PizzaBot.MCPServer.csproj" />
<ProjectReference Include="..\..\tests\BotSharp.Plugin.PizzaBot\BotSharp.Plugin.PizzaBot.csproj" /> <ProjectReference Include="..\..\tests\BotSharp.Plugin.PizzaBot\BotSharp.Plugin.PizzaBot.csproj" />
<ProjectReference Include="..\BotSharp.ServiceDefaults\BotSharp.ServiceDefaults.csproj" /> <ProjectReference Include="..\BotSharp.ServiceDefaults\BotSharp.ServiceDefaults.csproj" />
<ProjectReference Include="..\Infrastructure\BotSharp.MCP\BotSharp.MCP.csproj" /> <ProjectReference Include="..\Infrastructure\BotSharp.MCP\BotSharp.MCP.csproj" />

View file

@ -182,11 +182,9 @@
{ {
"Id": "PizzaServer", "Id": "PizzaServer",
"Name": "PizzaServer", "Name": "PizzaServer",
"TransportType": "stdio", "TransportType": "sse",
"TransportOptions": { "TransportOptions": [],
"command": "BotSharp.PizzaBot.MCPServer.exe", "Location": "http://localhost:58905/sse"
"workingDirectory": "E:\\GitHub\\BotSharp\\src\\WebStarter\\bin\\Debug\\net8.0"
}
} }
] ]
}, },

View file

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<LangVersion>12.0</LangVersion> <LangVersion>12.0</LangVersion>
@ -12,6 +11,7 @@
<PackageReference Include="Microsoft.Extensions.Logging" /> <PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Serilog.Extensions.Logging" /> <PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.File" /> <PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="System.Net.ServerSentEvents" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\mcpdotnet\src\mcpdotnet\mcpdotnet.csproj" /> <ProjectReference Include="..\..\..\mcpdotnet\src\mcpdotnet\mcpdotnet.csproj" />

View file

@ -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<ILoggerFactory>();
var mcpServerOptions = endpoints.ServiceProvider.GetRequiredService<IOptions<McpServerOptions>>();
var routeGroup = endpoints.MapGroup("");
routeGroup.MapGet("/sse", async (HttpResponse response, CancellationToken requestAborted) =>
{
await using var localTransport = transport = new SseServerStreamTransport(response.Body);
await using var localServer = server = McpServerFactory.Create(transport, mcpServerOptions.Value, loggerFactory, endpoints.ServiceProvider);
await localServer.StartAsync(requestAborted);
response.Headers.ContentType = "text/event-stream";
response.Headers.CacheControl = "no-cache";
try
{
await transport.RunAsync(requestAborted);
}
catch (OperationCanceledException) when (requestAborted.IsCancellationRequested)
{
// RequestAborted always triggers when the client disconnects before a complete response body is written,
// but this is how SSE connections are typically closed.
}
});
routeGroup.MapPost("/message", async (HttpContext context) =>
{
if (transport is null)
{
await Results.BadRequest("Connect to the /sse endpoint before sending messages.").ExecuteAsync(context);
return;
}
var message = await context.Request.ReadFromJsonAsync<IJsonRpcMessage>(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;
}
}

View file

@ -1,233 +1,236 @@
using McpDotNet.Protocol.Messages; using BotSharp.PizzaBot.MCPServer;
using McpDotNet.Protocol.Transport; using McpDotNet;
using McpDotNet.Protocol.Types;
using McpDotNet.Server;
using Microsoft.Extensions.Logging;
using Serilog;
using System.Dynamic;
using System.Text;
using System.Text.Json;
namespace BotSharp.PizzaBot.MCPServer var builder = WebApplication.CreateBuilder(args);
{ builder.Services.AddMcpServer().WithTools();
internal class Program var app = builder.Build();
{
private static HashSet<string> _subscribedResources = new();
private static readonly object _subscribedResourcesLock = new();
private static async Task Main(string[] args) app.MapGet("/", () => "This is a test server with only stub functionality!");
{ app.MapMcpSse();
Console.WriteLine("Starting server...");
McpServerOptions options = new McpServerOptions() app.Run();
{
ServerInfo = new Implementation() { Name = "PizzaServer", Version = "1.0.0" },
Capabilities = new ServerCapabilities()
{
Tools = ConfigureTools(),
},
ProtocolVersion = "2024-11-05",
ServerInstructions = "This is a test server with only stub functionality"
};
var loggerFactory = CreateLoggerFactory();
await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("TestServer", loggerFactory), options, loggerFactory);
Log.Logger.Information("Server initialized.");
await server.StartAsync();
Log.Logger.Information("Server started."); //namespace BotSharp.PizzaBot.MCPServer
//{
// internal class Program
// {
// private static HashSet<string> _subscribedResources = new();
// private static readonly object _subscribedResourcesLock = new();
// Run until process is stopped by the client (parent process) // private static async Task Main(string[] args)
while (true) // {
{ // Console.WriteLine("Starting server...");
await Task.Delay(5000);
// Snapshot the subscribed resources, rather than locking while sending notifications // McpServerOptions options = new McpServerOptions()
List<string> resources; // {
lock (_subscribedResourcesLock) // ServerInfo = new Implementation() { Name = "PizzaServer", Version = "1.0.0" },
{ // Capabilities = new ServerCapabilities()
resources = _subscribedResources.ToList(); // {
} // 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) // await server.StartAsync();
{
ResourceUpdatedNotificationParams notificationParams = new() { Uri = resource };
await server.SendMessageAsync(new JsonRpcNotification()
{
Method = NotificationMethods.ResourceUpdatedNotification,
Params = notificationParams
});
}
}
}
private static ToolsCapability ConfigureTools() // Log.Logger.Information("Server started.");
{
return new()
{
ListToolsHandler = (request, cancellationToken) =>
{
return Task.FromResult(new ListToolsResult()
{
Tools = [
new Tool()
{
Name = "make_payment",
Description = "call this function to make payment",
InputSchema = new JsonSchema()
{
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
{
["order_number"] = new JsonSchemaProperty() { Type = "string", Description = "order number." },
["total_amount"] = new JsonSchemaProperty() { Type = "string", Description = "total amount." },
},
Required = new List<string>() { "order_number", "total_amount" }
},
},
new Tool()
{
Name = "get_pizza_prices",
Description = "call this function to get pizza unit price",
InputSchema = new JsonSchema()
{
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
{
["pizza_type"] = new JsonSchemaProperty() { Type = "string", Description = "The pizza type." },
["quantity"] = new JsonSchemaProperty() { Type = "string", Description = "quantity of pizza." },
}, // // Run until process is stopped by the client (parent process)
Required = new List<string>(){ "pizza_type", "quantity" } // while (true)
} // {
}, // await Task.Delay(5000);
new Tool()
{
Name = "place_an_order",
Description = "Place an order when user has confirmed the pizza type and quantity.",
InputSchema = new JsonSchema()
{
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
{
["pizza_type"] = new JsonSchemaProperty() { Type = "string", Description = "The pizza type." },
["quantity"] = new JsonSchemaProperty() { Type = "number", Description = "quantity of pizza." },
["unit_price"] = new JsonSchemaProperty() { Type = "number", Description = "pizza unit price" },
}, // // Snapshot the subscribed resources, rather than locking while sending notifications
Required = new List<string>(){"pizza_type", "quantity", "unit_price" } // List<string> resources;
} // lock (_subscribedResourcesLock)
} // {
] // resources = _subscribedResources.ToList();
}); // }
},
CallToolHandler = async (request, cancellationToken) => // foreach (var resource in resources)
{ // {
if (request.Params.Name == "make_payment") // ResourceUpdatedNotificationParams notificationParams = new() { Uri = resource };
{ // await server.SendMessageAsync(new JsonRpcNotification()
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("order_number", out var order_number)) // {
{ // Method = NotificationMethods.ResourceUpdatedNotification,
throw new McpServerException("Missing required argument 'order_number'"); // Params = notificationParams
} // });
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 // private static ToolsCapability ConfigureTools()
//var jso = new JsonSerializerOptions() { WriteIndented = true }; // {
//var jsonMessage = JsonSerializer.Serialize(message, jso); // return new()
// {
// ListToolsHandler = (request, cancellationToken) =>
// {
// return Task.FromResult(new ListToolsResult()
// {
// Tools = [
// new Tool()
// {
// Name = "make_payment",
// Description = "call this function to make payment",
// InputSchema = new JsonSchema()
// {
// Type = "object",
// Properties = new Dictionary<string, JsonSchemaProperty>()
// {
// ["order_number"] = new JsonSchemaProperty() { Type = "string", Description = "order number." },
// ["total_amount"] = new JsonSchemaProperty() { Type = "string", Description = "total amount." },
// },
// Required = new List<string>() { "order_number", "total_amount" }
// },
// },
// new Tool()
// {
// Name = "get_pizza_prices",
// Description = "call this function to get pizza unit price",
// InputSchema = new JsonSchema()
// {
// Type = "object",
// Properties = new Dictionary<string, JsonSchemaProperty>()
// {
// ["pizza_type"] = new JsonSchemaProperty() { Type = "string", Description = "The pizza type." },
// ["quantity"] = new JsonSchemaProperty() { Type = "string", Description = "quantity of pizza." },
return new CallToolResponse() // },
{ // Required = new List<string>(){ "pizza_type", "quantity" }
Content = [new Content() { Text = "Payment proceed successfully. Thank you for your business. Have a great day!", Type = "text" }] // }
}; // },
} // new Tool()
else if (request.Params.Name == "get_pizza_prices") // {
{ // Name = "place_an_order",
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("pizza_type", out var pizza_type)) // Description = "Place an order when user has confirmed the pizza type and quantity.",
{ // InputSchema = new JsonSchema()
throw new McpServerException("Missing required argument 'pizza_type'"); // {
} // Type = "object",
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("quantity", out var quantity)) // Properties = new Dictionary<string, JsonSchemaProperty>()
{ // {
throw new McpServerException("Missing required argument 'quantity'"); // ["pizza_type"] = new JsonSchemaProperty() { Type = "string", Description = "The pizza type." },
} // ["quantity"] = new JsonSchemaProperty() { Type = "number", Description = "quantity of pizza." },
double unit_price = 0; // ["unit_price"] = new JsonSchemaProperty() { Type = "number", Description = "pizza unit price" },
if(pizza_type.ToString() == "Pepperoni Pizza")
{ // },
unit_price = 3.2 * (int)quantity; // Required = new List<string>(){"pizza_type", "quantity", "unit_price" }
} // }
else if(pizza_type.ToString() == "Cheese Pizza") // }
{ // ]
unit_price = 3.5 * (int)quantity; ; // });
} // },
else if(pizza_type.ToString() == "Margherita Pizza")
{ // CallToolHandler = async (request, cancellationToken) =>
unit_price = 3.8 * (int)quantity; ; // {
} // if (request.Params.Name == "make_payment")
dynamic message = new ExpandoObject(); // {
message.unit_price = unit_price; // if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("order_number", out var order_number))
var jso = new JsonSerializerOptions() { WriteIndented = true }; // {
var jsonMessage = JsonSerializer.Serialize(message, jso); // throw new McpServerException("Missing required argument 'order_number'");
return new CallToolResponse() // }
{ // if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("total_amount", out var total_amount))
Content = [new Content() { Text = jsonMessage, Type = "text" }] // {
}; // throw new McpServerException("Missing required argument 'total_amount'");
} // }
else if (request.Params.Name == "place_an_order") // //dynamic message = new ExpandoObject();
{ // //message.Transaction = Guid.NewGuid().ToString();
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("pizza_type", out var pizza_type)) // //message.Status = "Success";
{
throw new McpServerException("Missing required argument 'pizza_type'"); // //// Serialize the message to JSON
} // //var jso = new JsonSerializerOptions() { WriteIndented = true };
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("quantity", out var quantity)) // //var jsonMessage = JsonSerializer.Serialize(message, jso);
{
throw new McpServerException("Missing required argument 'quantity'"); // return new CallToolResponse()
} // {
if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("unit_price", out var unit_price)) // Content = [new Content() { Text = "Payment proceed successfully. Thank you for your business. Have a great day!", Type = "text" }]
{ // };
throw new McpServerException("Missing required argument 'unit_price'"); // }
} // else if (request.Params.Name == "get_pizza_prices")
//dynamic message = new ExpandoObject(); // {
//message.order_number = "P123-01"; // if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("pizza_type", out var pizza_type))
//message.Content = "The order number is P123-01"; // {
//// Serialize the message to JSON // throw new McpServerException("Missing required argument 'pizza_type'");
//var jso = new JsonSerializerOptions() { WriteIndented = true }; // }
//var jsonMessage = JsonSerializer.Serialize(message, jso); // if (request.Params.Arguments is null || !request.Params.Arguments.TryGetValue("quantity", out var quantity))
return new CallToolResponse() // {
{ // throw new McpServerException("Missing required argument 'quantity'");
Content = [new Content() { Text = "The order number is P123-01: {order_number = \"P123-01\" }", Type = "text" }] // }
}; // double unit_price = 0;
} // if(pizza_type.ToString() == "Pepperoni Pizza")
else // {
{ // unit_price = 3.2 * (int)quantity;
throw new McpServerException($"Unknown tool: {request.Params.Name}"); // }
} // 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() // private static ILoggerFactory CreateLoggerFactory()
{ // {
// Use serilog // // Use serilog
Log.Logger = new LoggerConfiguration() // Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose() // Capture all log levels // .MinimumLevel.Verbose() // Capture all log levels
.WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "TestServer_.log"), // .WriteTo.File(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "TestServer_.log"),
rollingInterval: RollingInterval.Day, // rollingInterval: RollingInterval.Day,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") // outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger(); // .CreateLogger();
var logsPath = Path.Combine(AppContext.BaseDirectory, "testserver.log"); // var logsPath = Path.Combine(AppContext.BaseDirectory, "testserver.log");
return LoggerFactory.Create(builder => // return LoggerFactory.Create(builder =>
{ // {
builder.AddSerilog(); // builder.AddSerilog();
}); // });
} // }
} // }
} //}

View file

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

View file

@ -0,0 +1,84 @@
using 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<IJsonRpcMessage> _incomingChannel = CreateSingleItemChannel<IJsonRpcMessage>();
private readonly Channel<SseItem<IJsonRpcMessage?>> _outgoingSseChannel = CreateSingleItemChannel<SseItem<IJsonRpcMessage?>>();
private Task? _sseWriteTask;
private Utf8JsonWriter? _jsonWriter;
public bool IsConnected => _sseWriteTask?.IsCompleted == false;
public Task RunAsync(CancellationToken cancellationToken)
{
void WriteJsonRpcMessageToBuffer(SseItem<IJsonRpcMessage?> item, IBufferWriter<byte> writer)
{
if (item.EventType == "endpoint")
{
writer.Write("/message"u8);
return;
}
JsonSerializer.Serialize(GetUtf8JsonWriter(writer), item.Data, 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<IJsonRpcMessage?>(null, "endpoint"));
var sseItems = _outgoingSseChannel.Reader.ReadAllAsync(cancellationToken);
return _sseWriteTask = SseFormatter.WriteAsync(sseItems, sseResponseStream, WriteJsonRpcMessageToBuffer, cancellationToken);
}
public ChannelReader<IJsonRpcMessage> MessageReader => _incomingChannel.Reader;
public ValueTask DisposeAsync()
{
_incomingChannel.Writer.TryComplete();
_outgoingSseChannel.Writer.TryComplete();
return new ValueTask(_sseWriteTask ?? Task.CompletedTask);
}
public Task SendMessageAsync(IJsonRpcMessage message, CancellationToken cancellationToken = default) =>
_outgoingSseChannel.Writer.WriteAsync(new SseItem<IJsonRpcMessage?>(message), cancellationToken).AsTask();
public Task OnMessageReceivedAsync(IJsonRpcMessage message, CancellationToken cancellationToken)
{
if (!IsConnected)
{
throw new McpTransportException("Transport is not connected");
}
return _incomingChannel.Writer.WriteAsync(message, cancellationToken).AsTask();
}
private static Channel<T> CreateSingleItemChannel<T>() =>
Channel.CreateBounded<T>(new BoundedChannelOptions(1)
{
SingleReader = true,
SingleWriter = false,
});
private Utf8JsonWriter GetUtf8JsonWriter(IBufferWriter<byte> writer)
{
if (_jsonWriter is null)
{
_jsonWriter = new Utf8JsonWriter(writer);
}
else
{
_jsonWriter.Reset(writer);
}
return _jsonWriter;
}
}

View file

@ -0,0 +1,25 @@
using 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!";
}
}

View file

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

View file

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