onmcp/scripts/test_cpp_multi_service_rout...

126 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
"$ROOT/scripts/build_cpp.sh" >/dev/null
mkdir -p "$ROOT/reports"
AMAP_PORT=19681
CAL_PORT=19682
MCPD_PORT=19680
"$ROOT/build/cpp/mock_mcp_server" --port "$AMAP_PORT" >"$ROOT/reports/multi_service_amap.log" 2>&1 &
AMAP_PID=$!
"$ROOT/build/cpp/mock_calendar_server" --port "$CAL_PORT" >"$ROOT/reports/multi_service_calendar.log" 2>&1 &
CAL_PID=$!
"$ROOT/build/cpp/mcpd" --port "$MCPD_PORT" \
--upstream "http://127.0.0.1:$AMAP_PORT/rpc" \
--service "calendar=http://127.0.0.1:$CAL_PORT/rpc,calendar." \
>"$ROOT/reports/multi_service_mcpd.log" 2>&1 &
MCPD_PID=$!
cleanup() {
kill "$MCPD_PID" "$AMAP_PID" "$CAL_PID" >/dev/null 2>&1 || true
wait "$MCPD_PID" "$AMAP_PID" "$CAL_PID" 2>/dev/null || true
}
trap cleanup EXIT
sleep 0.5
ROOT="$ROOT" MCPD_PORT="$MCPD_PORT" python3 - <<'PY'
import json
import os
import sys
import urllib.error
import urllib.request
root = os.environ["ROOT"]
endpoint = f"http://127.0.0.1:{os.environ['MCPD_PORT']}/rpc"
stats_endpoint = f"http://127.0.0.1:{os.environ['MCPD_PORT']}/stats"
seq = 0
checks = []
def rpc(method, params=None, token="demo-token"):
global seq
seq += 1
payload = {"jsonrpc": "2.0", "id": seq, "method": method, "params": params or {}}
req = urllib.request.Request(
endpoint,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json", "X-Agent-Token": token},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
return resp.status, json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8")
try:
parsed = json.loads(body)
except json.JSONDecodeError:
parsed = {"raw": body}
return e.code, parsed
except urllib.error.URLError as e:
return 0, {"error": str(e)}
def stats():
with urllib.request.urlopen(stats_endpoint, timeout=5) as resp:
return json.loads(resp.read().decode("utf-8"))["stats"]
def check(name, condition, detail):
checks.append({"name": name, "pass": bool(condition), "detail": detail})
print(("[PASS]" if condition else "[FAIL]"), name, detail)
import time
for i in range(50):
code_ready, body_ready = rpc("tools/list", {"compact": True})
if code_ready == 200 and body_ready.get("result", {}).get("tools") is not None:
break
time.sleep(0.1)
else:
print("[FAIL] readiness", {"code": code_ready, "body": body_ready})
sys.exit(1)
code_amap, amap = rpc("tools/list", {"service_id": "amap", "compact": True})
amap_names = [t.get("name", "") for t in amap.get("result", {}).get("tools", [])]
check("tools/list filters amap", code_amap == 200 and amap_names and all(n.startswith("amap.") for n in amap_names), amap_names)
code_cal, cal = rpc("tools/list", {"service_id": "calendar", "compact": True})
cal_names = [t.get("name", "") for t in cal.get("result", {}).get("tools", [])]
check("tools/list filters calendar", code_cal == 200 and cal_names and all(n.startswith("calendar.") for n in cal_names), cal_names)
code_all, all_tools = rpc("tools/list", {"compact": True})
all_names = [t.get("name", "") for t in all_tools.get("result", {}).get("tools", [])]
check("tools/list all services", code_all == 200 and any(n.startswith("amap.") for n in all_names) and any(n.startswith("calendar.") for n in all_names), all_names)
code_weather_acl, weather_acl = rpc("tools/list", {"service_id": "calendar", "compact": True}, token="weather-token")
check("weather-token denied calendar service", code_weather_acl == 403, {"code": code_weather_acl, "body": weather_acl})
code_call1, call1 = rpc("tools/call", {"name": "calendar.list_events", "arguments": {"date": "2026-05-14", "limit": 2}})
code_call2, call2 = rpc("tools/call", {"name": "calendar.list_events", "arguments": {"date": "2026-05-14", "limit": 2}})
check("calendar call routed", code_call1 == 200 and call1.get("service_id") == "calendar" and call1.get("cached") is False, call1)
check("calendar cache hit routed", code_call2 == 200 and call2.get("service_id") == "calendar" and call2.get("cached") is True, call2)
code_cross, cross = rpc("tools/call", {"name": "amap.maps_weather", "arguments": {"city": "苏州"}}, token="calendar-token")
check("calendar-token denied amap service", code_cross == 403, {"code": code_cross, "body": cross})
code_sw, sw = rpc("ohmcp/session.switch_service", {"session_id": "user-001", "to_service": "calendar", "task_id": "meeting-plan"})
check("switch service calendar", code_sw == 200 and sw.get("result", {}).get("current_service") == "calendar", sw)
code_session_list, session_list = rpc("tools/list", {"session_id": "user-001", "compact": True})
session_names = [t.get("name", "") for t in session_list.get("result", {}).get("tools", [])]
check("session tools/list uses current service", code_session_list == 200 and session_names and all(n.startswith("calendar.") for n in session_names), session_names)
st = stats()
check("stats sees two services", st.get("services") == 2 and st.get("service_switches") == 1, st)
summary = {
"test": "multi-service mcp router",
"pass": sum(1 for c in checks if c["pass"]),
"fail": sum(1 for c in checks if not c["pass"]),
"checks": checks,
"final_stats": st,
}
out = os.path.join(root, "reports", "multi_service_router_summary.json")
with open(out, "w", encoding="utf-8") as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print("summary:", out)
if summary["fail"]:
sys.exit(1)
PY