Troubleshooting auth walls¶
First connections fail in a small, well-mapped set of ways. This page
is symptom-first: match the error you see, read the wall behind it,
apply the fix. The W# labels cross-reference the deployer recipe's
auth-onramp matrix,
which carries the deepest realm-side detail.
Every client here is internal-only
MEHO is never publicly exposed. If a client cannot reach the
backplane's /.well-known/oauth-protected-resource metadata, the
fix is to put the client (or its mcp-remote shim) on the
VPN / internal network — never to expose the backplane through a
public ingress or tunnel.
Start with meho doctor¶
meho doctor checks your machine and names the layer that fails. It
only reports; it changes nothing.
meho doctor
- The offline layer checks the plugin shim, the vendored
mcp-remotelock and cache, your CA bundle, and which credential store holds your login. - The authenticated layer checks DNS, TLS, and your token, then runs the MCP handshake. It shows your scopes and any working-surface tool your session should see but does not.
- The server layer appears only on a Linux server where MEHO was
installed with its own install script (the file
/usr/local/sbin/meho-hostexists). Runsudo meho doctorthere. As root, the doctor runsmeho-host checkand shows its lines last: the node, the pods, the disk and the certificates of the built-in Kubernetes. Withoutsudoit shows one warning and runs nothing. A red server line gives exit code 1. Withsudo, the offline and authenticated lines check root's settings and login, not yours: runmeho doctorwithoutsudoto check your own login.
Use the failure class it prints (dns, tls, refresh_rejected,
surface_gap, ...) to pick the section below. See also the
CLI reference and
MCP surface and scopes.
The client never reaches the OAuth screen¶
Symptom. The /mcp call returns 401 with
WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource",
but the client never opens a browser or shows a login.
Wall. The client cannot fetch the resource_metadata URL, or the
URL points at a host it cannot resolve.
Fix. Confirm the client's machine reaches the backplane over the
VPN, and that BACKPLANE_URL in the backplane ConfigMap resolves to the
internal hostname clients actually use — if it is wrong, the metadata
document advertises the wrong host.
DCR fails with 403 Host not trusted (W1)¶
Symptom.
{"error":"insufficient_scope",
"error_description":"Policy 'Trusted Hosts' rejected request to
client-registration service. Details: Host not trusted."}
Wall. The client has no client_id in its config, so it attempted
Dynamic Client Registration (RFC 7591). Keycloak's default Trusted Hosts
policy ships with an empty whitelist, so anonymous DCR is disabled — the
correct, secure response.
Fix. Give the client a pre-registered client_id. Claude Code
takes it in .mcp.json (oauth.clientId); the
Claude Desktop shim takes it via
--static-oauth-client-info. For a client that exposes neither, carry a
token through the static-token shim. Do not
open DCR on a production realm to work around a per-client config gap.
Login start fails with unauthorized_client (W1)¶
Symptom. The device-code or authorization request 401s with
{"error":"unauthorized_client", …} before any browser approval.
Wall. The flow ran against a confidential client (typically
meho-backplane) that requires a secret the client cannot hold.
Fix. Point the client at a public client — meho-cli for the
CLI device-code flow, meho-mcp for MCP. See
Keycloak realm setup.
Authorization request fails with invalid_scope (W7)¶
Symptom. The Keycloak login page never appears. Keycloak sends the
browser back to the client's callback address with
error=invalid_scope and error_description=Invalid scopes: …. The
message names one or more of offline_access, mcp:read, mcp:execute
or mcp:admin. Claude Code shows this error. With the mcp-remote shim
(the MEHO plugin and the Claude Desktop bundle), the browser tab shows
Error: No authorization code received.
Wall. The client asked for a scope that the meho-mcp client does
not offer. Keycloak refuses a login that asks for a scope the client does
not offer as a default or optional client scope. It does not drop the
scope silently. Four scopes can cause this:
offline_access— the MEHO plugin, the Claude Desktop bundle, and Claude Code's own HTTP client all ask for it.mcp:readandmcp:execute— the MEHO plugin and the Claude Desktop bundle ask for them at every login.mcp:admin— asked for only when you turn on the operator tools.
Keycloak ships offline_access, but not the three mcp:* scopes. A realm
set up by hand often misses them.
Fix. Update the sign-in client meho-mcp: create each missing
mcp:* scope under Client scopes → Create client scope (or with
the operation keycloak.client_scope.create), then add each scope the
error names to meho-mcp as an optional scope (not default — only
flows that ask for a scope should get it). Use
meho admin keycloak bootstrap-clients only for a new realm: it does
all of this, but on a realm that already works it also rewrites the
meho-cli and meho-mcp clients and their tenant and role mappers.
To see what it would change, run it with --dry-run. See
Keycloak realm setup.
meho-cli does not get these scopes by default.
The MEHO login ends after about 30 minutes with no use¶
Symptom. MEHO tools work, but after about half an hour without a MEHO call, the next call opens a new browser login or does not answer.
Wall. The client did not ask for offline_access. Then Keycloak
issues a normal refresh token, and it ends at the realm's SSO idle limit
(30 minutes by default). Older versions of the MEHO plugin and the Claude
Desktop bundle did not ask for it.
Fix. Update the MEHO plugin (claude plugin update meho@meho, then
restart Claude Code) or install the current Claude Desktop bundle. If you
set the scopes yourself (MEHO_MCP_SCOPES or the bundle's scopes
field), keep offline_access in the list. The login then lasts until the
meho-mcp client's offline idle limit, 48 hours with the
recommended realm setting.
A MEHO call says "Your MEHO login has expired" (Claude Desktop)¶
Symptom. A MEHO tool call fails at once with "Your MEHO login has expired. Turn the MEHO extension off and on again in Claude Desktop (Settings > Extensions) to log in again." After that, the extension shows as stopped.
Wall. The saved login can no longer be renewed: it was not used for the offline idle limit, or the session was ended in Keycloak. A new browser login is needed. The bundle does not wait for that login in the middle of a call, so the call fails fast instead of hanging. Older bundles gave the call no answer at all.
Fix. In Claude Desktop, open Settings > Extensions and turn the MEHO extension off and on. Finish the login in the browser tab that opens; it may open behind the Claude Desktop window. Older bundles also opened a login tab when the call failed; you can close that one.
The extension stops with "MEHO did not connect" (Claude Desktop)¶
Symptom. The MEHO extension shows as stopped, and its log has a line
like meho-claude-desktop: MEHO did not connect within 5 minutes, so this
attempt stopped.
Wall. A browser login started, but nobody finished it within 5 minutes. Stopping frees the local login port, so the next attempt can log in. Older bundles waited forever instead.
Fix. Finish any open MEHO login in your browser. Then turn the MEHO extension off and on in Settings > Extensions. If several MEHO login tabs are open, close them all first.
A MEHO call says "Your MEHO login has expired" (Claude Code plugin)¶
Symptom. A MEHO tool call fails at once with "Your MEHO login has
expired. Run /mcp, pick meho, and reconnect to log in again." After that,
/mcp shows meho as failed.
Wall. The saved login can no longer be renewed: it was not used for the offline idle limit, or the session was ended in Keycloak. The plugin checked this before it gave up: it asked the login server again, and it looked for a newer login that another session saved. A new browser login is needed. The plugin does not wait for that login in the middle of a call, so the call fails fast instead of hanging. The saved login file stays; the new login replaces it.
Fix. Run /mcp, pick meho, and reconnect. Finish the login in the
browser tab that opens. Other Claude Code sessions that need a login at
the same time wait for this one, then use it. If Claude Code stops
waiting before you finish, finish the login anyway and reconnect once
more. Older plugin versions also opened a login tab when the call failed;
you can close that one.
A MEHO call says "The MEHO login server did not answer"¶
Symptom. A MEHO tool call fails with "The MEHO login server did not answer, so the login could not be renewed. Your login is kept. Try again in a moment." The Claude Code plugin or the Claude Desktop extension keeps running.
Wall. The login needed a renewal, and the login server (Keycloak) did not answer for about 30 seconds, or it answered with a server error (HTTP 5xx). The plugin tried again after 1, 2, 4 and 8 seconds. It did not open a browser login, and it kept the saved login.
Fix. Check that you are on the VPN and that Keycloak is up. Then run the call again. No reconnect is needed.
The plugin stops with "MEHO did not connect" (Claude Code plugin)¶
Symptom. meho shows as failed, and the MCP log has a line like
meho plugin: MEHO did not connect within 5 minutes, so this attempt
stopped.
Wall. The connect did not finish within MEHO_MCP_LOGIN_TIMEOUT
seconds (default 300). Two common reasons:
- A browser login started, but nobody finished it in time.
- Another Claude Code session was running a browser login at the same time, and nobody finished it. This session waits for that login, and the time limit counts the wait. When the other login is done in time, this session uses it and opens no browser.
Stopping frees the local callback port, so the next attempt can log in.
Older plugin versions waited forever instead, and every later session
failed with listen EADDRINUSE.
Fix. Finish any open MEHO login in your browser. Then run /mcp, pick
meho, and reconnect. If the login tab opens behind your terminal, look
for it there. If several MEHO login tabs are open, close them all and
reconnect once.
A session that is connected does not hold the callback port. It frees the port and removes its lockfile as soon as it connects, so a new login in another session can finish while it stays open.
Token issues but every call 401s¶
The token is minted, but the backplane rejects it on every call. From
v0.3.2 the 401 body carries a specific detail code — read it
first, then match the row:
detail code |
What it means | Fix |
|---|---|---|
invalid_audience |
The token's aud does not include the MCP resource URI (MCP_RESOURCE_URI, i.e. https://<host>/mcp). |
Confirm the meho-mcp-audience mapper is on the client that issues the operator's token (W2). A reverse proxy that rewrites the request path can also leave the audience pointing at the wrong URI — capture the issued token via the realm's introspection endpoint and read aud. |
missing_sub |
The token has no sub claim. Keycloak 25+ moved sub into a mapper inside the basic client scope, and clients created via the admin REST API do not auto-inherit realm default scopes. RFC 9068 makes sub required, so rejection is correct — the opacity is the problem. |
Assign basic (and roles, web-origins, acr) as default client scopes, then log out and back in (W3 — the deepest wall). |
missing_tenant_claim / missing_tenant_role_claim |
The person is in none of the five MEHO groups, or the tenant-id / tenant-role mapper is absent, so the backplane cannot place the caller in a tenant. |
Put the person in one group (Keycloak realm setup § groups, or Add a tenant for a new tenant), or add the tenant mappers (W2, § mappers). |
multiple_role_groups: … |
The person is in two or more of the five MEHO groups. Keycloak then copies the role from whichever group it finds first, so the role would be unpredictable. The message names the groups it found. | Keep the person in exactly one group (Keycloak realm setup § groups), then log out and back in. A site that renamed the groups sets JWT_ROLE_GROUPS on the backplane to the new names (comma-separated). |
multiple_role_groups: … of the same tenant … |
The login lists tenants, and the person is in two role groups of one tenant. The role in that tenant would not be clear. Groups of two different tenants are fine. | Keep the person in one role group of that tenant, then log out and back in. |
invalid_issuer |
The token's iss does not match the configured realm. |
Point the client at the same realm KEYCLOAK_ISSUER_URL names; a stale OAuth state from a previous realm can survive across config changes. |
token_expired / token_not_yet_valid |
exp is in the past, or nbf in the future beyond leeway (clock skew). |
Refresh the token; for nbf, sync the clock on the issuing or calling host. |
signature_verification_failed |
The JWS does not verify against the issuer's published keys. | Confirm the token comes from the realm the backplane validates against. |
invalid_token |
A structural failure with no more specific code — a truncated JWS, alg: none, or a kid absent from the JWKS. |
Inspect the token locally — confirm it is a three-segment RS256 JWS (segment count, alg, kid). Treat the bearer as a live credential: decode it on your own machine and never paste it into a shared issue, log, or chat — redact it from any diagnostic you share. |
The diagnostic detail (expected audience, claim name, exception class) is in the backplane's server log, not the 401 body — that body-vs-log split is the deliberate info-leak boundary an unauthenticated 401 honours.
Calls answer tenant_not_chosen or tenant_not_member¶
One login can be in several tenants. MEHO then works in one tenant at a
time: the request names it in the Meho-Tenant header (a tenant name or
ID). These answers come after a valid login:
detail code |
What it means | Fix |
|---|---|---|
tenant_not_chosen: … (HTTP 400) |
The login is in several tenants and the request named none. The message lists your tenants. meho status and the MCP tool meho_status still answer and list your tenants with the role in each. |
Choose a tenant with meho tenants use <name>, or send the Meho-Tenant header. For MCP, set the tenant in the connection settings and start a new session. |
tenant_not_member: … (HTTP 403) |
The Meho-Tenant header names a tenant you are not in. Agent, service and runner logins always stay in their own tenant. |
Pick one of the tenants the message lists. If you need another tenant, ask your admin to put you in one role group of that tenant, then log in again. |
tenant_not_found: … (HTTP 403) |
You are a platform admin and the header names a tenant that does not exist. MEHO never creates a tenant from this header. | Check the tenant name or ID. |
meho tenants use lists no tenant or only one, but you are in the groups of several tenants |
The token has no tenant list: the realm has no meho-tenants mapper. Or you did not log in again after the group change. |
Add the mapper (One person in several tenants, Step 1), then log in again. A new group shows at the next token refresh, at the latest at the next login. |
| Web interface: "The tenant changed. Reload the page." (HTTP 409) | You switched the tenant in another browser tab. This tab still shows the old tenant, so MEHO did not run the request. | Reload the page, then do the step again. |
| Web interface: "You are no longer in the tenant you worked in." | A refreshed token no longer lists the tenant: you were taken out of its role group. MEHO moved you to the one tenant left, or shows the tenant picker. | Pick a tenant. Ask your admin if you still need the old one. |
The full steps for an admin are in One person in several tenants.
meho login times out before you can approve (W4)¶
Symptom. meho: token exchange failed: context deadline exceeded,
often before you have finished approving in the browser.
Wall. Not the device-code TTL (10 minutes). An ambient parent deadline — a CI step timeout, an IDE task wrapper, an agent bash-tool timeout — truncates the approval wait.
Fix. Run meho login in a real interactive terminal without a short
wrapper deadline, or raise the wrapper timeout above the device-code
lifetime.
tools/list returns an empty list¶
Symptom. The client connects and authenticates, but no MEHO tools appear.
Wall. The token's tenant_role claim is below the rank a tool
requires (read_only < operator < tenant_admin), or the claim is
missing entirely.
Fix. Confirm the person is in one of the five MEHO groups and the
tenant-role mapper is on the client; walk back through the realm
setup (Keycloak realm setup).
The whole toolset is rejected (Claude Desktop)¶
Symptom. Settings → Connectors lists the MEHO tools, but a
conversation sees none of them; the UI names a tool-name pattern
error like
String should match pattern '^[a-zA-Z0-9_-]{1,64}$'.
Wall. Claude's frontend validates every remote-MCP tool name against
that pattern and rejects the whole toolset if any name fails. The
older dotted names (meho.status) failed.
Fix. Pin backplane v0.27.0 or later, where the tools were renamed
to underscores (meho_status, list_targets, …). See
Claude Desktop § pinning a build.
A tool call fails with "Tool execution failed" but the server logged 200¶
Symptom. A specific tool (e.g. list_targets, query_topology)
fails client-side with "Tool execution failed", while the backplane logs
a clean 200 and writes an audit row. meho_status works.
Wall. The tool declares an outputSchema, and MCP 2025-06-18
requires a conforming structuredContent result for such tools — which
Claude's frontend enforces. A build without the fix omits it, so every
schema-declaring tool hard-fails.
Fix. Pin a backplane build carrying the structuredContent fix
(merged after v0.27.0). Tools that declare no schema (meho_status) are
unaffected.
Newly-shipped tools don't appear after a backplane upgrade¶
Symptom. After upgrading the backplane, a new tool stays invisible;
tools/list keeps returning the old catalog.
Wall. The backplane advertises tools.listChanged: false (the
registry is fixed at process start), so a client that cached the catalog
at initialize has no signal to refetch.
Fix. Re-initialize the client — restart it, or disconnect and
reconnect the server. Also confirm the rollout actually cycled the
process (kubectl rollout status, or check the serverInfo.version),
since a new tool only exists in a new backplane process.
A successful call left no audit row¶
Symptom. A tool call succeeded, but no audit_log row exists for it.
Wall. The MCP audit writer fails closed — an unauditable call returns
JSON-RPC INTERNAL_ERROR (-32603), so a successful call with no row
means the row was rolled back mid-request, most often a transient DB
connectivity issue.
Fix. Check the backplane's structured logs for a
mcp_audit_write_failed event carrying the exception class before
suspecting the writer.
Going deeper¶
- Realm-side recipe, end to end: Keycloak realm setup.
- The full deployer auth-onramp matrix (per-audience capability claims, advanced realm shapes): values-examples deep-dive.
- Workstation TLS trust: TLS and ingress.