Schemas & Permissions

Defining Input and Output JSON Schemas for Abilities

⏱ 16 min

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.

What you'll learn in this lesson
The object schema shape abilities expect
type, properties, required, and how they nest.
Choosing the right type and constraints for each field
string, integer, boolean, enum, and when to add minimum/maximum.
Writing output_schema, and why it matters even though nothing enforces it at runtime
It documents the contract for both AI clients and future you.
A worked example
Tightening the create-draft-post ability from Lesson 2 with a stricter schema.
Prerequisites

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:

Common JSON Schema constraints for ability inputs
KeywordApplies toExample use
enumany typeRestricting post_status to draft, pending, or publish, nothing else.
minimum / maximuminteger, numberCapping numberposts at, say, 100 so an AI client can’t request an unbounded query.
minLength / maxLengthstringRejecting an empty title, or one absurdly long.
defaultany typeDocuments what happens when a client omits an optional field, informational for the schema; your callback still needs its own fallback logic.
itemsarrayDescribing 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).

Assuming the schema alone is your security boundary

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

← Registering Your First Ability with wp_register_ability Writing Permission Callbacks: Securing Who Can Call What →