# Ship the MCP discovery loop on WordPress

Web page: https://academy.memberpress.ai/courses/agent-discoverable-surfaces/l7/

Lesson 2 of 7 in [Agent-Discoverable Surfaces](https://academy.memberpress.ai/courses/agent-discoverable-surfaces/), 13 min.

Video: https://www.youtube.com/watch?v=_lFcSpKqTG0

**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:

```php
// 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:

```php
// 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:

```php
// 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:

```bash
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`](https://github.com/sethshoultes/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_redirect` fires 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: any` and filters at response time.** The audit question fails — one wrong conditional ships drafts. Move the wall into the `WP_Query` args.
- **The discovery file returns `{}` instead of a 404 when the catalog is off.** The empty object still leaks the endpoint URL. Send `404` and `exit` so 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`](https://github.com/sethshoultes/wp-mcp-discovery) shipped to [adventurebuildr.com](https://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"](https://sethshoultes.com/blog/closing-the-agent-discovery-loop.html) — the framing for why agent-mediated discovery is its own layer, distinct from schema.org.
- [`wp-mcp-discovery` on GitHub](https://github.com/sethshoultes/wp-mcp-discovery) — 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.

Next: [Formatters are the security architecture, not the permission check](https://academy.memberpress.ai/courses/agent-discoverable-surfaces/l6/)
