From e5b9894384156386c30906f33b97680484fbde64 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 8 Sep 2026 14:14:44 +0000 Subject: [PATCH] feat: surface the mcp endpoint and its scopes on the api keys page --- web/components/apikeys/AgentAccessPanel.tsx | 68 +++++++++++++++++++++ web/components/apikeys/ApiKeysPanel.tsx | 14 ++++- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 web/components/apikeys/AgentAccessPanel.tsx diff --git a/web/components/apikeys/AgentAccessPanel.tsx b/web/components/apikeys/AgentAccessPanel.tsx new file mode 100644 index 0000000..6f236e6 --- /dev/null +++ b/web/components/apikeys/AgentAccessPanel.tsx @@ -0,0 +1,68 @@ +import { useState } from "react"; + +/* + * Everything needed to point an LLM client at this instance, on the page where + * the credential it needs is minted. The endpoint is licence-gated + * (RequireFeature(license.FeatureMCP)), so the panel only exists where the + * connection would actually work. + * + * Styled as a well rather than a card: this is machine output being handed to + * the operator, the same treatment the install one-liner gets on /servers/new. + */ +function CopyLine({ label, value }: { label: string; value: string }) { + const [copied, setCopied] = useState(false); + + async function copy() { + await navigator.clipboard.writeText(value); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + + return ( +
+
{value}
+ +
+ ); +} + +export function AgentAccessPanel() { + const origin = typeof window === "undefined" ? "https://YOUR-INSTANCE" : window.location.origin; + const endpoint = `${origin}/api/mcp`; + const config = JSON.stringify( + { + mcpServers: { + vantage: { + type: "http", + url: endpoint, + headers: { Authorization: "Bearer vt_your_key_here" }, + }, + }, + }, + null, + 2, + ); + + return ( +
+

Agent access

+

+ An LLM client can call this instance over MCP with an API key. The key needs mcp:read, + plus mcp:write for tools that change anything, and its other scopes and tag restriction + still decide what those tools can reach. +

+ +
+ + +
+
+ ); +} diff --git a/web/components/apikeys/ApiKeysPanel.tsx b/web/components/apikeys/ApiKeysPanel.tsx index 6e64647..e22360b 100644 --- a/web/components/apikeys/ApiKeysPanel.tsx +++ b/web/components/apikeys/ApiKeysPanel.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { api, type ApiToken, type Role } from "@/lib/api"; import { useAuth } from "@/components/AuthProvider"; +import { useLicense } from "@/lib/useLicense"; import { AsyncBoundary, Button, @@ -15,6 +16,7 @@ import { } from "@/components/ui"; import { KeyLedger, LedgerSkeleton } from "./KeyLedger"; import { KeyPosture } from "./KeyPosture"; +import { AgentAccessPanel } from "./AgentAccessPanel"; import { CreateKeyDialog, EXPIRY_OPTIONS } from "./CreateKeyDialog"; const ROLES: Role[] = ["owner", "admin", "member"]; @@ -40,6 +42,11 @@ export function ApiKeysPanel() { const queryClient = useQueryClient(); const { user, isAdmin } = useAuth(); const toast = useToast(); + // Strict rather than useLicense's optimistic hasFeature: both consumers + // below hide rather than disable, and a panel that appears and then + // vanishes once the licence loads reads as a glitch. + const { license } = useLicense(); + const hasMCP = Boolean(license?.features?.mcp); const [showAll, setShowAll] = useState(false); const [createOpen, setCreateOpen] = useState(false); @@ -65,7 +72,10 @@ export function ApiKeysPanel() { const { data: scopesData } = useQuery({ queryKey: ["token-scopes"], queryFn: api.listTokenScopes, enabled: createOpen }); const availableScopes = scopesData?.scopes ?? []; - const resources = Array.from(new Set(availableScopes.map((s) => s.split(":")[0]))); + // The scope vocabulary is the server's, but mcp:* is unreachable without + // the licence feature, and offering a grant that cannot be used is a + // support ticket waiting to happen. + const resources = Array.from(new Set(availableScopes.map((s) => s.split(":")[0]))).filter((r) => r !== "mcp" || hasMCP); const invalidate = () => queryClient.invalidateQueries({ queryKey: ["api-tokens"] }); @@ -216,6 +226,8 @@ export function ApiKeysPanel() { + {hasMCP && } +