Skip to main content
Glama
exec_tools.py8.22 kB
"""命令执行工具 - exec. 学习 Claude Code 的 Bash 工具设计: - 支持权限过滤 - 强制超时限制(默认 2 分钟) - 资源限制(内存、CPU) 整合前:run_command 整合后:exec(支持 code/file/args) """ import shlex import uuid from pathlib import Path from typing import Any, Dict, List, Literal, Optional from mcp.server.fastmcp.utilities.logging import get_logger from ..command import CommandExecutor from ..operations import FileOperations from ..security import PathValidator logger = get_logger(__name__) async def exec_command( runtime: Literal["python"], command_executor: Optional[CommandExecutor], validator: PathValidator, operations: FileOperations, *, code: Optional[str] = None, file: Optional[str] = None, args: Optional[List[str]] = None, ) -> Dict[str, Any]: """执行 Python 代码或文件. Args: runtime: 运行时类型,目前仅支持 'python' command_executor: 命令执行器实例 validator: 路径验证器 operations: 文件操作实例 code: Python 代码字符串(与 file 二选一) file: Python 文件路径(虚拟路径,与 code 二选一) args: 命令行参数列表 Returns: { "success": bool, "exit_code": int, "stdout": str, "stderr": str, "execution_time": float, "timed_out": bool } Examples: # 执行代码 exec_command("python", executor, validator, ops, code="print(1+1)") # 执行文件 exec_command("python", executor, validator, ops, file="/script.py") # 带参数执行 exec_command("python", executor, validator, ops, file="/process.py", args=["input.csv", "output.csv"]) Raises: ValueError: 如果参数不合法 RuntimeError: 如果命令执行器不可用 """ logger.info("=" * 80) logger.info(f"exec_command ENTRY: runtime={runtime}, code={bool(code)}, file={file!r}, args={args}") logger.info(f"exec_command: validator.virtual_root={validator.virtual_root}") logger.info(f"exec_command: validator.allowed_dirs={validator.allowed_dirs}") if runtime != "python": raise ValueError(f"Unsupported runtime: {runtime}. Only 'python' is supported.") if command_executor is None: raise RuntimeError( "Command executor not available. " "Make sure you're using session mode with a workspace." ) # 参数验证:code 和 file 必须提供且只能提供一个 if not code and not file: raise ValueError("Must provide either 'code' or 'file' parameter") if code and file: raise ValueError("Cannot provide both 'code' and 'file' parameters") # 如果提供了 file,读取文件内容 code_content = code if file: logger.info(f"exec_command: Processing file parameter: {file!r}") logger.info(f"exec_command: Calling validator.validate_path({file!r})...") abs_path, allowed = await validator.validate_path(file) logger.info(f"exec_command: validate_path returned:") logger.info(f" - abs_path: {abs_path}") logger.info(f" - abs_path type: {type(abs_path)}") logger.info(f" - allowed: {allowed}") logger.info(f" - abs_path.exists(): {abs_path.exists()}") if abs_path.exists(): logger.info(f" - abs_path.is_file(): {abs_path.is_file()}") logger.info(f" - abs_path.stat(): {abs_path.stat()}") if not allowed: error_msg = f"File path not allowed: {file}" logger.error(f"exec_command: {error_msg}") raise ValueError(error_msg) if not abs_path.exists(): error_msg = f"File not found: {file} (resolved to: {abs_path})" logger.error(f"exec_command: {error_msg}") raise ValueError(error_msg) # 关键修复:传入虚拟路径字符串,而不是真实路径 Path 对象 # operations.read_file 内部会再次调用 validate_path logger.info(f"exec_command: Calling operations.read_file({file!r})...") code_content = await operations.read_file(file) # 传入虚拟路径! logger.info(f"exec_command: Successfully read file, size={len(code_content)} bytes") logger.info(f"exec_command: First 100 chars: {code_content[:100]!r}") else: logger.info(f"exec_command: Using code parameter, size={len(code)} bytes") # 准备参数列表 args_list = args or [] logger.info(f"exec_command: args_list={args_list}") # 构建执行命令 # 使用临时文件方式,这样可以正确传递 args 且不需要复杂的 shell 转义 temp_file_name = f".exec_{uuid.uuid4().hex[:8]}.py" logger.info(f"exec_command: temp_file_name={temp_file_name}") try: # 写入临时文件(使用文件名作为虚拟路径,自动落在 workspace 根目录) logger.info(f"exec_command: Calling operations.write_file({temp_file_name!r}, code_content)...") await operations.write_file(temp_file_name, code_content) logger.info(f"exec_command: Successfully created temporary file: {temp_file_name}") # 验证临时文件是否真的创建了 temp_abs_path, temp_allowed = await validator.validate_path(temp_file_name) logger.info(f"exec_command: Verified temp file: abs_path={temp_abs_path}, exists={temp_abs_path.exists()}") # 构建命令:python <temp_file> arg1 arg2 ... cmd_parts = ["python", temp_file_name] cmd_parts.extend(args_list) # 使用 shlex.join 安全地构建命令(Python 3.8+) python_cmd = shlex.join(cmd_parts) # 从配置中获取超时时间 timeout = command_executor.default_timeout logger.info(f"exec_command: Executing command: {python_cmd}") logger.info(f"exec_command: Working dir: /") logger.info(f"exec_command: Timeout: {timeout}s (from config)") # 执行命令(使用配置中的超时时间) result = await command_executor.execute( command=python_cmd, timeout=timeout, working_dir="/", # 在 workspace 根目录执行 ) logger.info(f"exec_command: Execution completed:") logger.info(f" - success: {result.success}") logger.info(f" - exit_code: {result.exit_code}") logger.info(f" - stdout length: {len(result.stdout)}") logger.info(f" - stderr length: {len(result.stderr)}") return { "success": result.success, "runtime": runtime, "exit_code": result.exit_code, "stdout": result.stdout, "stderr": result.stderr, "execution_time": result.execution_time, "timed_out": result.timed_out, "error": result.error, } except Exception as e: logger.error(f"exec_command: Exception during execution: {type(e).__name__}: {e}") logger.exception("Full traceback:") return { "success": False, "runtime": runtime, "exit_code": -1, "stdout": "", "stderr": str(e), "execution_time": 0, "timed_out": False, "error": str(e), } finally: # 清理临时文件(使用虚拟路径) logger.info(f"exec_command: Cleaning up temporary file: {temp_file_name}") try: # 获取临时文件的真实路径来检查是否存在 temp_abs_path, _ = await validator.validate_path(temp_file_name) logger.info(f"exec_command: Temp file abs_path={temp_abs_path}, exists={temp_abs_path.exists()}") if temp_abs_path.exists(): temp_abs_path.unlink() logger.info(f"exec_command: Cleaned up temporary file: {temp_file_name}") else: logger.warning(f"exec_command: Temp file doesn't exist, skipping cleanup") except Exception as e: logger.warning(f"exec_command: Failed to clean up temporary file: {e}")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/answerlink/MCP-Workspace-Server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server