mcp_tool.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. """MCPTool 本地实现 - 基于 subprocess 直接通信"""
  2. import json
  3. import os
  4. import subprocess
  5. from typing import Dict, Any, List, Optional
  6. from hello_agents.tools.base import Tool, ToolParameter
  7. class MCPTool(Tool):
  8. """MCP (Model Context Protocol) 工具 - subprocess 实现"""
  9. def __init__(self,
  10. name: str = "mcp",
  11. description: Optional[str] = None,
  12. server_command: Optional[List[str]] = None,
  13. env: Optional[Dict[str, str]] = None,
  14. auto_expand: bool = True):
  15. self.server_command = server_command
  16. self.server_env = env
  17. self.auto_expand = auto_expand
  18. self.prefix = f"{name}_" if auto_expand else ""
  19. self._available_tools = []
  20. self._request_id = 0
  21. if description is None:
  22. description = f"MCP工具服务器: {name}"
  23. super().__init__(name=name, description=description, expandable=auto_expand)
  24. if server_command:
  25. self._discover_tools()
  26. def _make_env(self) -> dict:
  27. env = os.environ.copy()
  28. if self.server_env:
  29. env.update(self.server_env)
  30. return env
  31. def _batch_requests(self, requests: List[dict]) -> List[dict]:
  32. """在同一个子进程中逐个发送 JSON-RPC 请求"""
  33. from queue import Queue, Empty
  34. import threading
  35. import time
  36. proc = subprocess.Popen(
  37. self.server_command,
  38. stdin=subprocess.PIPE,
  39. stdout=subprocess.PIPE,
  40. stderr=subprocess.PIPE, # 捕获stderr,防止管道阻塞
  41. env=self._make_env()
  42. )
  43. # 读取stderr避免阻塞
  44. def stderr_reader():
  45. for _ in iter(proc.stderr.readline, b""):
  46. pass
  47. st = threading.Thread(target=stderr_reader, daemon=True)
  48. st.start()
  49. out_queue = Queue()
  50. def reader():
  51. for line in iter(proc.stdout.readline, b""):
  52. out_queue.put(line)
  53. out_queue.put(None)
  54. t = threading.Thread(target=reader, daemon=True)
  55. t.start()
  56. results = []
  57. try:
  58. for req in requests:
  59. self._request_id += 1
  60. req["id"] = self._request_id
  61. proc.stdin.write((json.dumps(req) + "\n").encode())
  62. proc.stdin.flush()
  63. # 累积多行直到可解析(amap-mcp-server 长响应可能跨多行)
  64. import ast
  65. lines_buf = []
  66. response = None
  67. for _ in range(15): # 最多拼15行
  68. try:
  69. line = out_queue.get(timeout=20)
  70. except Empty:
  71. raise RuntimeError("MCP响应超时(20s)")
  72. if line is None:
  73. raise RuntimeError("MCP连接提前关闭")
  74. raw = line.decode(errors="replace")
  75. lines_buf.append(raw)
  76. full_text = "".join(lines_buf).strip()
  77. if not full_text:
  78. continue
  79. # 尝试解析: 先 json, 再 ast.literal_eval
  80. try:
  81. response = json.loads(full_text)
  82. break # 解析成功
  83. except json.JSONDecodeError:
  84. try:
  85. parsed = ast.literal_eval(full_text)
  86. response = json.loads(json.dumps(parsed))
  87. break # 解析成功
  88. except (SyntaxError, ValueError):
  89. # 可能是截断了,继续读下一行
  90. continue
  91. if response is None:
  92. print(f" [MCP] 无法解析响应(共{len(lines_buf)}行): {repr(full_text[:200])}")
  93. raise RuntimeError("无法解析MCP响应")
  94. if "error" in response:
  95. raise RuntimeError(f"MCP错误: {response['error']}")
  96. results.append(response.get("result", {}))
  97. # MCP 协议: initialize 后需发送 initialized 通知
  98. if req.get("method") == "initialize":
  99. notif = {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}}
  100. proc.stdin.write((json.dumps(notif) + "\n").encode())
  101. proc.stdin.flush()
  102. time.sleep(0.2) # 给服务器短暂时间处理通知
  103. finally:
  104. try:
  105. proc.stdin.close()
  106. except Exception:
  107. pass
  108. proc.wait(timeout=10)
  109. return results
  110. def _discover_tools(self):
  111. """发现 MCP 服务器的工具"""
  112. try:
  113. results = self._batch_requests([
  114. {
  115. "jsonrpc": "2.0",
  116. "method": "initialize",
  117. "params": {
  118. "protocolVersion": "2024-11-05",
  119. "capabilities": {},
  120. "clientInfo": {"name": "helloagents-trip-planner", "version": "1.0"}
  121. }
  122. },
  123. {
  124. "jsonrpc": "2.0",
  125. "method": "tools/list",
  126. "params": {}
  127. }
  128. ])
  129. if len(results) >= 2:
  130. tool_list = results[1]
  131. self._available_tools = [
  132. {
  133. "name": tool["name"],
  134. "description": tool.get("description", ""),
  135. "input_schema": tool.get("inputSchema", {})
  136. }
  137. for tool in tool_list.get("tools", [])
  138. ]
  139. except Exception as e:
  140. print(f" ⚠️ MCP工具发现失败: {e}")
  141. def get_expanded_tools(self) -> List[Tool]:
  142. if not self.auto_expand or not self._available_tools:
  143. return []
  144. return [MCPWrappedTool(self, info, self.prefix) for info in self._available_tools]
  145. def run(self, parameters: Dict[str, Any]) -> str:
  146. action = parameters.get("action", "").lower()
  147. if not action and "tool_name" in parameters:
  148. action = "call_tool"
  149. try:
  150. if action == "call_tool":
  151. tool_name = parameters.get("tool_name")
  152. arguments = parameters.get("arguments", {})
  153. results = self._batch_requests([
  154. {
  155. "jsonrpc": "2.0",
  156. "method": "initialize",
  157. "params": {
  158. "protocolVersion": "2024-11-05",
  159. "capabilities": {},
  160. "clientInfo": {"name": "helloagents-trip-planner", "version": "1.0"}
  161. }
  162. },
  163. {
  164. "jsonrpc": "2.0",
  165. "method": "tools/call",
  166. "params": {"name": tool_name, "arguments": arguments}
  167. }
  168. ])
  169. if len(results) < 2:
  170. return "MCP调用无返回"
  171. content = results[1].get("content", [])
  172. text_parts = []
  173. for c in content:
  174. if c.get("type") == "text":
  175. text_parts.append(c["text"])
  176. else:
  177. text_parts.append(str(c))
  178. return "\n".join(text_parts) if text_parts else str(results[1])
  179. elif action == "list_tools":
  180. return f"找到 {len(self._available_tools)} 个工具:\n" + "\n".join(
  181. f"- {t['name']}: {t['description']}" for t in self._available_tools
  182. )
  183. else:
  184. return f"不支持的操作: {action}"
  185. except Exception as e:
  186. return f"MCP 操作失败: {str(e)}"
  187. def get_parameters(self) -> List[ToolParameter]:
  188. return [
  189. ToolParameter(name="action", type="string",
  190. description="操作类型: list_tools, call_tool", required=True),
  191. ToolParameter(name="tool_name", type="string",
  192. description="工具名称", required=False),
  193. ToolParameter(name="arguments", type="object",
  194. description="工具参数", required=False),
  195. ]
  196. class MCPWrappedTool(Tool):
  197. """MCP 工具包装器 - 单个 MCP 工具"""
  198. def __init__(self, mcp_tool: MCPTool, tool_info: Dict[str, Any], prefix: str = ""):
  199. self.mcp_tool = mcp_tool
  200. self.tool_info = tool_info
  201. self.mcp_tool_name = tool_info.get("name", "unknown")
  202. tool_name = f"{prefix}{self.mcp_tool_name}" if prefix else self.mcp_tool_name
  203. description = tool_info.get("description", f"MCP工具: {self.mcp_tool_name}")
  204. self._parameters = self._parse_input_schema(tool_info.get("input_schema", {}))
  205. super().__init__(name=tool_name, description=description)
  206. def _parse_input_schema(self, input_schema: Dict[str, Any]) -> List[ToolParameter]:
  207. params = []
  208. properties = input_schema.get("properties", {})
  209. required_fields = input_schema.get("required", [])
  210. for name, info in properties.items():
  211. params.append(ToolParameter(
  212. name=name,
  213. type=info.get("type", "string"),
  214. description=info.get("description", ""),
  215. required=name in required_fields
  216. ))
  217. return params
  218. def get_parameters(self) -> List[ToolParameter]:
  219. return self._parameters
  220. def run(self, params: Dict[str, Any]) -> str:
  221. return self.mcp_tool.run({
  222. "action": "call_tool",
  223. "tool_name": self.mcp_tool_name,
  224. "arguments": params
  225. })