Discovery · The discovery loop
Ship the MCP discovery loop on WordPress
Hook
Open the WordPress plugin directory. Search "MCP". One result, under a thousand active installs. Yoast has thirteen million. WordPress runs forty percent of the web, and the agent-discoverability layer for it is empty.
Concept
Prerequisites: you have registered a custom post type, called register_rest_route once, and read enough wp-json/wp/v2/ output to recognize what WordPress auto-exposes. This is l5's static-site loop ported to WordPress — the three roles are the same, the materials change.
The same loop, different platform
The discovery loop has three roles: a .well-known/mcp.json file, a JSON-RPC endpoint, and a public-safe formatter. l5 built that loop on a static site — Cloudflare Worker, literal file in the repo, catalog read from /posts.json. WordPress ships the equivalent of all three in core. Post types are the data model. register_rest_route is the endpoint primitive. template_redirect is the discovery file mechanism. The radio parts are on the bench. Nobody has wired them together.
Why the layer is missing
The SEO plugins that own this audience treat AI as a defensive problem — which crawlers to block in robots.txt, what to put in schema.org JSON-LD. Schema.org gets your site crawled by GPTBot, ClaudeBot, and Perplexity. MCP is what gets it queried. Different layers. Yoast and Rank Math shipped the first. Nobody has shipped the second.
Walk-through
Endpoint. One register_rest_route call, one JSON-RPC dispatcher, three methods:
// includes/mcp-routes.php
add_action('rest_api_init', function () {
register_rest_route('your-prefix/v1', '/mcp', [
'methods' => 'POST',
'callback' => 'mcp_dispatch',
'permission_callback' => '__return_true',
]);
});
function mcp_dispatch(WP_REST_Request $request) {
$body = $request->get_json_params();
$method = $body['method'] ?? '';
$id = $body['id'] ?? null;
$result = match ($method) {
'initialize' => ['protocolVersion' => '2025-03-26'],
'tools/list' => ['tools' => mcp_tool_descriptors()],
'tools/call' => mcp_run_tool($body['params']),
default => null,
};
return ['jsonrpc' => '2.0', 'id' => $id, 'result' => $result];
}
permission_callback => '__return_true' is not the wall. l6 covered why — the wall is the formatter, not the gate.
Formatter. The wall lives in the WP_Query args, not in response shaping:
// includes/tools/list-posts.php
function list_posts_tool() {
$query = new WP_Query([
'post_type' => 'post',
'post_status' => 'publish',
'has_password' => false,
'posts_per_page' => 20,
]);
return array_map(fn($p) => [
'title' => $p->post_title,
'slug' => $p->post_name,
'permalink' => get_permalink($p),
], $query->posts);
}
No admin handle in the call chain. Run l6's audit question against it — if every permission check failed open today, what would leak? Drafts and password-protected posts are not in the query. They are not reachable from this function at all.
Discovery file. A template_redirect hook intercepts the path and serves a static JSON response:
// includes/well-known.php
add_action('template_redirect', function () {
if ($_SERVER['REQUEST_URI'] !== '/.well-known/mcp.json') return;
if (! get_option('mcp_catalog_public')) {
status_header(404);
exit;
}
header('Content-Type: application/json');
echo json_encode([
'mcp' => ['endpoint' => home_url('/wp-json/your-prefix/v1/mcp')],
]);
exit;
});
The shape of that JSON is a 2026-04 snapshot — verify against modelcontextprotocol.io before shipping. The 404 when the catalog flag is off is load-bearing: an empty {} still announces the endpoint exists, same trap as l5's missing file.
Test the loop with two curl commands, no auth:
curl https://yoursite.com/.well-known/mcp.json
# → {"mcp":{"endpoint":"https://yoursite.com/wp-json/your-prefix/v1/mcp"}}
curl -X POST https://yoursite.com/wp-json/your-prefix/v1/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
# → {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
Both succeed without credentials, or you have not shipped.
Three options today, and why this generalizes
Install wp-mcp-discovery as a drop-in, write the three pieces yourself, or wait twelve to eighteen months for an SEO plugin to ship it. If you control the calendar, you do not have to wait. The pattern ports — Drupal entity types, Shopify products and collections, Sanity/Contentful/Strapi. Any CMS with a typed data model and an existing REST API wraps the same way. When you ship a CMS, an SEO plugin, or a framework now, the MCP discovery layer is part of the table-stakes API surface.
If you get stuck
template_redirectfires too late. WordPress has already routed to a 404 page. Use a higher-priority hook (parse_request, priority 1) or a rewrite rule in.htaccess/nginx.- The formatter reads with
post_status: anyand filters at response time. The audit question fails — one wrong conditional ships drafts. Move the wall into theWP_Queryargs. - The discovery file returns
{}instead of a 404 when the catalog is off. The empty object still leaks the endpoint URL. Send404andexitso the disabled state is genuinely silent.
Cross-reference
l5 (m1) is the same loop on a static site — read it first if the three roles still feel abstract. l6 (m2) is the formatter deep dive that explains why the WP_Query args are the wall. l1 (m3) covers rate-limit calibration once OAuth lands and real AI-client traffic shows up.
Deliverable
Name the three roles for a WordPress site you own — endpoint route, formatter function, discovery file mechanism — and run both curl commands without auth. Both succeed, or you have not shipped.
Deployed in the wild
wp-mcp-discovery shipped to adventurebuildr.com on 2026-05-07. Twenty-eight tools registered automatically — Posts, Pages, MemberPress courses and lessons, plus eleven custom post types from AdventureBuildr (iasb_character, iasb_location, iasb_vehicle, iasb_weapon, iasb_lore, etc.). Under five minutes from plugin install to first successful tools/call. Verify it yourself: curl https://adventurebuildr.com/.well-known/mcp.json.
One compatibility note from that deploy: All in One SEO Pro v4.9.6.2 added llms.txt generation in early 2026, and AIOSEO's handler registers earlier than ours, so AIOSEO's llms.txt is what AI crawlers see when both plugins are active. There is no conflict — the MCP-specific surfaces (/.well-known/mcp.json and /wp-json/wp-mcp-discovery/v1/mcp) are unaffected because AIOSEO does not touch those namespaces. AIOSEO ships the index layer; this plugin ships the query layer; both run cleanly side by side.
Further reading
- "Closing the Agent Discovery Loop" — the framing for why agent-mediated discovery is its own layer, distinct from schema.org.
wp-mcp-discoveryon GitHub — the drop-in plugin source, plus the full v0.5 / v1.0 / v2.0 roadmap.
Try these prompts
- Write a register_rest_route call that exposes /mcp under a custom namespace with a JSON-RPC 2.0 handler dispatching initialize, tools/list, and tools/call.
- Write a list_posts WordPress formatter that uses WP_Query with post_status=publish and has_password=false, and returns title, slug, and permalink for each result.
- Write the template_redirect hook that serves a /.well-known/mcp.json JSON object when a public catalog flag is on, and returns a 404 when the flag is off.
Sign in to keep your place and tick off lessons. Reading stays open either way.