Skip to content

Instantly share code, notes, and snippets.

@cdevroe
Created May 11, 2026 13:48
Show Gist options
  • Select an option

  • Save cdevroe/a733813558cbcb9a046eb31335a05793 to your computer and use it in GitHub Desktop.

Select an option

Save cdevroe/a733813558cbcb9a046eb31335a05793 to your computer and use it in GitHub Desktop.
WordPress MCP Endpoint Tester
<?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