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):
|
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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user