From a75de11e701e3b8541f9d57717549fc19bdf9e9c Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Tue, 24 Feb 2026 21:34:19 +0100 Subject: [PATCH] :sparkles: Improve MCP section in the dashboard --- frontend/src/app/config.cljs | 2 +- .../app/main/ui/settings/integrations.cljs | 130 ++++++++++++------ .../app/main/ui/settings/integrations.scss | 38 +++-- frontend/translations/en.po | 28 ++-- frontend/translations/es.po | 28 ++-- 5 files changed, 153 insertions(+), 73 deletions(-) diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index e698aa6499..61a08b0cb6 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -151,7 +151,7 @@ (let [f (obj/get global "initializeExternalConfigInfo")] (when (fn? f) (f)))) -(def mcp-server-url (-> public-uri u/ensure-path-slash (u/join "mcp") str)) +(def mcp-server-url (-> public-uri u/ensure-path-slash (u/join "mcp/stream") str)) (def mcp-help-center-uri "https://help.penpot.app/technical-guide/") ;; --- Helper Functions diff --git a/frontend/src/app/main/ui/settings/integrations.cljs b/frontend/src/app/main/ui/settings/integrations.cljs index 82f90c47d4..5e56b5096f 100644 --- a/frontend/src/app/main/ui/settings/integrations.cljs +++ b/frontend/src/app/main/ui/settings/integrations.cljs @@ -8,6 +8,7 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.schema :as sm] [app.common.time :as ct] [app.config :as cf] @@ -44,18 +45,39 @@ (def notification-timeout 7000) -(def ^:private schema:form +(def ^:private schema:form-access-token [:map [:name [::sm/text {:max 250}]] [:expiration-date [::sm/text {:max 250}]]]) -(def form-initial-data +(def ^:private schema:form-mcp-key + [:map + [:expiration-date [::sm/text {:max 250}]]]) + +(def form-initial-data-access-token {:name "" :expiration-date "never"}) +(def form-initial-data-mcp-key + {:expiration-date "never"}) + +(mf/defc input-copy* + {::mf/private true} + [{:keys [value on-copy-to-clipboard]}] + [:div {:class (stl/css :input-copy)} + [:> input* {:type "text" + :default-value value + :read-only true}] + [:div {:class (stl/css :input-copy-button-wrapper)} + [:> icon-button* {:variant "secondary" + :class (stl/css :input-copy-button) + :aria-label (tr "integrations.copy-to-clipboard") + :on-click on-copy-to-clipboard + :icon i/clipboard}]]]) + (mf/defc token-created* {::mf/private true} - [{:keys [title]}] + [{:keys [title mcp-key?]}] (let [token-created (mf/deref token-created-ref) on-copy-to-clipboard @@ -77,18 +99,14 @@ [:> notification-pill* {:level :info :type :context} - (tr "integrations.info.non-recuperable")] + [:> text* {:as "div" + :typography t/body-small + :class (stl/css :color-primary)} + (tr "integrations.info.non-recuperable")]] [:div {:class (stl/css :modal-content)} - [:div {:class (stl/css :modal-token)} - [:> input* {:type "text" - :default-value (:token token-created "") - :read-only true}] - [:div {:class (stl/css :modal-token-button)} - [:> icon-button* {:variant "secondary" - :aria-label (tr "integrations.copy-token") - :on-click on-copy-to-clipboard - :icon i/clipboard}]]] + [:> input-copy* {:value (:token token-created "") + :on-copy-to-clipboard on-copy-to-clipboard}] [:> text* {:as "div" :typography t/body-small @@ -97,18 +115,40 @@ (tr "integrations.token-will-expire" (ct/format-inst (:expires-at token-created) "PPP")) (tr "integrations.token-will-not-expire"))]] + (when mcp-key? + [:div {:class (stl/css :modal-content)} + [:> text* {:as "div" + :typography t/body-small + :class (stl/css :color-primary)} + (tr "integrations.info.mcp-client-config")] + [:textarea {:class (stl/css :textarea) + :wrap "off" + :rows 7 + :read-only true} + (dm/str + "{\n" + " \"mcpServers\": {\n" + " \"penpot\": {\n" + " \"url\": \"" cf/mcp-server-url "?userToken=" (:token token-created "") "\"\n" + " }\n" + " }" + "\n}")]]) + [:div {:class (stl/css :modal-footer)} [:> button* {:variant "secondary" :on-click modal/hide!} (tr "labels.close")]]])) - (mf/defc create-token* {::mf/private true} [{:keys [title info mcp-key? on-created]}] (let [form (fm/use-form - :initial form-initial-data - :schema schema:form) + :initial (if mcp-key? + form-initial-data-mcp-key + form-initial-data-access-token) + :schema (if mcp-key? + schema:form-mcp-key + schema:form-access-token)) on-error (mf/use-fn @@ -131,7 +171,8 @@ params (cond-> {:name (:name cdata) :perms (:perms cdata)} (not= "never" expiration) (assoc :expiration expiration) - (true? mcp-key?) (assoc :type "mcp"))] + (true? mcp-key?) (assoc :type "mcp" + :name "MCP key"))] (st/emit! (du/create-access-token (with-meta params mdata))))))] [:> fc/form* {:form form @@ -146,15 +187,25 @@ (when (some? info) [:> notification-pill* {:level :info :type :context} - info]) + [:> text* {:as "div" + :typography t/body-small + :class (stl/css :color-primary)} + info]]) - [:div {:class (stl/css :modal-content)} - [:> fc/form-input* {:type "text" - :auto-focus? true - :form form - :name :name - :label (tr "integrations.name.label") - :placeholder (tr "integrations.name.placeholder")}]] + (if mcp-key? + [:div {:class (stl/css :modal-content)} + [:> text* {:as "div" + :typography t/body-medium + :class (stl/css :color-secondary)} + (tr "integrations.info.mcp-server")]] + + [:div {:class (stl/css :modal-content)} + [:> fc/form-input* {:type "text" + :auto-focus? true + :form form + :name :name + :label (tr "integrations.name.label") + :placeholder (tr "integrations.name.placeholder")}]]) [:div {:class (stl/css :modal-content)} [:> text* {:as "label" @@ -206,9 +257,9 @@ [:> create-token* {:title (tr "integrations.create-access-token.title") :on-created on-created}])]])) -(mf/defc create-mcp-key-modal +(mf/defc generate-mcp-key-modal {::mf/register modal/components - ::mf/register-as :create-mcp-key} + ::mf/register-as :generate-mcp-key} [] (let [created? (mf/use-state false) @@ -222,7 +273,7 @@ (mf/use-fn (fn [] (st/emit! (du/update-profile-props {:mcp-status true}) - (ev/event {::ev/name "create-mcp-key" + (ev/event {::ev/name "generate-mcp-key" ::ev/origin "integrations"}) (ev/event {::ev/name "enable-mcp" ::ev/origin "integrations" @@ -238,8 +289,9 @@ :icon i/close}]] (if @created? - [:> token-created* {:title (tr "integrations.create-mcp-key.title.created")}] - [:> create-token* {:title (tr "integrations.create-mcp-key.title") + [:> token-created* {:title (tr "integrations.generate-mcp-key.title.created") + :mcp-key? true}] + [:> create-token* {:title (tr "integrations.generate-mcp-key.title") :mcp-key? true :on-created on-created}])]])) @@ -277,7 +329,8 @@ :icon i/close}]] (if @created? - [:> token-created* {:title (tr "integrations.regenerate-mcp-key.title.created")}] + [:> token-created* {:title (tr "integrations.regenerate-mcp-key.title.created") + :mcp-key? true}] [:> create-token* {:title (tr "integrations.regenerate-mcp-key.title") :info (tr "integrations.regenerate-mcp-key.info") :mcp-key? true @@ -378,7 +431,7 @@ handle-initial-mcp-status (mf/use-fn - #(st/emit! (modal/show {:type :create-mcp-key}))) + #(st/emit! (modal/show {:type :generate-mcp-key}))) handle-regenerate-mcp-key (mf/use-fn @@ -484,16 +537,9 @@ :typography t/body-medium :class (stl/css :color-secondary)} (tr "integrations.mcp-server.mcp-keys.info")] - [:div {:class (stl/css :mcp-server-notification-line)} - [:> text* {:as "div" - :typography t/body-medium - :class (stl/css :color-primary)} - cf/mcp-server-url] - [:> text* {:as "div" - :typography t/body-medium - :on-click on-copy-to-clipboard - :class (stl/css :mcp-server-notification-link)} - [:> icon* {:icon-id i/clipboard}] (tr "integrations.mcp-server.mcp-keys.copy")]] + + [:> input-copy* {:value (dm/str cf/mcp-server-url "?userToken=") + :on-copy-to-clipboard on-copy-to-clipboard}] [:> text* {:as "div" :typography t/body-medium diff --git a/frontend/src/app/main/ui/settings/integrations.scss b/frontend/src/app/main/ui/settings/integrations.scss index 00ddf72231..d7be475bb4 100644 --- a/frontend/src/app/main/ui/settings/integrations.scss +++ b/frontend/src/app/main/ui/settings/integrations.scss @@ -8,8 +8,9 @@ @use "ds/_borders.scss" as *; @use "ds/_sizes.scss" as *; -@use "ds/spacing.scss" as *; @use "ds/mixins.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/typography.scss" as t; .color-primary { color: var(--color-foreground-primary); @@ -49,6 +50,7 @@ .modal-container { @extend .modal-container-base; inline-size: $sz-400; + max-block-size: fit-content; position: relative; } @@ -76,11 +78,11 @@ gap: var(--sp-s); } -.modal-token { +.input-copy { position: relative; } -.modal-token-button { +.input-copy-button-wrapper { position: absolute; top: 0; right: 0; @@ -88,6 +90,10 @@ border-end-start-radius: 0; } +.input-copy-button { + border-radius: 0 $br-8 $br-8 0; +} + .integrations { display: grid; grid-template-rows: auto 1fr; @@ -116,14 +122,8 @@ .mcp-server-notification { display: flex; flex-direction: column; - gap: var(--sp-s); -} - -.mcp-server-notification-line { - display: flex; - flex-direction: row; - align-items: center; gap: var(--sp-m); + padding-right: var(--sp-xxl); } .mcp-server-notification-link { @@ -219,3 +219,21 @@ inline-size: $sz-48; border-radius: 0 var(--sp-s) var(--sp-s) 0; } + +.textarea { + @include t.use-typography("body-small"); + border-radius: $br-8; + background-color: var(--color-background-tertiary); + color: var(--color-foreground-secondary); + padding: var(--sp-xs) var(--sp-s); + border: 0; + resize: none; + + &:hover { + background-color: var(--color-background-quaternary); + } + + &:focus-visible { + outline: $b-1 solid var(--color-accent-primary); + } +} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ff088bd743..e2f0073eb5 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2087,8 +2087,8 @@ msgstr "" "internal Penpot API" #: src/app/main/ui/settings/integrations.cljs:152, src/app/main/ui/settings/integrations.cljs:158 -msgid "integrations.copy-token" -msgstr "Copy token" +msgid "integrations.copy-to-clipboard" +msgstr "Copy to clipboard" #: src/app/main/ui/settings/integrations.cljs:432 msgid "integrations.create-access-token.title" @@ -2098,14 +2098,6 @@ msgstr "Create access token" msgid "integrations.create-access-token.title.created" msgstr "Access token created" -#: src/app/main/ui/settings/integrations.cljs:290 -msgid "integrations.create-mcp-key.title" -msgstr "Create new MCP key" - -#: src/app/main/ui/settings/integrations.cljs:291 -msgid "integrations.create-mcp-key.title.created" -msgstr "MCP key created" - #: src/app/main/ui/settings/integrations.cljs:257 msgid "integrations.delete-token.accept" msgstr "Delete token" @@ -2154,6 +2146,22 @@ msgstr "No expiration date" msgid "integrations.expiration-date.label" msgstr "Expiration date" +#: src/app/main/ui/settings/integrations.cljs:290 +msgid "integrations.generate-mcp-key.title" +msgstr "Generate MCP key" + +#: src/app/main/ui/settings/integrations.cljs:291 +msgid "integrations.generate-mcp-key.title.created" +msgstr "MCP key generated" + +#: src/app/main/ui/settings/integrations.cljs:113 +msgid "integrations.info.mcp-client-config" +msgstr "Add this configuration to your MCP client (e.g. ~/​​​​​​.mcp.json)." + +#: src/app/main/ui/settings/integrations.cljs:183 +msgid "integrations.info.mcp-server" +msgstr "The Penpot MCP Server enables MCP clients to interact directly with Penpot design files." + #: src/app/main/ui/settings/integrations.cljs:131 msgid "integrations.info.non-recuperable" msgstr "This unique token is non-recuperable. If you lose it, you will need to create a new one." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 33a89f18a6..fbb99990a1 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -2058,8 +2058,8 @@ msgstr "" "otras aplicaciones acceso a la API interna de Penpot" #: src/app/main/ui/settings/integrations.cljs:152, src/app/main/ui/settings/integrations.cljs:158 -msgid "integrations.copy-token" -msgstr "Copiar token" +msgid "integrations.copy-to-clipboard" +msgstr "Copiar al portapapeles" #: src/app/main/ui/settings/integrations.cljs:432 msgid "integrations.create-access-token.title" @@ -2069,14 +2069,6 @@ msgstr "Crear token de accesso" msgid "integrations.create-access-token.title.created" msgstr "Token de acceso creado" -#: src/app/main/ui/settings/integrations.cljs:290 -msgid "integrations.create-mcp-key.title" -msgstr "Crear nueva clave MCP" - -#: src/app/main/ui/settings/integrations.cljs:291 -msgid "integrations.create-mcp-key.title.created" -msgstr "Clave MCP creada" - #: src/app/main/ui/settings/integrations.cljs:257 msgid "integrations.delete-token.accept" msgstr "Borrar token" @@ -2125,6 +2117,22 @@ msgstr "Sin fecha de expiración" msgid "integrations.expiration-date.label" msgstr "Fecha de expiración" +#: src/app/main/ui/settings/integrations.cljs:290 +msgid "integrations.generate-mcp-key.title" +msgstr "Generar clave MCP" + +#: src/app/main/ui/settings/integrations.cljs:291 +msgid "integrations.generate-mcp-key.title.created" +msgstr "Clave MCP generada" + +#: src/app/main/ui/settings/integrations.cljs:113 +msgid "integrations.info.mcp-client-config" +msgstr "Agrega esta configuración a tu cliente MCP (por ejemplo, ~/.mcp.json)." + +#: src/app/main/ui/settings/integrations.cljs:183 +msgid "integrations.info.mcp-server" +msgstr "El servidor MCP de Penpot permite a los clientes MCP interactuar directamente con los archivos de diseño de Penpot." + #: src/app/main/ui/settings/integrations.cljs:131 msgid "integrations.info.non-recuperable" msgstr "Esta clave única no es recuperable. Si la pierdes, tendrás que crear una nueva."