Files
WP-LIT-MCP/tests/Integration/mcp-smoke.sh
T
janniclas.loosenandClaude Opus 5.5 ef3f73c5f2 fix: split MCP discovery check to bypass local nginx dotfile block
Local DDEV nginx denies all dotfile paths (including /.well-known/),
blocking check 1 before it reaches WordPress -- an environment issue,
not a WP-LIT-MCP/mcp-oauth defect. Check 1 now hits mcp-oauth's own
discovery rewrite target (index.php?mcp_oauth_discovery=...) with a
Host header derived from `wp option get home` as a HARD check, and
separately probes the real /.well-known/ path as a non-failing
ENVIRONMENT check that warns if the web server blocks it. Also adds
--max-time 20 to every curl call so the script can never hang.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
2026-09-25 18:55:41 +02:00

134 lines
6.9 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# End-to-end checks of the OAuth-protected MCP endpoint.
# Runs INSIDE the DDEV web container (wp-cli and curl available there):
# cd /home/janniclas/Projekte/WordPress && ddev exec bash wp-content/plugins/WP-LIT-MCP/tests/Integration/mcp-smoke.sh
set -euo pipefail
PLUGIN=wp-content/plugins/WP-LIT-MCP
ENDPOINT=http://localhost/wp-json/mcp/mcp-oauth-server
cd /var/www/html
fail() { echo "FAIL: $*"; exit 1; }
pass() { echo "ok - $*"; }
# --- Cleanup: remove every "lit-mcp-integration-test" application password
# this script mints (for every user it mints one for) and the throwaway
# subscriber it creates. Registered up front so it runs on any exit path
# (success, fail(), or an unexpected error under `set -e`).
ADMIN_LOGIN=""
cleanup_app_passwords_for() { # $1 = user_login
local login="$1"
[ -n "$login" ] || return 0
local escaped=${login//\'/\\\'}
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
}
cleanup() {
cleanup_app_passwords_for "$ADMIN_LOGIN"
cleanup_app_passwords_for lit-smoke-sub
wp user delete lit-smoke-sub --yes >/dev/null 2>&1 || true
}
trap cleanup EXIT
mcp() { # $1 token, $2 json body, $3 session id (optional) -> prints headers+body
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"
}
# 1. Discovery documents
#
# The local DDEV nginx config denies all dotfile paths (`location ~ /\. { deny
# all; }`, in the parent project's .ddev/nginx_full/nginx-site.conf, outside
# this plugin), which blocks the real `/.well-known/...` paths before the
# request ever reaches WordPress. That's an environment/deployment concern,
# not a WP-LIT-MCP or mcp-oauth defect, so it must not fail this test.
#
# The HARD checks below instead hit mcp-oauth's own rewrite target directly
# (index.php?mcp_oauth_discovery=...) via a request whose Host matches the
# site's home URL (WordPress issues a 301 to the canonical host otherwise),
# proving the discovery documents themselves are correct. The ENVIRONMENT
# check further down separately probes the real `/.well-known/` path and only
# warns, never fails, so a misconfigured web server doesn't block this suite
# while still surfacing the risk (real MCP clients like claude.ai use the
# real path, not the rewrite target).
HOST=$(wp option get home | sed -E 's#^[a-zA-Z]+://##')
curl -s --max-time 20 -H "Host: $HOST" -H 'X-Forwarded-Proto: https' \
'http://localhost/?mcp_oauth_discovery=protected-resource' \
| grep -q '"resource"' \
|| fail "protected-resource metadata missing"
pass "RFC 9728 protected-resource metadata served"
curl -s --max-time 20 -H "Host: $HOST" -H 'X-Forwarded-Proto: https' \
'http://localhost/?mcp_oauth_discovery=authorization-server' \
| grep -q 'client_id_metadata_document_supported' \
|| fail "authorization-server metadata missing CIMD flag"
pass "RFC 8414 authorization-server metadata advertises CIMD"
# ENVIRONMENT check (never fails): the real .well-known path, same Host/proto.
WELLKNOWN=$(curl -s --max-time 20 -H "Host: $HOST" -H 'X-Forwarded-Proto: https' \
'http://localhost/.well-known/oauth-protected-resource')
if echo "$WELLKNOWN" | grep -q '"resource"'; then
pass "/.well-known/ reachable"
else
echo "WARN - /.well-known/ blocked by web server config (claude.ai discovery will fail; allow .well-known in nginx)"
fi
# 2. Unauthenticated request -> 401 with WWW-Authenticate
UNAUTH=$(curl -s --max-time 20 -i -X POST "$ENDPOINT" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}')
echo "$UNAUTH" | head -1 | grep -q ' 401' || fail "expected 401 without token"
echo "$UNAUTH" | grep -qi '^www-authenticate: Bearer' || fail "missing WWW-Authenticate header"
pass "401 + WWW-Authenticate without token"
# 3. Administrator: initialize, tools/list, tools/call
ADMIN_LOGIN=$(wp user list --role=administrator --field=user_login | head -1)
TOKEN=$(wp eval-file "$PLUGIN/tests/Integration/mint-token.php" "$ADMIN_LOGIN")
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')
echo "$INIT" | grep -q 'LIT MCP pflegt' || fail "instructions missing in initialize result"
pass "initialize returns LIT instructions (session: ${SESSION:-none})"
LIST=$(mcp "$TOKEN" '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' "$SESSION")
for tool in lit-guide lit-site-status mcp-adapter-execute-ability; do
echo "$LIST" | grep -q "\"$tool\"" || fail "tools/list lacks $tool"
done
pass "tools/list contains direct tools and meta-tools"
CALL=$(mcp "$TOKEN" '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"lit-site-status","arguments":{}}}' "$SESSION")
echo "$CALL" | grep -q '"isError":false' || fail "lit-site-status failed: $CALL"
echo "$CALL" | grep -q 'the-events-calendar' || fail "site-status lacks components"
pass "tools/call lit-site-status works"
# 4. Token must not authenticate other REST routes (mcp-oauth issue #62)
ME=$(curl -s --max-time 20 -o /dev/null -w '%{http_code}' http://localhost/wp-json/wp/v2/users/me -H "Authorization: Bearer $TOKEN")
[ "$ME" = "401" ] || fail "MCP token authenticates /wp/v2/users/me (HTTP $ME) – scope filter needed"
pass "MCP token is rejected on /wp/v2/users/me"
# 5. Role gate: subscriber has a valid token but must not use tools
wp user create lit-smoke-sub lit-smoke-sub@example.invalid --role=subscriber >/dev/null
SUB_TOKEN=$(wp eval-file "$PLUGIN/tests/Integration/mint-token.php" lit-smoke-sub)
SUB_INIT=$(mcp "$SUB_TOKEN" '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}')
SUB_SESSION=$(echo "$SUB_INIT" | grep -i '^mcp-session-id:' | awk '{print $2}' | tr -d '\r')
SUB_CALL=$(mcp "$SUB_TOKEN" '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"lit-site-status","arguments":{}}}' "$SUB_SESSION")
echo "$SUB_CALL" | grep -q '"isError":true\|"error"' || fail "subscriber could call lit-site-status: $SUB_CALL"
SUB_META=$(mcp "$SUB_TOKEN" '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"mcp-adapter-execute-ability","arguments":{"ability_name":"lit/site-status","parameters":{}}}}' "$SUB_SESSION")
echo "$SUB_META" | grep -q '"isError":true\|"error"' || fail "subscriber could use execute-ability: $SUB_META"
pass "role gate blocks subscriber (direct tool and meta-tool)"
echo "ALL CHECKS PASSED"