import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Trash2 } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { PortalMenu } from "@/client/components/PortalMenu"; import { CopyButton } from "@/client/features/ai-mcp/SetupControls"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { captureClientEvent } from "@/client/lib/posthog"; import { authClient } from "@/lib/auth-client"; // Better Auth rejects longer names with INVALID_NAME_LENGTH. const MAX_KEY_NAME_LENGTH = 32; export function ApiKeySettings() { const queryClient = useQueryClient(); const [isCreateOpen, setIsCreateOpen] = useState(false); const [name, setName] = useState(""); const [createdKey, setCreatedKey] = useState(null); const mcpUrl = typeof window === "undefined" ? "https://app.openseo.so/mcp" : `${window.location.origin}/mcp`; const apiKeysQuery = useQuery({ queryKey: ["apiKeys"], queryFn: async () => { const result = await authClient.apiKey.list(); if (result.error) { throw new Error(result.error.message ?? "Failed to load API keys"); } return result.data.apiKeys.map((key) => ({ id: key.id, name: key.name, start: key.start, createdAt: new Date(key.createdAt), lastRequest: key.lastRequest ? new Date(key.lastRequest) : null, })); }, }); const createMutation = useMutation({ mutationFn: async (keyName: string) => { const result = await authClient.apiKey.create({ name: keyName }); if (result.error || !result.data?.key) { throw new Error(result.error?.message ?? "Failed to create the key"); } return result.data.key; }, onSuccess: (key) => { setCreatedKey(key); setName(""); captureClientEvent("mcp:api_key_created"); void queryClient.invalidateQueries({ queryKey: ["apiKeys"] }); }, onError: (error) => { toast.error(getStandardErrorMessage(error)); }, }); const revokeMutation = useMutation({ mutationFn: async (keyId: string) => { const result = await authClient.apiKey.delete({ keyId }); if (result.error) { throw new Error(result.error.message ?? "Failed to revoke the key"); } }, onSuccess: () => { captureClientEvent("mcp:api_key_revoked"); toast.success("API key revoked"); void queryClient.invalidateQueries({ queryKey: ["apiKeys"] }); }, onError: (error) => { toast.error(getStandardErrorMessage(error)); }, }); const apiKeys = apiKeysQuery.data ?? []; const closeCreateModal = () => { setIsCreateOpen(false); setCreatedKey(null); setName(""); }; return (

API keys

Authenticate MCP clients when OAuth doesn't work

Use this for remote agents like Hermes where the normal login flow doesn't work.

Setup guide

{apiKeysQuery.isError ? (

We couldn't load your API keys.

) : apiKeys.length > 0 ? (
{apiKeys.map((key) => ( ))}
Name Key Created Last used
{key.name || "Unnamed key"} {key.start || "oseo_"}… {key.createdAt.toLocaleDateString()} {key.lastRequest ? key.lastRequest.toLocaleDateString() : "Never"} {(close) => (
  • )}
    ) : null} {isCreateOpen ? (
    {createdKey ? ( <>

    Copy your new API key

    It won't be shown again. Send it as{" "} Authorization: Bearer {" "} to {mcpUrl}.

    {createdKey}
    ) : (
    { event.preventDefault(); if (name.trim()) createMutation.mutate(name.trim()); }} >

    Create API key

    )}
    {/* No backdrop close on the reveal step: the key is shown once. */} {createdKey ? (
    ) : (
    )}
    ) : null}
    ); }