From 7fe37768c492f26a5d19b20843e3915a67463f5e Mon Sep 17 00:00:00 2001 From: handsomeAq Date: Wed, 29 Jul 2026 15:14:45 +0800 Subject: [PATCH] first commit --- .codebuddy/.gitignore | 1 + .gitignore | 31 ++++++ mcp_tools/__init__.py | 1 + mcp_tools/command_executor/__init__.py | 1 + mcp_tools/command_executor/server.py | 127 +++++++++++++++++++++ my_agent/__init__.py | 2 + my_agent/agent.py | 146 +++++++++++++++++++++++++ 7 files changed, 309 insertions(+) create mode 100644 .codebuddy/.gitignore create mode 100644 .gitignore create mode 100644 mcp_tools/__init__.py create mode 100644 mcp_tools/command_executor/__init__.py create mode 100644 mcp_tools/command_executor/server.py create mode 100644 my_agent/__init__.py create mode 100644 my_agent/agent.py diff --git a/.codebuddy/.gitignore b/.codebuddy/.gitignore new file mode 100644 index 0000000..9f4c740 --- /dev/null +++ b/.codebuddy/.gitignore @@ -0,0 +1 @@ +db/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ecc390d --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg-info/ +dist/ +build/ +*.egg + +# Virtual environment +.venv/ +venv/ +env/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Environment +.env +.env.local + +# OS +.DS_Store +Thumbs.db + +# Project specific +*.db \ No newline at end of file diff --git a/mcp_tools/__init__.py b/mcp_tools/__init__.py new file mode 100644 index 0000000..3675efb --- /dev/null +++ b/mcp_tools/__init__.py @@ -0,0 +1 @@ +# mcp_tools package diff --git a/mcp_tools/command_executor/__init__.py b/mcp_tools/command_executor/__init__.py new file mode 100644 index 0000000..f4b0530 --- /dev/null +++ b/mcp_tools/command_executor/__init__.py @@ -0,0 +1 @@ +# command_executor package diff --git a/mcp_tools/command_executor/server.py b/mcp_tools/command_executor/server.py new file mode 100644 index 0000000..1fa0d87 --- /dev/null +++ b/mcp_tools/command_executor/server.py @@ -0,0 +1,127 @@ +""" +终端命令执行 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()) \ No newline at end of file diff --git a/my_agent/__init__.py b/my_agent/__init__.py new file mode 100644 index 0000000..0c38a7a --- /dev/null +++ b/my_agent/__init__.py @@ -0,0 +1,2 @@ +# my_agent package +from . import agent \ No newline at end of file diff --git a/my_agent/agent.py b/my_agent/agent.py new file mode 100644 index 0000000..6e36801 --- /dev/null +++ b/my_agent/agent.py @@ -0,0 +1,146 @@ +from google.adk.agents import LlmAgent +from google.adk.models.lite_llm import LiteLlm +from google.adk.tools.mcp_tool.mcp_toolset import McpToolset +from google.adk.tools.mcp_tool.mcp_session_manager import StdioConnectionParams +from google.adk.tools.function_tool import FunctionTool +from mcp.client.stdio import StdioServerParameters +import os +import asyncio +from dotenv import load_dotenv + +# Load environment variables from .env file +load_dotenv() + +# --- 使用 vLLM 端点的智能体 --- +api_base_url = os.getenv("VLLM_API_BASE", "https://9router.aqroid.cn/v1") +model_name = os.getenv("VLLM_MODEL", "") +api_key = os.getenv("VLLM_API_KEY", "") + +# Agent 可访问的工作目录 +WORKSPACE_DIR = os.getenv("AGENT_WORKSPACE_DIR", r"D:\nzy\workspace_git") + + +# --- 文件系统 MCP 工具 --- +filesystem_mcp = McpToolset( + connection_params=StdioConnectionParams( + server_params=StdioServerParameters( + command="npx", + args=[ + "-y", + "@modelcontextprotocol/server-filesystem", + os.path.abspath(WORKSPACE_DIR), + ], + ), + timeout=30.0, + ), +) + + +# --- 终端命令执行工具(Python 原生,绕开 MCP 通信问题)--- +async def run_command(command: str, cwd: str = None, timeout: int = 300) -> str: + """ + 在终端中执行一条命令,返回输出结果。 + + Args: + command: 要执行的命令,如 'npm run build'、'python -m pytest' 等 + cwd: 命令执行的工作目录,默认使用 AGENT_WORKSPACE_DIR + timeout: 超时时间(秒),默认 300 + + Returns: + 命令执行结果(stdout + stderr + 状态) + """ + if not command: + return "错误:命令不能为空" + + work_dir = cwd or os.path.abspath(WORKSPACE_DIR) + if not os.path.isdir(work_dir): + return f"错误:工作目录不存在 {work_dir}" + + try: + proc = await asyncio.create_subprocess_shell( + command, + cwd=work_dir, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout_bytes, stderr_bytes = await asyncio.wait_for( + proc.communicate(), timeout=timeout + ) + except asyncio.TimeoutError: + proc.kill() + await proc.wait() + return f"命令执行超时({timeout}秒): {command}" + except Exception as e: + return f"命令执行出错: {e}" + + stdout = stdout_bytes.decode("utf-8", errors="replace") + stderr = stderr_bytes.decode("utf-8", errors="replace") + + parts = [] + if stdout: + parts.append(f"[stdout]\n{stdout}") + if stderr: + parts.append(f"[stderr]\n{stderr}") + + output = "\n".join(parts) if 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})" + return f"命令执行{status}\n{output}" + + +# 注册为 ADK 工具 +run_command_tool = FunctionTool(run_command) + + +root_agent = LlmAgent( + model=LiteLlm( + model=model_name, + api_base=api_base_url, + api_key=api_key if api_key else None, + custom_llm_provider="openai", + ), + name="dev_agent", + description="全栈开发子 Agent,可以读写文件、浏览目录、执行开发任务。", + instruction=( + "你是一个全栈开发子 Agent(Dev Agent),由主控调度执行具体的开发任务。\n" + "\n" + "## 工作流程\n" + "1. 先理解任务需求和项目上下文\n" + "2. 使用文件系统工具浏览项目结构、读取相关文件\n" + "3. 编写或修改代码\n" + "4. 使用 run_command 工具运行编译/构建/测试,确保代码可正常工作\n" + "5. 验证结果后,按指定格式报告完成情况\n" + "\n" + "## 工作边界\n" + "- 所有文件操作限定在分配的工作目录范围内\n" + "- 你拥有的工具:文件系统操作(读/写/列目录)、终端命令执行\n" + "- 你可以自主完成:代码编写、bug 修复、样式调整、接口修改、简单重构\n" + "- 需要上报的情况:\n" + " • 架构设计或重大技术选型决策\n" + " • 依赖包版本不兼容导致的编译/运行时错误(需要升级/降级依赖时)\n" + " • 工具调用异常、环境配置问题、命令超时等非代码问题\n" + " • 超出你能力范围或不确定的问题\n" + "\n" + "## 编译/构建守则\n" + "- 写完代码后,优先运行编译或构建命令验证\n" + "- 编译报错时,先判断错误类型:\n" + " • 代码语法/逻辑错误 → 自行修复后重试\n" + " • 依赖缺失或版本不兼容 → 上报,由主控决定处理方式\n" + " • 环境/工具问题 → 上报\n" + "- 连续修复 3 次仍无法通过编译时,上报当前状态和所有错误信息\n" + "- 只有编译通过后才算任务完成\n" + "\n" + "## 报告格式\n" + "完成任务后,结构化报告:\n" + "**状态**:成功 / 部分完成 / 失败(需上报)\n" + "**修改的文件**:列出所有修改的文件路径\n" + "**变更摘要**:简述做了什么\n" + "**验证结果**:编译/测试是否通过,如有警告需列出\n" + "**需要主控关注**:如有需要上报的问题,详细说明" + ), + tools=[filesystem_mcp, run_command_tool], +)