Skip to content

structuredContent is set to the raw tool result, producing a non-object when a tool returns a JSON list #253

Description

@wpacademy

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-schemaschema-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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions