feat: add qmt_list_apis MCP tool (OpenAPI endpoint discovery for coding agents)
This commit is contained in:
@@ -173,10 +173,80 @@ def _tool_orders(args):
|
||||
def _tool_trades(args):
|
||||
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 =================
|
||||
# spec = what tools/list returns (name/description/inputSchema)
|
||||
# fn = handler(args dict) -> plain-JSON result
|
||||
_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": {
|
||||
"name": "qmt_kline",
|
||||
|
||||
+27
-5
@@ -96,16 +96,38 @@ check("initialize", st == 200 and r["result"]["protocolVersion"] == "2025-06-18"
|
||||
and r["result"]["capabilities"].get("tools") is not None,
|
||||
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"})
|
||||
tools = r["result"]["tools"]
|
||||
names = [t["name"] for t in tools]
|
||||
check("tools/list count", len(tools) == 9, str(names))
|
||||
for need in ["qmt_kline", "qmt_quote", "qmt_tick", "qmt_instrument",
|
||||
"qmt_trading_dates", "qmt_positions", "qmt_asset",
|
||||
"qmt_orders", "qmt_trades"]:
|
||||
check("tools/list count", len(tools) == 10, str(names))
|
||||
for need in ["qmt_list_apis", "qmt_kline", "qmt_quote", "qmt_tick",
|
||||
"qmt_instrument", "qmt_trading_dates", "qmt_positions",
|
||||
"qmt_asset", "qmt_orders", "qmt_trades"]:
|
||||
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
|
||||
st, r = post({"jsonrpc": "2.0", "id": 3, "method": "tools/call",
|
||||
"params": {"name": "qmt_kline",
|
||||
|
||||
Reference in New Issue
Block a user