mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
Merge remote-tracking branch 'origin/main' into staging
This commit is contained in:
commit
811d53be12
@ -68,9 +68,10 @@
|
||||
[modif-tree children objects bounds parent transformed-parent-bounds]
|
||||
|
||||
(letfn [(apply-modifiers [bounds child]
|
||||
[(-> @(get bounds (:id child))
|
||||
(gpo/parent-coords-bounds @transformed-parent-bounds))
|
||||
child])
|
||||
(when-let [child-bounds (get bounds (:id child))]
|
||||
[(-> @child-bounds
|
||||
(gpo/parent-coords-bounds @transformed-parent-bounds))
|
||||
child]))
|
||||
|
||||
(set-child-modifiers [[layout-line modif-tree] [child-bounds child]]
|
||||
(let [[modifiers layout-line]
|
||||
@ -83,7 +84,7 @@
|
||||
(->> children
|
||||
(keep (d/getf objects))
|
||||
(remove gco/invalid-geometry?)
|
||||
(map (partial apply-modifiers bounds)))
|
||||
(keep (partial apply-modifiers bounds)))
|
||||
|
||||
layout-data (gcfl/calc-layout-data parent @transformed-parent-bounds children bounds objects)
|
||||
children (into [] (cond-> children (not (:reverse? layout-data)) reverse))
|
||||
@ -106,9 +107,10 @@
|
||||
[modif-tree objects bounds parent transformed-parent-bounds]
|
||||
|
||||
(letfn [(apply-modifiers [bounds child]
|
||||
[(-> @(get bounds (:id child))
|
||||
(gpo/parent-coords-bounds @transformed-parent-bounds))
|
||||
child])
|
||||
(when-let [child-bounds (get bounds (:id child))]
|
||||
[(-> @child-bounds
|
||||
(gpo/parent-coords-bounds @transformed-parent-bounds))
|
||||
child]))
|
||||
|
||||
(set-child-modifiers [modif-tree grid-data cell-data [child-bounds child]]
|
||||
(let [modifiers
|
||||
@ -119,7 +121,7 @@
|
||||
|
||||
children
|
||||
(->> (cfh/get-immediate-children objects (:id parent) {:remove-hidden true})
|
||||
(map (partial apply-modifiers bounds)))
|
||||
(keep (partial apply-modifiers bounds)))
|
||||
grid-data (gcgl/calc-layout-data parent @transformed-parent-bounds children bounds objects)]
|
||||
(loop [modif-tree modif-tree
|
||||
bound+child (first children)
|
||||
@ -240,8 +242,10 @@
|
||||
(gcfl/layout-content-bounds bounds parent children objects)
|
||||
|
||||
(ctl/grid-layout? parent)
|
||||
(let [children (->> children
|
||||
(map (fn [child] [@(get bounds (:id child)) child])))
|
||||
(let [children (->> children
|
||||
(keep (fn [child]
|
||||
(when-let [child-bounds-ref (get bounds (:id child))]
|
||||
[@child-bounds-ref child]))))
|
||||
layout-data (gcgl/calc-layout-data parent @parent-bounds children bounds objects)]
|
||||
(gcgl/layout-content-bounds bounds parent layout-data))))
|
||||
|
||||
|
||||
189
common/test/common_tests/geom_modifiers_test.cljc
Normal file
189
common/test/common_tests/geom_modifiers_test.cljc
Normal file
@ -0,0 +1,189 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns common-tests.geom-modifiers-test
|
||||
(:require
|
||||
[app.common.geom.modifiers :as gm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.test-helpers.compositions :as tho]
|
||||
[app.common.test-helpers.files :as thf]
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
|
||||
;; ---- Helpers
|
||||
|
||||
(defn- add-flex-frame
|
||||
"Create a flex layout frame"
|
||||
[file frame-label & {:keys [width height] :as params}]
|
||||
(ths/add-sample-shape file frame-label
|
||||
(merge {:type :frame
|
||||
:name "FlexFrame"
|
||||
:layout-flex-dir :row
|
||||
:width (or width 200)
|
||||
:height (or height 200)}
|
||||
params)))
|
||||
|
||||
(defn- add-rect-child
|
||||
"Create a rectangle child inside a parent"
|
||||
[file rect-label parent-label & {:keys [width height x y] :as params}]
|
||||
(ths/add-sample-shape file rect-label
|
||||
(merge {:type :rect
|
||||
:name "Rect"
|
||||
:parent-label parent-label
|
||||
:width (or width 50)
|
||||
:height (or height 50)
|
||||
:x (or x 0)
|
||||
:y (or y 0)}
|
||||
params)))
|
||||
|
||||
(defn- add-ghost-child-id
|
||||
"Add a non-existent child ID to a frame's shapes list.
|
||||
This simulates data inconsistency where a child ID is referenced
|
||||
but the child shape doesn't exist in objects."
|
||||
[file frame-label ghost-id]
|
||||
(let [page (thf/current-page file)
|
||||
frame-id (thi/id frame-label)]
|
||||
(update file :data
|
||||
(fn [file-data]
|
||||
(update-in file-data [:pages-index (:id page) :objects frame-id :shapes]
|
||||
conj ghost-id)))))
|
||||
|
||||
;; ---- Tests
|
||||
|
||||
(t/deftest flex-layout-with-normal-children
|
||||
(t/testing "set-objects-modifiers processes flex layout children correctly"
|
||||
(let [file (-> (thf/sample-file :file1)
|
||||
(add-flex-frame :frame1)
|
||||
(add-rect-child :rect1 :frame1))
|
||||
page (thf/current-page file)
|
||||
objects (:objects page)
|
||||
frame-id (thi/id :frame1)
|
||||
rect-id (thi/id :rect1)
|
||||
|
||||
;; Create a move modifier for the rectangle
|
||||
modif-tree {rect-id {:modifiers (ctm/move-modifiers (gpt/point 10 20))}}
|
||||
|
||||
;; This should not crash
|
||||
result (gm/set-objects-modifiers modif-tree objects)]
|
||||
|
||||
(t/is (some? result))
|
||||
;; The rectangle should have modifiers
|
||||
(t/is (contains? result rect-id)))))
|
||||
|
||||
(t/deftest flex-layout-with-nonexistent-child
|
||||
(t/testing "set-objects-modifiers handles flex frame with non-existent child in shapes"
|
||||
(let [ghost-id (thi/next-uuid)
|
||||
file (-> (thf/sample-file :file1)
|
||||
(add-flex-frame :frame1)
|
||||
(add-rect-child :rect1 :frame1)
|
||||
;; Add a non-existent child ID to the frame's shapes
|
||||
(add-ghost-child-id :frame1 ghost-id))
|
||||
page (thf/current-page file)
|
||||
objects (:objects page)
|
||||
rect-id (thi/id :rect1)
|
||||
|
||||
;; Create a move modifier for the existing rectangle
|
||||
modif-tree {rect-id {:modifiers (ctm/move-modifiers (gpt/point 10 20))}}
|
||||
|
||||
;; This should NOT crash even though the flex frame has
|
||||
;; a child ID (ghost-id) that doesn't exist in objects
|
||||
result (gm/set-objects-modifiers modif-tree objects)]
|
||||
|
||||
(t/is (some? result))
|
||||
(t/is (contains? result rect-id)))))
|
||||
|
||||
(t/deftest flex-layout-with-all-ghost-children
|
||||
(t/testing "set-objects-modifiers handles flex frame with only non-existent children"
|
||||
(let [ghost1 (thi/next-uuid)
|
||||
ghost2 (thi/next-uuid)
|
||||
file (-> (thf/sample-file :file1)
|
||||
(add-flex-frame :frame1)
|
||||
;; Add only non-existent children to the frame's shapes
|
||||
(add-ghost-child-id :frame1 ghost1)
|
||||
(add-ghost-child-id :frame1 ghost2))
|
||||
page (thf/current-page file)
|
||||
objects (:objects page)
|
||||
frame-id (thi/id :frame1)
|
||||
|
||||
;; Create a move modifier for the frame itself
|
||||
modif-tree {frame-id {:modifiers (ctm/move-modifiers (gpt/point 5 5))}}
|
||||
|
||||
;; Should not crash even though the flex frame has
|
||||
;; no existing children in its shapes list
|
||||
result (gm/set-objects-modifiers modif-tree objects)]
|
||||
|
||||
(t/is (some? result)))))
|
||||
|
||||
(t/deftest grid-layout-with-nonexistent-child
|
||||
(t/testing "set-objects-modifiers handles grid frame with non-existent child in shapes"
|
||||
(let [ghost-id (thi/next-uuid)
|
||||
file (-> (thf/sample-file :file1)
|
||||
(ths/add-sample-shape :frame1
|
||||
{:type :frame
|
||||
:name "GridFrame"
|
||||
:layout-grid-dir :row
|
||||
:width 200
|
||||
:height 200})
|
||||
(add-rect-child :rect1 :frame1)
|
||||
(add-ghost-child-id :frame1 ghost-id))
|
||||
page (thf/current-page file)
|
||||
objects (:objects page)
|
||||
rect-id (thi/id :rect1)
|
||||
|
||||
modif-tree {rect-id {:modifiers (ctm/move-modifiers (gpt/point 10 20))}}
|
||||
|
||||
;; Should not crash for grid layout with ghost child
|
||||
result (gm/set-objects-modifiers modif-tree objects)]
|
||||
|
||||
(t/is (some? result))
|
||||
(t/is (contains? result rect-id)))))
|
||||
|
||||
(t/deftest flex-layout-resize-with-nonexistent-child
|
||||
(t/testing "resize modifier propagation handles non-existent children"
|
||||
(let [ghost-id (thi/next-uuid)
|
||||
file (-> (thf/sample-file :file1)
|
||||
(add-flex-frame :frame1)
|
||||
(add-rect-child :rect1 :frame1)
|
||||
(add-ghost-child-id :frame1 ghost-id))
|
||||
page (thf/current-page file)
|
||||
objects (:objects page)
|
||||
frame-id (thi/id :frame1)
|
||||
|
||||
;; Create a resize modifier for the frame itself
|
||||
modif-tree {frame-id {:modifiers (ctm/resize-modifiers
|
||||
(gpt/point 2 2)
|
||||
(gpt/point 0 0))}}
|
||||
|
||||
;; Should not crash when propagating resize through flex layout
|
||||
;; that has ghost children
|
||||
result (gm/set-objects-modifiers modif-tree objects)]
|
||||
|
||||
(t/is (some? result))
|
||||
;; The frame should have modifiers
|
||||
(t/is (contains? result frame-id)))))
|
||||
|
||||
(t/deftest nested-flex-layout-with-nonexistent-child
|
||||
(t/testing "nested flex layout handles non-existent children in outer frame"
|
||||
(let [ghost-id (thi/next-uuid)
|
||||
file (-> (thf/sample-file :file1)
|
||||
(add-flex-frame :outer-frame)
|
||||
(add-flex-frame :inner-frame :parent-label :outer-frame)
|
||||
(add-rect-child :rect1 :inner-frame)
|
||||
(add-ghost-child-id :outer-frame ghost-id))
|
||||
page (thf/current-page file)
|
||||
objects (:objects page)
|
||||
rect-id (thi/id :rect1)
|
||||
|
||||
modif-tree {rect-id {:modifiers (ctm/move-modifiers (gpt/point 5 10))}}
|
||||
|
||||
result (gm/set-objects-modifiers modif-tree objects)]
|
||||
|
||||
(t/is (some? result))
|
||||
(t/is (contains? result rect-id)))))
|
||||
@ -12,6 +12,7 @@
|
||||
[common-tests.data-test]
|
||||
[common-tests.files-changes-test]
|
||||
[common-tests.files-migrations-test]
|
||||
[common-tests.geom-modifiers-test]
|
||||
[common-tests.geom-point-test]
|
||||
[common-tests.geom-shapes-test]
|
||||
[common-tests.geom-test]
|
||||
@ -66,6 +67,7 @@
|
||||
'common-tests.data-test
|
||||
'common-tests.files-changes-test
|
||||
'common-tests.files-migrations-test
|
||||
'common-tests.geom-modifiers-test
|
||||
'common-tests.geom-point-test
|
||||
'common-tests.geom-shapes-test
|
||||
'common-tests.geom-test
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
<link rel="shortcut icon" href="/img/favicon.png">
|
||||
<link rel="alternate" href="{{ metadata.feed.path | url }}" type="application/atom+xml" title="{{ metadata.title }}">
|
||||
<link rel="alternate" href="{{ metadata.jsonfeed.path | url }}" type="application/json" title="{{ metadata.title }}">
|
||||
<link rel="stylesheet" href="https://unpkg.com/prismjs@1.20.0/themes/prism-coy.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
|
||||
45
docs/_includes/layouts/mcp.njk
Normal file
45
docs/_includes/layouts/mcp.njk
Normal file
@ -0,0 +1,45 @@
|
||||
---
|
||||
layout: layouts/base.njk
|
||||
templateClass: tmpl-mcp
|
||||
---
|
||||
|
||||
{%- macro show_children(item) -%}
|
||||
{%- for child in item | children | sorted('data.order') %}
|
||||
{%- if loop.first -%}<ul>{%- endif -%}
|
||||
<li>
|
||||
<a href="{{ child.url }}">{{ child.data.title }}</a>
|
||||
{%- if page.url.includes(child.url) -%}
|
||||
{{ show_children(child) }}
|
||||
{%- endif -%}
|
||||
{%- if child.url == page.url -%}
|
||||
{{ content | toc(tags=['h2', 'h3']) | stripHash | safe }}
|
||||
{%- endif -%}
|
||||
</li>
|
||||
{%- if loop.last -%}</ul>{%- endif -%}
|
||||
{%- endfor %}
|
||||
{%- endmacro -%}
|
||||
|
||||
<div class="main-container with-sidebar">
|
||||
<aside id="stickySidebar" class="sidebar">
|
||||
{%- set root = '/mcp/index' | find -%}
|
||||
<div id="toc">
|
||||
<div class="header mobile" id="toc-title">{{ root.data.title }}</div>
|
||||
<a class="header" href="{{ root.url }}">{{ root.data.title }}</a>
|
||||
{%- if page.url == root.url -%}
|
||||
{{ content
|
||||
| toc(tags=['h2', 'h3'])
|
||||
| replace('<ol', '<ul')
|
||||
| replace('</ol>', '</ul>')
|
||||
| stripHash
|
||||
| safe }}
|
||||
{%- else -%}
|
||||
{{ show_children(root) }}
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<content class="main-content">
|
||||
{{ content | safe }}
|
||||
</content>
|
||||
|
||||
</div>
|
||||
@ -92,7 +92,8 @@ a[href]:visited {
|
||||
}
|
||||
|
||||
/* href with code in the text */
|
||||
:not(pre) > a > code[class*="language-"] {
|
||||
:not(pre) > a > code[class*="language-"],
|
||||
:not(pre) > a > code {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@ -669,6 +670,13 @@ a[href].post-tag:visited {
|
||||
.main-container .sidebar #toc > ul ol {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
/* fallback for toc structures that render from ol->ul transform */
|
||||
.main-container .sidebar #toc ul ul,
|
||||
.main-container .sidebar #toc ul ol,
|
||||
.main-container .sidebar #toc ol ul,
|
||||
.main-container .sidebar #toc ol ol {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
/* first level */
|
||||
.main-container .sidebar ul li,
|
||||
.main-container .sidebar ul li a {
|
||||
@ -1138,7 +1146,8 @@ table tr:nth-child(odd) {
|
||||
z-index: 2;
|
||||
padding-right: 0;
|
||||
}
|
||||
.main-container.with-sidebar .sidebar #toc > ul {
|
||||
.main-container.with-sidebar .sidebar #toc > ul,
|
||||
.main-container.with-sidebar .sidebar #toc > nav.toc {
|
||||
background: white;
|
||||
border: 1px solid var(--graylight);
|
||||
color: var(--graymedium);
|
||||
@ -1148,7 +1157,12 @@ table tr:nth-child(odd) {
|
||||
box-shadow: 2px 0 10px rgba(68, 68, 68, 0.1);
|
||||
display: none;
|
||||
}
|
||||
.main-container.with-sidebar .sidebar #toc.open > ul{
|
||||
.main-container.with-sidebar .sidebar #toc > nav.toc > ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.main-container.with-sidebar .sidebar #toc.open > ul,
|
||||
.main-container.with-sidebar .sidebar #toc.open > nav.toc{
|
||||
display: block;
|
||||
}
|
||||
.main-container.with-sidebar .sidebar .open .header:before {
|
||||
|
||||
@ -24,7 +24,8 @@ pre[class*="language-"] {
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
}
|
||||
:not(pre) > code[class*="language-"] {
|
||||
:not(pre) > code[class*="language-"],
|
||||
:not(pre) > code {
|
||||
padding: .1em;
|
||||
border-radius: .3em;
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
code[class*="language-"],
|
||||
pre[class*="language-"] {
|
||||
color: #393A34;
|
||||
color: #121212;
|
||||
font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
@ -44,17 +44,20 @@ pre[class*="language-"] {
|
||||
padding: 1em;
|
||||
margin: .5em 0;
|
||||
overflow: auto;
|
||||
border: 1px solid #dddddd;
|
||||
background-color: white;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code[class*="language-"] {
|
||||
:not(pre) > code[class*="language-"],
|
||||
:not(pre) > code {
|
||||
padding: .2em;
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
background: #f8f8f8;
|
||||
border: 1px solid #dddddd;
|
||||
background: #f7f8fa;
|
||||
border: 1px solid #dde3e9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
@ -89,7 +92,7 @@ pre[class*="language-"] {
|
||||
.token.property,
|
||||
.token.regex,
|
||||
.token.inserted {
|
||||
color: #36acaa;
|
||||
color: #0a9a97;
|
||||
}
|
||||
|
||||
.token.atrule,
|
||||
|
||||
BIN
docs/img/home-mcp-server.webp
Normal file
BIN
docs/img/home-mcp-server.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/img/mcp/mcp-enable.webp
Normal file
BIN
docs/img/mcp/mcp-enable.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/img/mcp/mcp-flow.webp
Normal file
BIN
docs/img/mcp/mcp-flow.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
BIN
docs/img/mcp/mcp-generate-key.webp
Normal file
BIN
docs/img/mcp/mcp-generate-key.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/img/mcp/mcp-manage.webp
Normal file
BIN
docs/img/mcp/mcp-manage.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/img/mcp/mcp-server-url.webp
Normal file
BIN
docs/img/mcp/mcp-server-url.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
77
docs/mcp/design-file-structure-best-practices.md
Normal file
77
docs/mcp/design-file-structure-best-practices.md
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Design file structure and best practices
|
||||
order: 4
|
||||
desc: Organize Penpot pages, components, and libraries so humans and MCP-connected agents can navigate your files reliably.
|
||||
---
|
||||
# Design file structure and best practices
|
||||
|
||||
## 1. General file structure
|
||||
|
||||
### 1.1 Organization into boards
|
||||
|
||||
* One board per functional area or feature, not per screen. Example: "Onboarding," "Dashboard," "Settings."
|
||||
* Use the canvas as a logical map: group frames horizontally or vertically according to flow or hierarchy (e.g., left -> wireframes, right -> final design).
|
||||
* Avoid "chaotic infinite canvas": every board must have a clear purpose and a visual entry point.
|
||||
|
||||
## 2. Design system and tokens
|
||||
|
||||
### 2.1 Token hierarchy
|
||||
|
||||
* **Tier 1 - Global tokens:** The system base. E.g.: `color.base.neutral.100`, `spacing.base.8`.
|
||||
* **Tier 2 - Semantic tokens:** Assign meaning to the global tokens. E.g.: `color.bg.default`, `color.text.primary`.
|
||||
* **Tier 3 - Component tokens:** Specific to a component or pattern. E.g.: `color.button.primary.bg`.
|
||||
* Maintain the relationships between tiers documented in the design system: specify which tokens can be inherited or modified.
|
||||
* Use local and global variables with hierarchical names (e.g., `color.text.primary`, `radius.button.sm`).
|
||||
* Avoid "hard" (manual) values for colors, typography, or spacing. Everything must originate from tokens.
|
||||
|
||||
## 3. Components and variants
|
||||
|
||||
### 3.1 Organization
|
||||
|
||||
* Group components by functional categories, not visual ones: Button, FormField, Card.
|
||||
* Maintain semantic and consistent naming: `button/primary/default`, `button/primary/hover`, `form/input/text/focus`.
|
||||
* Use variants only when the differences are part of the same pattern (do not mix distinct components under one variant).
|
||||
|
||||
### 3.2 Composition
|
||||
|
||||
* Avoid excessive nesting of frames. Maintain a visual depth of 3-4 levels maximum.
|
||||
* Use layout (Flex or Grid) logically:
|
||||
* Flex for linear stacks (buttons, lists, forms).
|
||||
* Grid for predictable structures (cards, galleries, dashboards).
|
||||
* Define clear constraints or stretch behaviors for every component.
|
||||
|
||||
## 4. Naming and semantics
|
||||
|
||||
### 4.1 Layers
|
||||
|
||||
* Name layers by function, not appearance: ❌ `rectangle 23` -> ✅ `background`, `icon`, `title`.
|
||||
* Avoid duplicating context in names. If the component is named `button`, its internal layers should not start with `button-...`.
|
||||
|
||||
### 4.2 Hierarchy
|
||||
|
||||
* Use hierarchical names with `/` to group components: `form/input/text`, `form/input/checkbox`.
|
||||
|
||||
## 5. Layout and visual structure
|
||||
|
||||
### 5.1 Responsive layout
|
||||
|
||||
* Apply layout to the majority of containers.
|
||||
* Adjust padding, spacing, and alignment from the layout panel, not with invisible rectangles.
|
||||
* Avoid fixed boards except for graphic or decorative elements.
|
||||
|
||||
### 5.2 Grid and spacing
|
||||
|
||||
* Define a base spacing unit (for example, 8px) and derive all margins and paddings from it.
|
||||
* Use column grids on complex pages, but flex layout for internal structures.
|
||||
|
||||
## 6. Accessibility and consistency
|
||||
|
||||
* Maintain sufficient contrast between text and background (WCAG AA minimum).
|
||||
* Use typography in predefined scales (8, 10, 12, 14, 16, 20...).
|
||||
* Avoid the arbitrary use of color to communicate status without textual support.
|
||||
|
||||
## 7. Export and collaboration
|
||||
|
||||
* Prepare boards with consistent names for handoff (`/screens/login`, `/components/button`).
|
||||
* Use component and variable names that make sense to developers.
|
||||
* Avoid duplicates: establish a single source of truth per component or style.
|
||||
95
docs/mcp/good-prompting-practices-design.md
Normal file
95
docs/mcp/good-prompting-practices-design.md
Normal file
@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Good prompting practices (design)
|
||||
order: 2
|
||||
desc: Write clearer prompts when using AI with Penpot designs - scope, context, and safe iteration with MCP.
|
||||
---
|
||||
# Good prompting practices (design)
|
||||
|
||||
## AI isn't magic, it's a good brief (a guide for designers)
|
||||
|
||||
I've been wrestling with AI agents for a while now to integrate them into my design workflow, and the conclusion is the same as always: **garbage in, garbage out**. We can't ask a machine for something we don't know how to explain to a human.
|
||||
|
||||
### 1. The role: define it like you're hiring
|
||||
|
||||
Telling the AI "Act like a UX/UI designer" is not useful. It's too vague. Would you post a job offer on LinkedIn with just that? Give it seniority and clear boundaries.
|
||||
|
||||
* **Bad:** "You are a creative designer."
|
||||
* **Good:** "You are a Senior Product Designer. You are an expert in design systems, accessibility (WCAG), and you understand the connection between Penpot and code. You do not make business decisions without data."
|
||||
|
||||
If you don't set limits, the AI hallucinates. If you wouldn't hire someone with a vague description, don't expect the agent to work with one.
|
||||
|
||||
### 2. The prompt is a structured brief
|
||||
|
||||
Forget about writing pretty prose. We need structure. A good design prompt needs to look like a well-made Jira ticket:
|
||||
|
||||
* **Context:** What product is it, who is the user?
|
||||
* **Goal:** What problem are we solving today? (e.g., "Improve the visual hierarchy of form X").
|
||||
* **Inputs:** Your frames, tokens, and documentation go here.
|
||||
* **Constraints:** "Only existing components," "WCAG AA," "no inventing colors."
|
||||
* **Quality Criteria:** How do I know the work is finished?
|
||||
|
||||
Less inspirational adjectives, more data.
|
||||
|
||||
### 3. Images are not optional
|
||||
|
||||
This is visual design. If you don't attach screenshots, you are sabotaging the process. But be careful: just dropping the image isn't enough. You have to direct the agent's gaze:
|
||||
|
||||
> "In image 1, look only at the negative space between the label and the input. In image 2, ignore the header and focus on the table."
|
||||
|
||||
The AI "sees," but it doesn't know what matters unless you tell it.
|
||||
|
||||
### 4. Documentation: summarize and conquer
|
||||
|
||||
Sending a link to the documentation and expecting it to read the whole thing is naive. You have to chew it for them. If you use Penpot, give it the rules of the game:
|
||||
|
||||
* **Color:** "Use only tokens from /core/colors".
|
||||
* **Type:** "Don't stray from the defined typographic scale".
|
||||
* **Components:** "Don't duplicate, use variants".
|
||||
|
||||
This turns the agent into someone who respects the system, not a cowboy who breaks the rules because "it looks prettier."
|
||||
|
||||
### 5. Work via transformations, not "final results"
|
||||
|
||||
If you tell it "redesign this dashboard to make it modern," it will return a generic template. It's better to ask for concrete actions on what you already have:
|
||||
|
||||
* Adjust the visual hierarchy.
|
||||
* Reduce noise by eliminating unnecessary borders.
|
||||
* Normalize spacings using our tokens.
|
||||
|
||||
We want something actionable, not a Dribbble image that's impossible to code.
|
||||
|
||||
### 6. Design-to-code: speak the development language
|
||||
|
||||
If the goal is to go from Penpot to code, the agent needs to know what it's getting into. Tell it the framework (React, Vue...), the styling strategy (Tailwind, CSS variables...), and forbid magic numbers.
|
||||
|
||||
> "Generate the structure for React based on this frame. Map Penpot tokens to CSS variables. Don't invent breakpoints."
|
||||
|
||||
This avoids the classic "looks great in design, impossible to implement in production."
|
||||
|
||||
### 7. Ask for the "why," not just the "what"
|
||||
|
||||
A designer who can't justify their decisions is dangerous. The AI is the same. Always ask it to explain its changes: the trade-offs, what it discarded, and why. This helps you audit the result and see whether it understood the problem or was just guessing.
|
||||
|
||||
### 8. Iterate; don't look for the "one-shot"
|
||||
|
||||
The perfect prompt doesn't exist. What exists is the workflow cycle. Design the conversation: first analysis, then proposal, then feedback, and finally preparation for handoff.
|
||||
|
||||
If you try to get it to do everything in the first message, the result will be mediocre.
|
||||
|
||||
### 9. Tell it what NOT to do (negatives)
|
||||
|
||||
This works like a charm and almost no one does it. Set barriers so it doesn't go off the rails:
|
||||
|
||||
* "Don't add new navigation patterns."
|
||||
* "Don't touch the visual identity."
|
||||
* "Don't assume product decisions."
|
||||
|
||||
Limits, paradoxically, improve the quality of the proposal.
|
||||
|
||||
### 10. Penpot is the competitive advantage here
|
||||
|
||||
This is key. Penpot works well with AI because its structure (tokens, SVG, open standards) is much more "readable" for a machine than other proprietary formats. If you feed the agent good tokens and clear conventions, you can almost treat it like another team member. *(We're still figuring out the best way to export a design to give the best context to the AI... a .penpot file? Something else?)*
|
||||
|
||||
***
|
||||
|
||||
This isn't about writing beautifully - it's about reducing ambiguity. If you treat AI like a "creative magic box," you'll get chaos. If you treat it like a sharp designer who needs context and rules, it will take a lot of grunt work off your plate.
|
||||
455
docs/mcp/index.md
Normal file
455
docs/mcp/index.md
Normal file
@ -0,0 +1,455 @@
|
||||
---
|
||||
title: Penpot MCP server
|
||||
order: 1
|
||||
desc: Installing and using the Penpot MCP server with any AI agent or LLM you trust.
|
||||
---
|
||||
|
||||
<div class="main-illus">
|
||||
<img src="/img/home-mcp-server.webp" alt="Penpot MCP server" border="0">
|
||||
</div>
|
||||
|
||||
|
||||
# Penpot MCP server
|
||||
|
||||
Installing and using the Penpot MCP server with any AI agent or LLM you trust.
|
||||
|
||||
## What you can do with Penpot MCP server
|
||||
|
||||
Penpot MCP server connects an MCP-compatible AI client to your Penpot files. Once connected, an AI agent can interact with the design in natural language and help with both design and development tasks.
|
||||
|
||||
Penpot MCP enables **multi-directional workflows** between design and code. Because your agent can read and modify your Penpot file structure (components, styles, tokens, pages, layers, etc.), you can automate both creative and “maintenance” work.
|
||||
|
||||
<iframe
|
||||
title="Quick demo: Penpot MCP server in action"
|
||||
width="100%"
|
||||
height="480"
|
||||
src="https://www.youtube.com/embed/CfvcgMQEmLk?rel=0"
|
||||
loading="lazy"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
|
||||
### Common use cases
|
||||
|
||||
#### Design tasks
|
||||
|
||||
* **Create spacing/typography/color tokens** and apply them consistently.
|
||||
* **Generate variants** (and keep component sets tidy as they grow).
|
||||
* **Rename layers** to match a naming scheme (or audit naming consistency).
|
||||
* **Organize components** and file structure (pages, groups, libraries).
|
||||
* **Audit a design system** for consistency/redundancy (styles, components, usage).
|
||||
* **Apply broad visual changes** (for example, palette updates) across a file.
|
||||
* **Create new screens** based on an existing design system (design-to-design).
|
||||
|
||||
#### Developer tasks
|
||||
|
||||
* **Extract layout structure** and key UI metadata from a page.
|
||||
* **Generate HTML/CSS** (semantic and modular) from a design (design-to-code).
|
||||
* **Inspect tokens** and styles to translate them into code variables.
|
||||
* **Export assets** (for example, only icons used in a file).
|
||||
* **Map components to code** by aligning names/identifiers and documenting rules.
|
||||
* **Update frontend styles** based on design changes (and sync back when needed).
|
||||
* **Prototype interactions** and validate design-to-code translation quality.
|
||||
|
||||
Watch more applications in the **[Penpot MCP video playlist](https://www.youtube.com/watch?v=CfvcgMQEmLk&list=PLgcCPfOv5v57SKMuw1NmS0-lkAXevpn10)**.
|
||||
|
||||
## How Penpot MCP works
|
||||
|
||||
### Architecture and data flow
|
||||
|
||||
There are three key pieces:
|
||||
|
||||
* **MCP server**: a service that exposes tools to your AI client. It receives requests from the client and forwards them to Penpot.
|
||||
* **MCP plugin in Penpot**: a plugin that runs inside Penpot and connects your open file to the MCP server. It is what allows the server to access the currently focused page.
|
||||
* **MCP client**: the tool where you write prompts (Cursor, Claude Code, Copilot-style tools, etc.). It connects to the MCP server using a server URL and an MCP key (or your active Penpot session in the current local setup).
|
||||
|
||||

|
||||
|
||||
### Basic concepts
|
||||
|
||||
Some important concepts for users:
|
||||
* **Integrations page**: MCP is configured under **Your account → Integrations → MCP Server (Beta)**. Here you enable or disable MCP, get the server URL and manage the MCP key.
|
||||
* **MCP key**: a personal, non-recoverable token that authenticates your AI client with the MCP server. Only one key can exist per user at a time. This is used by the remote MCP setup.
|
||||
* **Currently focused page**: MCP always operates on the page you have in focus in Penpot. If you change the focused page (even in another browser window), the MCP context follows that page.
|
||||
* **Active MCP tab**: MCP can only be active in one browser tab at a time. If you have Penpot open in several tabs, you choose explicitly which one owns MCP before running agents.
|
||||
|
||||
### Tools and capabilities
|
||||
|
||||
The Penpot MCP server exposes tools for reading and writing to design files. For the most part you won't need to use them directly—your AI agent will handle the tool calls based on your prompts.
|
||||
|
||||
Current tools in **local MCP**:
|
||||
|
||||
* `execute_code`
|
||||
* `high_level_overview`
|
||||
* `penpot_api_info`
|
||||
* `export_shape`
|
||||
* `import_image`
|
||||
|
||||
Because **remote MCP** does not expose local file-system access:
|
||||
|
||||
* `import_image` from local paths is not available.
|
||||
* `export_shape` is available, but limited compared to local mode (for example, no direct export to local file paths).
|
||||
|
||||
<div class="advice">
|
||||
|
||||
### Agents can edit designs
|
||||
|
||||
**Be mindful:** when MCP is connected, your AI client can run **write operations** that change the currently focused Penpot page (create, rename, move, delete, restyle, etc.). To stay safe:
|
||||
|
||||
* Start with **read-only** actions (inspect, list, export) to verify your setup.
|
||||
* Ask the agent to **describe the intended changes** before applying them.
|
||||
* Prefer **small, reversible steps** over large “refactor everything” requests.
|
||||
</div>
|
||||
|
||||
***
|
||||
|
||||
## Quick start
|
||||
|
||||
If you just want to try Penpot MCP quickly, follow this path for the **hosted (remote) MCP server**.
|
||||
|
||||
### Remote MCP in 5 steps
|
||||
|
||||
1. #### Enable MCP in Penpot
|
||||
Go to **Your account → Integrations → MCP Server (Beta)** and enable the feature.
|
||||
|
||||

|
||||
|
||||
2. #### Generate your MCP key
|
||||
If you do not have one yet, create it. The key is shown only once—store it safely.
|
||||
|
||||

|
||||
|
||||
3. #### Copy the server URL
|
||||
In the same Integrations section, copy the **server URL** that already includes your MCP key as `userToken`.
|
||||
|
||||

|
||||
|
||||
4. #### Add the server to your MCP client
|
||||
In your MCP-aware IDE/agent (Cursor, Claude Code, etc.), add a new server pointing to that URL.
|
||||
**Example (generic JSON config):**
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"penpot": {
|
||||
"url": "https://<your-penpot-domain>/mcp/stream?userToken=YOUR_MCP_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
5. #### Open a Penpot file and connect MCP
|
||||
In Penpot, open a design file and use **File → MCP Server → Connect** to connect the plugin to your current file.
|
||||
|
||||

|
||||
|
||||
Once all five steps are done, your AI client should list Penpot tools.
|
||||
|
||||
### First prompts to try
|
||||
|
||||
After connecting, start with **read-only prompts** to confirm everything works and to understand what the agent can see:
|
||||
|
||||
* "List pages in this file."
|
||||
* "Show all components on this page."
|
||||
* "Analyze the structure of this design and summarize it."
|
||||
* "List the color styles and explain how they are used."
|
||||
|
||||
When you are comfortable with the responses, you can move on to **light write operations**, for example:
|
||||
|
||||
* "Create a color token set for primary colors based on this page."
|
||||
* "Rename layers on this page to follow a consistent naming scheme. Describe what you will change before applying it."
|
||||
|
||||
|
||||
<div class="advice">
|
||||
|
||||
### Model quality advice
|
||||
|
||||
For best results, use a strong model and a high-quality inference setup.
|
||||
|
||||
Using a vision-language model (VLM) is required to enable image understanding; most commercially provided LLMs are VLMs.
|
||||
|
||||
In any case, we recommend always using **frontier models**. The more complex the task is, the more the model will highly influence the quality of the results.
|
||||
|
||||
</div>
|
||||
|
||||
### Remote vs local MCP
|
||||
|
||||
You can use Penpot MCP server in two main ways:
|
||||
|
||||
* **Remote MCP server**
|
||||
* Hosted for you (no need to run anything on your machine).
|
||||
* Best option for most users, simpler installation and fewer moving parts.
|
||||
* Does **not** have privileged access to your local file system, it can only work with what Penpot exposes (design files, libraries, tokens, etc.).
|
||||
* The **server URL** is provided in **Your account → Integrations → MCP Server (Beta)** and looks like:
|
||||
* `https://<your-penpot-domain>/mcp/stream?userToken=YOUR_MCP_KEY`
|
||||
* The domain depends on the Penpot installation. In the official SaaS it will be `design.penpot.app`.
|
||||
* **Local MCP server**
|
||||
* Runs on your own machine.
|
||||
* Intended for advanced users who are comfortable using the terminal.
|
||||
* Can offer extra capabilities such as controlled access to the local file system (for example, reading or writing asset files), depending on configuration.
|
||||
* You can start it locally via an npm package without cloning the full Penpot repository.
|
||||
|
||||
***
|
||||
|
||||
## Connect your MCP client
|
||||
|
||||
Use the same client setup flow for both modes. What changes is the server URL and authentication method.
|
||||
|
||||
### Connection values by mode
|
||||
|
||||
* **Remote MCP**
|
||||
* URL: `https://<your-penpot-domain>/mcp/stream?userToken=YOUR_MCP_KEY`
|
||||
* Auth: MCP key in `userToken`
|
||||
* **Local MCP**
|
||||
* URL: `http://localhost:4401/mcp`
|
||||
* Auth: none (uses your active Penpot browser session)
|
||||
|
||||
### Cursor
|
||||
|
||||
1. Open Cursor MCP/tool configuration.
|
||||
2. Add a Penpot MCP server entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"penpot": {
|
||||
"url": "REMOTE_OR_LOCAL_URL",
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace `REMOTE_OR_LOCAL_URL` with the URL for your mode.
|
||||
|
||||
### Claude Code
|
||||
|
||||
1. Open MCP configuration in Claude Code.
|
||||
2. Add a Penpot server with `http` transport and the URL for your mode.
|
||||
3. Restart Claude Code or reload tools.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"penpot": {
|
||||
"transport": "http",
|
||||
"url": "REMOTE_OR_LOCAL_URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### VS Code / Copilot
|
||||
|
||||
1. Open external MCP server configuration in your extension/settings.
|
||||
2. Add Penpot with the URL for your mode.
|
||||
3. Save and reload tools.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp.servers": {
|
||||
"penpot": {
|
||||
"transport": "http",
|
||||
"url": "REMOTE_OR_LOCAL_URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Codex / OpenCode etc
|
||||
|
||||
1. Use your client's "Add MCP server" flow.
|
||||
2. Set the URL for your mode.
|
||||
3. Reload tools and verify Penpot tools are available.
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"penpot": {
|
||||
"url": "REMOTE_OR_LOCAL_URL",
|
||||
"transport": {
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Final check
|
||||
|
||||
In Penpot, open a file and connect the plugin from **File → MCP Server → Connect**, then run a read-only prompt first.
|
||||
|
||||
|
||||
***
|
||||
|
||||
## Remote MCP server
|
||||
|
||||
Remote MCP is the easiest way to start using AI agents with Penpot. It's hosted for you, so you don't need to install or run anything on your machine.
|
||||
|
||||
<a id="install-and-activate-remote"></a>
|
||||
### Install and activate
|
||||
|
||||
1. Open **Your account → Integrations**.
|
||||
2. In the **MCP Server (Beta)** section, read the short description to confirm that feature is available for your account.
|
||||
3. Use the **Status** toggle to enable MCP Server. Penpot remembers this state per user across sessions.
|
||||
4. If this is your first time, Penpot will ask you to **generate an MCP key**. The key is shown only once, store it safely.
|
||||
* Treat the MCP key like a password/token: do not share it in screenshots, logs, or code samples.
|
||||
5. Once enabled, you will see:
|
||||
* The **server URL** (used later in your MCP client).
|
||||
* An action to **copy** the URL to the clipboard.
|
||||
* A link to **How to configure MCP clients** (this Help Center content).
|
||||
|
||||
<a id="connect-remote"></a>
|
||||
### Connect
|
||||
|
||||
For client-specific setup, use the shared section **Connect your MCP client**.
|
||||
|
||||
For remote mode, use the URL shown in **Your account → Integrations → MCP Server (Beta)**, which includes your `userToken`.
|
||||
|
||||
<a id="use-remote"></a>
|
||||
### Use
|
||||
|
||||
Once everything is configured, day-to-day use of Penpot MCP follows a simple pattern.
|
||||
|
||||
#### Run
|
||||
|
||||
1. **Enable MCP**
|
||||
* Go to **Your account → Integrations → MCP Server (Beta)** and set **Status** to **Enabled**.
|
||||
2. **Connect plugin**:
|
||||
* Open a design file and use **File → MCP Server → Connect**.
|
||||
3. **Run prompts**:
|
||||
* Open your MCP client and start with read-only prompts first (`list`, `inspect`, `analyze`), then continue with write actions.
|
||||
|
||||
MCP always acts on the **currently focused page** in the active Penpot tab.
|
||||
|
||||
#### Manage
|
||||
|
||||
Most management happens in **Your account → Integrations → MCP Server**.
|
||||
|
||||
**Enable or disable MCP Server**
|
||||
|
||||
Use the **Status** toggle to enable or disable MCP Server. When disabling, Penpot shows a confirmation message (for example, `MCP server successfully disabled`).
|
||||
|
||||
**View and copy the server URL**
|
||||
|
||||
The information block explains what the URL is for and lets you **Copy link**. Copying the URL shows a confirmation toast (for example, `Link copied to clipboard`).
|
||||
|
||||
**Create MCP key**
|
||||
|
||||
If no key exists and you enable MCP, a modal guides you through creating one. The modal explains that the key is required for configuring clients and is shown only once. After creating the key, Penpot displays the MCP key itself (copy it safely, it won't be shown again), the expiration date for the key, and a ready-to-use configuration snippet in JSON format that you can copy directly into your MCP client configuration file. The configuration includes the server URL with your MCP key embedded as the `userToken` parameter.
|
||||
|
||||
**Regenerate MCP key**
|
||||
|
||||
The **Regenerate MCP key** action immediately revokes the current key. A warning explains that any client using the old key will stop working until you update its configuration. After regenerating, Penpot shows the new key and an updated configuration snippet with the new `userToken` that you need to copy into your MCP client configuration.
|
||||
|
||||
**Expired key state**
|
||||
|
||||
If your key is expired, an error block explains that the connection cannot be established until you regenerate the key. Regenerating the key clears the error state.
|
||||
|
||||
Security recommendations to highlight in the Help Center:
|
||||
|
||||
* Treat your MCP key like a password or access token, do not share it in screenshots or code samples.
|
||||
* Regenerate the key if you suspect it may have leaked.
|
||||
* Remember that disabling MCP Server or disconnecting the plugin stops agents from modifying your files, even if a client is still configured.
|
||||
|
||||
***
|
||||
|
||||
## Local MCP server
|
||||
|
||||
Local MCP is for users who want more control or need access to local resources. It runs on your own machine and requires some technical setup.
|
||||
|
||||
|
||||
|
||||
<a id="install-and-activate-local"></a>
|
||||
### Install and activate
|
||||
|
||||
Use npm as the recommended setup path.
|
||||
|
||||
At a high level:
|
||||
|
||||
1. Make sure you have Node.js installed (tested with `v22`; `v20` should also work).
|
||||
2. Start the MCP server and plugin server from your terminal:
|
||||
|
||||
```json
|
||||
npx @penpot/mcp@beta
|
||||
```
|
||||
|
||||
Leave this terminal running while you use MCP.
|
||||
|
||||
3. Open `https://design.penpot.app` and any design file.
|
||||
|
||||
4. Go to **Plugins → Load from URL** and use: `http://localhost:4400/manifest.json`.
|
||||
|
||||
5. Run the plugin and click **Connect to MCP server**.
|
||||
|
||||
6. Make sure the plugin shows **Connected** and keep the plugin window open while working with AI agents.
|
||||
|
||||
> Some Chromium-based browsers may block the connection from `https://design.penpot.app` to `http://localhost`. If that happens, explicitly allow local network access or use a browser like Firefox.
|
||||
|
||||
For advanced or repository-based workflows, see the [MCP README](https://github.com/penpot/penpot/blob/main/mcp/README.md) in the Penpot repository.
|
||||
|
||||
<a id="connect-local"></a>
|
||||
### Connect
|
||||
|
||||
For client-specific setup, use the shared section **Connect your MCP client**.
|
||||
|
||||
For local mode, use `http://localhost:4401/mcp` with HTTP transport (no MCP key; authentication uses your active Penpot browser session).
|
||||
|
||||
<a id="use-local"></a>
|
||||
### Use
|
||||
|
||||
Once everything is configured, day-to-day use of Penpot MCP follows a simple pattern.
|
||||
|
||||
#### Run
|
||||
|
||||
1. **Start MCP**
|
||||
|
||||
Run `npx -y @penpot/mcp@stable` (production) or `npx -y @penpot/mcp@beta` (test), and keep that terminal running.
|
||||
2. **Connect plugin**
|
||||
|
||||
In Penpot, load `http://localhost:4400/manifest.json`, run the plugin, and click **Connect to MCP server**.
|
||||
3. **Run prompts**
|
||||
|
||||
Open your MCP client and start with read-only prompts first (`list`, `inspect`, `analyze`), then continue with write actions.
|
||||
|
||||
MCP always acts on the **currently focused page** in the active Penpot tab.
|
||||
|
||||
#### Manage
|
||||
|
||||
For local MCP setups, management is simpler.
|
||||
|
||||
**Start or stop the MCP server**
|
||||
|
||||
Use the start command from the **Run** section above. To stop the server, press `Ctrl+C` in the terminal where it is running. If the process does not stop cleanly, close the remaining Node.js process from your system process manager (or terminate it by killing the processes in the terminal).
|
||||
|
||||
**Restart the plugin connection**
|
||||
|
||||
If the plugin disconnects, reload it from `http://localhost:4400/manifest.json` in Penpot and click **Connect to MCP server** again.
|
||||
|
||||
***
|
||||
|
||||
## Help & Guides
|
||||
|
||||
These resources complement the MCP server documentation:
|
||||
|
||||
* <a href="/mcp/good-prompting-practices-design/" target="_blank" rel="noopener">Good prompting practices (design)</a>
|
||||
* <a href="/mcp/prompting-token-aware/" target="_blank" rel="noopener">Prompting in a token-aware way</a>
|
||||
* <a href="/mcp/design-file-structure-best-practices/" target="_blank" rel="noopener">Design file structure and best practices</a>
|
||||
* <a href="https://www.youtube.com/watch?v=CfvcgMQEmLk&list=PLgcCPfOv5v57SKMuw1NmS0-lkAXevpn10" target="_blank" rel="noopener">Penpot MCP video playlist</a>
|
||||
|
||||
***
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**If connections fail**, try this checklist:
|
||||
|
||||
* Restart the MCP server process.
|
||||
* Restart the plugin connection in Penpot.
|
||||
* Restart your MCP client (Cursor, Claude Code, etc.), or trigger an MCP server reconnection from the client if available.
|
||||
* Keep the plugin window open in Penpot while using MCP.
|
||||
|
||||
**Found an issue** or something you think could be improved? You have several options:
|
||||
|
||||
* Open an issue [on GitHub](https://github.com/penpot/penpot/issues/new/choose)
|
||||
* Open a thread at [the Penpot Community](https://community.penpot.app/)
|
||||
* Write us at [support@penpot.app](mailto:support@penpot.app)
|
||||
4
docs/mcp/mcp.json
Normal file
4
docs/mcp/mcp.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "layouts/mcp.njk",
|
||||
"tags": "mcp"
|
||||
}
|
||||
142
docs/mcp/prompting-token-aware.md
Normal file
142
docs/mcp/prompting-token-aware.md
Normal file
@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Prompting in a token-aware way
|
||||
order: 3
|
||||
desc: Use Penpot color, typography, and design tokens effectively when prompting an MCP-connected AI agent.
|
||||
---
|
||||
# Prompting "token-aware"
|
||||
|
||||
## 1. Where the tokens go
|
||||
|
||||
In this type of prompt, token usage spikes for four clear reasons:
|
||||
|
||||
1. **Repeated natural-language instructions**
|
||||
* "Do not invent..."
|
||||
* "If something is missing..."
|
||||
* "Prefer X over Y..."
|
||||
These rules are fine, but written as prose they are expensive.
|
||||
2. **Verbose field lists**
|
||||
* Repeating "Include: ..." with full sentences.
|
||||
* Explaining the *why* instead of the *what*.
|
||||
3. **Unnecessary Markdown**
|
||||
* Headings, complete sentences, long explanations.
|
||||
* The LLM doesn't need "human readability".
|
||||
4. **Artificial separation between global rules and per-file rules**
|
||||
* Many rules get repeated with different words.
|
||||
|
||||
👉 The LLM **doesn't need narrative context**; it needs **structured contracts**.
|
||||
|
||||
***
|
||||
|
||||
## 2. Key principle to reduce tokens without losing quality
|
||||
|
||||
> **Turn prose into a grammar.**
|
||||
|
||||
That means:
|
||||
|
||||
* fewer sentences
|
||||
* more **compact bullet points**
|
||||
* more **pseudo-schema**
|
||||
* more **stable keywords**
|
||||
* fewer linguistic connectors
|
||||
|
||||
LLMs respond very well to:
|
||||
|
||||
* terse enumerations
|
||||
* "MUST / MUST NOT"
|
||||
* schema/checklist-style structures
|
||||
|
||||
***
|
||||
|
||||
## 3. What you should NOT cut
|
||||
|
||||
There are things you **shouldn't cut**, because they save tokens *in the long run* (fewer retries, less drift):
|
||||
|
||||
* The exact definition of the 5 files
|
||||
* The "do not invent" rule
|
||||
* The distinction between mockups vs. the real system
|
||||
* The hierarchy tokens → components → layout → screens
|
||||
|
||||
That's the skeleton. If you change it, the output gets worse and you'll end up spending more tokens fixing it.
|
||||
|
||||
***
|
||||
|
||||
## 4. Concrete optimizations (actionable)
|
||||
|
||||
### A. Collapse global rules into a "RULESET"
|
||||
|
||||
Instead of repeating rules in every section:
|
||||
|
||||
```
|
||||
GLOBAL RULESET - SOURCE = Penpot MCP only - NO_GUESSING = true - IF_MISSING -> TODO - PREFER = structured data - SCREENSHOTS = visual reference only - OUTPUT = deterministic, stable ordering
|
||||
```
|
||||
|
||||
This removes a lot of repeated text and the LLM will still respect it.
|
||||
|
||||
***
|
||||
|
||||
### B. Replace sentences with compact schemas
|
||||
|
||||
Example BEFORE:
|
||||
|
||||
> "Include font families, sizes, line heights, weights, and letter spacing."
|
||||
|
||||
AFTER:
|
||||
|
||||
```
|
||||
typography: fontFamily fontSize lineHeight fontWeight letterSpacing
|
||||
```
|
||||
|
||||
Fewer tokens, same information, clearer for the model.
|
||||
|
||||
***
|
||||
|
||||
### C. Remove "pedagogical" explanations
|
||||
|
||||
Things like:
|
||||
|
||||
* "This file is important because..."
|
||||
* "The goal of this is to..."
|
||||
|
||||
👉 They add nothing to the output. Remove them.
|
||||
|
||||
***
|
||||
|
||||
### D. Convert Markdown into structured plain text
|
||||
|
||||
`##`, `###`, etc. **don't help** the LLM and they cost tokens.
|
||||
|
||||
Use:
|
||||
|
||||
```
|
||||
[FILE 3] layout-and-rules.md
|
||||
```
|
||||
|
||||
instead of long headers.
|
||||
|
||||
***
|
||||
|
||||
### E. Explicitly limit the output size
|
||||
|
||||
This is **key** and many people forget it.
|
||||
|
||||
Add rules like:
|
||||
|
||||
```
|
||||
SIZE CONSTRAINTS - design-system.json: concise, no comments - components.catalog.json: components only, no explanations - layout-and-rules.md: max ~300 lines - screens.reference.json: max 6 screens
|
||||
```
|
||||
|
||||
The model **will try to comply** with these limits and reduce verbosity.
|
||||
|
||||
***
|
||||
|
||||
## 5. Optimized prompt ("token-aware" version)
|
||||
|
||||
I won't rewrite the entire prompt again (you already have it), but here's the **key block** I'd add or use as a replacement:
|
||||
|
||||
```
|
||||
GLOBAL RULESET - SOURCE: Penpot MCP - NO_GUESSING - IF_MISSING: TODO - PREFER: structured data > prose - SCREENSHOTS: visual context only - OUTPUT: deterministic, stable, minimal SIZE CONSTRAINTS - MAX FILES: 5 - design-system.json: tokens + mappings only - components.catalog.json: real components only - layout-and-rules.md: concise rules, no explanations - screens.reference.json: 3-6 screens max - README.md: brief, no marketing STYLE - Use schemas, lists, and key:value forms - Avoid narrative explanations - Avoid redundant wording
|
||||
```
|
||||
|
||||
With just this, in real tests, usage **drops by ~20-35%** depending on the model.
|
||||
|
||||
|
||||
@ -135,6 +135,26 @@
|
||||
(defn has-error-code? [error-key errors]
|
||||
(some #(= (:error/code %) error-key) errors))
|
||||
|
||||
(defn resolve-error-message
|
||||
"Returns the human-readable message string for a single error map.
|
||||
When the error carries an :error/fn key the function is called with
|
||||
:error/value to produce the message. Falls back to :message for
|
||||
errors that originate from schema-validation (which have no :error/fn)."
|
||||
[error]
|
||||
(if-let [f (:error/fn error)]
|
||||
(f (:error/value error))
|
||||
(:message error)))
|
||||
|
||||
(defn resolve-error-assoc-message
|
||||
"Returns the error map with a :message key set to the resolved human-
|
||||
readable string. When the error carries an :error/fn key the function
|
||||
is called with :error/value; otherwise the map is returned unchanged
|
||||
(it is expected to already carry a :message from schema-validation)."
|
||||
[error]
|
||||
(if-let [f (:error/fn error)]
|
||||
(assoc error :message (f (:error/value error)))
|
||||
error))
|
||||
|
||||
(defn humanize-errors [errors]
|
||||
(->> errors
|
||||
(map (fn [err]
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tinycolor :as tinycolor]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.data.workspace.tokens.format :as dwtf]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.ds.controls.input :as ds]
|
||||
@ -277,9 +278,7 @@
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
((:error/fn error) (:error/value error))))))
|
||||
(d/update-when result :error wte/resolve-error-message)))
|
||||
(rx/subs! (fn [{:keys [error value]}]
|
||||
(let [touched? (get-in @form [:touched input-name])]
|
||||
(when touched?
|
||||
@ -439,9 +438,7 @@
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
(assoc error :message ((:error/fn error) (:error/value error)))))))
|
||||
(d/update-when result :error wte/resolve-error-assoc-message)))
|
||||
|
||||
(rx/subs!
|
||||
(fn [{:keys [error value]}]
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
[app.config :as cf]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
@ -168,9 +169,7 @@
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
((:error/fn error) (:error/value error))))))
|
||||
(d/update-when result :error wte/resolve-error-message)))
|
||||
(rx/subs! (fn [{:keys [error value]}]
|
||||
(when touched?
|
||||
(if error
|
||||
@ -291,9 +290,7 @@
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
((:error/fn error) (:error/value error))))))
|
||||
(d/update-when result :error wte/resolve-error-message)))
|
||||
(rx/subs!
|
||||
(fn [{:keys [error value]}]
|
||||
(cond
|
||||
|
||||
@ -241,9 +241,7 @@
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
((:error/fn error) (:error/value error))))))
|
||||
(d/update-when result :error wte/resolve-error-message)))
|
||||
(rx/subs! (fn [{:keys [error value]}]
|
||||
(let [touched? (get-in @form [:touched input-name])]
|
||||
(when touched?
|
||||
@ -333,9 +331,7 @@
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
(assoc error :message ((:error/fn error) (:error/value error)))))))
|
||||
(d/update-when result :error wte/resolve-error-assoc-message)))
|
||||
|
||||
(rx/subs!
|
||||
(fn [{:keys [error value]}]
|
||||
@ -445,9 +441,7 @@
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
(assoc error :message ((:error/fn error) (:error/value error)))))))
|
||||
(d/update-when result :error wte/resolve-error-assoc-message)))
|
||||
|
||||
(rx/subs!
|
||||
(fn [{:keys [error value]}]
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
[frontend-tests.tokens.logic.token-data-test]
|
||||
[frontend-tests.tokens.logic.token-remapping-test]
|
||||
[frontend-tests.tokens.style-dictionary-test]
|
||||
[frontend-tests.tokens.token-errors-test]
|
||||
[frontend-tests.tokens.workspace-tokens-remap-test]
|
||||
[frontend-tests.util-object-test]
|
||||
[frontend-tests.util-range-tree-test]
|
||||
@ -49,6 +50,7 @@
|
||||
'frontend-tests.tokens.logic.token-data-test
|
||||
'frontend-tests.tokens.logic.token-remapping-test
|
||||
'frontend-tests.tokens.style-dictionary-test
|
||||
'frontend-tests.tokens.token-errors-test
|
||||
'frontend-tests.util-object-test
|
||||
'frontend-tests.util-range-tree-test
|
||||
'frontend-tests.util-simple-math-test
|
||||
|
||||
53
frontend/test/frontend_tests/tokens/token_errors_test.cljs
Normal file
53
frontend/test/frontend_tests/tokens/token_errors_test.cljs
Normal file
@ -0,0 +1,53 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns frontend-tests.tokens.token-errors-test
|
||||
(:require
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; resolve-error-message
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest resolve-error-message-with-error-fn
|
||||
(t/testing "calls :error/fn with :error/value when both keys are present"
|
||||
(let [error {:error/fn (fn [v] (str "bad value: " v))
|
||||
:error/value "abc"}]
|
||||
(t/is (= "bad value: abc" (wte/resolve-error-message error))))))
|
||||
|
||||
(t/deftest resolve-error-message-without-error-fn
|
||||
(t/testing "returns :message when :error/fn is absent (schema-validation error)"
|
||||
(let [error {:message "This field is required"}]
|
||||
(t/is (= "This field is required" (wte/resolve-error-message error))))))
|
||||
|
||||
(t/deftest resolve-error-message-nil-error-fn
|
||||
(t/testing "returns :message when :error/fn is explicitly nil"
|
||||
(let [error {:error/fn nil :message "fallback message"}]
|
||||
(t/is (= "fallback message" (wte/resolve-error-message error))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; resolve-error-assoc-message
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(t/deftest resolve-error-assoc-message-with-error-fn
|
||||
(t/testing "assocs :message produced by :error/fn into the error map"
|
||||
(let [error {:error/fn (fn [v] (str "invalid: " v))
|
||||
:error/value "42"
|
||||
:error/code :error.token/invalid-color}]
|
||||
(let [result (wte/resolve-error-assoc-message error)]
|
||||
(t/is (= "invalid: 42" (:message result)))
|
||||
(t/is (= :error.token/invalid-color (:error/code result)))))))
|
||||
|
||||
(t/deftest resolve-error-assoc-message-without-error-fn
|
||||
(t/testing "returns the error map unchanged when :error/fn is absent"
|
||||
(let [error {:message "This field is required"}]
|
||||
(t/is (= error (wte/resolve-error-assoc-message error))))))
|
||||
|
||||
(t/deftest resolve-error-assoc-message-nil-error-fn
|
||||
(t/testing "returns the error map unchanged when :error/fn is explicitly nil"
|
||||
(let [error {:error/fn nil :message "fallback"}]
|
||||
(t/is (= error (wte/resolve-error-assoc-message error))))))
|
||||
Loading…
x
Reference in New Issue
Block a user