Skip to content

Commit a457ac1

Browse files
Sbussisoclaude
andcommitted
ux(mcp): confirm revoke + warn about active AI clients
Tonight's debugging session opened with you revoking my Claude Code key and then us spending 30 minutes diagnosing why my MCP tools were returning 401. The server-side revoke worked instantly (correct behavior — bearer is validated per request), but my Claude Code session kept sending the now-stale bearer because nothing told it the key was gone. By the time we figured it out, my session had to restart anyway. Two-line UX win to prevent the same surprise next time anyone clicks Revoke: 1. Confirmation dialog spells out exactly what's about to happen: AI clients keep their connection process alive but every tool call returns 401 until you restart them with a new key. Operator acknowledges before the API call goes through. 2. Toast message after revoke is now action-oriented: "Key 'foo' revoked. Restart any AI client that was using it." instead of just "API key revoked". Also passes the full key object (not just the id) to handleRevoke so the dialog and the toast can reference the key name. The deeper fix — actively closing live HTTP connections using a revoked key — would require dropping FastMCP's `stateless_http=True` mode and tracking sessions in memory keyed by hash. Bigger change; not justified for this UX issue alone. This commit covers the operator-facing gap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9183764 commit a457ac1

1 file changed

Lines changed: 37 additions & 5 deletions

File tree

frontend/src/pages/McpPage.jsx

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,40 @@ function McpPage() {
352352
}
353353
}
354354

355-
const handleRevoke = async (keyId) => {
356-
setRevoking(keyId)
355+
const handleRevoke = async (key) => {
356+
// Confirmation + active-client warning. The MCP server validates
357+
// the bearer on every tool call, so revocation takes effect
358+
// immediately server-side — but a connected AI client (Claude
359+
// Desktop, Cursor, mcp-remote stdio bridges, etc.) keeps sending
360+
// the now-stale bearer on every subsequent tool call. Their
361+
// process stays "running" in their MCP panel; only the tool calls
362+
// fail with 401, and the user often doesn't notice until they
363+
// try to use the AI again.
364+
//
365+
// We can't push a "you've been deauth'd" notification to live
366+
// connections without moving off the FastMCP stateless-HTTP mode,
367+
// so the practical answer is to make sure the operator clicking
368+
// Revoke knows they need to restart the AI clients themselves.
369+
const confirmed = window.confirm(
370+
`Revoke API key "${key.name}"?\n\n` +
371+
`Any AI client currently using this key (Claude Desktop, Cursor, etc.) ` +
372+
`will start receiving 401 errors on every tool call. The clients will ` +
373+
`appear "running" in their MCP panel but won't actually work — you'll ` +
374+
`need to quit and restart each one with a new key.\n\n` +
375+
`Once revoked, this key cannot be reactivated. Generate a new key if ` +
376+
`you still need MCP access.`
377+
)
378+
if (!confirmed) return
379+
380+
setRevoking(key.id)
357381
try {
358382
const token = await getToken()
359-
await revokeMcpKey(() => Promise.resolve(token), keyId)
383+
await revokeMcpKey(() => Promise.resolve(token), key.id)
360384
await loadKeys()
361-
showToast("API key revoked", "success")
385+
showToast(
386+
`Key "${key.name}" revoked. Restart any AI client that was using it.`,
387+
"success",
388+
)
362389
} catch (err) {
363390
showToast(err.message || "Failed to revoke key", "error")
364391
} finally {
@@ -890,7 +917,12 @@ function McpPage() {
890917
{k.last_used_at && <> — Last used {new Date(k.last_used_at).toLocaleDateString()}</>}
891918
</span>
892919
</div>
893-
<button className="btn btn-small btn-danger" onClick={() => handleRevoke(k.id)} disabled={revoking === k.id}>
920+
<button
921+
className="btn btn-small btn-danger"
922+
onClick={() => handleRevoke(k)}
923+
disabled={revoking === k.id}
924+
title="Revoke this key (you'll need to restart any AI client using it)"
925+
>
894926
{revoking === k.id ? "Revoking..." : "Revoke"}
895927
</button>
896928
</div>

0 commit comments

Comments
 (0)