From f5143aee09859d0ef00d150a277fa4312c2a457d Mon Sep 17 00:00:00 2001 From: qihongkun Date: Mon, 25 May 2026 14:21:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Docker=20=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=94=AF=E6=8C=81=E5=92=8C=20MCP=20=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Dockerfile、docker-compose.yml 和 Jenkins 流水线配置 - 新增 MCP 原生集成模块 (mcp_native.py) - 移除旧的 mcp_bridge.py,更新依赖和文档 - 添加部署文档 (Docker 和服务器部署指南) Co-authored-by: Cursor --- .dockerignore | 35 +++ .env.example | 26 ++ AGENTS.md | 6 +- Dockerfile | 41 ++++ Jenkinsfile | 180 ++++++++++++++ README.md | 58 ++++- app/database.py | 4 +- app/main.py | 34 ++- app/mcp_native.py | 158 ++++++++++++ app/services/ssh_runner.py | 3 +- docker-compose.yml | 33 +++ docker/entrypoint.sh | 6 + docs/deploy_docker.md | 242 +++++++++++++++++++ docs/deploy_server.md | 481 +++++++++++++++++++++++++++++++++++++ docs/mcp_install.md | 351 +++++++++++++++------------ docs/mcp_quickstart.md | 208 ++++++++++++++-- docs/mcp_tools_for_ai.md | 41 ++-- mcp_bridge.py | 185 -------------- requirements.txt | 1 + start_project.command | 32 ++- 20 files changed, 1722 insertions(+), 403 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 Jenkinsfile create mode 100644 app/mcp_native.py create mode 100644 docker-compose.yml create mode 100644 docker/entrypoint.sh create mode 100644 docs/deploy_docker.md create mode 100644 docs/deploy_server.md delete mode 100644 mcp_bridge.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8e8fa1d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +.git +.gitignore +.venv +venv +__pycache__ +*.py[cod] +.pytest_cache +.mypy_cache +.ruff_cache + +ai_test.db +*.db +*.db-journal +*.db-wal +*.db-shm + +frontend-admin/node_modules +frontend-admin/dist +frontend-admin/.vite + +.env +.env.* +!.env.example + +.cursor +.serena +.idea +.vscode +.run_logs +*.log + +docs/ +*.md +!README.md +start_project.command diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0b8c5b9 --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# 复制为 .env 后修改:cp .env.example .env + +# 映射到宿主机的端口 +QIP_HOST_PORT=8000 + +# 持久化数据目录(SQLite、SSH 脚本等),挂载到容器 /data +QIP_DATA_HOST_DIR=/docker_data + +# 生产务必修改 +AUTH_TOKEN_SECRET=change-me-to-a-long-random-string +DEFAULT_SUPERADMIN_USERNAME=admin +DEFAULT_SUPERADMIN_PASSWORD=change-me-strong-password +DEFAULT_SUPERADMIN_DISPLAY_NAME=系统超管 + +# MCP 鉴权(建议 true) +MCP_REQUIRE_API_KEY=true + +# 从宿主机 / 其它机器用 Cursor 连 MCP 时建议 true +QIP_MCP_DISABLE_DNS_PROTECTION=true +# QIP_MCP_ALLOWED_HOSTS=10.0.0.5:8000,qip.internal:8000 +# QIP_MCP_ALLOWED_ORIGINS=http://10.0.0.5:8000 + +# 可选:Loki +# GENERAL_LOKI_EXPLORE_URL=https://grafana.example/explore +# LOKI_DATASOURCE=Loki +# LOKI_ORG_ID=1 diff --git a/AGENTS.md b/AGENTS.md index 49d7718..62c91c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ ## 快速规则 -- MCP 服务名:`quality-inspection-platform`(stdio 桥 `mcp_bridge.py`) +- MCP 服务名:`quality-inspection-platform`(Streamable HTTP:`http://<主机>:8000/mcp`,需 `sto-` API Key) - 写操作与执行类工具须 `sto-` API Key - 任务开始:`catalog_snapshot` - 跑批:`workflow_batch_create` → `workflow_batch_update` → `workflow_batch_run` @@ -18,5 +18,5 @@ ## 其它文档 -- 安装桥接:`docs/mcp_install.md` -- 人类调用示例:`docs/mcp_quickstart.md` +- MCP 安装与 Cursor 配置:`docs/mcp_install.md` +- 人类调用示例(curl):`docs/mcp_quickstart.md` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7bf2b66 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM node:20-bookworm-slim AS frontend-build + +WORKDIR /build/frontend-admin +COPY frontend-admin/package.json frontend-admin/package-lock.json* ./ +RUN npm install + +COPY frontend-admin/ ./ +RUN npm run build + + +FROM python:3.11-slim-bookworm + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app ./app +COPY --from=frontend-build /build/frontend-admin/dist ./frontend-admin/dist + +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + QIP_DATA_DIR=/data \ + DATABASE_URL=sqlite:////data/ai_test.db + +EXPOSE 8000 + +VOLUME ["/data"] + +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')" || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..bd1a656 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,180 @@ +pipeline { + agent any + + environment { + DEPLOY_DIR = '/opt/quality-inspection-platform' + DATA_DIR = '/docker_data' + } + + parameters { + string(name: 'DEPLOY_HOST', defaultValue: '', description: '部署目标服务器 SSH 地址,如 root@10.0.0.5') + string(name: 'SSH_PORT', defaultValue: '22', description: 'SSH 端口') + string(name: 'SSH_CREDENTIALS', defaultValue: 'deploy-ssh-key', description: 'Jenkins 凭据 ID(SSH 私钥或用户名密码)') + choice(name: 'ACTION', choices: ['deploy', 'restart', 'stop', 'logs'], description: '执行动作') + } + + options { + timeout(time: 30, unit: 'MINUTES') + disableConcurrentBuilds() + buildDiscarder(logRotator(numToKeepStr: '20')) + } + + stages { + + // ────────────────────────────────────── + // 1. 拉取代码 + // ────────────────────────────────────── + stage('Checkout') { + steps { + checkout scm + script { + env.GIT_COMMIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim() + env.GIT_BRANCH_NAME = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim() + } + echo "分支: ${env.GIT_BRANCH_NAME} 提交: ${env.GIT_COMMIT_SHORT}" + } + } + + // ────────────────────────────────────── + // 2. 推送源码到目标服务器 + // ────────────────────────────────────── + stage('Sync Source') { + when { expression { params.ACTION == 'deploy' } } + steps { + sshagent(credentials: [params.SSH_CREDENTIALS]) { + sh """ + ssh -p ${params.SSH_PORT} -o StrictHostKeyChecking=no ${params.DEPLOY_HOST} \ + 'mkdir -p ${DEPLOY_DIR}' + + rsync -avz --delete \ + --exclude '.git' \ + --exclude '.venv' \ + --exclude '__pycache__' \ + --exclude 'node_modules' \ + --exclude '*.pyc' \ + --exclude '.env' \ + -e "ssh -p ${params.SSH_PORT} -o StrictHostKeyChecking=no" \ + ./ ${params.DEPLOY_HOST}:${DEPLOY_DIR}/ + """ + } + echo '源码同步完成' + } + } + + // ────────────────────────────────────── + // 3. 远程服务器 build + 启动 + // ────────────────────────────────────── + stage('Remote Build & Start') { + when { expression { params.ACTION == 'deploy' } } + steps { + sshagent(credentials: [params.SSH_CREDENTIALS]) { + sh """ + ssh -p ${params.SSH_PORT} -o StrictHostKeyChecking=no ${params.DEPLOY_HOST} ' + set -eu + cd ${DEPLOY_DIR} + + # 确保 .env 存在(首次部署从 example 复制) + if [ ! -f .env ]; then + cp .env.example .env + echo "首次部署:已从 .env.example 创建 .env,请稍后修改密码和密钥" + fi + + # 确保数据目录存在 + DATA_HOST_DIR=\$(grep "^QIP_DATA_HOST_DIR=" .env | cut -d= -f2 || echo "/docker_data") + [ -z "\$DATA_HOST_DIR" ] && DATA_HOST_DIR="/docker_data" + mkdir -p "\$DATA_HOST_DIR" + + # 备份数据库 + if [ -f "\$DATA_HOST_DIR/ai_test.db" ]; then + cp "\$DATA_HOST_DIR/ai_test.db" \ + "\$DATA_HOST_DIR/ai_test.db.bak.\$(date +%Y%m%d%H%M%S)" + echo "数据库已备份" + fi + + # 构建镜像(使用 BuildKit 加速) + DOCKER_BUILDKIT=1 docker compose build --pull + + # 重启容器 + docker compose down --remove-orphans + docker compose up -d + + echo "等待容器启动..." + sleep 20 + + # 健康检查 + PORT=\$(grep "^QIP_HOST_PORT=" .env | cut -d= -f2 || echo "8000") + [ -z "\$PORT" ] && PORT="8000" + STATUS=\$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:\$PORT/health || true) + if [ "\$STATUS" = "200" ]; then + echo "✓ 部署成功,健康检查通过 (HTTP 200)" + else + echo "✗ 健康检查未通过 (HTTP \$STATUS),查看日志:" + docker compose logs --tail 30 + exit 1 + fi + ' + """ + } + } + } + + // ────────────────────────────────────── + // 4. 仅重启(不重新 build) + // ────────────────────────────────────── + stage('Restart Only') { + when { expression { params.ACTION == 'restart' } } + steps { + sshagent(credentials: [params.SSH_CREDENTIALS]) { + sh """ + ssh -p ${params.SSH_PORT} -o StrictHostKeyChecking=no ${params.DEPLOY_HOST} ' + cd ${DEPLOY_DIR} + docker compose restart + sleep 10 + curl -sf http://127.0.0.1:\$(grep "^QIP_HOST_PORT=" .env | cut -d= -f2 || echo 8000)/health \ + && echo "重启成功" || echo "重启后健康检查失败" + ' + """ + } + } + } + + // ────────────────────────────────────── + // 5. 停止 + // ────────────────────────────────────── + stage('Stop') { + when { expression { params.ACTION == 'stop' } } + steps { + sshagent(credentials: [params.SSH_CREDENTIALS]) { + sh """ + ssh -p ${params.SSH_PORT} -o StrictHostKeyChecking=no ${params.DEPLOY_HOST} ' + cd ${DEPLOY_DIR} && docker compose down + echo "容器已停止" + ' + """ + } + } + } + + // ────────────────────────────────────── + // 6. 查看日志 + // ────────────────────────────────────── + stage('View Logs') { + when { expression { params.ACTION == 'logs' } } + steps { + sshagent(credentials: [params.SSH_CREDENTIALS]) { + sh """ + ssh -p ${params.SSH_PORT} -o StrictHostKeyChecking=no ${params.DEPLOY_HOST} ' + cd ${DEPLOY_DIR} && docker compose logs --tail 100 + ' + """ + } + } + } + } + + post { + success { echo "✓ 流水线执行成功: ${params.ACTION}" } + failure { echo "✗ 流水线执行失败,请检查日志" } + always { cleanWs() } + } +} diff --git a/README.md b/README.md index b3c1cef..1d22d78 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - Mock 数据管理 - Workflow 流程编排与执行 -此外,项目还带了一个 `stdio MCP bridge`,可以把平台里暴露的工具注册成真正的 MCP Server,供 Cursor、Claude Desktop、Codex 等客户端调用。 +此外,平台内置 **Streamable HTTP MCP Server**(`/mcp`),供 Cursor、Claude Desktop、Codex 等客户端直接连接,无需单独桥接进程。 ## 技术栈 @@ -32,13 +32,29 @@ app/ templates/ Jinja2 页面模板 docs/ mcp_tools_for_ai.md MCP 能力说明(AI Agent 首选) - mcp_install.md MCP bridge 安装说明 + mcp_install.md MCP Server 安装说明(Cursor / HTTP) mcp_quickstart.md MCP 调用说明(人类 / curl) AGENTS.md Codex 等自动加载的项目 Agent 约定 -mcp_bridge.py stdio MCP Server 桥接入口 +app/mcp_native.py 内置 Streamable HTTP MCP(挂载于 /mcp) +start_project.command macOS 一键启动前后端(双击或终端执行) requirements.txt Python 依赖 ``` +## 服务器部署 + +| 方式 | 文档 | +|------|------| +| **Docker Compose(推荐,尤其 CentOS 7)** | [`docs/deploy_docker.md`](./docs/deploy_docker.md) | +| 裸机 systemd + Nginx | [`docs/deploy_server.md`](./docs/deploy_server.md) | + +快速启动: + +```bash +cp .env.example .env # 修改密码与密钥 +docker compose build +docker compose up -d +``` + ## 安装依赖 建议先进入项目目录,再安装依赖: @@ -61,7 +77,8 @@ python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload - 首页:`http://127.0.0.1:8000/` - API 文档:`http://127.0.0.1:8000/docs` -- MCP 工具列表:`http://127.0.0.1:8000/mcp/tools` +- MCP 协议端点:`http://127.0.0.1:8000/mcp` +- MCP 工具列表(HTTP 兼容):`http://127.0.0.1:8000/mcp/tools` ### 方式二:在 PyCharm 里启动 @@ -73,20 +90,37 @@ python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload 注意:`app.main:app` 必须放在 `uvicorn` 后面作为位置参数传入,不能写到最后。 -## 启动 MCP Bridge +### 方式三:macOS 快速启动(推荐) -如果你要把本平台作为 MCP Server 暴露给其他客户端,再额外启动: +项目根目录有 `start_project.command`,会分别打开两个 Terminal 窗口启动后端(8000)与前端(5173),就绪后自动打开浏览器。 ```bash -python3 mcp_bridge.py +# 在项目根目录执行(脚本会自动 pip install;前端需已 npm install) +./start_project.command ``` -桥接默认读取环境变量: +首次使用若前端未安装依赖:`cd frontend-admin && npm install` -- `QIP_BASE_URL`,默认值是 `http://127.0.0.1:8000`(兼容旧名 `AI_TEST_BASE_URL`) -- `QIP_API_KEY`:`sto-` 开头的 API Key(兼容 `AI_TEST_API_KEY`) +也可在 Finder 中双击该文件。脚本使用相对路径,不依赖旧目录名;若你曾在桌面/程序坞放了快捷方式,请把目标路径改为: -完整配置见: +`/Users/qihongkun/work/My_app/quality-inspection-platform/start_project.command` + +## 配置 MCP 客户端(Cursor 等) + +后端启动后,在 `~/.cursor/mcp.json` 中配置 URL 与 API Key(无需额外进程): + +```json +{ + "mcpServers": { + "quality-inspection-platform": { + "url": "http://127.0.0.1:8000/mcp", + "headers": { "Authorization": "Bearer sto-你的API密钥" } + } + } +} +``` + +完整说明(含内网部署、团队协作、Remote SSH、排障)见: - [`docs/mcp_install.md`](./docs/mcp_install.md) @@ -101,7 +135,7 @@ AI 使用 MCP 前请阅读: rg --files rg "workflow_run" app docs python -m uvicorn app.main:app --host 127.0.0.1 --port 8000 --reload -python3 -m py_compile app/main.py app/services/engine.py mcp_bridge.py +python3 -m py_compile app/main.py app/services/engine.py app/mcp_native.py ``` ## 当前已知情况 diff --git a/app/database.py b/app/database.py index 1f741cc..f7202dd 100644 --- a/app/database.py +++ b/app/database.py @@ -1,7 +1,9 @@ +import os + from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base, sessionmaker -DATABASE_URL = "sqlite:///./ai_test.db" +DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./ai_test.db") engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/app/main.py b/app/main.py index 760cfe6..0efc171 100644 --- a/app/main.py +++ b/app/main.py @@ -391,7 +391,10 @@ def _ensure_default_superadmin(): _ensure_sqlite_columns() _ensure_default_superadmin() -app = FastAPI(title="质量检测平台", version="0.1.0") +from .mcp_native import mcp_lifespan, mount_mcp_streamable_http + +app = FastAPI(title="质量检测平台", version="0.1.0", lifespan=mcp_lifespan) +mount_mcp_streamable_http(app) FRONTEND_DIST_DIR = Path(__file__).resolve().parent.parent / "frontend-admin" / "dist" FRONTEND_ASSETS_DIR = FRONTEND_DIST_DIR / "assets" FRONTEND_INDEX_FILE = FRONTEND_DIST_DIR / "index.html" @@ -1725,16 +1728,12 @@ def mcp_tools(request: Request, db: Session = Depends(get_db)): } -@app.post("/mcp/invoke", response_model=McpInvokeResponse) -async def mcp_invoke(payload: McpInvokeRequest, request: Request, db: Session = Depends(get_db)): - tool = payload.tool - args = payload.arguments or {} - actor = _resolve_mcp_actor( - request, - db, - api_key=payload.api_key, - require_credential=_mcp_tool_requires_credential(tool), - ) +async def _dispatch_mcp_tool( + tool: str, + args: dict[str, Any], + db: Session, + actor: User, +) -> McpInvokeResponse: try: if tool == "folder_ensure": target = str(args["target"]) @@ -2063,6 +2062,19 @@ async def mcp_invoke(payload: McpInvokeRequest, request: Request, db: Session = return McpInvokeResponse(ok=False, tool=tool, error=str(exc)) +@app.post("/mcp/invoke", response_model=McpInvokeResponse) +async def mcp_invoke(payload: McpInvokeRequest, request: Request, db: Session = Depends(get_db)): + tool = payload.tool + args = payload.arguments or {} + actor = _resolve_mcp_actor( + request, + db, + api_key=payload.api_key, + require_credential=_mcp_tool_requires_credential(tool), + ) + return await _dispatch_mcp_tool(tool, args, db, actor) + + @app.post("/mcp/invoke-batch") async def mcp_invoke_batch(payload: McpInvokeBatchRequest, request: Request, db: Session = Depends(get_db)): outputs: list[dict[str, Any]] = [] diff --git a/app/mcp_native.py b/app/mcp_native.py new file mode 100644 index 0000000..5e63232 --- /dev/null +++ b/app/mcp_native.py @@ -0,0 +1,158 @@ +"""Native MCP Server (Streamable HTTP) mounted on the FastAPI app.""" + +from __future__ import annotations + +import json +import logging +import os +from contextlib import asynccontextmanager +from typing import Any + +from fastapi import FastAPI, HTTPException, Request +from mcp import types +from mcp.server.lowlevel.server import Server +from mcp.server.streamable_http_manager import StreamableHTTPSessionManager +from mcp.server.transport_security import TransportSecuritySettings +from sqlalchemy.orm import Session +from starlette.requests import Request as StarletteRequest + +from .database import SessionLocal + +logger = logging.getLogger(__name__) + +SERVER_NAME = "quality-inspection-platform" +SERVER_VERSION = "0.2.0" +MCP_INSTRUCTIONS = ( + "Quality inspection platform (质量检测平台). Before complex tasks, read project file " + "docs/mcp_tools_for_ai.md (tool picker, auth, recipes). " + "Typical start: catalog_snapshot. " + "Batch runs: workflow_batch_create → workflow_batch_update → workflow_batch_run. " + "Writes and execution require sto- API Key (Authorization: Bearer or X-API-Key)." +) + +_mcp_server = Server(SERVER_NAME, version=SERVER_VERSION, instructions=MCP_INSTRUCTIONS) +_session_manager: StreamableHTTPSessionManager | None = None + + +def _mcp_transport_security() -> TransportSecuritySettings | None: + if os.getenv("QIP_MCP_DISABLE_DNS_PROTECTION", "").strip().lower() in {"1", "true", "yes", "on"}: + return TransportSecuritySettings(enable_dns_rebinding_protection=False) + hosts = [item.strip() for item in os.getenv("QIP_MCP_ALLOWED_HOSTS", "").split(",") if item.strip()] + origins = [item.strip() for item in os.getenv("QIP_MCP_ALLOWED_ORIGINS", "").split(",") if item.strip()] + if not hosts and not origins: + return None + return TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=hosts, + allowed_origins=origins, + ) + + +def _tool_specs() -> list[dict[str, Any]]: + from .main import MCP_TOOL_SPECS + + return MCP_TOOL_SPECS + + +def _specs_to_mcp_tools() -> list[types.Tool]: + tools: list[types.Tool] = [] + for spec in _tool_specs(): + schema = spec.get("input_schema") or {"type": "object"} + tools.append( + types.Tool( + name=str(spec.get("name", "")), + description=str(spec.get("description", "")), + inputSchema=schema, + ) + ) + return tools + + +def _starlette_request() -> StarletteRequest | None: + ctx = _mcp_server.request_context + request = ctx.request + return request if isinstance(request, StarletteRequest) else None + + +def _resolve_actor_for_tool(tool: str, request: StarletteRequest, db: Session): + from .main import _mcp_global_require_api_key, _mcp_tool_requires_credential, _resolve_mcp_actor + + require = _mcp_global_require_api_key() or _mcp_tool_requires_credential(tool) + return _resolve_mcp_actor(request, db, require_credential=require) + + +@_mcp_server.list_tools() +async def _handle_list_tools() -> list[types.Tool]: + request = _starlette_request() + if request is None: + return _specs_to_mcp_tools() + db = SessionLocal() + try: + from .main import _mcp_global_require_api_key + + if _mcp_global_require_api_key(): + _resolve_actor_for_tool("catalog_snapshot", request, db) + return _specs_to_mcp_tools() + finally: + db.close() + + +@_mcp_server.call_tool() +async def _handle_call_tool( + name: str, + arguments: dict[str, Any] | None, +) -> types.CallToolResult: + request = _starlette_request() + if request is None: + raise ValueError("MCP request context missing HTTP request") + + db = SessionLocal() + try: + try: + actor = _resolve_actor_for_tool(name, request, db) + from .main import _dispatch_mcp_tool + + result = await _dispatch_mcp_tool(name, arguments or {}, db, actor) + except HTTPException as exc: + payload = {"ok": False, "tool": name, "data": {}, "error": str(exc.detail)} + return types.CallToolResult( + content=[types.TextContent(type="text", text=json.dumps(payload, ensure_ascii=False, indent=2))], + isError=True, + ) + text = json.dumps(result.model_dump(), ensure_ascii=False, indent=2) + return types.CallToolResult( + content=[types.TextContent(type="text", text=text)], + isError=not result.ok, + ) + finally: + db.close() + + +def _get_session_manager() -> StreamableHTTPSessionManager: + global _session_manager + if _session_manager is None: + _session_manager = StreamableHTTPSessionManager( + app=_mcp_server, + json_response=False, + stateless=True, + security_settings=_mcp_transport_security(), + ) + return _session_manager + + +@asynccontextmanager +async def mcp_lifespan(_app: FastAPI): + manager = _get_session_manager() + async with manager.run(): + yield + + +def mount_mcp_streamable_http(fastapi_app: FastAPI, *, path: str = "/mcp") -> None: + """Register Streamable HTTP MCP endpoint on the FastAPI app.""" + manager = _get_session_manager() + + @fastapi_app.api_route(path, methods=["GET", "POST", "DELETE"], include_in_schema=False) + async def mcp_streamable_http_endpoint(request: Request) -> None: + await manager.handle_request(request.scope, request.receive, request._send) # type: ignore[attr-defined] + + logger.info("MCP Streamable HTTP mounted at %s (stateless)", path) diff --git a/app/services/ssh_runner.py b/app/services/ssh_runner.py index 8bef91b..f99be0b 100644 --- a/app/services/ssh_runner.py +++ b/app/services/ssh_runner.py @@ -12,7 +12,8 @@ import paramiko from ..schemas import SshProfileOut, SshScriptOut PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent -SSH_SCRIPTS_DIR = PROJECT_ROOT / "data" / "ssh-scripts" +_DATA_ROOT = Path(os.getenv("QIP_DATA_DIR", str(PROJECT_ROOT / "data"))) +SSH_SCRIPTS_DIR = _DATA_ROOT / "ssh-scripts" def resolve_script_body(script: SshScriptOut) -> str: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e2aad0b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + quality-inspection-platform: + build: + context: . + dockerfile: Dockerfile + image: quality-inspection-platform:latest + container_name: quality-inspection-platform + restart: unless-stopped + ports: + - "${QIP_HOST_PORT:-8000}:8000" + env_file: + - .env + environment: + DATABASE_URL: sqlite:////data/ai_test.db + QIP_DATA_DIR: /data + # 容器对外访问时 MCP 建议关闭 DNS 校验或配置白名单 + QIP_MCP_DISABLE_DNS_PROTECTION: ${QIP_MCP_DISABLE_DNS_PROTECTION:-true} + MCP_REQUIRE_API_KEY: ${MCP_REQUIRE_API_KEY:-true} + volumes: + # 宿主机目录(部署前执行: sudo mkdir -p /docker_data && sudo chown -R 1000:1000 /docker_data) + - ${QIP_DATA_HOST_DIR:-/docker_data}:/data + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health')", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 40s diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..a9a6133 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -eu + +mkdir -p /data/ssh-scripts + +exec "$@" diff --git a/docs/deploy_docker.md b/docs/deploy_docker.md new file mode 100644 index 0000000..2b53272 --- /dev/null +++ b/docs/deploy_docker.md @@ -0,0 +1,242 @@ +# Docker / Docker Compose 部署 + +在 **CentOS 7** 或其它 Linux 上,用容器自带 Python 3.11,**无需**在宿主机安装 Python 3.11 / Node。 + +--- + +## 0. 宿主机准备 + +### CentOS 7 安装 Docker + +```bash +sudo yum install -y yum-utils +sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +sudo yum install -y docker-ce docker-ce-cli containerd.io + +sudo systemctl enable docker +sudo systemctl start docker + +# Compose 插件(v2) +sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64" \ + -o /usr/local/bin/docker-compose +sudo chmod +x /usr/local/bin/docker-compose +docker compose version +``` + +内网无法访问外网时,请改用内网镜像源或离线 rpm 包。 + +--- + +## 1. 上传源码到服务器 + +**方式 A:Git** + +```bash +sudo mkdir -p /opt/quality-inspection-platform +sudo chown -R "$USER":"$USER" /opt/quality-inspection-platform +cd /opt/quality-inspection-platform +git clone <内网仓库地址> . +``` + +**方式 B:本机打包上传(无 Git 时)** + +在本机项目根目录: + +```bash +tar czf qip-src.tar.gz \ + --exclude='.venv' --exclude='node_modules' --exclude='frontend-admin/dist' \ + --exclude='ai_test.db' --exclude='.git' \ + . +scp qip-src.tar.gz user@<服务器>:/tmp/ +``` + +在服务器上: + +```bash +sudo mkdir -p /opt/quality-inspection-platform +sudo tar xzf /tmp/qip-src.tar.gz -C /opt/quality-inspection-platform +sudo chown -R "$USER":"$USER" /opt/quality-inspection-platform +``` + +--- + +## 2. 配置环境变量 + +```bash +cp .env.example .env +vi .env # 修改 AUTH_TOKEN_SECRET、DEFAULT_SUPERADMIN_PASSWORD 等 +``` + +--- + +## 3. 准备数据目录 + +```bash +sudo mkdir -p /docker_data +sudo chown -R "$(id -u)":"$(id -g)" /docker_data +``` + +--- + +## 4. 在服务器上构建并启动 + +```bash +cd /opt/quality-inspection-platform + +docker compose build +docker compose up -d + +docker compose ps +docker compose logs -f +``` + +--- + +## 5. 验证 + +```bash +curl -s http://127.0.0.1:8000/health +# {"ok":true} + +curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/ +# 200 +``` + +浏览器:`http://<服务器IP>:8000/` +默认账号见 `.env` 中 `DEFAULT_SUPERADMIN_*`。 + +MCP(Cursor): + +```json +{ + "mcpServers": { + "quality-inspection-platform": { + "url": "http://<服务器IP>:8000/mcp", + "headers": { "Authorization": "Bearer sto-你的密钥" } + } + } +} +``` + +--- + +## 6. 数据持久化(宿主机 `/docker_data`) + +默认把宿主机的 **`/docker_data`** 挂载到容器 **`/data`**: + +| 宿主机路径 | 容器路径 | 内容 | +|------------|----------|------| +| `/docker_data/ai_test.db` | `/data/ai_test.db` | SQLite 数据库 | +| `/docker_data/ssh-scripts/` | `/data/ssh-scripts/` | SSH 内联脚本等 | + +部署前在服务器上准备目录: + +```bash +sudo mkdir -p /docker_data +# 官方 python:3.11-slim 镜像内进程用户多为 root;若以后改用非 root 用户,再改属主 +sudo chown -R "$(id -u)":"$(id -g)" /docker_data +``` + +改用其它目录时,在 `.env` 中设置: + +```bash +QIP_DATA_HOST_DIR=/your/path +``` + +备份数据库(直接在宿主机): + +```bash +cp /docker_data/ai_test.db /docker_data/ai_test.db.bak.$(date +%Y%m%d) +``` + +--- + +## 7. 常用命令 + +```bash +# 停止 +docker compose down + +# 重启 +docker compose restart + +# 升级(拉代码后) +git pull +docker compose build --no-cache +docker compose up -d + +# 进入容器 +docker compose exec quality-inspection-platform sh +``` + +--- + +## 8. 防火墙(CentOS 7) + +```bash +sudo firewall-cmd --permanent --add-port=8000/tcp +sudo firewall-cmd --reload +``` + +--- + +## 9. 架构说明 + +``` +Dockerfile(多阶段) + ├─ node:20 → npm run build → frontend-admin/dist + └─ python:3.11 → pip install + uvicorn + 卷 /data → SQLite + SSH 脚本目录 +``` + +- 镜像内已包含前端静态资源,**宿主机不需要 Node**。 +- 镜像内已包含 Python 与 `mcp` 依赖,**宿主机不需要 Python 3.11**。 + +--- + +## 10. 排障 + +### 10.1 `lookup docker.mirrors.ustc.edu.cn: no such host` + +说明 Docker 配置了**已失效或内网不可达的镜像加速**,且 DNS 解析失败。 + +**处理:** 编辑 `/etc/docker/daemon.json`(没有则新建),去掉或更换 `registry-mirrors`: + +```json +{ + "registry-mirrors": [] +} +``` + +或改成你们内网可访问的镜像地址。然后: + +```bash +sudo systemctl daemon-reload +sudo systemctl restart docker +docker compose build +``` + +本仓库 `Dockerfile` 已去掉 `# syntax=docker/dockerfile:1`,避免额外拉取 `docker/dockerfile:1`(在劣质网络下常失败)。 + +### 10.2 完全无法访问 Docker Hub + +需任选其一: + +- 配置**内网 Harbor / registry 代理**,在能联网的机器 `docker pull node:20-bookworm-slim python:3.11-slim-bookworm` 后 `docker save`,传到服务器 `docker load` +- 让运维开放对 `registry-1.docker.io` 或可靠镜像站的访问 + +### 10.3 其它 + +| 现象 | 处理 | +|------|------| +| `git was not found`(WARN) | 可忽略;或 `yum install -y git` | +| 前端 503 | `docker compose build --no-cache` | +| MCP 连不上 | `.env` 中 `QIP_MCP_DISABLE_DNS_PROTECTION=true` | +| `/docker_data` 无写权限 | `chown` 见 §3 | + +--- + +## 11. 相关文档 + +- 裸机部署(不用 Docker):[deploy_server.md](./deploy_server.md) +- MCP 配置:[mcp_install.md](./mcp_install.md) diff --git a/docs/deploy_server.md b/docs/deploy_server.md new file mode 100644 index 0000000..86e0b38 --- /dev/null +++ b/docs/deploy_server.md @@ -0,0 +1,481 @@ +# 服务器部署完整步骤(Linux) + +本文假设在 **一台内网 Linux 服务器** 上从零部署,所有命令均在服务器上执行(通过 SSH 登录)。 + +- 部署目录示例:`/opt/quality-inspection-platform` +- 运行用户示例:`qip`(可改成你的业务用户) +- 监听端口:`8000`(也可用 Nginx 反代 80/443) + +**CentOS 7 用户**: + +- **推荐**:用 Docker,无需在宿主机装 Python 3.11 → **[deploy_docker.md](./deploy_docker.md)** +- 裸机部署:系统自带 Python 3.6 不够用 → **[§1B CentOS 7](#1b-安装系统依赖-centos-7)**,再从 [§2](#2-创建运行用户与目录) 继续 + +--- + +## 0. 部署前确认 + +| 项目 | 要求 | +|------|------| +| 系统 | **CentOS 7**(见 §1B)或 Ubuntu 20.04+ / Debian 11+(见 §1A) | +| Python | **3.10+**(必须,用于 `mcp` / FastAPI) | +| Node | **18+**(仅构建 `frontend-admin` 时需要) | +| 权限 | 能 `sudo` 安装软件、创建用户 | +| 网络 | 能访问内网 Git;`npm` / `pip` 可用(或本机构建 `dist` 后上传) | +| 磁盘 | 预留 ≥2GB | + +**交付能力(一个 `uvicorn` 进程)**: + +- Web 管理端:`http://<服务器>:8000/` +- API:`/api/*` +- MCP(Cursor/Codex):`http://<服务器>:8000/mcp` +- 数据文件:项目根目录 `ai_test.db`(SQLite,需备份) + +--- + +## 1A. 安装系统依赖(Ubuntu / Debian) + +```bash +sudo apt update +sudo apt install -y git curl ca-certificates \ + python3 python3-venv python3-pip \ + nodejs npm +``` + +检查版本: + +```bash +python3 --version # 须 >= 3.10 +node --version # 建议 >= 18 +``` + +若 `python3` 低于 3.10,请安装更新的 Python 后再继续。 +若 `nodejs` 过旧,改用 [NodeSource](https://github.com/nodesource/distributions) 安装 18+。 + +--- + +## 1B. 安装系统依赖(CentOS 7) + +CentOS 7 默认 `python3` 为 3.6,**无法直接运行本项目**。推荐用 **IUS 的 Python 3.11** + **NodeSource 18**。 + +### 1B.1 基础工具与编译依赖 + +```bash +sudo yum install -y epel-release +sudo yum install -y git curl ca-certificates gcc make \ + openssl-devel bzip2-devel libffi-devel zlib-devel \ + readline-devel sqlite-devel +``` + +### 1B.2 安装 Python 3.11(IUS) + +```bash +# 若服务器不能访问外网,请改用内网 yum 源或在本机构好 wheel 再上传 +sudo yum install -y https://repo.ius.io/ius-release-el7.rpm +sudo yum install -y python311 python311-pip python311-devel + +python3.11 --version # 应显示 3.11.x +``` + +后续所有 `python3` / `pip` 命令在 CentOS 7 上请写成 **`python3.11`** / **`python3.11 -m pip`**。 + +### 1B.3 安装 Node.js 18(构建前端) + +```bash +curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash - +sudo yum install -y nodejs + +node --version # 应 >= v18 +npm --version +``` + +**无法访问 NodeSource 时**:在开发机 `npm run build` 后,只把 `frontend-admin/dist/` 上传到服务器,可跳过 Node 安装(见 §5 说明)。 + +### 1B.4(可选)用 SCL Python 3.8 的说明 + +部分环境只有 `rh-python38`(3.8),可能装不上新版 `mcp` 包。**不推荐** CentOS 7 用 3.8 跑本仓库;请优先 3.11。 + +--- + + +## 2. 创建运行用户与目录 + +```bash +sudo useradd -r -m -d /opt/qip -s /bin/bash qip 2>/dev/null || true +sudo mkdir -p /opt/quality-inspection-platform +sudo chown -R qip:qip /opt/quality-inspection-platform +``` + +后续命令用 `qip` 执行(或你自己的用户): + +```bash +sudo -iu qip +cd /opt/quality-inspection-platform +``` + +--- + +## 3. 获取代码 + +**方式 A:Git(推荐)** + +```bash +cd /opt/quality-inspection-platform +git clone <你的内网仓库地址> . +# 或指定分支:git clone -b main . +``` + +**方式 B:上传压缩包** + +在本机打包后 `scp` 到服务器,再解压: + +```bash +cd /opt/quality-inspection-platform +tar xzf quality-inspection-platform.tar.gz --strip-components=1 +``` + +--- + +## 4. 安装 Python 依赖 + +```bash +cd /opt/quality-inspection-platform + +# Ubuntu / Debian: +python3 -m venv .venv + +# CentOS 7(IUS): +# python3.11 -m venv .venv + +source .venv/bin/activate + +pip install --upgrade pip +pip install -r requirements.txt + +# 验证关键包 +python -c "import fastapi, mcp, uvicorn; print('python deps ok')" +``` + +> CentOS 7 若 `pip install` 编译失败,确认已执行 §1B.1 的 `gcc` 与 `*-devel` 包;或在内网配置 pip 镜像源。 + +--- + +## 5. 构建前端(生产必做) + +```bash +cd /opt/quality-inspection-platform/frontend-admin + +npm install +npm run build + +cd .. +ls -la frontend-admin/dist/index.html +``` + +必须存在 `frontend-admin/dist/index.html`,否则浏览器访问 `/` 会提示前端未构建。 + +**CentOS 7 未装 Node 时**:在开发机构建后只上传 `dist`: + +```bash +cd frontend-admin && npm install && npm run build +scp -r dist qip@<服务器>:/opt/quality-inspection-platform/frontend-admin/ +``` + +**可选**:构建完成后删除 `node_modules` 节省空间(以后升级需重新 `npm install`): + +```bash +rm -rf /opt/quality-inspection-platform/frontend-admin/node_modules +``` + +--- + +## 6. 配置环境变量 + +```bash +cd /opt/quality-inspection-platform +mkdir -p data/ssh-scripts + +cat > /opt/quality-inspection-platform/.env <<'EOF' +# 生产务必修改下面三项 +AUTH_TOKEN_SECRET=请替换为随机长字符串 +DEFAULT_SUPERADMIN_USERNAME=admin +DEFAULT_SUPERADMIN_PASSWORD=请替换为强密码 + +# MCP:强制所有工具带 API Key +MCP_REQUIRE_API_KEY=true + +# 同事从其它机器用 Cursor 连 MCP 时,若连不上可开启: +QIP_MCP_DISABLE_DNS_PROTECTION=true +# 或精确白名单(二选一): +# QIP_MCP_ALLOWED_HOSTS=10.0.0.5:8000,qip.internal:8000 +# QIP_MCP_ALLOWED_ORIGINS=http://10.0.0.5:8000 + +# 可选:Loki 日志探索 +# GENERAL_LOKI_EXPLORE_URL=https://grafana.example/explore +EOF + +chmod 600 .env +``` + +> `.env` 已在 `.gitignore` 中,不会进 Git。 +> `uvicorn` 默认**不会**自动读 `.env`;下面 systemd 示例用 `EnvironmentFile` 加载。 + +--- + +## 7. 首次试跑(前台) + +```bash +cd /opt/quality-inspection-platform +source .venv/bin/activate + +set -a +source .env +set +a + +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +**另开一个 SSH 窗口**自检: + +```bash +curl -s http://127.0.0.1:8000/health +# 期望:{"ok":true} + +curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8000/ +# 期望:200 +``` + +浏览器访问:`http://<服务器IP>:8000/` +使用 `.env` 里的 `DEFAULT_SUPERADMIN_USERNAME` / `DEFAULT_SUPERADMIN_PASSWORD` 登录。 + +确认无误后,在前台试跑窗口 `Ctrl+C` 停止,进入第 8 步配置 systemd。 + +--- + +## 8. 配置 systemd 常驻 + +```bash +sudo tee /etc/systemd/system/quality-inspection-platform.service <<'EOF' +[Unit] +Description=Quality Inspection Platform +After=network.target + +[Service] +Type=simple +User=qip +Group=qip +WorkingDirectory=/opt/quality-inspection-platform +EnvironmentFile=/opt/quality-inspection-platform/.env +Environment=PATH=/opt/quality-inspection-platform/.venv/bin:/usr/bin:/bin +ExecStart=/opt/quality-inspection-platform/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +sudo systemctl daemon-reload +sudo systemctl enable quality-inspection-platform +sudo systemctl start quality-inspection-platform +sudo systemctl status quality-inspection-platform +``` + +查看日志: + +```bash +journalctl -u quality-inspection-platform -f +``` + +--- + +## 9. 防火墙(按需) + +**CentOS 7(firewalld)** — 仅内网访问 8000: + +```bash +sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.0/8" port protocol="tcp" port="8000" accept' +sudo firewall-cmd --reload +``` + +若用 Nginx 对外 80: + +```bash +sudo firewall-cmd --permanent --add-service=http +sudo firewall-cmd --reload +``` + +**Ubuntu(ufw)**: + +```bash +sudo ufw allow from 10.0.0.0/8 to any port 8000 proto tcp +sudo ufw reload +``` + +若前面有 Nginx 对外 80/443,则只开放 80/443,**不要**把 8000 暴露到公网。 + +--- + +## 10. Nginx 反向代理(可选) + +适用于希望用域名访问、或只暴露 80/443 的场景。 + +```bash +# Ubuntu / Debian +sudo apt install -y nginx + +# CentOS 7 +sudo yum install -y nginx +sudo systemctl enable nginx +``` + +```bash +sudo tee /etc/nginx/sites-available/quality-inspection-platform <<'EOF' +server { + listen 80; + server_name qip.internal; # 改成你的域名或 IP + + client_max_body_size 50m; + + location / { + proxy_pass http://127.0.0.1:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # MCP Streamable HTTP 可能需要较长连接 + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + } +} +EOF + +sudo ln -sf /etc/nginx/sites-available/quality-inspection-platform /etc/nginx/sites-enabled/ +sudo nginx -t && sudo systemctl reload nginx +``` + +此时访问地址变为:`http://qip.internal/` +MCP URL:`http://qip.internal/mcp` + +可将 systemd 里 uvicorn 改为只监听本机: + +```ini +ExecStart=.../uvicorn app.main:app --host 127.0.0.1 --port 8000 +``` + +--- + +## 11. 同事配置 MCP(不在服务器装 Python) + +每人本机 Cursor `~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "quality-inspection-platform": { + "url": "http://qip.internal/mcp", + "headers": { + "Authorization": "Bearer sto-个人API密钥" + } + } + } +} +``` + +- `sto-` Key:登录 Web → 个人中心创建 +- 详细说明:[mcp_install.md](./mcp_install.md) + +--- + +## 12. 数据备份 + +数据库与上传的 SSH 脚本目录: + +```bash +# 示例:每日备份 cron +0 2 * * * qip cp /opt/quality-inspection-platform/ai_test.db /opt/qip/backup/ai_test.db.$(date +\%Y\%m\%d) +``` + +**升级代码时不要覆盖** 已有 `ai_test.db`。 + +--- + +## 13. 版本升级流程 + +```bash +sudo systemctl stop quality-inspection-platform + +sudo -iu qip +cd /opt/quality-inspection-platform + +# 备份 +cp -a ai_test.db ai_test.db.bak.$(date +%Y%m%d) + +# 拉代码 +git pull + +source .venv/bin/activate +pip install -r requirements.txt + +cd frontend-admin +npm install +npm run build +cd .. + +exit +sudo systemctl start quality-inspection-platform +curl -s http://127.0.0.1:8000/health +``` + +--- + +## 14. 验收清单 + +| # | 检查 | 命令/操作 | +|---|------|-----------| +| 1 | 服务运行 | `systemctl is-active quality-inspection-platform` | +| 2 | 健康检查 | `curl -s http://127.0.0.1:8000/health` | +| 3 | Web 首页 | 浏览器打开 `/`,能登录 | +| 4 | API 文档 | `http://<主机>:8000/docs` | +| 5 | MCP 工具列表 | `curl -s http://127.0.0.1:8000/mcp/tools -H "Authorization: Bearer sto-xxx"` | +| 6 | 改密 | 登录后修改超管密码;API Key 仅个人保管 | +| 7 | 防火墙 | 仅内网可达 | + +--- + +## 15. 常见问题 + +| 现象 | 处理 | +|------|------| +| 访问 `/` 提示 frontend dist not found | 重新执行第 5 步 `npm run build`,或上传 `frontend-admin/dist` | +| `ModuleNotFoundError: mcp` | 第 4 步 `pip install -r requirements.txt`;CentOS 7 须用 3.11 的 venv | +| CentOS 7 `python3` 是 3.6 | 不要用系统 `python3`,改用 `python3.11` 建 venv | +| `pip` 编译卡住 / 失败 | `yum install` §1B.1 开发包;配置内网 pip 镜像 | +| IUS / NodeSource 无法访问 | 内网 yum 镜像;或本机构建 `dist` + 离线 wheel 装 Python 包 | +| MCP 在 Cursor 里连不上 | `.env` 设 `QIP_MCP_DISABLE_DNS_PROTECTION=true` 或配置 `QIP_MCP_ALLOWED_HOSTS` | +| 401 / 无 Key | Web 个人中心创建 `sto-` Key;或设 `MCP_REQUIRE_API_KEY` 后请求头带 Bearer | +| 端口被占用 | `ss -lntp \| grep 8000` 查占用进程 | +| 权限错误 | 确认 `ai_test.db`、`data/` 目录属主为 `qip` | +| SELinux 拦截(CentOS) | 临时:`sudo setenforce 0` 排查;长期:`setsebool -P httpd_can_network_connect 1` 或按策略放行 | + +--- + +## 16. 相关文档 + +| 文档 | 内容 | +|------|------| +| [mcp_install.md](./mcp_install.md) | MCP / Cursor / 内网排障 | +| [mcp_tools_for_ai.md](./mcp_tools_for_ai.md) | AI 工具能力与 Recipe | +| [mcp_quickstart.md](./mcp_quickstart.md) | curl / HTTP 网关示例 | + +--- + +## 快速命令抄录(已装好环境后) + +```bash +cd /opt/quality-inspection-platform && source .venv/bin/activate +git pull && pip install -r requirements.txt +cd frontend-admin && npm install && npm run build && cd .. +sudo systemctl restart quality-inspection-platform +``` diff --git a/docs/mcp_install.md b/docs/mcp_install.md index 983eed1..92a2272 100644 --- a/docs/mcp_install.md +++ b/docs/mcp_install.md @@ -1,201 +1,248 @@ -# 把本平台注册成真正的 MCP Server +# MCP Server 安装与接入说明 -本仓库已附带 `mcp_bridge.py`,它是一个标准 stdio MCP Server, -对外暴露 MCP 协议(JSON-RPC over stdio),内部桥接到本平台的 HTTP 网关。 +平台在 **同一个 `uvicorn` 进程** 内提供: -只要先启动平台后端,再让 Cursor / Claude Desktop / Codex CLI 启动这个桥接, -它就会被识别成一个真正的 MCP Server,并自动暴露所有平台工具(含跑批工作流工具)。 +| 能力 | 路径 | 适用场景 | +|------|------|----------| +| **Web 管理端** | `/`、`/api/*` | 人工在浏览器操作 | +| **MCP Server(推荐 AI 用)** | `/mcp` | Codex / Claude / Cursor(Streamable HTTP) | +| **HTTP 工具网关(兼容)** | `/mcp/tools`、`/mcp/invoke` | curl、CI、自研脚本 | + +三者共用数据库与工作流引擎;**Web UI 与 MCP 可同时使用,互不影响**。 + +> **历史变更**:已移除 `mcp_bridge.py`。不再需要本机 `python3` 子进程桥接;客户端直接连 `http://10.20.30.42:8000/mcp`。 --- -## 1. 启动平台后端 +## 1. 架构 + +``` +┌─────────────────┐ MCP (Streamable HTTP) ┌──────────────────────────┐ +│ Codex / Claude │ ──────────────────────────────►│ uvicorn app.main:app │ +│ Claude Desktop │ Authorization: Bearer │ ├─ /mcp (mcp_native) │ +└─────────────────┘ │ ├─ /api/* (Web) │ +┌─────────────────┐ HTTP 登录 / 点击 │ └─ /mcp/invoke (curl) │ +│ 浏览器 Web UI │ ──────────────────────────────►└──────────────────────────┘ +└─────────────────┘ +``` + +实现位置:`app/mcp_native.py`(挂载路由 + `lifespan`),工具逻辑:`app/main.py` 中 `MCP_TOOL_SPECS` 与 `_dispatch_mcp_tool`。 + +--- + +## 2. 服务端部署(管理员) + +> **完整步骤**(用户、Git、前端构建、systemd、Nginx、备份):[`deploy_server.md`](./deploy_server.md) + +### 2.1 安装与启动 ```bash -cd /Users/qihongkun/work/My_app/quality-inspection-platform -# 可选:生产/共享环境建议开启,所有 MCP 调用必须带 API Key +cd /path/to/quality-inspection-platform +python3 -m venv .venv +source .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -r requirements.txt + +# 生产建议:所有 MCP 调用必须带 API Key # export MCP_REQUIRE_API_KEY=true -uvicorn app.main:app --host 127.0.0.1 --port 8000 + +uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` -确认可访问: +### 2.2 启动后确认 -- `http://127.0.0.1:8000/mcp/tools` +| 检查项 | 命令或地址 | +|--------|------------| +| 健康 | `curl -s http://10.20.30.42:8000/health` | +| Web | 浏览器打开 `http://10.20.30.42:8000/` | +| 工具列表 | `curl -s http://10.20.30.42:8000/mcp/tools -H "Authorization: Bearer sto-密钥"` | +| MCP 端点 | `http://10.20.30.42:8000/mcp`(供 Codex 配置,非浏览器页面) | ---- +### 2.3 内网 systemd 示例(可选) -## 2. 安装到 Cursor +```ini +[Unit] +Description=Quality Inspection Platform +After=network.target -编辑 `~/.cursor/mcp.json`(不存在则新建),加入: +[Service] +User=qip +WorkingDirectory=/opt/quality-inspection-platform +Environment="PATH=/opt/quality-inspection-platform/.venv/bin" +Environment="MCP_REQUIRE_API_KEY=true" +# 使用内网地址访问且 MCP 连不上时可加: +# Environment="QIP_MCP_DISABLE_DNS_PROTECTION=true" +ExecStart=/opt/quality-inspection-platform/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 +Restart=always -```json -{ - "mcpServers": { - "quality-inspection-platform": { - "command": "python3", - "args": ["/Users/qihongkun/work/My_app/quality-inspection-platform/mcp_bridge.py"], - "env": { - "QIP_BASE_URL": "http://127.0.0.1:8000", - "QIP_API_KEY": "sto-你的API密钥" - } - } - } -} -``` - -- `QIP_BASE_URL`:平台后端地址(兼容旧环境变量 `AI_TEST_BASE_URL`)。 -- `QIP_API_KEY`:**必须配置**(兼容旧名 `AI_TEST_API_KEY`)。写操作与执行类工具(`workflow_run`、`workflow_batch_run`、`ssh_script_run` 等)无 Key 将返回 401;桥接会把 Key 放到 `Authorization: Bearer` 与 invoke body 的 `api_key` 字段。 -- 后端可选 `MCP_REQUIRE_API_KEY=true`:所有 MCP 工具(含只读、`GET /mcp/tools`)均要求 Key。 - -在平台 Web 端右上角「个人中心」生成 `sto-` 开头的 API Key。 - -重启 Cursor 后,在 MCP 面板里就能看到 `quality-inspection-platform`,里面会自动列出全部工具。 - ---- - -## 3. 安装到 Claude Desktop - -编辑 `~/Library/Application Support/Claude/claude_desktop_config.json`: - -```json -{ - "mcpServers": { - "quality-inspection-platform": { - "command": "python3", - "args": ["/Users/qihongkun/work/My_app/quality-inspection-platform/mcp_bridge.py"], - "env": { - "QIP_BASE_URL": "http://127.0.0.1:8000", - "QIP_API_KEY": "sto-你的API密钥" - } - } - } -} +[Install] +WantedBy=multi-user.target ``` --- -## 4. 安装到 Codex CLI +## 3. Codex 配置 -```bash -codex mcp add quality-inspection-platform \ - --command python3 \ - --args /Users/qihongkun/work/My_app/quality-inspection-platform/mcp_bridge.py \ - --env QIP_BASE_URL=http://127.0.0.1:8000 \ - --env QIP_API_KEY=sto-你的API密钥 -``` +编辑 **`~/.codex/config.toml`**。 -或在 `~/.codex/config.toml` 中: +> **远程环境**:配置文件要写在 Codex 实际运行的机器上。MCP 请求从 Codex 所在机器发出;团队默认 MCP 地址为 `http://10.20.30.42:8000/mcp`。 + +### 3.1 默认内网地址 ```toml -[mcp.servers.quality-inspection-platform] -command = "python3" -args = ["/Users/qihongkun/work/My_app/quality-inspection-platform/mcp_bridge.py"] +[mcp_servers.quality-inspection-platform] +url = "http://10.20.30.42:8000/mcp" -[mcp.servers.quality-inspection-platform.env] -QIP_BASE_URL = "http://127.0.0.1:8000" -QIP_API_KEY = "sto-你的API密钥" +[mcp_servers.quality-inspection-platform.http_headers] +Authorization = "Bearer sto-你的API密钥" ``` +### 3.2 连接其他部署地址 + +如果 MCP 平台部署到其他服务器,将 `10.20.30.42:8000` 替换为实际 IP、端口或域名,例如: + +```toml +[mcp_servers.quality-inspection-platform] +url = "http://qip.corp:8000/mcp" + +[mcp_servers.quality-inspection-platform.http_headers] +Authorization = "Bearer sto-你的API密钥" +``` + +### 3.3 配置生效 + +配置完成后重启 Codex 会话或 Codex 应用,让 MCP 配置重新加载。验证方式:在对话中要求 Agent 调用 `catalog_snapshot`,应返回 apis / workflows 等 JSON 快照。 + +### 3.4 API Key 获取 + +1. 浏览器登录 Web → 右上角「个人中心」 +2. 创建 `sto-` 开头的 API Key(仅显示一次,请妥善保存) +3. 写入 `~/.codex/config.toml` 的 `http_headers.Authorization`,或在 HTTP 网关调用时使用头 `X-API-Key: sto-...` + +**写操作**与**执行类**工具无有效 Key 返回 **401**。生产环境建议 `MCP_REQUIRE_API_KEY=true`,此时**只读工具**也须 Key。 + --- -## 5. 自检(不连客户端,先确认桥接可用) +## 4. 团队协作(同事无需克隆仓库) + +| 角色 | 需要做什么 | +|------|------------| +| **管理员** | 内网部署 `uvicorn`,开放防火墙,分发内网 URL | +| **每位开发者** | 配置 `~/.codex/config.toml`(URL + 个人 `sto-` Key) | +| **不需要** | 本机 Python、`mcp_bridge`、克隆完整项目(除非要改代码) | + +**远程 Codex 统一配置(推荐)** + +1. 服务器上部署一次项目(含 `.venv` 与 `uvicorn`) +2. 同事在远程机器上运行 Codex 或使用远程工作区 +3. 在**远程** `~/.codex/config.toml` 使用 `http://10.20.30.42:8000/mcp`(确保 Codex 所在机器可访问该内网地址) +4. 每人使用自己的 `sto-` Key,**不要**写入 Git 或共享配置文件 + +--- + +## 5. Cursor / Claude Desktop(可选) + +使用 **HTTP URL** 连接 MCP(以客户端当前版本支持的 Streamable HTTP / URL 配置为准): + +- URL:`http://10.20.30.42:8000/mcp` +- 头:`Authorization: Bearer sto-...` + +Cursor 示例: + +```json +{ + "mcpServers": { + "quality-inspection-platform": { + "url": "http://10.20.30.42:8000/mcp", + "headers": { + "Authorization": "Bearer sto-你的API密钥" + } + } + } +} +``` + +Claude Desktop 具体字段名见其 MCP 配置文档。 + +--- + +## 6. 从旧版 `mcp_bridge` 迁移 + +| 旧配置 | 新配置 | +|--------|--------| +| `"command": "python3"` | 删除 | +| `"args": [".../mcp_bridge.py"]` | 删除 | +| `"env": { "QIP_BASE_URL": "...", "QIP_API_KEY": "..." }` | 改为 Codex `url = "/mcp"` + `http_headers.Authorization = "Bearer "` | + +迁移后只需保证 **`uvicorn` 已启动**;无需再单独运行桥接脚本。 + +--- + +## 7. 自检清单 ```bash -printf '%s\n' \ - '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ - '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \ - '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"catalog_snapshot","arguments":{}}}' \ - | QIP_API_KEY=sto-你的API密钥 python3 mcp_bridge.py +# 1. 后端存活 +curl -s http://10.20.30.42:8000/health + +# 2. HTTP 网关(与 MCP 工具集一致) +curl -s http://10.20.30.42:8000/mcp/tools \ + -H "Authorization: Bearer sto-你的密钥" + +# 3. 试调一个工具 +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ + -H "Authorization: Bearer sto-你的密钥" \ + -H "Content-Type: application/json" \ + -d '{"tool":"catalog_snapshot","arguments":{}}' ``` -预期: - -- `initialize` 返回 `serverInfo` -- `tools/list` 返回工具清单(应包含 `workflow_batch_*` 等) -- `tools/call` 返回 `content[0].text` 内含调用结果 +**Codex 侧**:在对话中让 Agent 调用 `catalog_snapshot`,应返回 apis / workflows 等 JSON 快照。 --- -## 6. 暴露的工具(自动转发自平台) +## 8. 排障 -桥接启动时从 `GET /mcp/tools` 拉取列表,**无需改 `mcp_bridge.py`** 即可随平台升级获得新工具。 +| 现象 | 可能原因 | 处理 | +|------|----------|------| +| Codex 无工具 / 连接失败 | `uvicorn` 未启动或 URL 错误 | 确认 `/health`;检查 `~/.codex/config.toml` 中 URL 以 `/mcp` 结尾 | +| 401 / requires API Key | 未配置或 Key 无效 | 个人中心重新生成 Key,更新 `Authorization` | +| 内网 IP 可 curl 但 Codex 连不上 | DNS rebinding 校验 | 设置 `QIP_MCP_DISABLE_DNS_PROTECTION=true` 或配置 `QIP_MCP_ALLOWED_HOSTS` | +| 修改工具后客户端仍是旧的 | 客户端缓存 | 重启 Codex 会话或应用;服务端重启 `uvicorn` | +| 远程环境 MCP 失败 | 配在了本机而非 Codex 所在机器 | 改 Codex 实际运行机器上的 `~/.codex/config.toml`;从那台机器验证是否能访问 `http://10.20.30.42:8000/health` | +| Web 正常、MCP 不行 | 只检查了 Web | MCP 走 `/mcp`,与 `/api` 独立;分别自检 | -### 资源管理 - -- `api_upsert` — 创建/更新接口 -- `mock_upsert` — 创建/更新 mock 数据 -- `mcp_tool_upsert` — 创建/更新 MCP 工具配置 -- `catalog_snapshot` — 全量资源快照(含 `workflow_batches`、目录树、SSH 树) - -### 工作流(单次) - -- `workflow_upsert` — 创建/更新工作流(支持 `loop`、`condition` + `json_path`) -- `workflow_get` — 读取工作流及各节点 `last_run` -- `workflow_node_status` — 单节点最近执行状态 -- `workflow_patch_json` — 增量修改工作流 JSON -- `workflow_validate` — 校验 definition JSON -- `workflow_run` / `workflow_run_node` — 执行(需 API Key) -- `workflow_analyze_last_run` — 分析最近一次执行 -- `workflow_run_list` / `workflow_run_get` — 执行历史 -- `workflow_run_replay` / `workflow_run_loki_link` — 重放与 Loki 链接 - -### 跑批工作流 - -- `workflow_batch_create` — 创建草稿批跑任务(需 API Key) -- `workflow_batch_update` — 更新 `workflow_ids` / `base_url` 等(需 API Key) -- `workflow_batch_run` — 按任务配置顺序执行多个工作流 -- `workflow_batch_get` — 批跑详情与关联 `workflow_runs` -- `workflow_batch_list` — 批跑任务列表 - -推荐顺序:`create` → `update`(绑定 `workflow_ids`)→ `run` → `get`。详见 `docs/mcp_quickstart.md` 示例 H。 - -### 目录 - -- `folder_ensure` / `folder_list` -- `api_move_folder` / `workflow_move_folder` - -### SSH 脚本 - -- `ssh_tree` / `ssh_script_get` -- `ssh_script_upsert` / `ssh_script_run` +查看 MCP 相关日志:启动 `uvicorn` 的终端;Codex 客户端 MCP 日志。 --- +## 9. 环境变量 -## 7. 工作机制 - -``` -Cursor / Claude / Codex - | - | (MCP stdio JSON-RPC) - v -mcp_bridge.py - | - | HTTP (httpx) + Authorization / api_key - v -http://127.0.0.1:8000/mcp/invoke -``` - -- 桥接会在启动时调用一次 `/mcp/tools`,把工具映射成 MCP `tools/list` 返回值。 -- 每次 `tools/call`,桥接会以 `{tool, arguments}` POST 到 `/mcp/invoke`。 -- 后端返回 `{ok, tool, data, error}`,桥接将其作为 `content[0].text` 文本返回,并按 `ok` 设置 `isError`。 +| 变量 | 含义 | +|------|------| +| `MCP_REQUIRE_API_KEY` | `true` 时所有 MCP 工具(含只读、`GET /mcp/tools`)均需 Key | +| `QIP_MCP_DISABLE_DNS_PROTECTION` | `true` 时关闭 MCP 传输的 Host/Origin 校验 | +| `QIP_MCP_ALLOWED_HOSTS` | 逗号分隔 Host 白名单,如 `10.20.30.42:8000,qip.corp:8000` | +| `QIP_MCP_ALLOWED_ORIGINS` | 逗号分隔 Origin 白名单 | --- -## 8. Loki 日志(可选) +## 10. 维护与文档索引 -若需在管理端或 API 中打开 General/Grafana Loki 探索页,在后端进程环境中配置: - -- `GENERAL_LOKI_EXPLORE_URL` 或 `LOKI_EXPLORE_URL` -- 可选:`LOKI_DATASOURCE`、`LOKI_ORG_ID`、`LOKI_LABEL_SELECTOR`、`LOKI_TIME_PADDING_SECONDS` - -配置后,`GET /api/workflow-runs/{run_id}/loki-link` 可为指定 HTTP 节点生成带时间窗与路径过滤的 Explore URL。 +- **新增 MCP 工具**:修改 `app/main.py` 中 `MCP_TOOL_SPECS` 与 `_dispatch_mcp_tool` 分支 → 重启 `uvicorn` → 重启 MCP 客户端。 +- **AI 选型与 Recipe(Agent 必读)**:[mcp_tools_for_ai.md](./mcp_tools_for_ai.md) +- **curl / 批量调用示例**:[mcp_quickstart.md](./mcp_quickstart.md) +- **Codex 短约定**:项目根 [AGENTS.md](../AGENTS.md) --- -## 9. 维护建议 +## 11. 常见问题 -- 平台新增工具时,无需改桥接,重启 MCP 客户端即可重新拉取 `tools/list`。 -- 团队成员:`git pull` + `pip install -r requirements.txt` + 配置上述 MCP 入口与 `AI_TEST_API_KEY`。 -- **AI 能力说明(首选)**:**`docs/mcp_tools_for_ai.md`** -- 人类 curl 示例:**`docs/mcp_quickstart.md`** -- Codex 自动加载:**项目根 `AGENTS.md`** +**还需要 bridge 吗?** +不需要。`/mcp` 已是标准 MCP Server。 + +**每人都要装 Python 吗?** +仅**运行后端的服务器**需要 Python 环境;只用 Codex 连 MCP 的同事不需要本机 Python。 + +**能和 Web 一起用吗?** +可以。Web 走登录态 `/api/*`;AI 走 `/mcp` + API Key。 + +**脚本还能用吗?** +可以。继续 `POST /mcp/invoke` 与 `GET /mcp/tools`,与 MCP 协议共用同一套工具实现。 diff --git a/docs/mcp_quickstart.md b/docs/mcp_quickstart.md index 1ab7ac3..637f7b7 100644 --- a/docs/mcp_quickstart.md +++ b/docs/mcp_quickstart.md @@ -1,9 +1,176 @@ # MCP 快速接入与 AI 调用说明书 > **AI Agent 请优先阅读**:[mcp_tools_for_ai.md](./mcp_tools_for_ai.md)(工具决策表、鉴权、Recipe、节点约定)。 -> 本文档侧重 curl 示例与人工接入;项目根 [AGENTS.md](../AGENTS.md) 供 Codex 自动加载。 +> **Codex / 内网安装**:[mcp_install.md](./mcp_install.md)(`~/.codex/config.toml`、团队协作、排障)。 +> 本文档侧重 **HTTP curl** 与脚本调用;项目根 [AGENTS.md](../AGENTS.md) 供 Codex 自动加载。 -本文档给 AI Agent 和开发者使用,目标是让 AI 可以直接通过本平台的 MCP 接口完成: +## 接入方式选择 + +| 方式 | 端点 | 适用 | +|------|------|------| +| **MCP 协议(推荐)** | `http://10.20.30.42:8000/mcp` | Codex、Claude、Cursor,见 [mcp_install.md](./mcp_install.md) | +| **HTTP 网关(本文)** | `/mcp/tools`、`/mcp/invoke` | curl、CI、自研 Agent | +| **Web UI** | `/api/*` | 浏览器人工操作 | + +--- + +## 0. MCP 安装与接入(团队必读) + +平台现在直接在同一个 `uvicorn` 进程内提供标准 MCP Server: + +```text +Web UI: http://10.20.30.42:8000/ +MCP Server: http://10.20.30.42:8000/mcp +HTTP 网关: http://10.20.30.42:8000/mcp/tools、/mcp/invoke +``` + +已移除旧版 `mcp_bridge.py`;不再需要为 Codex / Claude / Cursor 额外启动本机 Python 桥接进程。客户端直接连接 `http://10.20.30.42:8000/mcp`。 + +### 0.1 服务端启动 + +管理员或本地开发者启动平台: + +```bash +cd /path/to/quality-inspection-platform +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt + +# 生产或共享环境建议开启:所有 MCP 调用必须带 API Key +# export MCP_REQUIRE_API_KEY=true + +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +启动后检查: + +```bash +curl -s http://10.20.30.42:8000/health +curl -s http://10.20.30.42:8000/mcp/tools \ + -H "Authorization: Bearer sto-你的API密钥" +``` + +### 0.2 Codex 配置 + +编辑 `~/.codex/config.toml`: + +```toml +[mcp_servers.quality-inspection-platform] +url = "http://10.20.30.42:8000/mcp" + +[mcp_servers.quality-inspection-platform.http_headers] +Authorization = "Bearer sto-你的API密钥" +``` + +团队默认 MCP 地址为 `10.20.30.42:8000`。如果 MCP 平台部署到其他服务器,将 `10.20.30.42:8000` 替换为实际 IP、端口或域名: + +```toml +[mcp_servers.quality-inspection-platform] +url = "http://qip.corp:8000/mcp" + +[mcp_servers.quality-inspection-platform.http_headers] +Authorization = "Bearer sto-你的API密钥" +``` + +配置完成后重启 Codex 会话或 Codex 应用,让 MCP 配置重新加载。验证方式是在对话中要求调用 `catalog_snapshot`。 + +### 0.3 Codex Remote / 远程环境 + +如果 Codex 运行在远程主机或远程工作区,`~/.codex/config.toml` 要配置在 Codex 实际运行的那台机器上。 + +如果 Codex 运行在远程机器上,URL 仍以 Codex 所在机器能访问到的 MCP 地址为准。团队默认写: + +```toml +url = "http://10.20.30.42:8000/mcp" +``` + +如果你改成其他部署地址,需要先在 Codex 所在机器上确认该地址可访问。 + +### 0.4 Cursor 配置(可选) + +如果团队中有人使用 Cursor,再配置 `~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "quality-inspection-platform": { + "url": "http://10.20.30.42:8000/mcp", + "headers": { + "Authorization": "Bearer sto-你的API密钥" + } + } + } +} +``` + +部分 Cursor 版本需要显式指定传输类型: + +```json +{ + "mcpServers": { + "quality-inspection-platform": { + "type": "streamableHttp", + "url": "http://10.20.30.42:8000/mcp", + "headers": { + "Authorization": "Bearer sto-你的API密钥" + } + } + } +} +``` + +Cursor Remote-SSH 时,`mcp.json` 要配置在远程主机的 `~/.cursor/mcp.json`,不是本机。 + +### 0.5 Claude + +Claude Desktop 使用 HTTP URL 连接 MCP: + +```text +URL: http://10.20.30.42:8000/mcp +Header: Authorization: Bearer sto-你的API密钥 +``` + +具体字段名按 Claude 当前 MCP 配置格式填写。优先使用 Streamable HTTP / URL 型 MCP,不再配置 `command + args + mcp_bridge.py`。 + +### 0.6 API Key 获取与安全 + +1. 浏览器登录 Web UI。 +2. 右上角进入「个人中心」。 +3. 创建 `sto-` 开头的 API Key。 +4. 写入 MCP 客户端配置的 `Authorization: Bearer sto-...`。 + +写操作和执行类工具必须带有效 Key;生产环境建议设置 `MCP_REQUIRE_API_KEY=true`,让只读工具也必须鉴权。API Key 不要提交到 Git,不要写入共享文档。 + +### 0.7 旧版 bridge 迁移 + +旧配置: + +```json +{ + "command": "python3", + "args": ["/path/to/mcp_bridge.py"], + "env": { + "QIP_BASE_URL": "http://10.20.30.42:8000", + "QIP_API_KEY": "sto-..." + } +} +``` + +新配置: + +```toml +[mcp_servers.quality-inspection-platform] +url = "http://10.20.30.42:8000/mcp" + +[mcp_servers.quality-inspection-platform.http_headers] +Authorization = "Bearer sto-你的API密钥" +``` + +完整安装、团队协作、systemd 与排障说明见 [mcp_install.md](./mcp_install.md)。 + +--- + +本文档给开发者和自动化脚本使用,目标是让调用方通过 HTTP 网关完成: - 接口创建/更新 - Mock 数据创建/更新 @@ -17,11 +184,12 @@ ## 1. 基础信息 -- 服务地址:`http://127.0.0.1:8000` -- 工具发现:`GET /mcp/tools` -- 单次调用:`POST /mcp/invoke` +- 服务地址:`http://10.20.30.42:8000` +- **Codex MCP 端点**:`http://10.20.30.42:8000/mcp`(Streamable HTTP,见 [mcp_install.md](./mcp_install.md)) +- 工具发现(HTTP 兼容):`GET /mcp/tools` +- 单次调用(HTTP 兼容):`POST /mcp/invoke` - 批量调用:`POST /mcp/invoke-batch` -- MCP 鉴权:在平台右上角「个人中心」生成 `sto-` 开头的 API Key,配置到 MCP Bridge 环境变量 `QIP_API_KEY`(兼容 `AI_TEST_API_KEY`),或在请求头使用 `Authorization: Bearer ` / `X-API-Key: `。 +- MCP 鉴权:在平台右上角「个人中心」生成 `sto-` 开头的 API Key;Codex 在 `~/.codex/config.toml` 的 `http_headers.Authorization` 中配置,curl/脚本可用 `Authorization: Bearer ` / `X-API-Key: `(body 字段 `api_key` 仍兼容)。 - **写操作**(创建/更新资源)与 **执行类操作**(跑工作流、批跑、单节点、SSH 执行、重放)**必须**带有效 Key,禁止无 Key 回落为 superadmin。 - 可选 **`MCP_REQUIRE_API_KEY=true`**(后端环境变量):所有 MCP 工具(含只读)均要求 Key;`GET /mcp/tools` 同样校验。 @@ -151,7 +319,7 @@ ### 4.1 通用调用模板 ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ @@ -163,7 +331,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ ### 4.2 批量调用模板 ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke-batch \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke-batch \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ @@ -182,7 +350,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke-batch \ ### 示例 A:AI 自动创建接口 ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ @@ -203,7 +371,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ ### 示例 B:AI 自动创建 Mock 数据 ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -d '{ "tool": "mock_upsert", @@ -217,7 +385,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ ### 示例 C:AI 自动创建 Workflow(含条件分支) ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ @@ -245,7 +413,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ ### 示例 D:AI 自动 Patch Workflow JSON ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ @@ -262,7 +430,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ ### 示例 E:AI 执行工作流并分析结果 ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -d '{ "tool": "workflow_run", @@ -275,7 +443,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ ``` ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -d '{ "tool": "workflow_analyze_last_run", @@ -286,7 +454,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ ### 示例 F:AI 批量编排(创建接口 → Mock → Patch → 执行) ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke-batch \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke-batch \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ @@ -328,7 +496,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke-batch \ ### 示例 G:按目录分功能管理 ```bash -curl -s -X POST http://127.0.0.1:8000/mcp/invoke-batch \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke-batch \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ @@ -362,7 +530,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke-batch \ ```bash # 1) 创建草稿批跑任务 -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ @@ -377,7 +545,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ # 假设返回 data.id = 3 # 2) 绑定要执行的工作流 ID 列表 -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ @@ -389,7 +557,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ }' # 3) 执行批跑 -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -d '{ "tool": "workflow_batch_run", @@ -397,7 +565,7 @@ curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ }' # 4) 查询结果(含每次 workflow_run 的 run_id) -curl -s -X POST http://127.0.0.1:8000/mcp/invoke \ +curl -s -X POST http://10.20.30.42:8000/mcp/invoke \ -H "Content-Type: application/json" \ -d '{ "tool": "workflow_batch_get", diff --git a/docs/mcp_tools_for_ai.md b/docs/mcp_tools_for_ai.md index 2b75894..10a88f0 100644 --- a/docs/mcp_tools_for_ai.md +++ b/docs/mcp_tools_for_ai.md @@ -1,6 +1,6 @@ # MCP 工具能力说明(AI 专用) -> **读者**:Cursor / Codex / Claude 等通过 `quality-inspection-platform` MCP 工作的 Agent。 +> **读者**:Codex / Claude / Cursor 等通过 `quality-inspection-platform` MCP 工作的 Agent。 > **目的**:在调用 `tools/list` 之外,提供「何时用哪个工具、按什么顺序、参数怎么填」的固定上下文。 > **维护**:工具以 `app/main.py` 中 `MCP_TOOL_SPECS` 和平台 `GET /mcp/tools` 为准;增删工具时请同步更新本文档。 > **重要**:某些 MCP 客户端可能缓存、裁剪或延迟刷新工具列表;不要仅凭当前客户端可见工具反向删减本文档能力。 @@ -10,16 +10,16 @@ ## 0. Agent 必读(30 秒) 1. **先** `catalog_snapshot` 了解现有 apis / workflows / workflow_batches / mocks / folders / ssh_tree,避免重复创建。 -2. **写操作与执行**必须带 `sto-` API Key(`Authorization: Bearer` 或桥接环境变量 `QIP_API_KEY`,兼容 `AI_TEST_API_KEY`)。 +2. **写操作与执行**必须带 `sto-` API Key(MCP 请求头 `Authorization: Bearer` 或 `X-API-Key`;HTTP 脚本可在 body 传 `api_key`,兼容旧名 `AI_TEST_API_KEY`)。 3. **接口**用 `api_upsert`;**流程**用 `workflow_upsert` 或 `workflow_patch_json`;**跑批**固定三步:`workflow_batch_create` → `workflow_batch_update` → `workflow_batch_run`。 4. **排障**:`workflow_run_get` 看 request/response → `workflow_run_loki_link` 查日志。 -5. 如果当前客户端没有显示某个工具,先确认平台 `GET /mcp/tools` 是否包含该工具,再刷新或重启 MCP 客户端。 -6. 人类可读安装与 curl 示例见 `docs/mcp_quickstart.md`;桥接安装见 `docs/mcp_install.md`。 +5. 如果当前客户端没有显示某个工具,先用 MCP `tools/list` 或 `GET /mcp/tools` 核对,再重启 MCP 客户端 / 会话。 +6. 人类可读安装见 `docs/mcp_install.md`;curl 示例见 `docs/mcp_quickstart.md`。 ### 0.1 工具来源与客户端刷新 - 权威工具清单来自后端 `app/main.py` 的 `MCP_TOOL_SPECS`,平台通过 `GET /mcp/tools` 对外暴露。 -- `mcp_bridge.py` 启动后会从 `QIP_BASE_URL`(或兼容 `AI_TEST_BASE_URL`)的 `/mcp/tools` 拉取工具并转换为 MCP `tools/list`。 +- 平台内置 Streamable HTTP MCP(`GET/POST /mcp`),`tools/list` 由 `MCP_TOOL_SPECS` 提供;兼容 HTTP 网关 `GET /mcp/tools`。 - Codex / Cursor / Claude 等客户端可能在会话启动时缓存工具列表;平台新增工具后,通常需要重启 MCP 客户端或重启会话。 - 如果客户端只显示部分工具,不代表平台没有该能力。先用 `/mcp/tools` 或本文档核对,再决定是否降级。 - Agent 更新本文档时,应以源码和 `/mcp/tools` 为准,不以单个客户端当前暴露的工具子集为准。 @@ -44,14 +44,14 @@ ### 2.1 平台地址 -MCP Bridge 通过环境变量访问平台: +Codex 等客户端在个人 MCP 配置中配置平台 URL 与 API Key,例如: -| 环境变量 | 含义 | 示例 | -|----------|------|------| -| `QIP_BASE_URL` | 质量检测平台后端地址(兼容 `AI_TEST_BASE_URL`) | `http://127.0.0.1:8000` | -| `QIP_API_KEY` | `sto-` 开头的 API Key(兼容 `AI_TEST_API_KEY`) | `sto-...` | +| 配置项 | 含义 | 示例 | +|--------|------|------| +| `url` | MCP 端点 | `http://10.20.30.42:8000/mcp` | +| `http_headers.Authorization` / `headers.Authorization` | `Bearer sto-...` | `Bearer sto-xxx` | -Agent 通过 MCP 工具访问平台,不需要在业务项目中启动平台服务;平台进程由质量检测平台项目本身或共享服务负责。 +Agent 通过 MCP 工具访问平台;平台进程由质量检测平台项目或共享内网服务负责。兼容环境变量名 `AI_TEST_API_KEY` 仅适用于旧 HTTP 脚本,MCP 客户端请用请求头传 Key。 ### 2.2 调用鉴权 @@ -92,6 +92,8 @@ Agent 通过 MCP 工具访问平台,不需要在业务项目中启动平台服 ## 4. 工具清单(按分类) +下文参数均为各工具的 **`arguments` 对象**(MCP `tools/call` 直接传该对象)。仅在使用 HTTP 网关时,外层再包一层 `{"tool":"<名称>","arguments":{...}}`,见 [mcp_quickstart.md](./mcp_quickstart.md)。 + ### 4.1 发现与目录 #### `catalog_snapshot` @@ -247,7 +249,7 @@ Agent 通过 MCP 工具访问平台,不需要在业务项目中启动平台服 #### `mcp_tool_upsert` -- **用途**:平台内「自定义 MCP 工具配置」表,**不是**本桥接工具列表本身。 +- **用途**:平台内「自定义 MCP 工具配置」表,**不是** Codex / Claude / Cursor 所见的内置 MCP 工具列表本身。 #### SSH 系列 @@ -289,6 +291,15 @@ Agent 通过 MCP 工具访问平台,不需要在业务项目中启动平台服 ## 6. 调用协议 +### 6.1 Codex / MCP 客户端(推荐) + +- **端点**:`http://10.20.30.42:8000/mcp`(Streamable HTTP) +- **鉴权**:请求头 `Authorization: Bearer sto-...`(Codex 配置在个人 `config.toml` 的 `mcp_servers..http_headers`) +- **工具发现**:MCP 方法 `tools/list`(与 `MCP_TOOL_SPECS` 一致) +- **工具调用**:MCP 方法 `tools/call`(`name` + `arguments`)→ `content[0].text` 为 JSON 字符串,解析后字段为 `ok` / `tool` / `data` / `error`;失败时 `isError` 可能为 true + +### 6.2 HTTP 网关(curl / CI) + - **发现**:`GET /mcp/tools` → `{ tools, require_api_key, ai_guide }` - **单次**:`POST /mcp/invoke` → `{ ok, tool, data, error }` - **批量**:`POST /mcp/invoke-batch` → `{ results: [...] }`,`stop_on_error: true` 时任一步失败即停 @@ -334,7 +345,7 @@ workflow_analyze_last_run → workflow_patch_json(只改变量/单节点 data) | error / 现象 | 处理 | |--------------|------| -| 401 MCP requires API Key | 配置 `QIP_API_KEY`(或 `AI_TEST_API_KEY`)或请求头 Bearer | +| 401 MCP requires API Key | Codex:个人 `config.toml` 中配置 `Authorization: Bearer sto-...`;curl:请求头或 body `api_key` | | workflow folder not registered | 先 `folder_ensure` workflows | | 仅 draft 批次可执行 | 已对 running/success 的 batch 再 run | | unknown mcp tool | `GET /mcp/tools` 刷新名称 | @@ -349,9 +360,9 @@ workflow_analyze_last_run → workflow_patch_json(只改变量/单节点 data) |------|------| | **本文** `docs/mcp_tools_for_ai.md` | **AI Agent(首选)** | | `docs/mcp_quickstart.md` | 人类 + curl 示例 | -| `docs/mcp_install.md` | MCP 桥接安装 | +| `docs/mcp_install.md` | MCP Server 安装、Codex/内网/远程环境 | | 项目根 `AGENTS.md` | Codex 自动加载的短指引 | --- -*文档版本:与仓库 `MCP_TOOL_SPECS` 同步(含 workflow_run_* / workflow_validate / 执行类鉴权)。* +*文档版本:与 `MCP_TOOL_SPECS`(28 个工具)同步;接入方式为内置 Streamable HTTP `/mcp`,见 `docs/mcp_install.md`。* diff --git a/mcp_bridge.py b/mcp_bridge.py deleted file mode 100644 index e72ecb2..0000000 --- a/mcp_bridge.py +++ /dev/null @@ -1,185 +0,0 @@ -"""Stdio MCP server that bridges to the local HTTP MCP gateway. - -It speaks the Model Context Protocol over stdin/stdout (newline-delimited -JSON-RPC 2.0) and forwards every tool call to: - - GET http://127.0.0.1:8000/mcp/tools - POST http://127.0.0.1:8000/mcp/invoke - -Run it with Cursor / Codex / Claude Desktop by configuring stdio MCP entry: - - { - "command": "python3", - "args": ["/abs/path/to/mcp_bridge.py"], - "env": {"AI_TEST_BASE_URL": "http://127.0.0.1:8000"} - } -""" - -from __future__ import annotations - -import json -import os -import sys -from typing import Any - -import httpx - -def _env(primary: str, legacy: str, default: str = "") -> str: - return (os.environ.get(primary) or os.environ.get(legacy) or default).strip() - - -BASE_URL = _env("QIP_BASE_URL", "AI_TEST_BASE_URL", "http://127.0.0.1:8000").rstrip("/") -API_KEY = _env("QIP_API_KEY", "AI_TEST_API_KEY", "") -SERVER_NAME = "quality-inspection-platform" -SERVER_VERSION = "0.1.0" -PROTOCOL_VERSION = "2024-11-05" - - -def log(msg: str) -> None: - sys.stderr.write(f"[mcp_bridge] {msg}\n") - sys.stderr.flush() - - -def write_message(message: dict[str, Any]) -> None: - sys.stdout.write(json.dumps(message, ensure_ascii=False) + "\n") - sys.stdout.flush() - - -def fetch_tools() -> list[dict[str, Any]]: - try: - response = httpx.get(f"{BASE_URL}/mcp/tools", headers=_request_headers(), timeout=10.0) - response.raise_for_status() - payload = response.json() or {} - tools = payload.get("tools", []) - except Exception as exc: - log(f"fetch tools failed: {exc}") - return [] - - converted = [] - for tool in tools: - converted.append( - { - "name": tool.get("name", ""), - "description": tool.get("description", ""), - "inputSchema": tool.get("input_schema") or {"type": "object"}, - } - ) - return converted - - -def _request_headers() -> dict[str, str]: - headers = {"Content-Type": "application/json"} - if API_KEY: - headers["Authorization"] = f"Bearer {API_KEY}" - return headers - - -def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]: - payload: dict[str, Any] = {"tool": name, "arguments": arguments or {}} - if API_KEY: - payload["api_key"] = API_KEY - try: - response = httpx.post( - f"{BASE_URL}/mcp/invoke", - json=payload, - headers=_request_headers(), - timeout=60.0, - ) - response.raise_for_status() - return response.json() or {} - except Exception as exc: - return {"ok": False, "tool": name, "data": {}, "error": str(exc)} - - -def make_response(req_id: Any, result: Any) -> dict[str, Any]: - return {"jsonrpc": "2.0", "id": req_id, "result": result} - - -def make_error(req_id: Any, code: int, message: str) -> dict[str, Any]: - return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}} - - -def handle_initialize(req_id: Any, _: dict[str, Any]) -> dict[str, Any]: - return make_response( - req_id, - { - "protocolVersion": PROTOCOL_VERSION, - "capabilities": {"tools": {"listChanged": False}}, - "serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION}, - "instructions": ( - "Quality inspection platform (质量检测平台). Before complex tasks, read project file " - "docs/mcp_tools_for_ai.md (tool picker, auth, recipes). " - "Typical start: catalog_snapshot. " - "Batch runs: workflow_batch_create → workflow_batch_update → workflow_batch_run. " - "Writes and execution require sto- API Key (AI_TEST_API_KEY or QIP_API_KEY)." - ), - }, - ) - - -def handle_tools_list(req_id: Any, _: dict[str, Any]) -> dict[str, Any]: - return make_response(req_id, {"tools": fetch_tools()}) - - -def handle_tools_call(req_id: Any, params: dict[str, Any]) -> dict[str, Any]: - name = str(params.get("name", "")) - arguments = params.get("arguments") or {} - if not name: - return make_error(req_id, -32602, "missing tool name") - - payload = call_tool(name, arguments) - text = json.dumps(payload, ensure_ascii=False, indent=2) - is_error = not bool(payload.get("ok", True)) - return make_response( - req_id, - { - "content": [{"type": "text", "text": text}], - "isError": is_error, - }, - ) - - -HANDLERS = { - "initialize": handle_initialize, - "tools/list": handle_tools_list, - "tools/call": handle_tools_call, -} - - -def main() -> None: - log(f"started, base_url={BASE_URL}") - for raw_line in sys.stdin: - line = raw_line.strip() - if not line: - continue - try: - message = json.loads(line) - except json.JSONDecodeError as exc: - log(f"invalid json: {exc}") - continue - - method = message.get("method") - req_id = message.get("id") - params = message.get("params") or {} - - if method is None: - continue - if "id" not in message: - # JSON-RPC notification, no response required. - continue - - handler = HANDLERS.get(method) - if handler is None: - write_message(make_error(req_id, -32601, f"method not found: {method}")) - continue - - try: - response = handler(req_id, params) - except Exception as exc: - log(f"handler error: {exc}") - response = make_error(req_id, -32000, str(exc)) - write_message(response) - - -if __name__ == "__main__": - main() diff --git a/requirements.txt b/requirements.txt index 417aa96..1414788 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ httpx jinja2 python-multipart paramiko +mcp>=1.6.0 diff --git a/start_project.command b/start_project.command index 3aba22c..4d4b20d 100755 --- a/start_project.command +++ b/start_project.command @@ -5,6 +5,7 @@ set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" BACKEND_PORT=8000 FRONTEND_PORT=5173 +VENV_PYTHON="$ROOT_DIR/.venv/bin/python" require_file() { local path="$1" @@ -25,6 +26,26 @@ require_command() { fi } +ensure_venv() { + if [[ ! -x "$VENV_PYTHON" ]]; then + echo "创建 Python 虚拟环境 (.venv)..." + require_command python3 + python3 -m venv "$ROOT_DIR/.venv" + fi + + if ! "$VENV_PYTHON" -c "import fastapi, mcp, uvicorn" >/dev/null 2>&1; then + echo "安装/更新 Python 依赖 (requirements.txt)..." + "$VENV_PYTHON" -m pip install -r "$ROOT_DIR/requirements.txt" + fi + + if ! "$VENV_PYTHON" -c "import fastapi, mcp, uvicorn" >/dev/null 2>&1; then + echo "Python 依赖安装失败,请检查网络或手动执行:" + echo " $VENV_PYTHON -m pip install -r requirements.txt" + read -r -k 1 "?按任意键关闭..." + exit 1 + fi +} + is_port_listening() { local port="$1" lsof -iTCP:"$port" -sTCP:LISTEN -t >/dev/null 2>&1 @@ -70,13 +91,15 @@ end run APPLESCRIPT } -require_file "$ROOT_DIR/.venv/bin/python" "未找到 Python 虚拟环境: .venv/bin/python" require_file "$ROOT_DIR/frontend-admin/package.json" "未找到前端工程: frontend-admin/package.json" +require_file "$ROOT_DIR/requirements.txt" "未找到 requirements.txt" require_command npm require_command curl require_command lsof require_command osascript +ensure_venv + if ! is_port_listening "$BACKEND_PORT"; then echo "启动后端..." open_backend_terminal @@ -93,14 +116,16 @@ fi echo "等待服务就绪..." -if ! wait_for_http "http://127.0.0.1:$BACKEND_PORT/health" 30; then +if ! wait_for_http "http://127.0.0.1:$BACKEND_PORT/health" 45; then echo "后端启动失败,请检查新打开的 Terminal 窗口" + echo "常见原因: 依赖未装全 → 执行: .venv/bin/python -m pip install -r requirements.txt" read -r -k 1 "?按任意键关闭..." exit 1 fi -if ! wait_for_http "http://127.0.0.1:$FRONTEND_PORT/" 30; then +if ! wait_for_http "http://127.0.0.1:$FRONTEND_PORT/" 45; then echo "前端启动失败,请检查新打开的 Terminal 窗口" + echo "常见原因: 未 npm install → 在 frontend-admin 目录执行 npm install" read -r -k 1 "?按任意键关闭..." exit 1 fi @@ -111,5 +136,6 @@ echo echo "启动成功" echo "前端: http://127.0.0.1:$FRONTEND_PORT/" echo "后端: http://127.0.0.1:$BACKEND_PORT/" +echo "MCP: http://127.0.0.1:$BACKEND_PORT/mcp" echo read -r -k 1 "?按任意键关闭..."