Give Claude real-time read access to your WordPress site via the Model Context Protocol — so it can write plugins that actually fit your setup.
Install the plugin, grab your token or complete the OAuth flow, and Claude.ai can immediately read your site's internals.
Upload the plugin to wp-content/plugins/ and activate it. A Bearer token is generated automatically on first activation.
Settings → Permalinks → Save
Add a custom connector in Claude.ai settings. Paste your MCP endpoint URL and choose Bearer Token or complete the OAuth 2.1 flow.
Profile → Connectors → Add custom
Claude can now inspect your plugins, database schema, ACF fields, source code, error logs, and more — in real time.
19 read-only tools available
19 read-only tools give Claude a complete picture of your site. Nothing is written, deleted, or modified — ever.
Read-only by design, with defense-in-depth across authentication, file access, database, and the OAuth flow.
hash_equals() — no timing attacksautoload = falsewp_get_options toolrealpath()wp-contentrealpath() returning false treated as access deniedSELECT queries allowed — enforced by regexuser_pass and user_activation_key always blockedSELECT * FROM wp_users permanently blockedclaude.ai origins onlyX-Frame-Options: DENYContent-Security-Policy: frame-ancestors 'none'rest_cookie_invalid_nonceTwo connection methods — pick the one that fits your workflow.
Every bug is documented with root cause, source reference, and precise fix. No handwaving.
2025-11-25 (the latest spec) not in the supported list — Claude.ai's version request was always downgraded to 2025-06-18. Now the highest-priority supported version, with all 2025-11-25 additions implemented.client_id_metadata_document_supported field. MCP spec 2025-11-25 requires this so clients know to use DCR rather than Client ID Metadata Documents. Now explicitly set to false.WWW-Authenticate 401 header missing scope parameter. MCP spec 2025-11-25 §Protected Resource Metadata (SEP-835) requires it for incremental scope consent. Now sends scope="claudeai".wp_mcp_bridge_format_ttl() helper formats correctly as "24 hours", "7 days", etc. across all configurable durations.serverInfo in initialize response missing optional description field added by MCP spec 2025-11-25 to align with the MCP registry server.json format.wp_get_options tool to avoid exposing internal config.wp_mcp_tool_db_schema() — $wpdb->esc_like() was used without $wpdb->prepare(). WordPress docs are explicit: "You must use prepare() in conjunction with esc_like()." A crafted table filter argument could escape the SQL string context.sanitize_text_field() instead of the canonical sanitize_key(). The wrong sanitizer strips HTML entities which can corrupt nonce values on certain hosting configs with magic quotes enabled.rest_cookie_invalid_nonce 403 when clicking "Allow Access". WordPress REST cookie auth validated _wpnonce as a wp_rest nonce before the handler ran — our consent nonce failed this check. Fixed with a two-nonce form: _wpnonce satisfies WP REST auth, _mcp_nonce is our CSRF check.wp_get_options could expose the plugin's own Bearer token. An authenticated Claude session could call the tool with keys:['wp_mcp_bridge_api_key'] and receive the credential it used to authenticate. Plugin's own options are now permanently blocked._get_cron_array() returns false on corrupt cron data. foreach(false) emits E_WARNING on PHP 8.0/8.1 and TypeError on PHP 8.2+, crashing the cron tool. Fixed with _get_cron_array() ?: [].$resp_types variable in the DCR handler caused PHP notices and created a latent risk of accidentally relying on an unchecked user-supplied value.wpmcp_code_*, wpmcp_token_*, wpmcp_rate_*) were not cleaned up on plugin deactivation, leaving orphaned rows in wp_options.registration_endpoint in Authorization Server Metadata. Claude Code fails with "does not support dynamic client registration" without it. Full RFC 7591 DCR endpoint added.Requires PHP: 7.4 but used union types and str_starts_with() — PHP 8.0+ only features. Fatal parse error on PHP 7.4. Header corrected to 8.0.realpath() returning false bypassed the entire path containment guard. str_starts_with($full, false . '/') is always true, allowing any file on the filesystem to be read.register_setting() only hooked to admin_init. WordPress docs require both admin_init and rest_api_init for the sanitize callback to apply on REST API updates.array_filter($params) stripped legitimate empty-string OAuth params. State and scope can be empty strings; removing them breaks CSRF protection and scope handling.do_action('rest_api_init') inside a tool callback double-fired every hook in the system, causing duplicate route warnings and third-party side effects.strncmp prefix check bypassed by sibling directories. /wp-content-evil/ matches /wp-content prefix. Fixed with separator-aware str_starts_with($full, $base . DIRECTORY_SEPARATOR).wp_get_users — get_userdata() called per user in a loop. On 200 users = 200 extra queries. Fixed by removing 'fields' restriction to get full WP_User objects.$client_display mixing raw HTML and escaped text in one variable, echoed without escaping — flagged by all WP security scanners as an XSS risk.X-Frame-Options. RFC 6749 §10.13 requires clickjacking prevention on authorization endpoints. Headers X-Frame-Options: DENY and frame-ancestors 'none' added.REDIRECT_HTTP_AUTHORIZATION and apache_request_headers() now runs at init priority 1.wp_safe_redirect() silently blocked delivery to claude.ai — not on WordPress's allowed redirect hosts list. OAuth flow could never complete.get_json_params() returns null on missing/malformed body. Accessing $body['id'] on null = PHP 8 TypeError, crashing the MCP handler on any malformed request.ini_get('error_log') · token fallback always HTTP 200 even on errors (RFC 6749 §5.2 violation).strtok() global state corruption · missing Cache-Control/Pragma on token fallback · target="_blank" link without rel="noopener"./.well-known/oauth-authorization-server (RFC 8414) and the old-spec fallback /authorize and /token paths. Without these, every connection attempt 404'd.post_count always cast to 1 via stdClass · missing wp_reset_postdata() after WP_Query.autoload = true · date() instead of gmdate() · MCP-Protocol-Version header only on initialize responses.