Symptom
Founder: "i logged out of my linear mcp thing multiple times and logged back in but it's still using shannonlabs instead of shannon-labs so the logout part doesn't work."
linear.app/shannonlabs is a different workspace from the intended linear.app/shannon-labs (AGENTS.md calls this out explicitly). Logging out and back in never changes which one the token is bound to.
Local logout is not the bug
crates/tui/src/mcp/oauth.rs derives the credential key identically on all three paths:
fn store_key(server_name: &str, url: &str) -> String {
// sha256(server_name || NUL || url), base64url-nopad
format!("mcp_oauth_{}", URL_SAFE_NO_PAD.encode(digest))
}
load_oauth_tokens, save_oauth_tokens, and delete_oauth_tokens all call it with the same arguments, so delete removes exactly what save wrote. Verified on disk: for linear @ https://mcp.linear.app/mcp the expected key mcp_oauth_TMpjAqZY78w3fuGMxVKdWr2zFq1W5uSOUteXS7nvrgQ is absent from both backends — keychain service deepseek and the file store ~/.codewhale/secrets/secrets.json (0 mcp_oauth_ entries). Local state really is clear.
The actual defect
The authorization request carries no re-consent or account-selection parameter:
AuthorizationRequest::new(redirect_uri)
.with_scopes(scopes.iter().copied())
.with_client_name("Codewhale")
grep -n "prompt" crates/tui/src/mcp/oauth.rs returns no query-parameter hit on either the dynamic-registration path or the configured-client_id path (OAuthClientConfig::new(client_id, redirect_uri).with_scopes(...)).
So the sequence is:
- Logout deletes the local token — correctly.
- Login opens the provider's authorize URL.
- The provider still holds a live session and a standing grant for this client, bound to
shannonlabs.
- With no
prompt=consent / prompt=select_account, the provider redirects straight back with a fresh code — no picker is ever shown.
- A new token is written for the same wrong workspace.
The user is logged out locally and unchanged remotely. Nothing in the product can currently move that binding, which is why repeating the cycle changes nothing.
Why this also explains the 401 loop
Codewhale's Linear server has no stored token at all, so every boot 401s:
'linear-connect': MCP server https://mcp.linear.app/mcp rejected the request with 401 Unauthorized
'linear-live': … 401 Unauthorized
'linear-live2': … 401 Unauthorized
The endpoint itself is healthy — an unauthenticated probe returns HTTP 401 in 0.047s. The failure is credential state, not reachability or a hung server.
Worth confirming as part of this fix whether a completed browser flow is actually persisting: the founder reports logging in multiple times, yet no mcp_oauth_* entry exists in either backend. If login reports success without a durable write, that is a second defect on the same path and the more serious of the two.
Suggested shape
- Send
prompt=consent on an explicit /mcp login after a logout, so the provider always re-shows the account/workspace picker instead of silently re-granting.
- Have
/mcp logout <server> say plainly that it cleared local credentials only, and surface the provider's revocation URL — local deletion cannot revoke a remote grant, and the UI currently implies it can.
- Use the RFC 7009 revocation endpoint when the server's metadata advertises one, so logout actually severs the grant rather than dropping our copy of it.
- Assert the token persisted after a successful flow and fail loudly if it did not.
Evidence
crates/tui/src/mcp/oauth.rs: store_key (~line 1458), delete_oauth_tokens_for_server (~line 1344), AuthorizationRequest construction (~line 1701).
- Keychain service
deepseek (codewhale_secrets::DEFAULT_SERVICE) — no Linear entry.
~/.codewhale/secrets/secrets.json — 1 key total, 0 mcp_oauth_ entries.
~/.codewhale/logs/*.log — three distinct 401s across linear-connect, linear-live, linear-live2.
Symptom
Founder: "i logged out of my linear mcp thing multiple times and logged back in but it's still using shannonlabs instead of shannon-labs so the logout part doesn't work."
linear.app/shannonlabsis a different workspace from the intendedlinear.app/shannon-labs(AGENTS.md calls this out explicitly). Logging out and back in never changes which one the token is bound to.Local logout is not the bug
crates/tui/src/mcp/oauth.rsderives the credential key identically on all three paths:load_oauth_tokens,save_oauth_tokens, anddelete_oauth_tokensall call it with the same arguments, sodeleteremoves exactly whatsavewrote. Verified on disk: forlinear@https://mcp.linear.app/mcpthe expected keymcp_oauth_TMpjAqZY78w3fuGMxVKdWr2zFq1W5uSOUteXS7nvrgQis absent from both backends — keychain servicedeepseekand the file store~/.codewhale/secrets/secrets.json(0mcp_oauth_entries). Local state really is clear.The actual defect
The authorization request carries no re-consent or account-selection parameter:
grep -n "prompt" crates/tui/src/mcp/oauth.rsreturns no query-parameter hit on either the dynamic-registration path or the configured-client_idpath (OAuthClientConfig::new(client_id, redirect_uri).with_scopes(...)).So the sequence is:
shannonlabs.prompt=consent/prompt=select_account, the provider redirects straight back with a fresh code — no picker is ever shown.The user is logged out locally and unchanged remotely. Nothing in the product can currently move that binding, which is why repeating the cycle changes nothing.
Why this also explains the 401 loop
Codewhale's Linear server has no stored token at all, so every boot 401s:
The endpoint itself is healthy — an unauthenticated probe returns
HTTP 401 in 0.047s. The failure is credential state, not reachability or a hung server.Worth confirming as part of this fix whether a completed browser flow is actually persisting: the founder reports logging in multiple times, yet no
mcp_oauth_*entry exists in either backend. If login reports success without a durable write, that is a second defect on the same path and the more serious of the two.Suggested shape
prompt=consenton an explicit/mcp loginafter a logout, so the provider always re-shows the account/workspace picker instead of silently re-granting./mcp logout <server>say plainly that it cleared local credentials only, and surface the provider's revocation URL — local deletion cannot revoke a remote grant, and the UI currently implies it can.Evidence
crates/tui/src/mcp/oauth.rs:store_key(~line 1458),delete_oauth_tokens_for_server(~line 1344),AuthorizationRequestconstruction (~line 1701).deepseek(codewhale_secrets::DEFAULT_SERVICE) — no Linear entry.~/.codewhale/secrets/secrets.json— 1 key total, 0mcp_oauth_entries.~/.codewhale/logs/*.log— three distinct 401s acrosslinear-connect,linear-live,linear-live2.