Fix: Add placeholder property for parameterless ability schemas#174
Fix: Add placeholder property for parameterless ability schemas#174lbbms wants to merge 1 commit into
Conversation
- Add NOOP_PROPERTY constant for empty schemas
- Normalize empty/object schemas to always include properties
- Add ensure_object_properties() helper method
- Improve compatibility with strict MCP clients (e.g., Anthropic)
Fixes schema validation failures when parameterless WordPress abilities
are transformed into {type: 'object'} without properties.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @brummermann@brunsdigital.de. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
There was a problem hiding this comment.
Pull request overview
Updates schema transformation so parameterless (or otherwise property-less) object schemas always include a properties object, improving compatibility with strict MCP clients that reject { "type": "object" } without properties.
Changes:
- Added a
_mcp_noopplaceholder property constant for parameterless object schemas. - Routed object-schema return paths through a new
ensure_object_properties()helper. - Added
ensure_object_properties()to inject placeholderproperties(and normalizerequired).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Handle null or empty schema - return minimal valid MCP object schema. | ||
| if ( empty( $schema ) ) { | ||
| return array( | ||
| 'schema' => array( | ||
| 'type' => 'object', | ||
| 'schema' => self::ensure_object_properties( | ||
| array( | ||
| 'type' => 'object', | ||
| ) | ||
| ), | ||
| 'was_transformed' => false, | ||
| 'wrapper_property' => null, |
There was a problem hiding this comment.
The comment above normalize() ("strip empty properties" because PHP encodes empty arrays as []) is now misleading given this branch returns a schema with injected placeholder properties via ensure_object_properties(). Consider updating that earlier comment to reflect the new placeholder-injection strategy so future readers don’t assume properties are always removed from the final schema.
| * If the schema already has non-empty properties, it is returned unchanged. | ||
| * Otherwise, it injects a no-op placeholder property to ensure the schema | ||
| * is a valid JSON object with at least one property. |
There was a problem hiding this comment.
ensure_object_properties() PHPDoc says schemas with non-empty properties are returned “unchanged”, but the method will still mutate the schema by adding required: [] when required is missing or non-array. Either update the docstring to mention required normalization, or avoid injecting an empty required field unless it’s strictly necessary to preserve the “unchanged” contract for valid object schemas.
| * If the schema already has non-empty properties, it is returned unchanged. | |
| * Otherwise, it injects a no-op placeholder property to ensure the schema | |
| * is a valid JSON object with at least one property. | |
| * If the schema already has non-empty properties, they are preserved. | |
| * Otherwise, it injects a no-op placeholder property to ensure the schema | |
| * is a valid JSON object with at least one property. This method also | |
| * normalizes the `required` field to an array when it is missing or invalid. |
| 'schema' => self::ensure_object_properties( | ||
| array( | ||
| 'type' => 'object', | ||
| ) | ||
| ), |
There was a problem hiding this comment.
This changes the contract for null/empty schemas: instead of returning the minimal { type: 'object' }, it now injects placeholder properties (and required). The existing PHPUnit expectations in tests/Unit/Domain/Utils/SchemaTransformerTest.php for null/empty schemas (and empty properties stripping) will fail unless updated to match the new output shape.
| if ( ! isset( $schema['properties'] ) || ! is_array( $schema['properties'] ) || empty( $schema['properties'] ) ) { | ||
| $schema['properties'] = array( | ||
| self::NOOP_PROPERTY => array( | ||
| 'type' => 'string', | ||
| 'description' => 'Optional no-op placeholder for parameterless tools.', | ||
| ), |
There was a problem hiding this comment.
SchemaTransformer is also used by RegisterAbilityAsMcpPrompt to derive prompt arguments from input_schema. Injecting a _mcp_noop property here means abilities with an otherwise-parameterless object schema (e.g. { type: 'object' } / empty properties) will now surface a _mcp_noop prompt argument, which is a user-visible API change. Consider making placeholder injection opt-in (e.g., a flag on transform_to_object_schema), or have prompt argument conversion explicitly ignore the NOOP_PROPERTY key so prompts remain argument-less.
| ); | ||
| } | ||
|
|
||
| if ( ! isset( $schema['required'] ) || ! is_array( $schema['required'] ) ) { |
There was a problem hiding this comment.
ensure_object_properties() currently normalizes required by overwriting any non-array value with an empty array. This can silently mask invalid schemas (e.g., a mis-specified string/object required) that the existing MCP validation layer would otherwise report. Consider only defaulting required when it is missing, and leaving invalid types untouched so validators can surface a clear error.
| if ( ! isset( $schema['required'] ) || ! is_array( $schema['required'] ) ) { | |
| if ( ! array_key_exists( 'required', $schema ) ) { |
Summary
This PR updates
SchemaTransformerto make parameterless ability schemas compatible with stricter MCP clients (notably Anthropic integrations) that rejecttype: objectschemas whenpropertiesis missing.Changes
1. Added a placeholder property constant
private const NOOP_PROPERTY = '_mcp_noop';2. Normalized empty/object schemas to always include properties
Changed all object-schema return paths in
transform_to_object_schema()to go through a new helper:empty($schema))type: object)type === 'object')All now call:
self::ensure_object_properties($schema)3. Added
ensure_object_properties()helperNew private method with the following behavior:
properties(array), it is returned unchangedproperties._mcp_noopwith:type: stringdescription: "Optional no-op placeholder for parameterless tools."requiredexists and is an array_mcp_noop(so no real behavioral change for callers)4. Why this approach (instead of
stdClassin schema)A previous approach used
new \stdClass()for empty properties, which helps JSON encoding but can be risky in internal DTO/validation paths expecting arrays.This patch keeps everything array-based and client-safe:
inputSchema.propertiesis always presentCompatibility Impact
✅ Fixes clients that require object schemas to include
properties✅ Keeps existing tools with real input schemas untouched
✅ Parameterless tools now expose one optional placeholder input key, which is harmless and ignored unless explicitly sent
Rationale
This improves interoperability with strict MCP client schema validation while preserving adapter internals and tool behavior. It specifically addresses cases where parameterless WordPress abilities were transformed into
{ type: "object" }without properties, causing client-side tool registration failures.