feat: surface the mcp endpoint and its scopes on the api keys page
This commit is contained in:
@@ -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 (
|
||||
<div className="flex flex-col overflow-hidden rounded border border-border bg-well sm:flex-row">
|
||||
<pre className="flex-1 overflow-x-auto p-3 font-mono text-xs text-text-primary">{value}</pre>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className="border-t border-border bg-surface-2 px-4 py-2.5 text-sm text-text-primary hover:bg-border sm:border-l sm:border-t-0"
|
||||
>
|
||||
{copied ? "Copied" : "Copy"}
|
||||
<span className="sr-only"> the {label}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="mt-8 rounded-lg border border-border bg-surface p-4 sm:p-5">
|
||||
<h2 className="text-base font-semibold text-text-primary">Agent access</h2>
|
||||
<p className="mt-1 max-w-[65ch] text-sm text-text-secondary">
|
||||
An LLM client can call this instance over MCP with an API key. The key needs <code className="font-mono text-xs">mcp:read</code>,
|
||||
plus <code className="font-mono text-xs">mcp:write</code> for tools that change anything, and its other scopes and tag restriction
|
||||
still decide what those tools can reach.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-3">
|
||||
<CopyLine label="endpoint" value={endpoint} />
|
||||
<CopyLine label="client configuration" value={config} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
</AsyncBoundary>
|
||||
</Card>
|
||||
|
||||
{hasMCP && <AgentAccessPanel />}
|
||||
|
||||
<ConfirmDialog
|
||||
open={revoking !== null}
|
||||
title="Revoke key"
|
||||
|
||||
Reference in New Issue
Block a user