MCP_server.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. # MCP_Server.py
  2. from fastmcp import FastMCP
  3. from ctypes import *
  4. import os
  5. import sys
  6. import subprocess
  7. import time
  8. import json
  9. from pathlib import Path
  10. # 记录已 read 过的文件,用于写入前校验
  11. _READ_CACHE = set()
  12. # 记录本会话由 agent 创建或成功写入过的文件,允许后续直接覆盖
  13. _WRITE_TRACK = set()
  14. mcp = FastMCP("服务器")
  15. @mcp.tool()
  16. def read_file(file_path: str) -> str:
  17. r"""
  18. 读取文件内容工具。
  19. args:
  20. file_path: 被读取的文件的地址 (使用linux系统,因此必须包含"./")
  21. returns:
  22. 成功: 返回"读取成功!"和文件内容
  23. 失败: 返回"ERROR:"和错误信息
  24. """
  25. try:
  26. with open(file_path, 'r', encoding='utf-8') as f:
  27. content = f.read()
  28. _READ_CACHE.add(os.path.abspath(file_path))
  29. return f"读取成功!文件内容:{content}"
  30. except Exception as e:
  31. return f"ERROR: {str(e)}"
  32. @mcp.tool()
  33. def write_file(file_path: str, content: str, mode: str = 'w') -> str:
  34. r"""
  35. 编辑文件内容工具。
  36. args:
  37. file_path: 被编辑的文件的地址 (使用linux系统,因此必须包含"./")
  38. content: 编辑的内容内容
  39. mode: 模式('默认覆盖模式'),'w'覆盖写入;'a'追加写入
  40. returns:
  41. 成功: 返回"编辑成功!"和文件路径
  42. 失败: 返回"ERROR:"和错误信息
  43. """
  44. try:
  45. abs_path = os.path.abspath(file_path)
  46. parent = Path(abs_path).parent
  47. # 动态工作目录限制
  48. session_root = os.environ.get('SESSION_WORK_DIR')
  49. if session_root:
  50. session_root_abs = os.path.abspath(session_root)
  51. # 允许写入: session_root 下的任意子路径
  52. if not abs_path.startswith(session_root_abs + os.sep):
  53. return (
  54. "ERROR: 写入被拒绝。文件不在本次会话工作目录内: "
  55. f"{file_path}. 允许根目录: {session_root_abs}/"
  56. )
  57. # 自动创建上级目录
  58. parent.mkdir(parents=True, exist_ok=True)
  59. # 覆盖策略:
  60. # 1. 若文件不存在 -> 直接写入并加入 _WRITE_TRACK
  61. # 2. 若文件存在且不是 append: 允许以下任一条件直接覆盖
  62. # a) 曾被 read 过 (兼容旧逻辑)
  63. # b) 曾在本会话通过 write_file 成功写入 (_WRITE_TRACK 命中)
  64. # 否则阻止并提示先 read 一次以确认外部文件语义
  65. exists_before = os.path.exists(abs_path)
  66. if exists_before and mode != 'a':
  67. if abs_path not in _READ_CACHE and abs_path not in _WRITE_TRACK:
  68. return (
  69. "ERROR: 试图覆盖已存在文件但之前未 read,且非本会话创建: "
  70. f"{file_path}. 如为外部文件请先调用 read_file;若期望直接覆盖,先执行一次 read 或首次写入。"
  71. )
  72. with open(abs_path, mode, encoding='utf-8') as f:
  73. f.write(content)
  74. # 标记写入跟踪(无论追加或覆盖)
  75. _WRITE_TRACK.add(abs_path)
  76. action = "追加" if (mode == 'a' and exists_before) else ("覆盖" if exists_before else "创建")
  77. return f"成功{action}文件: {file_path}"
  78. except Exception as e:
  79. return f"ERROR: {str(e)}"
  80. @mcp.tool()
  81. def run_bash(command: str) -> str:
  82. r"""
  83. 执行bash命令并返回输出的工具。
  84. args:
  85. command: 在bash中执行的命令
  86. returns:
  87. 成功: 返回"命令运行成功"和输入的命令以及对应的结果
  88. 失败: 返回"ERROR:"和错误信息
  89. """
  90. try:
  91. result = subprocess.run(
  92. command,
  93. shell=True,
  94. capture_output=True,
  95. text=True,
  96. timeout=30
  97. )
  98. if result.returncode == 0:
  99. return f"命令运行成功,[命令:{command}; {result.stdout}]"
  100. else:
  101. return f"ERROR: 命令:{command}; {result.stderr}"
  102. except subprocess.TimeoutExpired:
  103. return "ERROR: 命令执行超时"
  104. except Exception as e:
  105. return f"ERROR: {str(e)}"
  106. @mcp.tool()
  107. def run_shell(command: str) -> str:
  108. r"""
  109. 执行 shell 命令并返回结构化 JSON 字符串。
  110. 返回字段(JSON):
  111. command: 原始命令
  112. returncode: 进程退出码 (int 或 'TIMEOUT')
  113. stdout: 标准输出前若干字符 (全部保留, 若过长可由上层截断)
  114. stderr: 标准错误输出 (可能为空)
  115. duration_ms: 执行耗时 (毫秒, 仅非超时)
  116. compile_log: 若检测到 run_pika 输出中的 compile_log 行, 给出真实路径
  117. run_log: 若检测到 run_pika 输出中的 run_log 行, 给出真实路径
  118. note: 辅助说明
  119. 错误时也返回 JSON (不抛异常), 由上层 agent 自行判断。
  120. """
  121. start = time.time()
  122. try:
  123. proc = subprocess.run(
  124. command,
  125. shell=True,
  126. capture_output=True,
  127. text=True,
  128. timeout=120
  129. )
  130. dur_ms = int((time.time() - start) * 1000)
  131. stdout = proc.stdout or ""
  132. stderr = proc.stderr or ""
  133. compile_log_path = None
  134. run_log_path = None
  135. # 解析 run_pika.py 标准输出中的日志路径行
  136. # 典型行: [run_pika] compile_log: /abs/path/to/compile.log
  137. for line in stdout.splitlines():
  138. line_strip = line.strip()
  139. if 'compile_log:' in line_strip:
  140. # 拆分最后的路径部分
  141. try:
  142. compile_log_path = line_strip.split('compile_log:')[-1].strip()
  143. except Exception:
  144. pass
  145. if 'run_log:' in line_strip:
  146. try:
  147. run_log_path = line_strip.split('run_log:')[-1].strip()
  148. except Exception:
  149. pass
  150. payload = {
  151. "command": command,
  152. "returncode": proc.returncode,
  153. "stdout": stdout,
  154. "stderr": stderr,
  155. "duration_ms": dur_ms,
  156. "compile_log": compile_log_path,
  157. "run_log": run_log_path,
  158. "note": "ok" if proc.returncode == 0 else "non-zero returncode"
  159. }
  160. return json.dumps(payload, ensure_ascii=False)
  161. except subprocess.TimeoutExpired:
  162. payload = {
  163. "command": command,
  164. "returncode": "TIMEOUT",
  165. "stdout": "",
  166. "stderr": "",
  167. "duration_ms": 120000,
  168. "compile_log": None,
  169. "run_log": None,
  170. "note": "timeout"
  171. }
  172. return json.dumps(payload, ensure_ascii=False)
  173. except Exception as e:
  174. payload = {
  175. "command": command,
  176. "returncode": "EXCEPTION",
  177. "stdout": "",
  178. "stderr": str(e),
  179. "duration_ms": int((time.time() - start) * 1000),
  180. "compile_log": None,
  181. "run_log": None,
  182. "note": "raised"
  183. }
  184. return json.dumps(payload, ensure_ascii=False)
  185. if __name__ == "__main__":
  186. mcp.run()