---
title: MCP server (Streamable HTTP transport)
url: https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/
image: https://developers.wpengine.com/og/docs/ai/api/reference/operations/mcpstreamablehttp.png
---

# MCP server (Streamable HTTP transport)

POST

/v1/mcp

Select code sample

```php
<?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", "title": "example", "description": "example", "icons": [ { "src": "example", "mimeType": "example", "sizes": [ "example" ], "theme": "light" } ], "version": "example", "websiteUrl": "https://example.com" } } }',
  'headers' => [
    'Authorization' => 'Bearer <token>',
    'Content-Type' => 'application/json',
  ],
]);


echo $response->getBody();
```

- Current environment

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). A token issued for a support impersonation session is refused with `403`. An API-key caller receives a deprecation notice in the connect instructions and in each tool result’s `_meta["com.wpengine/deprecation"]`. The result’s content is unchanged. Where API keys are disabled, a key still connects, initializes and lists tools, and every tool call returns the tool error `forbidden`. 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, and `urn:wpengine:agent-sandbox:ro` and `urn:wpengine:agent-sandbox:rw` for the `sandbox_*` tools, which act on a live environment. The account and environment tools also need `urn:wpengine:accounts:rw` and `urn:wpengine:installs:rw`, because the token is passed on to the WP Engine API. A request whose token lacks a required scope returns `403` with `WWW-Authenticate: Bearer error="insufficient_scope", scope="..."` naming the scopes to request: the token’s current scopes plus those required. An OAuth access token names a person, not an account, so for such a caller the site, ability and search tools (`list_account_sites`, `list_site_abilities`, `run_site_ability` and `search_abilities`) take a required `account_id`. `list_accounts`, offered only to these callers, returns the accounts the person holds that have connected sites. An `account_id` the person does not hold returns the tool error `account_not_found`. An account the person holds that is not enabled for WP Engine AI APIs returns the tool error `entitlement_required`, or `entitlement_unavailable` when that cannot be checked. A person none of whose accounts is enabled is refused with `403` `entitlement_required`, or `account_required` when they hold no account. API-key and Okta callers do not pass `account_id`. 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. An account-wide `search_abilities` with no connected sites returns `found: false` and `reason: no_connected_sites` for OAuth and Okta callers, with `help` and `next_steps` directing the client to list sites with pagination and ask the user which site to connect. It does not select a connection URL implicitly. Personal API keys with no active connection receive the same selection guidance in a `not_personally_connected` tool error. `list_account_sites` includes `requires_https` on each site, computed from the recorded site URL on that page, so a client can skip sites that cannot start a personal connection without another lookup. Site-specific calls return guidance for the selected site. 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.

## Authorizations

- **[bearerAuth](https://developers.wpengine.com/docs/ai/api/reference/#bearerauth)**

## Parameters

### Header Parameters

**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. Optional for Okta JWT callers (Console Coworker): when omitted, the account’s project is used — created for an account that has none — and an account with more than one project uses the one named “Default project”, or its oldest when none has that name. 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`.

## Request Bodyrequired

Media typeapplication/json

One of:

- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-0-0)
- [Array](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-0-1)

One of: discriminator: method

- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-1-0)
- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-1-1)
- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-1-2)
- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-1-3)
- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-1-4)

JSON-RPC request initiating the MCP session handshake.

object

**jsonrpc**

required

string

Allowed values: 2.0

**id**

required

One of:

- [string](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-2-0)
- [integer](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-2-1)

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

Identity of an MCP client or server implementation. This server reports name wpengine, its build version, a display title, a one-line description, the WP Engine logomark as icons, and the MCP docs URL as websiteUrl.

object

**name**

required

Programmatic identifier of the implementation.

string

**title**

Optional human-readable name for display in a client UI.

string

**description**

Optional human-readable description of the implementation.

string

**icons**

Optional icons for display in a client UI. This server sends the same mark as an SVG and as a PNG, both as base64 data URIs.

Array\<object>

object

**src**

required

An https URL or a base64 data URI for the image.

string

**mimeType**

string

**sizes**

Sizes the image suits, such as 128x128, or any for a scalable format.

Array\<string>

**theme**

The background the icon is designed for, when it is designed for one.

string

Allowed values: light dark

**version**

required

Implementation version. This server reports an opaque identifier of the build serving the request, which changes with each deploy. Do not parse it or compare it for ordering.

string

**websiteUrl**

Optional URL of the implementation’s website or documentation.

string format: uri

## Responses

### 200

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:

- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-11-0)
- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-11-1)
- [Array](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-11-2)

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](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-12-0)
- [integer](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-12-1)

string

**result**

required

One of:

- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-13-0)
- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-13-1)
- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-13-2)
- [object](https://developers.wpengine.com/docs/ai/api/reference/operations/mcpstreamablehttp/#tab-panel-13-3)

Result of an `initialize` request.

object

**protocolVersion**

required

string

**capabilities**

required

Server-declared capabilities. This server advertises tools, and resources when its tool set includes resource documents — never prompts, sampling, roots, or list change notifications.

object

**_key_**

additional properties

any

**serverInfo**

required

Identity of an MCP client or server implementation. This server reports name wpengine, its build version, a display title, a one-line description, the WP Engine logomark as icons, and the MCP docs URL as websiteUrl.

object

**name**

required

Programmatic identifier of the implementation.

string

**title**

Optional human-readable name for display in a client UI.

string

**description**

Optional human-readable description of the implementation.

string

**icons**

Optional icons for display in a client UI. This server sends the same mark as an SVG and as a PNG, both as base64 data URIs.

Array\<object>

object

**src**

required

An https URL or a base64 data URI for the image.

string

**mimeType**

string

**sizes**

Sizes the image suits, such as 128x128, or any for a scalable format.

Array\<string>

**theme**

The background the icon is designed for, when it is designed for one.

string

Allowed values: light dark

**version**

required

Implementation version. This server reports an opaque identifier of the build serving the request, which changes with each deploy. Do not parse it or compare it for ordering.

string

**websiteUrl**

Optional URL of the implementation’s website or documentation.

string format: uri

**instructions**

Optional human-readable usage instructions from the server.

string

#### Example

```json
{
  "jsonrpc": "2.0",
  "result": {
    "serverInfo": {
      "icons": [
        {
          "theme": "light"
        }
      ]
    }
  }
}
```

### 202

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

### 400

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.

Select media type

string

### 401

Missing or invalid bearer token

Media typeapplication/json

object

**error**

required

object

**message**

required

string

**type**

required

string

**error_code**

Stable, machine-readable cause, set by routes that document their codes (the AI readiness and account AI routes, and an entitlement_required refusal, where it is one of not_eligible, terms_required, not_enabled or suspended); absent elsewhere and on errors from shared layers such as authentication. Clients must allow new codes.

string

**portal_url**

Where the customer can resolve the error, set on an entitlement_required refusal that has an error_code when one is configured.

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

```json
{
  "error": {
    "type": "invalid_request_error",
    "request_id": "80f1e2c3a4b5c6d7e8f9a0b1c2d3e4f5"
  }
}
```

#### Headers

**WWW-Authenticate**

string

##### Example

```
Bearer realm="ai-services"
```

### 403

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 may not use WP Engine AI, with error_code giving why when known (not_eligible, terms_required, not_enabled or suspended) and portal_url where to fix it; “account_required” means no billing account could be resolved; and “billing_account_pending” means billing isn’t set up for the account yet, usually because its setup after AI was turned on hasn’t finished. Retry shortly. 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

**error_code**

Stable, machine-readable cause, set by routes that document their codes (the AI readiness and account AI routes, and an entitlement_required refusal, where it is one of not_eligible, terms_required, not_enabled or suspended); absent elsewhere and on errors from shared layers such as authentication. Clients must allow new codes.

string

**portal_url**

Where the customer can resolve the error, set on an entitlement_required refusal that has an error_code when one is configured.

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

```json
{
  "error": {
    "type": "invalid_request_error",
    "request_id": "80f1e2c3a4b5c6d7e8f9a0b1c2d3e4f5"
  }
}
```

### 415

Request Content-Type is not application/json.

Media typetext/plain

string

### 503

A required dependency is temporarily unavailable. Retry after a short delay with exponential back-off. An entitlement lookup failure uses error type “entitlement_unavailable”. A model without complete rate coverage uses error type “model_unpriced”. A credit balance that can’t be read uses error type “balance_unavailable”.

Media typeapplication/json

object

**error**

required

object

**message**

required

string

**type**

required

string

**error_code**

Stable, machine-readable cause, set by routes that document their codes (the AI readiness and account AI routes, and an entitlement_required refusal, where it is one of not_eligible, terms_required, not_enabled or suspended); absent elsewhere and on errors from shared layers such as authentication. Clients must allow new codes.

string

**portal_url**

Where the customer can resolve the error, set on an entitlement_required refusal that has an error_code when one is configured.

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

```json
{
  "error": {
    "type": "invalid_request_error",
    "request_id": "80f1e2c3a4b5c6d7e8f9a0b1c2d3e4f5"
  }
}
```

## Was this helpful?

Thanks for the feedback!

```json
{"@context":"https://schema.org","@graph":[{"@type":"WebSite","@id":"https://developers.wpengine.com/#website","name":"WP Engine Product Documentation","url":"https://developers.wpengine.com/","description":"Resources for developing, deploying, and scaling on WP Engine.","inLanguage":"en-US","keywords":["API documentation","product documentation","headless WordPress","managed WordPress hosting"],"publisher":{"@id":"https://wpengine.com/#organization"}}]}
```
