Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
118 lines
4.9 KiB
Bash
118 lines
4.9 KiB
Bash
#!/usr/bin/env bash
|
||
# Shared helpers for the MCP integration scripts (mcp-smoke.sh, wp-basis-smoke.sh,
|
||
# and Etappe 1b's smoke scripts). Source it from a script that runs INSIDE the DDEV
|
||
# web container with /var/www/html as its working directory (cd there before
|
||
# sourcing – HOST below shells out to `wp` immediately).
|
||
# Requires jq and curl (both present in the DDEV web image).
|
||
|
||
PLUGIN=wp-content/plugins/WP-LIT-MCP
|
||
ENDPOINT=http://localhost/wp-json/mcp/mcp-oauth-server
|
||
|
||
# The site's own Host header (without scheme). A request that bypasses normal WP
|
||
# routing (e.g. the raw discovery rewrite target mcp-smoke.sh probes) needs this
|
||
# Host header, or WordPress 301s it to the canonical host before it ever reaches
|
||
# the code under test.
|
||
HOST=$(wp option get home | sed -E 's#^[a-zA-Z]+://##')
|
||
|
||
TOKEN=""
|
||
SESSION=""
|
||
INIT_RESPONSE=""
|
||
REQUEST_ID=100
|
||
|
||
fail() { echo "FAIL: $*"; exit 1; }
|
||
pass() { echo "ok - $*"; }
|
||
|
||
# Removes every "lit-mcp-integration-test" application password of a user.
|
||
cleanup_app_passwords_for() { # $1 = user_login
|
||
local login="$1"
|
||
[ -n "$login" ] || return 0
|
||
local escaped=${login//\\/\\\\}
|
||
escaped=${escaped//\'/\\\'}
|
||
wp eval "
|
||
\$u = get_user_by('login', '${escaped}');
|
||
if (\$u) {
|
||
foreach (WP_Application_Passwords::get_user_application_passwords(\$u->ID) as \$pw) {
|
||
if ((\$pw['name'] ?? '') === 'lit-mcp-integration-test') {
|
||
WP_Application_Passwords::delete_application_password(\$u->ID, \$pw['uuid']);
|
||
}
|
||
}
|
||
}
|
||
" >/dev/null 2>&1 || true
|
||
}
|
||
|
||
# Mints an MCP access token for a user, exactly like mcp-oauth's TokenEndpoint does
|
||
# after a completed OAuth flow (mint-token.php). Also creates the throwaway
|
||
# "lit-mcp-integration-test" application password cleanup_app_passwords_for removes.
|
||
mint_token() { # $1 = user_login -> prints the token
|
||
wp eval-file "$PLUGIN/tests/Integration/mint-token.php" "$1"
|
||
}
|
||
|
||
# Raw JSON-RPC POST to the MCP endpoint; prints headers+body (-i). $3 (session id)
|
||
# is optional. Low-level building block: used directly where a script needs the
|
||
# response headers (e.g. to read Mcp-Session-Id off the initialize response) and by
|
||
# rpc() below for everything after that.
|
||
mcp() { # $1 = token, $2 = json body, $3 = session id (optional)
|
||
local session_header=()
|
||
[ -n "${3:-}" ] && session_header=(-H "Mcp-Session-Id: $3")
|
||
curl -s --max-time 20 -i -X POST "$ENDPOINT" \
|
||
-H "Authorization: Bearer $1" \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Accept: application/json, text/event-stream' \
|
||
"${session_header[@]}" -d "$2"
|
||
}
|
||
|
||
# Mints a token for the user and opens an MCP session. Sets TOKEN, SESSION and
|
||
# INIT_RESPONSE (raw initialize response incl. headers).
|
||
connect() { # $1 = user_login
|
||
TOKEN=$(mint_token "$1")
|
||
local init
|
||
init=$(mcp "$TOKEN" '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}')
|
||
SESSION=$(echo "$init" | grep -i '^mcp-session-id:' | awk '{print $2}' | tr -d '\r' || true)
|
||
INIT_RESPONSE="$init"
|
||
echo "$init" | grep -q '"protocolVersion"' || fail "initialize failed: $init"
|
||
}
|
||
|
||
# JSON-RPC call over the session connect() opened; prints the response body only.
|
||
rpc() { # $1 = method, $2 = params JSON
|
||
REQUEST_ID=$((REQUEST_ID + 1))
|
||
local body
|
||
body=$(jq -cn --arg m "$1" --argjson p "$2" --argjson id "$REQUEST_ID" '{jsonrpc: "2.0", id: $id, method: $m, params: $p}')
|
||
local session_header=()
|
||
[ -n "$SESSION" ] && session_header=(-H "Mcp-Session-Id: $SESSION")
|
||
curl -s --max-time 60 -X POST "$ENDPOINT" \
|
||
-H "Authorization: Bearer $TOKEN" \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Accept: application/json, text/event-stream' \
|
||
"${session_header[@]}" -d "$body"
|
||
}
|
||
|
||
# Calls a tool; prints structuredContent, or {"__error": "<text>"} for isError results.
|
||
tool() { # $1 = tool name, $2 = arguments JSON
|
||
local response
|
||
response=$(rpc tools/call "$(jq -cn --arg n "$1" --argjson a "$2" '{name: $n, arguments: $a}')")
|
||
echo "$response" | jq -e '.result' >/dev/null 2>&1 || fail "$1: protocol error: $response"
|
||
if [ "$(echo "$response" | jq -r '.result.isError')" = "true" ]; then
|
||
echo "$response" | jq -c '{__error: .result.content[0].text}'
|
||
else
|
||
echo "$response" | jq -c '.result.structuredContent'
|
||
fi
|
||
}
|
||
|
||
# Runs an indirect ability through mcp-adapter-execute-ability; prints its result or {"__error": …}.
|
||
ability() { # $1 = ability name (lit/…), $2 = parameters JSON
|
||
local out
|
||
out=$(tool mcp-adapter-execute-ability "$(jq -cn --arg n "$1" --argjson p "$2" '{ability_name: $n, parameters: $p}')")
|
||
if echo "$out" | jq -e 'has("__error")' >/dev/null; then
|
||
echo "$out"
|
||
elif [ "$(echo "$out" | jq -r '.success')" = "true" ]; then
|
||
echo "$out" | jq -c '.data'
|
||
else
|
||
echo "$out" | jq -c '{__error: .error}'
|
||
fi
|
||
}
|
||
|
||
# Prints the lit error code (e.g. rev_mismatch) of a tool()/ability() result, or "".
|
||
error_code() { # $1 = result JSON
|
||
echo "$1" | jq -r '(.__error // "") | (try fromjson catch {}) | (.code // "")'
|
||
}
|