fix(wordpress): accept MCP requests under the internal service name

A client reaches the MCP endpoint by the provider's service name on the
container network, so the hop is plain HTTP. WordPress hands out an application
password only over SSL and, on an onion deployment, home_url() is
http://<onion>, so the gate stays shut: core never even attempts to validate the
credential and answers rest_not_logged_in. The client learns nothing about why.

Measured on a live deployment, same credential, same endpoint, the only
difference being the declaration of the scheme: 401 without it, 200 with it.
Deliberately wrong passwords produced the same rest_not_logged_in as correct
ones, which is what "never validated" looks like from outside.

The accommodation belongs on the provider's side, as 5e810c4a5a already settled
for Moodle: a probe that spoofs Host and X-Forwarded-Proto asserts the endpoint
works when spoofed and says nothing about how a real client reaches it. That is
exactly what happened here - the contract test sent the header and passed while
hermes and openclaw, which send only their Authorization, were refused in run
34699757369 on both compose and swarm.

WORDPRESS_INTERNAL_HOST is rendered from the same mcp.endpoint.service_key the
clients build their URL from, so both sides name one thing once. The filter
opens the gate for a request whose HTTP_HOST matches it, splits on ':' so a port
cannot defeat the match, and changes nothing when the variable is unset. Its
existing branches are untouched.

contract.py loses the X-Forwarded-Proto it used to send. The test now takes the
route a client takes, and can no longer be green while every client fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-13 15:17:43 +02:00
1 parent 5d0c1a442a
commit b260cc5604
4 files changed
+12 -2

No files matched your search

@@ -116,7 +116,6 @@ def rpc(method, params=None, authorization=None, url=None, notification=False):
request.add_header("Accept", "application/json, text/event-stream")
if HOST_HEADER:
request.add_header("Host", HOST_HEADER)
request.add_header("X-Forwarded-Proto", "https")
if authorization:
request.add_header("Authorization", authorization)
if SESSION["id"]:
@@ -60,7 +60,15 @@ add_filter( 'rest_pre_dispatch', 'infinito_mcp_guard_transport', 10, 3 );
* @param bool $available Whether core already considers them available.
*/
function infinito_mcp_app_passwords_available( $available ) {
return $available || 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME );
if ( $available || 'https' === wp_parse_url( home_url(), PHP_URL_SCHEME ) ) {
return true;
}
$internal = getenv( 'WORDPRESS_INTERNAL_HOST' );
if ( ! $internal ) {
return false;
}
$host = isset( $_SERVER['HTTP_HOST'] ) ? explode( ':', wp_unslash( $_SERVER['HTTP_HOST'] ) )[0] : '';
return $host === $internal;
}
add_filter( 'wp_is_application_passwords_available', 'infinito_mcp_app_passwords_available' );
+2
View File
@@ -7,6 +7,8 @@ WORDPRESS_DB_NAME={{ lookup('database', application_id, 'name') }}
WP_CLI_CACHE_DIR=/tmp/wp-cli-cache
WORDPRESS_INTERNAL_HOST={{ WORDPRESS_MCP_SERVICE_KEY }}
WP_DEBUG={{ MODE_DEBUG | lower }}
WP_DEBUG_LOG={{ MODE_DEBUG | lower }}
WP_DEBUG_DISPLAY={{ MODE_DEBUG | lower }}
+1
View File
@@ -22,6 +22,7 @@ WORDPRESS_USER: "www-data"
WORDPRESS_LITELLM_ENV_ID: "{{ lookup('config', application_id, 'addons.ai-engine.config.environment.id') }}"
WORDPRESS_MCP_ENABLED: "{{ lookup('config', application_id, 'mcp.enabled') }}"
WORDPRESS_MCP_ADAPTER_VERSION: "{{ lookup('config', application_id, 'addons.mcp-adapter.version') }}"
WORDPRESS_MCP_SERVICE_KEY: "{{ lookup('config', application_id, 'mcp.endpoint.service_key') }}"
WORDPRESS_MCP_ACCOUNT: "{{ lookup('users', 'mcp-web-app-wordpress') }}"
WORDPRESS_MCP_USERNAME: "{{ WORDPRESS_MCP_ACCOUNT.username }}"
WORDPRESS_MCP_EMAIL: "{{ WORDPRESS_MCP_ACCOUNT.email }}"