---
title: Troubleshooting
description: Fix the most common MCP server failures, organized by symptom.
url: https://developers.wpengine.com/docs/ai/api/mcp/troubleshooting/
image: https://developers.wpengine.com/og/docs/ai/api/mcp/troubleshooting.png
---

# Troubleshooting

Fixes for what you’re seeing, organized by symptom. Searching for a specific error code works too — each one is named in the section that resolves it.

Before working through anything here: **read the `help` and `next_steps` on the error itself.** The server generates them from live state and they name your actual site and the actual missing argument, so they are more specific than anything on this page. What follows covers the failures where that isn’t enough, or where the fix isn’t where you’d expect.

Tool failures arrive as a normal response carrying an `error.code` — not as an HTTP error — so a failing call still looks like a success at the transport layer. Your client should surface the code to you.

## Your client shows no tools at all

The connection is failing before tool discovery.

1. Confirm the endpoint is exactly `https://api.ai.wpengine.com/v1/mcp`. The transport is Streamable HTTP, with requests sent as `POST`.
2. Confirm the client authenticated with OAuth. On Claude Code, run `/mcp`, select the server, and select **Authenticate**. On Claude Desktop, select **Connect** on the connector. See [Authentication](https://developers.wpengine.com/docs/ai/api/authentication/#mcp-clients-authenticate-with-oauth).
3. On Claude Code, run `claude mcp list` to confirm the server was registered at all. A server added without `--scope user` only appears in the directory it was added from.
4. Reproduce outside your client with [MCP Inspector](https://github.com/modelcontextprotocol/inspector). If Inspector lists tools and your client doesn’t, the problem is client configuration, not the server.

An authentication failure at this stage is a real HTTP 401, unlike tool errors.

## Every tool says `forbidden`

Symptom: every tool fails with `forbidden`. The message says the credential is not accepted for the tool.

You configured the client with an **API key**, and this server is set to refuse keys. A tool call must come from a user authenticated with OAuth.

Where a server still accepts API keys, the connect answer’s instructions and each tool result’s `_meta["com.wpengine/deprecation"]` carry the notice “API keys on MCP are deprecated and will stop working. Sign in with MCP OAuth instead.” Treat that notice as your cue to move to OAuth. The result’s content is unchanged.

**Fix:**

1. Remove the key or token and the `Authorization` header from the client configuration.
2. Authenticate with OAuth. See [Connect Claude Code](https://developers.wpengine.com/docs/ai/api/mcp/how-to/configure-claude-code/) or [Connect Claude Desktop](https://developers.wpengine.com/docs/ai/api/mcp/how-to/configure-claude-desktop/).
3. Retry the request.

## The server rejects your token with HTTP 401 or 403

Symptom: the request fails before any tool runs. This is an HTTP error, not the tool error `forbidden`.

- A site service token gets HTTP 401 when it does not verify, and HTTP 403 with `insufficient scope` when it verifies without the accounts scope.
- A token issued for a support impersonation session gets HTTP 403. The tools act as the user authenticated with OAuth, and a support operator is not that user.

**Fix:** authenticate with OAuth. See [Connect Claude Code](https://developers.wpengine.com/docs/ai/api/mcp/how-to/configure-claude-code/) or [Connect Claude Desktop](https://developers.wpengine.com/docs/ai/api/mcp/how-to/configure-claude-desktop/).

## The agent can read but can’t change anything

Symptom: listing sites works, but any attempt to run an ability fails with `unauthorized`. The message is `a signed-in user is required for this tool; sign in with MCP OAuth`.

You use an **account API key** (the API calls it a project key). Ability tools run as a WordPress user. An account key carries no user, so there is no one to run as. A scope change does not fix this.

**Fix:** use one of these options.

1. Sign in with MCP OAuth. This is the preferred fix, because API keys on MCP are deprecated. See [Connect Claude Code](https://developers.wpengine.com/docs/ai/api/mcp/how-to/configure-claude-code/) or [Connect Claude Desktop](https://developers.wpengine.com/docs/ai/api/mcp/how-to/configure-claude-desktop/).
2. Or create a user API key and use it in place of the account key.

Knowledge base tools work with either kind of key. This is why reads can work while ability runs fail.

## A site is missing `wp_connection_status`

A revoked connection and a connection that never existed both omit the field, so absence does not tell you which one applies.

If the field is missing on **every** site, you probably use an **account API key**. `list_account_sites` adds the field only when the request identifies a WordPress user. An account key identifies no user, so the field is missing whatever the real state is.

**Fix:** sign in with MCP OAuth, or use a user API key. Then connect your WordPress account to any site that still has no status. See the next section.

## The agent says your WordPress user isn’t connected

The error code is `not_personally_connected`.

Registering a site to your account is not the same as connecting yourself to it. The second connection is per person and requires explicit consent in WordPress admin. See [The two connections](https://developers.wpengine.com/docs/ai/api/site-and-account-connections/).

If the error names one site, it includes `connect_url` for that site:

1. Log into that site’s WordPress admin **first**, then open `connect_url`. Opening it while logged out drops you at a login screen and loses the flow.
2. Confirm with **Connect WordPress User** on the consent screen.
3. Retry the original request.

If the request did not name a site, the error has no `connect_url`. Do not pick an example site or build a link from a site URL.

1. Call `list_account_sites` with `limit` and `offset`, starting at `offset` 0. Increase `offset` by `limit` until you have covered `pagination.total` from the first page. A page past the end reports `total` 0, so keep the first page’s total.
2. Each site includes `requires_https`, taken from its `site_url` on that same response. Show the customer `display_name` and `site_url`, and ask them to connect only a site where `requires_https` is false. If every site is true, tell them to enable SSL. Do not send them through Connect WordPress Account.
3. Call `list_site_abilities` with the chosen `site_id`. That response carries the Connect link when the site can be connected.
4. Log into that site’s WordPress admin, open the link, and confirm with **Connect WordPress User**.
5. Retry the original request.

The link is the only entry point to this flow — there’s no button to start it manually in WordPress.

An OAuth or Okta `search_abilities` with no connected sites is not this error. It returns `found: false`, `reason: no_connected_sites`, and the same `help` and `next_steps`.

## It worked yesterday and now says the connection is stale

`needs_reconnect`. The connection existed and has lapsed. Follow the same four steps as [The agent says your WordPress user isn’t connected](https://developers.wpengine.com/docs/ai/api/mcp/troubleshooting/#the-agent-says-your-wordpress-user-isnt-connected); the error carries the link. Nothing is wrong with the site registration or your sign-in.

If it instead reports `site_requires_https`, the site isn’t served over HTTPS and a WordPress-user connection can’t be established at all. Enable SSL first. The same code is returned on the first attempt to connect an HTTP site, before any connection exists. `list_account_sites` sets `requires_https: true` on those sites.

## A site is missing from `list_account_sites`

Work down in this order:

1. **Status filter.** The tool returns active registrations only unless you pass `status`. Ask for `revoked` or `suspended` to see the rest.
2. **Wrong account.** `site_not_found` deliberately doesn’t distinguish “no such site” from “belongs to someone else.”

If you name the site directly, the two codes tell you different things. `site_not_found` means nothing matched the identifier you gave — or the site belongs to another account, which is intentionally indistinguishable. `site_not_active` means the site *was* found but its registration has been revoked or suspended, so the identifier was right and the site was withdrawn.

## The agent can’t find an ability the site definitely has

1. **Are you connected to that site?** Ability listing and search only cover sites where your WordPress account connection is active.
2. **Is the catalog out of date?** Search reads a cached catalog and never syncs it. `search_abilities` lists every site whose catalog may lag the site under `degraded_sites`: one that hasn’t synced recently, has never synced, or failed its last sync (`abilities_sync_status` of `error`). Ask for that site’s abilities with `list_site_abilities`, which refreshes an out-of-date catalog, then search again.
3. **Is it withheld?** Some abilities are blocked from remote execution and return `ability_unavailable`. Retrying won’t help; the change has to be made in WordPress admin.

A search with no matches is not an error — it returns `found: false` with a `reason`. Only `no_semantic_match` improves on a rephrase; `no_connected_sites` means [a missing WordPress account connection](https://developers.wpengine.com/docs/ai/api/mcp/troubleshooting/#the-agent-says-your-wordpress-user-isnt-connected) and `empty_catalogs` means nothing has synced yet, so retrying either returns the same answer.

Search also drops weak matches rather than padding the result, so a plausible query can come back with nothing. It finds; it doesn’t browse — use `list_site_abilities` to see everything a site can do.

## Knowledge base search fails

The two “not found” codes mean different things:

- **`knowledge_base_not_found`** from `get_site_knowledge_base` — the *site* has no usable knowledge base. The error carries a `setup_url` and a three-step repair walkthrough; follow it, then call `get_site_knowledge_base` again to read the new `collection_id`.
- **`collection_not_found`** from search or fetch — the `collection_id` *you passed* is wrong or stale. Re-read it from `get_site_knowledge_base`; don’t reuse a cached one.

If `search_knowledge_base` reports that `collection_id` is required, call `get_site_knowledge_base` first — the collection ID isn’t something you can construct from the site ID.

Fetching a document by an unknown ID is not an error: it returns successfully with `not_found: true`.

## You’re being rate limited

The error code is `rate_limited`. Your agent made too many calls in a short window. There are two separate budgets, and the message says which one you hit:

- **`read tool rate limit exceeded`** — 120 calls/minute across `list_account_sites`, `search_abilities`, `list_site_abilities`, and the knowledge base tools.
- **`ability execution rate limit exceeded`** — `run_site_ability` only: 30 calls/minute on one site, or 60 across all your sites.

These limits are provisional and may change.

Wait a minute and ask again. Hitting one budget doesn’t affect the other, so a blocked search doesn’t stop you running an ability.

**120 reads a minute is far more than a person generates by hand**, so if you keep hitting the read limit your agent is probably looping — searching every site repeatedly, or re-reading a knowledge base. Narrow the request: name the one site you mean, or ask a more specific question.

`service_unavailable` is a different thing. That’s a fault on our side, not your usage. Try again shortly.

## A write may or may not have happened

`ambiguous_outcome` means a mutating ability failed in a way that leaves it unknown whether your site applied the change.

> **Don’t just ask your agent to try again.**
>
> Check the site first — did the post get created, did the plugin get activated — and only then decide. Repeating the request can duplicate a write that already succeeded.

This is the only error code with that property. `timeout` and `unavailable` are raised only for reads and are explicitly safe to retry, which is why their `help` text says so.

## Other codes you might see

These are usually self-explanatory from the message that comes with them:

| Code                         | What it means                                                                                                                                                                           |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`            | An argument failed validation, and the message names the problem. Unknown fields are rejected rather than ignored. If an ability needs one of several argument sets, `help` lists them. |
| `ability_not_found`          | The name isn’t in that site’s catalog, or isn’t runnable. Check the spelling, or search again.                                                                                          |
| `run_failed`                 | Your site rejected the call. The WordPress error is appended to the message — fix that, then retry.                                                                                     |
| `knowledge_base_unavailable` | Search is temporarily unavailable. Try again shortly.                                                                                                                                   |
| `request_too_large`          | The input exceeds the size cap. Send less.                                                                                                                                              |
| `run_url_untrusted`          | The site is advertising a location inconsistent with its registration. Contact support.                                                                                                 |
| `internal_error`             | An unexpected fault on our side. Retry once, then contact support.                                                                                                                      |

## None of these match your symptom

Collect the `request_id` from the error and include it in your support request. It identifies the exact call in our logs. `internal_error` always carries one and never carries a suggested fix, so it’s the expected case for this path.

---

[Site and WordPress account connections](https://developers.wpengine.com/docs/ai/api/site-and-account-connections/)Why most of the failures on this page exist, and which tools need which connection.

[Authentication](https://developers.wpengine.com/docs/ai/api/authentication/#key-types)User versus account keys, and what a 401 and a 403 each tell you.

## 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"}}]}
```
