Files
Nexus/mcp/multisync_server.py
T
2026-05-21 05:48:19 +08:00

174 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""Nexus MCP Server — 测试/运维工具集 (mcp 1.x API)"""
import json, os, re, subprocess
from mcp.server.fastmcp import FastMCP
API_BASE = "http://127.0.0.1:8600"
API_KEY = ""
def _load_db_creds():
"""Load DB credentials from .env file (never hardcode)"""
env_path = "/www/wwwroot/api.synaglobal.vip/.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-mcp")
# ================================================================
# 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 multisync 数据库。只读查询"""
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(['systemctl', action, 'multisync'], capture_output=True, text=True, timeout=15)
return p.stdout or p.stderr or f"multisync {action}: OK"
except Exception as e:
return str(e)
# ================================================================
# T4: 日志查看
# ================================================================
@server.tool()
async def logs(lines: int = 50):
"""查看 Python 后端日志。lines: 行数"""
try:
p = subprocess.run(['journalctl', '-u', 'multisync', '-n', str(lines), '--no-pager'],
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', '/www/wwwroot/api.synaglobal.vip/tests/test_api.py'],
capture_output=True, text=True, timeout=60,
cwd='/www/wwwroot/api.synaglobal.vip')
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='/www/wwwroot/api.synaglobal.vip')
return p.stdout
except Exception as e:
return str(e)
# ================================================================
# T7: 部署同步
# ================================================================
@server.tool()
async def deploy():
"""同步最新代码到运行目录并重启服务"""
try:
cmds = [
"cd /www/wwwroot/api.synaglobal.vip && git fetch --all 2>&1 && git reset --hard origin/master 2>&1",
"systemctl restart multisync 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('/www/wwwroot/api.synaglobal.vip/.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())