[wp-trac] [WordPress Trac] #64955: Abilities API: Add schema compiler for AI tool calling compatibility
WordPress Trac
noreply at wordpress.org
Thu Mar 26 10:50:24 UTC 2026
#64955: Abilities API: Add schema compiler for AI tool calling compatibility
---------------------------------+------------------------------
Reporter: gziolo | Owner: gziolo
Type: enhancement | Status: assigned
Priority: normal | Milestone: Awaiting Review
Component: AI | Version: 6.9
Severity: normal | Resolution:
Keywords: abilities ai-client | Focuses: rest-api
---------------------------------+------------------------------
Changes (by gziolo):
* owner: (none) => gziolo
* status: new => assigned
Comment:
I did extensive research into how JSON schemas flow through the WordPress
AI stack, what each AI provider actually accepts, and how the client-side
validation layer interacts with the server-side one. The findings below
informed the ticket description above. I'm sharing the full technical
detail here so the community can verify the claims and correct anything I
got wrong — particularly around the provider plugin internals, AI client,
or MCP adapter.
== How schemas flow today
No component in the WordPress AI stack transforms ability schemas for AI
provider compatibility. Here's the path:
{{{
wp_register_ability( input_schema ) ← WordPress draft-04 style
│
WP_Abilities_Registry
│
┌────┼──────────────────────┬───────────────────────┐
│ │ │ │
▼ ▼ ▼ ▼
REST API WP AI Client MCP Adapter
(/wp-abilities/v1/) (Prompt Builder) (/wp-json/mcp/...)
│ │ │
│ PHP AI Client SDK MCP tools/list
│ (opaque pass-through) (limited fixes)
│ │ │
│ Provider Plugin │
│ (no schema transform) │
▼ ▼ ▼
Canonical Schema AI Provider API MCP Client
(unchanged) (may reject/strip) (may reject/strip)
}}}
The WP AI Client converts ability names to provider-safe tool names (e.g.,
`my-plugin/translate-content` → `my_plugin__translate_content`) but passes
`input_schema` verbatim to `FunctionDeclaration` in the
[https://github.com/WordPress/php-ai-client PHP AI Client SDK]. The SDK
treats schemas as opaque payloads. The three provider plugins
([https://github.com/WordPress/ai-provider-for-openai OpenAI],
[https://github.com/WordPress/ai-provider-for-anthropic Anthropic],
[https://github.com/WordPress/ai-provider-for-google Google]) extend the
SDK's base model classes and handle API formatting but include no schema
normalization logic. The [https://github.com/WordPress/mcp-adapter MCP
adapter] performs limited structural fixes (empty schema normalization,
non-object wrapping for MCP compliance) but no keyword-level
transformation.
== Incompatibility details
The ticket lists six incompatibilities. Here's the deeper technical
context for each.
=== `required` keyword syntax
WordPress supports both draft-03 per-property `required: true`
(predominant since WP 4.7) and [https://make.wordpress.org/core/2020/07/16
/rest-api-parameter-json-schema-changes-in-wordpress-5-5/ draft-04 array
syntax] (added in WP 5.5). AI providers only understand v4 syntax.
[https://developers.openai.com/api/docs/guides/function-calling OpenAI
further requires] that **all** properties appear in the `required` array —
optional fields must use `"type": ["string", "null"]` union types.
A schema using `'required' => true` on individual properties passes
WordPress server-side validation but the requirement is invisible to both
AI providers and the client-side AJV validator (which only recognizes v4
array syntax). Abilities using draft-03 syntax effectively have no
required-field enforcement on the client.
=== `additionalProperties` default
[https://developer.wordpress.org/rest-api/extending-the-rest-
api/schema/#additionalproperties JSON Schema and WordPress] default
`additionalProperties` to `true` when absent. [https://learn.microsoft.com
/en-us/azure/foundry/openai/how-to/structured-outputs OpenAI mandates]
`additionalProperties: false` on every object in strict mode. Anthropic
strongly recommends it.
=== `oneOf` vs `anyOf`
WordPress supports both (#51025, WP 5.6). OpenAI rejects `oneOf` — only
`anyOf` accepted. Anthropic normalizes `oneOf` to `anyOf`. Both providers
[https://github.com/anthropics/claude-code/issues/4886 reject] `oneOf`,
`allOf`, and `anyOf` at the root level of a tool's `input_schema`.
Converting `oneOf` to `anyOf` is safe for generation since `anyOf` is
semantically more permissive — the canonical validators still enforce
`oneOf` semantics on both client and server.
=== Validation keywords
Keywords like `minimum`, `maximum`, `minLength`, `maxLength`, `pattern`,
`multipleOf`, and most `format` values are fully supported by WordPress's
validators but unsupported by AI providers' constrained decoding.
[https://docs.claude.com/en/docs/build-with-claude/structured-outputs
Anthropic's SDK strips them] and injects constraint descriptions into
`description`. [https://developers.openai.com/api/docs/guides/structured-
outputs OpenAI rejects them] in strict mode. [https://cloud.google.com
/vertex-ai/generative-ai/docs/reference/rest/v1beta1/FunctionDeclaration
Google's Gemini API] has broader support including numeric constraints.
The compiler relocates these to `description` text as soft guidance.
Enforcement remains with WordPress's validators — if an AI model produces
a value outside the `minimum`/`maximum` range, the client-side or server-
side validator catches it.
=== `$ref` / `definitions` gap
WordPress does not support `$ref` or `definitions`/`$defs` for schema
composition. AI providers support internal `$ref` references and use
`$defs` (draft 2019-09 keyword name), while draft-04 uses `definitions`.
WordPress schemas are already fully inlined, so no `$ref` resolution is
needed in the compiler — but these keywords aren't in the
[https://developer.wordpress.org/reference/functions/rest_get_allowed_schema_keywords/
allowed keywords list] and get silently stripped.
Canonical ability schemas should use `definitions` (draft-04) rather than
`$defs` (draft 2019-09), because the client-side `ajv-draft-04` validator
recognizes `definitions` but not `$defs`. The compiler maps `definitions`
→ `$defs` in AI-facing output. Similarly, canonical schemas should use
single-value `enum` rather than `const` (draft-06), since `const` doesn't
exist in draft-04. AI providers accept both forms.
=== WordPress-only keywords
`context` (view/edit/embed array), `readonly` (lowercase — see #56152),
and `arg_options` (escape hatch for custom callbacks,
[https://developer.wordpress.org/reference/classes/wp_rest_controller/get_public_item_schema/
stripped by `get_public_item_schema()`]). Meaningless to AI providers.
== Client-side validation and the strictness gradient
The `@wordpress/abilities` package validates schemas client-side using
`ajv-draft-04` with `coerceTypes: false` — stricter than WordPress PHP,
which performs type juggling. The
[https://github.com/WordPress/gutenberg/blob/b0040c266e843f93255f5343f393866a9f06e2e2/packages/abilities/src/validation.ts#L1-L9
header comment] in `validation.ts` explicitly states the design intent:
> Rules are configured to support the intersection of common rules between
JSON Schema draft-04, WordPress (a subset of JSON Schema draft-04), and
various providers like OpenAI and Anthropic.
Both server-side and client-side abilities are validated on the client.
`executeAbility()` in `api.ts` validates input and output unconditionally
when schemas are present. Server-side abilities fetched by `@wordpress
/core-abilities` carry their schemas into the client-side store, creating
a double-validation path: client-side AJV first, then server-side
`rest_validate_value_from_schema` when the REST API call executes.
This creates a natural strictness gradient:
{{{
AI Provider Schema (strictest) ← canonical schema compiled for AI
consumption
│
│ AI model generates values conforming to this
▼
Client-side AJV draft-04 (strict) ← validates against canonical schema
│
│ values pass because compiled schema is a strict subset
▼
Server-side PHP validator (most permissive) ← validates against canonical
schema
│
│ values pass because PHP is even more permissive than AJV
▼
Ability callback executes
}}}
Because values flow from the strictest layer to the most permissive, any
value that satisfies the compiled AI schema automatically passes both
validators. The compiler's transformations only narrow what's accepted —
setting `additionalProperties: false`, adding all properties to
`required`, converting `oneOf` to `anyOf` — so the compiled schema is
inherently safe for the double-validation path. The canonical schema in
the client-side store never needs to change.
== Proposed compiler architecture
The compiler sits at every boundary where schemas leave WordPress for AI
consumption:
{{{
wp_register_ability( input_schema ) ← WordPress draft-04 style
│
WP_Abilities_Registry
│
┌────┼──────────────────────┬───────────────────────┐
│ │ │ │
▼ ▼ ▼ ▼
REST API WP AI Client MCP Adapter
(/wp-abilities/v1/) (Prompt Builder) (standalone plugin)
│ │ │
│ compile_schema() compile_schema()
│ │ │
▼ ▼ ▼
Canonical Schema AI-Strict Schema AI-Strict Schema
(unchanged for REST (before SDK hand-off) (in tools/list)
API consumers)
}}}
=== Compiler transformations (in order)
1. **Normalize `required`**: Walk all object nodes. Collect per-property
`required: true` flags (draft-03) and merge with any existing `required`
array (draft-04). For AI targets, add all remaining properties to
`required` and convert their types to `["original_type", "null"]` unions.
Remove per-property `required` booleans.
2. **Set `additionalProperties: false`**: Recursively on every object
node, including nested objects inside `properties`, `items`, and `anyOf`
sub-schemas.
3. **Convert `oneOf` to `anyOf`**: Direct keyword replacement. Sub-schema
array unchanged.
4. **Relocate validation keywords to descriptions**: For each property
with `minimum`, `maximum`, `minLength`, `maxLength`, `pattern`,
`multipleOf`, or unsupported `format` values, append a human-readable
constraint summary to the property's `description`. Then remove the
keywords.
5. **Strip WordPress-only keywords**: Remove `context`, `readonly`,
`arg_options`, and any other WordPress-specific annotations.
6. **Map `definitions` to `$defs`**: If the schema uses `definitions`
(draft-04), rename it to `$defs` (draft 2019-09) in the output. AI
providers expect the modern keyword name.
7. **Validate structural limits**: Check for total properties, nesting
levels, and enum values ([https://developers.openai.com/api/docs/guides
/structured-outputs OpenAI limits]). Emit `_doing_it_wrong()` warnings if
exceeded.
=== Provider-specific `$target` behavior
* **OpenAI**: Strictest. Strip all validation keywords, enforce
`additionalProperties: false`, put all properties in `required`. Strip
`format` entirely.
* **Anthropic**: Moderately strict. Same structural requirements, but
allows some optional properties (up to
[https://platform.claude.com/docs/en/build-with-claude/structured-outputs
optional parameters, parameters with union types] across all strict
tools). May preserve some `format` values.
* **Google Gemini**: Most permissive. Supports [https://cloud.google.com
/vertex-ai/generative-ai/docs/reference/rest/v1beta1/FunctionDeclaration
numeric constraints] and does not require `additionalProperties: false`.
The compiler can preserve more keywords.
* **`default`**: Targets the most restrictive common subset (effectively
OpenAI's constraints), or whatever else we decide as a better approach.
== Why this approach over alternatives
**Alternative A: Dual-mode validator in core.** Add a `strict` flag to
`rest_validate_value_from_schema`. Invasive — touches the most critical
validation path in WordPress, risks regressions in every REST API
endpoint, and couples AI provider requirements to core's validation logic.
AI provider requirements change faster than WordPress release cycles.
Rejected.
**Alternative B: Require ability authors to write AI-compatible schemas
directly.** Shifts complexity to every plugin author, guarantees
inconsistency, and doesn't help with `required` normalization (which
requires semantic changes like null-union types). Existing core abilities
would all need manual rewrites. Rejected.
**Alternative C: Add schema transformation to the PHP AI Client SDK or
provider plugins.** Fixes the WP AI Client path but not MCP adapter,
WebMCP, or any future adapter. Places WordPress-specific schema knowledge
inside a [https://make.wordpress.org/ai/2025/07/17/php-ai-api/ provider-
agnostic SDK] that treats schemas as opaque payloads. Rejected.
**Alternative D: Schema compiler as a shared core utility (proposed).**
Pure, testable, filterable function alongside the Abilities API. Changes
no validation behavior. All AI-facing components call the same function.
The strictness gradient guarantees compiled schemas are safe for the
existing double-validation path. Accepted.
== Implementation steps
Steps are ordered by dependency. Steps within the same group can be done
in parallel.
=== Step 1: Ship the schema compiler function (PHP)
Introduce `wp_compile_ability_schema_for_ai( $schema, $target = 'default'
)` in WordPress core, alongside the Abilities API. Purely additive —
transforms schemas on output, changes no validation behavior. Filterable
via `wp_ability_schema_compiled`.
No dependencies. Enables all downstream work.
=== Step 2: Integrate compiler into the WP AI Client
The WP AI Client's `Ability_Function_Resolver` should call
`wp_compile_ability_schema_for_ai()` on each ability's `input_schema`
before passing it to the [https://github.com/WordPress/php-ai-client PHP
AI Client SDK]. The `$target` should be derived from the active provider
plugin.
Highest-impact integration point — covers the primary path through which
WordPress sites interact with AI providers.
Depends on Step 1.
=== Step 3: Add `wp_rest_allowed_schema_keywords` filter
Add a filter hook to
[https://developer.wordpress.org/reference/functions/rest_get_allowed_schema_keywords/
rest_get_allowed_schema_keywords()]:
{{{#!php
apply_filters( 'wp_rest_allowed_schema_keywords', $keywords, $context );
}}}
Minimal core change with broad utility beyond the Abilities API. Enables
plugins to pass through keywords like `definitions` and `$ref` without
patching core.
No dependencies. Parallel with Steps 1-2.
=== Step 4: Add `definitions` and `$ref` to the allowed keywords list
Add these to
[https://developer.wordpress.org/reference/functions/rest_get_allowed_schema_keywords/
rest_get_allowed_schema_keywords()] for output passthrough — not
validation resolution. `definitions` is the draft-04 keyword compatible
with the client-side `ajv-draft-04` validator; the compiler maps it to
`$defs` in AI-facing output. `$ref` is supported by AJV draft-04 natively.
AI providers also support `$defs` (draft 2019-09) and `const` (draft-06),
but neither exists in draft-04 and the client-side validator does not
recognize them. Canonical ability schemas should use `definitions` and
single-value `enum` respectively.
Depends on Step 3, or can be done directly in core alongside it.
=== Step 5: Emit authoring guidance via `_doing_it_wrong()`
When abilities are registered, check schemas for patterns that cause AI
compatibility issues:
* `oneOf` used where `anyOf` would suffice → suggest `anyOf`.
* `additionalProperties` absent on object schemas → suggest setting it
explicitly.
* Per-property `required: true` (draft-03) → suggest v4 array syntax.
This syntax is also not enforced by the client-side AJV validator, so
switching improves client-side validation coverage.
* `readonly` used instead of `readOnly` → note the casing deviation
(#56152).
Advisory notices, not validation errors.
Depends on Step 1.
=== Step 6: Introduce `schema_version` for opt-in strict authoring
Add an optional `schema_version` key to ability registration args.
Abilities declaring `schema_version: 2` opt into stricter conventions:
* Draft-03 per-property `required: true` is rejected at registration
time.
* `additionalProperties` must be explicitly set on all object nodes.
* `oneOf` is rejected (use `anyOf` instead).
* The compiler can skip most transformation steps for v2 schemas.
Migration path: new abilities adopt v2, existing abilities continue on
implicit v1.
Depends on Steps 1 and 5.
=== Adjacent: MCP adapter integration
The [https://github.com/WordPress/mcp-adapter MCP adapter] is a standalone
plugin. Once Step 1 lands, it should call
`wp_compile_ability_schema_for_ai()` in its `tools/list` handler. Does not
require coordination with core releases.
== Future considerations (not blocking)
* **General-purpose `wp_validate_json_schema()` function.**
`rest_validate_value_from_schema` now serves the Abilities API in contexts
unrelated to the REST API. A `wp_validate_json_schema( $value, $schema,
$args )` wrapper would reflect this broader role and provide a natural
home for strict-mode validation tied to `schema_version`. Initially a thin
passthrough.
* **`$ref` resolution in the validator.** Teaching the validator to
resolve internal `$ref` references would unlock schema composition for
validation, not just output passthrough. Significant change — should be
its own tracked effort.
* **Client-side schema compilation for browser-agent protocols.** Since
executing any ability routes through the REST API where server-side
compilation applies, a JavaScript compiler is not needed today. As
[https://github.com/WordPress/ai/pull/224 WebMCP] matures, it may need to
expose client-side ability schemas directly to browser-based AI agents.
The PHP compiler's test fixtures should be portable to JavaScript when
this need arises.
* **Schema compilation in the PHP AI Client SDK.** If the SDK eventually
gains its own schema normalization, the WP AI Client could delegate to it.
The SDK currently treats schemas as opaque payloads; changing this is a
separate decision.
* **Structural limit warnings.** OpenAI limits schemas for properties,
nesting levels, and enum values. Worth checking at registration time but
limits may change. Implement as soft warnings.
* **`readOnly` casing alignment.** #56152 tracks this. Not blocking since
both forms are stripped by the compiler.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/64955#comment:1>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list