Files
tick-stock-panel/mcp-server/smoke_test.py
T
shy3130 826eed8cf4 feat(mcp): MCP 服务器 — 开放接口的 AI 工具桥 (Model Context Protocol)
mcp-server/ 独立薄桥包 (MCP SDK 2.x, stdio 传输):
- 12 个精选工具: 标的搜索/日K/指数/市场总览/环境阶段/策略清单与定义/
  告警/扩展表查读写/策略回测 — 按 TSP_SCOPES 声明暴露, 权限裁决仍在
  面板网关 (桥零业务逻辑, 面板升级自动受益)
- 模拟盘交易类端点刻意不包装 (不建议把下单交给 AI)
- smoke_test.py 完整协议冒烟: 握手/tools列表/真实调用; 实测只读 Token
  暴露 10 工具、全量 Token 暴露 12 工具, 三次调用 (搜索/总览/环境) 全通
- README: Claude Desktop 等客户端配置示例; features.md 增 MCP 接入小节
2026-09-26 12:47:08 +08:00

92 lines
2.9 KiB
Python

"""MCP 服务器冒烟测试 — 子进程拉起, 走完整 JSON-RPC: 握手/工具清单/实际调用。
用法: TSP_TOKEN=tsp_xxx [TSP_BASE=...] python smoke_test.py
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
def main() -> None:
proc = subprocess.Popen(
[sys.executable, "-m", "tsp_mcp_server.server"],
cwd=os.path.join(BASE_DIR, "src"),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
env=os.environ,
)
next_id = 0
def send(method: str, params: dict | None = None, *, notify: bool = False) -> dict | None:
nonlocal next_id
msg: dict = {"jsonrpc": "2.0", "method": method}
if params is not None:
msg["params"] = params
if not notify:
next_id += 1
msg["id"] = next_id
proc.stdin.write(json.dumps(msg) + "\n")
proc.stdin.flush()
if notify:
return None
# 读到带对应 id 的响应 (跳过日志/通知行)
while True:
line = proc.stdout.readline()
if not line:
err = proc.stderr.read()
raise RuntimeError(f"MCP server 提前退出: {err[-500:]}")
try:
resp = json.loads(line)
except json.JSONDecodeError:
continue
if resp.get("id") == next_id:
if "error" in resp:
raise RuntimeError(f"JSON-RPC 错误: {resp['error']}")
return resp["result"]
try:
# 1. 握手
init = send("initialize", {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "smoke", "version": "0"},
})
print("server:", init["serverInfo"]["name"])
send("notifications/initialized", notify=True)
# 2. 工具清单
tools = send("tools/list")["tools"]
print(f"tools ({len(tools)}):", ", ".join(sorted(t["name"] for t in tools)))
names = {t["name"] for t in tools}
# 3. 实际调用: 搜索 + 总览 + 环境
calls = [
("search_instrument", {"query": "600519", "limit": 2}, "贵州茅台"),
("get_market_overview", {}, "as_of"),
("get_regime_state", {}, "row"),
]
for name, args, expect in calls:
if name not in names:
print(f"skip {name} (scope 未开放)")
continue
r = send("tools/call", {"name": name, "arguments": args})
text = r["content"][0]["text"]
assert expect in text, f"{name} 返回异常: {text[:200]}"
print(f"{name}: OK -> {text[:100]}")
print("SMOKE PASS")
finally:
proc.terminate()
proc.wait(timeout=5)
if __name__ == "__main__":
main()