126 lines
5.5 KiB
Bash
Executable File
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
|