feat(frontend): offer the MCP tools that change the library, with a confirm

They're marked as changing the library, and turning one on asks first.
This commit is contained in:
Rui Valim committed 2026-10-20 14:54:42 -03:00
1 parent a36a99639a
commit 699fa8a1c5
3 files changed
+56 -6

No files matched your search

+20 -1
View File
@@ -246,8 +246,27 @@
"library_stats": {
"title": "Library stats",
"description": "Your totals, most viewed, most printed and newest models."
},
"get_import_status": {
"title": "Check an import",
"description": "Follow an import that's still running."
},
"import_model": {
"title": "Import a model",
"description": "Import a model from a link, as + Add > Import does."
},
"create_collection": {
"title": "Create a collection",
"description": "Make a new, empty collection."
},
"add_to_collection": {
"title": "Add to a collection",
"description": "Put models from your library in one of your collections."
}
}
},
"writeConfirmTitle": "Let the assistant use “{{tool}}”?",
"writeConfirmMessage": "This tool changes your library. An assistant connected with this token will be able to use it without asking you each time. Nothing it does deletes models.",
"writeConfirm": "Turn on"
}
},
"language": {
+20 -1
View File
@@ -247,8 +247,27 @@
"library_stats": {
"title": "Bibliotekos statistika",
"description": "Jūsų suvestinė, daugiausiai peržiūrėti, spausdinti ir naujausi modeliai."
},
"get_import_status": {
"title": "Importo būsena",
"description": "Sekti importą, kuris dar vyksta."
},
"import_model": {
"title": "Importuoti modelį",
"description": "Importuoti modelį iš nuorodos, kaip + Pridėti > Importuoti."
},
"create_collection": {
"title": "Sukurti kolekciją",
"description": "Sukurti naują, tuščią kolekciją."
},
"add_to_collection": {
"title": "Įtraukti į kolekciją",
"description": "Įdėti bibliotekos modelius į vieną iš jūsų kolekcijų."
}
}
},
"writeConfirmTitle": "Leisti asistentui naudoti „{{tool}}“?",
"writeConfirmMessage": "Šis įrankis keičia jūsų biblioteką. Su šiuo raktu prijungtas asistentas galės jį naudoti kiekvieną kartą jūsų neklausdamas. Jis niekada netrina modelių.",
"writeConfirm": "Įjungti"
}
},
"language": {
@@ -9,6 +9,7 @@ import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { authApi, type ApiTokenInfo, type McpToolInfo } from "../../api/auth";
import { UnauthorizedError } from "../../api/client";
import { useConfirm } from "../../components/ConfirmProvider";
type Props = {
info: ApiTokenInfo;
@@ -20,6 +21,7 @@ type Props = {
* unsaved state to lose; a refused save puts the box back. */
export default function ApiTokenMcpTools({ info, onChange, onUnauthorized }: Props) {
const { t, i18n } = useTranslation(["app", "common"]);
const confirm = useConfirm();
const [saving, setSaving] = React.useState<string | null>(null);
const [error, setError] = React.useState<string | null>(null);
// Ahead of the server while a save is in flight; back in step with it once the save answers.
@@ -33,10 +35,20 @@ export default function ApiTokenMcpTools({ info, onChange, onUnauthorized }: Pro
return i18n.exists(key, { ns: "app" }) ? t(key) : tool[part];
};
const toggle = async (name: string, on: boolean) => {
const toggle = async (tool: McpToolInfo, on: boolean) => {
const name = tool.name;
// Reading needs no second thought; letting an assistant change the library does.
if (on && !tool.read_only) {
const ok = await confirm({
title: t("profile.apiToken.mcp.writeConfirmTitle", { tool: label(tool, "title") }),
message: t("profile.apiToken.mcp.writeConfirmMessage"),
confirmLabel: t("profile.apiToken.mcp.writeConfirm"),
});
if (!ok) return;
}
const next = info.available_mcp_tools
.map((tool) => tool.name)
.filter((tool) => (tool === name ? on : enabled.has(tool)));
.map((available) => available.name)
.filter((other) => (other === name ? on : enabled.has(other)));
setSelected(next);
setSaving(name);
setError(null);
@@ -70,7 +82,7 @@ export default function ApiTokenMcpTools({ info, onChange, onUnauthorized }: Pro
sx={{ pt: 0.25 }}
checked={enabled.has(tool.name)}
disabled={saving !== null}
onChange={(e) => void toggle(tool.name, e.target.checked)}
onChange={(e) => void toggle(tool, e.target.checked)}
/>
}
label={