Dev.to Security 🔐 Cybersecurity 👁 0 📖 4 min read

MCP server connects but never asks you to log in

The client says “connected”, tools/list answers, and every account-scoped tool returns 401 forever. There is no error to search for — this article is the missing error message. The symptom (all of these, same ca

The client says “connected”, tools/list answers, and every account-scoped
tool returns 401 forever. There is no error to search for — this article is the
missing error message.

The symptom (all of these, same cause)

  • Login finishes with no tokenmcp login <server> (or the client's
    “connect / authenticate” button) ends with something like “the server responded, but no OAuth token
    was obtained”.

  • No window, no URLNo browser, no consent URL, no device code is ever shown — the
    flow simply never starts.

  • Only the client registration existsThe token directory holds the
    *.client.json (the dynamic registration result) and not the *.json
    with an access_token.

  • Connected, and 401 on everything account-scopedPublic tools (search, catalog,
    docs) work; profile, library, orders and credits return 401.

The cause: the 401 that never arrives during login

The MCP authorization spec makes the client start the flow reactively: it only
discovers the authorization server and begins OAuth when it gets back

`HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://<host>/.well-known/oauth-protected-resource"`

A server that answers initialize and tools/list without requiring auth
(200) and only challenges 401 on account-scoped calls never trips that trigger during a login
probe. The probe sees “connected” and stops — the client exits with no URL, no consent step and no
token.

That is a server-side design choice (public tool surface, private account tools), not a broken
server and not a broken client: what fails is the interaction between the two.

Diagnosis: 4 commands, in this order

  • Does the server challenge you?Call a tool that needs an account and look for
    401 + WWW-Authenticate with resource_metadata=. A 200 means
    the tool is public: pick another (profile / library / credits / orders).
    curl -sS -i -m 20 -X POST "$MCP_URL" -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
    "params":{"name":"<account_scoped_tool>","arguments":{}}}' | head -30

  • Which authorization server does it advertise?Look for
    authorization_servers[] and scopes_supported[].
    curl -sS -m 20 "$MCP_URL_ORIGIN/.well-known/oauth-protected-resource"

  • Does the server accept a client created on the fly?Look for
    registration_endpoint (you do not have to register a client by hand),
    authorization_endpoint, token_endpoint,
    code_challenge_methods_supported: S256 and
    token_endpoint_auth_methods_supported: none (public client + PKCE).
    curl -sS -m 20 "$AUTH_SERVER/.well-known/oauth-authorization-server"

  • Did a token land on disk?Registration is not authentication. Only the
    *.client.json means OAuth did not complete.
    ls -la "$TOKEN_DIR" # ~/.config/<client>/mcp-tokens/, ~/.mcp/tokens/

How to read it: (1) 401 with WWW-Authenticate = the server is correctly
configured, the client just never asked; (3) with registration_endpoint = the flow is
automatic and there is no client to register by hand — whatever the client's error message
suggests.

The fix: force one 401 while the OAuth provider is live

  • A. Client-native (best)Find the key that marks the server as OAuth and, if the
    client has one, the “authenticate now / login” command. Set it, then call an account-scoped tool
    from a normal session — most clients run the flow the first time a 401 comes back mid-session. The
    keys are usually <server>.auth: oauth plus an oauth block
    (flow: browser or device, timeout).

  • B. Script the challenge (works everywhere)Import the client's own MCP connection
    layer, wrap the connection in its interactive-OAuth context and call an account-scoped tool. The 401
    arrives, the provider starts the flow and the consent URL is printed for a human to approve.

  • C. Manual (last resort)Register a client with the server's
    registration_endpoint (JSON with redirect_uris, client_name,
    grant_types:["authorization_code","refresh_token"],
    response_types:["code"], token_endpoint_auth_method:"none"), open the
    authorization_endpoint with response_type=code,
    code_challenge (S256) and state, catch the code on the
    loopback redirect, POST it to the token_endpoint with the code_verifier,
    then write the token into the client's store in the shape it expects.

The pitfalls that cost the most time

  • A short timeout cancels the callback waitWith the interactive flow, the wait for
    the browser callback happens inside the tool call. If it times out, the human approves, the
    code is lost, and the approval must be repeated. Raise the per-call timeout and the OAuth
    flow timeout above the time a human needs.

  • registration_endpoint present, no tokenA client registration file
    proves only dynamic registration. Check for access_token.

  • An already-open session will not gain the toolsMCP servers are registered when the
    agent session starts. After the token lands, open a new session (or restart the client/gateway) —
    otherwise you conclude “still broken” while the fix is already in place.

  • Verify with a real call, not with “connected”Passing a login probe means nothing
    here: call an account-scoped tool and read the response. Without that output the authentication is
    not verified — say so.

  • Refresh tokensAfter the first fix, keep the refresh_token and re-read
    the token file when the access token expires, instead of re-running the interactive flow.

How to report it (a shape that leaves no doubt)

`CAUSE: <one sentence: which step of the flow never happened>
EVIDENCE: <the 401 + WWW-Authenticate line, the registration_endpoint line, the token file path>
FIX APPLIED: <A | B | C + what changed>
VERIFIED: <the exact account-scoped call and its returned text, or "NOT VERIFIED: <why>">
NEXT: <single next action>`

The last line matters most: VERIFIED with the raw output, or the admission that it
could not be checked. “Connected” is not evidence.

Start now

The MCP OAuth Debug skill ships the full diagnosis, the one-page checklist and the
reference implementation of fix B (scripts/force-oauth-challenge.py), for any MCP client
and any OAuth-protected server.

See the MCP OAuth Debug skill
See all skills

Need it solved inside your project? Guided setup from
R$ 1.500, with the first cycle run together.

← all skills

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.