Conversation
Nothing in the console showed which MCP clients had connected over OAuth, and disconnecting one meant editing the database. The page lists the signed-in user's MCP clients, API keys (the existing /account surface) and browser sessions, each revocable after a confirmation. Revoking an MCP client deletes its access tokens and its consent: it is refused on its next request, open session included (MCP authenticates every request), and must be approved again. The /api/access plane answers the signed-in browser only, refusing any Authorization or x-api-key header, so an agent cannot list or revoke its own user's credentials; mutations also require a same-origin Origin. Everything is scoped to the caller's own rows, another user's ids read as not found, and no token or secret is served. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
In production the Connected clients page failed with "Unreadable rows (session)": Better Auth omits empty columns, so a session with no recorded IP arrives without an ipAddress key, and Effect v4's NullishOr accepts a null/undefined value but still requires the key. Optional fields are now optional(NullOr(...)), timestamps accept Date, ISO string or epoch ms, and sessions are read through the adapter like the OAuth rows. A row that still fails to decode is skipped and logged by field TYPES only (never values: these rows hold tokens) instead of blanking the whole list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Self-host had no view of what is signed in as you. MCP clients register themselves over OAuth (Claude Code, Cursor, Codex, …), and once connected there was no way to see them and no way to disconnect one short of deleting rows from the database. API keys had a page; browser sessions and OAuth clients had nothing.
What this adds
A Connected clients page listing, for the signed-in user only:
/account/api-keyssurface rather than a second implementation; creating keys stays on the API keys page.Each row can be revoked behind a confirmation. Revoking an MCP client deletes its access tokens and its consent, so it is refused on its next request — a session it already has open included, because the MCP envelope authenticates every request — and it has to pass the approval screen again before it can call a tool.
Security
The page and its API (
/api/access/*) exist to cut off credentials, so the rules are the interesting part:Authorizationorx-api-keyis refused, even though Better Auth would resolve it. These are exactly the credentials an agent holds; an agent must not be able to enumerate or revoke the credentials of the person it acts for, or quietly re-authorize itself.Originon a DELETE, so a page elsewhere cannot drive a revoke with the user's cookie.Tests
apps/host-selfhost/src/access/connected-clients.test.tsboots the real app and walks the real OAuth flow:x-api-keyheader (403);Plus
row-decoding.test.ts, a regression test for the decoding bug this hit in practice: Better Auth omits empty columns, so a session with no recorded IP arrives with noipAddresskey at all, and Effect v4'sNullishOraccepts a null value but still requires the key.I removed each guard in turn and confirmed the matching test fails.
Notes
🤖 Generated with Claude Code