feat: 添加 Docker 部署支持和 MCP 原生集成
- 添加 Dockerfile、docker-compose.yml 和 Jenkins 流水线配置 - 新增 MCP 原生集成模块 (mcp_native.py) - 移除旧的 mcp_bridge.py,更新依赖和文档 - 添加部署文档 (Docker 和服务器部署指南) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a41f61bcf6
commit
f5143aee09
35
.dockerignore
Normal file
35
.dockerignore
Normal file
@ -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
|
||||
26
.env.example
Normal file
26
.env.example
Normal file
@ -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
|
||||
@ -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`
|
||||
|
||||
41
Dockerfile
Normal file
41
Dockerfile
Normal file
@ -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"]
|
||||
180
Jenkinsfile
vendored
Normal file
180
Jenkinsfile
vendored
Normal file
@ -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() }
|
||||
}
|
||||
}
|
||||
58
README.md
58
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
|
||||
```
|
||||
|
||||
## 当前已知情况
|
||||
|
||||
@ -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)
|
||||
|
||||
34
app/main.py
34
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]] = []
|
||||
|
||||
158
app/mcp_native.py
Normal file
158
app/mcp_native.py
Normal file
@ -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)
|
||||
@ -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:
|
||||
|
||||
33
docker-compose.yml
Normal file
33
docker-compose.yml
Normal file
@ -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
|
||||
6
docker/entrypoint.sh
Normal file
6
docker/entrypoint.sh
Normal file
@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
mkdir -p /data/ssh-scripts
|
||||
|
||||
exec "$@"
|
||||
242
docs/deploy_docker.md
Normal file
242
docs/deploy_docker.md
Normal file
@ -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)
|
||||
481
docs/deploy_server.md
Normal file
481
docs/deploy_server.md
Normal file
@ -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 <url> .
|
||||
```
|
||||
|
||||
**方式 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
|
||||
```
|
||||
@ -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 = "<QIP_BASE_URL>/mcp"` + `http_headers.Authorization = "Bearer <Key>"` |
|
||||
|
||||
迁移后只需保证 **`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 协议共用同一套工具实现。
|
||||
|
||||
@ -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 <api_key>` / `X-API-Key: <api_key>`。
|
||||
- MCP 鉴权:在平台右上角「个人中心」生成 `sto-` 开头的 API Key;Codex 在 `~/.codex/config.toml` 的 `http_headers.Authorization` 中配置,curl/脚本可用 `Authorization: Bearer <api_key>` / `X-API-Key: <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 <sto-api-key>" \
|
||||
-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 <sto-api-key>" \
|
||||
-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 <sto-api-key>" \
|
||||
-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 <sto-api-key>" \
|
||||
-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 <sto-api-key>" \
|
||||
-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 <sto-api-key>" \
|
||||
-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 <sto-api-key>" \
|
||||
-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 <sto-api-key>" \
|
||||
-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 <sto-api-key>" \
|
||||
-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",
|
||||
|
||||
@ -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.<name>.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`。*
|
||||
|
||||
185
mcp_bridge.py
185
mcp_bridge.py
@ -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()
|
||||
@ -6,3 +6,4 @@ httpx
|
||||
jinja2
|
||||
python-multipart
|
||||
paramiko
|
||||
mcp>=1.6.0
|
||||
|
||||
@ -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 "?按任意键关闭..."
|
||||
|
||||
Loading…
Reference in New Issue
Block a user