[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