The input_schema and output_schema arguments in wp_register_ability() aren’t
decoration, they’re standard JSON Schema, the same
specification used across the MCP ecosystem to describe tool inputs and outputs. Get
these right and an AI client can validate its own calls before ever hitting your PHP,
and can generate correct arguments without guessing. Get them loose or wrong, and you’ll
be defending against malformed input inside every execute_callback instead.
The my-plugin/create-draft-post ability from Lesson 2. Basic familiarity with JSON
Schema helps but isn’t required, every construct used here is explained inline.
Step 1: The base object shape
Almost every input_schema you write will be a type: object with a properties map
and a required array:
// File: my-mcp-abilities.php (schema fragment)
'input_schema' => array(
'type' => 'object',
'properties' => array(
'title' => array(
'type' => 'string',
'description' => 'The post title.',
),
),
'required' => array( 'title' ),
),
required is a list of property names, not a per-property flag, this trips up people
coming from other schema systems. A property you don’t list in required is implicitly
optional, and your execute_callback needs to handle it being absent (usually with
$input['field'] ?? $default).
Step 2: Picking types and constraints
JSON Schema’s primitive types cover almost everything an ability needs: string,
integer, number, boolean, array, and object. The constraint keywords you’ll
reach for most:
| Keyword | Applies to | Example use |
|---|---|---|
enum | any type | Restricting post_status to draft, pending, or publish, nothing else. |
minimum / maximum | integer, number | Capping numberposts at, say, 100 so an AI client can’t request an unbounded query. |
minLength / maxLength | string | Rejecting an empty title, or one absurdly long. |
default | any type | Documents what happens when a client omits an optional field, informational for the schema; your callback still needs its own fallback logic. |
items | array | Describing what each entry in an array input looks like, e.g. an array of tag strings. |
Applied to a tightened version of the create-draft-post ability:
// File: my-mcp-abilities.php (tightened input_schema)
'input_schema' => array(
'type' => 'object',
'properties' => array(
'title' => array(
'type' => 'string',
'description' => 'The post title.',
'minLength' => 1,
'maxLength' => 200,
),
'body' => array(
'type' => 'string',
'description' => 'The post content, as HTML or plain text.',
),
'status' => array(
'type' => 'string',
'description' => 'The initial post status.',
'enum' => array( 'draft', 'pending' ),
'default' => 'draft',
),
),
'required' => array( 'title', 'body' ),
),
Note that status deliberately doesn’t include publish in its enum. This ability’s
job is drafting, not publishing, an AI client physically cannot pass a value the schema
doesn’t allow, which is a stronger guarantee than checking it inside your callback after
the fact.
Step 3: Writing output_schema
output_schema documents the shape of whatever execute_callback returns. Nothing in
WordPress core enforces it against the actual return value at runtime, it’s descriptive,
not a validator, but MCP clients and any human reading your code both rely on it being
accurate:
// File: my-mcp-abilities.php (output_schema)
'output_schema' => array(
'type' => 'object',
'properties' => array(
'post_id' => array(
'type' => 'integer',
'description' => 'The ID of the newly created post.',
),
'edit_url' => array(
'type' => 'string',
'description' => 'The wp-admin edit link for the new post.',
),
),
),
Because it isn’t enforced, an out-of-date output_schema is a silent lie: it’ll look
correct in your code review and mislead every client that reads it. Treat changing what
execute_callback returns as a reason to update output_schema in the same commit,
every time.
Test it
Ask WordPress to show you the registered schema back, exactly what an AI client would see if it asked:
# File: terminal
wp eval '
$ability = wp_get_ability( "my-plugin/create-draft-post" );
echo wp_json_encode( $ability->get_input_schema(), JSON_PRETTY_PRINT );
'
Then confirm the schema actually rejects what it should, by passing a status value
outside the enum and checking your execute_callback never got there in the first place
(this depends on where validation happens in your specific WordPress version, some
validate before execute_callback runs; treat the schema as documentation the client
relies on, and keep basic defensive checks in your callback regardless).
A tight input_schema stops an AI client from constructing obviously invalid requests
and helps it generate correct ones, but it is not a substitute for validating and
sanitizing inside execute_callback, and it is never a substitute for
permission_callback. Schema validation answers “is this shaped correctly?” Permission
checks answer “is this caller allowed to do this at all?” You need both, the next lesson
is entirely about the second one.
Recap
input_schema and output_schema are standard JSON Schema: a type: object with
properties and required for inputs, and a documented, non-enforced return shape for
outputs. Use enum, minimum/maximum, and length constraints to narrow what an AI
client can even attempt, which reduces the defensive code you need inside your callback,
without replacing it entirely.
Resources & further reading
- Abilities API reference, developer.wordpress.org
- wp_register_ability() function reference, developer.wordpress.org
- MCP Adapter repository, GitHub