Skip to content
WP EngineDocumentation

MCP server (Streamable HTTP transport)

POST
/v1/mcp
<?php
$client = new \GuzzleHttp\Client();
$response = $client->request('POST', 'https://api.ai.wpengine.com/v1/mcp', [
'body' => '{ "jsonrpc": "2.0", "id": "example", "method": "initialize", "params": { "protocolVersion": "example", "capabilities": {}, "clientInfo": { "name": "example", "version": "example" } } }',
'headers' => [
'Authorization' => 'Bearer <token>',
'Content-Type' => 'application/json',
],
]);
echo $response->getBody();

Exposes the gateway as a remote MCP server over the Streamable HTTP transport (JSON-RPC 2.0 over HTTP, optionally upgrading to Server-Sent Events for streamed responses). Accepts either a WP Engine API key (prefixed “wpe_”) carrying the mcp scope, scoped to the key’s project, or an Okta JWT with WPEngine-Account and WPEngine-Project headers (Standalone Coworker). External MCP clients (e.g. Claude Desktop) can instead use interactive OAuth sign-in, discovered through /.well-known/oauth-protected-resource/v1/mcp. An OAuth access token must grant a scope for each tool it calls: urn:wpengine:ai:wordpress:rw for the site, knowledge base and ability tools, and urn:wpengine:hosting:tools:rw for the WP Engine account and environment tools. A request whose token lacks the scope returns 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="..." naming the scopes to request: the token’s current scopes plus the one required. Scopes do not apply to API keys or Okta JWTs. The gateway does not preserve MCP session state between requests; clients should not rely on server-side session continuity across calls. The list_site_abilities, search_abilities, and run_site_ability tools additionally require a personal API key when using API-key auth; a project-scoped key resolves no acting user and these tools return an unauthorized error. Okta JWT callers use the verified subject instead. For run_site_ability, a timed-out read returns the tool error code timeout, including when WordPress reports an upstream timeout with a REST 504 response. Any WordPress error code and message are retained in the tool error message. Retrying the identical read is safe. An uncertain write returns ambiguous_outcome and must not be retried automatically. Site abilities publish their own input and output schemas through discovery. When available, wp-engine-ai/search-content performs literal search of editable stored content and REST-exposed custom fields, rather than relevance-ranked knowledge-base search. Matching ignores letter case by default; set case_sensitive: true for exact case. Each call visits up to 200 posts subject to work and result limits. Pass its returned next_cursor as cursor with the same query, case mode, scope and limit until complete is true; an empty page does not imply completion. Each ability response returns up to 100 occurrences by default, with a maximum of 200 occurrences and 100,000 encoded JSON bytes, excluding the MCP transport envelope. Every literal occurrence, including overlaps, produces a result with a zero-based Unicode character match_offset within its stored string leaf and a preview. Field identities can repeat at different offsets. Incomplete pages have totals: null. Cursors use compact unpadded base64url and must be copied unchanged, including letter case, with the original limit and search options. The final page has cumulative totals across all pages: matched_posts, matched_fields, and occurrences, even when its matches is empty. Separate metadata rows count as separate matching fields. Summaries should use these totals verbatim rather than recounting generated tables. Search results identify update targets, not replacement values or a snapshot of the site. When available, wp-engine-ai/replace-content applies an approved literal, case-sensitive, non-overlapping replacement to stored post content and/or manual excerpts. Send ids, find and replace; optional fields defaults to ["content"]. Explicitly select ["excerpt"] for excerpts only or ["content","excerpt"] for both. Only changed selected fields are written, together per post. A changed field that the post endpoint does not accept fails that post with unknown_fields before any selected field is saved. Core pages expose writable REST excerpts even without editor excerpt support. Custom types/controllers must expose the selected field. Generated excerpts, meta/custom fields and ACF are not replaced. Empty, duplicate or unsupported field selections reject the request before reads or writes. An empty replacement deletes the text. Search for equivalent targets with the same fields and case_sensitive: true. wp-engine-ai/manage-posts applies the same status, categories and/or tags to explicit ids, with no action selector or replacement inputs. categories and tags replace complete sets. Each category/tag list accepts at most 100 input entries before deduplication. Over-limit lists reject the entire request before reads or writes. Larger incremental tag changes can be split across calls; never split full-set category/tag replacements, since each call replaces the complete set. Use tags_add to add existing tag IDs or tags_remove to remove only those IDs, preserving all unrelated tags. Both delta fields may be combined with each other or status/categories, but not with tags; overlapping addition/removal IDs reject the request before writes. Already-present additions and absent removals are unchanged without a write. Tag deltas require a writable REST-visible tags field and term assignment permissions. Non-mutating PATCH probes honor matched REST route permissions and REST policy filters before each native tag operation without running the controller’s full-set save. A failed entry with partial: true means some changes may already have landed; inspect current state before retrying. No rollback is performed. Ordinary PATCH errors are potentially partial too: core fields can save before a later operation fails. Authorized read-back verifies status/term values; content and excerpt differences are not compared because WordPress save filters may alter them. Unavailable verification is explicitly reported with diagnostics_truncated; zero not_applied_count then is not proof that requested values landed. Both tools accept at most 50 positive integer ID entries per call. Deduplicate search-content match IDs after collecting every page and split approved IDs into batches. Both check a 10-second cooperative budget before starting each post. After expiry, remaining IDs are failed with reason not_attempted, included in failed_count, and were not read or written. Completed outcomes are preserved. Submit only those unattempted IDs in a new approved call, never replay the entire batch. Catchable unexpected site errors also stop the batch: completed outcomes remain, the interrupted post fails with execution_failed or wp_fatal_error, and later IDs are not_attempted. A started write makes the interrupted post potentially partial with unverified, truncated diagnostics. Expected WordPress error responses remain per-post failures. A started post normally finishes its mutation and verification before the next deadline check. A blocking save hook/controller or shorter host/gateway timeout can still prevent a result envelope; inspect current state instead of automatically retrying that ambiguous outcome. Invalid IDs and unsupported fields reject the batch before writes; individual permission or save failures do not roll back other posts. Already-matching values are returned as unchanged without a write. Both require an editable supported post type and recheck each ID. Inspect per-post not_applied_count and not_applied for requested status or terms ignored or changed on save. Full result envelopes are capped at 100,000 encoded bytes, retaining all IDs, outcomes and counts. diagnostics_truncated marks shortened diagnostics, values_truncated marks shortened requested/stored value previews within a diagnostic entry, and diagnostics_error reports unavailable diagnostics. Never retry changed IDs to retrieve diagnostics; replacement is not necessarily idempotent. Search results do not authorize writes. Fresh content is read at execution, but there is no snapshot or atomic conflict guard. Site-written strings and object keys in run_site_ability, get_site_knowledge_base, search_knowledge_base, and fetch_knowledge_base_document results have invisible characters removed. Each serialized result is limited to 128 KiB. Larger results are replaced with {"result_truncated":true,"excerpt":"... [tool result truncated]"}; the excerpt is a JSON prefix, not a complete document. Error messages are similarly cleaned and limited to 16 KiB of JSON-encoded text.

WPEngine-Account
string

WP Engine account context. For API keys the account acted on is derived from the key and this header is informational only. For Okta JWT (Standalone Coworker) this header is required and must match the token’s account claim.

WPEngine-Project
string

WP Engine project context. Required for Okta JWT callers (Console Coworker). For API keys the project is derived from the key.

Agent-Session-Id
string

Optional. The agent session a site or knowledge-base tool call is being performed for, when the call answers a tool request from that session. The call is grouped with the rest of the session’s activity in observability data, and the session’s site_ids limits which sites, and which sites’ knowledge bases, those tools may reach: an out-of- scope call fails with the site_not_in_scope tool error. A value that does not name a session in the caller’s project fails those tool calls with agent_session_invalid.

Media typeapplication/json
One of:
One of: discriminator: method

JSON-RPC request initiating the MCP session handshake.

object
jsonrpc
required
string
Allowed values: 2.0
id
required
One of:
string
method
required
string
Allowed values: initialize
params
required
object
protocolVersion
required

MCP protocol version the client supports, e.g. “2025-06-18”.

string
capabilities
required

Client-declared capabilities (roots, sampling, elicitation, etc.) per the MCP spec.

object
key
additional properties
any
clientInfo
required

Name and version of an MCP client or server implementation.

object
name
required
string
version
required
string

One application/json JSON-RPC response for a single request, or an array of responses when the POST body was a JSON-RPC batch.

Media typeapplication/json
One of:

JSON-RPC 2.0 success response. result’s shape depends on the request’s method — see McpInitializeResult, McpPingResult, McpListToolsResult, and McpCallToolResult.

object
jsonrpc
required
string
Allowed values: 2.0
id
required
One of:
string
result
required
One of:

Result of an initialize request.

object
protocolVersion
required
string
capabilities
required

Server-declared capabilities. This server advertises tools only — no resources, prompts, sampling, or roots.

object
key
additional properties
any
serverInfo
required

Name and version of an MCP client or server implementation.

object
name
required
string
version
required
string
instructions

Optional human-readable usage instructions from the server.

string
Example
{
"jsonrpc": "2.0"
}

Accepted with no body for JSON-RPC notifications (such as notifications/initialized) that do not expect a response.

Invalid MCP request — for example, a missing or invalid Accept header, an empty POST body, a JSON-RPC batch exceeding 10 messages, or malformed JSON-RPC payload. Oversized batches return HTTP 400 with a JSON-RPC Invalid Request error (code: -32600, message: batch exceeds maximum of 10 calls) before any message is dispatched. Most other validation failures return plain text; MCP standard header mismatches return a JSON-RPC error object instead.

string

Missing or invalid bearer token

Media typeapplication/json
object
error
required
object
message
required
string
type
required
string
code
required
integer
request_id
required

Correlation ID for this request, matching the X-Request-ID response header — a 32-character trace ID behind the load balancer, otherwise a UUID. Quote it in support reports.

string
Example
{
"error": {
"type": "invalid_request_error",
"request_id": "80f1e2c3a4b5c6d7e8f9a0b1c2d3e4f5"
}
}
WWW-Authenticate
string
Example
Bearer realm="ai-services"

The token is valid, but the request is forbidden. Error type “permission_error” means the token’s scope/permission is insufficient or the authenticated user is not authorized for the target account; “entitlement_required” means the account lacks the required product entitlement; and “account_required” means no billing account could be resolved. On /v1/chat/completions when content moderation is enabled, and on /v1/sessions/{session_id}/events, error type “content_policy_violation” instead means the request content was blocked by content moderation policy; that response never identifies which filter or category matched.

Media typeapplication/json
object
error
required
object
message
required
string
type
required
string
code
required
integer
request_id
required

Correlation ID for this request, matching the X-Request-ID response header — a 32-character trace ID behind the load balancer, otherwise a UUID. Quote it in support reports.

string
Example
{
"error": {
"type": "invalid_request_error",
"request_id": "80f1e2c3a4b5c6d7e8f9a0b1c2d3e4f5"
}
}

Request Content-Type is not application/json.

Media typetext/plain
string

A required dependency is temporarily unavailable. Retry after a short delay with exponential back-off. An entitlement lookup failure uses error type “entitlement_unavailable”.

Media typeapplication/json
object
error
required
object
message
required
string
type
required
string
code
required
integer
request_id
required

Correlation ID for this request, matching the X-Request-ID response header — a 32-character trace ID behind the load balancer, otherwise a UUID. Quote it in support reports.

string
Example
{
"error": {
"type": "invalid_request_error",
"request_id": "80f1e2c3a4b5c6d7e8f9a0b1c2d3e4f5"
}
}