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.
- Claude
- Claude Code
- ChatGPT
- Cursor
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.
Nothing to configure — Claude Code sends the resource indicator automatically.
If calls are still failing, re-run the authorization rather than re-adding the server: inside Claude Code, run /mcp, select the Leed server and authenticate again. That discards the stale token and requests a correctly bound one.
Add the server by URL and let the client complete the OAuth flow itself. Do not paste a client id or secret into any manual OAuth form — a hand-configured client is exactly the case that omits the resource indicator and produces this symptom.
If every call fails after a successful sign-in, remove the connector and add it again by URL alone.
Configure the server with a URL only:
{
"mcpServers": {
"leed": {
"url": "https://app.leed.ai/mcp"
}
}
}Adding OAuth fields by hand is what usually breaks this. If you have a client id, a client secret or a token endpoint in that entry, remove them, delete any cached credentials for the server, and authenticate again from the MCP settings panel.
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.
| Reason | What it means | Fix | Is it a plan problem? |
|---|---|---|---|
user_banned | Your Leed account is under an active ban | An administrator lifts the ban | No |
no_cms_membership | You are not a member of the workspace the token is bound to | Reconnect and pick a workspace you belong to | No |
mcp_ineligible_tier | The workspace’s stored plan tier is unset or unrecognized | Contact support — the workspace’s entitlements need repairing | No |
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
| Code | Name | HTTP status | Most likely cause | Fix |
|---|---|---|---|---|
-32700 | Parse error | 400 | The body was not valid JSON | Fix the serialization |
-32600 | Invalid request | 400 | Not a JSON-RPC 2.0 message, an id that is neither string nor integer, or an array body | One well-formed message per POST |
-32601 | Method not found | 404 | A method this revision does not define — initialize most often | Use server/discover, tools/list, tools/call or ping |
-32602 | Invalid params | 400 | A missing or malformed params._meta envelope | Send the envelope on every request |
-32603 | Internal error | 500 | A server-side failure | Retry; report if it persists |
-32020 | Header mismatch | 400 | A routable header disagrees with the body, or a required one is absent | Make the headers match the message |
-32022 | Unsupported protocol version | 400 | A revision this server does not speak | Use 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 insideparams._meta.Mcp-Method— required on every request, and must equal the body’smethod.Mcp-Name— required only fortools/call,resources/readandprompts/get, and must equal the target named in the body. Sending it ontools/list,pingorserver/discoveris 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.
| Revision | Era | Handshake | Error HTTP status |
|---|---|---|---|
2026-07-28 | Modern | server/discover | Real status codes |
2025-11-25 | Legacy | initialize | Always 200 |
2025-06-18 | Legacy | initialize | Always 200 |
2025-03-26 | Legacy | initialize | Always 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/mcpThe 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.