c9a99f4fb3
代码修复: - web/agent/agent.py: datetime.utcnow() → datetime.now(timezone.utc) - requirements.txt: 移除 paramiko==3.5.0 (已无活跃引用) - 删除 server/infrastructure/ssh/pool.py (DEPRECATED, 无引用) 连接池三层对齐 (config.py/.env.example/session.py): - DB_POOL_SIZE 100→160, DB_MAX_OVERFLOW 100→120 - 基于 MySQL max_connections=400, install.php 公式 文档修复 (21项): - P0: 硬编码域名/IP替换为配置变量占位符 - P1: 10个过时设计文档加归档标注 (引用旧文件结构) - P2: step-3-webssh报告 paramiko引用修正 - 6份审查报告连接池参数勘误 100/100→160/120 - ECC安全报告 EC5 datetime.utcnow 标记已修复 - docs/README.md 文档索引重写 - docs/memory/mem_nexus_overview.md 移除硬编码凭证 - docs/project/tech-stack-inventory.md paramiko标记已移除 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
179 lines
7.0 KiB
Python
179 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Nexus MCP Server — 测试/运维工具集 (mcp 1.x API)
|
|
|
|
All paths read from NEXUS_DEPLOY_DIR env var (default: /opt/nexus).
|
|
"""
|
|
import json, os, re, subprocess
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
API_BASE = "http://127.0.0.1:8600"
|
|
API_KEY = ""
|
|
DEPLOY_DIR = os.environ.get("NEXUS_DEPLOY_DIR", "/opt/nexus")
|
|
|
|
def _load_db_creds():
|
|
"""Load DB credentials from .env file (never hardcode)"""
|
|
env_path = os.path.join(DEPLOY_DIR, ".env")
|
|
creds = {"user": "nexus", "pass": "", "db": "nexus"}
|
|
try:
|
|
if os.path.exists(env_path):
|
|
with open(env_path) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line.startswith("NEXUS_DATABASE_URL="):
|
|
url = line.split("=", 1)[1].strip()
|
|
m = re.match(r"mysql\+pymysql://([^:]+):([^@]+)@([^/]+)/(.+)", url)
|
|
if m:
|
|
creds["user"] = m.group(1)
|
|
creds["pass"] = m.group(2)
|
|
creds["db"] = m.group(4)
|
|
except Exception:
|
|
pass
|
|
return creds
|
|
|
|
server = FastMCP("nexus")
|
|
|
|
# ================================================================
|
|
# T1: API 调用
|
|
# ================================================================
|
|
@server.tool()
|
|
async def api_call(method: str = "GET", path: str = "/health", body: str = "{}"):
|
|
"""调用 Nexus Python API。method: GET/POST/PUT/DELETE, path: /api/servers/等, body: JSON字符串"""
|
|
import urllib.request
|
|
url = f"{API_BASE}{path}"
|
|
data = body.encode() if method in ("POST","PUT") else None
|
|
req = urllib.request.Request(url, data=data, method=method)
|
|
req.add_header("Content-Type", "application/json")
|
|
if API_KEY: req.add_header("X-API-Key", API_KEY)
|
|
try:
|
|
resp = urllib.request.urlopen(req, timeout=10)
|
|
return json.dumps(json.loads(resp.read()), indent=2, ensure_ascii=False)
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
# ================================================================
|
|
# T2: 数据库查询 (read-only, credentials from .env)
|
|
# ================================================================
|
|
@server.tool()
|
|
async def db_query(sql: str = "SELECT COUNT(*) FROM servers"):
|
|
"""直查 MySQL nexus 数据库。只读查询"""
|
|
sql_upper = sql.strip().upper()
|
|
if not sql_upper.startswith("SELECT") and not sql_upper.startswith("SHOW") and not sql_upper.startswith("DESCRIBE"):
|
|
return "Error: Only SELECT/SHOW/DESCRIBE queries allowed"
|
|
if ";" in sql.rstrip(";").strip() and len(sql.rstrip(";").strip().split(";")) > 1:
|
|
return "Error: Multiple statements not allowed"
|
|
try:
|
|
creds = _load_db_creds()
|
|
p = subprocess.run(
|
|
['mysql', f'-u{creds["user"]}', f'-p{creds["pass"]}', creds["db"], '-e', sql],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
return p.stdout if p.returncode == 0 else p.stderr
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
# ================================================================
|
|
# T3: 服务管理
|
|
# ================================================================
|
|
@server.tool()
|
|
async def service(action: str = "status"):
|
|
"""管理 Python 后端服务。action: status/start/stop/restart"""
|
|
try:
|
|
p = subprocess.run(['supervisorctl', action, 'nexus'], capture_output=True, text=True, timeout=15)
|
|
return p.stdout or p.stderr or f"nexus {action}: OK"
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
# ================================================================
|
|
# T4: 日志查看
|
|
# ================================================================
|
|
@server.tool()
|
|
async def logs(lines: int = 50):
|
|
"""查看 Python 后端日志。lines: 行数"""
|
|
try:
|
|
log_path = os.path.join(DEPLOY_DIR, "logs", "nexus.log")
|
|
p = subprocess.run(['tail', '-n', str(lines), log_path],
|
|
capture_output=True, text=True, timeout=10)
|
|
return p.stdout[-3000:]
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
# ================================================================
|
|
# T5: 运行测试
|
|
# ================================================================
|
|
@server.tool()
|
|
async def run_test():
|
|
"""运行 test_api.py 端到端测试"""
|
|
try:
|
|
p = subprocess.run(['python3', os.path.join(DEPLOY_DIR, 'tests', 'test_api.py')],
|
|
capture_output=True, text=True, timeout=60,
|
|
cwd=DEPLOY_DIR)
|
|
return p.stdout
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
# ================================================================
|
|
# T6: Git 日志
|
|
# ================================================================
|
|
@server.tool()
|
|
async def git_log(count: int = 10):
|
|
"""查看 project Git 提交历史"""
|
|
try:
|
|
p = subprocess.run(['git', 'log', f'-{count}', '--oneline'],
|
|
capture_output=True, text=True, timeout=5,
|
|
cwd=DEPLOY_DIR)
|
|
return p.stdout
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
# ================================================================
|
|
# T7: 部署同步
|
|
# ================================================================
|
|
@server.tool()
|
|
async def deploy():
|
|
"""同步最新代码到运行目录并重启服务"""
|
|
try:
|
|
cmds = [
|
|
f"cd {DEPLOY_DIR} && git fetch --all 2>&1 && git reset --hard origin/master 2>&1",
|
|
"supervisorctl restart nexus 2>&1",
|
|
]
|
|
result = []
|
|
for cmd in cmds:
|
|
p = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
|
|
result.append(p.stdout or p.stderr or "OK")
|
|
return "\n---\n".join(result)
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
# ================================================================
|
|
# T8: 配置查看
|
|
# ================================================================
|
|
@server.tool()
|
|
async def config_view():
|
|
"""查看当前非敏感配置(隐藏密码/密钥)"""
|
|
try:
|
|
with open(os.path.join(DEPLOY_DIR, '.env')) as f:
|
|
lines = []
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith('#'):
|
|
lines.append(line)
|
|
continue
|
|
for sensitive in ('DATABASE_URL', 'SECRET_KEY', 'API_KEY', 'ADMIN_PASSWORD',
|
|
'TELEGRAM_BOT_TOKEN', 'TELEGRAM_CHAT_ID', 'ENCRYPTION_KEY'):
|
|
if line.startswith(f'NEXUS_{sensitive}='):
|
|
key = line.split('=', 1)[0]
|
|
lines.append(f'{key}=***REDACTED***')
|
|
break
|
|
else:
|
|
lines.append(line)
|
|
return '\n'.join(lines)
|
|
except Exception as e:
|
|
return str(e)
|
|
|
|
# ================================================================
|
|
# Main
|
|
# ================================================================
|
|
if __name__ == "__main__":
|
|
import asyncio
|
|
asyncio.run(server.run_stdio_async())
|