Skip to content

WP Loupe MCP Integration

API Version Status

PropertyValue
MCP API Versionv1
Implementation StatusDraft/Beta
WordPress Minimum6.3+
PHP Minimum8.2+

This document describes the Model Context Protocol (MCP) capabilities exposed by the WP Loupe plugin. It covers discovery, authentication, commands, pagination, rate limiting, and extension points.

The MCP server provides a structured interface for external agents or tools to:

  • Discover supported commands and metadata
  • Perform search queries (anonymous or authenticated)
  • Retrieve post data and schema information
  • Perform health checks (authenticated)

The design uses hybrid access: anonymous users can perform limited search; authenticated clients (Bearer tokens) receive higher limits and access protected commands.

WP Loupe exposes two discovery endpoints:

PurposePathNotes
MCP Manifest/.well-known/mcp.jsonHigh-level manifest (commands, scopes) with ETag caching
Protected Resource Metadata/.well-known/oauth-protected-resourceOAuth2 resource metadata (subset)

Fallback REST routes (if rewrite rules are missing):

  • GET /wp-json/wp-loupe-mcp/v1/discovery/manifest
  • GET /wp-json/wp-loupe-mcp/v1/discovery/protected-resource

If rewrites fail (multisite edge cases), a raw path fallback is used and a logging action fires: do_action( 'wp_loupe_mcp_raw_wellknown_fallback', $type, $uri ).

Before any MCP endpoints are reachable you must explicitly enable the MCP server.

  1. Navigate to: Settings → WP Loupe → MCP tab.
  2. Check “Enable MCP Server” and save. This activates:
  • /.well-known/mcp.json
  • /.well-known/oauth-protected-resource
  • REST namespace: /wp-json/wp-loupe-mcp/v1/*
  1. When disabled these endpoints return 404 (hard fail, not soft-deny) to reduce surface area.

You can toggle this at any time; existing transient tokens become unusable when disabled because command routing stops registering.

On the MCP tab (when enabled):

  • Create a token by providing an optional label, selecting scopes, and setting a TTL (hours). The raw token is shown exactly once – copy it immediately.
  • Scopes: uncheck any to restrict (principle of least privilege). All are pre-selected by default.
  • TTL (hours): 1–168 (7 days) or 0 for a non-expiring (indefinite) token. Use 0 sparingly.
  • List columns: Label, Scopes, Issued, Expires (Never if TTL=0), and Actions.
  • Last-used timestamp (if present) is updated on each successful authenticated command.
  • Revoke (single) removes a token immediately. Revoke All removes every issued token.
  • CLI-issued tokens now appear automatically after issuance (registry mirroring). Older tokens from before this feature will not retroactively display.

Previously “future” features (scope selection, adjustable TTL) are now implemented.

Authentication uses OAuth2 client_credentials (scaffold-level) with in-memory (transient) token persistence.

  • Token endpoint: POST /wp-json/wp-loupe-mcp/v1/oauth/token
  • Body supports either JSON or form-encoded:
    • grant_type=client_credentials
    • client_id=wp-loupe-local (default)
    • client_secret (optional – if not defined by constant, secret-less dev mode allowed)
    • scope space-separated (optional)
ScopeDescription
search.readPerform search queries (higher auth limits)
post.readRetrieve post metadata/content (future restrictions may apply)
schema.readAccess schema details
health.readHealth check command
commands.readList commands metadata
CommandAnonymousAuthenticated Requirement
searchPostsYes (lower limits)search.read (if token presented)
getPostYes (currently unrestricted)(Will enforce post.read if tightened)
getSchemaYes(Will enforce schema.read if tightened)
listCommandsYescommands.read (if enforced later)
healthCheckNohealth.read
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "search.read health.read"
}

Standard OAuth-style headers are set for auth errors, e.g.:

WWW-Authenticate: Bearer realm="wp-loupe", error="insufficient_scope", error_description="Missing required scopes: health.read", scope="health.read"

Possible error codes: invalid_request, invalid_client, unsupported_grant_type, invalid_scope, invalid_token, insufficient_scope, invalid_header, missing_token.

All commands are invoked via: POST /wp-json/wp-loupe-mcp/v1/commands

Request envelope:

{
"command": "searchPosts",
"params": { ... },
"requestId": "optional-correlation-id"
}

Response envelope:

{
"success": true,
"error": null,
"requestId": "...",
"data": { ... }
}

On failure:

{
"success": false,
"error": { "code": "rate_limited", "message": "Rate limit exceeded" },
"data": null
}

Search published posts/pages.

Params:

NameTypeDescription
querystring (required)Search phrase
limitint (optional)Max hits (auth: up to 100 default; anon: up to 10)
cursorstring (optional)Pagination cursor
fieldsstring[] (optional)Whitelist fields (subset of: id, title, excerpt, url, content, taxonomies, post_type)
postTypesstring[] (optional)Restrict to specific post types

Response data object:

{
"hits": [ { "id": 123, "title": "...", "url": "..." } ],
"tookMs": 12,
"pageInfo": { "nextCursor": "..." }
}

Pagination cursor is base64 + HMAC signed JSON containing offset + query hash.

Retrieve a single published post by ID. Params: id (required), fields (optional array).

Returns schema info (indexable/filterable/sortable fields per post type).

Example response:

{
"success": true,
"error": null,
"requestId": null,
"data": {
"post": {
"indexable": ["post_title", "post_content", "post_excerpt", "post_date", "post_author", "permalink"],
"filterable": ["post_title", "post_date", "post_author"],
"sortable": ["post_title", "post_date", "post_author"]
},
"page": {
"indexable": ["post_title", "post_content", "post_excerpt", "post_date", "post_author", "permalink"],
"filterable": ["post_title", "post_date", "post_author"],
"sortable": ["post_title", "post_date", "post_author"]
}
}
}

Returns metadata describing supported commands.

Example response:

{
"success": true,
"error": null,
"requestId": null,
"data": {
"healthCheck": {
"description": "Return plugin / environment health information",
"params": {}
},
"getSchema": {
"description": "Retrieve index schema details for supported post types",
"params": {}
},
"searchPosts": {
"description": "Full-text search posts/pages with pagination cursor",
"params": {
"query": "string (required) search phrase",
"limit": "int (optional, 1-100, default 10)",
"cursor": "string (optional) pagination cursor",
"fields": "string[] optional whitelist of fields (id,title,excerpt,url,content,taxonomies,post_type)",
"postTypes": "string[] optional post types to restrict"
}
},
"getPost": {
"description": "Retrieve a single published post by ID with optional field selection",
"params": {
"id": "int (required) WordPress post ID",
"fields": "string[] optional fields to include"
}
},
"listCommands": {
"description": "List available MCP commands and their parameter hints",
"params": {}
}
}
}

Protected; returns environment diagnostics (version, phpVersion, wpVersion, hasSqlite, timestamp).

Cursor creation:

  • Encode JSON { o: <nextOffset>, q: md5(query) }
  • Append HMAC SHA256 over payload using WP auth salt
  • Base64URL encode

Validation rejects tampered or cross-query cursors.

Rate limiting currently applies to searchPosts and is configurable via the MCP settings UI or filter overrides.

On the MCP tab you can set:

SettingAnonymousAuthenticatedNotes
Requests per windowanon_limitauth_limitMax command invocations in a rolling window
Window (seconds)anon_windowauth_windowShared logical bucket per IP/token fragment
Max search hits per requestmax_search_anonmax_search_authCaps the limit param requested by clients

Saved values are stored in the option wp_loupe_mcp_rate_limits and immediately applied to future requests.

  1. Saved option values (if present)
  2. Filter overrides (allow deployment-specific adjustments without DB changes)
  3. Hard-coded defaults (fallback only)

Each searchPosts response includes:

  • X-RateLimit-Limit – window quota in effect (post-filter, after option)
  • X-RateLimit-Remaining – remaining allowance in the current window
  • Retry-After – only present on 429 responses
ContextWindowRequestsMax Hits per Search
Anonymous60s1510
Authenticated60s60100

You can still override any piece via filters (they run after option retrieval):

FilterPurposeOption-Based Default Passed In
wp_loupe_mcp_rate_window_secondsEffective window length (seconds)anon_window or auth_window selected based on auth
wp_loupe_mcp_search_rate_limit_anonAnonymous requests per windowSaved anon_limit
wp_loupe_mcp_search_rate_limit_authAuth requests per windowSaved auth_limit
wp_loupe_mcp_search_max_limit_anonMax hits per search (anon)Saved max_search_anon
wp_loupe_mcp_search_max_limit_authMax hits per search (auth)Saved max_search_auth

Example override (increase auth window only):

add_filter( 'wp_loupe_mcp_rate_window_seconds', function( $window ) {
if ( is_user_logged_in() ) { // or custom condition
return 120; // 2 minute window
}
return $window;
});

The rate limiter keys buckets by client IP plus a token-fragment (derived from scopes) for authenticated requests. Anonymous traffic is grouped under anon. Buckets reset automatically after the configured window.

If a client exceeds the quota a standardized error is returned:

{
"success": false,
"error": { "code": "rate_limited", "message": "Rate limit exceeded" },
"data": null
}

With HTTP status 429 and Retry-After header indicating when to try again.

Clients can request specific fields to minimize payload size. Heavy fields like content and taxonomies are only included if explicitly requested.

Robust extraction checks HTTP_AUTHORIZATION, REDIRECT_HTTP_AUTHORIZATION, and getallheaders(). Custom environments may override via: apply_filters( 'wp_loupe_mcp_raw_authorization_header', $header ).

Error envelope codes map to transport-level status codes. Auth errors also emit WWW-Authenticate.

CodeTypical HTTPNotes
missing_command400No command field
unknown_command400Unsupported command name
invalid_header401Malformed Authorization header
missing_token401Protected command w/out auth
invalid_token401Expired or unknown token
insufficient_scope403Token lacks required scope
rate_limited429Rate limit exceeded

A helper script is included: test-mcp-search.sh.

Example manual token issuance:

Terminal window
curl -s -X POST "$BASE/oauth/token" \
-H 'Content-Type: application/json' \
-d '{"grant_type":"client_credentials","client_id":"wp-loupe-local","scope":"search.read health.read"}'

Search example:

Terminal window
curl -s -X POST "$BASE/commands" \
-H 'Content-Type: application/json' \
-d '{"command":"searchPosts","params":{"query":"hello","limit":5}}'

You can issue tokens directly via WP-CLI (helpful for server-to-server integration without crafting HTTP calls manually).

If WordPress is running as a multisite network and the WP Loupe plugin is activated on a specific sub‑site, you MUST scope WP-CLI commands to that site using --url. Tokens are stored per-site (transient cache); issuing a token on the network root will not make it valid for a sub-site endpoint.

Example (sub-site at http://plugins.local/loupe/):

Terminal window
wp --url=http://plugins.local/loupe/ wp-loupe mcp issue-token --scopes="search.read health.read" --format=json

Then use the returned access_token against the sub-site REST endpoint:

Terminal window
TOKEN="<paste-token>"
curl -s -H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{"command":"healthCheck"}' \
http://plugins.local/loupe/wp-json/wp-loupe-mcp/v1/commands

If you omit --url you may see invalid_token because the token transient was written for a different blog/site ID.

List help:

Terminal window
wp help wp-loupe mcp issue-token

Issue a token with default (all) scopes:

Terminal window
wp wp-loupe mcp issue-token

Issue a token limited to search + health scopes (JSON output):

Terminal window
wp wp-loupe mcp issue-token --scopes="search.read health.read"

Table output format:

Terminal window
wp wp-loupe mcp issue-token --format=table

Specify explicit client credentials (if constants configured):

Terminal window
wp wp-loupe mcp issue-token --client_id=wp-loupe-local --client_secret="$CLIENT_SECRET"

The command returns (JSON format):

{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "search.read health.read"
}

Below are quick-start instructions for integrating the WP Loupe MCP server with popular agent / IDE environments. Always create a scoped token (principle of least privilege) unless you intentionally allow anonymous low‑limit access.

1. Claude Desktop (Anthropic) – Local mcp-server Config

Section titled “1. Claude Desktop (Anthropic) – Local mcp-server Config”

Claude Desktop supports local JSON config listing MCP servers. Add or extend your claude_desktop_config.json (path varies by OS). Example entry:

{
"mcpServers": {
"wp-loupe": {
"type": "http",
"url": "https://example.com/.well-known/mcp.json",
"headers": {
"Authorization": "Bearer REPLACE_WITH_TOKEN"
}
}
}
}

Steps:

  1. Enable MCP in WP admin and create a token with scopes you need (e.g., search.read health.read).
  2. Paste token into the header above.
  3. Restart Claude Desktop; it should list wp-loupe as a connected tool. Use natural prompts like: “Search WordPress for posts about performance using wp-loupe.”

Anonymous mode: Remove headers object—Claude will still reach the server but hit anonymous limits (can’t run healthCheck).

If using an MCP-compatible VS Code extension (e.g., experimental MCP bridge), configure a server entry similar to:

// .vscode/mcp.json (example – actual file name/extension may differ)
{
"servers": [
{
"name": "wp-loupe",
"manifestUrl": "https://example.com/.well-known/mcp.json",
"auth": {
"type": "bearer",
"token": "REPLACE_WITH_TOKEN"
}
}
]
}

After reload, trigger the extension’s command palette action (e.g., “MCP: Refresh Servers”) and invoke commands via chat or command listing UI. If the extension supports streaming, search output appears as JSON payloads; you can refine queries by adjusting limit or fields.

3. ChatGPT (OpenAI) – Custom Tool Manifest (Workaround)

Section titled “3. ChatGPT (OpenAI) – Custom Tool Manifest (Workaround)”

ChatGPT doesn’t (yet) natively load arbitrary MCP manifests, but you can approximate integration by:

  1. Supplying the manifest JSON inline (copy from /.well-known/mcp.json).
  2. Instructing ChatGPT to treat POST https://example.com/wp-json/wp-loupe-mcp/v1/commands as the primary endpoint with envelope {command, params}.
  3. Providing a fixed Bearer token (remove after session if temporary).

Prompt snippet:

You are an assistant with access to a WordPress MCP search API. To search, POST JSON to:
https://example.com/wp-json/wp-loupe-mcp/v1/commands
Body example: {"command":"searchPosts","params":{"query":"performance", "limit":5}}
Auth header: Authorization: Bearer TOKEN
Return only the 'hits' array when summarizing unless I ask for raw JSON.

Limitations: No automatic schema refresh; you must paste updated manifest if scopes or commands change.

Minimal scriptable invocation (with token):

Terminal window
TOKEN="YOUR_TOKEN"; BASE="https://example.com/wp-json/wp-loupe-mcp/v1"
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"command":"searchPosts","params":{"query":"accessibility","limit":5}}' \
"$BASE/commands" | jq '.data.hits'

If a workstation is lost or you suspect leakage:

  1. Open MCP tab → “Revoke All Tokens” (immediate invalidation)
  2. Issue replacement tokens and update client configs.
GoalParam Strategy
Minimal metadatafields: ["id","title","url"]
Include taxonomy slugsAdd taxonomies to fields
Full content fetchUse small search limit, then getPost per ID with content
SymptomLikely CauseFix
404 on manifest URLMCP not enabledEnable in admin MCP tab
401 invalid_tokenWrong / expired tokenIssue new token, update config
403 insufficient_scopeMissing scope (e.g., search.read)Reissue token with required scopes
429 rate_limitedQuota exceededWait for window or raise limits (if appropriate)
Cursor yields no progressQuery text changedDiscard old cursor and restart search
  • Use separate tokens per client (enables per-client last_used audit).
  • Prefer short TTL tokens; only use indefinite (0) for tightly controlled back-end tasks.
  • Scope-minimize: if client only searches, drop health.read.
  • Rotate tokens periodically and after personnel changes.
  • Monitor server logs (add custom logging via filters/actions if needed) to detect abuse patterns.

9. Example Manifest Consumption Cache Policy

Section titled “9. Example Manifest Consumption Cache Policy”

MCP clients should honor ETag headers on /.well-known/mcp.json to reduce bandwidth and auto-refresh capabilities when changed.


  • Transient-backed tokens: ephemeral; not persistent beyond TTL.
  • Token hash storage avoids leaking raw tokens via options table.
  • No refresh tokens implemented (simple rotation model acceptable for server-to-server integrations).
  • Consider adding per-client secret & revocation list for production.

Setting TTL to 0 issues a logically non-expiring token (expires_at = 0). It is cached for a long duration (currently 1 year in transient storage) and treated as never expiring in validation. Prefer time-bound tokens whenever possible and revoke indefinite tokens if no longer required.

Each authenticated command updates last_used for that token. This enables future automation (e.g., pruning stale tokens or alerting on dormant credentials).

The “Revoke All Tokens” action wipes every active token and its transient. Use during incident response or credential rotation.

FeatureSupportedNotes
Scope selectionYesAll pre-selected; uncheck to restrict
TTL hoursYes1–168 or 0 (never)
Indefinite tokensYes (0)Use sparingly; rotate manually
Last-used trackingYesUpdated on success
Bulk revokeYesOne-click cleanup
  • Refresh tokens & revocation list
  • Persistent token storage (custom table) for durability & querying
  • Per-command rate limiting & usage metrics
  • Write / indexing commands with dedicated scopes
  • Schema introspection expansion
  • WP-CLI: pass TTL + list/revoke tokens natively
  • Hardening: IP allow/deny lists & anomaly detection
  • Automated stale token pruning (using last_used)
  • 0.5.2-draft: Added configurable rate limit UI (window, per-window quotas, max search hits) with option + filter precedence; server now consumes saved configuration.
  • 0.5.1-draft: Added scope selection UI, adjustable TTL (0 = indefinite), revoke-all, last-used tracking, indefinite token handling.
  • 0.5.0-draft: Initial hybrid MCP server (discovery, commands, OAuth client_credentials, pagination, rate limiting, scopes, ETag).

This document will evolve as MCP capabilities expand.

and accordingly.
  1. Keep ordering identical to document flow.
  2. Avoid deep nesting unless readability benefits. —>