MCP Troubleshooting

Almost every MCP failure is one of six things, and they arrive in a predictable order: the connection will not complete, or it completes and no tools appear, or the tools appear and every call is refused. This page is organized by symptom, because that is what you have when you arrive.

One thing is true throughout and worth stating before anything else: no MCP failure is ever caused by your plan. Both MCP services are free on every plan, including Free. The one tier-shaped refusal you can hit is described below, and it means a workspace’s entitlements are broken, not that they are too small.

flowchart TD
    S["Something is wrong"] --> A{"Does the connection complete?"}
    A -->|"Sign-in never finishes"| A1["Consent screen never appears<br/>→ client did not follow the 401 challenge"]
    A -->|"403 at connect"| A2["Read the reason field<br/>→ user_banned / no_cms_membership / mcp_ineligible_tier"]
    A -->|Yes| B{"Do tools appear?"}
    B -->|"No tools, or every call 401s"| B1["Missing resource indicator<br/>→ opaque token, rejected"]
    B -->|Yes| C{"What do calls return?"}
    C -->|"Permission denied"| C1["Your Leed role<br/>→ describe_capabilities"]
    C -->|"-32020"| C2["Headers disagree with the body"]
    C -->|"-32602"| C3["Missing or malformed params._meta"]
    C -->|"404 unknown method"| C4["Method not in this revision<br/>e.g. initialize"]
    C -->|"400 batch refused"| C5["One request per POST"]

Connected, but every call fails

This is the most common report by a wide margin: the sign-in works, the client says it is connected, and then every single call comes back unauthorized.

The cause is a missing resource indicator. The MCP authorization specification requires a client to name the resource it wants a token for — RFC 8707 — by sending resource=https://app.leed.ai/mcp on its token request. When a client omits it, Leed’s authorization server has nothing to bind the token to, so it mints an opaque token instead of an audience-bound JWT. That token is perfectly real and completely useless here: /mcp accepts audience-bound tokens only, and rejects everything else outright.

That rejection is deliberate rather than incidental. A server that honors a token minted for a different resource is the confused-deputy problem the audience requirement exists to close, so there is no fallback to admit one.

The fix is always the same: use a client that sends the indicator, or configure it to.

Nothing to configure — Claude sends the resource indicator automatically when you add https://app.leed.ai/mcp as a custom connector.

If you are seeing this symptom on Claude anyway, the likely cause is a token minted before the connector was re-added. Remove the connector, add it again, and complete the sign-in.

403 on connect

There are exactly three reasons the Operator MCP refuses a connection, and the response names which one in a reason field. None of them is your plan.

ReasonWhat it meansFixIs it a plan problem?
user_bannedYour Leed account is under an active banAn administrator lifts the banNo
no_cms_membershipYou are not a member of the workspace the token is bound toReconnect and pick a workspace you belong toNo
mcp_ineligible_tierThe workspace’s stored plan tier is unset or unrecognizedContact support — the workspace’s entitlements need repairingNo

user_banned

An issued token survives a ban until it expires, so the ban is enforced on every request rather than only at sign-in. A temporary ban whose expiry has passed does not block: it is treated as lifted, and access resumes with no reconnection.

no_cms_membership

A connection is bound to one workspace at consent, and that binding is a preference, not an authorization. Membership is re-resolved from scratch on every single request, against the workspace the token names. A binding that is stale, or that names a workspace you have since been removed from, resolves to no role and is refused — a forged one fares no better.

If you have just been added to a workspace and are still seeing this, you are connected to the wrong one. Reconnect and pick again.

mcp_ineligible_tier

405 on GET or DELETE

Expected, not a fault. /mcp is POST-only: this transport opens no server-initiated event stream, so there is nothing for a GET to subscribe to, and it holds no session, so there is nothing for a DELETE to terminate.

So a 401 on your first probe is the system working. A 405 means you are authenticated and using the wrong method.

The connection works but tools return permission errors

The connection carries your Leed role. Tool calls are dispatched through the same guarded routes the CMS uses, with the same permission checks, so the rule is simple: if you cannot do it in the CMS, neither can the AI acting for you.

If the role is not what you expected, the fix is in the CMS, not the client: an administrator adjusts your role or grants a resource override. What each role can do is laid out in the privilege matrix.

Wrong workspace

A connection binds to exactly one workspace, chosen on the consent screen. There is no in-session switch and no way to change it from the client’s configuration.

Remove the server from your client, add it again, and pick the right workspace at consent. If you belong to only one workspace, you will not be asked and the binding is automatic.

The client asks for an API key or client secret

Leave both empty. There is no pre-shared secret anywhere in this design — clients register themselves dynamically and receive a public identifier with no secret at all.

A client that insists on a value is offering you a manual OAuth configuration path. Use the URL-only path instead; hand-configured clients are the ones that omit the resource indicator and produce the symptom at the top of this page. Setting the URL is the whole of the configuration, as Connecting to the Operator MCP describes.

Protocol-level errors

This section is for anyone reading raw JSON-RPC — writing a client, or reading a client’s debug log.

The full JSON-RPC error table
CodeNameHTTP statusMost likely causeFix
-32700Parse error400The body was not valid JSONFix the serialization
-32600Invalid request400Not a JSON-RPC 2.0 message, an id that is neither string nor integer, or an array bodyOne well-formed message per POST
-32601Method not found404A method this revision does not define — initialize most oftenUse server/discover, tools/list, tools/call or ping
-32602Invalid params400A missing or malformed params._meta envelopeSend the envelope on every request
-32603Internal error500A server-side failureRetry; report if it persists
-32020Header mismatch400A routable header disagrees with the body, or a required one is absentMake the headers match the message
-32022Unsupported protocol version400A revision this server does not speakUse one of the revisions the error names

A notification — a message with no id — is answered with 202 and an empty body, whatever it says. And -32021 is deliberately not defined by this server; if you see it, it did not come from Leed.

-32601 unknown method: initialize

Expected on the modern path, and not a sign that anything is broken. The 2026-07-28 revision has no initialize handshake. server/discover replaces it, returning the supported revision, the server’s capabilities and an optional instruction string. The error response names the revision on offer so a client can correct itself.

A client that opens with initialize and carries none of the modern headers or envelope is not served this error at all — it is classified as a legacy client and served on the compatibility path instead. Getting this error means your client declared itself modern and then sent an obsolete method.

-32020 header mismatch

Three headers are load-bearing in this revision, because an intermediary is entitled to route and cache on them without parsing the body. The server therefore proves they describe the message it is about to run.

  • MCP-Protocol-Version — required on every POST, and must equal the version inside params._meta.
  • Mcp-Method — required on every request, and must equal the body’s method.
  • Mcp-Name — required only for tools/call, resources/read and prompts/get, and must equal the target named in the body. Sending it on tools/list, ping or server/discover is not an error; omitting it there is not either.

Header values may be wrapped in the =?base64?…?= sentinel when they cannot be expressed in the ASCII header charset. The server decodes before comparing, so a legitimately encoded non-ASCII tool name does not produce a spurious mismatch — but a malformed payload inside the sentinel does.

-32022 unsupported protocol version

Your MCP-Protocol-Version header names a revision this server does not implement. The error’s data lists every revision on offer, modern and legacy, so a client can negotiate without guessing.

-32602 invalid params

Nearly always the params._meta envelope. Because the handshake is gone and there is no session, every request restates its own context, and two keys are mandatory on every single one:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_pages",
    "arguments": {},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

io.modelcontextprotocol/clientInfo is optional, and must be an object when present. Nothing on either server consumes the capabilities or the client info today — they are validated because the specification requires them, not because a tool reads them.

Batches are refused

An array body is rejected before any handler runs: one invalid-request error, HTTP 400, nothing executed. This holds in every revision, including the older ones whose specification permits batching.

[
  { "jsonrpc": "2.0", "id": 1, "method": "tools/list" },
  { "jsonrpc": "2.0", "id": 2, "method": "ping" }
]
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32600,
    "message": "the request body must be a single JSON-RPC request or notification, not a batch"
  }
}

Send one request per POST. A smaller batch does not help — there is no batch size that is accepted.

Old clients still work

You configure nothing for this. Point any specification-compliant MCP client at the URL and it works, old or new.

RevisionEraHandshakeError HTTP status
2026-07-28Modernserver/discoverReal status codes
2025-11-25LegacyinitializeAlways 200
2025-06-18LegacyinitializeAlways 200
2025-03-26LegacyinitializeAlways 200

A legacy client that asks for some other old revision is negotiated down to 2025-11-25. What decides the era is not the method name but the shape of the request: no version header and no modern envelope means legacy; a version header naming a legacy revision with no modern envelope means legacy; a header and a body naming different eras is refused as a header mismatch, because something between you and the server rewrote one of them.

The one behavior worth knowing if you are reading a legacy client’s log: on the legacy path every JSON-RPC error is served with HTTP 200, with the error in the body. A 200 is not a success there.

Docs MCP: the surface returns 404

When a site’s MCP entitlement is off, https://YOUR-DOMAIN/mcp, both OAuth endpoints and both agent well-known documents return 404 rather than 403. That is deliberate: to an unauthenticated probe, the surface is indistinguishable from “there is nothing here”, which is the correct answer for a site that never opted in.

If you own the site, check the MCP Configuration section — the section itself disappears from Settings when the entitlement is off, which is a diagnosis in its own right. Configuring the Docs MCP covers it, including the fact that switching it off removes the public site agent at the same time.

A preview build of a site also answers 404 on these paths regardless of entitlement. A staged site is never agent-readable.

Docs MCP: 403 after signing in

A reader who completed email verification and is now getting 403 on every call has hit one of two isolation checks:

  • token does not match this site — the token is bound to a different company than the site serving the request. Cross-site access is never permitted, and a token from one customer’s documentation is worthless at another’s. This normally means a client has cached a token against the wrong host.
  • MCP is not enabled for this site — the site’s entitlement was switched off after the token was issued. Existing tokens are not retro-actively deleted; they stop being honored at the next call.

Neither is fixed from the client. The first is fixed by reconnecting to the correct site; the second by turning the entitlement back on.

Local development

Point your client at your local backend:

http://localhost:8787/mcp

The Vite dev server on :5173 will not work. It is a different origin, it carries no authorization-server session cookie, and — decisively — an access token is bound to the origin that issued it, so a token minted for localhost:8787 is not valid anywhere else and vice versa. Use the backend port for the whole flow, including the browser sign-in.

Dynamic client registration accepts http for loopback addresses specifically so that native and desktop clients can receive a callback locally. Everything else must be https.

Every error string quoted on this page also appears, verbatim and alphabetized, in Common Error Messages, which links back here for the explanation. Things that genuinely do not work yet — as opposed to things that are failing — are collected in Known Limitations.

ESC