From cc5aa030cc4a5f2b4b2a2cc764da2e34492eb46e Mon Sep 17 00:00:00 2001 From: Tautvydas Derzinskas Date: Wed, 21 Oct 2026 22:01:14 +0200 Subject: [PATCH] feat: show how to connect AI assistants over MCP The admin MCP section, now "MCP tools" with a clearer subtitle, gets an animated tile picturing the access level (link cut, reads flowing to the assistant, changes flowing back) and a How to connect button. The same button sits at the top of a token's MCP permissions, in place of the help icon there. How to connect opens a guide with this instance's address filled in: Claude Code, Claude's custom connectors, ChatGPT's developer mode connectors, Codex CLI, and Cursor and VS Code configs, each with a copy button. The copyable box moves out of What's new into a shared CodeBlock. --- docs/guides/mcp.md | 2 +- frontend/src/components/CodeBlock.tsx | 79 ++++++++ .../authAnimations/McpLinkAnimation.tsx | 177 ++++++++++++++++++ .../src/components/mcp/McpConnectDialog.tsx | 169 +++++++++++++++++ .../components/whatsNew/WhatsNewDialog.tsx | 75 +------- frontend/src/i18n/locales/en.json | 46 ++++- frontend/src/i18n/locales/es.json | 46 ++++- frontend/src/i18n/locales/fr.json | 46 ++++- frontend/src/i18n/locales/lt.json | 46 ++++- .../pages/AdminSettingsPage/McpSection.tsx | 56 ++++-- .../ProfilePage/McpPermissionsSection.tsx | 14 +- 11 files changed, 641 insertions(+), 115 deletions(-) create mode 100644 frontend/src/components/CodeBlock.tsx create mode 100644 frontend/src/components/authAnimations/McpLinkAnimation.tsx create mode 100644 frontend/src/components/mcp/McpConnectDialog.tsx diff --git a/docs/guides/mcp.md b/docs/guides/mcp.md index ce1d024..6297bad 100644 --- a/docs/guides/mcp.md +++ b/docs/guides/mcp.md @@ -75,7 +75,7 @@ changes it, that change is kept. ## What an admin decides -On the admin **Auth** page, **AI assistants (MCP)** sets what every API token on the instance may do over MCP: +On the admin **Auth** page, **MCP tools** sets what every API token on the instance may do over MCP: - **Read and change** (the default): each user picks their token's tools, including the ones that change the library. - **Read only**: only the tools that read are offered or answered. A tool someone already allowed stays allowed, and works diff --git a/frontend/src/components/CodeBlock.tsx b/frontend/src/components/CodeBlock.tsx new file mode 100644 index 0000000..92ab1b9 --- /dev/null +++ b/frontend/src/components/CodeBlock.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import Box from "@mui/material/Box"; +import IconButton from "@mui/material/IconButton"; +import Tooltip from "@mui/material/Tooltip"; +import CheckIcon from "@mui/icons-material/Check"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; + +/** A command or config to copy, in monospace, with a copy button. */ +export default function CodeBlock({ code }: { code: string }) { + const { t } = useTranslation("app"); + const box = React.useRef(null); + const [copyState, setCopyState] = React.useState<"idle" | "copied" | "failed">("idle"); + React.useEffect(() => { + if (copyState === "idle") return; + const timer = window.setTimeout(() => setCopyState("idle"), copyState === "failed" ? 4000 : 1500); + return () => window.clearTimeout(timer); + }, [copyState]); + // The clipboard API only exists on HTTPS or localhost, and self-hosted instances are often plain HTTP on a LAN + // address; there the command is selected, to copy by hand. + const copy = async () => { + try { + await navigator.clipboard.writeText(code); + setCopyState("copied"); + } catch { + if (box.current) window.getSelection()?.selectAllChildren(box.current); + setCopyState("failed"); + } + }; + const copyLabel = + copyState === "failed" + ? t("codeBlock.copyFailed") + : copyState === "copied" + ? t("codeBlock.copied") + : t("codeBlock.copy"); + return ( + + + {code} + + + void copy()} + sx={{ position: "absolute", top: 4, right: 4 }} + > + {copyState === "copied" ? ( + + ) : ( + + )} + + + + ); +} diff --git a/frontend/src/components/authAnimations/McpLinkAnimation.tsx b/frontend/src/components/authAnimations/McpLinkAnimation.tsx new file mode 100644 index 0000000..5759de5 --- /dev/null +++ b/frontend/src/components/authAnimations/McpLinkAnimation.tsx @@ -0,0 +1,177 @@ +import { keyframes } from "@emotion/react"; +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import type { McpAccess } from "../../api/settings"; +import { prefersReducedMotion } from "../storageAnimations/parts"; + +// An assistant on the left, the library on the right, and the MCP link between them: cut when off, data +// flowing to the assistant when it may read, and back to the library too when it may change it. +const CYCLE_S = 1.8; +const SLATE = "#334155"; +const TRACK = "#cbd5e1"; +const WRITE = "#f59e0b"; + +const toAssistant = keyframes` + 0% { transform: translateX(var(--span)); opacity: 0; } + 15%, 85% { opacity: 1; } + 100% { transform: translateX(0); opacity: 0; } +`; +const toLibrary = keyframes` + 0% { transform: translateX(0); opacity: 0; } + 15%, 85% { opacity: 1; } + 100% { transform: translateX(var(--span)); opacity: 0; } +`; +const blink = keyframes` + 0%, 92%, 100% { transform: scaleY(1); } + 95% { transform: scaleY(0.15); } +`; + +export default function McpLinkAnimation({ size, access }: { size: number; access: McpAccess }) { + const theme = useTheme(); + const still = prefersReducedMotion(); + const accent = theme.palette.primary.main; + const off = access === "off"; + const u = size / 76; + const trackLeft = 27 * u; + const trackWidth = 21 * u; + const dot = 6 * u; + const midY = 38 * u; + const fade = { transition: "opacity 220ms ease-out, filter 220ms ease-out" }; + + const packet = (color: string, frames: ReturnType, delay: number, startAtEnd: boolean) => ( + + ); + + return ( + + {/* The assistant: a robot head with an antenna. */} + + + + + {[6, 14].map((left) => ( + + ))} + + + + {/* The link: solid with data on it, or dashed and cut. */} + + {off ? ( + + ) : ( + <> + {packet(accent, toAssistant, 0, true)} + {packet(accent, toAssistant, CYCLE_S / 2, true)} + {access === "write" && packet(WRITE, toLibrary, CYCLE_S / 4, false)} + + )} + + {/* The library: three models stacked. */} + + {[0, 1, 2].map((i) => ( + + ))} + + + ); +} diff --git a/frontend/src/components/mcp/McpConnectDialog.tsx b/frontend/src/components/mcp/McpConnectDialog.tsx new file mode 100644 index 0000000..96678ba --- /dev/null +++ b/frontend/src/components/mcp/McpConnectDialog.tsx @@ -0,0 +1,169 @@ +import React from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { keyframes } from "@emotion/react"; +import Button from "@mui/material/Button"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import IconButton from "@mui/material/IconButton"; +import Link from "@mui/material/Link"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import CloseIcon from "@mui/icons-material/Close"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; +import CodeBlock from "../CodeBlock"; +import Segmented from "../controls/Segmented"; + +type Client = "claudeCode" | "claude" | "chatgpt" | "codex" | "editors"; +const CLIENTS: Client[] = ["claudeCode", "claude", "chatgpt", "codex", "editors"]; +const TOKEN = "tp_your_token"; +const GUIDE_URL = "https://github.com/TautvydasDerzinskas/Thingport/blob/main/docs/guides/mcp.md"; + +const fadeIn = keyframes` + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: none; } +`; + +/** The commands and settings each client needs, with this instance's address filled in. */ +function snippets(endpoint: string) { + return { + claudeCode: `claude mcp add --transport http thingport ${endpoint} --header "Authorization: Bearer ${TOKEN}"`, + remote: `${endpoint}\nAuthorization: Bearer ${TOKEN}`, + codex: `export THINGPORT_TOKEN=${TOKEN}\ncodex mcp add thingport --url ${endpoint} --bearer-token-env-var THINGPORT_TOKEN`, + cursor: JSON.stringify( + { mcpServers: { thingport: { url: endpoint, headers: { Authorization: `Bearer ${TOKEN}` } } } }, + null, + 2, + ), + vscode: JSON.stringify( + { servers: { thingport: { type: "http", url: endpoint, headers: { Authorization: `Bearer ${TOKEN}` } } } }, + null, + 2, + ), + }; +} + +function Step({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +function Note({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +type Props = { open: boolean; onClose: () => void }; + +/** How to point Claude, ChatGPT, Codex and editors at this instance's /api/mcp with an API token. */ +export default function McpConnectDialog({ open, onClose }: Props) { + const { t } = useTranslation("app"); + const [client, setClient] = React.useState("claudeCode"); + const endpoint = `${window.location.origin}/api/mcp`; + const code = snippets(endpoint); + const k = (key: string) => `mcpConnect.${key}`; + const strong = { strong: }; + + return ( + + + {t(k("title"))} + + + + + + + + }} /> + + + label={t(k("clientLabel"))} + value={client} + onChange={setClient} + options={CLIENTS.map((value) => ({ value, label: t(k(`clients.${value}`)) }))} + /> + + {client === "claudeCode" && ( + <> + {t(k("claudeCode.step"))} + + + )} + {client === "claude" && ( + <> + + + + + {t(k("claude.note"))} + + )} + {client === "chatgpt" && ( + <> + + + + + {t(k("chatgpt.note"))} + + )} + {client === "codex" && ( + <> + {t(k("codex.step"))} + + {t(k("codex.note"))} + + )} + {client === "editors" && ( + <> + + }} /> + + + + }} /> + + + {t(k("editors.note"))} + + )} + + + + + + {t(k("guide"))} + + + + + + ); +} diff --git a/frontend/src/components/whatsNew/WhatsNewDialog.tsx b/frontend/src/components/whatsNew/WhatsNewDialog.tsx index ea299b2..c5fed93 100644 --- a/frontend/src/components/whatsNew/WhatsNewDialog.tsx +++ b/frontend/src/components/whatsNew/WhatsNewDialog.tsx @@ -8,19 +8,17 @@ import Dialog from "@mui/material/Dialog"; import IconButton from "@mui/material/IconButton"; import Link from "@mui/material/Link"; import Stack from "@mui/material/Stack"; -import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import { alpha, useTheme, type Theme } from "@mui/material/styles"; import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined"; import BoltOutlinedIcon from "@mui/icons-material/BoltOutlined"; import BuildOutlinedIcon from "@mui/icons-material/BuildOutlined"; -import CheckIcon from "@mui/icons-material/Check"; import CloseIcon from "@mui/icons-material/Close"; -import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import NewReleasesOutlinedIcon from "@mui/icons-material/NewReleasesOutlined"; import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded"; import { releaseNotesApi, type ReleaseNoteItem, type ReleaseNoteKind, type ReleaseNotes } from "../../api/releaseNotes"; +import CodeBlock from "../CodeBlock"; import { ListRowSkeletons } from "../ListSkeleton"; export type WhatsNewRequest = { @@ -116,77 +114,6 @@ function Screenshots({ ); } -function CodeBlock({ code }: { code: string }) { - const { t } = useTranslation("app"); - const box = React.useRef(null); - const [copyState, setCopyState] = React.useState<"idle" | "copied" | "failed">("idle"); - React.useEffect(() => { - if (copyState === "idle") return; - const timer = window.setTimeout(() => setCopyState("idle"), copyState === "failed" ? 4000 : 1500); - return () => window.clearTimeout(timer); - }, [copyState]); - // The clipboard API only exists on HTTPS or localhost, and self-hosted instances are often plain HTTP on a LAN - // address; there the command is selected, to copy by hand. - const copy = async () => { - try { - await navigator.clipboard.writeText(code); - setCopyState("copied"); - } catch { - if (box.current) window.getSelection()?.selectAllChildren(box.current); - setCopyState("failed"); - } - }; - const copyLabel = - copyState === "failed" - ? t("whatsNew.copyFailed") - : copyState === "copied" - ? t("whatsNew.copied") - : t("whatsNew.copy"); - return ( - - - {code} - - - void copy()} - sx={{ position: "absolute", top: 4, right: 4 }} - > - {copyState === "copied" ? ( - - ) : ( - - )} - - - - ); -} - /** A release's headline change, above the grouped list. */ function HighlightCard({ item, diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index a26bf04..92aa38e 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -207,7 +207,6 @@ "mcp": { "heading": "MCP", "subtitle": "Tools an AI assistant may use", - "description": "What an AI assistant connected with this token (over MCP, at /api/mcp) may use. Changes apply straight away, without a new token.", "connect": "To connect an AI assistant, for example Claude Code:", "writes": "Changes your library", "failed": "Couldn't save the token's MCP tools. Please try again.", @@ -1663,9 +1662,9 @@ "failed": "Couldn't save the public URL." }, "mcp": { - "heading": "AI assistants (MCP)", - "subtitle": "What API tokens may do at /api/mcp.", - "accessLabel": "MCP access", + "heading": "MCP tools", + "subtitle": "Let AI assistants such as Claude search and manage the library with their user's API token.", + "accessLabel": "What API tokens may do", "help": "Each user still chooses their own token's tools within this. Tools that change the library import without a captcha, as Thingport Grab does.", "failed": "Couldn't save the MCP access. Please try again.", "levels": { @@ -2195,10 +2194,47 @@ "empty": "No release notes for this version yet.", "openGithub": "Open on GitHub", "zoom": "Enlarge screenshot", - "highlight": "Highlight", + "highlight": "Highlight" + }, + "codeBlock": { "copy": "Copy", "copied": "Copied", "copyFailed": "Couldn't copy here. It's selected: press Ctrl+C or ⌘C." + }, + "mcpConnect": { + "open": "How to connect", + "title": "Connect an AI assistant", + "intro": "Each person connects with their own API token, made in Profile → API token. Put it in place of {{token}}.", + "clientLabel": "AI assistant", + "clients": { + "claudeCode": "Claude Code", + "claude": "Claude", + "chatgpt": "ChatGPT", + "codex": "Codex CLI", + "editors": "Cursor & VS Code" + }, + "claudeCode": { + "step": "Run this in a terminal:" + }, + "claude": { + "step": "In Claude, open Settings → Connectors, choose Add custom connector and enter this address. Under request headers, add this Authorization header:", + "note": "Claude connects from Anthropic's servers, so Thingport must be reachable from the internet over HTTPS. Request headers are in beta and may depend on your plan." + }, + "chatgpt": { + "step": "In ChatGPT, turn on Developer mode under Settings → Apps & Connectors, create a connector with this address, and choose Token as its authentication:", + "note": "ChatGPT connects from OpenAI's servers, so Thingport must be reachable from the internet over HTTPS. The Token option depends on your plan; without it, use Codex CLI." + }, + "codex": { + "step": "Run this in a terminal:", + "note": "THINGPORT_TOKEN must be set in the shell that starts Codex." + }, + "editors": { + "cursor": "Cursor: add this to ~/.cursor/mcp.json.", + "vscode": "VS Code: add this to .vscode/mcp.json in your workspace.", + "note": "Other clients work the same way if they support Streamable HTTP and let you set a header." + }, + "guide": "MCP guide", + "close": "Close" } }, "common": { diff --git a/frontend/src/i18n/locales/es.json b/frontend/src/i18n/locales/es.json index c10e284..ccfb863 100644 --- a/frontend/src/i18n/locales/es.json +++ b/frontend/src/i18n/locales/es.json @@ -210,7 +210,6 @@ "copyCommand": "Copiar comando", "heading": "MCP", "subtitle": "Herramientas que puede usar un asistente de IA", - "description": "Lo que puede usar un asistente de IA conectado con este token (por MCP, en /api/mcp). Los cambios se aplican al instante, sin un token nuevo.", "connect": "Para conectar un asistente de IA, por ejemplo Claude Code:", "writes": "Cambia tu biblioteca", "failed": "No se pudieron guardar las herramientas MCP del token. Inténtalo de nuevo.", @@ -1663,9 +1662,9 @@ "failed": "No se pudo guardar la URL pública." }, "mcp": { - "heading": "Asistentes de IA (MCP)", - "subtitle": "Qué pueden hacer los tokens de API en /api/mcp.", - "accessLabel": "Acceso MCP", + "heading": "Herramientas MCP", + "subtitle": "Permite que asistentes de IA como Claude busquen en la biblioteca y la gestionen con el token de API de su usuario.", + "accessLabel": "Qué pueden hacer los tokens de API", "help": "Cada usuario sigue eligiendo las herramientas de su propio token dentro de este límite. Las herramientas que cambian la biblioteca importan sin captcha, como Thingport Grab.", "failed": "No se pudo guardar el acceso MCP. Inténtalo de nuevo.", "levels": { @@ -2195,10 +2194,47 @@ "empty": "Aún no hay notas para esta versión.", "openGithub": "Abrir en GitHub", "zoom": "Ampliar captura", - "highlight": "Destacado", + "highlight": "Destacado" + }, + "codeBlock": { "copy": "Copiar", "copied": "Copiado", "copyFailed": "No se pudo copiar aquí. Está seleccionado: pulsa Ctrl+C o ⌘C." + }, + "mcpConnect": { + "open": "Cómo conectar", + "title": "Conecta un asistente de IA", + "intro": "Cada persona se conecta con su propio token de API, creado en Perfil → Token de API. Ponlo en lugar de {{token}}.", + "clientLabel": "Asistente de IA", + "clients": { + "claudeCode": "Claude Code", + "claude": "Claude", + "chatgpt": "ChatGPT", + "codex": "Codex CLI", + "editors": "Cursor y VS Code" + }, + "claudeCode": { + "step": "Ejecuta esto en una terminal:" + }, + "claude": { + "step": "En Claude, abre Settings → Connectors, elige Add custom connector e introduce esta dirección. En los encabezados de la solicitud, añade este encabezado Authorization:", + "note": "Claude se conecta desde los servidores de Anthropic, así que Thingport debe ser accesible desde internet por HTTPS. Los encabezados de solicitud están en beta y pueden depender de tu plan." + }, + "chatgpt": { + "step": "En ChatGPT, activa el Developer mode en Settings → Apps & Connectors, crea un conector con esta dirección y elige Token como autenticación:", + "note": "ChatGPT se conecta desde los servidores de OpenAI, así que Thingport debe ser accesible desde internet por HTTPS. La opción Token depende de tu plan; si no la tienes, usa Codex CLI." + }, + "codex": { + "step": "Ejecuta esto en una terminal:", + "note": "THINGPORT_TOKEN debe estar definido en la terminal que inicia Codex." + }, + "editors": { + "cursor": "Cursor: añade esto a ~/.cursor/mcp.json.", + "vscode": "VS Code: añade esto a .vscode/mcp.json en tu espacio de trabajo.", + "note": "Otros clientes funcionan igual si admiten Streamable HTTP y permiten definir un encabezado." + }, + "guide": "Guía de MCP", + "close": "Cerrar" } }, "common": { diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 68b44a4..1e2b506 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -210,7 +210,6 @@ "copyCommand": "Copier la commande", "heading": "MCP", "subtitle": "Les outils qu'un assistant IA peut utiliser", - "description": "Ce qu'un assistant IA connecté avec ce jeton (via MCP, sur /api/mcp) peut utiliser. Les changements s'appliquent aussitôt, sans nouveau jeton.", "connect": "Pour connecter un assistant IA, par exemple Claude Code :", "writes": "Modifie votre bibliothèque", "failed": "Impossible d'enregistrer les outils MCP du jeton. Veuillez réessayer.", @@ -1663,9 +1662,9 @@ "failed": "Impossible d’enregistrer l’URL publique." }, "mcp": { - "heading": "Assistants IA (MCP)", - "subtitle": "Ce que les jetons d'API peuvent faire sur /api/mcp.", - "accessLabel": "Accès MCP", + "heading": "Outils MCP", + "subtitle": "Laissez des assistants IA comme Claude chercher dans la bibliothèque et la gérer avec le jeton d’API de leur utilisateur.", + "accessLabel": "Ce que les jetons d’API peuvent faire", "help": "Chaque utilisateur choisit toujours les outils de son propre jeton dans cette limite. Les outils qui modifient la bibliothèque importent sans captcha, comme Thingport Grab.", "failed": "Impossible d'enregistrer l'accès MCP. Veuillez réessayer.", "levels": { @@ -2208,10 +2207,47 @@ "empty": "Pas encore de notes pour cette version.", "openGithub": "Ouvrir sur GitHub", "zoom": "Agrandir la capture", - "highlight": "À la une", + "highlight": "À la une" + }, + "codeBlock": { "copy": "Copier", "copied": "Copié", "copyFailed": "Copie impossible ici. Le texte est sélectionné : appuyez sur Ctrl+C ou ⌘C." + }, + "mcpConnect": { + "open": "Comment se connecter", + "title": "Connecter un assistant IA", + "intro": "Chacun se connecte avec son propre jeton d’API, créé dans Profil → Jeton d’API. Mettez-le à la place de {{token}}.", + "clientLabel": "Assistant IA", + "clients": { + "claudeCode": "Claude Code", + "claude": "Claude", + "chatgpt": "ChatGPT", + "codex": "Codex CLI", + "editors": "Cursor et VS Code" + }, + "claudeCode": { + "step": "Lancez ceci dans un terminal :" + }, + "claude": { + "step": "Dans Claude, ouvrez Settings → Connectors, choisissez Add custom connector et saisissez cette adresse. Dans les en-têtes de requête, ajoutez cet en-tête Authorization :", + "note": "Claude se connecte depuis les serveurs d’Anthropic : Thingport doit donc être accessible depuis internet en HTTPS. Les en-têtes de requête sont en bêta et peuvent dépendre de votre offre." + }, + "chatgpt": { + "step": "Dans ChatGPT, activez le Developer mode dans Settings → Apps & Connectors, créez un connecteur avec cette adresse et choisissez Token comme authentification :", + "note": "ChatGPT se connecte depuis les serveurs d’OpenAI : Thingport doit donc être accessible depuis internet en HTTPS. L’option Token dépend de votre offre ; sans elle, utilisez Codex CLI." + }, + "codex": { + "step": "Lancez ceci dans un terminal :", + "note": "THINGPORT_TOKEN doit être défini dans le shell qui lance Codex." + }, + "editors": { + "cursor": "Cursor : ajoutez ceci à ~/.cursor/mcp.json.", + "vscode": "VS Code : ajoutez ceci à .vscode/mcp.json dans votre espace de travail.", + "note": "Les autres clients fonctionnent de la même façon s’ils prennent en charge Streamable HTTP et permettent de définir un en-tête." + }, + "guide": "Guide MCP", + "close": "Fermer" } }, "common": { diff --git a/frontend/src/i18n/locales/lt.json b/frontend/src/i18n/locales/lt.json index f3b8a2c..0e00c6c 100644 --- a/frontend/src/i18n/locales/lt.json +++ b/frontend/src/i18n/locales/lt.json @@ -208,7 +208,6 @@ "mcp": { "heading": "MCP", "subtitle": "Įrankiai, kuriuos gali naudoti DI asistentas", - "description": "Ką gali naudoti DI asistentas, prijungtas su šiuo raktu (per MCP, adresu /api/mcp). Pakeitimai įsigalioja iš karto, naujo rakto nereikia.", "connect": "Kad prijungtumėte DI asistentą, pavyzdžiui Claude Code:", "writes": "Keičia biblioteką", "failed": "Nepavyko išsaugoti rakto MCP įrankių. Bandykite dar kartą.", @@ -1704,9 +1703,9 @@ "failed": "Nepavyko išsaugoti viešo adreso." }, "mcp": { - "heading": "DI asistentai (MCP)", - "subtitle": "Ką API raktai gali daryti per /api/mcp.", - "accessLabel": "MCP prieiga", + "heading": "MCP įrankiai", + "subtitle": "Leiskite DI asistentams, pvz., Claude, ieškoti bibliotekoje ir ją tvarkyti naudotojo API raktu.", + "accessLabel": "Ką gali API raktai", "help": "Kiekvienas naudotojas vis tiek pats pasirenka savo rakto įrankius šiose ribose. Biblioteką keičiantys įrankiai importuoja be captcha, kaip ir Thingport Grab.", "failed": "Nepavyko išsaugoti MCP prieigos. Bandykite dar kartą.", "levels": { @@ -2261,10 +2260,47 @@ "empty": "Šiai versijai naujienų dar nėra.", "openGithub": "Atidaryti GitHub", "zoom": "Padidinti ekrano vaizdą", - "highlight": "Svarbiausia", + "highlight": "Svarbiausia" + }, + "codeBlock": { "copy": "Kopijuoti", "copied": "Nukopijuota", "copyFailed": "Čia nepavyko nukopijuoti. Tekstas pažymėtas: spauskite Ctrl+C arba ⌘C." + }, + "mcpConnect": { + "open": "Kaip prijungti", + "title": "Prijunkite DI asistentą", + "intro": "Kiekvienas jungiasi savo API raktu, sukurtu Profilis → API raktas. Įrašykite jį vietoj {{token}}.", + "clientLabel": "DI asistentas", + "clients": { + "claudeCode": "Claude Code", + "claude": "Claude", + "chatgpt": "ChatGPT", + "codex": "Codex CLI", + "editors": "Cursor ir VS Code" + }, + "claudeCode": { + "step": "Paleiskite terminale:" + }, + "claude": { + "step": "Claude atidarykite Settings → Connectors, pasirinkite Add custom connector ir įveskite šį adresą. Prie užklausos antraščių pridėkite šią Authorization antraštę:", + "note": "Claude jungiasi iš Anthropic serverių, todėl Thingport turi būti pasiekiamas internetu per HTTPS. Užklausos antraštės yra beta versijos ir gali priklausyti nuo plano." + }, + "chatgpt": { + "step": "ChatGPT įjunkite Developer mode skiltyje Settings → Apps & Connectors, sukurkite jungtį su šiuo adresu ir autentifikavimui pasirinkite Token:", + "note": "ChatGPT jungiasi iš OpenAI serverių, todėl Thingport turi būti pasiekiamas internetu per HTTPS. Token parinktis priklauso nuo plano; jei jos nėra, naudokite Codex CLI." + }, + "codex": { + "step": "Paleiskite terminale:", + "note": "THINGPORT_TOKEN turi būti nustatytas apvalkale, iš kurio paleidžiamas Codex." + }, + "editors": { + "cursor": "Cursor: pridėkite tai į ~/.cursor/mcp.json.", + "vscode": "VS Code: pridėkite tai į darbo srities .vscode/mcp.json.", + "note": "Kiti klientai veikia taip pat, jei palaiko Streamable HTTP ir leidžia nustatyti antraštę." + }, + "guide": "MCP vadovas", + "close": "Uždaryti" } }, "common": { diff --git a/frontend/src/pages/AdminSettingsPage/McpSection.tsx b/frontend/src/pages/AdminSettingsPage/McpSection.tsx index 4c76d4b..3dc4237 100644 --- a/frontend/src/pages/AdminSettingsPage/McpSection.tsx +++ b/frontend/src/pages/AdminSettingsPage/McpSection.tsx @@ -1,15 +1,20 @@ import React from "react"; import { useTranslation } from "react-i18next"; +import Button from "@mui/material/Button"; import CircularProgress from "@mui/material/CircularProgress"; import Paper from "@mui/material/Paper"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; +import TerminalIcon from "@mui/icons-material/Terminal"; import { UnauthorizedError } from "../../api/client"; import { settingsApi, type McpAccess } from "../../api/settings"; import Segmented from "../../components/controls/Segmented"; import HelpTip from "../../components/HelpTip"; import SectionHeader from "../../components/SectionHeader"; import { useToast } from "../../components/ToastProvider"; +import { AnimatedTile } from "../../components/storageAnimations/parts"; +import McpLinkAnimation from "../../components/authAnimations/McpLinkAnimation"; +import McpConnectDialog from "../../components/mcp/McpConnectDialog"; type Props = { onUnauthorized?: () => void; @@ -24,6 +29,7 @@ export default function McpSection({ onUnauthorized }: Props) { const [access, setAccess] = React.useState("write"); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(false); + const [connectOpen, setConnectOpen] = React.useState(false); React.useEffect(() => { let active = true; @@ -60,28 +66,42 @@ export default function McpSection({ onUnauthorized }: Props) { return ( - + } onClick={() => setConnectOpen(true)}> + {t("mcpConnect.open")} + + } + /> - - - {t("adminSettings.mcp.accessLabel")} - - - - void select(next)} - disabled={loading || saving} - options={LEVELS.map((level) => ({ value: level, label: t(`adminSettings.mcp.levels.${level}`) }))} - /> - {saving && } + + + + + + + {t("adminSettings.mcp.accessLabel")} + + + + void select(next)} + disabled={loading || saving} + options={LEVELS.map((level) => ({ value: level, label: t(`adminSettings.mcp.levels.${level}`) }))} + /> + {saving && } + + + {t(`adminSettings.mcp.levelHelp.${access}`)} + - - {t(`adminSettings.mcp.levelHelp.${access}`)} - + setConnectOpen(false)} /> ); } diff --git a/frontend/src/pages/ProfilePage/McpPermissionsSection.tsx b/frontend/src/pages/ProfilePage/McpPermissionsSection.tsx index dc384b9..558e34b 100644 --- a/frontend/src/pages/ProfilePage/McpPermissionsSection.tsx +++ b/frontend/src/pages/ProfilePage/McpPermissionsSection.tsx @@ -1,12 +1,14 @@ import React from "react"; import { useTranslation } from "react-i18next"; import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; +import TerminalIcon from "@mui/icons-material/Terminal"; import { authApi, type ApiTokenInfo, type McpToolInfo } from "../../api/auth"; import { UnauthorizedError } from "../../api/client"; import { useConfirm } from "../../components/ConfirmProvider"; -import HelpTip from "../../components/HelpTip"; +import McpConnectDialog from "../../components/mcp/McpConnectDialog"; import { PanelHeader, SectionLabel } from "../../components/Layout/ConfigurationDialog/parts"; import PermissionRow from "../../components/PermissionRow"; import { useToast } from "../../components/ToastProvider"; @@ -24,6 +26,7 @@ export default function McpPermissionsSection({ info, onChange, onUnauthorized } const confirm = useConfirm(); const toast = useToast(); const [saving, setSaving] = React.useState(null); + const [connectOpen, setConnectOpen] = React.useState(false); // Ahead of the server while a save is in flight; back in step with it once the save answers. const [selected, setSelected] = React.useState(info.mcp_tools); React.useEffect(() => setSelected(info.mcp_tools), [info.mcp_tools]); @@ -73,8 +76,15 @@ export default function McpPermissionsSection({ info, onChange, onUnauthorized } } + action={ + info.mcp_access !== "off" && ( + + ) + } /> + setConnectOpen(false)} /> {info.mcp_access === "off" ? ( {t("profile.apiToken.mcp.adminOff")}