fix(frontend): stop the MCP Usage tab calling an unknown owner "Personal" - #7480
Merged
Conversation
…n MCP usage `McpServerAgentUsage` — the shape behind the registry card's "used by" tooltip and the server's Usage tab — carried `scope` and `ownerEmail` only. Both queries that build it (`AgentToolModel .getAssignedAgentDetailsForMcpServers`, `AgentModel .getAutoModeAgentDetailsByOrganizations`) LEFT JOIN the author, so `ownerEmail` is null for an agent whose author no longer exists — `agents.author_id` is ON DELETE SET NULL. That leaves a surface with no way to say "this one is yours" on exactly the rows where it matters most. Select `agents.author_id` as `ownerId` in both queries so identity, not a display string, answers it.
…nal" The Owner column printed the agent's owner email, and fell back to `scopeLabel(agent.scope)` when there wasn't one. For a personal agent that fallback is the word "Personal" — a statement about visibility dressed up as an answer to "whose is this". Two rows for the same person could therefore disagree: one spelled out an email, the next said "Personal", and neither told the viewer which agents were their own. Replace the fallback with an explicit decision, `describeAgentOwner`: the viewer's own personal agents read "You" (email kept in the title), someone else's read their email, a personal agent with no author on record reads "Unknown", and team/org agents keep their scope — there the scope really is the owner. The personal scope label no longer appears in that column at all. The unknown case is a real and permanent state, not a loading gap: `agents.author_id` is ON DELETE SET NULL, so a deleted account leaves its personal agents authorless for good. `agentOwnerLabel` stays for the two surfaces that append the owner to an agent's name (registry card tooltip, uninstall dialog), where rendering nothing was already the right answer for an unattributable agent.
…eleted Removing someone's last organization membership hard-deletes their `user` row (`routes/auth.ts` -> `UserModel.delete`), and `agents.author_id` is `ON DELETE SET NULL`. So an agent that outlives its author keeps the row and loses every trace of who wrote it — no id, no name, no email. Any surface that names an owner can then do no better than shrug at a perfectly ordinary departed colleague's agent. Add `agents.deleted_author_name` / `deleted_author_email`, written by `AgentModel.snapshotAuthorIdentityForDeletion` immediately before the user row goes, from both deletion paths: `UserModel.delete` for the app-driven removals and better-auth's `user.delete.before` hook for the self-service endpoint. It is idempotent and returns early once the user is gone, so the paths overlapping is harmless and a re-run cannot overwrite a good snapshot with nulls. Written only at deletion time, never on create: while the author exists `author_id` is the single source of truth and a denormalised copy would drift the moment they changed their email. The pair `author_id IS NULL AND deleted_author_email IS NOT NULL` therefore means exactly "the author's account was deleted, and this is who they were". `McpServerAgentUsage` carries the retained identity as `formerOwnerName` / `formerOwnerEmail`, kept separate from the live `ownerId`/`ownerEmail` rather than coalesced into them, so a consumer cannot mistake a dead address for a current one. Agents orphaned before this existed have nothing to recover and stay unattributed; the set only shrinks from here.
Users are hard-deleted (no soft delete on `user`), so an agent that outlives its author needs something retained to name them at all — but one column is enough. The Owner column speaks emails on every other row, and an email is the identifier that does not collide, so carrying the display name as well bought a second column, a preferred/fallback rule and a title attribute for no added clarity. Drops `agents.deleted_author_name`; `deleted_author_email` alone backs `formerOwnerEmail`, and the cell reads "Deleted user (kim@example.com)".
…hemas `InsertAgentSchemaBase` / `UpdateAgentSchemaBase` are derived from the agents table and omit the server-owned columns one by one, so the new `deleted_author_email` was accepted from a request body and written straight through by `AgentModel.create`/`update`. That is a forgeable attribution, not just a stray field: the MCP Usage tab renders the column as "Deleted user (<address>)", so any caller who could create an agent could make it claim it once belonged to whoever they named. Omit it in both schemas, alongside `author_id`, whose absence it stands in for. The route test asserts the supplied value does not reach the row — it fails on the previous revision with the forged address persisted. Also moves the `deletePersonalMcpGatewaysForUser` docblock back above its own method; inserting the snapshot helper ahead of it had left the comment describing the wrong function.
Omitting `deletedAuthorEmail` from the agent create/update schemas narrows both request bodies, and the generated artifacts still advertised the field. Regenerated: `CreateAgentData` and `UpdateAgentData` no longer accept it.
… owner `types/mcp-server.ts` now holds two different "owner"s: the agent's author on `McpServerAgentUsageSchema`, and the person who installed the server on `SelectMcpServerSchema`. Say so at the point of confusion. The field is named for the pre-existing `ownerEmail` it sits beside rather than for its column, so the pair stays consistent. Renaming both to `author*` would match `SelectAgentSchema` and read better, but it reaches `ToolDelegationTarget` and around twenty consumers of a pre-existing field, so it belongs in its own change rather than riding along with a bug fix.
The snapshot column treated a symptom. Sixteen tables lose a user identity when an account goes — `audit_log.actor_id` among them — so a column on `agents` fixes one instance and invites the next one. It also introduced its own bug: a writable server-owned field, forgeable through the public agent APIs. Nothing needs storing to say the author is gone. For a personal agent, `author_id IS NULL` already means exactly that: every create path stamps an author, and both routes that could make an existing agent personal refuse to do so without one, so there is no other way into that state. The cell now derives "Deleted user" from the shape of the data. Removed with it: the migration, `deleted_author_name`/`_email`, the snapshot helper and its two deletion-path hooks, the `.omit()` guard that existed only to protect the column, and the tests for all of it. What remains is `ownerId` on the usage payload and the decision that reads it. Naming WHICH deleted user needs the identity to survive, which means not destroying it — soft-deleting users, so all sixteen tables keep their authorship. That is a change to user deletion and the auth read paths, not to this table, and it belongs in its own reviewed PR.
github-merge-queue
Bot
removed this pull request from the merge queue due to no response for status checks
Aug 26, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to no response for status checks
Aug 26, 2026
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.
The bug
On an MCP server's Usage tab, the Owner column is inconsistent: some
personal agents are attributed to an email address, while others — including
agents belonging to the person looking at the page — just say "Personal". So
"Personal" cannot be read as "this one is yours", and two rows for the same
owner can disagree.
mcp-server-usage-tab.tsxrendered the cell like this:agentOwnerLabelreturnsownerEmailfor a personal agent andnullotherwise, so the fallback is doing two unrelated jobs at once:
which is right — such an agent belongs to the team or the organization, so
the scope is the owner;
different question than the column asks. It is a visibility scope wearing the
clothes of an owner, and it reads as "mine".
Why the email was missing
ownerEmailcomes from a LEFT JOIN onto the agent's author(
AgentToolModel.getAssignedAgentDetailsForMcpServersandAgentModel.getAutoModeAgentDetailsByOrganizations), andagents.author_idisdeclared
ON DELETE SET NULL.The
userrow is genuinely deleted, and by an ordinary flow: removing someonefrom an organization deletes their account outright once it was their last
membership —
So every agent a departing colleague authored kept its row and lost its author,
with no name or email left anywhere to recover. Those are exactly the rows that
came out labelled "Personal", sitting in the same table as rows that named a
real person.
The Usage tab was also the only surface that took this fallback. The two other
consumers of
agentOwnerLabel(the registry card's "used by" tooltip and theuninstall dialog) append the owner to an agent's name and render nothing when
there isn't one, which was already correct.
The fix
1. Carry the owner's id, not just their email
McpServerAgentUsageSchemagainsownerId(agents.author_id), selected inboth queries that build the shape. Deciding "is this row mine" by matching the
session's email against a display string is the wrong instrument, and that
string is null on precisely the rows that most need telling apart.
2. Say the author is gone, without storing anything
For a personal agent,
author_id IS NULLalready means "the author's accountwas deleted": every create path stamps an author, and both routes that could
make an existing agent personal (
PUT /api/agents/:idand the bulk visibilityroute) refuse to do so without one. So there is no other way into that state,
and the cell can derive "Deleted user" from the shape of the data.
An earlier revision of this PR added an
agents.deleted_author_emailsnapshotcolumn so the row could name which person. That was the wrong shape and it is
gone. Sixteen tables lose a user identity when an account is deleted —
audit_log.actor_idamong them — so a column onagentsfixes one instance andinvites the next; it also introduced a bug of its own, a server-owned field that
turned out to be writable through the public agent APIs.
Naming the person requires the identity to survive, which means not destroying
it: soft-deleting users instead of hard-deleting them, so all sixteen tables
keep their authorship and no table needs a column. That is a change to
UserModel.deleteand to the auth read paths —accountandsessioncascadetoday,
user.emailis hard-unique, and better-auth owns every sign-in query —so it deserves its own PR rather than riding along with a UI fix.
3. One explicit decision instead of a fallback
describeAgentOwner(lib/agent-owner-label.ts) replaces the ternary with adiscriminated result the cell renders branch by branch:
title)The personal scope label no longer appears in that column at all, so nothing in
it can be misread as an owner. "Deleted user" stays in front of the address
deliberately: a bare address invites the reader to go and ask a colleague who
has left. Unknown is now only reachable for agents orphaned before the snapshot
existed — a set that only shrinks.
agentOwnerLabelremains, documented as the short qualifier form, for the twoname-suffix surfaces above.
Notes
mcp-server-usage-tab.test.tsx: the viewer's own agent renders"You" and never "Personal", and an authorless personal agent renders "Deleted
user" — not "Personal", not "You". Those are the readings the bug conflated,
and they are decided entirely by props, so an e2e would add fixtures without
testing anything more.
then removed two members through the real deletion path and watched their
rows read "Deleted user". Checked at desktop and narrow widths, no new
console errors.
a
SET NULLFK to them, so every account removal silently erases authorshipacross the platform — including
audit_log.actor_id, which loses its actor.Soft-deleting users fixes all of them at once and removes any need for
per-table snapshots. It also carries a retention question of its own, since
keeping the row keeps the email.
the Agents list's "Accessible to" column still renders
-for a deletedauthor — but this PR changes only the column that was reported.
MCP gateway rejected this session's token), so this works from the
second-hand summary of it.