Summary
ToolsHandler::handle_tools_call() assigns a tool's return value directly to structuredContent:
https://github.com/WordPress/mcp-adapter/blob/trunk/includes/Handlers/Tools/ToolsHandler.php#L307
'structuredContent' => $result,
When a tool returns a JSON list rather than an associative array, structuredContent serialises as [...] instead of {...}. Strict MCP clients reject the response with a dictionary-validation error.
Why this is a spec violation
The MCP schema types the field as an object, and the schema package vendored alongside the adapter agrees:
wordpress/php-mcp-schema — schema-server.json:
"structuredContent": "{ [key: string]: unknown }?",
CallToolResult / ToolResultContent type it as array<string, mixed>|null.
So any list-shaped result is invalid regardless of client leniency.
Reproduction
Any tool that forwards another API's payload verbatim will hit this. Ours dispatches to the WooCommerce REST API and returns WP_REST_Response::get_data() unchanged; a number of wc/v3 routes answer with a top-level array.
Concretely, GET reports/products/totals returns:
[
{ "slug": "external", "name": "External/Affiliate product", "total": 0 },
{ "slug": "simple", "name": "Simple product", "total": 14 }
]
which becomes structuredContent verbatim and is rejected. Product, order and customer list endpoints are affected the same way.
Reported independently against two clients (Hermes, and ChatGPT/Codex), both of which validate the field.
A related observation
#51 asked for structuredContent when outputSchema is defined. The shipped implementation sets it unconditionally: ToolsHandler.php contains no reference to output_schema at all. That is how the bug surfaces for tools that declare no output schema, which is where we see it.
That suggests two possible fixes, and the second is probably wanted regardless:
1. Honour the original scope — only populate structuredContent when the ability declares an outputSchema. This matches #51 and the spec's output-schema section.
2. Coerce to an object — guarantee the field is well-formed whatever a tool returns:
$structured_content = is_array( $result ) && ! array_is_list( $result )
? $result
: array( 'data' => $result );
'structuredContent' => $structured_content,
Associative arrays and objects pass through unchanged; lists, scalars and null are wrapped under data. array_is_list() is PHP 8.1+, matching the adapter's own floor.
Workaround
For anyone hitting this before a fix lands, the mcp_adapter_tool_call_result filter runs before the assignment and is a clean place to normalize:
add_filter( 'mcp_adapter_tool_call_result', function ( $result ) {
if ( is_wp_error( $result ) || is_object( $result ) ) {
return $result;
}
return is_array( $result ) && ! array_is_list( $result )
? $result
: array( 'data' => $result );
}, 99 );
Environment
wordpress/mcp-adapter ^0.5 (vendored)
- WordPress 7.0.2, PHP 8.2.31
Happy to open a PR for whichever direction you prefer.
Summary
ToolsHandler::handle_tools_call()assigns a tool's return value directly tostructuredContent:https://github.com/WordPress/mcp-adapter/blob/trunk/includes/Handlers/Tools/ToolsHandler.php#L307
When a tool returns a JSON list rather than an associative array,
structuredContentserialises as[...]instead of{...}. Strict MCP clients reject the response with a dictionary-validation error.Why this is a spec violation
The MCP schema types the field as an object, and the schema package vendored alongside the adapter agrees:
wordpress/php-mcp-schema—schema-server.json:CallToolResult/ToolResultContenttype it asarray<string, mixed>|null.So any list-shaped result is invalid regardless of client leniency.
Reproduction
Any tool that forwards another API's payload verbatim will hit this. Ours dispatches to the WooCommerce REST API and returns
WP_REST_Response::get_data()unchanged; a number ofwc/v3routes answer with a top-level array.Concretely,
GET reports/products/totalsreturns:[ { "slug": "external", "name": "External/Affiliate product", "total": 0 }, { "slug": "simple", "name": "Simple product", "total": 14 } ]which becomes
structuredContentverbatim and is rejected. Product, order and customer list endpoints are affected the same way.Reported independently against two clients (Hermes, and ChatGPT/Codex), both of which validate the field.
A related observation
#51 asked for
structuredContentwhenoutputSchemais defined. The shipped implementation sets it unconditionally:ToolsHandler.phpcontains no reference tooutput_schemaat all. That is how the bug surfaces for tools that declare no output schema, which is where we see it.That suggests two possible fixes, and the second is probably wanted regardless:
1. Honour the original scope — only populate
structuredContentwhen the ability declares anoutputSchema. This matches #51 and the spec's output-schema section.2. Coerce to an object — guarantee the field is well-formed whatever a tool returns:
Associative arrays and objects pass through unchanged; lists, scalars and
nullare wrapped underdata.array_is_list()is PHP 8.1+, matching the adapter's own floor.Workaround
For anyone hitting this before a fix lands, the
mcp_adapter_tool_call_resultfilter runs before the assignment and is a clean place to normalize:Environment
wordpress/mcp-adapter^0.5 (vendored)Happy to open a PR for whichever direction you prefer.