release: nexus btpanel session fix and app-v2
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
#!/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():
|
||||
"""同步最新代码到运行目录并重启服务(需通过 pre-deploy 门控检查)"""
|
||||
# ── Pull latest code first ──
|
||||
try:
|
||||
pull = subprocess.run(
|
||||
f"cd {DEPLOY_DIR} && git fetch --all 2>&1 && git reset --hard origin/main 2>&1",
|
||||
shell=True, capture_output=True, text=True, timeout=30,
|
||||
)
|
||||
if pull.returncode != 0:
|
||||
return f"🚫 Git pull failed:\n\n{pull.stdout}\n{pull.stderr}"
|
||||
except Exception as e:
|
||||
return f"🚫 Git pull error: {e}"
|
||||
|
||||
# ── Gate Check (after pull so files are up to date) ──
|
||||
gate_script = os.path.join(DEPLOY_DIR, "deploy", "pre_deploy_check.sh")
|
||||
if os.path.exists(gate_script):
|
||||
try:
|
||||
g = subprocess.run(
|
||||
['bash', gate_script],
|
||||
capture_output=True, text=True, timeout=30,
|
||||
env={**os.environ, "NEXUS_DEPLOY_DIR": DEPLOY_DIR},
|
||||
)
|
||||
if g.returncode != 0:
|
||||
return f"🚫 DEPLOY BLOCKED — Pre-deploy gate check failed:\n\n{g.stdout}\n\nFix the blocked gates before deploying."
|
||||
except Exception as e:
|
||||
return f"🚫 Gate check error: {e}"
|
||||
else:
|
||||
# Gate script not on server yet — warn but allow (bootstrap scenario)
|
||||
pass
|
||||
|
||||
# ── Restart service ──
|
||||
try:
|
||||
p = subprocess.run("supervisorctl restart nexus 2>&1", shell=True, capture_output=True, text=True, timeout=30)
|
||||
return f"Pull:\n{pull.stdout}\n---\nRestart:\n{p.stdout or p.stderr or 'OK'}"
|
||||
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)
|
||||
|
||||
# ================================================================
|
||||
# T9: 门控日志
|
||||
# ================================================================
|
||||
@server.tool()
|
||||
async def gate_log(lines: int = 20):
|
||||
"""查看门控执行日志。lines: 显示最近N条记录"""
|
||||
try:
|
||||
log_path = os.path.join(DEPLOY_DIR, "deploy", "gate_log.jsonl")
|
||||
if not os.path.exists(log_path):
|
||||
return "No gate log found — no gate checks have been recorded yet"
|
||||
result = []
|
||||
with open(log_path, encoding="utf-8") as f:
|
||||
all_lines = f.readlines()
|
||||
for line in all_lines[-lines:]:
|
||||
d = json.loads(line)
|
||||
icon = {"PASS": "✅", "BLOCK": "🚫", "SKIP": "⏭️", "WARN": "⚠️"}.get(d["result"], "?")
|
||||
ts = d.get("ts", "")[:19]
|
||||
gate = d.get("gate", "?")
|
||||
detail = d.get("detail", "")
|
||||
result.append(f"{ts} {icon} {gate}: {detail}")
|
||||
return "\n".join(result) if result else "No entries"
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
# ================================================================
|
||||
# Main
|
||||
# ================================================================
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(server.run_stdio_async())
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Firefox Browser MCP — 控制 Firefox 进行 Web 测试"""
|
||||
import json, time, base64, asyncio
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
from selenium.webdriver.firefox.service import Service
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
server = Server("firefox-mcp")
|
||||
driver = None
|
||||
|
||||
def _get_driver():
|
||||
global driver
|
||||
if driver is None:
|
||||
opts = Options()
|
||||
opts.add_argument("--headless")
|
||||
opts.add_argument("--no-sandbox")
|
||||
opts.add_argument("--disable-dev-shm-usage")
|
||||
driver = webdriver.Firefox(options=opts)
|
||||
driver.set_page_load_timeout(30)
|
||||
return driver
|
||||
|
||||
# ================================================================
|
||||
# T1: 导航
|
||||
# ================================================================
|
||||
@server.tool()
|
||||
async def firefox_navigate(url: str = "https://192.168.124.10/app/login.html"):
|
||||
"""Firefox 打开指定 URL,返回页面标题和 URL"""
|
||||
try:
|
||||
d = _get_driver()
|
||||
d.get(url)
|
||||
return f"标题: {d.title}\nURL: {d.current_url}"
|
||||
except Exception as e:
|
||||
return f"导航失败: {e}"
|
||||
|
||||
# ================================================================
|
||||
# T2: 截图
|
||||
# ================================================================
|
||||
@server.tool()
|
||||
async def firefox_screenshot():
|
||||
"""截取当前页面截图,返回 base64 PNG"""
|
||||
try:
|
||||
d = _get_driver()
|
||||
png = d.get_screenshot_as_base64()
|
||||
return f"截图大小: {len(png)} bytes (base64). 可用工具查看。\n前100字符: {png[:100]}..."
|
||||
except Exception as e:
|
||||
return f"截图失败: {e}"
|
||||
|
||||
# ================================================================
|
||||
# T3: 获取页面文本
|
||||
# ================================================================
|
||||
@server.tool()
|
||||
async def firefox_get_text():
|
||||
"""获取当前页面可见文本内容(用于检查错误信息)"""
|
||||
try:
|
||||
d = _get_driver()
|
||||
body = d.find_element(By.TAG_NAME, "body")
|
||||
text = body.text[:3000]
|
||||
return text if text else "(空页面)"
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
# ================================================================
|
||||
# T4: 填充输入框并提交
|
||||
# ================================================================
|
||||
@server.tool()
|
||||
async def firefox_fill(selector: str = "input[name='username']", value: str = "admin"):
|
||||
"""在指定 CSS selector 的输入框中填入值"""
|
||||
try:
|
||||
d = _get_driver()
|
||||
el = d.find_element(By.CSS_SELECTOR, selector)
|
||||
el.clear()
|
||||
el.send_keys(value)
|
||||
return f"已填入 {selector} = {value}"
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
# ================================================================
|
||||
# T5: 点击
|
||||
# ================================================================
|
||||
@server.tool()
|
||||
async def firefox_click(selector: str = "button[type='submit']"):
|
||||
"""点击指定 CSS selector 的元素"""
|
||||
try:
|
||||
d = _get_driver()
|
||||
el = d.find_element(By.CSS_SELECTOR, selector)
|
||||
el.click()
|
||||
time.sleep(2)
|
||||
return f"已点击 {selector}。当前页面: {d.title} ({d.current_url})"
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
# ================================================================
|
||||
# T6: 控制台日志
|
||||
# ================================================================
|
||||
@server.tool()
|
||||
async def firefox_console():
|
||||
"""获取浏览器控制台日志"""
|
||||
try:
|
||||
d = _get_driver()
|
||||
logs = d.get_log("browser")
|
||||
entries = [f"[{e['level']}] {e['message'][:200]}" for e in logs[-20:]]
|
||||
return "\n".join(entries) if entries else "(无日志)"
|
||||
except Exception as e:
|
||||
return str(e)
|
||||
|
||||
# ================================================================
|
||||
# Main
|
||||
# ================================================================
|
||||
async def main():
|
||||
async with stdio_server() as (read, write):
|
||||
await server.run(read, write, server.create_initialization_options())
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,29 @@
|
||||
"""MCP Bridge — launches remote MCP server via SSH and bridges stdio
|
||||
|
||||
Configure via environment variables:
|
||||
NEXUS_SSH_KEY — path to SSH private key (default: ~/.ssh/id_rsa)
|
||||
NEXUS_SSH_HOST — remote server hostname/IP (required)
|
||||
NEXUS_DEPLOY_DIR — remote installation path (default: /opt/nexus)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ssh_key = os.environ.get("NEXUS_SSH_KEY", str(Path.home() / ".ssh" / "id_rsa"))
|
||||
ssh_host = os.environ.get("NEXUS_SSH_HOST", "")
|
||||
deploy_dir = os.environ.get("NEXUS_DEPLOY_DIR", "/opt/nexus")
|
||||
|
||||
if not ssh_host:
|
||||
print("ERROR: NEXUS_SSH_HOST not set (e.g. root@1.2.3.4)", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
ssh = subprocess.Popen([
|
||||
"ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-i", ssh_key,
|
||||
ssh_host,
|
||||
"python3", f"{deploy_dir}/mcp/Nexus_server.py"
|
||||
], stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
|
||||
|
||||
sys.exit(ssh.wait())
|
||||
Reference in New Issue
Block a user