quality-inspection-platform/mcp_bridge.py
qihongkun a41f61bcf6 重命名项目为 quality-inspection-platform
将仓库目录从 ai_auto_test 迁至 quality-inspection-platform,统一质量检测平台命名;MCP 环境变量新增 QIP_* 并兼容 AI_TEST_*。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 15:49:59 +08:00

186 lines
5.5 KiB
Python

"""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()