Created
May 11, 2026 13:48
-
-
Save cdevroe/a733813558cbcb9a046eb31335a05793 to your computer and use it in GitHub Desktop.
WordPress MCP Endpoint Tester
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * WordPress MCP Endpoint Tester | |
| * | |
| * Tests whether a WordPress site's MCP endpoint is reachable or blocked | |
| * by Cloudflare / a WAF / other firewall. If credentials are provided, | |
| * additionally performs a read-only health check (MCP initialize handshake). | |
| * | |
| * Usage: | |
| * php mcp-endpoint-test.php <url> [username] [app_password] | |
| * | |
| * Examples: | |
| * php mcp-endpoint-test.php https://example.com/wp-json/wp/v2/mcp | |
| * php mcp-endpoint-test.php https://example.com/wp-json/wp/v2/mcp admin "xxxx xxxx xxxx xxxx" | |
| * | |
| * Exit codes: | |
| * 0 = reachable (and healthy if credentials provided) | |
| * 1 = blocked / unreachable / unhealthy | |
| * 2 = usage error | |
| */ | |
| // ----------------------------------------------------------------------------- | |
| // CLI arg parsing | |
| // ----------------------------------------------------------------------------- | |
| if (PHP_SAPI !== 'cli') { | |
| fwrite(STDERR, "This script must be run from the command line.\n"); | |
| exit(2); | |
| } | |
| $argvLocal = $argv; | |
| array_shift($argvLocal); // drop script name | |
| if (count($argvLocal) < 1) { | |
| fwrite(STDERR, "Usage: php mcp-endpoint-test.php <url> [username] [app_password]\n"); | |
| exit(2); | |
| } | |
| $url = $argvLocal[0]; | |
| $username = $argvLocal[1] ?? null; | |
| $appPass = $argvLocal[2] ?? null; | |
| $haveCreds = ($username !== null && $appPass !== null); | |
| if (!filter_var($url, FILTER_VALIDATE_URL)) { | |
| fwrite(STDERR, "Error: '$url' is not a valid URL.\n"); | |
| exit(2); | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // Helpers | |
| // ----------------------------------------------------------------------------- | |
| /** | |
| * Issue an HTTP request via cURL and return useful response metadata. | |
| */ | |
| function http_request(string $url, array $opts = []): array { | |
| $method = $opts['method'] ?? 'GET'; | |
| $headers = $opts['headers'] ?? []; | |
| $body = $opts['body'] ?? null; | |
| $authUser = $opts['user'] ?? null; | |
| $authPass = $opts['pass'] ?? null; | |
| $timeout = $opts['timeout'] ?? 15; | |
| $ch = curl_init($url); | |
| curl_setopt_array($ch, [ | |
| CURLOPT_RETURNTRANSFER => true, | |
| CURLOPT_HEADER => true, | |
| CURLOPT_FOLLOWLOCATION => true, | |
| CURLOPT_MAXREDIRS => 5, | |
| CURLOPT_TIMEOUT => $timeout, | |
| CURLOPT_CONNECTTIMEOUT => 10, | |
| CURLOPT_USERAGENT => 'MCP-Endpoint-Tester/1.0 (+health-check)', | |
| CURLOPT_CUSTOMREQUEST => $method, | |
| ]); | |
| if (!empty($headers)) { | |
| curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); | |
| } | |
| if ($body !== null) { | |
| curl_setopt($ch, CURLOPT_POSTFIELDS, $body); | |
| } | |
| if ($authUser !== null && $authPass !== null) { | |
| curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); | |
| curl_setopt($ch, CURLOPT_USERPWD, "$authUser:$authPass"); | |
| } | |
| $raw = curl_exec($ch); | |
| $errno = curl_errno($ch); | |
| $error = curl_error($ch); | |
| $info = curl_getinfo($ch); | |
| $headerLen = $info['header_size'] ?? 0; | |
| curl_close($ch); | |
| if ($raw === false) { | |
| return [ | |
| 'ok' => false, | |
| 'curl_errno' => $errno, | |
| 'curl_error' => $error, | |
| 'status' => 0, | |
| 'headers' => '', | |
| 'body' => '', | |
| 'info' => $info, | |
| ]; | |
| } | |
| $rawHeaders = substr($raw, 0, $headerLen); | |
| $body = substr($raw, $headerLen); | |
| return [ | |
| 'ok' => true, | |
| 'curl_errno' => 0, | |
| 'curl_error' => '', | |
| 'status' => (int) ($info['http_code'] ?? 0), | |
| 'headers' => $rawHeaders, | |
| 'body' => $body, | |
| 'info' => $info, | |
| ]; | |
| } | |
| /** | |
| * Detect signs of Cloudflare / WAF blocking in headers + body. | |
| */ | |
| function detect_block(array $resp): array { | |
| $findings = []; | |
| $headers = strtolower($resp['headers'] ?? ''); | |
| $body = $resp['body'] ?? ''; | |
| $bodyLow = strtolower($body); | |
| $status = $resp['status'] ?? 0; | |
| // Cloudflare-specific header signals | |
| if (strpos($headers, 'server: cloudflare') !== false) { | |
| $findings[] = 'Cloudflare detected (Server: cloudflare)'; | |
| } | |
| if (strpos($headers, 'cf-ray:') !== false) { | |
| $findings[] = 'Cloudflare detected (CF-Ray header present)'; | |
| } | |
| if (strpos($headers, 'cf-mitigated:') !== false) { | |
| $findings[] = 'Cloudflare mitigation triggered (CF-Mitigated header)'; | |
| } | |
| // Cloudflare body signals | |
| $cfBodySignals = [ | |
| 'attention required! | cloudflare', | |
| 'cf-error-details', | |
| 'sorry, you have been blocked', | |
| 'cloudflare ray id', | |
| 'cf-challenge', | |
| '__cf_chl', | |
| 'just a moment...', | |
| 'checking your browser before accessing', | |
| ]; | |
| foreach ($cfBodySignals as $needle) { | |
| if (strpos($bodyLow, $needle) !== false) { | |
| $findings[] = "Cloudflare block/challenge page signal: \"$needle\""; | |
| } | |
| } | |
| // Generic WAF signals | |
| $wafBodySignals = [ | |
| 'access denied', | |
| 'request blocked', | |
| 'web application firewall', | |
| 'mod_security', | |
| 'wordfence', | |
| 'sucuri website firewall', | |
| 'incident id', | |
| ]; | |
| foreach ($wafBodySignals as $needle) { | |
| if (strpos($bodyLow, $needle) !== false) { | |
| $findings[] = "Possible WAF signal: \"$needle\""; | |
| } | |
| } | |
| // Status-based heuristics | |
| if (in_array($status, [403, 406, 429, 503, 520, 521, 522, 523, 524, 525, 526, 530], true)) { | |
| $findings[] = "Suspicious status code: $status"; | |
| } | |
| return $findings; | |
| } | |
| function header_value(string $rawHeaders, string $name): ?string { | |
| $name = strtolower($name); | |
| foreach (preg_split("/\r?\n/", $rawHeaders) as $line) { | |
| $parts = explode(':', $line, 2); | |
| if (count($parts) === 2 && strtolower(trim($parts[0])) === $name) { | |
| return trim($parts[1]); | |
| } | |
| } | |
| return null; | |
| } | |
| function print_section(string $title): void { | |
| echo "\n" . str_repeat('=', 70) . "\n"; | |
| echo " $title\n"; | |
| echo str_repeat('=', 70) . "\n"; | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // Step 1: Reachability / firewall probe (HEAD then GET fallback) | |
| // ----------------------------------------------------------------------------- | |
| print_section('1. Endpoint reachability & firewall probe'); | |
| echo "Target: $url\n"; | |
| echo "Mode: " . ($haveCreds ? 'firewall probe + authenticated health check' : 'firewall probe only') . "\n"; | |
| $probe = http_request($url, ['method' => 'GET']); | |
| if (!$probe['ok']) { | |
| echo "\nFAIL: Could not connect.\n"; | |
| echo " cURL error ({$probe['curl_errno']}): {$probe['curl_error']}\n"; | |
| exit(1); | |
| } | |
| $status = $probe['status']; | |
| $cfRay = header_value($probe['headers'], 'cf-ray'); | |
| $server = header_value($probe['headers'], 'server'); | |
| $findings = detect_block($probe); | |
| echo "\nHTTP status: $status\n"; | |
| echo "Server header: " . ($server ?? '(none)') . "\n"; | |
| echo "CF-Ray: " . ($cfRay ?? '(none)') . "\n"; | |
| echo "Final URL: " . ($probe['info']['url'] ?? $url) . "\n"; | |
| if (!empty($findings)) { | |
| echo "\nFirewall / block signals:\n"; | |
| foreach ($findings as $f) { | |
| echo " - $f\n"; | |
| } | |
| } else { | |
| echo "\nNo firewall block signals detected.\n"; | |
| } | |
| // 401/404 on the MCP endpoint without auth is actually fine — it means | |
| // the request reached WordPress. 403 with CF signals is the bad case. | |
| $hardBlocked = false; | |
| if (in_array($status, [403, 503, 520, 521, 522, 523, 524, 525, 526], true) && !empty($findings)) { | |
| $hardBlocked = true; | |
| } | |
| if ($status === 429) { | |
| $hardBlocked = true; | |
| } | |
| if ($hardBlocked) { | |
| echo "\nRESULT: Endpoint appears to be BLOCKED by Cloudflare / WAF.\n"; | |
| exit(1); | |
| } | |
| if ($status >= 500) { | |
| echo "\nRESULT: Endpoint returned a server error ($status). Could be origin issue or WAF.\n"; | |
| // Don't exit yet — let auth check run if credentials were provided. | |
| } | |
| if (!$haveCreds) { | |
| if ($status === 401 || $status === 404 || ($status >= 200 && $status < 500)) { | |
| echo "\nRESULT: Endpoint is REACHABLE (no auth supplied, so health not verified).\n"; | |
| echo " A 401/404 here is normal — it means the request reached WordPress.\n"; | |
| exit(0); | |
| } | |
| echo "\nRESULT: Endpoint reachability inconclusive (status $status).\n"; | |
| exit(1); | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // Step 2: Authenticated read-only health check (MCP initialize) | |
| // ----------------------------------------------------------------------------- | |
| print_section('2. Authenticated read-only health check'); | |
| $initPayload = json_encode([ | |
| 'jsonrpc' => '2.0', | |
| 'id' => 1, | |
| 'method' => 'initialize', | |
| 'params' => [ | |
| 'protocolVersion' => '2025-06-18', | |
| 'capabilities' => new stdClass(), | |
| 'clientInfo' => [ | |
| 'name' => 'mcp-endpoint-tester', | |
| 'version' => '1.0.0', | |
| ], | |
| ], | |
| ], JSON_UNESCAPED_SLASHES); | |
| $auth = http_request($url, [ | |
| 'method' => 'POST', | |
| 'headers' => [ | |
| 'Content-Type: application/json', | |
| 'Accept: application/json, text/event-stream', | |
| ], | |
| 'body' => $initPayload, | |
| 'user' => $username, | |
| 'pass' => $appPass, | |
| ]); | |
| if (!$auth['ok']) { | |
| echo "FAIL: Could not connect for auth check.\n"; | |
| echo " cURL error ({$auth['curl_errno']}): {$auth['curl_error']}\n"; | |
| exit(1); | |
| } | |
| $astatus = $auth['status']; | |
| $afindings = detect_block($auth); | |
| echo "HTTP status: $astatus\n"; | |
| echo "CF-Ray: " . (header_value($auth['headers'], 'cf-ray') ?? '(none)') . "\n"; | |
| if (!empty($afindings)) { | |
| echo "\nFirewall / block signals on authenticated request:\n"; | |
| foreach ($afindings as $f) { | |
| echo " - $f\n"; | |
| } | |
| } | |
| $bodyPreview = trim($auth['body']); | |
| if (strlen($bodyPreview) > 600) { | |
| $bodyPreview = substr($bodyPreview, 0, 600) . '... [truncated]'; | |
| } | |
| echo "\nResponse body preview:\n$bodyPreview\n"; | |
| // Try to parse JSON-RPC response. MCP servers may return SSE — handle that too. | |
| $jsonText = $auth['body']; | |
| if (stripos($auth['headers'], 'content-type: text/event-stream') !== false) { | |
| // Extract the first `data: {...}` line. | |
| if (preg_match('/^data:\s*(.+)$/m', $auth['body'], $m)) { | |
| $jsonText = $m[1]; | |
| } | |
| } | |
| $decoded = json_decode($jsonText, true); | |
| if ($astatus === 401 || $astatus === 403) { | |
| echo "\nRESULT: Authentication FAILED (status $astatus). Check username / application password.\n"; | |
| exit(1); | |
| } | |
| if ($astatus >= 200 && $astatus < 300 && is_array($decoded)) { | |
| if (isset($decoded['result']['serverInfo'])) { | |
| $info = $decoded['result']['serverInfo']; | |
| echo "\nServer info:\n"; | |
| echo " name: " . ($info['name'] ?? '(unknown)') . "\n"; | |
| echo " version: " . ($info['version'] ?? '(unknown)') . "\n"; | |
| if (isset($decoded['result']['protocolVersion'])) { | |
| echo " protocol: " . $decoded['result']['protocolVersion'] . "\n"; | |
| } | |
| echo "\nRESULT: HEALTHY — endpoint reachable and MCP initialize succeeded.\n"; | |
| exit(0); | |
| } | |
| if (isset($decoded['error'])) { | |
| echo "\nMCP error response: " . json_encode($decoded['error']) . "\n"; | |
| echo "RESULT: Endpoint responded but returned an MCP error.\n"; | |
| exit(1); | |
| } | |
| echo "\nRESULT: Endpoint responded with 2xx but no recognizable MCP initialize result.\n"; | |
| exit(1); | |
| } | |
| echo "\nRESULT: Health check inconclusive (status $astatus).\n"; | |
| exit(1); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment