Files
mcp-agent-tools/Main Agent Tools.py
2026-08-28 11:44:49 +08:00

1578 lines
64 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
import subprocess
import asyncio
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import Optional, Dict
import json
import platform
import re
from datetime import datetime
import tempfile
import uuid
import shlex
import threading
import time
import paramiko
import base64
import secrets
# ========== 禁用 __pycache__ ==========
os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
sys.dont_write_bytecode = True
# ========== 命令行确认/输入支持 ==========
def is_windows():
return platform.system() == "Windows"
MB_YESNO = 0x04
MB_YESNOCANCEL = 0x03
MB_OKCANCEL = 0x01
MB_OK = 0x00
MB_ICONQUESTION = 0x20
MB_ICONWARNING = 0x30
MB_ICONINFORMATION = 0x40
MB_ICONERROR = 0x10
IDYES = 6
IDNO = 7
IDCANCEL = 2
IDOK = 1
def show_confirm_dialog(title: str, message: str, buttons: int = MB_YESNO, icon: int = MB_ICONQUESTION) -> int:
"""命令行确认(纯 input,不使用 GUI 弹窗)"""
print(f"\n{'='*50}")
print(f"🔔 {title}")
print(f"{'='*50}")
print(f"📝 {message}")
print(f"{'='*50}")
if buttons == MB_YESNO:
print("请选择: [y] 是 [n] 否")
while True:
answer = input("> ").strip().lower()
if answer in ['y', 'yes']:
return IDYES
elif answer in ['n', 'no']:
return IDNO
else:
print("请输入 y 或 n")
elif buttons == MB_YESNOCANCEL:
print("请选择: [y] 是 [n] 否 [c] 取消")
while True:
answer = input("> ").strip().lower()
if answer in ['y', 'yes']:
return IDYES
elif answer in ['n', 'no']:
return IDNO
elif answer in ['c', 'cancel']:
return IDCANCEL
else:
print("请输入 y、n 或 c")
elif buttons == MB_OKCANCEL:
print("请选择: [o] 确定 [c] 取消")
while True:
answer = input("> ").strip().lower()
if answer in ['o', 'ok', '确定']:
return IDOK
elif answer in ['c', 'cancel']:
return IDCANCEL
else:
print("请输入 o 或 c")
else:
print("按 Enter 继续...")
input()
return IDOK
return IDNO
def show_input_dialog(title: str, message: str, default: str = "") -> Optional[str]:
print(f"\n{'='*50}")
print(f"📝 {title}")
print(f"{'='*50}")
print(f"💬 {message}")
if default:
print(f"📌 默认值: {default}")
result = input("请输入: ").strip()
return result if result else default
def input_text(title: str, message: str, default: str = "") -> Optional[str]:
"""命令行输入(纯 input,不使用 GUI 弹窗)"""
return show_input_dialog(title, message, default)
# ========== 配置 ==========
print("=" * 60)
print("🚀 MCP CMD + SSH 服务器启动 (v3.0.0 - 严格模式)")
print("=" * 60)
def get_initial_workspace():
workspace_input = input("📂 请输入工作区目录路径: ").strip()
return workspace_input
workspace = get_initial_workspace()
workspace = os.path.abspath(workspace)
if not os.path.exists(workspace):
print(f"❌ 错误:目录 '{workspace}' 不存在!")
if is_windows():
show_confirm_dialog("错误", f"目录 '{workspace}' 不存在!", buttons=MB_OK, icon=MB_ICONERROR)
exit(1)
if not os.path.isdir(workspace):
print(f"❌ 错误:'{workspace}' 不是目录!")
if is_windows():
show_confirm_dialog("错误", f"'{workspace}' 不是目录!", buttons=MB_OK, icon=MB_ICONERROR)
exit(1)
temp_dir = os.path.join(workspace, "temp")
os.makedirs(temp_dir, exist_ok=True)
print(f"✅ 临时目录:{temp_dir}")
print(f"✅ 工作区已设置为:{workspace}")
print(f"🖥️ 操作系统:{platform.system()}")
print("=" * 60)
# ========== FastAPI 应用 ==========
app = FastAPI(title="MCP CMD + SSH Server", version="3.0.0")
# ========== 全局变量 ==========
current_workspace = workspace
current_temp_dir = temp_dir
# ========== 🔐 SSH 会话管理 ==========
class SSHConnectionPool:
def __init__(self):
self.sessions: Dict[str, dict] = {}
self.tokens: Dict[str, str] = {}
self.session_counter = 0
def create_session(self, host: str, username: str, password: str = None,
key_file: str = None, port: int = 22) -> tuple:
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
if key_file:
client.connect(
host, port=port, username=username,
key_filename=key_file,
timeout=15,
allow_agent=False,
look_for_keys=False
)
else:
if not password:
return None, None, False, "❌ 密码或密钥文件必须提供其一"
client.connect(
host, port=port, username=username,
password=password,
timeout=15,
allow_agent=False,
look_for_keys=False
)
channel = client.invoke_shell(
term='xterm',
width=200,
height=50
)
time.sleep(1)
output = ""
while channel.recv_ready():
output += channel.recv(1024).decode('utf-8', errors='ignore')
self.session_counter += 1
session_id = f"ssh_{self.session_counter}_{int(time.time())}"
token = secrets.token_urlsafe(32)
self.sessions[token] = {
'id': session_id,
'client': client,
'channel': channel,
'host': host,
'username': username,
'port': port,
'created_at': time.time(),
'last_used': time.time(),
'buffer': output,
'is_alive': True
}
self.tokens[token] = session_id
print(f"✅ SSH会话创建: {session_id} -> {username}@{host}:{port}")
return token, session_id, True, f"✅ SSH连接成功!会话ID: {session_id}"
except Exception as e:
print(f"❌ SSH连接失败: {e}")
return None, None, False, f"❌ SSH连接失败: {str(e)}"
def get_session(self, token: str) -> Optional[dict]:
if token not in self.sessions:
return None
session = self.sessions[token]
transport = session['client'].get_transport()
if not session['is_alive'] or transport is None or not transport.is_active():
self.close_session(token)
return None
session['last_used'] = time.time()
return session
def execute_command(self, token: str, command: str, timeout: int = 30) -> dict:
session = self.get_session(token)
if not session:
return {
'success': False,
'output': '',
'error': '❌ SSH会话已过期或不存在,请重新连接',
'session_expired': True
}
try:
channel = session['channel']
# 清空缓冲区
self._flush_buffer(channel)
# 发送命令
channel.send(command + "\n")
time.sleep(0.2)
output = ""
start_time = time.time()
while time.time() - start_time < timeout:
if channel.recv_ready():
data = channel.recv(65535).decode('utf-8', errors='ignore')
output += data
if re.search(r'[$#>]', output[-30:]):
break
else:
time.sleep(0.05)
session['buffer'] = output
return {
'success': True,
'output': output,
'error': '',
'session_id': session['id']
}
except Exception as e:
print(f"❌ SSH命令执行失败: {e}")
return {
'success': False,
'output': '',
'error': f'❌ 命令执行失败: {str(e)}',
'session_expired': False
}
def _flush_buffer(self, channel):
while channel.recv_ready():
channel.recv(1024)
def close_session(self, token: str) -> bool:
if token in self.sessions:
session = self.sessions[token]
try:
session['client'].close()
except:
pass
session['is_alive'] = False
del self.sessions[token]
if token in self.tokens:
del self.tokens[token]
print(f"🗑️ SSH会话已关闭: {token[:16]}...")
return True
return False
def list_sessions(self) -> list:
result = []
for token, session in self.sessions.items():
transport = session['client'].get_transport()
if session['is_alive'] and transport is not None and transport.is_active():
result.append({
'token': token[:16] + '...',
'session_id': session['id'],
'host': session['host'],
'username': session['username'],
'created_at': datetime.fromtimestamp(session['created_at']).strftime('%Y-%m-%d %H:%M:%S'),
'last_used': datetime.fromtimestamp(session['last_used']).strftime('%Y-%m-%d %H:%M:%S')
})
return result
def cleanup_expired(self, max_age: int = 3600):
now = time.time()
to_remove = []
for token, session in self.sessions.items():
if now - session['last_used'] > max_age:
to_remove.append(token)
for token in to_remove:
self.close_session(token)
return len(to_remove)
# ========== 全局SSH连接池 ==========
ssh_pool = SSHConnectionPool()
# ========== 数据模型 ==========
class SSHConnectRequest(BaseModel):
host: str
username: str
port: int = 22
class SSHExecuteRequest(BaseModel):
token: str
command: str
purpose: str # 必须50-100字说明用途
timeout: int = 60
# ========== 安全函数 ==========
def ask_user_permission_ui(title: str, message: str, purpose: str = "") -> bool:
"""命令行确认(不用 GUI 弹窗),输入 y 确认 / n 拒绝"""
full_message = message
if purpose:
full_message += f"\n\n📝 用途说明:\n{purpose}\n"
print(f"\n{'='*55}")
print(f"🔐 {title}")
print(f"{'='*55}")
print(full_message)
print(f"{'='*55}")
while True:
answer = input("> 确认请输 y,拒绝输 n [y/n]: ").strip().lower()
if answer in ('y', 'yes', '是'):
return True
elif answer in ('n', 'no', '否'):
return False
else:
print("请输入 y 或 n")
def is_path_in_workspace(target_path: str) -> bool:
global current_workspace
try:
target_abs = os.path.abspath(target_path)
workspace_abs = os.path.abspath(current_workspace)
return os.path.commonpath([target_abs, workspace_abs]) == workspace_abs
except ValueError:
return False
def get_safe_cwd(request_cwd: Optional[str]) -> str:
global current_workspace
if not request_cwd:
return current_workspace
request_cwd = os.path.abspath(request_cwd)
if is_path_in_workspace(request_cwd):
return request_cwd
if ask_user_permission_ui("目录访问确认", f"命令试图在工作区外的目录执行:\n\n📂 工作区:{current_workspace}\n🎯 目标目录:{request_cwd}\n\n是否允许?"):
return request_cwd
else:
print(f" ❌ 用户拒绝,使用工作区目录:{current_workspace}")
return current_workspace
# ========== 命令执行函数 ==========
def fix_command_quoting(command: str) -> tuple:
is_windows_os = platform.system() == "Windows"
if not is_windows_os:
return command, False, "非 Windows 系统,无需修复"
original = command
fixed = command
modifications = []
if 'python -c' in command or 'python -c "' in command:
if '\n' in command and 'python -c' in command:
match = re.search(r'python\s+-c\s+(["\'])(.*?)\1', command, re.DOTALL)
if match:
quote_char = match.group(1)
code_content = match.group(2)
if '\n' in code_content:
lines = [line.strip() for line in code_content.split('\n') if line.strip()]
code_lines = []
for line in lines:
if line.startswith('#'):
continue
if '#' in line and not (line.count("'") >= 2 or line.count('"') >= 2):
parts = line.split('#')
if len(parts) > 1 and not (parts[0].count("'") % 2 == 1 or parts[0].count('"') % 2 == 1):
line = parts[0].strip()
if line:
code_lines.append(line)
if code_lines:
single_line = '; '.join(code_lines)
if quote_char == '"':
fixed_code = single_line.replace('"', '\\"')
else:
fixed_code = single_line
fixed = command.replace(match.group(0), f'python -c {quote_char}{fixed_code}{quote_char}')
modifications.append(f"将多行代码压缩为单行({len(lines)}行 -> 1行)")
if 'python -c' in fixed:
match = re.search(r'python\s+-c\s+(["\'])(.*?)\1', fixed, re.DOTALL)
if match:
outer_quote = match.group(1)
inner_content = match.group(2)
if outer_quote == '"' and '"' in inner_content and '\\"' not in inner_content:
inner_content = inner_content.replace('"', '\\"')
fixed = fixed.replace(match.group(0), f'python -c "{inner_content}"')
modifications.append("转义内部双引号")
was_modified = original != fixed
fix_message = "; ".join(modifications) if modifications else "无需修复"
return fixed, was_modified, fix_message
def execute_command_safe(command: str, cwd: str, timeout: int = 60) -> dict:
try:
is_windows_os = platform.system() == "Windows"
fixed_command, was_modified, fix_message = fix_command_quoting(command)
if was_modified:
print(f" 🔧 自动修复命令引号问题")
print(f" 📝 修复说明:{fix_message}")
env = os.environ.copy()
env['PYTHONIOENCODING'] = 'utf-8'
env['PYTHONUTF8'] = '1'
if is_windows_os:
shell_command = f'cmd /c "chcp 65001 >nul && set PYTHONIOENCODING=utf-8 && {fixed_command}"'
else:
shell_command = fixed_command
process = subprocess.Popen(
shell_command,
cwd=cwd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8',
errors='replace',
bufsize=1,
universal_newlines=True,
env=env
)
try:
stdout, stderr = process.communicate(timeout=timeout)
return_code = process.returncode
except subprocess.TimeoutExpired:
process.kill()
stdout, stderr = process.communicate()
return {
"success": False,
"output": stdout or "",
"error": f"⏰ 命令执行超时({timeout}秒)",
"return_code": -1,
"cwd": cwd,
"command": command
}
output = stdout or ""
error = stderr or ""
success = return_code == 0
if not success and "Argument expected for the -c option" in error:
return {
"success": False,
"output": output,
"error": error,
"return_code": return_code,
"cwd": cwd,
"command": command,
"suggestion": "💡 建议使用 python_runner 工具"
}
if not output and error:
if "warning" not in error.lower() and "error" not in error.lower():
output = error
error = ""
return {
"success": success,
"output": output,
"error": error,
"return_code": return_code,
"cwd": cwd,
"command": command
}
except Exception as e:
return {
"success": False,
"output": "",
"error": f"❌ 执行失败:{str(e)}",
"return_code": -1,
"cwd": cwd,
"command": command
}
def create_and_run_python(code: str, timeout: int = 60, cwd: Optional[str] = None) -> dict:
global current_temp_dir, current_workspace
try:
file_id = uuid.uuid4().hex[:8]
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"temp_{timestamp}_{file_id}.py"
filepath = os.path.join(current_temp_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(code)
exec_cwd = cwd or current_workspace
python_cmd = f'python "{filepath}"'
result = execute_command_safe(python_cmd, exec_cwd, timeout)
result['file_created'] = filepath
result['file_name'] = filename
return result
except Exception as e:
return {
"success": False,
"output": "",
"error": f"❌ 创建或执行失败:{str(e)}",
"return_code": -1,
"cwd": cwd or current_workspace,
"command": code[:100] + "..."
}
def switch_workspace(new_workspace: str) -> dict:
global current_workspace, current_temp_dir
new_workspace = os.path.abspath(new_workspace)
if not os.path.exists(new_workspace):
return {"success": False, "error": f"目录 '{new_workspace}' 不存在!"}
if not os.path.isdir(new_workspace):
return {"success": False, "error": f"'{new_workspace}' 不是目录!"}
old_workspace = current_workspace
message = f"确认切换工作区?\n\n📂 当前工作区:{old_workspace}\n📂 新工作区:{new_workspace}"
result = show_confirm_dialog("🔄 切换工作区", message, buttons=MB_YESNO, icon=MB_ICONQUESTION)
if result != IDYES:
return {"success": False, "error": "用户取消切换", "current_workspace": current_workspace}
current_workspace = new_workspace
current_temp_dir = os.path.join(current_workspace, "temp")
os.makedirs(current_temp_dir, exist_ok=True)
return {
"success": True,
"old_workspace": old_workspace,
"new_workspace": current_workspace,
"temp_dir": current_temp_dir,
"message": f"✅ 成功切换到 {current_workspace}"
}
# ========== MCP 核心端点 ==========
@app.post("/mcp")
async def mcp_handler(request: Request):
global current_workspace, current_temp_dir
try:
body = await request.json()
method = body.get("method")
params = body.get("params", {})
request_id = body.get("id")
if method == "initialize":
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"protocolVersion": "1.0.0",
"serverInfo": {
"name": "mcp-cmd-ssh-server",
"version": "3.0.0"
},
"capabilities": {"tools": {}}
}
}
elif method == "tools/list":
tools_response = {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"tools": [
{
"name": "command_executor",
"description": f"执行系统命令。默认在工作区 '{current_workspace}' 执行。",
"inputSchema": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "要执行的命令"},
"cwd": {"type": "string", "description": f"可选,执行目录。默认工作区 '{current_workspace}'"},
"timeout": {"type": "integer", "description": "可选,超时时间(秒),默认 60", "default": 60}
},
"required": ["command"]
}
},
{
"name": "python_runner",
"description": "创建临时 Python 文件并执行。",
"inputSchema": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "Python 代码内容"},
"cwd": {"type": "string", "description": f"可选,执行目录。默认工作区 '{current_workspace}'"},
"timeout": {"type": "integer", "description": "可选,超时时间(秒),默认 60", "default": 60}
},
"required": ["code"]
}
},
{
"name": "ssh_connect",
"description": "🔐 发起SSH连接请求。用户需手动输入密码,成功返回token。",
"inputSchema": {
"type": "object",
"properties": {
"host": {"type": "string", "description": "SSH服务器地址"},
"username": {"type": "string", "description": "SSH用户名"},
"port": {"type": "integer", "description": "SSH端口,默认22", "default": 22}
},
"required": ["host", "username"]
}
},
{
"name": "ssh_execute",
"description": "🔐 在SSH会话中执行命令。⚠️ 必须提供50-100字的用途说明,每次执行都需要用户命令行确认。",
"inputSchema": {
"type": "object",
"properties": {
"token": {"type": "string", "description": "SSH连接返回的token"},
"command": {"type": "string", "description": "要执行的命令"},
"purpose": {"type": "string", "description": "⚠️ 用途说明(必须50-100字),详细说明为什么执行此命令、预期效果、涉及的操作"},
"timeout": {"type": "integer", "description": "可选,超时时间(秒),默认60", "default": 60}
},
"required": ["token", "command", "purpose"]
}
},
{
"name": "ssh_close",
"description": "🔐 关闭SSH会话",
"inputSchema": {
"type": "object",
"properties": {
"token": {"type": "string", "description": "SSH连接返回的token"}
},
"required": ["token"]
}
},
{
"name": "ssh_list_sessions",
"description": "🔐 列出所有活跃的SSH会话",
"inputSchema": {"type": "object", "properties": {}}
},
{
"name": "file_reader",
"description": "读取工作区内文件的内容",
"inputSchema": {
"type": "object",
"properties": {
"filepath": {"type": "string", "description": "文件路径"},
"encoding": {"type": "string", "description": "文件编码,默认 utf-8", "default": "utf-8"}
},
"required": ["filepath"]
}
},
{
"name": "file_writer",
"description": "在工作区内创建或修改文件",
"inputSchema": {
"type": "object",
"properties": {
"filepath": {"type": "string", "description": "文件路径"},
"content": {"type": "string", "description": "要写入的内容"},
"mode": {"type": "string", "description": "写入模式:'w' 覆盖,'a' 追加", "enum": ["w", "a"], "default": "w"}
},
"required": ["filepath", "content"]
}
},
{
"name": "directory_lister",
"description": "列出指定目录的内容",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": f"目录路径。默认工作区 '{current_workspace}'"},
"show_hidden": {"type": "boolean", "description": "是否显示隐藏文件,默认 false", "default": False}
}
}
},
{
"name": "file_searcher",
"description": "在工作区内搜索文件",
"inputSchema": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "搜索模式(如 '*.txt')"},
"search_root": {"type": "string", "description": f"搜索根目录。默认工作区 '{current_workspace}'"},
"recursive": {"type": "boolean", "description": "是否递归搜索,默认 true", "default": True}
},
"required": ["pattern"]
}
},
{
"name": "git_operations",
"description": "在工作区内执行 Git 操作",
"inputSchema": {
"type": "object",
"properties": {
"git_command": {"type": "string", "description": "Git 命令"},
"repo_path": {"type": "string", "description": f"Git 仓库路径。默认工作区 '{current_workspace}'"}
},
"required": ["git_command"]
}
},
{
"name": "switch_workspace",
"description": f"🔄 切换工作区。当前:'{current_workspace}'",
"inputSchema": {
"type": "object",
"properties": {
"new_workspace": {"type": "string", "description": "新的工作区目录路径"}
},
"required": ["new_workspace"]
}
},
{
"name": "get_current_workspace",
"description": "📂 获取当前工作区信息",
"inputSchema": {"type": "object", "properties": {}}
}
]
}
}
print(f"\n📤 返回工具列表,共 {len(tools_response['result']['tools'])} 个工具")
return tools_response
elif method == "tools/call":
tool_name = params.get("name")
arguments = params.get("arguments", {})
# ========== command_executor ==========
if tool_name == "command_executor":
command = arguments.get("command")
cwd = arguments.get("cwd")
timeout = arguments.get("timeout", 60)
if not command:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 command 参数"}
}
safe_cwd = get_safe_cwd(cwd)
result = execute_command_safe(command, safe_cwd, timeout)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]
}
}
# ========== python_runner ==========
elif tool_name == "python_runner":
code = arguments.get("code")
cwd = arguments.get("cwd")
timeout = arguments.get("timeout", 60)
if not code:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 code 参数"}
}
safe_cwd = get_safe_cwd(cwd)
result = create_and_run_python(code, timeout, safe_cwd)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]
}
}
# ========== 🔐 ssh_connect ==========
elif tool_name == "ssh_connect":
host = arguments.get("host")
username = arguments.get("username")
port = arguments.get("port", 22)
if not host or not username:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 host 或 username 参数"}
}
# 命令行询问密码(使用 input,不用 GUI 弹窗)
print(f"\n🔐 请输入 {username}@{host}:{port} 的 SSH 密码: ", end="", flush=True)
password = input().strip()
if not password:
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"message": "❌ 用户未输入密码,连接取消"
}, ensure_ascii=False, indent=2)
}]
}
}
token, session_id, success, message = ssh_pool.create_session(
host, username, password, port=port
)
result = {
"success": success,
"message": message,
"token": token if success else None,
"session_id": session_id if success else None,
"host": host,
"username": username,
"port": port
}
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]
}
}
# ========== 🔐 ssh_execute(严格模式) ==========
elif tool_name == "ssh_execute":
token = arguments.get("token")
command = arguments.get("command")
purpose = arguments.get("purpose", "")
timeout = arguments.get("timeout", 60)
if not token or not command:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 token 或 command 参数"}
}
# ========== 严格检查:用途必须50-100字 ==========
purpose_len = len(purpose)
if purpose_len < 50:
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 用途说明字数不足!当前 {purpose_len} 字,必须 50-100 字。",
"requirement": "请提供至少50字的详细用途说明,包括:为什么要执行此命令、预期达到什么效果、涉及哪些操作和风险。",
"current_purpose": purpose,
"purpose_length": purpose_len,
"minimum_required": 50,
"maximum_required": 100
}, ensure_ascii=False, indent=2)
}]
}
}
if purpose_len > 100:
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 用途说明超长!当前 {purpose_len} 字,必须 50-100 字。",
"requirement": "请精简用途说明到100字以内,保持清晰简洁。",
"current_purpose": purpose,
"purpose_length": purpose_len,
"minimum_required": 50,
"maximum_required": 100
}, ensure_ascii=False, indent=2)
}]
}
}
# 获取会话信息
session = ssh_pool.get_session(token)
if not session:
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": "❌ SSH会话已过期或不存在,请重新连接",
"token": token[:16] + "..."
}, ensure_ascii=False, indent=2)
}]
}
}
# ========== 🔐 用户确认(命令行 input) ==========
confirm_message = (
f"SSH会话:{session['username']}@{session['host']}:{session['port']}\n"
f"会话ID:{session['id']}\n\n"
f"📝 用途说明({purpose_len}字):\n{purpose}\n\n"
f"💻 命令:\n{command[:300]}{'...' if len(command) > 300 else ''}\n\n"
f"⚠️ 请确认此操作是您期望的,且了解可能的风险。"
)
if not ask_user_permission_ui("SSH命令执行确认", confirm_message):
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": "❌ 用户拒绝执行SSH命令",
"command": command,
"purpose": purpose,
"purpose_length": purpose_len
}, ensure_ascii=False, indent=2)
}]
}
}
# 执行命令
result = ssh_pool.execute_command(token, command, timeout)
result['purpose'] = purpose
result['purpose_length'] = purpose_len
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]
}
}
# ========== 🔐 ssh_close ==========
elif tool_name == "ssh_close":
token = arguments.get("token")
if not token:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 token 参数"}
}
success = ssh_pool.close_session(token)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": success,
"message": "✅ SSH会话已关闭" if success else "❌ 会话不存在或已关闭",
"token": token[:16] + "..."
}, ensure_ascii=False, indent=2)
}]
}
}
# ========== 🔐 ssh_list_sessions ==========
elif tool_name == "ssh_list_sessions":
sessions = ssh_pool.list_sessions()
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": True,
"total": len(sessions),
"sessions": sessions
}, ensure_ascii=False, indent=2)
}]
}
}
# ========== file_reader ==========
elif tool_name == "file_reader":
filepath = arguments.get("filepath")
encoding = arguments.get("encoding", "utf-8")
if not filepath:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 filepath 参数"}
}
if os.path.isabs(filepath):
target_path = filepath
else:
target_path = os.path.join(current_workspace, filepath)
target_path = os.path.abspath(target_path)
if not is_path_in_workspace(target_path):
if not ask_user_permission_ui("文件读取确认", f"读取工作区外的文件:\n📂 文件:{target_path}\n\n是否允许?"):
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 用户拒绝访问:{target_path}"
}, ensure_ascii=False, indent=2)
}]
}
}
try:
with open(target_path, 'r', encoding=encoding) as f:
content = f.read()
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": True,
"content": content,
"filepath": target_path,
"size": len(content)
}, ensure_ascii=False, indent=2)
}]
}
}
except Exception as e:
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 读取失败:{str(e)}",
"filepath": target_path
}, ensure_ascii=False, indent=2)
}]
}
}
# ========== file_writer ==========
elif tool_name == "file_writer":
filepath = arguments.get("filepath")
content = arguments.get("content")
mode = arguments.get("mode", "w")
if not filepath or content is None:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 filepath 或 content 参数"}
}
if os.path.isabs(filepath):
target_path = filepath
else:
target_path = os.path.join(current_workspace, filepath)
target_path = os.path.abspath(target_path)
if not is_path_in_workspace(target_path):
if not ask_user_permission_ui("文件写入确认", f"写入工作区外的文件:\n📂 文件:{target_path}\n\n是否允许?"):
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 用户拒绝写入:{target_path}"
}, ensure_ascii=False, indent=2)
}]
}
}
try:
os.makedirs(os.path.dirname(target_path), exist_ok=True)
with open(target_path, mode, encoding='utf-8') as f:
f.write(content)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": True,
"filepath": target_path,
"mode": mode,
"size": len(content),
"message": f"✅ 文件已{'追加' if mode == 'a' else '写入'}"
}, ensure_ascii=False, indent=2)
}]
}
}
except Exception as e:
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 写入失败:{str(e)}",
"filepath": target_path
}, ensure_ascii=False, indent=2)
}]
}
}
# ========== directory_lister ==========
elif tool_name == "directory_lister":
path = arguments.get("path", current_workspace)
show_hidden = arguments.get("show_hidden", False)
if os.path.isabs(path):
target_path = path
else:
target_path = os.path.join(current_workspace, path)
target_path = os.path.abspath(target_path)
if not is_path_in_workspace(target_path):
if not ask_user_permission_ui("目录列表确认", f"列出工作区外的目录:\n📂 目录:{target_path}\n\n是否允许?"):
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 用户拒绝访问:{target_path}"
}, ensure_ascii=False, indent=2)
}]
}
}
try:
items = []
for item in os.listdir(target_path):
if not show_hidden and item.startswith('.'):
continue
full_path = os.path.join(target_path, item)
is_dir = os.path.isdir(full_path)
size = os.path.getsize(full_path) if not is_dir else 0
mtime = datetime.fromtimestamp(os.path.getmtime(full_path)).strftime("%Y-%m-%d %H:%M:%S")
items.append({
"name": item,
"type": "目录" if is_dir else "文件",
"size": size,
"modified": mtime,
"path": full_path
})
items.sort(key=lambda x: (x['type'] != '目录', x['name'].lower()))
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": True,
"path": target_path,
"items": items,
"count": len(items)
}, ensure_ascii=False, indent=2)
}]
}
}
except Exception as e:
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 列出目录失败:{str(e)}",
"path": target_path
}, ensure_ascii=False, indent=2)
}]
}
}
# ========== file_searcher ==========
elif tool_name == "file_searcher":
pattern = arguments.get("pattern")
search_root = arguments.get("search_root", current_workspace)
recursive = arguments.get("recursive", True)
if not pattern:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 pattern 参数"}
}
if os.path.isabs(search_root):
root_path = search_root
else:
root_path = os.path.join(current_workspace, search_root)
root_path = os.path.abspath(root_path)
if not is_path_in_workspace(root_path):
if not ask_user_permission_ui("文件搜索确认", f"在工作区外搜索文件:\n📂 目录:{root_path}\n\n是否允许?"):
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 用户拒绝搜索:{root_path}"
}, ensure_ascii=False, indent=2)
}]
}
}
try:
matches = []
search_pattern = pattern.replace('*', '.*').replace('?', '.')
regex = re.compile(search_pattern, re.IGNORECASE)
if recursive:
for dirpath, dirnames, filenames in os.walk(root_path):
dirnames[:] = [d for d in dirnames if not d.startswith('.')]
for filename in filenames:
if regex.match(filename):
full_path = os.path.join(dirpath, filename)
matches.append(full_path)
else:
for item in os.listdir(root_path):
if regex.match(item):
full_path = os.path.join(root_path, item)
if os.path.isfile(full_path):
matches.append(full_path)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": True,
"pattern": pattern,
"search_root": root_path,
"recursive": recursive,
"matches": matches,
"count": len(matches)
}, ensure_ascii=False, indent=2)
}]
}
}
except Exception as e:
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 搜索失败:{str(e)}",
"pattern": pattern
}, ensure_ascii=False, indent=2)
}]
}
}
# ========== git_operations ==========
elif tool_name == "git_operations":
git_command = arguments.get("git_command")
repo_path = arguments.get("repo_path", current_workspace)
if not git_command:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 git_command 参数"}
}
if os.path.isabs(repo_path):
target_path = repo_path
else:
target_path = os.path.join(current_workspace, repo_path)
target_path = os.path.abspath(target_path)
if not is_path_in_workspace(target_path):
if not ask_user_permission_ui("Git操作确认", f"在工作区外执行Git操作:\n📂 仓库:{target_path}\n\n是否允许?"):
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{
"type": "text",
"text": json.dumps({
"success": False,
"error": f"❌ 用户拒绝 Git 操作:{target_path}"
}, ensure_ascii=False, indent=2)
}]
}
}
full_command = f"git {git_command}"
result = execute_command_safe(full_command, target_path)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]
}
}
# ========== switch_workspace ==========
elif tool_name == "switch_workspace":
new_workspace = arguments.get("new_workspace")
if not new_workspace:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32602, "message": "❌ 缺少 new_workspace 参数"}
}
result = switch_workspace(new_workspace)
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]
}
}
# ========== get_current_workspace ==========
elif tool_name == "get_current_workspace":
result = {
"workspace": current_workspace,
"temp_dir": current_temp_dir,
"os": platform.system(),
"version": "3.0.0"
}
return {
"jsonrpc": "2.0",
"id": request_id,
"result": {
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, indent=2)}]
}
}
else:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32601, "message": f"❌ 未知工具:{tool_name}"}
}
else:
return {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32601, "message": f"❌ 未知方法:{method}"}
}
except Exception as e:
print(f"\n❌ 服务器错误:{str(e)}")
return {
"jsonrpc": "2.0",
"id": body.get("id", None),
"error": {"code": -32603, "message": f"❌ 内部服务器错误:{str(e)}"}
}
# ========== 辅助API ==========
@app.post("/switch_workspace")
async def switch_workspace_api(request: Request):
global current_workspace, current_temp_dir
try:
body = await request.json()
new_workspace = body.get("workspace", "").strip()
if not new_workspace:
return JSONResponse({"success": False, "error": "请提供 workspace 参数"})
result = switch_workspace(new_workspace)
return JSONResponse(result)
except Exception as e:
return JSONResponse({"success": False, "error": f"切换失败:{str(e)}"})
@app.get("/current_workspace")
async def get_current_workspace_api():
global current_workspace, current_temp_dir
return {
"workspace": current_workspace,
"temp_dir": current_temp_dir,
"os": platform.system(),
"version": "3.0.0"
}
@app.get("/ssh/sessions")
async def list_ssh_sessions():
"""列出所有SSH会话(管理用)"""
sessions = ssh_pool.list_sessions()
return {
"success": True,
"total": len(sessions),
"sessions": sessions
}
@app.post("/ssh/cleanup")
async def cleanup_ssh_sessions():
"""清理过期SSH会话"""
count = ssh_pool.cleanup_expired()
return {
"success": True,
"cleaned": count,
"message": f"✅ 清理了 {count} 个过期会话"
}
@app.get("/health")
async def health():
global current_workspace, current_temp_dir
sessions = ssh_pool.list_sessions()
return {
"status": "ok",
"workspace": current_workspace,
"temp_dir": current_temp_dir,
"os": platform.system(),
"version": "3.0.0",
"ssh_sessions": len(sessions),
"features": [
"SSH交互支持",
"严格50-100字用途说明",
"每次命令命令行确认",
"会话Token管理",
"自动清理过期会话"
]
}
@app.get("/")
async def root():
global current_workspace, current_temp_dir
return {
"message": "🚀 MCP CMD + SSH Server v3.0.0 (严格模式)",
"workspace": current_workspace,
"temp_dir": current_temp_dir,
"endpoints": {
"mcp": "POST /mcp",
"health": "GET /health",
"current_workspace": "GET /current_workspace",
"switch_workspace": "POST /switch_workspace",
"ssh_sessions": "GET /ssh/sessions",
"ssh_cleanup": "POST /ssh/cleanup"
},
"ssh_features": {
"connect": "用户手动输入密码,返回token",
"execute": "必须50-100字用途说明 + 命令行确认",
"close": "关闭会话",
"list": "查看所有会话"
},
"strict_rules": {
"purpose_min_length": 50,
"purpose_max_length": 100,
"confirmation_required": True,
"per_command_confirmation": True
}
}
# ========== 启动服务器 ==========
if __name__ == "__main__":
import uvicorn
print(f"\n🔥 MCP + SSH 服务器启动中...")
print(f"📍 主端点:http://localhost:3000/mcp")
print(f"📂 工作区:{workspace}")
print(f"📁 临时目录:{temp_dir}")
print(f"\n🔐 SSH功能:")
print(f" - ssh_connect: 发起连接,用户手动输入密码")
print(f" - ssh_execute: 执行命令(⚠️ 必须50-100字用途说明 + 命令行确认)")
print(f" - ssh_close: 关闭会话")
print(f" - ssh_list_sessions: 列出会话")
print(f"\n📋 严格规则:")
print(f" - 用途说明:50-100字(不足50字拒绝,超过100字拒绝)")
print(f" - 每次执行必须命令行确认")
print(f" - 每个命令单独审批")
print(f"\n💡 按 Ctrl+C 停止服务器")
print("=" * 60)
uvicorn.run(app, host="0.0.0.0", port=3000, log_level="info")