test: end-to-end MCP smoke test against DDEV
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
@@ -0,0 +1,102 @@
|
|||||||
|
#!/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 -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
|
||||||
|
curl -s http://localhost/.well-known/oauth-protected-resource | grep -q '"resource"' \
|
||||||
|
|| fail "protected-resource metadata missing"
|
||||||
|
pass "RFC 9728 protected-resource metadata served"
|
||||||
|
curl -s http://localhost/.well-known/oauth-authorization-server | grep -q 'client_id_metadata_document_supported' \
|
||||||
|
|| fail "authorization-server metadata missing CIMD flag"
|
||||||
|
pass "RFC 8414 authorization-server metadata advertises CIMD"
|
||||||
|
|
||||||
|
# 2. Unauthenticated request -> 401 with WWW-Authenticate
|
||||||
|
UNAUTH=$(curl -s -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 -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"
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mints an MCP access token for a user, exactly like mcp-oauth's TokenEndpoint
|
||||||
|
* does after a completed OAuth flow. For local integration tests only.
|
||||||
|
*
|
||||||
|
* Usage: wp eval-file tests/Integration/mint-token.php <user_login>
|
||||||
|
*
|
||||||
|
* Note: no `declare(strict_types=1);` here — PHP forbids that declaration
|
||||||
|
* inside eval()'d code, and `wp eval-file` runs this file through eval().
|
||||||
|
* Confirmed with a minimal repro during Task 7 (see task-7-report.md).
|
||||||
|
*/
|
||||||
|
|
||||||
|
use WPMedia\MCP\OAuth\Auth\JWT;
|
||||||
|
use WPMedia\MCP\OAuth\Auth\SecretManager;
|
||||||
|
|
||||||
|
$login = $args[0] ?? '';
|
||||||
|
$user = get_user_by('login', $login);
|
||||||
|
if (! $user) {
|
||||||
|
fwrite(STDERR, "unknown user {$login}\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[, $item] = WP_Application_Passwords::create_new_application_password(
|
||||||
|
$user->ID,
|
||||||
|
['name' => 'lit-mcp-integration-test', 'app_id' => 'lit-mcp-integration-test']
|
||||||
|
);
|
||||||
|
|
||||||
|
$now = time();
|
||||||
|
echo JWT::encode([
|
||||||
|
'iss' => home_url(),
|
||||||
|
'aud' => get_rest_url(null, 'mcp/mcp-oauth-server'),
|
||||||
|
'sub' => (string) $user->ID,
|
||||||
|
'app_pass_id' => $item['uuid'],
|
||||||
|
'client_id' => 'lit-mcp-integration-test',
|
||||||
|
'scope' => 'mcp',
|
||||||
|
'iat' => $now,
|
||||||
|
'exp' => $now + 600,
|
||||||
|
], SecretManager::get_secret());
|
||||||
Reference in New Issue
Block a user