MemberPress AI Academy memberpress.ai

Discovery · The discovery loop

Ship the MCP discovery loop on WordPress

Video13 min

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_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 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

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.