🎉 Add workspace menu for MCP server

This commit is contained in:
Luis de Dios 2026-02-24 15:40:50 +01:00 committed by Alonso Torres
parent 0a5de10dff
commit adc3fa41e9
10 changed files with 577 additions and 381 deletions

View File

@ -0,0 +1,10 @@
name: New MCP server
version: 0.0.1
schema: v1
mcpServers:
- name: New MCP server
command: npx
args:
- -y
- <your-mcp-server>
env: {}

View File

@ -25,6 +25,7 @@
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.broadcast :as mbc]
[app.main.data.changes :as dch]
[app.main.data.comments :as dcmt]
[app.main.data.common :as dcm]
@ -214,7 +215,7 @@
(watch [_ _ _]
(rx/of (dp/check-open-plugin)
(fdf/fix-deleted-fonts-for-local-library file-id)
(mcp/init-mcp-connexion)))))
(mcp/init-mcp-connection)))))
(defn- bundle-fetched
[{:keys [file file-id thumbnails] :as bundle}]
@ -370,6 +371,12 @@
(rx/take 1)
(rx/tap (fn [_] (perf/setup)))))
(when (contains? cf/flags :mcp)
(->> mbc/stream
(rx/filter (mbc/type? :mcp-enabled-change))
(rx/map deref)
(rx/map mcp/update-mcp-status)))
(->> stream
(rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1)

View File

@ -34,6 +34,27 @@
[event]
(= (ptk/type event) :app.main.data.workspace/finalize-workspace))
(defn update-mcp-status
[value]
(ptk/reify ::update-mcp-status
ptk/UpdateEvent
(update [_ state]
(update-in state [:profile :props] assoc :mcp-enabled value))
ptk/WatchEvent
(watch [_ _ _]
(case value
true (rx/of (ptk/data-event ::connect))
false (rx/of (ptk/data-event ::disconnect))
nil))))
(defn update-mcp-connection
[value]
(ptk/reify ::update-mcp-plugin-connection
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :mcp] assoc :connected value))))
(defn init-mcp!
[stream]
(->> (rp/cmd! :get-current-mcp-token)
@ -52,8 +73,12 @@
:getServerUrl #(str cf/mcp-ws-uri)
:setMcpStatus
(fn [status]
;; TODO: Visual feedback
(log/info :hint "MCP STATUS" :status status))
(let [mcp-connected? (case status
"connected" true
"disconnected" false
nil)]
(st/emit! (update-mcp-connection mcp-connected?))
(log/info :hint "MCP STATUS" :status status)))
:on
(fn [event cb]
@ -77,11 +102,11 @@
[]
(st/emit! (ptk/data-event ::connect)))
(defn init-mcp-connexion
(defn init-mcp-connection
[]
(ptk/reify ::init-mcp-connexion
(ptk/reify ::init-mcp-connection
ptk/EffectEvent
(effect [_ state stream]
(when (and (contains? cf/flags :mcp)
(-> state :profile :props :mcp-status))
(-> state :profile :props :mcp-enabled))
(init-mcp! stream)))))

View File

@ -12,6 +12,7 @@
[app.common.schema :as sm]
[app.common.time :as ct]
[app.config :as cf]
[app.main.broadcast :as mbc]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
@ -272,12 +273,13 @@
on-created
(mf/use-fn
(fn []
(st/emit! (du/update-profile-props {:mcp-status true})
(st/emit! (du/update-profile-props {:mcp-enabled true})
(ev/event {::ev/name "generate-mcp-key"
::ev/origin "integrations"})
(ev/event {::ev/name "enable-mcp"
::ev/origin "integrations"
:source "key-creation"}))
(mbc/emit! :mcp-enabled-change true)
(reset! created? true)))]
[:div {:class (stl/css :modal-overlay)}
@ -315,9 +317,10 @@
(mf/use-fn
(fn []
(st/emit! (du/delete-access-token {:id mcp-key-id})
(du/update-profile-props {:mcp-status true})
(du/update-profile-props {:mcp-enabled true})
(ev/event {::ev/name "regenerate-mcp-key"
::ev/origin "integrations"}))
(mbc/emit! :mcp-enabled-change true)
(reset! created? true)))]
[:div {:class (stl/css :modal-overlay)}
@ -406,8 +409,8 @@
(let [tokens (mf/deref tokens-ref)
profile (mf/deref refs/profile)
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
mcp-active? (d/nilv (-> profile :props :mcp-status) false)
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
mcp-enabled? (d/nilv (-> profile :props :mcp-enabled) false)
expires-at (:expires-at mcp-key)
expired? (and (some? expires-at) (> (ct/now) expires-at))
@ -415,21 +418,22 @@
tooltip-id
(mf/use-id)
handle-mcp-status-change
handle-mcp-change
(mf/use-fn
(fn [mcp-status]
(st/emit! (du/update-profile-props {:mcp-status mcp-status})
(fn [value]
(st/emit! (du/update-profile-props {:mcp-enabled value})
(ntf/show {:level :info
:type :toast
:content (if (true? mcp-status)
:content (if (true? value)
(tr "integrations.notification.success.mcp-server-enabled")
(tr "integrations.notification.success.mcp-server-disabled"))
:timeout notification-timeout})
(ev/event {::ev/name (if (true? mcp-status) "enable-mcp" "disable-mcp")
(ev/event {::ev/name (if (true? value) "enable-mcp" "disable-mcp")
::ev/origin "integrations"
:source "toggle"}))))
:source "toggle"}))
(mbc/emit! :mcp-enabled-change value)))
handle-initial-mcp-status
handle-generate-mcp-key
(mf/use-fn
#(st/emit! (modal/show {:type :generate-mcp-key})))
@ -444,7 +448,8 @@
(let [params {:id (:id mcp-key)}
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
(st/emit! (du/delete-access-token (with-meta params mdata))
(du/update-profile-props {:mcp-status false})))))
(du/update-profile-props {:mcp-enabled false}))
(mbc/emit! :mcp-enabled-change false))))
on-copy-to-clipboard
(mf/use-fn
@ -497,14 +502,14 @@
(tr "integrations.mcp-server.status.expired.1")]]])
[:div {:class (stl/css :mcp-server-switch)}
[:> switch* {:label (if mcp-active?
[:> switch* {:label (if mcp-enabled?
(tr "integrations.mcp-server.status.enabled")
(tr "integrations.mcp-server.status.disabled"))
:default-checked mcp-active?
:on-change handle-mcp-status-change}]
(when (and (false? mcp-active?) (nil? mcp-key))
:default-checked mcp-enabled?
:on-change handle-mcp-change}]
(when (and (false? mcp-enabled?) (nil? mcp-key))
[:div {:class (stl/css :mcp-server-switch-cover)
:on-click handle-initial-mcp-status}])]]]
:on-click handle-generate-mcp-key}])]]]
(when (some? mcp-key)
[:div {:class (stl/css :mcp-server-key)}

View File

@ -15,7 +15,6 @@
[app.main.refs :as refs]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.main-menu :as main-menu]
[app.util.dom :as dom]
@ -27,12 +26,10 @@
;; --- Header Component
(mf/defc left-header*
[{:keys [file layout project page-id class]}]
(let [profile (mf/deref refs/profile)
file-id (:id file)
[{:keys [file layout project class]}]
(let [file-id (:id file)
file-name (:name file)
project-id (:id project)
team-id (:team-id project)
shared? (:is-shared file)
persistence
(mf/deref refs/persistence)
@ -40,8 +37,6 @@
persistence-status
(get persistence :status)
read-only? (mf/use-ctx ctx/workspace-read-only?)
editing* (mf/use-state false)
editing? (deref editing*)
input-ref (mf/use-ref nil)
@ -137,10 +132,5 @@
(when ^boolean shared?
[:span {:class (stl/css :shared-badge)} deprecated-icon/library])
[:div {:class (stl/css :menu-section)}
[:& main-menu/menu
{:layout layout
:file file
:profile profile
:read-only? read-only?
:team-id team-id
:page-id page-id}]]]))
[:> main-menu/menu* {:layout layout
:file file}]]]))

File diff suppressed because it is too large Load Diff

View File

@ -4,125 +4,174 @@
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/typography.scss" as t;
@use "ds/z-index.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.base-menu {
position: absolute;
display: flex;
flex-direction: column;
gap: var(--sp-xs);
padding: var(--sp-xs);
border-radius: $br-8;
z-index: var(--z-index-dropdown);
background-color: var(--menu-background-color);
border: $b-2 solid var(--panel-border-color);
box-shadow: 0 0 $sz-12 0 var(--menu-shadow-color);
}
.menu {
@extend .menu-dropdown;
top: deprecated.$s-48;
left: calc(var(--right-sidebar-width, deprecated.$s-256) - deprecated.$s-16);
width: deprecated.$s-192;
margin: 0;
}
.menu-item {
@extend .menu-item-base;
cursor: pointer;
.open-arrow {
@include deprecated.flexCenter;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
&:hover {
color: var(--menu-foreground-color-hover);
.open-arrow {
svg {
stroke: var(--menu-foreground-color-hover);
}
}
.shortcut-key {
color: var(--menu-shortcut-foreground-color-hover);
}
}
}
.separator {
border-top: deprecated.$s-1 solid var(--color-background-quaternary);
height: deprecated.$s-4;
left: calc(-1 * deprecated.$s-4);
margin-top: deprecated.$s-8;
position: relative;
width: calc(100% + deprecated.$s-8);
}
.shortcut {
@extend .shortcut-base;
}
.shortcut-key {
@extend .shortcut-key-base;
top: $sz-48;
left: calc(var(--right-sidebar-width) - $sz-40);
inline-size: $sz-192;
}
.sub-menu {
@extend .menu-dropdown;
left: calc(var(--right-sidebar-width, deprecated.$s-256) + deprecated.$s-180);
width: deprecated.$s-192;
min-width: calc(deprecated.$s-272 - deprecated.$s-2);
width: 110%;
left: calc(var(--right-sidebar-width) + $sz-154);
min-width: $sz-284;
width: 115%;
.submenu-item {
@extend .menu-item-base;
&:hover {
color: var(--menu-foreground-color-hover);
.shortcut-key {
color: var(--menu-shortcut-foreground-color-hover);
}
}
&.pos-1 {
top: calc($sz-16 + $sz-32);
}
.menu-disabled {
color: var(--color-foreground-secondary);
&:hover {
cursor: default;
color: var(--color-foreground-secondary);
background-color: var(--menu-background-color);
}
&.pos-2 {
top: calc($sz-16 + (2 * $sz-32));
}
&.file {
top: deprecated.$s-48;
&.pos-3 {
top: calc($sz-16 + (3 * $sz-32));
}
&.edit {
top: deprecated.$s-76;
&.pos-4 {
top: calc($sz-16 + (4 * $sz-32));
}
&.view {
top: deprecated.$s-116;
&.pos-5 {
top: calc($sz-16 + (5 * $sz-32));
}
&.preferences {
top: deprecated.$s-148;
&.pos-6 {
top: calc($sz-16 + (6 * $sz-32));
}
&.pos-final-5 {
top: calc($sz-32 + (5 * $sz-32));
}
&.pos-final-6 {
top: calc($sz-32 + (6 * $sz-32));
}
&.pos-final-7 {
top: calc($sz-32 + (7 * $sz-32));
}
&.plugins {
top: deprecated.$s-180;
max-height: calc(100vh - deprecated.$s-180);
max-height: calc(100vh - $sz-200);
overflow-x: hidden;
overflow-y: auto;
}
}
&.help-info {
top: deprecated.$s-232;
.base-menu-item {
@include t.use-typography("body-small");
display: grid;
align-items: center;
grid-template-columns: auto $sz-16 $sz-16;
grid-template-areas: "name indicator arrow";
block-size: $sz-28;
inline-size: 100%;
padding: $sz-6;
border-radius: $br-8;
color: var(--menu-foreground-color);
background-color: var(--menu-background-color);
&:hover {
--menu-foreground-color: var(--menu-foreground-color-hover);
--menu-background-color: var(--menu-background-color-hover);
--menu-shortcut-foreground-color: var(--menu-shortcut-foreground-color-hover);
--menu-icon-foreground-color: var(--menu-foreground-color-hover);
}
&.help-info-old {
top: deprecated.$s-192;
&.disabled {
--menu-foreground-color: var(--color-foreground-secondary);
pointer-events: none;
}
}
.menu-item {
display: grid;
align-items: center;
grid-template-columns: auto $sz-16 $sz-16;
grid-template-areas: "name indicator arrow";
}
.submenu-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.item-name {
grid-area: name;
}
.item-indicator {
--menu-indicator-color: var(--color-foreground-secondary);
grid-area: indicator;
display: flex;
align-items: center;
justify-content: center;
inline-size: px2rem(8);
block-size: px2rem(8);
border-radius: $br-circle;
background-color: var(--menu-indicator-color);
&.active {
--menu-indicator-color: var(--color-accent-primary);
}
}
.item-arrow {
grid-area: arrow;
color: var(--menu-icon-foreground-color);
}
.item-icon {
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
color: var(--menu-icon-foreground-color);
display: flex;
align-items: center;
justify-content: center;
}
.separator {
position: relative;
block-size: var(--sp-xs);
inline-size: calc(100% + var(--sp-s));
border-top: $b-1 solid var(--color-background-quaternary);
left: calc(-1 * var(--sp-xs));
margin-top: var(--sp-s);
}
.shortcut {
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-xxs);
color: var(--menu-shortcut-foreground-color);
}
.shortcut-key {
@include t.use-typography("body-small");
display: flex;
align-items: center;
justify-content: center;
height: px2rem(20);
padding: var(--sp-xxs) px2rem(6);
border-radius: $br-6;
background-color: var(--menu-shortcut-background-color);
}

View File

@ -119,7 +119,7 @@
(mf/defc left-sidebar*
{::mf/memo true}
[{:keys [layout file page-id tokens-lib active-tokens resolved-active-tokens]}]
[{:keys [layout file tokens-lib active-tokens resolved-active-tokens]}]
(let [options-mode (mf/deref refs/options-mode-global)
project (mf/deref refs/project)
file-id (get file :id)
@ -185,12 +185,10 @@
:class aside-class
:style {:--left-sidebar-width (dm/str width "px")}}
[:> left-header*
{:file file
:layout layout
:project project
:page-id page-id
:class (stl/css :left-header)}]
[:> left-header* {:file file
:layout layout
:project project
:class (stl/css :left-header)}]
[:div {:on-pointer-down on-pointer-down
:on-lost-pointer-capture on-lost-pointer-capture

View File

@ -5719,6 +5719,18 @@ msgstr "Hide rulers"
msgid "workspace.header.menu.hide-textpalette"
msgstr "Hide fonts palette"
msgid "workspace.header.menu.mcp.plugin.status.connect"
msgstr "Connect"
msgid "workspace.header.menu.mcp.plugin.status.disconnect"
msgstr "Disconnect"
msgid "workspace.header.menu.mcp.server.status.enabled"
msgstr "Manage (Status: enabled)"
msgid "workspace.header.menu.mcp.server.status.disabled"
msgstr "Manage (Status: disabled)"
#: src/app/main/ui/workspace/main_menu.cljs:884
msgid "workspace.header.menu.option.edit"
msgstr "Edit"
@ -5731,6 +5743,9 @@ msgstr "File"
msgid "workspace.header.menu.option.help-info"
msgstr "Help & info"
msgid "workspace.header.menu.option.mcp"
msgstr "MCP server"
#: src/app/main/ui/workspace/main_menu.cljs:916
#, unused
msgid "workspace.header.menu.option.power-up"

View File

@ -5686,6 +5686,18 @@ msgstr "Ocultar reglas"
msgid "workspace.header.menu.hide-textpalette"
msgstr "Ocultar paleta de textos"
msgid "workspace.header.menu.mcp.plugin.status.connect"
msgstr "Conectar"
msgid "workspace.header.menu.mcp.plugin.status.disconnect"
msgstr "Desconectar"
msgid "workspace.header.menu.mcp.server.status.enabled"
msgstr "Gestionar (estado: habilitado)"
msgid "workspace.header.menu.mcp.server.status.disabled"
msgstr "Gestionar (estado: deshabilitado)"
#: src/app/main/ui/workspace/main_menu.cljs:884
msgid "workspace.header.menu.option.edit"
msgstr "Editar"
@ -5698,6 +5710,9 @@ msgstr "Archivo"
msgid "workspace.header.menu.option.help-info"
msgstr "Ayuda e información"
msgid "workspace.header.menu.option.mcp"
msgstr "Servidor MCP"
#: src/app/main/ui/workspace/main_menu.cljs:906
msgid "workspace.header.menu.option.preferences"
msgstr "Preferencias"