The previous lesson’s fetch-and-Basic-Auth code works, but repeating it for every
ability, with the URL, method, and response shape hand-typed each time, is exactly the
kind of duplication that drifts out of sync with WordPress the moment an ability’s
schema changes. This lesson wraps that boilerplate in a small typed client, and
generates its TypeScript types directly from the same input_schema and
output_schema JSON Schema definitions you already wrote in PHP, so the client and the
ability can’t quietly disagree about shape.
The Next.js server-side fetch pattern from Lesson 3, and at least one ability with a
real input_schema and output_schema (Course 1’s ability anatomy lesson).
Step 1: Export your ability schemas as plain JSON
Your abilities’ schemas already exist in PHP. Rather than retyping them by hand in TypeScript, dump them somewhere your build process can read, a simple WP-CLI command run at build time works well for this:
// File: my-plugin.php
add_action( 'cli_init', function () {
WP_CLI::add_command( 'my-plugin export-schemas', function () {
$abilities = wp_get_abilities();
$out = array();
foreach ( $abilities as $ability ) {
$out[ $ability->get_name() ] = array(
'input_schema' => $ability->get_input_schema(),
'output_schema' => $ability->get_output_schema(),
);
}
WP_CLI::line( wp_json_encode( $out, JSON_PRETTY_PRINT ) );
} );
} );
wp-env run cli wp my-plugin export-schemas > schemas/abilities.json
Now schemas/abilities.json contains real JSON Schema, exactly what your PHP
registration declared, not a manually re-typed guess sitting in a separate repository.
Step 2: Generate TypeScript types from that JSON Schema
json-schema-to-typescript is a widely used, general-purpose npm package for exactly
this: turning a JSON Schema document into .d.ts type definitions. It has no
WordPress-specific knowledge, and needs none, input_schema and output_schema are
already standard JSON Schema.
npm install --save-dev json-schema-to-typescript
// File: scripts/generate-types.ts
import { compile } from 'json-schema-to-typescript';
import { writeFileSync, readFileSync } from 'node:fs';
const schemas = JSON.parse(readFileSync('schemas/abilities.json', 'utf-8'));
async function run() {
let output = '';
for (const [name, { input_schema, output_schema }] of Object.entries(schemas)) {
const typeName = name.replace(/[^a-zA-Z0-9]/g, '_');
output += await compile(input_schema as any, `${typeName}_Input`);
output += await compile(output_schema as any, `${typeName}_Output`);
}
writeFileSync('src/generated/abilities.d.ts', output);
}
run();
Run this as a build step whenever schemas/abilities.json changes, and the generated
.d.ts file stays a direct reflection of what WordPress actually registered.
If you’ve generated a typed client from an OpenAPI spec before, this is the same idea: the schema is the single source of truth, and the types are a build artifact, not something a developer maintains by hand in two places at once.
Step 3: A small, generic typed client
With generated types in place, wrap the actual fetch logic once:
// File: src/lib/abilities-client.ts
type CallOptions = {
baseUrl: string;
username: string;
appPassword: string;
};
export function createAbilitiesClient(opts: CallOptions) {
const auth = Buffer.from(`${opts.username}:${opts.appPassword}`).toString('base64');
return {
async call<TInput, TOutput>(abilityName: string, input: TInput): Promise<TOutput> {
const res = await fetch(`${opts.baseUrl}/wp-json/mcp/v1/${abilityName}`, {
method: 'POST',
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
cache: 'no-store',
});
if (!res.ok) {
throw new Error(`Ability ${abilityName} failed with status ${res.status}`);
}
return res.json() as Promise<TOutput>;
},
};
}
Step 4: Calling an ability with real types, not any
Import the generated types alongside the client, and every call is now checked against the ability’s actual schema:
// File: app/api/latest-post-title/route.ts
import { createAbilitiesClient } from '@/lib/abilities-client';
import type {
my_plugin_get_latest_post_title_Input,
my_plugin_get_latest_post_title_Output,
} from '@/generated/abilities';
import { NextResponse } from 'next/server';
const client = createAbilitiesClient({
baseUrl: process.env.WP_BASE_URL!,
username: process.env.WP_APP_USERNAME!,
appPassword: process.env.WP_APP_PASSWORD!,
});
export async function GET() {
const result = await client.call<
my_plugin_get_latest_post_title_Input,
my_plugin_get_latest_post_title_Output
>('my-plugin/get-latest-post-title', {});
return NextResponse.json(result);
}
If the PHP output_schema later adds a required field, regenerating types surfaces a
real TypeScript compile error at every call site expecting the old shape, instead of a
silent runtime mismatch discovered by a user.
Test it: change a schema and watch the type break on purpose
As a deliberate check, add a new required property to an ability’s output_schema in
PHP, re-run the export and generation steps, and try to build your Next.js project
without updating any call sites:
wp-env run cli wp my-plugin export-schemas > schemas/abilities.json
npx tsx scripts/generate-types.ts
npm run build
You should see a TypeScript error at the call site accessing the old shape. That failure, at build time rather than in production, is the entire point of generating types from the real schema instead of hand-writing them.
Nothing forces you to re-run schema export and type generation when an ability changes. Wire it into your CI pipeline or a pre-build script so a schema change on the WordPress side can’t silently drift out of sync with a client that was only ever generated once and never refreshed.
Recap
A thin, generic call() function centralizes auth, the base URL, and error handling in
one place, and generating its input and output types from your abilities’
input_schema and output_schema JSON Schema keeps the client honest about what
WordPress actually expects and returns, without hand-maintaining a parallel type
definition. The next lesson takes this same shape of call, minus a Node.js server in
the middle, to a mobile app talking to WordPress directly from the device.
Resources & further reading
- json-schema-to-typescript, GitHub
- Abilities API reference, developer.wordpress.org
- Adding custom REST API endpoints, developer.wordpress.org