Schema Errors

Input Schema Edge Cases: Empty Objects and Validation Failures

⏱ 15 min

An ability with no input parameters sounds like the simplest possible case, and it’s also the one that has produced two separate, real, closed bugs in the MCP Adapter, for two entirely different reasons. This lesson covers both, what the official Abilities API documentation actually says an empty schema should look like, and what real MCP clients did with it before both bugs were fixed.

What you'll learn in this lesson
What core's own docs recommend for a no-parameter ability
input_schema is optional and defaults to null, the simplest correct option.
Issue #35: an empty schema that behaved differently in different clients
The PHP array-to-JSON ambiguity that broke tool registration in Cursor.
Issue #116: a separate bug in how empty call arguments were validated
A client sending {} got silently mangled by PHP before validation even ran.
How to check which of these you're actually hitting
And which adapter version you need to be on.
Prerequisites

An ability with no required input parameters, and a way to call it from at least two different MCP clients or MCP Inspector (Lesson 6), since both bugs in this lesson are easiest to spot as an inconsistency between clients rather than a single clear error.

Step 1: What core actually recommends

The Abilities API’s own PHP reference documentation is direct about this: input_schema is an optional argument, and it defaults to null when omitted. For an ability that genuinely takes no input, the simplest, most correct option is to leave the key out of $args entirely, rather than reaching for an empty array or an explicit empty-object schema:

// File: my-plugin.php, simplest correct option for a no-input ability
add_action( 'wp_abilities_api_init', function () {
	wp_register_ability( 'my-plugin/get-site-status', array(
		'label'               => __( 'Get site status', 'my-plugin' ),
		'description'         => __( 'Returns whether the site is in maintenance mode.', 'my-plugin' ),
		'category'            => 'site',
		// no input_schema key at all
		'execute_callback'    => function () {
			return array( 'maintenance_mode' => (bool) get_option( 'wp_maintenance_mode' ) );
		},
		'permission_callback' => '__return_true',
	) );
} );

If you’d rather be explicit that the ability accepts an empty object and not accidentally imply it accepts anything, an object schema with an empty properties is the defensible alternative, and it’s what Course 2’s own examples use:

// File: my-plugin.php, explicit alternative
'input_schema' => array(
	'type'       => 'object',
	'properties' => array(),
),

Both are valid starting points. The two real bugs below are about what happened when developers wrote the bare empty version instead, 'input_schema' => array(), with no type key at all, and about a separate, deeper issue in how the adapter validated actual empty arguments sent by a client.

Step 2: Issue #35, an empty schema that broke registration in Cursor

Issue #35 reported that abilities registered with a bare input_schema of array() behaved inconsistently across MCP clients. In VS Code, the tool registered and worked fine. In Cursor, the same ability failed with “Expected object, received array” and never appeared in the tool list at all. The cause: PHP’s array() is ambiguous when JSON-encoded, an empty PHP array serializes to [], not {}, and Cursor’s stricter client-side validation expected the schema’s properties field to be a JSON object, not an array, even when empty.

The fix, PR #36, “Add type property to empty input schema,” addressed it by ensuring the schema explicitly declares 'type' => 'object' rather than leaving the schema as a bare, untyped empty array. With an explicit type, the adapter’s own JSON encoding treats the accompanying structure as an object correctly, and the ambiguity that Cursor was rejecting goes away.

A bare array() is the version to avoid

If your input_schema for a no-parameter ability is literally 'input_schema' => array() with no type key, that’s the exact shape issue #35 was filed against. Either omit input_schema entirely (Step 1), or declare it explicitly as array( 'type' => 'object', 'properties' => array() ). Don’t leave it as a bare, untyped empty array.

Step 3: Issue #116, a deeper bug in validating empty call arguments

Issue #116 is a separate, later bug, and it can bite you even with a correctly-typed input_schema. When an MCP client calls a tool and sends an empty arguments object, {}, PHP’s JSON decoding turns that into an empty PHP array, [], the same fundamental ambiguity as issue #35, but this time on the incoming call arguments, not the schema declaration. The adapter’s input validator at the time expected the decoded params to be a PHP associative array representing an object and rejected the empty array with “input is not of type object,” even though {} is exactly what the schema asked for.

The root cause and fix are documented directly in the issue thread: the project added a new AbilityArgumentNormalizer utility that detects this specific case, an object-typed schema paired with empty decoded arguments, and normalizes it back into the object shape the validator expects, applied consistently across tool calls, prompts, and the built-in execute-ability path. This landed in MCP Adapter v0.5.0. If you’re still on v0.4.1 or earlier and seeing this exact “input is not of type object” error against an ability that otherwise looks correctly schema’d, upgrading is the actual fix, not a workaround in your own code.

# File: terminal, check your installed adapter version
wp eval 'echo defined( "MCP_ADAPTER_VERSION" ) ? MCP_ADAPTER_VERSION : "check composer.lock or the plugin readme instead";'
Two different bugs, two different fixes, don't conflate them

Issue #35 is about how you declare an empty schema and was fixed in the schema encoding itself (PR #36). Issue #116 is about how the adapter validates incoming call arguments against a correct schema and was fixed in v0.5.0’s argument normalization. A site could have the first problem fixed and still hit the second on an older version, or vice versa. Check which symptom you actually have before assuming one fix covers both.

Step 4: Diagnosing which one you’re looking at

Fails only in one specific client, others work fine
Likely issue #35's pattern, check your input_schema is explicitly type: object, not a bare array().
Fails with "not of type object" when the client sends genuinely empty arguments
Likely issue #116, check your adapter version, upgrade to v0.5.0 or later.
Fails identically everywhere, including MCP Inspector
Probably not either of these two, revisit permission_callback and execute_callback instead.

Test it: call the ability with genuinely empty arguments

Use MCP Inspector (full setup in the next lesson) or a direct WP-CLI call to confirm your no-parameter ability accepts an actual empty object, not just no arguments at all:

# File: terminal
wp eval '
$ability = wp_get_ability( "my-plugin/get-site-status" );
var_dump( $ability->execute( array() ) );
'

If this returns your expected result rather than a validation error, your schema and your adapter version are both handling the empty-object case correctly.

Recap

input_schema is optional and defaults to null, the simplest choice for a no-parameter ability is to omit it. If you declare one explicitly, always include 'type' => 'object', a bare array() is the exact shape that broke tool registration in Cursor (issue #35, fixed in PR #36). Separately, older adapter versions had a real bug validating genuinely empty call arguments sent as {}, fixed in v0.5.0 via the AbilityArgumentNormalizer utility (issue #116). Check your adapter version before assuming a schema-declaration fix covers a call-argument validation problem, they’re different bugs with different fixes.

Resources & further reading

← Fixing Hook-Timing and Load-Order Errors Debugging With the MCP Inspector →