Merge pull request #8310 from oraios/mcp-tokens

 MCP improvements to enable UC2, design token handling
This commit is contained in:
Andrey Antukh
2026-02-12 09:47:32 +01:00
committed by GitHub
10 changed files with 10831 additions and 7087 deletions

View File

@@ -90,32 +90,6 @@ This bootstrap command will:
* build all components (`pnpm -r run build`) * build all components (`pnpm -r run build`)
* start all components (`pnpm -r --parallel run start`) * start all components (`pnpm -r --parallel run start`)
If you want to have types scrapped from a remote repository, the best
approach is executing the following:
```shell
PENPOT_PLUGINS_API_DOC_URL=https://doc.plugins.penpot.app pnpm run build:types
pnpm run bootstrap
```
Or this, if you want skip build step bacause you have already have all
build artifacts ready (per example from previous `bootstrap` command):
```
PENPOT_PLUGINS_API_DOC_URL=https://doc.plugins.penpot.app pnpm run build:types
pnpm run start
```
If you want just to update the types definitions with the plugins api doc from the
current branch:
```shell
pnpm run build:types
```
(That command will build plugins doc locally and will generate the types yaml from
the locally build documentation)
### 2. Load the Plugin in Penpot and Establish the Connection ### 2. Load the Plugin in Penpot and Establish the Connection
> [!NOTE] > [!NOTE]

View File

@@ -5,7 +5,7 @@
"scripts": { "scripts": {
"build": "pnpm -r run build", "build": "pnpm -r run build",
"build:multi-user": "pnpm -r run build:multi-user", "build:multi-user": "pnpm -r run build:multi-user",
"build:types": "./scripts/build-types", "build:types": "bash ./scripts/build-types",
"start": "pnpm -r --parallel run start", "start": "pnpm -r --parallel run start",
"start:multi-user": "pnpm -r --parallel --filter \"./packages/*\" run start:multi-user", "start:multi-user": "pnpm -r --parallel --filter \"./packages/*\" run start:multi-user",
"bootstrap": "pnpm -r install && pnpm run build && pnpm run start", "bootstrap": "pnpm -r install && pnpm run build && pnpm run start",

View File

@@ -25,7 +25,6 @@ export class PenpotUtils {
id: shape.id, id: shape.id,
name: shape.name, name: shape.name,
type: shape.type, type: shape.type,
children: children,
}; };
// add layout information if present // add layout information if present
@@ -48,6 +47,23 @@ export class PenpotUtils {
}; };
} }
// add component instance information if present
if (shape.isComponentInstance()) {
result.componentInstance = {};
const component = shape.component();
if (component) {
result.componentInstance.componentId = component.id;
result.componentInstance.componentName = component.name;
const mainInstance = component.mainInstance();
if (mainInstance) {
result.componentInstance.mainInstanceId = mainInstance.id;
}
}
}
// finally, add children (last for more readable nesting order)
result.children = children;
return result; return result;
} }
@@ -55,9 +71,9 @@ export class PenpotUtils {
* Finds all shapes that matches the given predicate in the given shape tree. * Finds all shapes that matches the given predicate in the given shape tree.
* *
* @param predicate - A function that takes a shape and returns true if it matches the criteria * @param predicate - A function that takes a shape and returns true if it matches the criteria
* @param root - The root shape to start the search from (defaults to penpot.root) * @param root - The root shape to start the search from (if null, searches all pages)
*/ */
public static findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = penpot.root): Shape[] { public static findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape[] {
let result = new Array<Shape>(); let result = new Array<Shape>();
let find = function (shape: Shape | null) { let find = function (shape: Shape | null) {
@@ -74,7 +90,16 @@ export class PenpotUtils {
} }
}; };
find(root); if (root === null) {
const pages = penpot.currentFile?.pages;
if (pages) {
for (let page of pages) {
find(page.root);
}
}
} else {
find(root);
}
return result; return result;
} }
@@ -422,4 +447,94 @@ export class PenpotUtils {
throw new Error(`Unsupported export mode: ${mode}`); throw new Error(`Unsupported export mode: ${mode}`);
} }
} }
/**
* Finds all tokens that match the given name across all token sets.
*
* @param name - The name of the token to search for (case-sensitive exact match)
* @returns An array of all matching tokens (may be empty)
*/
public static findTokensByName(name: string): any[] {
const tokens: any[] = [];
// @ts-ignore
const tokenCatalog = penpot.library.local.tokens;
for (const set of tokenCatalog.sets) {
for (const token of set.tokens) {
if (token.name === name) {
tokens.push(token);
}
}
}
return tokens;
}
/**
* Finds the first token that matches the given name across all token sets.
*
* @param name - The name of the token to search for (case-sensitive exact match)
* @returns The first matching token, or null if not found
*/
public static findTokenByName(name: string): any | null {
// @ts-ignore
const tokenCatalog = penpot.library.local.tokens;
for (const set of tokenCatalog.sets) {
for (const token of set.tokens) {
if (token.name === name) {
return token;
}
}
}
return null;
}
/**
* Gets the token set that contains the given token.
*
* @param token - The token whose set to find
* @returns The TokenSet containing this token, or null if not found
*/
public static getTokenSet(token: any): any | null {
// @ts-ignore
const tokenCatalog = penpot.library.local.tokens;
for (const set of tokenCatalog.sets) {
if (set.tokens.includes(token)) {
return set;
}
}
return null;
}
/**
* Generates an overview of all tokens organized by token set name, token type, and token name.
* The result is a nested object structure: {tokenSetName: {tokenType: [tokenName, ...]}}.
*
* @returns An object mapping token set names to objects that map token types to arrays of token names
*/
public static tokenOverview(): Record<string, Record<string, string[]>> {
const overview: Record<string, Record<string, string[]>> = {};
// @ts-ignore
const tokenCatalog = penpot.library.local.tokens;
for (const set of tokenCatalog.sets) {
const setOverview: Record<string, string[]> = {};
for (const token of set.tokens) {
const tokenType = token.type;
if (!setOverview[tokenType]) {
setOverview[tokenType] = [];
}
setOverview[tokenType].push(token.name);
}
overview[set.name] = setOverview;
}
return overview;
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,333 @@
You have access to Penpot tools in order to interact with a Penpot design project directly.
As a precondition, the user must connect the Penpot design project to the MCP server using the Penpot MCP Plugin.
IMPORTANT: When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
non-creative defaults such as white/black if you are lacking information).
# Executing Code
One of your key tools is the `execute_code` tool, which allows you to run JavaScript code using the Penpot Plugin API
directly in the connected project.
VERY IMPORTANT: When writing code, NEVER LOG INFORMATION YOU ARE ALSO RETURNING. It would duplicate the information you receive!
To execute code correctly, you need to understand the Penpot Plugin API. You can retrieve API documentation via
the `penpot_api_info` tool.
This is the full list of types/interfaces in the Penpot API: $api_types
You use the `storage` object extensively to store data and utility functions you define across tool calls.
This allows you to inspect intermediate results while still being able to build on them in subsequent code executions.
# The Structure of Penpot Designs
A Penpot design ultimately consists of shapes.
The type `Shape` is a union type, which encompasses both containers and low-level shapes.
Shapes in a Penpot design are organized hierarchically.
At the top level, a design project contains one or more `Page` objects.
Each `Page` contains a tree of elements. For a given instance `page`, its root shape is `page.root`.
A Page is frequently structured into boards. A `Board` is a high-level grouping element.
A `Group` is a more low-level grouping element used to organize low-level shapes into a logical unit.
Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`, `Boolean`, and `SvgRaw`.
`ShapeBase` is a base type most shapes build upon.
# Core Shape Properties and Methods
**Type**:
Any given shape contains information on the concrete type via its `type` field.
**Position and Dimensions**:
* The location properties `x` and `y` refer to the top left corner of a shape's bounding box in the absolute (Page) coordinate system.
These are writable - set them directly to position shapes.
* `parentX` and `parentY` (as well as `boardX` and `boardY`) are READ-ONLY computed properties showing position relative to parent/board.
To position relative to parent, use `penpotUtils.setParentXY(shape, parentX, parentY)` or manually set `shape.x = parent.x + parentX`.
* `width` and `height` are READ-ONLY. Use `resize(width, height)` method to change dimensions.
* `bounds` is a READ-ONLY property. Use `x`, `y` with `resize()` to modify shape bounds.
**Other Writable Properties**:
* `name` - Shape name
* `fills`, `strokes` - Styling properties
* `rotation`, `opacity`, `blocked`, `hidden`, `visible`
**Z-Order**:
* The z-order of shapes is determined by the order in the `children` array of the parent shape.
Therefore, when creating shapes that should be on top of each other, add them to the parent in the correct order
(i.e. add background shapes first, then foreground shapes later).
CRITICAL: NEVER use the broken function `appendChild` to achieve this, ALWAYS use `parent.insertChild(parent.children.length, shape)`
* To modify z-order after creation, use these methods: `bringToFront()`, `sendToBack()`, `bringForward()`, `sendBackward()`,
and, for precise control, `setParentIndex(index)` (0-based).
**Modification Methods**:
* `resize(width, height)` - Change dimensions (required for width/height since they're read-only)
* `rotate(angle, center?)` - Rotate shape
* `remove()` - Permanently destroy the shape (use only for deletion, NOT for reparenting).
Exception: When the shape is a descendant of a board that is a component (asset), the shape will not be removed but instead be made invisible.
**Hierarchical Structure**:
* `parent` - The parent shape (null for root shapes)
Note: Hierarchical nesting does not necessarily imply visual containment
* CRITICAL: To add children to a parent shape (e.g. a `Board`):
- ALWAYS use `parent.insertChild(index, shape)` to add a child, e.g. `parent.insertChild(parent.children.length, shape)` to append
- NEVER use `parent.appendChild(shape)` as it is BROKEN and will not insert in a predictable place (except in flex layout boards)
* Reparenting: `newParent.appendChild(shape)` or `newParent.insertChild(index, shape)` will move a shape to new parent
- Automatically removes the shape from its old parent
- Absolute x/y positions are preserved (use `penpotUtils.setParentXY` to adjust relative position)
Cloning: Use `shape.clone(): Shape` to create an exact duplicate (including all properties and children) of a shape; same position as original.
# Images
The `Image` type is a legacy type. Images are now typically embedded in a `Fill`, with `fillImage` set to an
`ImageData` object, i.e. the `fills` property of of a shape (e.g. a `Rectangle`) will contain a fill where `fillImage` is set.
Use the `export_shape` and `import_image` tools to export and import images.
# Layout Systems
Boards can have layout systems that automatically control the positioning and spacing of their children:
* If a board has a layout system, then child positions are controlled by the layout system.
For every child, key properties of the child within the layout are stored in `child.layoutChild: LayoutChildProperties`:
- `absolute: boolean` - if true, child position is not controlled by layout system. x/y will set *relative* position within parent!
- margins (`topMargin`, `rightMargin`, `bottomMargin`, `leftMargin` or combined `verticalMargin`, `horizontalMargin`)
- sizing (`verticalSizing`, `horizontalSizing`: "fill" | "auto" | "fix")
- min/max sizes (`minWidth`, `maxWidth`, `minHeight`, `maxHeight`)
- `zIndex: number` (higher numbers on top)
* **Flex Layout**: A flexbox-style layout system
- Properties: `dir`, `rowGap`, `columnGap`, `alignItems`, `justifyContent`;
- `dir`: "row" | "column" | "row-reverse" | "column-reverse"
- Padding: `topPadding`, `rightPadding`, `bottomPadding`, `leftPadding`, or combined `verticalPadding`, `horizontalPadding`
- To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions.
Optionally, adjust indivudual child margins via `child.layoutChild`.
- When a board has flex layout,
- child positions are controlled by the layout system, not by individual x/y coordinates (unless `child.layoutChild.absolute` is true);
appending or inserting children automatically positions them according to the layout rules.
- CRITICAL: For for dir="column" or dir="row", the order of the `children` array is reversed relative to the visual order!
Therefore, the element that appears first in the array, appears visually at the end (bottom/right) and vice versa.
ALWAYS BEAR IN MIND THAT THE CHILDREN ARRAY ORDER IS REVERSED FOR dir="column" OR dir="row"!
- CRITICAL: The FlexLayout method `board.flex.appendChild` is BROKEN. To append children to a flex layout board such that
they appear visually at the end, ALWAYS use the Board's method `board.appendChild(shape)`; it will insert at the front
of the `children` array for dir="column" or dir="row", which is what you want. So call it in the order of visual appearance.
To insert at a specific index, use `board.insertChild(index, shape)`, bearing in mind the reversed order for dir="column"
or dir="row".
- Add to a board with `board.addFlexLayout(): FlexLayout`; instance then accessible via `board.flex`.
IMPORTANT: When adding a flex layout to a container that already has children,
use `penpotUtils.addFlexLayout(container, dir)` instead! This preserves the existing visual order of children.
Otherwise, children will be arbitrarily reordered when the children order suddenly determines the display order.
- Check with: `if (board.flex) { ... }`
* **Grid Layout**: A CSS grid-style layout system
- Add to a board with `board.addGridLayout(): GridLayout`; instance then accessibly via `board.grid`;
Check with: `if (board.grid) { ... }`
- Properties: `rows`, `columns`, `rowGap`, `columnGap`
- Children are positioned via 1-based row/column indices
- Add to grid via `board.flex.appendChild(shape, row, column)`
- Modify grid positioning after the fact via `shape.layoutCell: LayoutCellProperties`
* When working with boards:
- ALWAYS check if the board has a layout system before attempting to reposition children
- Modify layout properties (gaps, padding) instead of trying to set child x/y positions directly
- Layout systems override manual positioning of children
# Text Elements
The rendered content of `Text` element is given by the `characters` property.
To change the size of the text, change the `fontSize` property; applying `resize()` does NOT change the font size,
it only changes the formal bounding box; if the text does not fit it, it will overflow.
The bounding box is sized automatically as long as the `growType` property is set to "auto-width" or "auto-height".
`resize` always sets `growType` to "fixed", so ALWAYS set it back to "auto-*" if you want automatic sizing - otherwise the bounding box will be meaningless, with the text overflowing!
The auto-sizing is not immediate; sleep for a short time (100ms) if you want to read the updated bounding box.
# The `penpot` and `penpotUtils` Objects, Exploring Designs
A key object to use in your code is the `penpot` object (which is of type `Penpot`):
* `penpot.selection` provides the list of shapes the user has selected in the Penpot UI.
If it is unclear which elements to work on, you can ask the user to select them for you.
ALWAYS immediately copy the selected shape(s) into `storage`! Do not assume that the selection remains unchanged.
* `penpot.root` provides the root shape of the currently active page.
* Generation of CSS content for elements via `penpot.generateStyle`
* Generation of HTML/SVG content for elements via `penpot.generateMarkup`
For example, to generate CSS for the currently selected elements, you can execute this:
return penpot.generateStyle(penpot.selection, { type: "css", withChildren: true });
CRITICAL: The `penpotUtils` object provides essential utilities - USE THESE INSTEAD OF WRITING YOUR OWN:
* getPages(): { id: string; name: string }[]
* getPageById(id: string): Page | null
* getPageByName(name: string): Page | null
* shapeStructure(shape: Shape, maxDepth: number | undefined = undefined): { id, name, type, children?, layout? }
Generates an overview structure of the given shape.
- children: recursive, limited by maxDepth
- layout: present if shape has flex/grid layout, contains { type: "flex" | "grid", ... }
* findShapeById(id: string): Shape | null
* findShape(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape | null
If no root is provided, search globally (in all pages).
* findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape[]
* isContainedIn(shape: Shape, container: Shape): boolean
Returns true iff shape is fully within the container's geometric bounds.
Note that a shape's bounds may not always reflect its actual visual content - descendants can overflow; check using analyzeDescendants (see below).
* setParentXY(shape: Shape, parentX: number, parentY: number): void
Sets shape position relative to its parent (since parentX/parentY are read-only)
* analyzeDescendants<T>(root: Shape, evaluator: (root: Shape, descendant: Shape) => T | null | undefined, maxDepth?: number): Array<{ shape: Shape, result: T }>
General-purpose utility for analyzing/validating descendants
Calls evaluator on each descendant; collects non-null/undefined results
Powerful pattern: evaluator can return corrector functions or diagnostic data
* Further functions for specific tasks (described in the sections below)
General pointers for working with Penpot designs:
* Prefer `penpotUtils` helper functions — avoid reimplementing shape searching.
* To get an overview of a single page, use `penpotUtils.shapeStructure(page.root, 3)`.
Note that `penpot.root` refers to the current page only. When working across pages, first determine the relevant page(s).
* Use `penpotUtils.findShapes()` or `penpotUtils.findShape()` with predicates to locate elements efficiently.
Common tasks - Quick Reference (ALWAYS use penpotUtils for these):
* Find all images:
const images = penpotUtils.findShapes(
shape => shape.type === 'image' || shape.fills?.some(fill => fill.fillImage),
penpot.root
);
* Find text elements:
const texts = penpotUtils.findShapes(shape => shape.type === 'text', penpot.root);
* Find (the first) shape with a given name:
const shape = penpotUtils.findShape(shape => shape.name === 'MyShape');
* Get structure of current selection:
const structure = penpotUtils.shapeStructure(penpot.selection[0]);
* Find shapes in current selection/board:
const shapes = penpotUtils.findShapes(predicate, penpot.selection[0] || penpot.root);
* Validate/analyze descendants (returning corrector functions):
const fixes = penpotUtils.analyzeDescendants(board, (root, shape) => {
const xMod = shape.parentX % 4;
if (xMod !== 0) {
return () => penpotUtils.setParentXY(shape, Math.round(shape.parentX / 4) * 4, shape.parentY);
}
});
fixes.forEach(f => f.result()); // Apply all fixes
* Find containment violations:
const violations = penpotUtils.analyzeDescendants(board, (root, shape) => {
return !penpotUtils.isContainedIn(shape, root) ? 'outside-bounds' : null;
});
Always validate against the root container that is supposed to contain the shapes.
# Visual Inspection of Designs
For many tasks, it can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose!
# Revising Designs
* Before applying design changes, ask: "Would a designer consider this appropriate?"
* When dealing with containment issues, ask: Is the parent too small OR is the child too large?
Container sizes are usually intentional, check content first.
* Check for reasonable font sizes and typefaces
* The use of flex layouts is encouraged for cases where elements are arranged in rows or columns with consistent spacing/positioning.
Consider converting boards to flex layout when appropriate.
# Asset Libraries
Libraries in Penpot are collections of reusable design assets (components, colors, and typographies) that can be shared across files.
They enable design systems and consistent styling across projects.
Each Penpot file has its own local library and can connect to external shared libraries.
Accessing libraries: via `penpot.library` (type: `LibraryContext`):
* `penpot.library.local` (type: `Library`) - The current file's own library
* `penpot.library.connected` (type: `Library[]`) - Array of already-connected external libraries
* `penpot.library.availableLibraries()` (returns: `Promise<LibrarySummary[]>`) - Libraries available to connect
* `penpot.library.connectLibrary(libraryId: string)` (returns: `Promise<Library>`) - Connect a new library
Each `Library` object has:
* `id: string`
* `name: string`
* `components: LibraryComponent[]` - Array of components
* `colors: LibraryColor[]` - Array of colors
* `typographies: LibraryTypography[]` - Array of typographies
Using library components:
* find a component in the library by name:
const component: LibraryComponent = library.components.find(comp => comp.name.includes('Button'));
* create a new instance of the component on the current page:
const instance: Shape = component.instance();
This returns a `Shape` (often a `Board` containing child elements).
After instantiation, modify the instance's properties as desired.
* get the reference to the main component shape:
const mainShape: Shape = component.mainInstance();
Adding assets to a library:
* const newColor: LibraryColor = penpot.library.local.createColor();
newColor.name = 'Brand Primary';
newColor.color = '#0066FF';
* const newTypo: LibraryTypography = penpot.library.local.createTypography();
newTypo.name = 'Heading Large';
// Set typography properties...
* const shapes: Shape[] = [shape1, shape2]; // shapes to include
const newComponent: LibraryComponent = penpot.library.local.createComponent(shapes);
newComponent.name = 'My Button';
Detaching:
* When creating new design elements based on a component instance/copy, use `shape.detach()` to break the link to the main component, allowing independent modification.
* Without detaching, some manipulations will have no effect; e.g. child/descendant removal will not work.
# Design Tokens
Design tokens are reusable design values (colors, dimensions, typography, etc.) for consistent styling.
The token library: `penpot.library.local.tokens` (type: `TokenCatalog`)
* `sets: TokenSet[]` - Token collections (order matters for precedence)
* `themes: TokenTheme[]` - Presets that activate specific sets
* `addSet(name: string): TokenSet` - Create new set
* `addTheme(group: string, name: string): TokenTheme` - Create new theme
`TokenSet` contains tokens with unique names:
* `active: boolean` - Only active sets affect shapes; use `set.toggleActive()` to change: `if (!set.active) set.toggleActive();`
* `tokens: Token[]` - All tokens in set
* `addToken(type: TokenType, name: string, value: TokenValueString): Token` - Creates a token, adding it to the set.
- `TokenType`: "color" | "dimension" | "spacing" | "typography" | "shadow" | "opacity" | "borderRadius" | "borderWidth" | "fontWeights" | "fontSizes" | "fontFamilies" | "letterSpacing" | "textDecoration" | "textCase"
- Examples:
const token = set.addToken("color", "color.primary", "#0066FF"); // direct value
const token2 = set.addToken("color", "color.accent", "{color.primary}"); // reference to another token
`Token`:
* `name: string` - Token name (may include group path like "color.base.white")
* `value: string | TokenValueString` - Raw value (may be direct value or reference to another token like "{color.primary}")
* `resolvedValue` - Computed final value (follows references)
* `type: TokenType`
Discovering tokens:
* `penpotUtils.tokenOverview()`: Maps from token set name to a mapping from token type to list of token names
* `penpotUtils.findTokenByName(name: string): Token | null`: Finds the first applicable token matching the given name
* `penpotUtils.findTokensByName(name: string): Token[]`: Finds all tokens that match the given name across all token sets
* `penpotUtils.getTokenSet(token: Token): TokenSet | null`: Gets the token set that contains the given token
Applying tokens:
* `shape.applyToken(token, properties: undefined | TokenProperty[])` - Apply a token to a shape for one or more properties
(if properties is undefined, use a default property based on the token type - not usually recommended).
`TokenProperty` is a union type; possible values are:
- "all": applies the token to all properties it can control
- TokenBorderRadiusProps: "r1", "r2", "r3", "r4"
- TokenShadowProps: "shadow"
- TokenColorProps: "fill", "stroke-color"
- TokenDimensionProps: "x", "y", "stroke-width"
- TokenFontFamiliesProps: "font-families"
- TokenFontSizesProps: "font-size"
- TokenFontWeightProps: "font-weight"
- TokenLetterSpacingProps: "letter-spacing"
- TokenNumberProps: "rotation", "line-height"
- TokenOpacityProps: "opacity"
- TokenSizingProps: "width", "height", "layout-item-min-w", "layout-item-max-w", "layout-item-min-h", "layout-item-max-h"
- TokenSpacingProps: "row-gap", "column-gap", "p1", "p2", "p3", "p4", "m1", "m2", "m3", "m4"
- TokenBorderWidthProps: "stroke-width"
- TokenTextCaseProps: "text-case"
- TokenTextDecorationProps: "text-decoration"
- TokenTypographyProps: "typography"
* `token.applyToShapes(shapes, properties)` - Apply from token
* Application is **asynchronous** (wait for ~100ms to see the effects)
* After application:
- `shape.tokens` returns a mapping `{ propertyName: "token.name" }` from `TokenProperty` to token name
- The actual shape properties that the tokens control will reflect the token's resolved value.
Removing tokens:
Simply set the respective property directly - token binding is automatically removed, e.g.
shape.fills = [{ fillColor: "#000000", fillOpacity: 1 }]; // Removes fill token
--
You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again.

View File

@@ -1,267 +0,0 @@
# Prompts configuration for Penpot MCP Server
# This file contains various prompts and instructions that can be used by the server
initial_instructions: |
You have access to Penpot tools in order to interact with a Penpot design project directly.
As a precondition, the user must connect the Penpot design project to the MCP server using the Penpot MCP Plugin.
IMPORTANT: When transferring styles from a Penpot design to code, make sure that you strictly adhere to the design.
NEVER make assumptions about missing values and don't get overly creative (e.g. don't pick your own colours and stick to
non-creative defaults such as white/black if you are lacking information).
# Executing Code
One of your key tools is the `execute_code` tool, which allows you to run JavaScript code using the Penpot Plugin API
directly in the connected project.
VERY IMPORTANT: When writing code, NEVER LOG INFORMATION YOU ARE ALSO RETURNING. It would duplicate the information you receive!
To execute code correctly, you need to understand the Penpot Plugin API. You can retrieve API documentation via
the `penpot_api_info` tool.
This is the full list of types/interfaces in the Penpot API: $api_types
You use the `storage` object extensively to store data and utility functions you define across tool calls.
This allows you to inspect intermediate results while still being able to build on them in subsequent code executions.
# The Structure of Penpot Designs
A Penpot design ultimately consists of shapes.
The type `Shape` is a union type, which encompasses both containers and low-level shapes.
Shapes in a Penpot design are organized hierarchically.
At the top level, a design project contains one or more `Page` objects.
Each `Page` contains a tree of elements. For a given instance `page`, its root shape is `page.root`.
A Page is frequently structured into boards. A `Board` is a high-level grouping element.
A `Group` is a more low-level grouping element used to organize low-level shapes into a logical unit.
Actual low-level shape types are `Rectangle`, `Path`, `Text`, `Ellipse`, `Image`, `Boolean`, and `SvgRaw`.
`ShapeBase` is a base type most shapes build upon.
# Core Shape Properties and Methods
**Type**:
Any given shape contains information on the concrete type via its `type` field.
**Position and Dimensions**:
* The location properties `x` and `y` refer to the top left corner of a shape's bounding box in the absolute (Page) coordinate system.
These are writable - set them directly to position shapes.
* `parentX` and `parentY` (as well as `boardX` and `boardY`) are READ-ONLY computed properties showing position relative to parent/board.
To position relative to parent, use `penpotUtils.setParentXY(shape, parentX, parentY)` or manually set `shape.x = parent.x + parentX`.
* `width` and `height` are READ-ONLY. Use `resize(width, height)` method to change dimensions.
* `bounds` is a READ-ONLY property. Use `x`, `y` with `resize()` to modify shape bounds.
**Other Writable Properties**:
* `name` - Shape name
* `fills`, `strokes` - Styling properties
* `rotation`, `opacity`, `blocked`, `hidden`, `visible`
**Z-Order**:
* The z-order of shapes is determined by the order in the `children` array of the parent shape.
Therefore, when creating shapes that should be on top of each other, add them to the parent in the correct order
(i.e. add background shapes first, then foreground shapes later).
CRITICAL: NEVER use the broken function `appendChild` to achieve this, ALWAYS use `parent.insertChild(parent.children.length, shape)`
* To modify z-order after creation, use these methods: `bringToFront()`, `sendToBack()`, `bringForward()`, `sendBackward()`,
and, for precise control, `setParentIndex(index)` (0-based).
**Modification Methods**:
* `resize(width, height)` - Change dimensions (required for width/height since they're read-only)
* `rotate(angle, center?)` - Rotate shape
* `remove()` - Permanently destroy the shape (use only for deletion, NOT for reparenting)
**Hierarchical Structure**:
* `parent` - The parent shape (null for root shapes)
Note: Hierarchical nesting does not necessarily imply visual containment
* CRITICAL: To add children to a parent shape (e.g. a `Board`):
- ALWAYS use `parent.insertChild(index, shape)` to add a child, e.g. `parent.insertChild(parent.children.length, shape)` to append
- NEVER use `parent.appendChild(shape)` as it is BROKEN and will not insert in a predictable place (except in flex layout boards)
* Reparenting: `newParent.appendChild(shape)` or `newParent.insertChild(index, shape)` will move a shape to new parent
- Automatically removes the shape from its old parent
- Absolute x/y positions are preserved (use `penpotUtils.setParentXY` to adjust relative position)
# Images
The `Image` type is a legacy type. Images are now typically embedded in a `Fill`, with `fillImage` set to an
`ImageData` object, i.e. the `fills` property of of a shape (e.g. a `Rectangle`) will contain a fill where `fillImage` is set.
Use the `export_shape` and `import_image` tools to export and import images.
# Layout Systems
Boards can have layout systems that automatically control the positioning and spacing of their children:
* If a board has a layout system, then child positions are controlled by the layout system.
For every child, key properties of the child within the layout are stored in `child.layoutChild: LayoutChildProperties`:
- `absolute: boolean` - if true, child position is not controlled by layout system. x/y will set *relative* position within parent!
- margins (`topMargin`, `rightMargin`, `bottomMargin`, `leftMargin` or combined `verticalMargin`, `horizontalMargin`)
- sizing (`verticalSizing`, `horizontalSizing`: "fill" | "auto" | "fix")
- min/max sizes (`minWidth`, `maxWidth`, `minHeight`, `maxHeight`)
- `zIndex: number` (higher numbers on top)
* **Flex Layout**: A flexbox-style layout system
- Properties: `dir`, `rowGap`, `columnGap`, `alignItems`, `justifyContent`;
- `dir`: "row" | "column" | "row-reverse" | "column-reverse"
- Padding: `topPadding`, `rightPadding`, `bottomPadding`, `leftPadding`, or combined `verticalPadding`, `horizontalPadding`
- To modify spacing: adjust `rowGap` and `columnGap` properties, not individual child positions.
Optionally, adjust indivudual child margins via `child.layoutChild`.
- When a board has flex layout,
- child positions are controlled by the layout system, not by individual x/y coordinates (unless `child.layoutChild.absolute` is true);
appending or inserting children automatically positions them according to the layout rules.
- CRITICAL: For for dir="column" or dir="row", the order of the `children` array is reversed relative to the visual order!
Therefore, the element that appears first in the array, appears visually at the end (bottom/right) and vice versa.
ALWAYS BEAR IN MIND THAT THE CHILDREN ARRAY ORDER IS REVERSED FOR dir="column" OR dir="row"!
- CRITICAL: The FlexLayout method `board.flex.appendChild` is BROKEN. To append children to a flex layout board such that
they appear visually at the end, ALWAYS use the Board's method `board.appendChild(shape)`; it will insert at the front
of the `children` array for dir="column" or dir="row", which is what you want. So call it in the order of visual appearance.
To insert at a specific index, use `board.insertChild(index, shape)`, bearing in mind the reversed order for dir="column"
or dir="row".
- Add to a board with `board.addFlexLayout(): FlexLayout`; instance then accessible via `board.flex`.
IMPORTANT: When adding a flex layout to a container that already has children,
use `penpotUtils.addFlexLayout(container, dir)` instead! This preserves the existing visual order of children.
Otherwise, children will be arbitrarily reordered when the children order suddenly determines the display order.
- Check with: `if (board.flex) { ... }`
* **Grid Layout**: A CSS grid-style layout system
- Add to a board with `board.addGridLayout(): GridLayout`; instance then accessibly via `board.grid`;
Check with: `if (board.grid) { ... }`
- Properties: `rows`, `columns`, `rowGap`, `columnGap`
- Children are positioned via 1-based row/column indices
- Add to grid via `board.flex.appendChild(shape, row, column)`
- Modify grid positioning after the fact via `shape.layoutCell: LayoutCellProperties`
* When working with boards:
- ALWAYS check if the board has a layout system before attempting to reposition children
- Modify layout properties (gaps, padding) instead of trying to set child x/y positions directly
- Layout systems override manual positioning of children
# Text Elements
The rendered content of `Text` element is given by the `characters` property.
To change the size of the text, change the `fontSize` property; applying `resize()` does NOT change the font size,
it only changes the formal bounding box; if the text does not fit it, it will overflow.
The bounding box is sized automatically as long as the `growType` property is set to "auto-width" or "auto-height".
`resize` always sets `growType` to "fixed", so ALWAYS set it back to "auto-*" if you want automatic sizing - otherwise the bounding box will be meaningless, with the text overflowing!
The auto-sizing is not immediate; sleep for a short time (100ms) if you want to read the updated bounding box.
# The `penpot` and `penpotUtils` Objects, Exploring Designs
A key object to use in your code is the `penpot` object (which is of type `Penpot`):
* `penpot.selection` provides the list of shapes the user has selected in the Penpot UI.
If it is unclear which elements to work on, you can ask the user to select them for you.
ALWAYS immediately copy the selected shape(s) into `storage`! Do not assume that the selection remains unchanged.
* `penpot.root` provides the root shape of the currently active page.
* Generation of CSS content for elements via `penpot.generateStyle`
* Generation of HTML/SVG content for elements via `penpot.generateMarkup`
For example, to generate CSS for the currently selected elements, you can execute this:
return penpot.generateStyle(penpot.selection, { type: "css", withChildren: true });
CRITICAL: The `penpotUtils` object provides essential utilities - USE THESE INSTEAD OF WRITING YOUR OWN:
* getPages(): { id: string; name: string }[]
* getPageById(id: string): Page | null
* getPageByName(name: string): Page | null
* shapeStructure(shape: Shape, maxDepth: number | undefined = undefined): { id, name, type, children?, layout? }
Generates an overview structure of the given shape.
- children: recursive, limited by maxDepth
- layout: present if shape has flex/grid layout, contains { type: "flex" | "grid", ... }
* findShapeById(id: string): Shape | null
* findShape(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape | null
If no root is provided, search globally (in all pages).
* findShapes(predicate: (shape: Shape) => boolean, root: Shape | null = null): Shape[]
* isContainedIn(shape: Shape, container: Shape): boolean
Returns true iff shape is fully within the container's geometric bounds.
Note that a shape's bounds may not always reflect its actual visual content - descendants can overflow; check using analyzeDescendants (see below).
* setParentXY(shape: Shape, parentX: number, parentY: number): void
Sets shape position relative to its parent (since parentX/parentY are read-only)
* analyzeDescendants<T>(root: Shape, evaluator: (root: Shape, descendant: Shape) => T | null | undefined, maxDepth?: number): Array<{ shape: Shape, result: T }>
General-purpose utility for analyzing/validating descendants
Calls evaluator on each descendant; collects non-null/undefined results
Powerful pattern: evaluator can return corrector functions or diagnostic data
General pointers for working with Penpot designs:
* Prefer `penpotUtils` helper functions — avoid reimplementing shape searching.
* To get an overview of a single page, use `penpotUtils.shapeStructure(page.root, 3)`.
Note that `penpot.root` refers to the current page only. When working across pages, first determine the relevant page(s).
* Use `penpotUtils.findShapes()` or `penpotUtils.findShape()` with predicates to locate elements efficiently.
Common tasks - Quick Reference (ALWAYS use penpotUtils for these):
* Find all images:
const images = penpotUtils.findShapes(
shape => shape.type === 'image' || shape.fills?.some(fill => fill.fillImage),
penpot.root
);
* Find text elements:
const texts = penpotUtils.findShapes(shape => shape.type === 'text', penpot.root);
* Find (the first) shape with a given name:
const shape = penpotUtils.findShape(shape => shape.name === 'MyShape');
* Get structure of current selection:
const structure = penpotUtils.shapeStructure(penpot.selection[0]);
* Find shapes in current selection/board:
const shapes = penpotUtils.findShapes(predicate, penpot.selection[0] || penpot.root);
* Validate/analyze descendants (returning corrector functions):
const fixes = penpotUtils.analyzeDescendants(board, (root, shape) => {
const xMod = shape.parentX % 4;
if (xMod !== 0) {
return () => penpotUtils.setParentXY(shape, Math.round(shape.parentX / 4) * 4, shape.parentY);
}
});
fixes.forEach(f => f.result()); // Apply all fixes
* Find containment violations:
const violations = penpotUtils.analyzeDescendants(board, (root, shape) => {
return !penpotUtils.isContainedIn(shape, root) ? 'outside-bounds' : null;
});
Always validate against the root container that is supposed to contain the shapes.
# Visual Inspection of Designs
For many tasks, it can be critical to visually inspect the design. Remember to use the `export_shape` tool for this purpose!
# Revising Designs
* Before applying design changes, ask: "Would a designer consider this appropriate?"
* When dealing with containment issues, ask: Is the parent too small OR is the child too large?
Container sizes are usually intentional, check content first.
* Check for reasonable font sizes and typefaces
* The use of flex layouts is encouraged for cases where elements are arranged in rows or columns with consistent spacing/positioning.
Consider converting boards to flex layout when appropriate.
# Asset Libraries
Libraries in Penpot are collections of reusable design assets (components, colors, and typographies) that can be shared across files.
They enable design systems and consistent styling across projects.
Each Penpot file has its own local library and can connect to external shared libraries.
Accessing libraries: via `penpot.library` (type: `LibraryContext`):
* `penpot.library.local` (type: `Library`) - The current file's own library
* `penpot.library.connected` (type: `Library[]`) - Array of already-connected external libraries
* `penpot.library.availableLibraries()` (returns: `Promise<LibrarySummary[]>`) - Libraries available to connect
* `penpot.library.connectLibrary(libraryId: string)` (returns: `Promise<Library>`) - Connect a new library
Each `Library` object has:
* `id: string`
* `name: string`
* `components: LibraryComponent[]` - Array of components
* `colors: LibraryColor[]` - Array of colors
* `typographies: LibraryTypography[]` - Array of typographies
Using library components:
* find a component in the library by name:
const component: LibraryComponent = library.components.find(comp => comp.name.includes('Button'));
* create a new instance of the component on the current page:
const instance: Shape = component.instance();
This returns a `Shape` (often a `Board` containing child elements).
After instantiation, modify the instance's properties as desired.
* get the reference to the main component shape:
const mainShape: Shape = component.mainInstance();
Adding assets to a library:
* const newColor: LibraryColor = penpot.library.local.createColor();
newColor.name = 'Brand Primary';
newColor.color = '#0066FF';
* const newTypo: LibraryTypography = penpot.library.local.createTypography();
newTypo.name = 'Heading Large';
// Set typography properties...
* const shapes: Shape[] = [shape1, shape2]; // shapes to include
const newComponent: LibraryComponent = penpot.library.local.createComponent(shapes);
newComponent.name = 'My Button';
--
You have hereby read the 'Penpot High-Level Overview' and need not use a tool to read it again.

View File

@@ -1,18 +1,7 @@
import { readFileSync, existsSync } from "fs"; import { existsSync, readFileSync } from "fs";
import { join, dirname } from "path"; import { join } from "path";
import { fileURLToPath } from "url";
import yaml from "js-yaml";
import { createLogger } from "./logger.js"; import { createLogger } from "./logger.js";
/**
* Interface defining the structure of the prompts configuration file.
*/
export interface PromptsConfig {
/** Initial instructions displayed when the server starts or connects to a client */
initial_instructions: string;
[key: string]: any; // Allow for future extension with additional prompt types
}
/** /**
* Configuration loader for prompts and server settings. * Configuration loader for prompts and server settings.
* *
@@ -23,7 +12,7 @@ export interface PromptsConfig {
export class ConfigurationLoader { export class ConfigurationLoader {
private readonly logger = createLogger("ConfigurationLoader"); private readonly logger = createLogger("ConfigurationLoader");
private readonly baseDir: string; private readonly baseDir: string;
private promptsConfig: PromptsConfig | null = null; private initialInstructions: string;
/** /**
* Creates a new configuration loader instance. * Creates a new configuration loader instance.
@@ -32,34 +21,14 @@ export class ConfigurationLoader {
*/ */
constructor(baseDir: string) { constructor(baseDir: string) {
this.baseDir = baseDir; this.baseDir = baseDir;
this.initialInstructions = this.loadFileContent(join(this.baseDir, "data", "initial_instructions.md"));
} }
/** private loadFileContent(filePath: string): string {
* Loads the prompts configuration from the YAML file. if (!existsSync(filePath)) {
* throw new Error(`Configuration file not found at ${filePath}`);
* Reads and parses the prompts.yml file, providing cached access
* to configuration values on subsequent calls.
*
* @returns The parsed prompts configuration object
*/
public getPromptsConfig(): PromptsConfig {
if (this.promptsConfig !== null) {
return this.promptsConfig;
} }
return readFileSync(filePath, "utf8");
const promptsPath = join(this.baseDir, "data", "prompts.yml");
if (!existsSync(promptsPath)) {
throw new Error(`Prompts configuration file not found at ${promptsPath}, using defaults`);
}
const fileContent = readFileSync(promptsPath, "utf8");
const parsedConfig = yaml.load(fileContent) as PromptsConfig;
this.promptsConfig = parsedConfig || {};
this.logger.info(`Loaded prompts configuration from ${promptsPath}`);
return this.promptsConfig;
} }
/** /**
@@ -68,18 +37,6 @@ export class ConfigurationLoader {
* @returns The initial instructions string, or undefined if not configured * @returns The initial instructions string, or undefined if not configured
*/ */
public getInitialInstructions(): string { public getInitialInstructions(): string {
const config = this.getPromptsConfig(); return this.initialInstructions;
return config.initial_instructions;
}
/**
* Reloads the configuration from disk.
*
* Forces a fresh read of the configuration file on the next access,
* useful for development or when configuration files are updated at runtime.
*/
public reloadConfiguration(): void {
this.promptsConfig = null;
this.logger.info("Configuration cache cleared, will reload on next access");
} }
} }

View File

@@ -15,9 +15,9 @@ fi
if [[ "$URL" = "http://localhost:9090" ]]; then if [[ "$URL" = "http://localhost:9090" ]]; then
pnpx concurrently --kill-others-on-fail -s last -k \ pnpx concurrently --kill-others-on-fail -s last -k \
"caddy file-server --root ../../plugins/dist/doc/ --listen :9090" \ "caddy file-server --root ../../plugins/dist/doc/ --listen :9090" \
"../types-generator/build $URL"; "bash ../types-generator/build $URL";
else else
../types-generator/build $URL; bash ../types-generator/build $URL;
fi fi
popd popd

View File

@@ -1,7 +1,8 @@
# Types Generator # Types Generator
This subproject contains helper scripts used in the development of the This subproject contains helper scripts used in the development of the
Penpot MCP server for generate the types yaml. Penpot MCP server, specifically for the generation of a YAML file containing
Penpot plugin API types and their documentation.
## Setup ## Setup
@@ -12,15 +13,41 @@ Install the environment via (optional, already handled by `build` script)
pixi install pixi install
## Running the API Documentation Preparation Script
### Buld API types The script `prepare_api_docs.py` reads API documentation from a Web URL
and collects it in a single YAML file, which is then used by an MCP
The script `prepare_api_docs.py` reads API documentation from the Web
and collects it in a single yaml file, which is then used by an MCP
tool to provide API documentation to an LLM on demand. tool to provide API documentation to an LLM on demand.
Successful execution will generate the output file `../packages/server/data/api_types.yml`.
### Generating the YAML File for a Given URL
Running the script: Running the script:
./build <optional-url> pixi run python prepare_api_docs.py <url>
You can alternatively run `./build <url>`, which additionally performs pixi environment installation.
For example, to generate the API documentation based on the current PROD Penpot API documentation,
use the URL
https://doc.plugins.penpot.app
### Generating the YAML File Based on the Current Documentation in the Repository
Requirement: [Caddy](https://caddyserver.com/download) must be installed and available in the system path.
To generate the API documentation based on the current documentation in the repository,
run the `build:types` script in the parent directory, i.e.
cd ..
pnpm run build:types
This will spawn a local HTTP server on port 9090 and run the `prepare_api_docs.py` script with the
URL `http://localhost:9090`.
To run only the server without executing the script, run
cd ..
caddy file-server --root ../plugins/dist/doc/ --listen 127.0.0.1:9090
This will generate `../packages/server/data/api_types.yml`.

View File

@@ -80,6 +80,25 @@ class PenpotAPIContentMarkdownConverter(MarkdownConverter):
# return as code block # return as code block
return f"\n```\n{soup.get_text()}\n```\n\n" return f"\n```\n{soup.get_text()}\n```\n\n"
# check for <ul> tag with a single <li>: move the <li> content a <div> and process it as normal,
# to avoid single list items with superfluous bullet points and indentations.
# This happens frequently, especially in new versions of the docs generator, e.g. for methods:
# <ul class="tsd-signatures tsd-is-inherited">
# <li class="tsd-is-inherited">
# <div class="tsd-signature tsd-anchor-link" id="remove-1">...</div>
# </li>
# </ul>
if node.name == "ul" and "class" in node.attrs and "tsd-signatures" in node.attrs["class"]:
soup_ul = soup.find("ul")
if soup_ul is not None:
li_children = soup_ul.find_all("li", recursive=False)
if len(li_children) == 1:
# create a new div with the content of the single li
new_div = soup.new_tag("div")
for child in list(li_children[0].contents):
new_div.append(child)
return self.process_tag(new_div, parent_tags=parent_tags)
# other cases: use the default processing # other cases: use the default processing
return super().process_tag(node, parent_tags=parent_tags) return super().process_tag(node, parent_tags=parent_tags)
@@ -135,7 +154,7 @@ class YamlConverter:
class PenpotAPIDocsProcessor: class PenpotAPIDocsProcessor:
def __init__(self, url=None): def __init__(self, url: str):
self.md_converter = PenpotAPIContentMarkdownConverter() self.md_converter = PenpotAPIContentMarkdownConverter()
self.base_url = url self.base_url = url
self.types: dict[str, TypeInfo] = {} self.types: dict[str, TypeInfo] = {}
@@ -157,7 +176,7 @@ class PenpotAPIDocsProcessor:
type_name = href.split("/")[-1].replace(".html", "") type_name = href.split("/")[-1].replace(".html", "")
log.info("Processing page: %s", type_name) log.info("Processing page: %s", type_name)
type_info = self.process_page(href, type_name) type_info = self.process_page(href, type_name)
print(f"Adding '{type_name}' with {type_info}") log.info(f"Adding '{type_name}' with {type_info}")
self.types[type_name] = type_info self.types[type_name] = type_info
# add type reference information # add type reference information
@@ -201,11 +220,21 @@ class PenpotAPIDocsProcessor:
members_in_group = {} members_in_group = {}
members[members_type] = members_in_group members[members_type] = members_in_group
for member_tag in el.find_all(attrs={"class": "tsd-member"}): for member_tag in el.find_all(attrs={"class": "tsd-member"}):
# determine member name
member_name = None
member_anchor = member_tag.find("a", attrs={"class": "tsd-anchor"}, recursive=False) member_anchor = member_tag.find("a", attrs={"class": "tsd-anchor"}, recursive=False)
member_name = member_anchor.attrs["id"] if member_anchor is not None:
member_heading = member_tag.find("h3") member_name = member_anchor.attrs["id"]
else:
member_h3 = member_tag.find("h3", recursive=False)
if member_h3 is not None:
h3_span = member_h3.find("span", recursive=False)
if h3_span is not None:
member_name = h3_span.get_text().strip()
assert member_name is not None, f"Could not determine member name for\n{member_tag}"
# extract tsd-tag info (e.g., "Readonly") from the heading and reinsert it into the signature, # extract tsd-tag info (e.g., "Readonly") from the heading and reinsert it into the signature,
# where we want to see it. The heading is removed, as it is redundant. # where we want to see it. The heading is removed, as it is redundant.
member_heading = member_tag.find("h3")
if member_heading: if member_heading:
tags_in_heading = member_heading.find_all(attrs={"class": "tsd-tag"}) tags_in_heading = member_heading.find_all(attrs={"class": "tsd-tag"})
if tags_in_heading: if tags_in_heading:
@@ -237,25 +266,29 @@ class PenpotAPIDocsProcessor:
) )
DEFAULT_API_DOCS_URL = "http://localhost:9090" LOCAL_API_DOCS_URL = "http://localhost:9090"
PROD_API_DOCS_URL = "https://doc.plugins.penpot.app"
DEFAULT_API_DOCS_URL = LOCAL_API_DOCS_URL
def main(): def main():
target_dir = Path(__file__).parent.parent / "packages" / "server" / "data" target_dir = Path(__file__).parent.parent / "packages" / "server" / "data"
url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_API_DOCS_URL url = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_API_DOCS_URL
print("Fetching plugin data from: {}".format(url)) log.info("Fetching plugin data from: {}".format(url))
PenpotAPIDocsProcessor(url).run(target_dir=str(target_dir)) PenpotAPIDocsProcessor(url).run(target_dir=str(target_dir))
def debug_type_conversion(rel_url: str): def debug_type_conversion(rel_url: str, base_url: str):
""" """
This function is for debugging purposes only. This function is for debugging purposes only.
It processes a single type page and prints the converted markdown to the console. It processes a single type page and prints the converted markdown to the console.
:param base_url: base URL of the API docs (e.g., "http://localhost:9090")
:param rel_url: relative URL of the type page (e.g., "interfaces/ShapeBase") :param rel_url: relative URL of the type page (e.g., "interfaces/ShapeBase")
""" """
type_name = rel_url.split("/")[-1] type_name = rel_url.split("/")[-1].replace(".html", "")
processor = PenpotAPIDocsProcessor() processor = PenpotAPIDocsProcessor(url=base_url)
type_info = processor.process_page(rel_url, type_name) type_info = processor.process_page(rel_url, type_name)
print(f"--- overview ---\n{type_info.overview}\n") print(f"--- overview ---\n{type_info.overview}\n")
for member_type, members in type_info.members.items(): for member_type, members in type_info.members.items():
@@ -265,5 +298,5 @@ def debug_type_conversion(rel_url: str):
if __name__ == '__main__': if __name__ == '__main__':
# debug_type_conversion("interfaces/LayoutChildProperties") # debug_type_conversion("interfaces/Path.html", LOCAL_API_DOCS_URL)
logging.run_main(main) logging.run_main(main)