""" 终端命令执行 MCP Server 通过 MCP 协议暴露 run_command 命令,供 Dev Agent 使用 """ import asyncio import subprocess import shlex import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent def log(msg): """写日志到 stderr,不污染 MCP stdio 协议通道""" print(msg, file=sys.stderr, flush=True) app = Server("command-executor") @app.list_tools() async def list_tools(): return [ Tool( name="run_command", description="在终端中执行一条命令,返回输出结果。适用于编译、构建、运行测试等场景。", inputSchema={ "type": "object", "properties": { "command": { "type": "string", "description": "要执行的命令,如 'npm run build'、'npm test' 等", }, "cwd": { "type": "string", "description": "命令执行的工作目录,默认使用当前目录", }, "timeout": { "type": "integer", "description": "超时时间(秒),默认 300 秒", "default": 300, }, }, "required": ["command"], }, ), ] @app.call_tool() async def call_tool(name: str, arguments: dict): if name != "run_command": raise ValueError(f"Unknown tool: {name}") cmd = arguments.get("command", "") cwd = arguments.get("cwd") or os.getcwd() timeout = int(arguments.get("timeout", 300)) if not cmd: return [TextContent(type="text", text="错误:命令不能为空")] log(f" [command] {cmd}") log(f" [cwd] {cwd}") log(f" [timeout] {timeout}s") log(f" [PATH] {os.environ.get('PATH', 'N/A')[:200]}") log(f" [where npm] {__import__('shutil').which('npm.cmd')}") try: # 使用异步 subprocess,避免阻塞 asyncio 事件循环 # 在 Windows 上,npm 是 .cmd 文件,需要通过 shell 执行 proc = await asyncio.create_subprocess_shell( cmd, cwd=cwd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout_bytes, stderr_bytes = await asyncio.wait_for( proc.communicate(), timeout=timeout, ) stdout = stdout_bytes.decode("utf-8", errors="replace") stderr = stderr_bytes.decode("utf-8", errors="replace") output_parts = [] if stdout: output_parts.append(f"[stdout]\n{stdout}") if stderr: output_parts.append(f"[stderr]\n{stderr}") output = "\n".join(output_parts) if output_parts else "(无输出)" max_len = 10000 if len(output) > max_len: output = output[:max_len] + f"\n\n...(输出已截断,共 {len(output)} 字符)" status = "成功" if proc.returncode == 0 else f"失败 (退出码 {proc.returncode})" log(f" [result] {status}") return [TextContent(type="text", text=f"命令执行{status}\n{output}")] except asyncio.TimeoutError: # 超时后杀进程 proc.kill() await proc.wait() log(f" [error] 超时") return [TextContent(type="text", text=f"命令执行超时({timeout}秒): {cmd}")] except Exception as e: log(f" [error] {e}") return [TextContent(type="text", text=f"命令执行出错: {e}")] async def main(): log("MCP 命令执行服务器启动中...") async with stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, app.create_initialization_options() ) if __name__ == "__main__": asyncio.run(main())