From 2303c1f92d2f6cc346f97b834ef2a6560efed8fa Mon Sep 17 00:00:00 2001 From: stroblme Date: Sun, 16 Aug 2026 16:36:48 +0200 Subject: [PATCH] Show and revoke registered agents under Admin An agent that registered itself was invisible, and withdrawing one meant deleting rows or rotating the signing key. It now sits next to Users, with the live-token count that tells an approved agent from one that only registered. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A --- .../src/components/Admin/OAuthClients.tsx | 147 ++++++++++++++++++ frontend/src/routes/_layout/admin.tsx | 35 +++-- 2 files changed, 171 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/Admin/OAuthClients.tsx diff --git a/frontend/src/components/Admin/OAuthClients.tsx b/frontend/src/components/Admin/OAuthClients.tsx new file mode 100644 index 0000000..0ba2335 --- /dev/null +++ b/frontend/src/components/Admin/OAuthClients.tsx @@ -0,0 +1,147 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { Bot } from "lucide-react" +import { useState } from "react" + +import { OauthService, type RegisteredClient } from "@/client" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import useCustomToast from "@/hooks/useCustomToast" +import { handleError } from "@/utils" + +const clientsKey = ["oauth", "clients"] + +function when(value: string): string { + return new Date(value).toLocaleString() +} + +/** The agents that registered themselves for the MCP endpoint, and the way to + * withdraw one — the alternative being to rotate the signing key, which cuts + * off every agent at once. */ +export function OAuthClients() { + const { data } = useQuery({ + queryKey: clientsKey, + queryFn: () => OauthService.readClients(), + }) + const queryClient = useQueryClient() + const { showSuccessToast, showErrorToast } = useCustomToast() + const [pending, setPending] = useState(null) + + const revoke = useMutation({ + mutationFn: (clientId: string) => OauthService.revokeClient({ clientId }), + onSuccess: (message) => { + showSuccessToast(message.message) + setPending(null) + }, + onError: handleError.bind(showErrorToast), + onSettled: () => { + queryClient.invalidateQueries({ queryKey: clientsKey }) + }, + }) + + const clients = data?.data ?? [] + + return ( +
+
+

Agents

+

+ Clients that registered themselves to reach the MCP endpoint. Each one + still needed a person to approve it before it could do anything. +

+
+ + {clients.length === 0 ? ( +

+ Nothing has registered yet. An agent adds itself here the first time + it asks for access. +

+ ) : ( +
+ {clients.map((client) => ( +
+
+ + + {client.client_name} + + + {client.redirect_uris.join(" ")} + + + Registered {when(client.created_at)} + {client.last_authorized_at + ? ` · approved ${when(client.last_authorized_at)}` + : ""} + +
+
+ 0 ? "default" : "secondary"} + > + {client.active_tokens > 0 + ? `${client.active_tokens} live token${ + client.active_tokens === 1 ? "" : "s" + }` + : "Never approved"} + + +
+
+ ))} +
+ )} + + !open && setPending(null)} + > + + + Revoke '{pending?.client_name}'? + + It loses its refresh tokens, so it can get nothing new and has to + be approved again from scratch. An access token already in its + hands keeps working until it expires. Every other agent is left + alone. + + + + + + + + +
+ ) +} + +export default OAuthClients diff --git a/frontend/src/routes/_layout/admin.tsx b/frontend/src/routes/_layout/admin.tsx index 1c64be9..69c34b5 100644 --- a/frontend/src/routes/_layout/admin.tsx +++ b/frontend/src/routes/_layout/admin.tsx @@ -5,8 +5,10 @@ import { Suspense } from "react" import { type UserPublic, UsersService } from "@/client" import AddUser from "@/components/Admin/AddUser" import { columns, type UserTableData } from "@/components/Admin/columns" +import OAuthClients from "@/components/Admin/OAuthClients" import { DataTable } from "@/components/Common/DataTable" import PendingUsers from "@/components/Pending/PendingUsers" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import useAuth from "@/hooks/useAuth" function getUsersQueryOptions() { @@ -56,18 +58,29 @@ function UsersTable() { } function Admin() { + // Agents get in through the same door as people, so they are administered + // next to them rather than in a screen of their own. return ( -
-
-
-

Users

-

- Manage user accounts and permissions -

+ + + Users + Agents + + +
+
+

Users

+

+ Manage user accounts and permissions +

+
+
- -
- -
+ + + + + + ) }