feat : test pizza with mcp

This commit is contained in:
geffzhang 2025-02-27 12:39:07 +08:00
parent 10837003b7
commit ffcb1e3b52
15 changed files with 233 additions and 116 deletions

View file

@ -131,6 +131,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.MCP", "src\Infrast
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BotSharp.PizzaBot.MCPServer", "tests\BotSharp.PizzaBot.MCPServer\BotSharp.PizzaBot.MCPServer.csproj", "{AD91B4ED-0623-4710-913E-6D7C893E76EF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mcpdotnet", "..\mcpdotnet\src\mcpdotnet\mcpdotnet.csproj", "{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -539,6 +541,14 @@ Global
{AD91B4ED-0623-4710-913E-6D7C893E76EF}.Release|Any CPU.Build.0 = Release|Any CPU
{AD91B4ED-0623-4710-913E-6D7C893E76EF}.Release|x64.ActiveCfg = Release|Any CPU
{AD91B4ED-0623-4710-913E-6D7C893E76EF}.Release|x64.Build.0 = Release|Any CPU
{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A}.Debug|x64.ActiveCfg = Debug|Any CPU
{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A}.Debug|x64.Build.0 = Debug|Any CPU
{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A}.Release|Any CPU.Build.0 = Release|Any CPU
{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A}.Release|x64.ActiveCfg = Release|Any CPU
{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -602,6 +612,7 @@ Global
{AF329442-B48E-4B48-A18A-1C869D1BA6F5} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{8ED8EEF4-06DD-45F5-AA91-BD2395E901B5} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
{AD91B4ED-0623-4710-913E-6D7C893E76EF} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
{DF0DBE28-C0FE-4947-8E95-9FBC42B4D44A} = {E29DC6C4-5E57-48C5-BCB0-6B8F84782749}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A9969D89-C98B-40A5-A12B-FC87E55B3A19}

View file

@ -72,7 +72,7 @@ public partial class AgentService
hook.OnAgentUtilityLoaded(agent);
}
if(agent.McpTools != null)
if(agent.McpTools != null && agent.McpTools.Count >0)
{
hook.OnAgentMCPToolLoaded(agent);
}

View file

@ -8,11 +8,8 @@
<OutputPath>$(SolutionDir)packages</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="mcpdotnet" Version="1.0.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\mcpdotnet\src\mcpdotnet\mcpdotnet.csproj" />
<ProjectReference Include="..\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
</ItemGroup>
</Project>

View file

@ -1,7 +1,11 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Functions;
using McpDotNet.Client;
using McpDotNet.Protocol.Types;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
@ -13,12 +17,14 @@ namespace BotSharp.Core.Mcp.Functions;
public class McpToolFunction : IFunctionCallback
{
private readonly Tool _tool;
private readonly IMcpClient _client;
private readonly MCPClientManager _clientManager;
private readonly IServiceProvider _serviceProvider;
public McpToolFunction(Tool tool, IMcpClient client)
public McpToolFunction(IServiceProvider provider, Tool tool, MCPClientManager client)
{
_serviceProvider = provider ?? throw new ArgumentNullException(nameof(provider));
_tool = tool ?? throw new ArgumentNullException(nameof(tool));
_client = client ?? throw new ArgumentNullException(nameof(client));
_clientManager = client ?? throw new ArgumentNullException(nameof(client));
}
public string Name => _tool.Name;
@ -27,9 +33,14 @@ public class McpToolFunction : IFunctionCallback
{
// Convert arguments to dictionary format expected by mcpdotnet
Dictionary<string, object> argDict = JsonToDictionary(message.FunctionArgs);
var currentAgentId = message.CurrentAgentId;
var agentService = _serviceProvider.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(currentAgentId);
var serverId = agent.McpTools.Where(t => t.Functions.Any(f => f.Name == Name)).FirstOrDefault().ServerId;
var client = await _clientManager.Factory.GetClientAsync(serverId);
// Call the tool through mcpdotnet
var result = await _client.CallToolAsync(
var result = await client.CallToolAsync(
_tool.Name,
argDict.Count == 0 ? new() : argDict
);

View file

@ -26,14 +26,11 @@ public class MCPToolAgentHook : AgentHookBase
{
if (agent.Type == AgentType.Routing)
return;
var conv = _services.GetRequiredService<IConversationService>();
var isConvMode = conv.IsConversationMode();
if (!isConvMode) return;
agent.SecondaryFunctions ??= [];
agent.SecondaryInstructions ??= [];
agent.McpTools ??= [];
var functions = GetMCPContent(agent);

View file

@ -1,7 +1,11 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Functions;
using BotSharp.Abstraction.Plugins;
using BotSharp.Core.Mcp.Functions;
using BotSharp.Core.Mcp.Settings;
using BotSharp.MCP.Hooks;
using McpDotNet.Protocol.Types;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@ -31,13 +35,16 @@ public class McpPlugin : IBotSharpPlugin
var tools = client.ListToolsAsync().Result;
foreach (var tool in tools.Tools)
{
services.AddScoped<IFunctionCallback>(provider =>
{
var func = new McpToolFunction(tool, client);
return func;
});
{
services.AddScoped( provider => { return tool; });
services.AddScoped<IFunctionCallback>( provider => {
var funcTool = new McpToolFunction( provider, tool, clientManager);
return funcTool;
});
}
}
// Register hooks
services.AddScoped<IAgentHook, MCPToolAgentHook>();
}
}

View file

@ -50,10 +50,7 @@
"Name": "gpt-35-turbo",
"Version": "1106",
"ApiKey": "",
"Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/",
"Type": "chat",
"PromptCost": 0.0015,
"CompletionCost": 0.002
"Endpoint": "https://gpt-35-turbo-instruct.openai.azure.com/"
},
{
"Name": "gpt-35-turbo-instruct",
@ -170,7 +167,7 @@
"HostAgentId": "01e2fc5c-2c89-4ec7-8470-7688608b496c",
"EnableTranslator": false,
"LlmConfig": {
"Provider": "openai",
"Provider": "azure-openai",
"Model": "gpt-4o-mini"
}
},
@ -187,7 +184,8 @@
"Name": "PizzaServer",
"TransportType": "stdio",
"TransportOptions": {
"Command": "BotSharp.PizzaBot.MCPServer.exe"
"command": "BotSharp.PizzaBot.MCPServer.exe",
"workingDirectory": "E:\\GitHub\\BotSharp\\src\\WebStarter\\bin\\Debug\\net8.0"
}
}
]
@ -409,6 +407,7 @@
"BotSharp.Core.SideCar",
"BotSharp.Core.Crontab",
"BotSharp.Logger",
"BotSharp.MCP",
"BotSharp.Plugin.MongoStorage",
"BotSharp.Plugin.Dashboard",
"BotSharp.Plugin.OpenAI",

View file

@ -54,6 +54,39 @@ namespace BotSharp.PizzaBot.MCPServer
},
Required = new List<string>() { "order_number", "total_amount" }
},
},
new Tool()
{
Name = "get_pizza_prices",
Description = "call this function to get pizza prices",
InputSchema = new JsonSchema()
{
Type = "object",
Properties = new Dictionary<string, JsonSchemaProperty>()
{
["pizza_type"] = new JsonSchemaProperty() { Type = "string", Description = "The pizza type." },
["quantity"] = new JsonSchemaProperty() { Type = "string", Description = "quantity of pizza." },
},
Required = new List<string>(){ "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<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 = "unit price" },
},
Required = new List<string>(){"pizza_type", "quantity", "unit_price" }
}
}
]
});
@ -72,9 +105,8 @@ namespace BotSharp.PizzaBot.MCPServer
throw new McpServerException("Missing required argument 'total_amount'");
}
dynamic message = new ExpandoObject();
message.pepperoni_unit_price = 3.2;
message.cheese_unit_price = 3.5;
message.margherita_unit_price = 3.8;
message.Transaction = Guid.NewGuid().ToString();
message.Status = "Success";
// Serialize the message to JSON
var jso = new JsonSerializerOptions() { WriteIndented = true };
@ -85,6 +117,53 @@ namespace BotSharp.PizzaBot.MCPServer
Content = [new Content() { Text = jsonMessage , Type = "text" }]
};
}
else if(request.Name == "get_pizza_prices")
{
if (request.Arguments is null || !request.Arguments.TryGetValue("pizza_type", out var pizza_type))
{
throw new McpServerException("Missing required argument 'pizza_type'");
}
if (request.Arguments is null || !request.Arguments.TryGetValue("quantity", out var quantity))
{
throw new McpServerException("Missing required argument 'quantity'");
}
dynamic message = new ExpandoObject();
message.pepperoni_unit_price = 3.2;
message.cheese_unit_price = 3.5;
message.margherita_unit_price = 3.8;
// Serialize the message to JSON
var jso = new JsonSerializerOptions() { WriteIndented = true };
var jsonMessage = JsonSerializer.Serialize(message, jso);
return new CallToolResponse()
{
Content = [new Content() { Text = jsonMessage, Type = "text" }]
};
}
else if (request.Name == "place_an_order")
{
if (request.Arguments is null || !request.Arguments.TryGetValue("pizza_type", out var pizza_type))
{
throw new McpServerException("Missing required argument 'pizza_type'");
}
if (request.Arguments is null || !request.Arguments.TryGetValue("quantity", out var quantity))
{
throw new McpServerException("Missing required argument 'quantity'");
}
if (request.Arguments is null || !request.Arguments.TryGetValue("unit_price", out var unit_price))
{
throw new McpServerException("Missing required argument 'unit_price'");
}
dynamic message = new ExpandoObject();
message.order_number = "P123-01";
message.Content = "The order number is P123-01";
// Serialize the message to JSON
var jso = new JsonSerializerOptions() { WriteIndented = true };
var jsonMessage = JsonSerializer.Serialize(message, jso);
return new CallToolResponse()
{
Content = [new Content() { Text = jsonMessage, Type = "text" }]
};
}
else
{
throw new McpServerException($"Unknown tool: {request.Name}");

View file

@ -1,21 +1,21 @@
using BotSharp.Abstraction.Conversations.Models;
using System.Text.Json;
//using BotSharp.Abstraction.Conversations.Models;
//using System.Text.Json;
namespace BotSharp.Plugin.PizzaBot.Functions;
//namespace BotSharp.Plugin.PizzaBot.Functions;
public class GetPizzaPricesFn : IFunctionCallback
{
public string Name => "get_pizza_price";
//public class GetPizzaPricesFn : IFunctionCallback
//{
// public string Name => "get_pizza_price";
public async Task<bool> Execute(RoleDialogModel message)
{
message.Data = new
{
pepperoni_unit_price = 3.2,
cheese_unit_price = 3.5,
margherita_unit_price = 3.8,
};
message.Content = JsonSerializer.Serialize(message.Data);
return true;
}
}
// public async Task<bool> Execute(RoleDialogModel message)
// {
// message.Data = new
// {
// pepperoni_unit_price = 3.2,
// cheese_unit_price = 3.5,
// margherita_unit_price = 3.8,
// };
// message.Content = JsonSerializer.Serialize(message.Data);
// return true;
// }
//}

View file

@ -46,7 +46,7 @@ public class GetPizzaTypesFn : IFunctionCallback
}).ToArray()
}
};
return true;
}
}

View file

@ -1,24 +1,24 @@
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Models;
//using BotSharp.Abstraction.Conversations;
//using BotSharp.Abstraction.Conversations.Models;
namespace BotSharp.Plugin.PizzaBot.Functions;
//namespace BotSharp.Plugin.PizzaBot.Functions;
public class PlaceOrderFn : IFunctionCallback
{
public string Name => "place_an_order";
//public class PlaceOrderFn : IFunctionCallback
//{
// public string Name => "place_an_order";
private readonly IServiceProvider _service;
public PlaceOrderFn(IServiceProvider service)
{
_service = service;
}
// private readonly IServiceProvider _service;
// public PlaceOrderFn(IServiceProvider service)
// {
// _service = service;
// }
public async Task<bool> Execute(RoleDialogModel message)
{
message.Content = "The order number is P123-01";
var state = _service.GetRequiredService<IConversationStateService>();
state.SetState("order_number", "P123-01");
// public async Task<bool> Execute(RoleDialogModel message)
// {
// message.Content = "The order number is P123-01";
// var state = _service.GetRequiredService<IConversationStateService>();
// state.SetState("order_number", "P123-01");
return true;
}
}
// return true;
// }
//}

View file

@ -6,5 +6,19 @@
"id": "c2b57a74-ae4e-4c81-b3ad-9ac5bff982bd",
"disabled": false,
"isPublic": true,
"profiles": [ "pizza" ]
"profiles": [ "pizza" ],
"mcptools": [
{
"serverid": "PizzaServer",
"disabled": false,
"functions": [
{
"Name": "get_pizza_price"
},
{
"Name": "place_an_order"
}
]
}
]
}

View file

@ -1,18 +1,18 @@
{
"name": "get_pizza_price",
"description": "call this function to get the pizza price",
"parameters": {
"type": "object",
"properties": {
"pizza_type": {
"type": "string",
"description": "The pizza type."
},
"quantity": {
"type": "string",
"description": "quantity of pizza."
}
},
"required": [ "pizza_type", "quantity" ]
}
}
//{
// "name": "get_pizza_price",
// "description": "call this function to get the pizza price",
// "parameters": {
// "type": "object",
// "properties": {
// "pizza_type": {
// "type": "string",
// "description": "The pizza type."
// },
// "quantity": {
// "type": "string",
// "description": "quantity of pizza."
// }
// },
// "required": [ "pizza_type", "quantity" ]
// }
//}

View file

@ -1,22 +1,22 @@
{
"name": "place_an_order",
"description": "Place an order when user has confirmed the pizza type and quantity.",
"parameters": {
"type": "object",
"properties": {
"pizza_type": {
"type": "string",
"description": "The pizza type."
},
"quantity": {
"type": "number",
"description": "quantity of pizza."
},
"unit_price": {
"type": "number",
"description": "unit price"
}
},
"required": [ "pizza_type", "quantity", "unit_price" ]
}
}
//{
// "name": "place_an_order",
// "description": "Place an order when user has confirmed the pizza type and quantity.",
// "parameters": {
// "type": "object",
// "properties": {
// "pizza_type": {
// "type": "string",
// "description": "The pizza type."
// },
// "quantity": {
// "type": "number",
// "description": "quantity of pizza."
// },
// "unit_price": {
// "type": "number",
// "description": "unit price"
// }
// },
// "required": [ "pizza_type", "quantity", "unit_price" ]
// }
//}

View file

@ -7,15 +7,17 @@
"disabled": false,
"isPublic": true,
"profiles": [ "pizza" ],
"McpTools": {
"ServerId": "b284db86-e9c2-4c25-a59e-4649797dd130",
"Disabled": "false",
"Functions": [
{
"Name": "make_payment"
}
]
},
"mcptools": [
{
"serverid": "PizzaServer",
"disabled": false,
"functions": [
{
"Name": "make_payment"
}
]
}
],
"routingRules": [
{
"field": "order_number",