Troubleshooting
Five real failure modes, each with its symptom and its fix.
Five things go wrong, and they look different from each other. Match the symptom first.
The service will not start after enabling MCP
Symptom. The process exits at startup instead of serving. The error names
four settings: KEYCLOAK_ISSUER, MCP_PUBLIC_BASE_URL,
KEYCLOAK_MCP_RESOURCE_AUDIENCE and KEYCLOAK_MCP_REQUIRED_SCOPE.
Cause. MCP_ENABLED is true and at least one of those four is empty. The
error names all four whichever one is missing, so it does not tell you which.
Fix. Set all four. See Operator setup.
Why it behaves this way. This is deliberate and it is not a bug. A /mcp
route with no issuer to verify tokens against, or no public URL to publish in
its own metadata, would answer every request the same wrong way. Refusing to
start is the safer failure, so there is no degraded mode to fall back to.
Source: services/runtime/src/runtime/config.py:561-580.
The client connects but every call is refused
Symptom. Authentication appears to succeed, you signed in, the client shows
the server as connected, and then every tool call comes back refused. The
refusal is a 403 about insufficient scope, not a 401.
Cause. The token carries the right audience but was never granted the required scope. The scope is configured in the realm as an optional client scope, so it is granted only when the client's authorization request explicitly asks for it.
Fix, in order.
- Fetch the discovery document, which is public and needs no credential, and
check that
scopes_supportedcontains the scope. If that field is missing or empty, no compliant client can ever learn to ask, and the operator has to fix the configuration first. Source:services/runtime/src/runtime/mcp/well_known.py:43-56. - Make the client re-authenticate from scratch, so it rebuilds its authorization request from the discovery document rather than reusing a token it already holds.
- If it still fails, the realm's client scope is the place to look. The scope
has to exist under exactly the name in
KEYCLOAK_MCP_REQUIRED_SCOPEand be assignable to the self-registered client.
Source: services/runtime/src/runtime/mcp/asgi.py:78-82 for the requirement and
services/runtime/src/runtime/mcp/verifier.py:136 for the scopes the token
actually carries. The 403 and its insufficient_scope label come from the MCP
SDK's RequireAuthMiddleware, which is what enforces the requirement.
Calls that worked have stopped working
Symptom. The same client, the same workspace, calls that succeeded yesterday. Now every one is refused with a message saying this identity's MCP access has been revoked in this workspace. Everything else about the account still works: signing in, the product, other workspaces.
Cause. An admin revoked this member's MCP access in that workspace.
Fix. An admin restores it:
POST /v1/workspace/members/{user_id}/mcp-access/restoreIt takes effect on your next call. There is nothing to wait out and no need to reconnect.
Source: services/runtime/src/runtime/auth/mcp_access.py:85-91 for the refusal
and services/runtime/src/runtime/api/workspace_admin.py:686-733 for the
restore. See Workspace access.
How to tell this apart from the scope problem. The scope failure refuses every call from the first one, in every workspace. This one starts refusing part-way through a working session, and only in the workspace where it was revoked. If you belong to several workspaces, try naming a different one: if that call succeeds, this is the cause.
The token has expired
Symptom. A connection that was working starts answering 401, and the
response carries a WWW-Authenticate header naming the resource metadata
document. Unlike the revocation case, this is not workspace-specific: naming a
different workspace does not help.
Cause. Access tokens have a lifetime set by the realm. The runtime checks the validity window on every verification, with a small allowance for clock skew.
Fix. Re-authenticate. A compliant client does this for you: it reads the
metadata document named in the WWW-Authenticate header and runs the
authorization flow again, which may put a sign-in window in front of you. If
your client does not recover on its own, remove the server and add it again,
which forces a fresh flow.
Source: services/runtime/src/runtime/mcp/verifier.py:120-128 for the
verification, and services/runtime/src/runtime/mcp/asgi.py:78-82 for the
metadata URL published on the refusal.
Nothing is wrong with your access when this happens. An expired token is the normal end of a token's life, not a signal that anything changed.
No workspace named, and you belong to more than one
Symptom. The call is refused with a 403 saying the account belongs to
several workspaces and asking for one to be named. It happens on every tool,
including one called with no arguments at all.
Cause. You did not say which workspace the call acts in, and the server will not guess. An arbitrary pick would quietly show one customer's data to someone who asked for no customer in particular, and would do it without any error for anyone to notice.
Fix. Pass workspace_id on the tool call. In practice, tell the client
which workspace you mean and it will fill in the argument. Every one of the five
tools accepts it.
Source: services/runtime/src/runtime/auth/workspaces.py:234-240 for the
refusal, services/runtime/src/runtime/mcp/tools.py:132-166 for where it is
resolved, and services/runtime/src/runtime/mcp/tools.py:238-246 for the
argument every tool carries.
A related case that looks the same and is not. If you name a workspace and
get a 404, you are not a member of it, or it does not exist. The server
answers both the same way on purpose, so a refusal cannot be used to find out
which workspaces exist.
Source: services/runtime/src/runtime/auth/workspaces.py:243-255.
One message you can ignore the wording of
The several-workspaces refusal is written for a caller making an HTTP request,
so it asks you to name the workspace in a request header. Over MCP there is no
header to set. Pass workspace_id on the tool call instead, which is the same
selection by a different route.