243 lines
8.6 KiB
Bash
Executable File
243 lines
8.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Lightweight MCP smoke test for HTTP MCP servers.
|
|
# Default target: local Brave MCP server.
|
|
|
|
MCP_URL="${MCP_URL:-http://192.168.153.113:18802/mcp}"
|
|
TIMEOUT_SEC="${TIMEOUT_SEC:-10}"
|
|
BASELINE_FILE="${BASELINE_FILE:-memory/mcp-smoke-tools-baseline.txt}"
|
|
PROBE_QUERY="${PROBE_QUERY:-openclaw}"
|
|
UPDATE_BASELINE=0
|
|
SKIP_TOOL_CALL=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--url)
|
|
MCP_URL="$2"; shift 2 ;;
|
|
--timeout)
|
|
TIMEOUT_SEC="$2"; shift 2 ;;
|
|
--baseline)
|
|
BASELINE_FILE="$2"; shift 2 ;;
|
|
--query)
|
|
PROBE_QUERY="$2"; shift 2 ;;
|
|
--update-baseline)
|
|
UPDATE_BASELINE=1; shift ;;
|
|
--skip-tool-call)
|
|
SKIP_TOOL_CALL=1; shift ;;
|
|
-h|--help)
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [options]
|
|
--url <mcp_url> MCP endpoint (default: ${MCP_URL})
|
|
--timeout <seconds> Curl timeout (default: ${TIMEOUT_SEC})
|
|
--baseline <path> Baseline tool-name file (default: ${BASELINE_FILE})
|
|
--query <text> Query used for brave_web_search probe (default: ${PROBE_QUERY})
|
|
--skip-tool-call Skip tools/call probe
|
|
--update-baseline Save current tool names as baseline
|
|
EOF
|
|
exit 0 ;;
|
|
*)
|
|
echo "Unknown arg: $1" >&2
|
|
exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
TS_DAY="$(date -u +%F)"
|
|
TS_STAMP="$(date -u +%H%M%S)"
|
|
ARTIFACT_DIR="${MCP_SMOKE_OUTPUT_DIR:-/tmp/openclaw-mcp-smoke}/${TS_DAY}/${TS_STAMP}"
|
|
mkdir -p "$ARTIFACT_DIR"
|
|
|
|
NOW=()
|
|
WATCH=()
|
|
NEXT=()
|
|
P1=0
|
|
P2=0
|
|
|
|
add_now(){ NOW+=("$1"); }
|
|
add_watch(){ WATCH+=("$1"); }
|
|
add_next(){ NEXT+=("$1"); }
|
|
mark_p1(){ P1=$((P1+1)); }
|
|
mark_p2(){ P2=$((P2+1)); }
|
|
|
|
ms_now() { date +%s%3N; }
|
|
|
|
# 1) initialize
|
|
init_headers="$ARTIFACT_DIR/init.headers"
|
|
init_body="$ARTIFACT_DIR/init.body"
|
|
init_start="$(ms_now)"
|
|
if ! curl -sS -m "$TIMEOUT_SEC" -D "$init_headers" -o "$init_body" \
|
|
-H 'Accept: text/event-stream, application/json' \
|
|
-H 'Content-Type: application/json' \
|
|
-X POST "$MCP_URL" \
|
|
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"mcp-smoke","version":"1.0"}}}' \
|
|
2>"$ARTIFACT_DIR/init.err"; then
|
|
add_now "P1 initialize request failed (${MCP_URL})"
|
|
mark_p1
|
|
add_next "Check MCP endpoint reachability and auth requirements"
|
|
fi
|
|
init_ms=$(( $(ms_now) - init_start ))
|
|
|
|
session_id="$(awk -F': ' 'tolower($1)=="mcp-session-id" {gsub(/\r/,"",$2); print $2}' "$init_headers" | tail -n1 || true)"
|
|
init_data_line="$(grep '^data: ' "$init_body" | tail -n1 | sed 's/^data: //' || true)"
|
|
|
|
if [[ -z "$session_id" ]]; then
|
|
add_now "P1 initialize succeeded without mcp-session-id header"
|
|
mark_p1
|
|
add_next "Confirm endpoint is MCP over HTTP (streamable)"
|
|
else
|
|
add_watch "P4 initialize OK (${init_ms}ms)"
|
|
fi
|
|
|
|
if [[ -n "$init_data_line" ]] && jq -e '.error' >/dev/null 2>&1 <<<"$init_data_line"; then
|
|
init_err_msg="$(jq -r '.error.message // "unknown initialize error"' <<<"$init_data_line")"
|
|
add_now "P1 initialize error: ${init_err_msg}"
|
|
mark_p1
|
|
add_next "Verify MCP auth/API key configuration"
|
|
fi
|
|
|
|
# 2) notifications/initialized (best effort)
|
|
if [[ -n "$session_id" ]]; then
|
|
curl -sS -m "$TIMEOUT_SEC" -D "$ARTIFACT_DIR/initialized.headers" -o "$ARTIFACT_DIR/initialized.body" \
|
|
-H "mcp-session-id: ${session_id}" \
|
|
-H 'Accept: text/event-stream, application/json' \
|
|
-H 'Content-Type: application/json' \
|
|
-X POST "$MCP_URL" \
|
|
--data '{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}' \
|
|
> /dev/null 2>"$ARTIFACT_DIR/initialized.err" || true
|
|
fi
|
|
|
|
# 3) tools/list
|
|
tools_names_file="$ARTIFACT_DIR/tools.current.txt"
|
|
tools_ms=0
|
|
if [[ -n "$session_id" ]]; then
|
|
tools_start="$(ms_now)"
|
|
if curl -sS -m "$TIMEOUT_SEC" -o "$ARTIFACT_DIR/tools.body" \
|
|
-H "mcp-session-id: ${session_id}" \
|
|
-H 'Accept: text/event-stream, application/json' \
|
|
-H 'Content-Type: application/json' \
|
|
-X POST "$MCP_URL" \
|
|
--data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
|
|
2>"$ARTIFACT_DIR/tools.err"; then
|
|
tools_ms=$(( $(ms_now) - tools_start ))
|
|
grep '^data: ' "$ARTIFACT_DIR/tools.body" | sed 's/^data: //' | tail -n1 > "$ARTIFACT_DIR/tools.json" || true
|
|
if jq -e '.error' "$ARTIFACT_DIR/tools.json" >/dev/null 2>&1; then
|
|
msg="$(jq -r '.error.message // "tools/list failed"' "$ARTIFACT_DIR/tools.json")"
|
|
add_now "P1 tools/list error: ${msg}"
|
|
mark_p1
|
|
add_next "Check MCP upstream provider credentials"
|
|
else
|
|
jq -r '.result.tools[]?.name' "$ARTIFACT_DIR/tools.json" | sort -u > "$tools_names_file"
|
|
tool_count="$(wc -l < "$tools_names_file" | tr -d ' ')"
|
|
add_watch "P4 tools/list OK (${tools_ms}ms, ${tool_count} tools)"
|
|
fi
|
|
else
|
|
add_now "P1 tools/list request failed"
|
|
mark_p1
|
|
add_next "Inspect MCP server logs and network path"
|
|
fi
|
|
fi
|
|
|
|
# 4) optional tool probe (auth + runtime)
|
|
if (( SKIP_TOOL_CALL == 0 )) && [[ -n "$session_id" ]] && [[ -s "$tools_names_file" ]]; then
|
|
if grep -qx 'brave_web_search' "$tools_names_file"; then
|
|
call_start="$(ms_now)"
|
|
if curl -sS -m "$TIMEOUT_SEC" -o "$ARTIFACT_DIR/tool-call.body" \
|
|
-H "mcp-session-id: ${session_id}" \
|
|
-H 'Accept: text/event-stream, application/json' \
|
|
-H 'Content-Type: application/json' \
|
|
-X POST "$MCP_URL" \
|
|
--data "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"brave_web_search\",\"arguments\":{\"query\":\"${PROBE_QUERY}\",\"count\":1}}}" \
|
|
2>"$ARTIFACT_DIR/tool-call.err"; then
|
|
call_ms=$(( $(ms_now) - call_start ))
|
|
grep '^data: ' "$ARTIFACT_DIR/tool-call.body" | sed 's/^data: //' | tail -n1 > "$ARTIFACT_DIR/tool-call.json" || true
|
|
if jq -e '.error' "$ARTIFACT_DIR/tool-call.json" >/dev/null 2>&1; then
|
|
msg="$(jq -r '.error.message // "tools/call failed"' "$ARTIFACT_DIR/tool-call.json")"
|
|
add_now "P1 tools/call error: ${msg}"
|
|
mark_p1
|
|
add_next "Verify Brave API key/plan and outbound internet access"
|
|
else
|
|
add_watch "P4 tools/call brave_web_search OK (${call_ms}ms)"
|
|
fi
|
|
else
|
|
add_now "P1 tools/call request failed"
|
|
mark_p1
|
|
add_next "Check MCP service health and external API reachability"
|
|
fi
|
|
else
|
|
add_watch "P3 brave_web_search not present; skipped tools/call probe"
|
|
fi
|
|
fi
|
|
|
|
# 5) tool-list drift
|
|
if [[ -s "$tools_names_file" ]]; then
|
|
if [[ -f "$BASELINE_FILE" ]]; then
|
|
sort -u "$BASELINE_FILE" > "$ARTIFACT_DIR/tools.baseline.sorted.txt"
|
|
comm -13 "$ARTIFACT_DIR/tools.baseline.sorted.txt" "$tools_names_file" > "$ARTIFACT_DIR/tools.added.txt" || true
|
|
comm -23 "$ARTIFACT_DIR/tools.baseline.sorted.txt" "$tools_names_file" > "$ARTIFACT_DIR/tools.removed.txt" || true
|
|
|
|
added_n="$(wc -l < "$ARTIFACT_DIR/tools.added.txt" | tr -d ' ')"
|
|
removed_n="$(wc -l < "$ARTIFACT_DIR/tools.removed.txt" | tr -d ' ')"
|
|
if (( added_n > 0 || removed_n > 0 )); then
|
|
add_watch "P2 Tool-list drift detected (+${added_n}/-${removed_n})"
|
|
mark_p2
|
|
add_next "Review drift and update baseline if expected"
|
|
else
|
|
add_watch "P4 Tool list matches baseline"
|
|
fi
|
|
else
|
|
if (( UPDATE_BASELINE == 1 )); then
|
|
add_watch "P4 Baseline bootstrap mode (creating ${BASELINE_FILE})"
|
|
else
|
|
add_watch "P3 No baseline file yet (${BASELINE_FILE})"
|
|
add_next "Run with --update-baseline after confirming current tool list"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if (( UPDATE_BASELINE == 1 )) && [[ -s "$tools_names_file" ]]; then
|
|
mkdir -p "$(dirname "$BASELINE_FILE")"
|
|
cp "$tools_names_file" "$BASELINE_FILE"
|
|
add_watch "P4 Baseline updated: ${BASELINE_FILE}"
|
|
fi
|
|
|
|
# 6) mcporter quick config signal (optional)
|
|
if command -v mcporter >/dev/null 2>&1; then
|
|
if mcporter list --json >"$ARTIFACT_DIR/mcporter-list.json" 2>"$ARTIFACT_DIR/mcporter-list.err"; then
|
|
configured="$(jq -r '(.servers // []) | length' "$ARTIFACT_DIR/mcporter-list.json" 2>/dev/null || echo 0)"
|
|
add_watch "P4 mcporter configured servers: ${configured}"
|
|
fi
|
|
fi
|
|
|
|
VERDICT="OK"
|
|
EXIT_CODE=0
|
|
if (( P1 > 0 )); then
|
|
VERDICT="NEEDS_ATTENTION"
|
|
EXIT_CODE=2
|
|
elif (( P2 > 0 )); then
|
|
VERDICT="MONITOR"
|
|
EXIT_CODE=1
|
|
fi
|
|
|
|
{
|
|
echo "Verdict: ${VERDICT}"
|
|
echo "Counts: p1=${P1} p2=${P2}"
|
|
echo "Endpoint: ${MCP_URL}"
|
|
echo "Session: ${session_id:-none}"
|
|
echo "Artifact path: ${ARTIFACT_DIR}"
|
|
echo
|
|
echo "Now:"
|
|
if (( ${#NOW[@]} == 0 )); then echo "- P4 Nothing urgent"; else for x in "${NOW[@]}"; do echo "- ${x}"; done; fi
|
|
echo
|
|
echo "Watch:"
|
|
if (( ${#WATCH[@]} == 0 )); then echo "- P4 No watch items"; else for x in "${WATCH[@]}"; do echo "- ${x}"; done; fi
|
|
echo
|
|
echo "Next actions:"
|
|
if (( ${#NEXT[@]} == 0 )); then
|
|
echo "- Keep current baseline and run periodically"
|
|
else
|
|
printf '%s\n' "${NEXT[@]}" | awk '!seen[$0]++' | sed 's/^/- /'
|
|
fi
|
|
} | tee "$ARTIFACT_DIR/summary.txt"
|
|
|
|
exit "$EXIT_CODE"
|