feat: add qmt_list_apis MCP tool (OpenAPI endpoint discovery for coding agents)

This commit is contained in:
Docker
2026-08-26 17:44:06 +08:00
parent 9ed025a1d1
commit 527dcf672b
2 changed files with 97 additions and 5 deletions
+70
View File
@@ -173,10 +173,80 @@ def _tool_orders(args):
def _tool_trades(args): def _tool_trades(args):
return {"ok": True, "data": get_trade_data("deal")} return {"ok": True, "data": get_trade_data("deal")}
# ================= OPENAPI DISCOVERY TOOL =================
# qmt_list_apis: lets a coding agent inspect which HTTP endpoints the bridge
# exposes. Data source = the deployed openapi.json (same file served by
# GET /openapi.json), so the tool always reflects the current spec.
import os
def _load_openapi():
"""Load the deployed openapi.json next to this module. Returns dict or None."""
try:
deploy_dir = os.path.dirname(os.path.abspath(__file__))
path = os.path.join(deploy_dir, "openapi.json")
if not os.path.exists(path):
return None
with open(path, "rb") as f:
return json.loads(f.read().decode("utf-8"))
except Exception as exc:
_log_err("mcp.load_openapi", exc)
return None
def _openapi_summary(doc):
"""Build a compact endpoint list from the spec (paths + methods + summary)."""
paths = doc.get("paths") or {}
out = []
for path in sorted(paths.keys()):
item = paths[path]
for method in ("get", "post", "put", "delete"):
op = item.get(method)
if not op:
continue
out.append({
"path": path,
"method": method.upper(),
"summary": op.get("summary", ""),
})
return out
def _tool_list_apis(args):
"""List the bridge's HTTP API surface from the deployed OpenAPI spec.
detail=true returns the full spec (paths/params/examples, ~40KB);
default returns a compact {path, method, summary} list.
"""
doc = _load_openapi()
if doc is None:
raise McpError(-32603, "openapi.json not deployed next to bridge module")
detail = bool(args.get("detail"))
if detail:
return {"ok": True, "title": doc.get("info", {}).get("title"),
"version": doc.get("info", {}).get("version"),
"spec": doc}
return {"ok": True, "title": doc.get("info", {}).get("title"),
"version": doc.get("info", {}).get("version"),
"endpoints": _openapi_summary(doc)}
# ================= TOOL REGISTRY ================= # ================= TOOL REGISTRY =================
# spec = what tools/list returns (name/description/inputSchema) # spec = what tools/list returns (name/description/inputSchema)
# fn = handler(args dict) -> plain-JSON result # fn = handler(args dict) -> plain-JSON result
_TOOLS = [ _TOOLS = [
{
"spec": {
"name": "qmt_list_apis",
"description": "List the bridge HTTP API surface from the deployed OpenAPI spec. "
"Use this FIRST to discover available endpoints before calling other tools. "
"detail=true returns the full spec (~40KB); default returns compact path/method/summary.",
"inputSchema": {
"type": "object",
"properties": {
"detail": {"type": "boolean",
"description": "true = full OpenAPI spec, false (default) = compact endpoint list"},
},
},
},
"fn": _tool_list_apis,
},
{ {
"spec": { "spec": {
"name": "qmt_kline", "name": "qmt_kline",
+27 -5
View File
@@ -96,16 +96,38 @@ check("initialize", st == 200 and r["result"]["protocolVersion"] == "2025-06-18"
and r["result"]["capabilities"].get("tools") is not None, and r["result"]["capabilities"].get("tools") is not None,
str(r)) str(r))
# 2. tools/list -> 9 tools # 2. tools/list -> 10 tools (9 query + qmt_list_apis)
st, r = post({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}) st, r = post({"jsonrpc": "2.0", "id": 2, "method": "tools/list"})
tools = r["result"]["tools"] tools = r["result"]["tools"]
names = [t["name"] for t in tools] names = [t["name"] for t in tools]
check("tools/list count", len(tools) == 9, str(names)) check("tools/list count", len(tools) == 10, str(names))
for need in ["qmt_kline", "qmt_quote", "qmt_tick", "qmt_instrument", for need in ["qmt_list_apis", "qmt_kline", "qmt_quote", "qmt_tick",
"qmt_trading_dates", "qmt_positions", "qmt_asset", "qmt_instrument", "qmt_trading_dates", "qmt_positions",
"qmt_orders", "qmt_trades"]: "qmt_asset", "qmt_orders", "qmt_trades"]:
check("tool %s" % need, need in names) check("tool %s" % need, need in names)
# 2b. tools/call qmt_list_apis (needs openapi.json next to src module)
import shutil, os as _os
SRC = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..", "src")
SPEC_SRC = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..",
"docs", "api_spec", "openapi.json")
SPEC_DST = _os.path.join(SRC, "openapi.json")
if _os.path.exists(SPEC_SRC):
shutil.copy(SPEC_SRC, SPEC_DST)
st, r = post({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "qmt_list_apis", "arguments": {}}})
text = json.loads(r["result"]["content"][0]["text"])
check("qmt_list_apis compact", text["ok"] is True
and len(text["endpoints"]) == 13, str(text)[:150])
st, r = post({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "qmt_list_apis", "arguments": {"detail": True}}})
text = json.loads(r["result"]["content"][0]["text"])
check("qmt_list_apis detail", text["ok"] is True
and "paths" in text["spec"] and "/mcp" in text["spec"]["paths"],
str(text)[:150])
if _os.path.exists(SPEC_DST):
_os.remove(SPEC_DST)
# 3. tools/call qmt_kline # 3. tools/call qmt_kline
st, r = post({"jsonrpc": "2.0", "id": 3, "method": "tools/call", st, r = post({"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "qmt_kline", "params": {"name": "qmt_kline",