WP Loupe MCP Integration
- Overview
- Discovery
- Authentication Model
- Commands
- Pagination
- Rate Limiting
- Field Filtering and Heavy Fields
- Authorization Header Handling
- Error Semantics
- Testing
- WP-CLI Usage
- Connecting from MCP-Capable Clients
- Security Considerations
- Indefinite Tokens (TTL = 0)
- Last-Used Tracking
- Revoke-All Operation
- Adjustable TTL & Scopes Summary
- Roadmap (Potential Enhancements)
- Changelog (MCP Portion)
| Property | Value |
|---|---|
| MCP API Version | v1 |
| Implementation Status | Draft/Beta |
| WordPress Minimum | 6.3+ |
| PHP Minimum | 8.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.
Overview
Section titled “Overview”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.
Discovery
Section titled “Discovery”WP Loupe exposes two discovery endpoints:
| Purpose | Path | Notes |
|---|---|---|
| MCP Manifest | /.well-known/mcp.json | High-level manifest (commands, scopes) with ETag caching |
| Protected Resource Metadata | /.well-known/oauth-protected-resource | OAuth2 resource metadata (subset) |
Fallback REST routes (if rewrite rules are missing):
GET /wp-json/wp-loupe-mcp/v1/discovery/manifestGET /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 ).
Authentication Model
Section titled “Authentication Model”Before any MCP endpoints are reachable you must explicitly enable the MCP server.
Enabling the MCP Server
Section titled “Enabling the MCP Server”- Navigate to: Settings → WP Loupe → MCP tab.
- 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/*
- 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.
Token Management UI
Section titled “Token Management UI”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) or0for a non-expiring (indefinite) token. Use0sparingly. - List columns: Label, Scopes, Issued, Expires (
Neverif 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_credentialsclient_id=wp-loupe-local(default)client_secret(optional – if not defined by constant, secret-less dev mode allowed)scopespace-separated (optional)
Scopes
Section titled “Scopes”| Scope | Description |
|---|---|
search.read | Perform search queries (higher auth limits) |
post.read | Retrieve post metadata/content (future restrictions may apply) |
schema.read | Access schema details |
health.read | Health check command |
commands.read | List commands metadata |
Hybrid Rules
Section titled “Hybrid Rules”| Command | Anonymous | Authenticated Requirement |
|---|---|---|
searchPosts | Yes (lower limits) | search.read (if token presented) |
getPost | Yes (currently unrestricted) | (Will enforce post.read if tightened) |
getSchema | Yes | (Will enforce schema.read if tightened) |
listCommands | Yes | commands.read (if enforced later) |
healthCheck | No | health.read |
Token Response
Section titled “Token Response”{ "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "scope": "search.read health.read"}Error Responses
Section titled “Error Responses”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.
Commands
Section titled “Commands”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}searchPosts
Section titled “searchPosts”Search published posts/pages.
Params:
| Name | Type | Description |
|---|---|---|
query | string (required) | Search phrase |
limit | int (optional) | Max hits (auth: up to 100 default; anon: up to 10) |
cursor | string (optional) | Pagination cursor |
fields | string[] (optional) | Whitelist fields (subset of: id, title, excerpt, url, content, taxonomies, post_type) |
postTypes | string[] (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.
getPost
Section titled “getPost”Retrieve a single published post by ID.
Params: id (required), fields (optional array).
getSchema
Section titled “getSchema”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"] } }}listCommands
Section titled “listCommands”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": {} } }}healthCheck
Section titled “healthCheck”Protected; returns environment diagnostics (version, phpVersion, wpVersion, hasSqlite, timestamp).
Pagination
Section titled “Pagination”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
Section titled “Rate Limiting”Rate limiting currently applies to searchPosts and is configurable via the MCP settings UI or filter overrides.
Configuration UI (Preferred)
Section titled “Configuration UI (Preferred)”On the MCP tab you can set:
| Setting | Anonymous | Authenticated | Notes |
|---|---|---|---|
| Requests per window | anon_limit | auth_limit | Max command invocations in a rolling window |
| Window (seconds) | anon_window | auth_window | Shared logical bucket per IP/token fragment |
| Max search hits per request | max_search_anon | max_search_auth | Caps the limit param requested by clients |
Saved values are stored in the option wp_loupe_mcp_rate_limits and immediately applied to future requests.
Precedence Order
Section titled “Precedence Order”- Saved option values (if present)
- Filter overrides (allow deployment-specific adjustments without DB changes)
- Hard-coded defaults (fallback only)
HTTP Headers
Section titled “HTTP Headers”Each searchPosts response includes:
X-RateLimit-Limit– window quota in effect (post-filter, after option)X-RateLimit-Remaining– remaining allowance in the current windowRetry-After– only present on 429 responses
Defaults (If Not Modified)
Section titled “Defaults (If Not Modified)”| Context | Window | Requests | Max Hits per Search |
|---|---|---|---|
| Anonymous | 60s | 15 | 10 |
| Authenticated | 60s | 60 | 100 |
Filters (Optional Overrides)
Section titled “Filters (Optional Overrides)”You can still override any piece via filters (they run after option retrieval):
| Filter | Purpose | Option-Based Default Passed In |
|---|---|---|
wp_loupe_mcp_rate_window_seconds | Effective window length (seconds) | anon_window or auth_window selected based on auth |
wp_loupe_mcp_search_rate_limit_anon | Anonymous requests per window | Saved anon_limit |
wp_loupe_mcp_search_rate_limit_auth | Auth requests per window | Saved auth_limit |
wp_loupe_mcp_search_max_limit_anon | Max hits per search (anon) | Saved max_search_anon |
wp_loupe_mcp_search_max_limit_auth | Max 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;});Implementation Details
Section titled “Implementation Details”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.
Field Filtering and Heavy Fields
Section titled “Field Filtering and Heavy Fields”Clients can request specific fields to minimize payload size. Heavy fields like content and taxonomies are only included if explicitly requested.
Authorization Header Handling
Section titled “Authorization Header Handling”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 Semantics
Section titled “Error Semantics”Error envelope codes map to transport-level status codes. Auth errors also emit WWW-Authenticate.
| Code | Typical HTTP | Notes |
|---|---|---|
missing_command | 400 | No command field |
unknown_command | 400 | Unsupported command name |
invalid_header | 401 | Malformed Authorization header |
missing_token | 401 | Protected command w/out auth |
invalid_token | 401 | Expired or unknown token |
insufficient_scope | 403 | Token lacks required scope |
rate_limited | 429 | Rate limit exceeded |
Testing
Section titled “Testing”A helper script is included: test-mcp-search.sh.
Example manual token issuance:
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:
curl -s -X POST "$BASE/commands" \ -H 'Content-Type: application/json' \ -d '{"command":"searchPosts","params":{"query":"hello","limit":5}}'WP-CLI Usage
Section titled “WP-CLI Usage”You can issue tokens directly via WP-CLI (helpful for server-to-server integration without crafting HTTP calls manually).
Multisite Note
Section titled “Multisite Note”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/):
wp --url=http://plugins.local/loupe/ wp-loupe mcp issue-token --scopes="search.read health.read" --format=jsonThen use the returned access_token against the sub-site REST endpoint:
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/commandsIf you omit --url you may see invalid_token because the token transient was written for a different blog/site ID.
List help:
wp help wp-loupe mcp issue-tokenIssue a token with default (all) scopes:
wp wp-loupe mcp issue-tokenIssue a token limited to search + health scopes (JSON output):
wp wp-loupe mcp issue-token --scopes="search.read health.read"Table output format:
wp wp-loupe mcp issue-token --format=tableSpecify explicit client credentials (if constants configured):
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"}Connecting from MCP-Capable Clients
Section titled “Connecting from MCP-Capable Clients”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:
- Enable MCP in WP admin and create a token with scopes you need (e.g.,
search.read health.read). - Paste token into the header above.
- Restart Claude Desktop; it should list
wp-loupeas 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).
2. VS Code – Copilot / MCP Extensions
Section titled “2. VS Code – Copilot / MCP Extensions”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:
- Supplying the manifest JSON inline (copy from
/.well-known/mcp.json). - Instructing ChatGPT to treat
POST https://example.com/wp-json/wp-loupe-mcp/v1/commandsas the primary endpoint with envelope{command, params}. - 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/commandsBody example: {"command":"searchPosts","params":{"query":"performance", "limit":5}}Auth header: Authorization: Bearer TOKENReturn 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.
4. cURL / Scripted Agent Reference
Section titled “4. cURL / Scripted Agent Reference”Minimal scriptable invocation (with token):
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'5. Rotating / Revoking Tokens for Clients
Section titled “5. Rotating / Revoking Tokens for Clients”If a workstation is lost or you suspect leakage:
- Open MCP tab → “Revoke All Tokens” (immediate invalidation)
- Issue replacement tokens and update client configs.
6. Field & Payload Optimization Tips
Section titled “6. Field & Payload Optimization Tips”| Goal | Param Strategy |
|---|---|
| Minimal metadata | fields: ["id","title","url"] |
| Include taxonomy slugs | Add taxonomies to fields |
| Full content fetch | Use small search limit, then getPost per ID with content |
7. Troubleshooting
Section titled “7. Troubleshooting”| Symptom | Likely Cause | Fix |
|---|---|---|
| 404 on manifest URL | MCP not enabled | Enable in admin MCP tab |
| 401 invalid_token | Wrong / expired token | Issue new token, update config |
| 403 insufficient_scope | Missing scope (e.g., search.read) | Reissue token with required scopes |
| 429 rate_limited | Quota exceeded | Wait for window or raise limits (if appropriate) |
| Cursor yields no progress | Query text changed | Discard old cursor and restart search |
8. Security Best Practices
Section titled “8. Security Best Practices”- 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.
Security Considerations
Section titled “Security Considerations”- 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.
Indefinite Tokens (TTL = 0)
Section titled “Indefinite Tokens (TTL = 0)”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.
Last-Used Tracking
Section titled “Last-Used Tracking”Each authenticated command updates last_used for that token. This enables future automation (e.g., pruning stale tokens or alerting on dormant credentials).
Revoke-All Operation
Section titled “Revoke-All Operation”The “Revoke All Tokens” action wipes every active token and its transient. Use during incident response or credential rotation.
Adjustable TTL & Scopes Summary
Section titled “Adjustable TTL & Scopes Summary”| Feature | Supported | Notes |
|---|---|---|
| Scope selection | Yes | All pre-selected; uncheck to restrict |
| TTL hours | Yes | 1–168 or 0 (never) |
| Indefinite tokens | Yes (0) | Use sparingly; rotate manually |
| Last-used tracking | Yes | Updated on success |
| Bulk revoke | Yes | One-click cleanup |
Roadmap (Potential Enhancements)
Section titled “Roadmap (Potential Enhancements)”- 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)
Changelog (MCP Portion)
Section titled “Changelog (MCP Portion)”- 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.- Keep ordering identical to document flow.
- Avoid deep nesting unless readability benefits. —>
📦 Source: soderlind/wp-loupe · Edit on GitHub