From b3e2664acd2f4a55248f14ff6e419748cfcb2fea Mon Sep 17 00:00:00 2001 From: PeterZhong Date: Mon, 19 May 2025 12:14:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5MCP=20ListTool=E8=83=BD?= =?UTF-8?q?=E5=8A=9B=E8=A1=A5=E5=85=85=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LinkToolAddin.csproj | 1 + Properties/launchSettings.json | 2 +- client/tool/ArcGisPro.cs | 19 +++- host/CallMcp.cs | 4 +- host/Gateway.cs | 183 ++++++++++++++++++++++++++++++- host/McpServerList.cs | 50 ++++++++- host/mcp/InnerMcpServer.cs | 26 ++++- host/mcp/McpToolDefinition.cs | 29 ++++- host/prompt/SystemPrompt.cs | 8 ++ server/CallArcGISPro.cs | 25 ++++- server/JsonRpcErrorEntity.cs | 4 +- ui/dockpane/TestDockpane.xaml.cs | 2 +- 12 files changed, 327 insertions(+), 26 deletions(-) diff --git a/LinkToolAddin.csproj b/LinkToolAddin.csproj index e0d0d0e..c53da32 100644 --- a/LinkToolAddin.csproj +++ b/LinkToolAddin.csproj @@ -106,6 +106,7 @@ + diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 5a2c881..e98eaad 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "LinkToolAddin": { "commandName": "Executable", - "executablePath": "C:\\Program Files\\ArcGIS\\Pro\\bin\\ArcGISPro.exe", + "executablePath": "C:\\Users\\PeterZhong\\AppData\\Local\\Programs\\ArcGIS\\Pro\\bin\\ArcGISPro.exe", "applicationUrl": "https://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/client/tool/ArcGisPro.cs b/client/tool/ArcGisPro.cs index 64e38d9..c94f2e3 100644 --- a/client/tool/ArcGisPro.cs +++ b/client/tool/ArcGisPro.cs @@ -1,6 +1,21 @@ -namespace LinkToolAddin.client.tool; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using LinkToolAddin.server; +using ModelContextProtocol.Server; +using Newtonsoft.Json; + +namespace LinkToolAddin.client.tool; public class ArcGisPro { - + [McpServerTool, Description("ArcGIS Pro Tool")] + public static async Task ArcGisProTool(string toolName, List toolParams) + { + // Call the ArcGIS Pro method and get the result + var result = await server.CallArcGISPro.CallArcGISProTool(toolName, toolParams); + + // Serialize the result back to a JSON string + return result; + } } \ No newline at end of file diff --git a/host/CallMcp.cs b/host/CallMcp.cs index a98488d..0e48789 100644 --- a/host/CallMcp.cs +++ b/host/CallMcp.cs @@ -18,8 +18,8 @@ namespace LinkToolAddin.host log.Info("通过反射调用内部MCP工具"); var jsonRpcEntity = JsonConvert.DeserializeObject(jsonRpcString); - Type type = Type.GetType("LinkToolAddin.client."+jsonRpcEntity.Method.Split('.')[0]); - MethodInfo method = type.GetMethod(jsonRpcEntity.Method.Split('.')[1],BindingFlags.Public | BindingFlags.Static); + Type type = Type.GetType("LinkToolAddin.client.tool"+jsonRpcEntity.Method.Split(':')[0]); + MethodInfo method = type.GetMethod(jsonRpcEntity.Method.Split(':')[1],BindingFlags.Public | BindingFlags.Static); var task = method.Invoke(null, new object[] { jsonRpcEntity.Params }) as Task; JsonRpcResultEntity result = await task; return JsonConvert.SerializeObject(result); diff --git a/host/Gateway.cs b/host/Gateway.cs index 2b209b1..847fcd3 100644 --- a/host/Gateway.cs +++ b/host/Gateway.cs @@ -1,7 +1,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Threading.Tasks; using System.Xml.Linq; using LinkToolAddin.client; using LinkToolAddin.client.prompt; @@ -10,13 +16,22 @@ using LinkToolAddin.host.llm.entity; using LinkToolAddin.host.mcp; using LinkToolAddin.host.prompt; using LinkToolAddin.message; +using LinkToolAddin.server; +using LinkToolAddin.ui.dockpane; +using log4net; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Client; using ModelContextProtocol.Protocol.Types; using Newtonsoft.Json; +using Newtonsoft.Json.Schema; +using Newtonsoft.Json.Schema.Generation; +using Tool = LinkToolAddin.host.mcp.Tool; namespace LinkToolAddin.host; public class Gateway { + private static ILog log = LogManager.GetLogger(typeof(Gateway)); public static async void SendMessage(string message, string model, string gdbPath, Action callback) { Llm bailian = new Bailian @@ -24,10 +39,12 @@ public class Gateway api_key = "sk-db177155677e438f832860e7f4da6afc" }; List messages = new List(); + string toolInfos = await GetToolInfos(new McpServerList()); + log.Info(SystemPrompt.SysPrompt(gdbPath, toolInfos)); messages.Add(new Message { Role = "system", - Content = SystemPrompt.SysPromptTemplate + Content = SystemPrompt.SysPrompt(gdbPath, toolInfos) }); messages.Add(new Message { @@ -37,7 +54,7 @@ public class Gateway bool goOn = true; string pattern = "[\\s\\S]*?<\\/tool_use>"; string promptPattern = "[\\s\\S]*?<\\/prompt>"; - Dictionary servers = new Dictionary(); + McpServerList mcpServerList = new McpServerList(); while (goOn) { string reponse = await bailian.SendChatAsync(new LlmJsonContent() @@ -62,7 +79,7 @@ public class Gateway Dictionary toolParams = JsonConvert.DeserializeObject>(toolArgs); string serverName = fullToolName.Contains(":") ? fullToolName.Split(':')[0] : fullToolName; string toolName = fullToolName.Contains(":") ? fullToolName.Split(':')[1] : fullToolName; - McpServer mcpServer = servers[serverName]; + McpServer mcpServer = mcpServerList.GetServer(serverName); if (mcpServer is SseMcpServer) { SseMcpServer sseMcpServer = mcpServer as SseMcpServer; @@ -111,6 +128,55 @@ public class Gateway Content = JsonConvert.SerializeObject(toolResponse) }); callback?.Invoke(toolMessageItem); + }else if (mcpServer is InnerMcpServer) + { + Type type = Type.GetType("LinkToolAddin.client.tool."+serverName); + MethodInfo method = type.GetMethod(toolName,BindingFlags.Public | BindingFlags.Static); + var task = method.Invoke(null, toolParams.Values.ToArray()) as Task; + JsonRpcResultEntity innerResult = await task; + if (innerResult is JsonRpcErrorEntity) + { + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = "fail", + content = JsonConvert.SerializeObject(innerResult) + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ErrorPromptTemplate + }); + messages.Add(new Message + { + Role = "user", + Content = JsonConvert.SerializeObject(innerResult) + }); + callback?.Invoke(toolMessageItem); + }else if (innerResult is JsonRpcSuccessEntity) + { + MessageListItem toolMessageItem = new ToolMessageItem + { + toolName = toolName, + toolParams = toolParams, + type = MessageType.TOOL_MESSAGE, + status = "success", + content = JsonConvert.SerializeObject(innerResult) + }; + messages.Add(new Message + { + Role = "user", + Content = SystemPrompt.ContinuePromptTemplate + }); + messages.Add(new Message + { + Role = "user", + Content = JsonConvert.SerializeObject(innerResult) + }); + callback?.Invoke(toolMessageItem); + } } } else if (Regex.IsMatch(reponse, promptPattern)) @@ -145,6 +211,115 @@ public class Gateway } } + private static async Task GetToolInfos(McpServerList mcpServerList) + { + StringBuilder toolInfos = new StringBuilder(); + foreach (McpServer mcpServer in mcpServerList.GetAllServers()) + { + if (mcpServer is InnerMcpServer) + { + string serverName = mcpServer.Name; + if (serverName is null) + { + continue; + } + Type type = Type.GetType("LinkToolAddin.client.tool." + serverName); + Type type2 = Type.GetType("LinkToolAddin.client.tool.ArcGisPro"); + MethodInfo[] methods = type.GetMethods(); + foreach (MethodInfo method in methods) + { + if (method.IsPublic && method.IsStatic) + { + string methodName = method.Name; + string methodDescription = method.GetCustomAttribute()?.Description; + string methodParamSchema = GenerateMethodParamSchema(method); + McpToolDefinition toolDefinition = new McpToolDefinition + { + Tool = new Tool + { + Name = methodName, + Description = methodDescription, + Arguments = methodParamSchema + } + }; + toolInfos.AppendLine(JsonConvert.DeserializeXmlNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + } + } + } + else if(mcpServer is SseMcpServer) + { + SseMcpClient client = new SseMcpClient((mcpServer as SseMcpServer).BaseUrl); + IList tools = await client.GetToolListAsync(); + foreach (McpClientTool tool in tools) + { + string toolName = tool.Name; + string toolDescription = tool.Description; + string toolParamSchema = tool.JsonSchema.ToString(); + McpToolDefinition toolDefinition = new McpToolDefinition + { + Tool = new Tool + { + Name = toolName, + Description = toolDescription, + Arguments = toolParamSchema + } + }; + toolInfos.AppendLine(JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + } + }else if (mcpServer is StdioMcpServer) + { + StdioMcpClient client = new StdioMcpClient((mcpServer as StdioMcpServer).Command, (mcpServer as StdioMcpServer).Args); + IList tools = await client.GetToolListAsync(); + foreach (McpClientTool tool in tools) + { + string toolName = tool.Name; + string toolDescription = tool.Description; + string toolParamSchema = tool.JsonSchema.ToString(); + McpToolDefinition toolDefinition = new McpToolDefinition + { + Tool = new Tool + { + Name = toolName, + Description = toolDescription, + Arguments = toolParamSchema + } + }; + toolInfos.AppendLine(JsonConvert.DeserializeXNode(JsonConvert.SerializeObject(toolDefinition)).ToString()); + } + } + } + return toolInfos.ToString(); + } + + private static string GenerateMethodParamSchema(MethodInfo method) + { + var generator = new JSchemaGenerator + { + // 启用属性注解处理 + DefaultRequired = Required.DisallowNull, + SchemaReferenceHandling = SchemaReferenceHandling.None + }; + + var paramSchema = new JSchema { Type = JSchemaType.Object }; + + foreach (ParameterInfo param in method.GetParameters()) + { + // 生成参数类型的基础Schema + JSchema typeSchema = generator.Generate(param.ParameterType); + + // 添加Description描述 + var descriptionAttr = param.GetCustomAttribute(); + if (descriptionAttr != null) + { + typeSchema.Description = descriptionAttr.Description; // 网页6的Description特性处理 + } + + paramSchema.Properties.Add(param.Name, typeSchema); + } + + return paramSchema.ToString(); + } + public static async void TestChatMessage(string message, string model, string gdbPath, Action callback) { @@ -178,7 +353,7 @@ public class Gateway callback?.Invoke(toolListItem); } - public static async void TestWOrkflow(string message, string model, string gdbPath, Action callback) + public static async void TestWorkflow(string message, string model, string gdbPath, Action callback) { Thread.Sleep(2000); MessageListItem chatListItem = new ChatMessageItem diff --git a/host/McpServerList.cs b/host/McpServerList.cs index 48f66c4..52dffd7 100644 --- a/host/McpServerList.cs +++ b/host/McpServerList.cs @@ -1,6 +1,54 @@ -namespace LinkToolAddin.host; +using System.Collections.Generic; +using LinkToolAddin.host.mcp; + +namespace LinkToolAddin.host; public class McpServerList { + private Dictionary servers = new Dictionary(); + + public McpServerList() + { + servers.Add("gaode",new SseMcpServer + { + Name = "gaode", + Type = "sse", + Description = "高德地图API", + IsActive = true, + BaseUrl = "https://mcp.amap.com/sse?key=ed418512c94ade8f83d42c37b77d2bb2", + Headers = new Dictionary() + { + {"Content-Type","application/json"} + } + }); + servers.Add("arcgis", new InnerMcpServer + { + Name = "ArcGisPro", + Type = "inner", + Description = "可以调用arcgis的地理处理工具或执行python代码等", + IsActive = true + }); + } + public McpServer GetServer(string name) + { + if (servers.ContainsKey(name)) + { + return servers[name]; + } + else + { + return null; + } + } + + public List GetAllServers() + { + List serverList = new List(); + foreach (var server in servers) + { + serverList.Add(server.Value); + } + return serverList; + } } \ No newline at end of file diff --git a/host/mcp/InnerMcpServer.cs b/host/mcp/InnerMcpServer.cs index ccf5884..2b06319 100644 --- a/host/mcp/InnerMcpServer.cs +++ b/host/mcp/InnerMcpServer.cs @@ -1,6 +1,24 @@ -namespace LinkToolAddin.host.mcp; - -public class InnerMcpServer +namespace LinkToolAddin.host.mcp { - + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class InnerMcpServer : McpServer + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("isActive")] + public bool IsActive { get; set; } + } } \ No newline at end of file diff --git a/host/mcp/McpToolDefinition.cs b/host/mcp/McpToolDefinition.cs index 3bd9adc..6e79610 100644 --- a/host/mcp/McpToolDefinition.cs +++ b/host/mcp/McpToolDefinition.cs @@ -1,6 +1,27 @@ -namespace LinkToolAddin.host.mcp; - -public class McpDefinition +namespace LinkToolAddin.host.mcp { - + using System; + using System.Collections.Generic; + + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + public partial class McpToolDefinition + { + [JsonProperty("tool")] + public Tool Tool { get; set; } + } + + public partial class Tool + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("arguments")] + public string Arguments { get; set; } + } } \ No newline at end of file diff --git a/host/prompt/SystemPrompt.cs b/host/prompt/SystemPrompt.cs index bcd3768..186439b 100644 --- a/host/prompt/SystemPrompt.cs +++ b/host/prompt/SystemPrompt.cs @@ -8,4 +8,12 @@ public class SystemPrompt public static string ErrorPromptTemplate = "执行上一个工具的时候出现以下错误,请根据报错信息重试"; + public static string SysPrompt(string gdbPath, string toolInfos) + { + string sysPrompt = SysPromptTemplate; + sysPrompt = sysPrompt.Replace("{{gdbPath}}", gdbPath); + sysPrompt = sysPrompt.Replace("{{toolInfos}}", toolInfos); + return sysPrompt; + } + } \ No newline at end of file diff --git a/server/CallArcGISPro.cs b/server/CallArcGISPro.cs index 39f5ef0..049fd3f 100644 --- a/server/CallArcGISPro.cs +++ b/server/CallArcGISPro.cs @@ -10,14 +10,29 @@ namespace LinkToolAddin.server; public class CallArcGISPro { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(CallArcGISPro)); + public async static Task CallArcGISProTool(string toolName, List toolParams) { var results = await Geoprocessing.ExecuteToolAsync(toolName, toolParams); - log.Info($"CallArcGISProTool: {toolName} | {toolParams}"); - return new JsonRpcSuccessEntity() + JsonRpcResultEntity jsonRpcResultEntity; + if (results.ErrorCode == 0) { - Id = 1, - Result = results.ToString() - }; + jsonRpcResultEntity = new JsonRpcErrorEntity() + { + Error = new Error() + { + Code = results.ErrorCode, + Message = results.ErrorMessages.ToString() + } + }; + } + else + { + jsonRpcResultEntity = new JsonRpcSuccessEntity + { + Result = results.Messages.ToString() + }; + } + return jsonRpcResultEntity; } } \ No newline at end of file diff --git a/server/JsonRpcErrorEntity.cs b/server/JsonRpcErrorEntity.cs index 46381de..ad0d795 100644 --- a/server/JsonRpcErrorEntity.cs +++ b/server/JsonRpcErrorEntity.cs @@ -2,10 +2,10 @@ namespace LinkToolAddin.server { using Newtonsoft.Json; - public partial class JsonRpcErrorEntity + public partial class JsonRpcErrorEntity : JsonRpcResultEntity { [JsonProperty("jsonrpc")] - public string Jsonrpc { get; set; } + public string Jsonrpc { get; set; } = "2.0"; [JsonProperty("error")] public Error Error { get; set; } diff --git a/ui/dockpane/TestDockpane.xaml.cs b/ui/dockpane/TestDockpane.xaml.cs index 6f33bb6..802464a 100644 --- a/ui/dockpane/TestDockpane.xaml.cs +++ b/ui/dockpane/TestDockpane.xaml.cs @@ -146,7 +146,7 @@ namespace LinkToolAddin.ui.dockpane private void TestWorkflow_OnClick(object sender, RoutedEventArgs e) { - Gateway.SendMessage("你好","qwen-max","test.gdb",ShowMessage); + Gateway.SendMessage("你有什么工具","qwen-max","test.gdb",ShowMessage); } public void ShowMessage(MessageListItem msg)