AuralisDocs

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.

  1. Fetch the discovery document, which is public and needs no credential, and check that scopes_supported contains 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.
  2. 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.
  3. 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_SCOPE and 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/restore

It 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.

On this page