This is page 1 of 2. Use http://codebase.md/montevive/penpot-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .editorconfig
├── .flake8
├── .github
│ ├── dependabot.yml
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── SETUP_CICD.md
│ └── workflows
│ ├── ci.yml
│ ├── code-quality.yml
│ ├── publish.yml
│ └── version-bump.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .vscode
│ └── launch.json
├── CHANGELOG.md
├── CLAUDE_INTEGRATION.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── env.example
├── fix-lint-deps.sh
├── images
│ └── penpot-mcp.png
├── lint.py
├── LINTING.md
├── Makefile
├── penpot_mcp
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ └── penpot_api.py
│ ├── resources
│ │ ├── penpot-schema.json
│ │ └── penpot-tree-schema.json
│ ├── server
│ │ ├── __init__.py
│ │ ├── client.py
│ │ └── mcp_server.py
│ ├── tools
│ │ ├── __init__.py
│ │ ├── cli
│ │ │ ├── __init__.py
│ │ │ ├── tree_cmd.py
│ │ │ └── validate_cmd.py
│ │ └── penpot_tree.py
│ └── utils
│ ├── __init__.py
│ ├── cache.py
│ ├── config.py
│ └── http_server.py
├── pyproject.toml
├── README.md
├── SECURITY.md
├── test_credentials.py
├── tests
│ ├── __init__.py
│ ├── conftest.py
│ ├── test_cache.py
│ ├── test_config.py
│ ├── test_mcp_server.py
│ └── test_penpot_tree.py
└── uv.lock
```
# Files
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
```
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | indent_style = space
9 | indent_size = 4
10 |
11 | [*.{json,yml,yaml}]
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
17 | [Makefile]
18 | indent_style = tab
19 |
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | env/
8 | build/
9 | develop-eggs/
10 | dist/
11 | downloads/
12 | eggs/
13 | .eggs/
14 | lib/
15 | lib64/
16 | parts/
17 | sdist/
18 | var/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 |
23 | # Virtual Environment
24 | venv/
25 | ENV/
26 | env/
27 | .venv/
28 |
29 | # uv
30 | .python-version
31 |
32 | # Environment variables
33 | .env
34 |
35 | # IDE files
36 | .idea/
37 | .vscode/
38 | *.swp
39 | *.swo
40 |
41 | # OS specific
42 | .DS_Store
43 | Thumbs.db
44 |
45 | # Logs
46 | logs/
47 | *.log
48 | *.json
49 | !penpot-schema.json
50 | !penpot-tree-schema.json
51 | .coverage
52 |
53 | # Unit test / coverage reports
54 | htmlcov/
55 | .tox/
56 | .nox/
57 | .coverage
58 | .coverage.*
59 | .cache
60 | nosetests.xml
61 | coverage.xml
62 | *.cover
63 | *.py,cover
64 | .hypothesis/
65 | .pytest_cache/
66 | pytestdebug.log
67 |
```
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
```
1 | [flake8]
2 | max-line-length = 88
3 | exclude =
4 | .venv,
5 | venv,
6 | __pycache__,
7 | .git,
8 | build,
9 | dist,
10 | *.egg-info,
11 | node_modules,
12 | .tox,
13 | .pytest_cache
14 | ignore =
15 | # Line too long (handled by max-line-length)
16 | E501,
17 | # Missing docstrings (can be addressed later)
18 | D100, D101, D102, D103, D105, D107,
19 | # Docstring formatting (can be addressed later)
20 | D200, D205, D401,
21 | # Whitespace issues (auto-fixable)
22 | W293, W291, W292,
23 | # Unused imports (will be cleaned up)
24 | F401,
25 | # Unused variables (will be cleaned up)
26 | F841,
27 | # Bare except (will be improved)
28 | E722,
29 | # f-string without placeholders
30 | F541,
31 | # Comparison to True (minor issue)
32 | E712,
33 | # Continuation line formatting
34 | E128,
35 | # Blank line formatting
36 | E302, E306
37 | per-file-ignores =
38 | # Tests can be more lenient
39 | tests/*:D,E,F,W
40 | # CLI tools can be more lenient
41 | */cli/*:D401
42 | # Allow unused imports in __init__.py files
43 | */__init__.py:F401
44 | # Allow long lines in configuration files
45 | */config.py:E501
46 | select = E,W,F
```
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
```yaml
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v4.4.0
4 | hooks:
5 | - id: trailing-whitespace
6 | - id: end-of-file-fixer
7 | - id: check-yaml
8 | - id: check-added-large-files
9 |
10 | - repo: https://github.com/pycqa/flake8
11 | rev: 6.1.0
12 | hooks:
13 | - id: flake8
14 | additional_dependencies: [flake8-docstrings]
15 | types: [python]
16 | files: ^(penpot_mcp|tests)/.*\.py$
17 |
18 | - repo: https://github.com/pycqa/isort
19 | rev: 5.12.0
20 | hooks:
21 | - id: isort
22 | args: ["--profile", "black", "--filter-files"]
23 | types: [python]
24 | files: ^(penpot_mcp|tests)/.*\.py$
25 |
26 | - repo: https://github.com/asottile/pyupgrade
27 | rev: v3.13.0
28 | hooks:
29 | - id: pyupgrade
30 | args: [--py312-plus]
31 | types: [python]
32 | files: ^(penpot_mcp|tests)/.*\.py$
33 |
34 | - repo: https://github.com/pre-commit/mirrors-autopep8
35 | rev: v2.0.4
36 | hooks:
37 | - id: autopep8
38 | args: [--aggressive, --aggressive, --select=E,W]
39 | types: [python]
40 | files: ^(penpot_mcp|tests)/.*\.py$
41 | additional_dependencies: [setuptools>=65.5.0]
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # Penpot MCP Server 🎨🤖
2 |
3 | <p align="center">
4 | <img src="images/penpot-mcp.png" alt="Penpot MCP Logo" width="400"/>
5 | </p>
6 |
7 | <p align="center">
8 | <strong>AI-Powered Design Workflow Automation</strong><br>
9 | Connect Claude AI and other LLMs to Penpot designs via Model Context Protocol
10 | </p>
11 |
12 | <p align="center">
13 | <a href="https://github.com/montevive/penpot-mcp/blob/main/LICENSE">
14 | <img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT">
15 | </a>
16 | <a href="https://www.python.org/downloads/">
17 | <img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="Python Version">
18 | </a>
19 | <a href="https://pypi.org/project/penpot-mcp/">
20 | <img src="https://img.shields.io/pypi/v/penpot-mcp" alt="PyPI version">
21 | </a>
22 | <a href="https://github.com/montevive/penpot-mcp/actions">
23 | <img src="https://img.shields.io/github/workflow/status/montevive/penpot-mcp/CI" alt="Build Status">
24 | </a>
25 | </p>
26 |
27 | ---
28 |
29 | ## 🚀 What is Penpot MCP?
30 |
31 | **Penpot MCP** is a revolutionary Model Context Protocol (MCP) server that bridges the gap between AI language models and [Penpot](https://penpot.app/), the open-source design and prototyping platform. This integration enables AI assistants like Claude (in both Claude Desktop and Cursor IDE) to understand, analyze, and interact with your design files programmatically.
32 |
33 | ### 🎯 Key Benefits
34 |
35 | - **🤖 AI-Native Design Analysis**: Let Claude AI analyze your UI/UX designs, provide feedback, and suggest improvements
36 | - **⚡ Automated Design Workflows**: Streamline repetitive design tasks with AI-powered automation
37 | - **🔍 Intelligent Design Search**: Find design components and patterns across your projects using natural language
38 | - **📊 Design System Management**: Automatically document and maintain design systems with AI assistance
39 | - **🎨 Cross-Platform Integration**: Works with any MCP-compatible AI assistant (Claude Desktop, Cursor IDE, etc.)
40 |
41 | ## 🎥 Demo Video
42 |
43 | Check out our demo video to see Penpot MCP in action:
44 |
45 | [](https://www.youtube.com/watch?v=vOMEh-ONN1k)
46 |
47 | ## ✨ Features
48 |
49 | ### 🔌 Core Capabilities
50 | - **MCP Protocol Implementation**: Full compliance with Model Context Protocol standards
51 | - **Real-time Design Access**: Direct integration with Penpot's API for live design data
52 | - **Component Analysis**: AI-powered analysis of design components and layouts
53 | - **Export Automation**: Programmatic export of design assets in multiple formats
54 | - **Design Validation**: Automated design system compliance checking
55 |
56 | ### 🛠️ Developer Tools
57 | - **Command-line Utilities**: Powerful CLI tools for design file analysis and validation
58 | - **Python SDK**: Comprehensive Python library for custom integrations
59 | - **REST API**: HTTP endpoints for web application integration
60 | - **Extensible Architecture**: Plugin system for custom AI workflows
61 |
62 | ### 🎨 AI Integration Features
63 | - **Claude Desktop & Cursor Integration**: Native support for Claude AI assistant in both Claude Desktop and Cursor IDE
64 | - **Design Context Sharing**: Provide design context to AI models for better responses
65 | - **Visual Component Recognition**: AI can "see" and understand design components
66 | - **Natural Language Queries**: Ask questions about your designs in plain English
67 | - **IDE Integration**: Seamless integration with modern development environments
68 |
69 | ## 💡 Use Cases
70 |
71 | ### For Designers
72 | - **Design Review Automation**: Get instant AI feedback on accessibility, usability, and design principles
73 | - **Component Documentation**: Automatically generate documentation for design systems
74 | - **Design Consistency Checks**: Ensure brand guidelines compliance across projects
75 | - **Asset Organization**: AI-powered tagging and categorization of design components
76 |
77 | ### For Developers
78 | - **Design-to-Code Workflows**: Bridge the gap between design and development with AI assistance
79 | - **API Integration**: Programmatic access to design data for custom tools and workflows
80 | - **Automated Testing**: Generate visual regression tests from design specifications
81 | - **Design System Sync**: Keep design tokens and code components in sync
82 |
83 | ### For Product Teams
84 | - **Design Analytics**: Track design system adoption and component usage
85 | - **Collaboration Enhancement**: AI-powered design reviews and feedback collection
86 | - **Workflow Optimization**: Automate repetitive design operations and approvals
87 | - **Cross-tool Integration**: Connect Penpot with other tools in your design workflow
88 |
89 | ## 🚀 Quick Start
90 |
91 | ### Prerequisites
92 |
93 | - **Python 3.12+** (Latest Python recommended for optimal performance)
94 | - **Penpot Account** ([Sign up free](https://penpot.app/))
95 | - **Claude Desktop or Cursor IDE** (Optional, for AI integration)
96 |
97 | ## Installation
98 |
99 | ### Prerequisites
100 |
101 | - Python 3.12+
102 | - Penpot account credentials
103 |
104 | ### Installation
105 |
106 | #### Option 1: Install from PyPI
107 |
108 | ```bash
109 | pip install penpot-mcp
110 | ```
111 |
112 | #### Option 2: Using uv (recommended for modern Python development)
113 |
114 | ```bash
115 | # Install directly with uvx (when published to PyPI)
116 | uvx penpot-mcp
117 |
118 | # For local development, use uvx with local path
119 | uvx --from . penpot-mcp
120 |
121 | # Or install in a project with uv
122 | uv add penpot-mcp
123 | ```
124 |
125 | #### Option 3: Install from source
126 |
127 | ```bash
128 | # Clone the repository
129 | git clone https://github.com/montevive/penpot-mcp.git
130 | cd penpot-mcp
131 |
132 | # Using uv (recommended)
133 | uv sync
134 | uv run penpot-mcp
135 |
136 | # Or using traditional pip
137 | python -m venv .venv
138 | source .venv/bin/activate # On Windows: .venv\Scripts\activate
139 | pip install -e .
140 | ```
141 |
142 | ### Configuration
143 |
144 | Create a `.env` file based on `env.example` with your Penpot credentials:
145 |
146 | ```
147 | PENPOT_API_URL=https://design.penpot.app/api
148 | PENPOT_USERNAME=your_penpot_username
149 | PENPOT_PASSWORD=your_penpot_password
150 | PORT=5000
151 | DEBUG=true
152 | ```
153 |
154 | > **⚠️ CloudFlare Protection Notice**: The Penpot cloud site (penpot.app) uses CloudFlare protection that may occasionally block API requests. If you encounter authentication errors or blocked requests:
155 | > 1. Open your web browser and navigate to [https://design.penpot.app](https://design.penpot.app)
156 | > 2. Log in to your Penpot account
157 | > 3. Complete any CloudFlare human verification challenges if prompted
158 | > 4. Once verified, the API requests should work normally for a period of time
159 |
160 | ## Usage
161 |
162 | ### Running the MCP Server
163 |
164 | ```bash
165 | # Using uvx (when published to PyPI)
166 | uvx penpot-mcp
167 |
168 | # Using uvx for local development
169 | uvx --from . penpot-mcp
170 |
171 | # Using uv in a project (recommended for local development)
172 | uv run penpot-mcp
173 |
174 | # Using the entry point (if installed)
175 | penpot-mcp
176 |
177 | # Or using the module directly
178 | python -m penpot_mcp.server.mcp_server
179 | ```
180 |
181 | ### Debugging the MCP Server
182 |
183 | To debug the MCP server, you can:
184 |
185 | 1. Enable debug mode in your `.env` file by setting `DEBUG=true`
186 | 2. Use the Penpot API CLI for testing API operations:
187 |
188 | ```bash
189 | # Test API connection with debug output
190 | python -m penpot_mcp.api.penpot_api --debug list-projects
191 |
192 | # Get details for a specific project
193 | python -m penpot_mcp.api.penpot_api --debug get-project --id YOUR_PROJECT_ID
194 |
195 | # List files in a project
196 | python -m penpot_mcp.api.penpot_api --debug list-files --project-id YOUR_PROJECT_ID
197 |
198 | # Get file details
199 | python -m penpot_mcp.api.penpot_api --debug get-file --file-id YOUR_FILE_ID
200 | ```
201 |
202 | ### Command-line Tools
203 |
204 | The package includes utility command-line tools:
205 |
206 | ```bash
207 | # Generate a tree visualization of a Penpot file
208 | penpot-tree path/to/penpot_file.json
209 |
210 | # Validate a Penpot file against the schema
211 | penpot-validate path/to/penpot_file.json
212 | ```
213 |
214 | ### MCP Monitoring & Testing
215 |
216 | #### MCP CLI Monitor
217 |
218 | ```bash
219 | # Start your MCP server in one terminal
220 | python -m penpot_mcp.server.mcp_server
221 |
222 | # In another terminal, use mcp-cli to monitor and interact with your server
223 | python -m mcp.cli monitor python -m penpot_mcp.server.mcp_server
224 |
225 | # Or connect to an already running server on a specific port
226 | python -m mcp.cli monitor --port 5000
227 | ```
228 |
229 | #### MCP Inspector
230 |
231 | ```bash
232 | # Start your MCP server in one terminal
233 | python -m penpot_mcp.server.mcp_server
234 |
235 | # In another terminal, run the MCP Inspector (requires Node.js)
236 | npx @modelcontextprotocol/inspector
237 | ```
238 |
239 | ### Using the Client
240 |
241 | ```bash
242 | # Run the example client
243 | penpot-client
244 | ```
245 |
246 | ## MCP Resources & Tools
247 |
248 | ### Resources
249 | - `server://info` - Server status and information
250 | - `penpot://schema` - Penpot API schema as JSON
251 | - `penpot://tree-schema` - Penpot object tree schema as JSON
252 | - `rendered-component://{component_id}` - Rendered component images
253 | - `penpot://cached-files` - List of cached Penpot files
254 |
255 | ### Tools
256 | - `list_projects` - List all Penpot projects
257 | - `get_project_files` - Get files for a specific project
258 | - `get_file` - Retrieve a Penpot file by its ID and cache it
259 | - `export_object` - Export a Penpot object as an image
260 | - `get_object_tree` - Get the object tree structure for a Penpot object
261 | - `search_object` - Search for objects within a Penpot file by name
262 |
263 | ## AI Integration
264 |
265 | The Penpot MCP server can be integrated with AI assistants using the Model Context Protocol. It supports both Claude Desktop and Cursor IDE for seamless design workflow automation.
266 |
267 | ### Claude Desktop Integration
268 |
269 | For detailed Claude Desktop setup instructions, see [CLAUDE_INTEGRATION.md](CLAUDE_INTEGRATION.md).
270 |
271 | Add the following configuration to your Claude Desktop config file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
272 |
273 | ```json
274 | {
275 | "mcpServers": {
276 | "penpot": {
277 | "command": "uvx",
278 | "args": ["penpot-mcp"],
279 | "env": {
280 | "PENPOT_API_URL": "https://design.penpot.app/api",
281 | "PENPOT_USERNAME": "your_penpot_username",
282 | "PENPOT_PASSWORD": "your_penpot_password"
283 | }
284 | }
285 | }
286 | }
287 | ```
288 |
289 | ### Cursor IDE Integration
290 |
291 | Cursor IDE supports MCP servers through its AI integration features. To configure Penpot MCP with Cursor:
292 |
293 | 1. **Install the MCP server** (if not already installed):
294 | ```bash
295 | pip install penpot-mcp
296 | ```
297 |
298 | 2. **Configure Cursor settings** by adding the MCP server to your Cursor configuration. Open Cursor settings and add:
299 |
300 | ```json
301 | {
302 | "mcpServers": {
303 | "penpot": {
304 | "command": "uvx",
305 | "args": ["penpot-mcp"],
306 | "env": {
307 | "PENPOT_API_URL": "https://design.penpot.app/api",
308 | "PENPOT_USERNAME": "your_penpot_username",
309 | "PENPOT_PASSWORD": "your_penpot_password"
310 | }
311 | }
312 | }
313 | }
314 | ```
315 |
316 | 3. **Alternative: Use environment variables** by creating a `.env` file in your project root:
317 | ```bash
318 | PENPOT_API_URL=https://design.penpot.app/api
319 | PENPOT_USERNAME=your_penpot_username
320 | PENPOT_PASSWORD=your_penpot_password
321 | ```
322 |
323 | 4. **Start the MCP server** in your project:
324 | ```bash
325 | # In your project directory
326 | penpot-mcp
327 | ```
328 |
329 | 5. **Use in Cursor**: Once configured, you can interact with your Penpot designs directly in Cursor by asking questions like:
330 | - "Show me all projects in my Penpot account"
331 | - "Analyze the design components in project X"
332 | - "Export the main button component as an image"
333 | - "What design patterns are used in this file?"
334 |
335 | ### Key Integration Features
336 |
337 | Both Claude Desktop and Cursor integration provide:
338 | - **Direct access** to Penpot projects and files
339 | - **Visual component analysis** with AI-powered insights
340 | - **Design export capabilities** for assets and components
341 | - **Natural language queries** about your design files
342 | - **Real-time design feedback** and suggestions
343 | - **Design system documentation** generation
344 |
345 | ## Package Structure
346 |
347 | ```
348 | penpot_mcp/
349 | ├── api/ # Penpot API client
350 | ├── server/ # MCP server implementation
351 | │ ├── mcp_server.py # Main MCP server
352 | │ └── client.py # Client implementation
353 | ├── tools/ # Utility tools
354 | │ ├── cli/ # Command-line interfaces
355 | │ └── penpot_tree.py # Penpot object tree visualization
356 | ├── resources/ # Resource files and schemas
357 | └── utils/ # Helper utilities
358 | ```
359 |
360 | ## Development
361 |
362 | ### Testing
363 |
364 | The project uses pytest for testing:
365 |
366 | ```bash
367 | # Using uv (recommended)
368 | uv sync --extra dev
369 | uv run pytest
370 |
371 | # Run with coverage
372 | uv run pytest --cov=penpot_mcp tests/
373 |
374 | # Using traditional pip
375 | pip install -e ".[dev]"
376 | pytest
377 | pytest --cov=penpot_mcp tests/
378 | ```
379 |
380 | ### Linting
381 |
382 | ```bash
383 | # Using uv (recommended)
384 | uv sync --extra dev
385 |
386 | # Set up pre-commit hooks
387 | uv run pre-commit install
388 |
389 | # Run linting
390 | uv run python lint.py
391 |
392 | # Auto-fix linting issues
393 | uv run python lint.py --autofix
394 |
395 | # Using traditional pip
396 | pip install -r requirements-dev.txt
397 | pre-commit install
398 | ./lint.py
399 | ./lint.py --autofix
400 | ```
401 |
402 | ## Contributing
403 |
404 | Contributions are welcome! Please feel free to submit a Pull Request.
405 |
406 | 1. Fork the repository
407 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
408 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
409 | 4. Push to the branch (`git push origin feature/amazing-feature`)
410 | 5. Open a Pull Request
411 |
412 | Please make sure your code follows the project's coding standards and includes appropriate tests.
413 |
414 | ## License
415 |
416 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
417 |
418 | ## Acknowledgments
419 |
420 | - [Penpot](https://penpot.app/) - The open-source design and prototyping platform
421 | - [Model Context Protocol](https://modelcontextprotocol.io) - The standardized protocol for AI model context
422 |
```
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
```markdown
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Project Overview
6 |
7 | Penpot MCP Server is a Python-based Model Context Protocol (MCP) server that bridges AI language models with Penpot, an open-source design platform. It enables programmatic interaction with design files through a well-structured API.
8 |
9 | ## Key Commands
10 |
11 | ### Development Setup
12 |
13 | ```bash
14 | # Install dependencies (recommended)
15 | uv sync --extra dev
16 |
17 | # Run the MCP server
18 | uv run penpot-mcp
19 |
20 | # Run tests
21 | uv run pytest
22 | uv run pytest --cov=penpot_mcp tests/ # with coverage
23 |
24 | # Lint and fix code
25 | uv run python lint.py # check issues
26 | uv run python lint.py --autofix # auto-fix issues
27 | ```
28 |
29 | ### Running the Server
30 |
31 | ```bash
32 | # Default stdio mode (for Claude Desktop/Cursor)
33 | make mcp-server
34 |
35 | # SSE mode (for debugging with inspector)
36 | make mcp-server-sse
37 |
38 | # Launch MCP inspector (requires SSE mode)
39 | make mcp-inspector
40 | ```
41 |
42 | ### CLI Tools
43 |
44 | ```bash
45 | # Generate tree visualization
46 | penpot-tree path/to/penpot_file.json
47 |
48 | # Validate Penpot file
49 | penpot-validate path/to/penpot_file.json
50 | ```
51 |
52 | ## Architecture Overview
53 |
54 | ### Core Components
55 |
56 | 1. **MCP Server** (`penpot_mcp/server/mcp_server.py`)
57 | - Built on FastMCP framework
58 | - Implements resources and tools for Penpot interaction
59 | - Memory cache with 10-minute TTL
60 | - Supports stdio (default) and SSE modes
61 |
62 | 2. **API Client** (`penpot_mcp/api/penpot_api.py`)
63 | - REST client for Penpot platform
64 | - Transit+JSON format handling
65 | - Cookie-based authentication with auto-refresh
66 | - Lazy authentication pattern
67 |
68 | 3. **Key Design Patterns**
69 | - **Authentication**: Cookie-based with automatic re-authentication on 401/403
70 | - **Caching**: In-memory file cache to reduce API calls
71 | - **Resource/Tool Duality**: Resources can be exposed as tools via RESOURCES_AS_TOOLS config
72 | - **Transit Format**: Special handling for UUIDs (`~u` prefix) and keywords (`~:` prefix)
73 |
74 | ### Available Tools/Functions
75 |
76 | - `list_projects`: Get all Penpot projects
77 | - `get_project_files`: List files in a project
78 | - `get_file`: Retrieve and cache file data
79 | - `search_object`: Search design objects by name (regex)
80 | - `get_object_tree`: Get filtered object tree with screenshot
81 | - `export_object`: Export design objects as images
82 | - `penpot_tree_schema`: Get schema for object tree fields
83 |
84 | ### Environment Configuration
85 |
86 | Create a `.env` file with:
87 |
88 | ```env
89 | PENPOT_API_URL=https://design.penpot.app/api
90 | PENPOT_USERNAME=your_username
91 | PENPOT_PASSWORD=your_password
92 | ENABLE_HTTP_SERVER=true # for image serving
93 | RESOURCES_AS_TOOLS=false # MCP resource mode
94 | DEBUG=true # debug logging
95 | ```
96 |
97 | ### Working with the Codebase
98 |
99 | 1. **Adding New Tools**: Decorate functions with `@self.mcp.tool()` in mcp_server.py
100 | 2. **API Extensions**: Add methods to PenpotAPI class following existing patterns
101 | 3. **Error Handling**: Always check for `"error"` keys in API responses
102 | 4. **Testing**: Use `test_mode=True` when creating server instances in tests
103 | 5. **Transit Format**: Remember to handle Transit+JSON when working with raw API
104 |
105 | ### Common Workflow for Code Generation
106 |
107 | 1. List projects → Find target project
108 | 2. Get project files → Locate design file
109 | 3. Search for component → Find specific element
110 | 4. Get tree schema → Understand available fields
111 | 5. Get object tree → Retrieve structure with screenshot
112 | 6. Export if needed → Get rendered component image
113 |
114 | ### Testing Patterns
115 |
116 | - Mock fixtures in `tests/conftest.py`
117 | - Test both stdio and SSE modes
118 | - Verify Transit format conversions
119 | - Check cache behavior and expiration
120 |
121 | ## Memories
122 |
123 | - Keep the current transport format for the current API requests
```
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
```markdown
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | We actively support the following versions of Penpot MCP with security updates:
6 |
7 | | Version | Supported |
8 | | ------- | ------------------ |
9 | | 0.1.x | :white_check_mark: |
10 | | < 0.1 | :x: |
11 |
12 | ## Reporting a Vulnerability
13 |
14 | The Penpot MCP team takes security seriously. If you discover a security vulnerability, please follow these steps:
15 |
16 | ### 🔒 Private Disclosure
17 |
18 | **DO NOT** create a public GitHub issue for security vulnerabilities.
19 |
20 | Instead, please email us at: **[email protected]**
21 |
22 | ### 📧 What to Include
23 |
24 | Please include the following information in your report:
25 |
26 | - **Description**: A clear description of the vulnerability
27 | - **Impact**: What could an attacker accomplish?
28 | - **Reproduction**: Step-by-step instructions to reproduce the issue
29 | - **Environment**: Affected versions, operating systems, configurations
30 | - **Proof of Concept**: Code, screenshots, or other evidence (if applicable)
31 | - **Suggested Fix**: If you have ideas for how to fix the issue
32 |
33 | ### 🕐 Response Timeline
34 |
35 | - **Initial Response**: Within 48 hours
36 | - **Triage**: Within 1 week
37 | - **Fix Development**: Depends on severity and complexity
38 | - **Public Disclosure**: After fix is released and users have time to update
39 |
40 | ### 🏆 Recognition
41 |
42 | We believe in recognizing security researchers who help keep our users safe:
43 |
44 | - **Security Hall of Fame**: Public recognition (with your permission)
45 | - **CVE Assignment**: For qualifying vulnerabilities
46 | - **Coordinated Disclosure**: We'll work with you on timing and attribution
47 |
48 | ## Security Considerations
49 |
50 | ### 🔐 Authentication & Credentials
51 |
52 | - **Penpot Credentials**: Store securely using environment variables or secure credential management
53 | - **API Keys**: Never commit API keys or passwords to version control
54 | - **Environment Files**: Add `.env` files to `.gitignore`
55 |
56 | ### 🌐 Network Security
57 |
58 | - **HTTPS Only**: Always use HTTPS for Penpot API connections
59 | - **Certificate Validation**: Don't disable SSL certificate verification
60 | - **Rate Limiting**: Respect API rate limits to avoid service disruption
61 |
62 | ### 🛡️ Input Validation
63 |
64 | - **User Input**: All user inputs are validated and sanitized
65 | - **File Uploads**: Penpot file parsing includes safety checks
66 | - **API Responses**: External API responses are validated before processing
67 |
68 | ### 🔍 Data Privacy
69 |
70 | - **Minimal Data**: We only access necessary Penpot data
71 | - **No Storage**: Design data is not permanently stored by default
72 | - **User Control**: Users control what data is shared with AI assistants
73 |
74 | ### 🚀 Deployment Security
75 |
76 | - **Dependencies**: Regularly update dependencies for security patches
77 | - **Permissions**: Run with minimal required permissions
78 | - **Isolation**: Use virtual environments or containers
79 |
80 | ## Security Best Practices for Users
81 |
82 | ### 🔧 Configuration
83 |
84 | ```bash
85 | # Use environment variables for sensitive data
86 | export PENPOT_USERNAME="your_username"
87 | export PENPOT_PASSWORD="your_secure_password"
88 | export PENPOT_API_URL="https://design.penpot.app/api"
89 |
90 | # Or use a .env file (never commit this!)
91 | echo "PENPOT_USERNAME=your_username" > .env
92 | echo "PENPOT_PASSWORD=your_secure_password" >> .env
93 | echo "PENPOT_API_URL=https://design.penpot.app/api" >> .env
94 | ```
95 |
96 | ### 🔒 Access Control
97 |
98 | - **Principle of Least Privilege**: Only grant necessary Penpot permissions
99 | - **Regular Audits**: Review and rotate credentials regularly
100 | - **Team Access**: Use team accounts rather than personal credentials for shared projects
101 |
102 | ### 🖥️ Local Development
103 |
104 | ```bash
105 | # Keep your development environment secure
106 | chmod 600 .env # Restrict file permissions
107 | git add .env # This should fail if .gitignore is properly configured
108 | ```
109 |
110 | ### 🤖 AI Integration
111 |
112 | - **Data Sensitivity**: Be mindful of what design data you share with AI assistants
113 | - **Public vs Private**: Consider using private AI instances for sensitive designs
114 | - **Audit Logs**: Monitor what data is being accessed and shared
115 |
116 | ## Vulnerability Disclosure Policy
117 |
118 | ### 🎯 Scope
119 |
120 | This security policy applies to:
121 |
122 | - **Penpot MCP Server**: Core MCP protocol implementation
123 | - **API Client**: Penpot API integration code
124 | - **CLI Tools**: Command-line utilities
125 | - **Documentation**: Security-related documentation
126 |
127 | ### ⚠️ Out of Scope
128 |
129 | The following are outside our direct control but we'll help coordinate:
130 |
131 | - **Penpot Platform**: Report to Penpot team directly
132 | - **Third-party Dependencies**: We'll help coordinate with upstream maintainers
133 | - **AI Assistant Platforms**: Report to respective platform security teams
134 |
135 | ### 🚫 Testing Guidelines
136 |
137 | When testing for vulnerabilities:
138 |
139 | - **DO NOT** test against production Penpot instances without permission
140 | - **DO NOT** access data you don't own
141 | - **DO NOT** perform destructive actions
142 | - **DO** use test accounts and data
143 | - **DO** respect rate limits and terms of service
144 |
145 | ## Security Updates
146 |
147 | ### 📢 Notifications
148 |
149 | Security updates will be announced through:
150 |
151 | - **GitHub Security Advisories**: Primary notification method
152 | - **Release Notes**: Detailed in version release notes
153 | - **Email**: For critical vulnerabilities (if you've subscribed)
154 |
155 | ### 🔄 Update Process
156 |
157 | ```bash
158 | # Always update to the latest version for security fixes
159 | pip install --upgrade penpot-mcp
160 |
161 | # Or with uv
162 | uv add penpot-mcp@latest
163 | ```
164 |
165 | ## Contact
166 |
167 | - **Security Issues**: [email protected]
168 | - **General Questions**: Use [GitHub Discussions](https://github.com/montevive/penpot-mcp/discussions)
169 | - **Bug Reports**: [GitHub Issues](https://github.com/montevive/penpot-mcp/issues)
170 |
171 | ---
172 |
173 | Thank you for helping keep Penpot MCP and our community safe! 🛡️
```
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Contributing to Penpot MCP 🤝
2 |
3 | Thank you for your interest in contributing to Penpot MCP! This project aims to bridge AI assistants with Penpot design tools, and we welcome contributions from developers, designers, and AI enthusiasts.
4 |
5 | ## 🌟 Ways to Contribute
6 |
7 | ### For Developers
8 | - **Bug Fixes**: Help us squash bugs and improve stability
9 | - **New Features**: Add new MCP tools, resources, or AI integrations
10 | - **Performance**: Optimize API calls, caching, and response times
11 | - **Documentation**: Improve code documentation and examples
12 | - **Testing**: Add unit tests, integration tests, and edge case coverage
13 |
14 | ### For Designers
15 | - **Use Case Documentation**: Share how you use Penpot MCP in your workflow
16 | - **Feature Requests**: Suggest new AI-powered design features
17 | - **UI/UX Feedback**: Help improve the developer and user experience
18 | - **Design Examples**: Contribute example Penpot files for testing
19 |
20 | ### For AI Enthusiasts
21 | - **Prompt Engineering**: Improve AI interaction patterns
22 | - **Model Integration**: Add support for new AI models and assistants
23 | - **Workflow Automation**: Create AI-powered design automation scripts
24 | - **Research**: Explore new applications of AI in design workflows
25 |
26 | ## 🚀 Getting Started
27 |
28 | ### 1. Fork and Clone
29 |
30 | ```bash
31 | # Fork the repository on GitHub, then clone your fork
32 | git clone https://github.com/YOUR_USERNAME/penpot-mcp.git
33 | cd penpot-mcp
34 | ```
35 |
36 | ### 2. Set Up Development Environment
37 |
38 | ```bash
39 | # Install uv (recommended Python package manager)
40 | curl -LsSf https://astral.sh/uv/install.sh | sh
41 |
42 | # Install dependencies and set up development environment
43 | uv sync --extra dev
44 |
45 | # Install pre-commit hooks
46 | uv run pre-commit install
47 | ```
48 |
49 | ### 3. Configure Environment
50 |
51 | ```bash
52 | # Copy environment template
53 | cp env.example .env
54 |
55 | # Edit .env with your Penpot credentials
56 | # PENPOT_API_URL=https://design.penpot.app/api
57 | # PENPOT_USERNAME=your_username
58 | # PENPOT_PASSWORD=your_password
59 | ```
60 |
61 | ### 4. Run Tests
62 |
63 | ```bash
64 | # Run the full test suite
65 | uv run pytest
66 |
67 | # Run with coverage
68 | uv run pytest --cov=penpot_mcp
69 |
70 | # Run specific test categories
71 | uv run pytest -m "not slow" # Skip slow tests
72 | uv run pytest tests/test_api/ # Test specific module
73 | ```
74 |
75 | ## 🔧 Development Workflow
76 |
77 | ### Code Style
78 |
79 | We use automated code formatting and linting:
80 |
81 | ```bash
82 | # Run all linting and formatting
83 | uv run python lint.py
84 |
85 | # Auto-fix issues where possible
86 | uv run python lint.py --autofix
87 |
88 | # Check specific files
89 | uv run flake8 penpot_mcp/
90 | uv run isort penpot_mcp/
91 | ```
92 |
93 | ### Testing Guidelines
94 |
95 | - **Unit Tests**: Test individual functions and classes
96 | - **Integration Tests**: Test MCP protocol interactions
97 | - **API Tests**: Test Penpot API integration (use mocks for CI)
98 | - **End-to-End Tests**: Test complete workflows with real data
99 |
100 | ```bash
101 | # Test structure
102 | tests/
103 | ├── unit/ # Fast, isolated tests
104 | ├── integration/ # MCP protocol tests
105 | ├── api/ # Penpot API tests
106 | └── e2e/ # End-to-end workflow tests
107 | ```
108 |
109 | ### Adding New Features
110 |
111 | 1. **Create an Issue**: Discuss your idea before implementing
112 | 2. **Branch Naming**: Use descriptive names like `feature/ai-design-analysis`
113 | 3. **Small PRs**: Keep changes focused and reviewable
114 | 4. **Documentation**: Update README, docstrings, and examples
115 | 5. **Tests**: Add comprehensive tests for new functionality
116 |
117 | ### MCP Protocol Guidelines
118 |
119 | When adding new MCP tools or resources:
120 |
121 | ```python
122 | # Follow this pattern for new tools
123 | @mcp_tool("tool_name")
124 | async def new_tool(param1: str, param2: int = 10) -> dict:
125 | """
126 | Brief description of what this tool does.
127 |
128 | Args:
129 | param1: Description of parameter
130 | param2: Optional parameter with default
131 |
132 | Returns:
133 | Dictionary with tool results
134 | """
135 | # Implementation here
136 | pass
137 | ```
138 |
139 | ## 📝 Commit Guidelines
140 |
141 | We follow [Conventional Commits](https://www.conventionalcommits.org/):
142 |
143 | ```bash
144 | # Format: type(scope): description
145 | git commit -m "feat(api): add design component analysis tool"
146 | git commit -m "fix(mcp): handle connection timeout errors"
147 | git commit -m "docs(readme): add Claude Desktop setup guide"
148 | git commit -m "test(api): add unit tests for file export"
149 | ```
150 |
151 | ### Commit Types
152 | - `feat`: New features
153 | - `fix`: Bug fixes
154 | - `docs`: Documentation changes
155 | - `test`: Adding or updating tests
156 | - `refactor`: Code refactoring
157 | - `perf`: Performance improvements
158 | - `chore`: Maintenance tasks
159 |
160 | ## 🐛 Reporting Issues
161 |
162 | ### Bug Reports
163 | Use our [bug report template](.github/ISSUE_TEMPLATE/bug_report.md) and include:
164 | - Clear reproduction steps
165 | - Environment details (OS, Python version, etc.)
166 | - Error messages and logs
167 | - Expected vs actual behavior
168 |
169 | ### Feature Requests
170 | Use our [feature request template](.github/ISSUE_TEMPLATE/feature_request.md) and include:
171 | - Use case description
172 | - Proposed solution
173 | - Implementation ideas
174 | - Priority level
175 |
176 | ## 🔍 Code Review Process
177 |
178 | 1. **Automated Checks**: All PRs must pass CI/CD checks
179 | 2. **Peer Review**: At least one maintainer review required
180 | 3. **Testing**: New features must include tests
181 | 4. **Documentation**: Update relevant documentation
182 | 5. **Backwards Compatibility**: Avoid breaking changes when possible
183 |
184 | ## 🏆 Recognition
185 |
186 | Contributors are recognized in:
187 | - GitHub contributors list
188 | - Release notes for significant contributions
189 | - Special mentions for innovative features
190 | - Community showcase for creative use cases
191 |
192 | ## 📚 Resources
193 |
194 | ### Documentation
195 | - [MCP Protocol Specification](https://modelcontextprotocol.io)
196 | - [Penpot API Documentation](https://help.penpot.app/technical-guide/developer-resources/)
197 | - [Claude AI Integration Guide](CLAUDE_INTEGRATION.md)
198 |
199 | ### Community
200 | - [GitHub Discussions](https://github.com/montevive/penpot-mcp/discussions)
201 | - [Issues](https://github.com/montevive/penpot-mcp/issues)
202 | - [Penpot Community](https://community.penpot.app/)
203 |
204 | ## 📄 License
205 |
206 | By contributing to Penpot MCP, you agree that your contributions will be licensed under the [MIT License](LICENSE).
207 |
208 | ## ❓ Questions?
209 |
210 | - **General Questions**: Use [GitHub Discussions](https://github.com/montevive/penpot-mcp/discussions)
211 | - **Bug Reports**: Create an [issue](https://github.com/montevive/penpot-mcp/issues)
212 | - **Feature Ideas**: Use our [feature request template](.github/ISSUE_TEMPLATE/feature_request.md)
213 | - **Security Issues**: Email us at [email protected]
214 |
215 | ---
216 |
217 | Thank you for helping make Penpot MCP better! 🎨🤖
```
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Package tests."""
2 |
```
--------------------------------------------------------------------------------
/penpot_mcp/tools/cli/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Command-line interface tools for Penpot MCP."""
2 |
```
--------------------------------------------------------------------------------
/penpot_mcp/tools/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Tool implementations for the Penpot MCP server."""
2 |
```
--------------------------------------------------------------------------------
/penpot_mcp/server/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Server implementation for the Penpot MCP server."""
2 |
```
--------------------------------------------------------------------------------
/penpot_mcp/utils/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Utility functions and helper modules for the Penpot MCP server."""
2 |
```
--------------------------------------------------------------------------------
/penpot_mcp/api/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """PenpotAPI module for interacting with the Penpot design platform."""
2 |
```
--------------------------------------------------------------------------------
/penpot_mcp/__init__.py:
--------------------------------------------------------------------------------
```python
1 | """Penpot MCP Server - Model Context Protocol server for Penpot."""
2 |
3 | __version__ = "0.1.2"
4 | __author__ = "Montevive AI Team"
5 | __email__ = "[email protected]"
6 |
```
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Debug Penpot MCP Server",
9 | "type": "debugpy",
10 | "request": "launch",
11 | "program": "${workspaceFolder}/penpot_mcp/server/mcp_server.py",
12 | "justMyCode": false,
13 | "console": "integratedTerminal",
14 | "args": [
15 | "--mode",
16 | "sse"
17 | ]
18 | }
19 | ]
20 | }
```
--------------------------------------------------------------------------------
/penpot_mcp/utils/config.py:
--------------------------------------------------------------------------------
```python
1 | """Configuration module for the Penpot MCP server."""
2 |
3 | import os
4 |
5 | from dotenv import find_dotenv, load_dotenv
6 |
7 | # Load environment variables
8 | load_dotenv(find_dotenv())
9 |
10 | # Server configuration
11 | PORT = int(os.environ.get('PORT', 5000))
12 | DEBUG = os.environ.get('DEBUG', 'true').lower() == 'true'
13 | RESOURCES_AS_TOOLS = os.environ.get('RESOURCES_AS_TOOLS', 'true').lower() == 'true'
14 |
15 | # HTTP server for exported images
16 | ENABLE_HTTP_SERVER = os.environ.get('ENABLE_HTTP_SERVER', 'true').lower() == 'true'
17 | HTTP_SERVER_HOST = os.environ.get('HTTP_SERVER_HOST', 'localhost')
18 | HTTP_SERVER_PORT = int(os.environ.get('HTTP_SERVER_PORT', 0))
19 |
20 | # Penpot API configuration
21 | PENPOT_API_URL = os.environ.get('PENPOT_API_URL', 'https://design.penpot.app/api')
22 | PENPOT_USERNAME = os.environ.get('PENPOT_USERNAME')
23 | PENPOT_PASSWORD = os.environ.get('PENPOT_PASSWORD')
24 |
25 | RESOURCES_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'resources')
26 |
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve Penpot MCP
4 | title: '[BUG] '
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Environment (please complete the following information):**
27 | - OS: [e.g. Ubuntu 22.04, macOS 14.0, Windows 11]
28 | - Python version: [e.g. 3.12.0]
29 | - Penpot MCP version: [e.g. 0.1.0]
30 | - Penpot version: [e.g. 2.0.0]
31 | - AI Assistant: [e.g. Claude Desktop, Custom MCP client]
32 |
33 | **Configuration**
34 | - Are you using environment variables or .env file?
35 | - What's your PENPOT_API_URL?
36 | - Any custom configuration?
37 |
38 | **Logs**
39 | If applicable, add relevant log output:
40 | ```
41 | Paste logs here
42 | ```
43 |
44 | **Additional context**
45 | Add any other context about the problem here.
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for Penpot MCP
4 | title: '[FEATURE] '
5 | labels: 'enhancement'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Use case**
20 | Describe how this feature would be used:
21 | - Who would benefit from this feature?
22 | - In what scenarios would it be useful?
23 | - How would it improve the Penpot MCP workflow?
24 |
25 | **Implementation ideas**
26 | If you have ideas about how this could be implemented, please share them:
27 | - API changes needed
28 | - New MCP tools or resources
29 | - Integration points with Penpot or AI assistants
30 |
31 | **Additional context**
32 | Add any other context, screenshots, mockups, or examples about the feature request here.
33 |
34 | **Priority**
35 | How important is this feature to you?
36 | - [ ] Nice to have
37 | - [ ] Important for my workflow
38 | - [ ] Critical for adoption
```
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
```yaml
1 | version: 2
2 | updates:
3 | # Python dependencies
4 | - package-ecosystem: "pip"
5 | directory: "/"
6 | schedule:
7 | interval: "weekly"
8 | day: "monday"
9 | time: "09:00"
10 | timezone: "UTC"
11 | open-pull-requests-limit: 5
12 | reviewers:
13 | - "montevive"
14 | assignees:
15 | - "montevive"
16 | commit-message:
17 | prefix: "deps"
18 | include: "scope"
19 | labels:
20 | - "dependencies"
21 | - "python"
22 | groups:
23 | dev-dependencies:
24 | patterns:
25 | - "pytest*"
26 | - "flake8*"
27 | - "coverage*"
28 | - "pre-commit*"
29 | - "isort*"
30 | - "autopep8*"
31 | - "pyupgrade*"
32 | - "setuptools*"
33 | production-dependencies:
34 | patterns:
35 | - "mcp*"
36 | - "requests*"
37 | - "python-dotenv*"
38 | - "gunicorn*"
39 | - "anytree*"
40 | - "jsonschema*"
41 | - "PyYAML*"
42 |
43 | # GitHub Actions
44 | - package-ecosystem: "github-actions"
45 | directory: "/"
46 | schedule:
47 | interval: "monthly"
48 | day: "monday"
49 | time: "09:00"
50 | timezone: "UTC"
51 | open-pull-requests-limit: 3
52 | reviewers:
53 | - "montevive"
54 | commit-message:
55 | prefix: "ci"
56 | include: "scope"
57 | labels:
58 | - "dependencies"
59 | - "github-actions"
```
--------------------------------------------------------------------------------
/tests/test_config.py:
--------------------------------------------------------------------------------
```python
1 | """Tests for config module."""
2 |
3 | from penpot_mcp.utils import config
4 |
5 |
6 | def test_config_values():
7 | """Test that config has the expected values and types."""
8 | assert isinstance(config.PORT, int)
9 | assert isinstance(config.DEBUG, bool)
10 | assert isinstance(config.PENPOT_API_URL, str)
11 | assert config.RESOURCES_PATH is not None
12 |
13 |
14 | def test_environment_variable_override(monkeypatch):
15 | """Test that environment variables override default config values."""
16 | # Save original values
17 | original_port = config.PORT
18 | original_debug = config.DEBUG
19 | original_api_url = config.PENPOT_API_URL
20 |
21 | # Override with environment variables
22 | monkeypatch.setenv("PORT", "8080")
23 | monkeypatch.setenv("DEBUG", "false")
24 | monkeypatch.setenv("PENPOT_API_URL", "https://test.example.com/api")
25 |
26 | # Reload the config module to apply the environment variables
27 | import importlib
28 | importlib.reload(config)
29 |
30 | # Check the new values
31 | assert config.PORT == 8080
32 | assert config.DEBUG is False
33 | assert config.PENPOT_API_URL == "https://test.example.com/api"
34 |
35 | # Restore original values
36 | monkeypatch.setattr(config, "PORT", original_port)
37 | monkeypatch.setattr(config, "DEBUG", original_debug)
38 | monkeypatch.setattr(config, "PENPOT_API_URL", original_api_url)
39 |
```
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ### Added
11 | - Comprehensive CI/CD pipeline with GitHub Actions
12 | - Automated PyPI publishing on version bumps
13 | - CloudFlare error detection and user-friendly error handling
14 | - Version bump automation workflow
15 |
16 | ### Changed
17 | - Enhanced error handling in API client and MCP server
18 | - Improved documentation for setup and usage
19 |
20 | ### Fixed
21 | - CloudFlare protection blocking issues with helpful resolution instructions
22 |
23 | ## [0.1.1] - 2024-06-29
24 |
25 | ### Added
26 | - Initial MCP server implementation
27 | - Penpot API client with authentication
28 | - Object tree visualization and analysis tools
29 | - Export functionality for design objects
30 | - Cache system for improved performance
31 | - Comprehensive test suite
32 |
33 | ### Features
34 | - List and access Penpot projects and files
35 | - Search design objects by name with regex support
36 | - Get object tree structure with field filtering
37 | - Export design objects as images
38 | - Claude Desktop and Cursor IDE integration
39 | - HTTP server for image serving
40 |
41 | ## [0.1.0] - 2024-06-28
42 |
43 | ### Added
44 | - Initial project structure
45 | - Basic Penpot API integration
46 | - MCP protocol implementation
47 | - Core tool definitions
```
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
```markdown
1 | ## Description
2 |
3 | Brief description of the changes in this PR.
4 |
5 | ## Type of Change
6 |
7 | - [ ] Bug fix (non-breaking change which fixes an issue)
8 | - [ ] New feature (non-breaking change which adds functionality)
9 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
10 | - [ ] Documentation update
11 | - [ ] Performance improvement
12 | - [ ] Code refactoring
13 |
14 | ## Related Issues
15 |
16 | Fixes #(issue number)
17 |
18 | ## Changes Made
19 |
20 | - [ ] Added/modified MCP tools or resources
21 | - [ ] Updated Penpot API integration
22 | - [ ] Enhanced AI assistant compatibility
23 | - [ ] Improved error handling
24 | - [ ] Added tests
25 | - [ ] Updated documentation
26 |
27 | ## Testing
28 |
29 | - [ ] Tests pass locally
30 | - [ ] Added tests for new functionality
31 | - [ ] Tested with Claude Desktop integration
32 | - [ ] Tested with Penpot API
33 | - [ ] Manual testing completed
34 |
35 | ## Checklist
36 |
37 | - [ ] My code follows the project's style guidelines
38 | - [ ] I have performed a self-review of my code
39 | - [ ] I have commented my code, particularly in hard-to-understand areas
40 | - [ ] I have made corresponding changes to the documentation
41 | - [ ] My changes generate no new warnings
42 | - [ ] I have added tests that prove my fix is effective or that my feature works
43 | - [ ] New and existing unit tests pass locally with my changes
44 |
45 | ## Screenshots (if applicable)
46 |
47 | Add screenshots to help explain your changes.
48 |
49 | ## Additional Notes
50 |
51 | Any additional information that reviewers should know.
```
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
```python
1 | """Test configuration for Penpot MCP tests."""
2 |
3 | import os
4 | from unittest.mock import MagicMock
5 |
6 | import pytest
7 |
8 | from penpot_mcp.api.penpot_api import PenpotAPI
9 | from penpot_mcp.server.mcp_server import PenpotMCPServer
10 |
11 | # Add the project root directory to the Python path
12 | os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
13 |
14 |
15 | @pytest.fixture
16 | def mock_penpot_api(monkeypatch):
17 | """Create a mock PenpotAPI object."""
18 | mock_api = MagicMock(spec=PenpotAPI)
19 | # Add default behavior to the mock
20 | mock_api.list_projects.return_value = [
21 | {"id": "project1", "name": "Test Project 1"},
22 | {"id": "project2", "name": "Test Project 2"}
23 | ]
24 | mock_api.get_project_files.return_value = [
25 | {"id": "file1", "name": "Test File 1"},
26 | {"id": "file2", "name": "Test File 2"}
27 | ]
28 | mock_api.get_file.return_value = {
29 | "id": "file1",
30 | "name": "Test File",
31 | "data": {
32 | "pages": [
33 | {
34 | "id": "page1",
35 | "name": "Page 1",
36 | "objects": {
37 | "obj1": {"id": "obj1", "name": "Object 1", "type": "frame"},
38 | "obj2": {"id": "obj2", "name": "Object 2", "type": "text"}
39 | }
40 | }
41 | ]
42 | }
43 | }
44 | return mock_api
45 |
46 |
47 | @pytest.fixture
48 | def mock_server(mock_penpot_api):
49 | """Create a mock PenpotMCPServer with a mock API."""
50 | server = PenpotMCPServer(name="Test Server")
51 | server.api = mock_penpot_api
52 | return server
53 |
```
--------------------------------------------------------------------------------
/penpot_mcp/tools/cli/tree_cmd.py:
--------------------------------------------------------------------------------
```python
1 | """Command-line interface for the Penpot tree visualization tool."""
2 |
3 | import argparse
4 | import json
5 | import sys
6 | from typing import Any, Dict
7 |
8 | from penpot_mcp.tools.penpot_tree import build_tree, export_tree_to_dot, print_tree
9 |
10 |
11 | def parse_args() -> argparse.Namespace:
12 | """Parse command line arguments."""
13 | parser = argparse.ArgumentParser(description='Generate a tree from a Penpot JSON file')
14 | parser.add_argument('input_file', help='Path to the Penpot JSON file')
15 | parser.add_argument('--filter', '-f', help='Filter nodes by regex pattern')
16 | parser.add_argument('--export', '-e', help='Export tree to a file (supports PNG, SVG, etc.)')
17 | return parser.parse_args()
18 |
19 |
20 | def load_penpot_file(file_path: str) -> Dict[str, Any]:
21 | """
22 | Load a Penpot JSON file.
23 |
24 | Args:
25 | file_path: Path to the JSON file
26 |
27 | Returns:
28 | The loaded JSON data
29 |
30 | Raises:
31 | FileNotFoundError: If the file doesn't exist
32 | json.JSONDecodeError: If the file isn't valid JSON
33 | """
34 | try:
35 | with open(file_path, 'r') as f:
36 | return json.load(f)
37 | except FileNotFoundError:
38 | sys.exit(f"Error: File not found: {file_path}")
39 | except json.JSONDecodeError:
40 | sys.exit(f"Error: Invalid JSON file: {file_path}")
41 |
42 |
43 | def main() -> None:
44 | """Main entry point for the command."""
45 | args = parse_args()
46 |
47 | # Load the Penpot file
48 | data = load_penpot_file(args.input_file)
49 |
50 | # Build the tree
51 | root = build_tree(data)
52 |
53 | # Export the tree if requested
54 | if args.export:
55 | export_tree_to_dot(root, args.export, args.filter)
56 |
57 | # Print the tree
58 | print_tree(root, args.filter)
59 |
60 |
61 | if __name__ == '__main__':
62 | main()
63 |
```
--------------------------------------------------------------------------------
/tests/test_cache.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Tests for the memory caching functionality.
3 | """
4 |
5 | import time
6 |
7 | import pytest
8 |
9 | from penpot_mcp.utils.cache import MemoryCache
10 |
11 |
12 | @pytest.fixture
13 | def memory_cache():
14 | """Create a MemoryCache instance with a short TTL for testing."""
15 | return MemoryCache(ttl_seconds=2)
16 |
17 | def test_cache_set_get(memory_cache):
18 | """Test setting and getting a file from cache."""
19 | test_data = {"test": "data"}
20 | file_id = "test123"
21 |
22 | # Set data in cache
23 | memory_cache.set(file_id, test_data)
24 |
25 | # Get data from cache
26 | cached_data = memory_cache.get(file_id)
27 | assert cached_data == test_data
28 |
29 | def test_cache_expiration(memory_cache):
30 | """Test that cached files expire after TTL."""
31 | test_data = {"test": "data"}
32 | file_id = "test123"
33 |
34 | # Set data in cache
35 | memory_cache.set(file_id, test_data)
36 |
37 | # Data should be available immediately
38 | assert memory_cache.get(file_id) == test_data
39 |
40 | # Wait for cache to expire
41 | time.sleep(3)
42 |
43 | # Data should be expired
44 | assert memory_cache.get(file_id) is None
45 |
46 | def test_cache_clear(memory_cache):
47 | """Test clearing the cache."""
48 | test_data = {"test": "data"}
49 | file_id = "test123"
50 |
51 | # Set data in cache
52 | memory_cache.set(file_id, test_data)
53 |
54 | # Verify data is cached
55 | assert memory_cache.get(file_id) == test_data
56 |
57 | # Clear cache
58 | memory_cache.clear()
59 |
60 | # Verify data is gone
61 | assert memory_cache.get(file_id) is None
62 |
63 | def test_get_all_cached_files(memory_cache):
64 | """Test getting all cached files."""
65 | test_data1 = {"test": "data1"}
66 | test_data2 = {"test": "data2"}
67 |
68 | # Set multiple files in cache
69 | memory_cache.set("file1", test_data1)
70 | memory_cache.set("file2", test_data2)
71 |
72 | # Get all cached files
73 | all_files = memory_cache.get_all_cached_files()
74 |
75 | # Verify all files are present
76 | assert len(all_files) == 2
77 | assert all_files["file1"] == test_data1
78 | assert all_files["file2"] == test_data2
79 |
80 | # Wait for cache to expire
81 | time.sleep(3)
82 |
83 | # Verify expired files are removed
84 | all_files = memory_cache.get_all_cached_files()
85 | assert len(all_files) == 0
86 |
87 | def test_cache_nonexistent_file(memory_cache):
88 | """Test getting a nonexistent file from cache."""
89 | assert memory_cache.get("nonexistent") is None
```
--------------------------------------------------------------------------------
/penpot_mcp/utils/cache.py:
--------------------------------------------------------------------------------
```python
1 | """
2 | Cache utilities for Penpot MCP server.
3 | """
4 |
5 | import time
6 | from typing import Any, Dict, Optional
7 |
8 |
9 | class MemoryCache:
10 | """In-memory cache implementation with TTL support."""
11 |
12 | def __init__(self, ttl_seconds: int = 600):
13 | """
14 | Initialize the memory cache.
15 |
16 | Args:
17 | ttl_seconds: Time to live in seconds (default 10 minutes)
18 | """
19 | self.ttl_seconds = ttl_seconds
20 | self._cache: Dict[str, Dict[str, Any]] = {}
21 |
22 | def get(self, file_id: str) -> Optional[Dict[str, Any]]:
23 | """
24 | Get a file from cache if it exists and is not expired.
25 |
26 | Args:
27 | file_id: The ID of the file to retrieve
28 |
29 | Returns:
30 | The cached file data or None if not found/expired
31 | """
32 | if file_id not in self._cache:
33 | return None
34 |
35 | cache_data = self._cache[file_id]
36 |
37 | # Check if cache is expired
38 | if time.time() - cache_data['timestamp'] > self.ttl_seconds:
39 | del self._cache[file_id] # Remove expired cache
40 | return None
41 |
42 | return cache_data['data']
43 |
44 | def set(self, file_id: str, data: Dict[str, Any]) -> None:
45 | """
46 | Store a file in cache.
47 |
48 | Args:
49 | file_id: The ID of the file to cache
50 | data: The file data to cache
51 | """
52 | self._cache[file_id] = {
53 | 'timestamp': time.time(),
54 | 'data': data
55 | }
56 |
57 | def clear(self) -> None:
58 | """Clear all cached files."""
59 | self._cache.clear()
60 |
61 | def get_all_cached_files(self) -> Dict[str, Dict[str, Any]]:
62 | """
63 | Get all valid cached files.
64 |
65 | Returns:
66 | Dictionary mapping file IDs to their cached data
67 | """
68 | result = {}
69 | current_time = time.time()
70 |
71 | # Create a list of expired keys to remove
72 | expired_keys = []
73 |
74 | for file_id, cache_data in self._cache.items():
75 | if current_time - cache_data['timestamp'] <= self.ttl_seconds:
76 | result[file_id] = cache_data['data']
77 | else:
78 | expired_keys.append(file_id)
79 |
80 | # Remove expired entries
81 | for key in expired_keys:
82 | del self._cache[key]
83 |
84 | return result
```
--------------------------------------------------------------------------------
/CLAUDE_INTEGRATION.md:
--------------------------------------------------------------------------------
```markdown
1 | # Using Penpot MCP with Claude
2 |
3 | This guide explains how to integrate the Penpot MCP server with Claude AI using the Model Context Protocol (MCP).
4 |
5 | ## Prerequisites
6 |
7 | 1. Claude Desktop application installed
8 | 2. Penpot MCP server set up and configured
9 |
10 | ## Installing the Penpot MCP Server in Claude Desktop
11 |
12 | The easiest way to use the Penpot MCP server with Claude is to install it directly in Claude Desktop:
13 |
14 | 1. Make sure you have installed the required dependencies:
15 | ```bash
16 | pip install -r requirements.txt
17 | ```
18 |
19 | 2. Install the MCP server in Claude Desktop:
20 | ```bash
21 | mcp install mcp_server.py
22 | ```
23 |
24 | 3. Claude will ask for your permission to install the server. Click "Allow".
25 |
26 | 4. The Penpot MCP server will now appear in Claude's tool menu.
27 |
28 | ## Using Penpot in Claude
29 |
30 | Once installed, you can interact with Penpot through Claude by:
31 |
32 | 1. Open Claude Desktop
33 | 2. Click on the "+" button in the message input area
34 | 3. Select "Penpot MCP Server" from the list
35 | 4. Claude now has access to your Penpot projects and can:
36 | - List your projects
37 | - Get project details
38 | - Access file information
39 | - View components
40 |
41 | ## Example Prompts for Claude
42 |
43 | Here are some example prompts you can use with Claude to interact with your Penpot data:
44 |
45 | ### Listing Projects
46 |
47 | ```
48 | Can you show me a list of my Penpot projects?
49 | ```
50 |
51 | ### Getting Project Details
52 |
53 | ```
54 | Please show me the details of my most recent Penpot project.
55 | ```
56 |
57 | ### Working with Files
58 |
59 | ```
60 | Can you list the files in my "Website Redesign" project?
61 | ```
62 |
63 | ### Exploring Components
64 |
65 | ```
66 | Please show me the available UI components in Penpot.
67 | ```
68 |
69 | ## Troubleshooting
70 |
71 | If you encounter issues:
72 |
73 | 1. Check that your Penpot access token is correctly set in the environment variables
74 | 2. Verify that the Penpot API URL is correct
75 | 3. Try reinstalling the MCP server in Claude Desktop:
76 | ```bash
77 | mcp uninstall "Penpot MCP Server"
78 | mcp install mcp_server.py
79 | ```
80 |
81 | ## Advanced: Using with Other MCP-compatible Tools
82 |
83 | The Penpot MCP server can be used with any MCP-compatible client, not just Claude Desktop. Other integrations include:
84 |
85 | - OpenAI Agents SDK
86 | - PydanticAI
87 | - Python MCP clients (see `example_client.py`)
88 |
89 | Refer to the specific documentation for these tools for integration instructions.
90 |
91 | ## Resources
92 |
93 | - [Model Context Protocol Documentation](https://modelcontextprotocol.io)
94 | - [Claude Developer Documentation](https://docs.anthropic.com)
95 | - [MCP Python SDK Documentation](https://github.com/modelcontextprotocol/python-sdk)
```
--------------------------------------------------------------------------------
/penpot_mcp/tools/cli/validate_cmd.py:
--------------------------------------------------------------------------------
```python
1 | """Command-line interface for validating Penpot files against a schema."""
2 |
3 | import argparse
4 | import json
5 | import os
6 | import sys
7 | from typing import Any, Dict, Optional, Tuple
8 |
9 | from jsonschema import SchemaError, ValidationError, validate
10 |
11 | from penpot_mcp.utils import config
12 |
13 |
14 | def parse_args() -> argparse.Namespace:
15 | """Parse command line arguments."""
16 | parser = argparse.ArgumentParser(description='Validate a Penpot JSON file against a schema')
17 | parser.add_argument('input_file', help='Path to the Penpot JSON file to validate')
18 | parser.add_argument(
19 | '--schema',
20 | '-s',
21 | default=os.path.join(
22 | config.RESOURCES_PATH,
23 | 'penpot-schema.json'),
24 | help='Path to the JSON schema file (default: resources/penpot-schema.json)')
25 | parser.add_argument('--verbose', '-v', action='store_true',
26 | help='Enable verbose output with detailed validation errors')
27 | return parser.parse_args()
28 |
29 |
30 | def load_json_file(file_path: str) -> Dict[str, Any]:
31 | """
32 | Load a JSON file.
33 |
34 | Args:
35 | file_path: Path to the JSON file
36 |
37 | Returns:
38 | The loaded JSON data
39 |
40 | Raises:
41 | FileNotFoundError: If the file doesn't exist
42 | json.JSONDecodeError: If the file isn't valid JSON
43 | """
44 | try:
45 | with open(file_path, 'r') as f:
46 | return json.load(f)
47 | except FileNotFoundError:
48 | sys.exit(f"Error: File not found: {file_path}")
49 | except json.JSONDecodeError:
50 | sys.exit(f"Error: Invalid JSON file: {file_path}")
51 |
52 |
53 | def validate_penpot_file(data: Dict[str, Any], schema: Dict[str,
54 | Any]) -> Tuple[bool, Optional[str]]:
55 | """
56 | Validate a Penpot file against a schema.
57 |
58 | Args:
59 | data: The Penpot file data
60 | schema: The JSON schema
61 |
62 | Returns:
63 | Tuple of (is_valid, error_message)
64 | """
65 | try:
66 | validate(instance=data, schema=schema)
67 | return True, None
68 | except ValidationError as e:
69 | return False, str(e)
70 | except SchemaError as e:
71 | return False, f"Schema error: {str(e)}"
72 |
73 |
74 | def main() -> None:
75 | """Main entry point for the command."""
76 | args = parse_args()
77 |
78 | # Load the files
79 | print(f"Loading Penpot file: {args.input_file}")
80 | data = load_json_file(args.input_file)
81 |
82 | print(f"Loading schema file: {args.schema}")
83 | schema = load_json_file(args.schema)
84 |
85 | # Validate the file
86 | print("Validating file...")
87 | is_valid, error = validate_penpot_file(data, schema)
88 |
89 | if is_valid:
90 | print("✅ Validation successful! The file conforms to the schema.")
91 | else:
92 | print("❌ Validation failed!")
93 | if args.verbose and error:
94 | print("\nError details:")
95 | print(error)
96 | sys.exit(1)
97 |
98 |
99 | if __name__ == '__main__':
100 | main()
101 |
```
--------------------------------------------------------------------------------
/test_credentials.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Test script to verify Penpot API credentials and list projects.
4 | """
5 |
6 | import os
7 |
8 | from dotenv import load_dotenv
9 |
10 | from penpot_mcp.api.penpot_api import PenpotAPI
11 |
12 |
13 | def test_credentials():
14 | """Test Penpot API credentials and list projects."""
15 | load_dotenv()
16 |
17 | api_url = os.getenv("PENPOT_API_URL")
18 | username = os.getenv("PENPOT_USERNAME")
19 | password = os.getenv("PENPOT_PASSWORD")
20 |
21 | if not all([api_url, username, password]):
22 | print("❌ Missing credentials in .env file")
23 | print("Required: PENPOT_API_URL, PENPOT_USERNAME, PENPOT_PASSWORD")
24 | return False
25 |
26 | print(f"🔗 Testing connection to: {api_url}")
27 | print(f"👤 Username: {username}")
28 |
29 | try:
30 | api = PenpotAPI(api_url, debug=False, email=username, password=password)
31 |
32 | print("🔐 Authenticating...")
33 | token = api.login_with_password()
34 | print("✅ Authentication successful!")
35 |
36 | print("📁 Fetching projects...")
37 | projects = api.list_projects()
38 |
39 | if isinstance(projects, dict) and "error" in projects:
40 | print(f"❌ Failed to list projects: {projects['error']}")
41 | return False
42 |
43 | print(f"✅ Found {len(projects)} projects:")
44 | for i, project in enumerate(projects, 1):
45 | if isinstance(project, dict):
46 | name = project.get('name', 'Unnamed')
47 | project_id = project.get('id', 'N/A')
48 | team_name = project.get('team-name', 'Unknown Team')
49 | print(f" {i}. {name} (ID: {project_id}) - Team: {team_name}")
50 | else:
51 | print(f" {i}. {project}")
52 |
53 | # Test getting project files if we have a project
54 | if projects and isinstance(projects[0], dict):
55 | project_id = projects[0].get('id')
56 | if project_id:
57 | print(f"\n📄 Testing project files for project: {project_id}")
58 | try:
59 | files = api.get_project_files(project_id)
60 | print(f"✅ Found {len(files)} files:")
61 | for j, file in enumerate(files[:3], 1): # Show first 3 files
62 | if isinstance(file, dict):
63 | print(f" {j}. {file.get('name', 'Unnamed')} (ID: {file.get('id', 'N/A')})")
64 | else:
65 | print(f" {j}. {file}")
66 | if len(files) > 3:
67 | print(f" ... and {len(files) - 3} more files")
68 | except Exception as file_error:
69 | print(f"❌ Error getting files: {file_error}")
70 |
71 | return True
72 |
73 | except Exception as e:
74 | print(f"❌ Error: {e}")
75 | return False
76 |
77 |
78 | if __name__ == "__main__":
79 | success = test_credentials()
80 | exit(0 if success else 1)
```
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches: [ main, develop ]
6 | push:
7 | branches: [ main, develop ]
8 | workflow_call:
9 |
10 | jobs:
11 | test:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | python-version: ["3.10", "3.11", "3.12", "3.13"]
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - name: Set up Python ${{ matrix.python-version }}
21 | uses: actions/setup-python@v5
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 |
25 | - name: Install uv
26 | uses: astral-sh/setup-uv@v6
27 | with:
28 | version: "latest"
29 |
30 | - name: Install dependencies
31 | run: |
32 | uv sync --extra dev
33 |
34 | - name: Run linting
35 | run: |
36 | uv run python lint.py || echo "Linting found issues but continuing..."
37 | continue-on-error: true
38 |
39 | - name: Run tests with coverage
40 | run: |
41 | uv run pytest --cov=penpot_mcp tests/ --cov-report=xml --cov-report=term-missing
42 |
43 | - name: Upload coverage to Codecov
44 | uses: codecov/codecov-action@v5
45 | if: matrix.python-version == '3.12'
46 | with:
47 | file: ./coverage.xml
48 | flags: unittests
49 | name: codecov-umbrella
50 | fail_ci_if_error: false
51 |
52 | security-check:
53 | runs-on: ubuntu-latest
54 | steps:
55 | - uses: actions/checkout@v4
56 |
57 | - name: Set up Python
58 | uses: actions/setup-python@v5
59 | with:
60 | python-version: "3.12"
61 |
62 | - name: Install uv
63 | uses: astral-sh/setup-uv@v6
64 |
65 | - name: Install dependencies
66 | run: |
67 | uv sync --extra dev
68 |
69 | - name: Run security checks with bandit
70 | run: |
71 | uv add bandit[toml]
72 | uv run bandit -r penpot_mcp/ -f json -o bandit-report.json || true
73 |
74 | - name: Upload security scan results
75 | uses: github/codeql-action/upload-sarif@v3
76 | if: always()
77 | with:
78 | sarif_file: bandit-report.json
79 | continue-on-error: true
80 |
81 | build-test:
82 | runs-on: ubuntu-latest
83 | steps:
84 | - uses: actions/checkout@v4
85 |
86 | - name: Set up Python
87 | uses: actions/setup-python@v5
88 | with:
89 | python-version: "3.12"
90 |
91 | - name: Install uv
92 | uses: astral-sh/setup-uv@v6
93 |
94 | - name: Install dependencies
95 | run: |
96 | uv sync --extra dev
97 |
98 | - name: Build package
99 | run: |
100 | uv build
101 |
102 | - name: Test package installation
103 | run: |
104 | python -m pip install dist/*.whl
105 | penpot-mcp --help || echo "CLI help command failed"
106 | python -c "import penpot_mcp; print(f'Version: {penpot_mcp.__version__}')"
107 |
108 | - name: Upload build artifacts
109 | uses: actions/upload-artifact@v4
110 | with:
111 | name: dist-files
112 | path: dist/
113 | retention-days: 7
114 |
115 | test-docker:
116 | runs-on: ubuntu-latest
117 | steps:
118 | - uses: actions/checkout@v4
119 |
120 | - name: Set up Docker Buildx
121 | uses: docker/setup-buildx-action@v3
122 |
123 | - name: Create test Dockerfile
124 | run: |
125 | cat > Dockerfile.test << 'EOF'
126 | FROM python:3.12-slim
127 |
128 | # Install uv
129 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
130 |
131 | # Set working directory
132 | WORKDIR /app
133 |
134 | # Copy project files
135 | COPY . .
136 |
137 | # Install dependencies and run tests
138 | RUN uv sync --extra dev
139 | RUN uv run pytest
140 |
141 | # Test CLI commands
142 | RUN uv run penpot-mcp --help || echo "CLI help test completed"
143 | EOF
144 |
145 | - name: Build and test Docker image
146 | run: |
147 | docker build -f Dockerfile.test -t penpot-mcp-test .
```
--------------------------------------------------------------------------------
/.github/workflows/code-quality.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Code Quality
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | # Run weekly on Sundays at 2 AM UTC
7 | - cron: '0 2 * * 0'
8 |
9 | jobs:
10 | code-quality:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Set up Python
17 | uses: actions/setup-python@v5
18 | with:
19 | python-version: "3.12"
20 |
21 | - name: Install uv
22 | uses: astral-sh/setup-uv@v6
23 |
24 | - name: Install dependencies
25 | run: |
26 | uv sync --extra dev
27 |
28 | - name: Run comprehensive linting
29 | run: |
30 | echo "Running full linting analysis..."
31 | uv run python lint.py --autofix || true
32 |
33 | - name: Check for auto-fixes
34 | run: |
35 | if [[ -n $(git status --porcelain) ]]; then
36 | echo "Auto-fixes were applied"
37 | git diff
38 | else
39 | echo "No auto-fixes needed"
40 | fi
41 |
42 | - name: Create Pull Request for fixes
43 | if: success()
44 | uses: peter-evans/create-pull-request@v7
45 | with:
46 | token: ${{ secrets.GITHUB_TOKEN }}
47 | commit-message: "🔧 Auto-fix code quality issues"
48 | title: "🔧 Automated Code Quality Improvements"
49 | body: |
50 | ## Automated Code Quality Fixes
51 |
52 | This PR contains automated fixes for code quality issues:
53 |
54 | ### Changes Applied
55 | - Line length adjustments
56 | - Import sorting
57 | - Whitespace cleanup
58 | - Unused import removal
59 |
60 | ### Review Notes
61 | - All changes are automatically applied by linting tools
62 | - Tests should still pass after these changes
63 | - Manual review recommended for any significant changes
64 |
65 | 🤖 This PR was automatically created by the Code Quality workflow.
66 | branch: automated-code-quality-fixes
67 | delete-branch: true
68 | reviewers: montevive
69 | labels: |
70 | code-quality
71 | automated
72 | enhancement
73 |
74 | - name: Security Analysis
75 | run: |
76 | echo "Running security analysis..."
77 | uv add bandit[toml]
78 | uv run bandit -r penpot_mcp/ -f json -o bandit-report.json || true
79 |
80 | if [ -f bandit-report.json ]; then
81 | echo "Security report generated"
82 | cat bandit-report.json | head -20
83 | fi
84 |
85 | - name: Code Coverage Analysis
86 | run: |
87 | echo "Running code coverage analysis..."
88 | uv run pytest --cov=penpot_mcp tests/ --cov-report=html --cov-report=term
89 |
90 | echo "Coverage report generated in htmlcov/"
91 |
92 | - name: Upload Coverage Report
93 | uses: actions/upload-artifact@v4
94 | with:
95 | name: coverage-report
96 | path: htmlcov/
97 | retention-days: 30
98 |
99 | - name: Upload Security Report
100 | uses: actions/upload-artifact@v4
101 | if: always()
102 | with:
103 | name: security-report
104 | path: bandit-report.json
105 | retention-days: 30
106 |
107 | - name: Summary
108 | run: |
109 | echo "## Code Quality Summary" >> $GITHUB_STEP_SUMMARY
110 | echo "" >> $GITHUB_STEP_SUMMARY
111 | echo "### Linting" >> $GITHUB_STEP_SUMMARY
112 | echo "- Auto-fixes applied (if any)" >> $GITHUB_STEP_SUMMARY
113 | echo "" >> $GITHUB_STEP_SUMMARY
114 | echo "### Security Analysis" >> $GITHUB_STEP_SUMMARY
115 | echo "- Bandit security scan completed" >> $GITHUB_STEP_SUMMARY
116 | echo "" >> $GITHUB_STEP_SUMMARY
117 | echo "### Coverage" >> $GITHUB_STEP_SUMMARY
118 | echo "- Code coverage report generated" >> $GITHUB_STEP_SUMMARY
119 | echo "" >> $GITHUB_STEP_SUMMARY
120 | echo "### Artifacts" >> $GITHUB_STEP_SUMMARY
121 | echo "- Coverage report: htmlcov/" >> $GITHUB_STEP_SUMMARY
122 | echo "- Security report: bandit-report.json" >> $GITHUB_STEP_SUMMARY
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [build-system]
2 | requires = ["setuptools>=61.0", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "penpot-mcp"
7 | dynamic = ["version"]
8 | description = "Model Context Protocol server for Penpot - AI-powered design workflow automation"
9 | readme = "README.md"
10 | license = "MIT"
11 | authors = [
12 | {name = "Montevive AI Team", email = "[email protected]"}
13 | ]
14 | keywords = ["penpot", "mcp", "llm", "ai", "design", "prototyping", "claude", "cursor", "model-context-protocol"]
15 | classifiers = [
16 | "Development Status :: 4 - Beta",
17 | "Intended Audience :: Developers",
18 | "Intended Audience :: End Users/Desktop",
19 | "Programming Language :: Python :: 3",
20 | "Programming Language :: Python :: 3.10",
21 | "Programming Language :: Python :: 3.11",
22 | "Programming Language :: Python :: 3.12",
23 | "Programming Language :: Python :: 3.13",
24 | "Topic :: Software Development :: Libraries :: Python Modules",
25 | "Topic :: Multimedia :: Graphics :: Graphics Conversion",
26 | "Topic :: Scientific/Engineering :: Artificial Intelligence",
27 | "Topic :: Software Development :: User Interfaces",
28 | "Environment :: Console",
29 | "Operating System :: OS Independent",
30 | ]
31 | requires-python = ">=3.10"
32 | dependencies = [
33 | "mcp>=1.7.0",
34 | "python-dotenv>=1.0.0",
35 | "requests>=2.26.0",
36 | "gunicorn>=20.1.0",
37 | "anytree>=2.8.0",
38 | "jsonschema>=4.0.0",
39 | "PyYAML>=6.0.0",
40 | "twine>=6.1.0",
41 | ]
42 |
43 | [project.optional-dependencies]
44 | dev = [
45 | "pytest>=7.4.0",
46 | "pytest-mock>=3.11.1",
47 | "pytest-cov>=4.1.0",
48 | "flake8>=6.1.0",
49 | "flake8-docstrings>=1.7.0",
50 | "pre-commit>=3.5.0",
51 | "isort>=5.12.0",
52 | "autopep8>=2.0.4",
53 | "pyupgrade>=3.13.0",
54 | "setuptools>=65.5.0",
55 | ]
56 | cli = [
57 | "mcp[cli]>=1.7.0",
58 | ]
59 |
60 | [project.urls]
61 | Homepage = "https://github.com/montevive/penpot-mcp"
62 | Repository = "https://github.com/montevive/penpot-mcp.git"
63 | Issues = "https://github.com/montevive/penpot-mcp/issues"
64 | Documentation = "https://github.com/montevive/penpot-mcp#readme"
65 | Changelog = "https://github.com/montevive/penpot-mcp/releases"
66 |
67 | [project.scripts]
68 | penpot-mcp = "penpot_mcp.server.mcp_server:main"
69 | penpot-client = "penpot_mcp.server.client:main"
70 | penpot-tree = "penpot_mcp.tools.cli.tree_cmd:main"
71 | penpot-validate = "penpot_mcp.tools.cli.validate_cmd:main"
72 |
73 | [tool.setuptools.dynamic]
74 | version = {attr = "penpot_mcp.__version__"}
75 |
76 | [tool.setuptools.packages.find]
77 | where = ["."]
78 | include = ["penpot_mcp*"]
79 |
80 | [tool.setuptools.package-data]
81 | penpot_mcp = ["resources/*.json"]
82 |
83 | # pytest configuration
84 | [tool.pytest.ini_options]
85 | testpaths = ["tests"]
86 | python_files = ["test_*.py", "*_test.py"]
87 | python_classes = ["Test*"]
88 | python_functions = ["test_*"]
89 | addopts = [
90 | "--strict-markers",
91 | "--strict-config",
92 | "--verbose",
93 | ]
94 | markers = [
95 | "slow: marks tests as slow (deselect with '-m \"not slow\"')",
96 | "integration: marks tests as integration tests",
97 | ]
98 |
99 | # Coverage configuration
100 | [tool.coverage.run]
101 | source = ["penpot_mcp"]
102 | omit = [
103 | "*/tests/*",
104 | "*/test_*",
105 | "*/__pycache__/*",
106 | "*/venv/*",
107 | "*/.venv/*",
108 | ]
109 |
110 | [tool.coverage.report]
111 | exclude_lines = [
112 | "pragma: no cover",
113 | "def __repr__",
114 | "if self.debug:",
115 | "if settings.DEBUG",
116 | "raise AssertionError",
117 | "raise NotImplementedError",
118 | "if 0:",
119 | "if __name__ == .__main__.:",
120 | "class .*\\bProtocol\\):",
121 | "@(abc\\.)?abstractmethod",
122 | ]
123 |
124 | # isort configuration
125 | [tool.isort]
126 | profile = "black"
127 | multi_line_output = 3
128 | line_length = 88
129 | known_first_party = ["penpot_mcp"]
130 | skip = [".venv", "venv", "__pycache__"]
131 |
132 | # Black configuration (if you decide to use it)
133 | [tool.black]
134 | line-length = 88
135 | target-version = ['py312']
136 | include = '\.pyi?$'
137 | extend-exclude = '''
138 | /(
139 | # directories
140 | \.eggs
141 | | \.git
142 | | \.hg
143 | | \.mypy_cache
144 | | \.tox
145 | | \.venv
146 | | build
147 | | dist
148 | )/
149 | '''
150 |
```
--------------------------------------------------------------------------------
/fix-lint-deps.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 | # Helper script to install missing linting dependencies
3 |
4 | # Colors for output
5 | RED='\033[0;31m'
6 | GREEN='\033[0;32m'
7 | YELLOW='\033[0;33m'
8 | NC='\033[0m' # No Color
9 |
10 | # Function to create and activate a virtual environment
11 | create_venv() {
12 | echo -e "${YELLOW}Creating virtual environment in '$1'...${NC}"
13 | python3 -m venv "$1"
14 |
15 | if [ $? -ne 0 ]; then
16 | echo -e "${RED}Failed to create virtual environment.${NC}"
17 | echo "Make sure python3-venv is installed."
18 | echo "On Ubuntu/Debian: sudo apt install python3-venv"
19 | exit 1
20 | fi
21 |
22 | echo -e "${GREEN}Virtual environment created successfully.${NC}"
23 |
24 | # Activate the virtual environment
25 | if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
26 | # Windows
27 | source "$1/Scripts/activate"
28 | else
29 | # Unix/Linux/MacOS
30 | source "$1/bin/activate"
31 | fi
32 |
33 | if [ $? -ne 0 ]; then
34 | echo -e "${RED}Failed to activate virtual environment.${NC}"
35 | exit 1
36 | fi
37 |
38 | echo -e "${GREEN}Virtual environment activated.${NC}"
39 |
40 | # Upgrade pip to avoid issues
41 | pip install --upgrade pip
42 |
43 | if [ $? -ne 0 ]; then
44 | echo -e "${YELLOW}Warning: Could not upgrade pip, but continuing anyway.${NC}"
45 | fi
46 | }
47 |
48 | # Check if we're in a virtual environment
49 | if [[ -z "$VIRTUAL_ENV" ]]; then
50 | echo -e "${YELLOW}You are not in a virtual environment.${NC}"
51 |
52 | # Check if a virtual environment already exists
53 | if [ -d ".venv" ]; then
54 | echo "Found existing virtual environment in .venv directory."
55 | read -p "Would you like to use it? (y/n): " use_existing
56 |
57 | if [[ $use_existing == "y" || $use_existing == "Y" ]]; then
58 | create_venv ".venv"
59 | else
60 | read -p "Create a new virtual environment? (y/n): " create_new
61 |
62 | if [[ $create_new == "y" || $create_new == "Y" ]]; then
63 | read -p "Enter path for new virtual environment [.venv]: " venv_path
64 | venv_path=${venv_path:-.venv}
65 | create_venv "$venv_path"
66 | else
67 | echo -e "${RED}Cannot continue without a virtual environment.${NC}"
68 | echo "Using system Python is not recommended and may cause permission issues."
69 | echo "Please run this script again and choose to create a virtual environment."
70 | exit 1
71 | fi
72 | fi
73 | else
74 | read -p "Would you like to create a virtual environment? (y/n): " create_new
75 |
76 | if [[ $create_new == "y" || $create_new == "Y" ]]; then
77 | read -p "Enter path for new virtual environment [.venv]: " venv_path
78 | venv_path=${venv_path:-.venv}
79 | create_venv "$venv_path"
80 | else
81 | echo -e "${RED}Cannot continue without a virtual environment.${NC}"
82 | echo "Using system Python is not recommended and may cause permission issues."
83 | echo "Please run this script again and choose to create a virtual environment."
84 | exit 1
85 | fi
86 | fi
87 | else
88 | echo -e "${GREEN}Using existing virtual environment: $VIRTUAL_ENV${NC}"
89 | fi
90 |
91 | # Install development dependencies
92 | echo -e "${YELLOW}Installing linting dependencies...${NC}"
93 | pip install -r requirements-dev.txt
94 |
95 | if [ $? -ne 0 ]; then
96 | echo -e "${RED}Failed to install dependencies.${NC}"
97 | exit 1
98 | fi
99 |
100 | echo -e "${GREEN}Dependencies installed successfully.${NC}"
101 |
102 | # Install pre-commit hooks
103 | echo -e "${YELLOW}Setting up pre-commit hooks...${NC}"
104 | pre-commit install
105 |
106 | if [ $? -ne 0 ]; then
107 | echo -e "${RED}Failed to install pre-commit hooks.${NC}"
108 | exit 1
109 | fi
110 |
111 | echo -e "${GREEN}Pre-commit hooks installed successfully.${NC}"
112 |
113 | echo -e "\n${GREEN}Setup completed!${NC}"
114 | echo "You can now run the linting script with:"
115 | echo " ./lint.py"
116 | echo "Or with auto-fix:"
117 | echo " ./lint.py --autofix"
118 | echo ""
119 | echo "Remember to activate your virtual environment whenever you open a new terminal:"
120 | echo " source .venv/bin/activate # On Linux/macOS"
121 | echo " .venv\\Scripts\\activate # On Windows"
```
--------------------------------------------------------------------------------
/penpot_mcp/utils/http_server.py:
--------------------------------------------------------------------------------
```python
1 | """HTTP server module for serving exported images from memory."""
2 |
3 | import io
4 | import json
5 | import socketserver
6 | import threading
7 | from http.server import BaseHTTPRequestHandler, HTTPServer
8 |
9 |
10 | class InMemoryImageHandler(BaseHTTPRequestHandler):
11 | """HTTP request handler for serving images stored in memory."""
12 |
13 | # Class variable to store images
14 | images = {}
15 |
16 | def do_GET(self):
17 | """Handle GET requests."""
18 | # Remove query parameters if any
19 | path = self.path.split('?', 1)[0]
20 | path = path.split('#', 1)[0]
21 |
22 | # Extract image ID from path
23 | # Expected path format: /images/{image_id}.{format}
24 | parts = path.split('/')
25 | if len(parts) == 3 and parts[1] == 'images':
26 | # Extract image_id by removing the file extension if present
27 | image_id_with_ext = parts[2]
28 | image_id = image_id_with_ext.split('.')[0]
29 |
30 | if image_id in self.images:
31 | img_data = self.images[image_id]['data']
32 | img_format = self.images[image_id]['format']
33 |
34 | # Set content type based on format
35 | content_type = f"image/{img_format}"
36 | if img_format == 'svg':
37 | content_type = 'image/svg+xml'
38 |
39 | self.send_response(200)
40 | self.send_header('Content-type', content_type)
41 | self.send_header('Content-length', len(img_data))
42 | self.end_headers()
43 | self.wfile.write(img_data)
44 | return
45 |
46 | # Return 404 if image not found
47 | self.send_response(404)
48 | self.send_header('Content-type', 'application/json')
49 | self.end_headers()
50 | response = {'error': 'Image not found'}
51 | self.wfile.write(json.dumps(response).encode())
52 |
53 |
54 | class ImageServer:
55 | """Server for in-memory images."""
56 |
57 | def __init__(self, host='localhost', port=0):
58 | """Initialize the HTTP server.
59 |
60 | Args:
61 | host: Host address to listen on
62 | port: Port to listen on (0 means use a random available port)
63 | """
64 | self.host = host
65 | self.port = port
66 | self.server = None
67 | self.server_thread = None
68 | self.is_running = False
69 | self.base_url = None
70 |
71 | def start(self):
72 | """Start the HTTP server in a background thread.
73 |
74 | Returns:
75 | Base URL of the server with actual port used
76 | """
77 | if self.is_running:
78 | return self.base_url
79 |
80 | # Create TCP server with address reuse enabled
81 | class ReuseAddressTCPServer(socketserver.TCPServer):
82 | allow_reuse_address = True
83 |
84 | self.server = ReuseAddressTCPServer((self.host, self.port), InMemoryImageHandler)
85 |
86 | # Get the actual port that was assigned
87 | self.port = self.server.socket.getsockname()[1]
88 | self.base_url = f"http://{self.host}:{self.port}"
89 |
90 | # Start server in a separate thread
91 | self.server_thread = threading.Thread(target=self.server.serve_forever)
92 | self.server_thread.daemon = True # Don't keep process running if main thread exits
93 | self.server_thread.start()
94 | self.is_running = True
95 |
96 | print(f"Image server started at {self.base_url}")
97 | return self.base_url
98 |
99 | def stop(self):
100 | """Stop the HTTP server."""
101 | if not self.is_running:
102 | return
103 |
104 | self.server.shutdown()
105 | self.server.server_close()
106 | self.is_running = False
107 | print("Image server stopped")
108 |
109 | def add_image(self, image_id, image_data, image_format='png'):
110 | """Add image to in-memory storage.
111 |
112 | Args:
113 | image_id: Unique identifier for the image
114 | image_data: Binary image data
115 | image_format: Image format (png, jpg, etc.)
116 |
117 | Returns:
118 | URL to access the image
119 | """
120 | InMemoryImageHandler.images[image_id] = {
121 | 'data': image_data,
122 | 'format': image_format
123 | }
124 | return f"{self.base_url}/images/{image_id}.{image_format}"
125 |
126 | def remove_image(self, image_id):
127 | """Remove image from in-memory storage."""
128 | if image_id in InMemoryImageHandler.images:
129 | del InMemoryImageHandler.images[image_id]
```
--------------------------------------------------------------------------------
/LINTING.md:
--------------------------------------------------------------------------------
```markdown
1 | # Linting Guide
2 |
3 | This document provides guidelines on how to work with the linting tools configured in this project.
4 |
5 | ## Overview
6 |
7 | The project uses the following linting tools:
8 |
9 | - **flake8**: Code style and quality checker
10 | - **isort**: Import sorting
11 | - **autopep8**: PEP 8 code formatting with auto-fix capability
12 | - **pyupgrade**: Upgrades Python syntax for newer versions
13 | - **pre-commit**: Framework for managing pre-commit hooks
14 |
15 | ## Quick Start
16 |
17 | 1. Use the setup script to install all dependencies and set up pre-commit hooks:
18 |
19 | ```bash
20 | ./fix-lint-deps.sh
21 | ```
22 |
23 | Or install dependencies manually:
24 |
25 | ```bash
26 | pip install -r requirements-dev.txt
27 | pre-commit install
28 | ```
29 |
30 | 2. Run the linting script:
31 |
32 | ```bash
33 | # Check for issues
34 | ./lint.py
35 |
36 | # Fix issues automatically where possible
37 | ./lint.py --autofix
38 | ```
39 |
40 | ## Dependencies
41 |
42 | The linting tools require specific dependencies:
43 |
44 | - **flake8** and **flake8-docstrings**: For code style and documentation checking
45 | - **isort**: For import sorting
46 | - **autopep8**: For automatic PEP 8 compliance
47 | - **pyupgrade**: For Python syntax upgrading
48 | - **setuptools**: Required for lib2to3 which is used by autopep8
49 |
50 | If you encounter a `ModuleNotFoundError: No module named 'lib2to3'` error, make sure you have setuptools installed:
51 |
52 | ```bash
53 | pip install setuptools>=65.5.0
54 | ```
55 |
56 | Or simply run the fix script:
57 |
58 | ```bash
59 | ./fix-lint-deps.sh
60 | ```
61 |
62 | ## Configuration
63 |
64 | The linting tools are configured in the following files:
65 |
66 | - **setup.cfg**: Contains settings for flake8, autopep8, etc.
67 | - **.pre-commit-config.yaml**: Configuration for pre-commit hooks
68 | - **.editorconfig**: Editor settings for consistent code formatting
69 |
70 | ## Linting Rules
71 |
72 | ### Code Style Rules
73 |
74 | We follow PEP 8 with some exceptions:
75 |
76 | - **Line Length**: Max line length is 100 characters
77 | - **Ignored Rules**:
78 | - E203: Whitespace before ':' (conflicts with Black)
79 | - W503: Line break before binary operator (conflicts with Black)
80 |
81 | ### Documentation Rules
82 |
83 | All public modules, functions, classes, and methods should have docstrings. We use the Google style for docstrings.
84 |
85 | Example:
86 |
87 | ```python
88 | def function(param1, param2):
89 | """Summary of function purpose.
90 |
91 | More detailed explanation if needed.
92 |
93 | Args:
94 | param1: Description of param1.
95 | param2: Description of param2.
96 |
97 | Returns:
98 | Description of return value.
99 |
100 | Raises:
101 | ExceptionType: When and why this exception is raised.
102 | """
103 | # function implementation
104 | ```
105 |
106 | ### Import Sorting
107 |
108 | Imports should be sorted using isort with the black profile. Imports are grouped in the following order:
109 |
110 | 1. Standard library imports
111 | 2. Related third-party imports
112 | 3. Local application/library specific imports
113 |
114 | With each group sorted alphabetically.
115 |
116 | ## Auto-Fixing Issues
117 |
118 | Many issues can be fixed automatically:
119 |
120 | - **Import Sorting**: `isort` can sort imports automatically
121 | - **PEP 8 Formatting**: `autopep8` can fix many style issues
122 | - **Python Syntax**: `pyupgrade` can update syntax to newer Python versions
123 |
124 | Run the auto-fix command:
125 |
126 | ```bash
127 | ./lint.py --autofix
128 | ```
129 |
130 | ## Troubleshooting
131 |
132 | If you encounter issues with the linting tools:
133 |
134 | 1. **Missing dependencies**: Run `./fix-lint-deps.sh` to install all required dependencies
135 | 2. **Autopep8 errors**: Make sure setuptools is installed for lib2to3 support
136 | 3. **Pre-commit hook failures**: Run `pre-commit run --all-files` to see which files are causing issues
137 |
138 | ## Pre-commit Hooks
139 |
140 | Pre-commit hooks run automatically when you commit changes. They ensure that linting issues are caught before code is committed.
141 |
142 | If hooks fail during a commit:
143 |
144 | 1. The commit will be aborted
145 | 2. Review the error messages
146 | 3. Fix the issues manually or using auto-fix
147 | 4. Stage the fixed files
148 | 5. Retry your commit
149 |
150 | ## Common Issues and Solutions
151 |
152 | ### Disabling Linting for Specific Lines
153 |
154 | Sometimes it's necessary to disable linting for specific lines:
155 |
156 | ```python
157 | # For flake8
158 | some_code = "example" # noqa: E501
159 |
160 | # For multiple rules
161 | some_code = "example" # noqa: E501, F401
162 | ```
163 |
164 | ### Handling Third-Party Code
165 |
166 | For third-party code that doesn't follow our style, consider isolating it in a separate file or directory and excluding it from linting.
167 |
168 | ## IDE Integration
169 |
170 | ### VSCode
171 |
172 | Install the Python, Flake8, and EditorConfig extensions. Add to settings.json:
173 |
174 | ```json
175 | {
176 | "python.linting.enabled": true,
177 | "python.linting.flake8Enabled": true,
178 | "editor.formatOnSave": true,
179 | "python.formatting.provider": "autopep8",
180 | "python.sortImports.args": ["--profile", "black"]
181 | }
182 | ```
183 |
184 | ### PyCharm
185 |
186 | Enable Flake8 in:
187 | Settings → Editor → Inspections → Python → Flake8
188 |
189 | Configure isort:
190 | Settings → Editor → Code Style → Python → Imports
191 |
192 | ## Customizing Linting Rules
193 |
194 | To modify linting rules:
195 |
196 | 1. Edit `setup.cfg` for flake8 and autopep8 settings
197 | 2. Edit `.pre-commit-config.yaml` for pre-commit hook settings
198 | 3. Run `pre-commit autoupdate` to update hook versions
199 |
200 | ## Continuous Integration
201 |
202 | Linting checks are part of the CI pipeline. Pull requests that fail linting will not be merged until issues are fixed.
203 |
```
--------------------------------------------------------------------------------
/.github/workflows/version-bump.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Version Bump
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | version-type:
7 | description: 'Version bump type'
8 | required: true
9 | default: 'patch'
10 | type: choice
11 | options:
12 | - patch
13 | - minor
14 | - major
15 | custom-version:
16 | description: 'Custom version (optional, overrides version-type)'
17 | required: false
18 | type: string
19 |
20 | jobs:
21 | bump-version:
22 | runs-on: ubuntu-latest
23 | steps:
24 | - uses: actions/checkout@v4
25 | with:
26 | token: ${{ secrets.GITHUB_TOKEN }}
27 | fetch-depth: 0
28 |
29 | - name: Set up Python
30 | uses: actions/setup-python@v5
31 | with:
32 | python-version: "3.12"
33 |
34 | - name: Install dependencies
35 | run: |
36 | python -m pip install --upgrade pip
37 | pip install packaging
38 |
39 | - name: Get current version
40 | id: current-version
41 | run: |
42 | CURRENT_VERSION=$(python -c "import penpot_mcp; print(penpot_mcp.__version__)")
43 | echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
44 | echo "Current version: $CURRENT_VERSION"
45 |
46 | - name: Calculate new version
47 | id: new-version
48 | run: |
49 | python << 'EOF'
50 | import os
51 | from packaging import version
52 |
53 | current = "${{ steps.current-version.outputs.current }}"
54 | custom = "${{ github.event.inputs.custom-version }}"
55 | bump_type = "${{ github.event.inputs.version-type }}"
56 |
57 | if custom:
58 | new_version = custom
59 | else:
60 | v = version.parse(current)
61 | if bump_type == "major":
62 | new_version = f"{v.major + 1}.0.0"
63 | elif bump_type == "minor":
64 | new_version = f"{v.major}.{v.minor + 1}.0"
65 | else: # patch
66 | new_version = f"{v.major}.{v.minor}.{v.micro + 1}"
67 |
68 | print(f"New version: {new_version}")
69 |
70 | with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
71 | f.write(f"version={new_version}\n")
72 | EOF
73 |
74 | - name: Update version in files
75 | run: |
76 | NEW_VERSION="${{ steps.new-version.outputs.version }}"
77 |
78 | # Update __init__.py
79 | sed -i "s/__version__ = \".*\"/__version__ = \"$NEW_VERSION\"/" penpot_mcp/__init__.py
80 |
81 | # Verify the change
82 | echo "Updated version in penpot_mcp/__init__.py:"
83 | grep "__version__" penpot_mcp/__init__.py
84 |
85 | - name: Create changelog entry
86 | run: |
87 | NEW_VERSION="${{ steps.new-version.outputs.version }}"
88 | DATE=$(date +"%Y-%m-%d")
89 |
90 | # Create CHANGELOG.md if it doesn't exist
91 | if [ ! -f CHANGELOG.md ]; then
92 | cat > CHANGELOG.md << 'EOF'
93 | # Changelog
94 |
95 | All notable changes to this project will be documented in this file.
96 |
97 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
98 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
99 |
100 | EOF
101 | fi
102 |
103 | # Add new version entry
104 | sed -i "3i\\\\n## [$NEW_VERSION] - $DATE\\\\n\\\\n### Added\\\\n- Version bump to $NEW_VERSION\\\\n\\\\n### Changed\\\\n- Update dependencies and improve stability\\\\n\\\\n### Fixed\\\\n- Bug fixes and performance improvements\\\\n" CHANGELOG.md
105 |
106 | echo "Updated CHANGELOG.md with version $NEW_VERSION"
107 |
108 | - name: Commit and push changes
109 | run: |
110 | NEW_VERSION="${{ steps.new-version.outputs.version }}"
111 |
112 | git config --local user.email "[email protected]"
113 | git config --local user.name "GitHub Action"
114 |
115 | git add penpot_mcp/__init__.py CHANGELOG.md
116 | git commit -m "Bump version to $NEW_VERSION
117 |
118 | - Update version in __init__.py to $NEW_VERSION
119 | - Add changelog entry for version $NEW_VERSION
120 |
121 | 🤖 Generated with GitHub Actions"
122 |
123 | git push
124 |
125 | echo "✅ Version bumped to $NEW_VERSION and pushed to repository"
126 |
127 | - name: Create pull request (if on branch)
128 | if: github.ref != 'refs/heads/main'
129 | uses: peter-evans/create-pull-request@v7
130 | with:
131 | token: ${{ secrets.GITHUB_TOKEN }}
132 | commit-message: "Bump version to ${{ steps.new-version.outputs.version }}"
133 | title: "🔖 Bump version to ${{ steps.new-version.outputs.version }}"
134 | body: |
135 | ## Version Bump to ${{ steps.new-version.outputs.version }}
136 |
137 | This PR was automatically created to bump the version.
138 |
139 | ### Changes
140 | - Updated `__version__` in `penpot_mcp/__init__.py`
141 | - Added changelog entry for version ${{ steps.new-version.outputs.version }}
142 |
143 | ### Type of Change
144 | - [${{ github.event.inputs.version-type == 'major' && 'x' || ' ' }}] Major version (breaking changes)
145 | - [${{ github.event.inputs.version-type == 'minor' && 'x' || ' ' }}] Minor version (new features)
146 | - [${{ github.event.inputs.version-type == 'patch' && 'x' || ' ' }}] Patch version (bug fixes)
147 |
148 | ### Checklist
149 | - [x] Version updated in `__init__.py`
150 | - [x] Changelog updated
151 | - [ ] Tests pass (will be verified by CI)
152 | - [ ] Ready for merge and auto-publish
153 |
154 | **Note**: Merging this PR to `main` will trigger automatic publishing to PyPI.
155 | branch: version-bump-${{ steps.new-version.outputs.version }}
156 | delete-branch: true
```
--------------------------------------------------------------------------------
/.github/SETUP_CICD.md:
--------------------------------------------------------------------------------
```markdown
1 | # CI/CD Setup Guide
2 |
3 | This guide explains how to set up the CI/CD pipeline for automatic testing and PyPI publishing.
4 |
5 | ## 🚀 Quick Setup
6 |
7 | ### 1. PyPI API Tokens
8 |
9 | You need to create API tokens for both PyPI and Test PyPI:
10 |
11 | #### Create PyPI API Token
12 | 1. Go to [PyPI Account Settings](https://pypi.org/manage/account/)
13 | 2. Scroll to "API tokens" section
14 | 3. Click "Add API token"
15 | 4. Set name: `penpot-mcp-github-actions`
16 | 5. Scope: "Entire account" (or specific to `penpot-mcp` project if it exists)
17 | 6. Copy the token (starts with `pypi-`)
18 |
19 | #### Create Test PyPI API Token
20 | 1. Go to [Test PyPI Account Settings](https://test.pypi.org/manage/account/)
21 | 2. Follow same steps as above
22 | 3. Copy the token
23 |
24 | ### 2. GitHub Secrets Configuration
25 |
26 | Add the following secrets to your GitHub repository:
27 |
28 | 1. Go to your GitHub repository
29 | 2. Navigate to **Settings** → **Secrets and variables** → **Actions**
30 | 3. Click **New repository secret** and add:
31 |
32 | | Secret Name | Value | Description |
33 | |-------------|-------|-------------|
34 | | `PYPI_API_TOKEN` | `pypi-AgEIcHl...` | Your PyPI API token |
35 | | `TEST_PYPI_API_TOKEN` | `pypi-AgEIcHl...` | Your Test PyPI API token |
36 |
37 | ### 3. Enable GitHub Actions
38 |
39 | 1. Go to **Settings** → **Actions** → **General**
40 | 2. Ensure "Allow all actions and reusable workflows" is selected
41 | 3. Under "Workflow permissions":
42 | - Select "Read and write permissions"
43 | - Check "Allow GitHub Actions to create and approve pull requests"
44 |
45 | ## 📋 Workflow Overview
46 |
47 | ### CI Workflow (`.github/workflows/ci.yml`)
48 |
49 | **Triggers:**
50 | - Pull requests to `main` or `develop` branches
51 | - Pushes to `main` or `develop` branches
52 |
53 | **Jobs:**
54 | - **Test Matrix**: Tests across Python 3.10, 3.11, 3.12, 3.13
55 | - **Security Check**: Runs `bandit` security analysis
56 | - **Build Test**: Tests package building and installation
57 | - **Docker Test**: Tests Docker containerization
58 |
59 | **Features:**
60 | - ✅ Cross-platform testing (Linux, macOS, Windows can be added)
61 | - ✅ Multiple Python version support
62 | - ✅ Code coverage reporting (uploads to Codecov)
63 | - ✅ Security vulnerability scanning
64 | - ✅ Package build verification
65 | - ✅ Docker compatibility testing
66 |
67 | ### CD Workflow (`.github/workflows/publish.yml`)
68 |
69 | **Triggers:**
70 | - Pushes to `main` branch (automatic)
71 | - GitHub releases (manual)
72 |
73 | **Auto-Publish Process:**
74 | 1. ✅ Runs full CI test suite first
75 | 2. ✅ Checks if version was bumped in `__init__.py`
76 | 3. ✅ Skips publishing if version already exists on PyPI
77 | 4. ✅ Builds and validates package
78 | 5. ✅ Tests package installation
79 | 6. ✅ Publishes to Test PyPI first (optional)
80 | 7. ✅ Publishes to PyPI
81 | 8. ✅ Creates GitHub release automatically
82 | 9. ✅ Uploads release assets
83 |
84 | ## 🔄 Version Management
85 |
86 | ### Automatic Publishing
87 |
88 | The pipeline automatically publishes when:
89 | 1. You push to `main` branch
90 | 2. The version in `penpot_mcp/__init__.py` is different from the latest PyPI version
91 |
92 | ### Manual Version Bump
93 |
94 | To trigger a new release:
95 |
96 | ```bash
97 | # 1. Update version in penpot_mcp/__init__.py
98 | echo '__version__ = "0.1.2"' > penpot_mcp/__init__.py
99 |
100 | # 2. Commit and push to main
101 | git add penpot_mcp/__init__.py
102 | git commit -m "Bump version to 0.1.2"
103 | git push origin main
104 |
105 | # 3. Pipeline will automatically:
106 | # - Run tests
107 | # - Build package
108 | # - Publish to PyPI
109 | # - Create GitHub release
110 | ```
111 |
112 | ### Manual Release (Alternative)
113 |
114 | You can also create releases manually:
115 |
116 | ```bash
117 | # 1. Create and push a tag
118 | git tag v0.1.2
119 | git push origin v0.1.2
120 |
121 | # 2. Create release on GitHub UI
122 | # 3. Pipeline will automatically publish to PyPI
123 | ```
124 |
125 | ## 🛠 Advanced Configuration
126 |
127 | ### Environment Variables
128 |
129 | You can customize the pipeline behavior using environment variables:
130 |
131 | ```yaml
132 | env:
133 | SKIP_TESTS: false # Skip tests (not recommended)
134 | SKIP_TESTPYPI: false # Skip Test PyPI upload
135 | CREATE_RELEASE: true # Create GitHub releases
136 | PYTHON_VERSION: "3.12" # Default Python version
137 | ```
138 |
139 | ### Dependency Caching
140 |
141 | The workflows use `uv` for fast dependency management:
142 |
143 | ```yaml
144 | - name: Install dependencies
145 | run: |
146 | uv sync --extra dev # Install with dev dependencies
147 | uv sync --frozen # Use locked dependencies (production)
148 | ```
149 |
150 | ### Security Scanning
151 |
152 | The pipeline includes multiple security checks:
153 |
154 | - **Bandit**: Python security linter
155 | - **Safety**: Dependency vulnerability scanner (can be added)
156 | - **CodeQL**: GitHub's semantic code analysis (can be enabled)
157 |
158 | ### Adding Security Scanning
159 |
160 | To add more security tools:
161 |
162 | ```yaml
163 | - name: Run safety check
164 | run: |
165 | uv add safety
166 | uv run safety check --json --output safety-report.json
167 | ```
168 |
169 | ## 🐛 Troubleshooting
170 |
171 | ### Common Issues
172 |
173 | #### 1. "Version already exists" error
174 | - Check that you bumped the version in `__init__.py`
175 | - Verify the version doesn't exist on PyPI already
176 |
177 | #### 2. PyPI upload fails
178 | - Verify your API tokens are correct
179 | - Check that token has proper scope permissions
180 | - Ensure package name doesn't conflict
181 |
182 | #### 3. Tests fail in CI but pass locally
183 | - Check Python version compatibility
184 | - Verify all dependencies are specified in `pyproject.toml`
185 | - Check for environment-specific issues
186 |
187 | #### 4. GitHub Actions permissions error
188 | - Ensure "Read and write permissions" are enabled
189 | - Check that secrets are properly configured
190 |
191 | ### Debug Commands
192 |
193 | ```bash
194 | # Test build locally
195 | uv build
196 | uv run twine check dist/*
197 |
198 | # Test package installation
199 | python -m pip install dist/*.whl
200 | penpot-mcp --help
201 |
202 | # Check version
203 | python -c "import penpot_mcp; print(penpot_mcp.__version__)"
204 |
205 | # Verify PyPI package
206 | pip index versions penpot-mcp
207 | ```
208 |
209 | ## 📊 Monitoring
210 |
211 | ### GitHub Actions Dashboard
212 | - View workflow runs: `https://github.com/YOUR_ORG/penpot-mcp/actions`
213 | - Monitor success/failure rates
214 | - Check deployment status
215 |
216 | ### PyPI Package Page
217 | - Package stats: `https://pypi.org/project/penpot-mcp/`
218 | - Download statistics
219 | - Version history
220 |
221 | ### Codecov (Optional)
222 | - Code coverage reports
223 | - Coverage trends over time
224 | - Pull request coverage analysis
225 |
226 | ## 🔐 Security Best Practices
227 |
228 | 1. **API Tokens**:
229 | - Use scoped tokens (project-specific when possible)
230 | - Rotate tokens regularly
231 | - Never commit tokens to code
232 |
233 | 2. **Repository Settings**:
234 | - Enable branch protection on `main`
235 | - Require status checks to pass
236 | - Require up-to-date branches
237 |
238 | 3. **Secrets Management**:
239 | - Use GitHub Secrets for sensitive data
240 | - Consider using environment-specific secrets
241 | - Audit secret access regularly
242 |
243 | ## 🎯 Next Steps
244 |
245 | After setup:
246 |
247 | 1. **Test the Pipeline**:
248 | - Create a test PR to verify CI
249 | - Push a version bump to test CD
250 |
251 | 2. **Configure Notifications**:
252 | - Set up Slack/Discord webhooks
253 | - Configure email notifications
254 |
255 | 3. **Add Integrations**:
256 | - CodeQL for security analysis
257 | - Dependabot for dependency updates
258 | - Pre-commit hooks for code quality
259 |
260 | 4. **Documentation**:
261 | - Update README with CI/CD badges
262 | - Document release process
263 | - Create contribution guidelines
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Publish to PyPI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | paths-ignore:
7 | - 'README.md'
8 | - 'CHANGELOG.md'
9 | - 'docs/**'
10 | - '.gitignore'
11 | release:
12 | types: [published]
13 |
14 | jobs:
15 | # Only run if tests pass first
16 | check-tests:
17 | uses: ./.github/workflows/ci.yml
18 |
19 | publish:
20 | needs: check-tests
21 | runs-on: ubuntu-latest
22 | if: github.ref == 'refs/heads/main' && github.event_name == 'push'
23 | permissions:
24 | contents: write # Required for creating releases
25 |
26 | steps:
27 | - uses: actions/checkout@v4
28 | with:
29 | fetch-depth: 0 # Fetch full history for version bump detection
30 |
31 | - name: Set up Python
32 | uses: actions/setup-python@v5
33 | with:
34 | python-version: "3.12"
35 |
36 | - name: Install uv
37 | uses: astral-sh/setup-uv@v6
38 | with:
39 | version: "latest"
40 |
41 | - name: Install dependencies
42 | run: |
43 | uv sync --extra dev
44 |
45 | - name: Check if version was bumped
46 | id: version-check
47 | run: |
48 | # Get current version from __init__.py
49 | CURRENT_VERSION=$(python -c "import penpot_mcp; print(penpot_mcp.__version__)")
50 | echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
51 |
52 | # Check if this version already exists on PyPI using the JSON API
53 | HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/penpot-mcp/$CURRENT_VERSION/json")
54 | if [ "$HTTP_STATUS" = "200" ]; then
55 | echo "version_exists=true" >> $GITHUB_OUTPUT
56 | echo "Version $CURRENT_VERSION already exists on PyPI"
57 | else
58 | echo "version_exists=false" >> $GITHUB_OUTPUT
59 | echo "Version $CURRENT_VERSION is new, will publish"
60 | fi
61 |
62 | - name: Build package
63 | if: steps.version-check.outputs.version_exists == 'false'
64 | run: |
65 | uv build
66 |
67 | - name: Check package quality
68 | if: steps.version-check.outputs.version_exists == 'false'
69 | run: |
70 | # Install twine for checking
71 | uv add twine
72 |
73 | # Check the built package
74 | uv run twine check dist/*
75 |
76 | # Verify package contents
77 | python -m tarfile -l dist/*.tar.gz
78 | python -m zipfile -l dist/*.whl
79 |
80 | - name: Test package installation
81 | if: steps.version-check.outputs.version_exists == 'false'
82 | run: |
83 | # Test installation in a clean environment
84 | python -m pip install dist/*.whl
85 |
86 | # Test basic imports and CLI
87 | python -c "import penpot_mcp; print(f'Successfully imported penpot_mcp v{penpot_mcp.__version__}')"
88 | penpot-mcp --help
89 |
90 | # Uninstall to avoid conflicts
91 | python -m pip uninstall -y penpot-mcp
92 |
93 | - name: Publish to Test PyPI
94 | if: steps.version-check.outputs.version_exists == 'false'
95 | env:
96 | TWINE_USERNAME: __token__
97 | TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
98 | run: |
99 | uv run twine upload --repository testpypi dist/* --verbose
100 | continue-on-error: true # Test PyPI upload can fail, but don't stop main PyPI upload
101 |
102 | - name: Wait for Test PyPI propagation
103 | if: steps.version-check.outputs.version_exists == 'false'
104 | run: |
105 | echo "Waiting 60 seconds for Test PyPI propagation..."
106 | sleep 60
107 |
108 | - name: Test installation from Test PyPI
109 | if: steps.version-check.outputs.version_exists == 'false'
110 | run: |
111 | # Try to install from Test PyPI (may fail due to dependencies)
112 | python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ penpot-mcp==${{ steps.version-check.outputs.current_version }} || echo "Test PyPI installation failed (expected due to dependencies)"
113 | continue-on-error: true
114 |
115 | - name: Publish to PyPI
116 | if: steps.version-check.outputs.version_exists == 'false'
117 | env:
118 | TWINE_USERNAME: __token__
119 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
120 | run: |
121 | uv run twine upload dist/* --verbose
122 |
123 | - name: Create GitHub Release
124 | if: steps.version-check.outputs.version_exists == 'false'
125 | uses: softprops/action-gh-release@v2
126 | with:
127 | tag_name: v${{ steps.version-check.outputs.current_version }}
128 | name: Release v${{ steps.version-check.outputs.current_version }}
129 | body: |
130 | ## Changes in v${{ steps.version-check.outputs.current_version }}
131 |
132 | Auto-generated release for version ${{ steps.version-check.outputs.current_version }}.
133 |
134 | ### Installation
135 | ```bash
136 | pip install penpot-mcp==${{ steps.version-check.outputs.current_version }}
137 | # or
138 | uvx penpot-mcp
139 | ```
140 |
141 | ### What's Changed
142 | See commit history for detailed changes.
143 |
144 | **Full Changelog**: https://github.com/montevive/penpot-mcp/compare/v${{ steps.version-check.outputs.current_version }}...HEAD
145 | files: dist/*
146 | draft: false
147 | prerelease: false
148 |
149 | - name: Notify on success
150 | if: steps.version-check.outputs.version_exists == 'false'
151 | run: |
152 | echo "✅ Successfully published penpot-mcp v${{ steps.version-check.outputs.current_version }} to PyPI!"
153 | echo "📦 Package: https://pypi.org/project/penpot-mcp/${{ steps.version-check.outputs.current_version }}/"
154 | echo "🏷️ Release: https://github.com/montevive/penpot-mcp/releases/tag/v${{ steps.version-check.outputs.current_version }}"
155 |
156 | - name: Skip publishing
157 | if: steps.version-check.outputs.version_exists == 'true'
158 | run: |
159 | echo "⏭️ Skipping publish - version ${{ steps.version-check.outputs.current_version }} already exists on PyPI"
160 |
161 | # Manual release workflow (triggered by GitHub releases)
162 | publish-release:
163 | runs-on: ubuntu-latest
164 | if: github.event_name == 'release' && github.event.action == 'published'
165 |
166 | steps:
167 | - uses: actions/checkout@v4
168 |
169 | - name: Set up Python
170 | uses: actions/setup-python@v5
171 | with:
172 | python-version: "3.12"
173 |
174 | - name: Install uv
175 | uses: astral-sh/setup-uv@v6
176 |
177 | - name: Install dependencies
178 | run: |
179 | uv sync --extra dev
180 |
181 | - name: Update version to match release tag
182 | run: |
183 | RELEASE_VERSION="${{ github.event.release.tag_name }}"
184 | # Remove 'v' prefix if present
185 | VERSION="${RELEASE_VERSION#v}"
186 |
187 | # Update version in __init__.py
188 | sed -i "s/__version__ = \".*\"/__version__ = \"$VERSION\"/" penpot_mcp/__init__.py
189 |
190 | echo "Updated version to: $VERSION"
191 |
192 | - name: Build and publish
193 | env:
194 | TWINE_USERNAME: __token__
195 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
196 | run: |
197 | uv build
198 | uv run twine check dist/*
199 | uv run twine upload dist/* --verbose
```
--------------------------------------------------------------------------------
/penpot_mcp/server/client.py:
--------------------------------------------------------------------------------
```python
1 | """Client for connecting to the Penpot MCP server."""
2 |
3 | import asyncio
4 | from typing import Any, Dict, List, Optional
5 |
6 | from mcp import ClientSession, StdioServerParameters
7 | from mcp.client.stdio import stdio_client
8 |
9 |
10 | class PenpotMCPClient:
11 | """Client for interacting with the Penpot MCP server."""
12 |
13 | def __init__(self, server_command="python", server_args=None, env=None):
14 | """
15 | Initialize the Penpot MCP client.
16 |
17 | Args:
18 | server_command: The command to run the server
19 | server_args: Arguments to pass to the server command
20 | env: Environment variables for the server process
21 | """
22 | self.server_command = server_command
23 | self.server_args = server_args or ["-m", "penpot_mcp.server.mcp_server"]
24 | self.env = env
25 | self.session = None
26 |
27 | async def connect(self):
28 | """
29 | Connect to the MCP server.
30 |
31 | Returns:
32 | The client session
33 | """
34 | # Create server parameters for stdio connection
35 | server_params = StdioServerParameters(
36 | command=self.server_command,
37 | args=self.server_args,
38 | env=self.env,
39 | )
40 |
41 | # Connect to the server
42 | read, write = await stdio_client(server_params).__aenter__()
43 | self.session = await ClientSession(read, write).__aenter__()
44 |
45 | # Initialize the connection
46 | await self.session.initialize()
47 |
48 | return self.session
49 |
50 | async def disconnect(self):
51 | """Disconnect from the server."""
52 | if self.session:
53 | await self.session.__aexit__(None, None, None)
54 | self.session = None
55 |
56 | async def list_resources(self) -> List[Dict[str, Any]]:
57 | """
58 | List available resources from the server.
59 |
60 | Returns:
61 | List of resource information
62 | """
63 | if not self.session:
64 | raise RuntimeError("Not connected to server")
65 |
66 | return await self.session.list_resources()
67 |
68 | async def list_tools(self) -> List[Dict[str, Any]]:
69 | """
70 | List available tools from the server.
71 |
72 | Returns:
73 | List of tool information
74 | """
75 | if not self.session:
76 | raise RuntimeError("Not connected to server")
77 |
78 | return await self.session.list_tools()
79 |
80 | async def get_server_info(self) -> Dict[str, Any]:
81 | """
82 | Get server information.
83 |
84 | Returns:
85 | Server information
86 | """
87 | if not self.session:
88 | raise RuntimeError("Not connected to server")
89 |
90 | info, _ = await self.session.read_resource("server://info")
91 | return info
92 |
93 | async def list_projects(self) -> Dict[str, Any]:
94 | """
95 | List Penpot projects.
96 |
97 | Returns:
98 | Project information
99 | """
100 | if not self.session:
101 | raise RuntimeError("Not connected to server")
102 |
103 | return await self.session.call_tool("list_projects")
104 |
105 | async def get_project(self, project_id: str) -> Dict[str, Any]:
106 | """
107 | Get details for a specific project.
108 |
109 | Args:
110 | project_id: The project ID
111 |
112 | Returns:
113 | Project information
114 | """
115 | if not self.session:
116 | raise RuntimeError("Not connected to server")
117 |
118 | return await self.session.call_tool("get_project", {"project_id": project_id})
119 |
120 | async def get_project_files(self, project_id: str) -> Dict[str, Any]:
121 | """
122 | Get files for a specific project.
123 |
124 | Args:
125 | project_id: The project ID
126 |
127 | Returns:
128 | File information
129 | """
130 | if not self.session:
131 | raise RuntimeError("Not connected to server")
132 |
133 | return await self.session.call_tool("get_project_files", {"project_id": project_id})
134 |
135 | async def get_file(self, file_id: str, features: Optional[List[str]] = None,
136 | project_id: Optional[str] = None) -> Dict[str, Any]:
137 | """
138 | Get details for a specific file.
139 |
140 | Args:
141 | file_id: The file ID
142 | features: List of features to include
143 | project_id: Optional project ID
144 |
145 | Returns:
146 | File information
147 | """
148 | if not self.session:
149 | raise RuntimeError("Not connected to server")
150 |
151 | params = {"file_id": file_id}
152 | if features:
153 | params["features"] = features
154 | if project_id:
155 | params["project_id"] = project_id
156 |
157 | return await self.session.call_tool("get_file", params)
158 |
159 | async def get_components(self) -> Dict[str, Any]:
160 | """
161 | Get components from the server.
162 |
163 | Returns:
164 | Component information
165 | """
166 | if not self.session:
167 | raise RuntimeError("Not connected to server")
168 |
169 | components, _ = await self.session.read_resource("content://components")
170 | return components
171 |
172 | async def export_object(self, file_id: str, page_id: str, object_id: str,
173 | export_type: str = "png", scale: int = 1,
174 | save_to_file: Optional[str] = None) -> Dict[str, Any]:
175 | """
176 | Export an object from a Penpot file.
177 |
178 | Args:
179 | file_id: The ID of the file containing the object
180 | page_id: The ID of the page containing the object
181 | object_id: The ID of the object to export
182 | export_type: Export format (png, svg, pdf)
183 | scale: Scale factor for the export
184 | save_to_file: Optional path to save the exported file
185 |
186 | Returns:
187 | If save_to_file is None: Dictionary with the exported image data
188 | If save_to_file is provided: Dictionary with the saved file path
189 | """
190 | if not self.session:
191 | raise RuntimeError("Not connected to server")
192 |
193 | params = {
194 | "file_id": file_id,
195 | "page_id": page_id,
196 | "object_id": object_id,
197 | "export_type": export_type,
198 | "scale": scale
199 | }
200 |
201 | result = await self.session.call_tool("export_object", params)
202 |
203 | # The result is now directly an Image object which has 'data' and 'format' fields
204 |
205 | # If the client wants to save the file
206 | if save_to_file:
207 | import os
208 |
209 | # Create directory if it doesn't exist
210 | os.makedirs(os.path.dirname(os.path.abspath(save_to_file)), exist_ok=True)
211 |
212 | # Save to file
213 | with open(save_to_file, "wb") as f:
214 | f.write(result["data"])
215 |
216 | return {"file_path": save_to_file, "format": result.get("format")}
217 |
218 | # Otherwise return the result as is
219 | return result
220 |
221 |
222 | async def run_client_example():
223 | """Run a simple example using the client."""
224 | # Create and connect the client
225 | client = PenpotMCPClient()
226 | await client.connect()
227 |
228 | try:
229 | # Get server info
230 | print("Getting server info...")
231 | server_info = await client.get_server_info()
232 | print(f"Server info: {server_info}")
233 |
234 | # List projects
235 | print("\nListing projects...")
236 | projects_result = await client.list_projects()
237 | if "error" in projects_result:
238 | print(f"Error: {projects_result['error']}")
239 | else:
240 | projects = projects_result.get("projects", [])
241 | print(f"Found {len(projects)} projects:")
242 | for project in projects[:5]: # Show first 5 projects
243 | print(f"- {project.get('name', 'Unknown')} (ID: {project.get('id', 'N/A')})")
244 |
245 | # Example of exporting an object (uncomment and update with actual IDs to test)
246 | """
247 | print("\nExporting object...")
248 | # Replace with actual IDs from your Penpot account
249 | export_result = await client.export_object(
250 | file_id="your-file-id",
251 | page_id="your-page-id",
252 | object_id="your-object-id",
253 | export_type="png",
254 | scale=2,
255 | save_to_file="exported_object.png"
256 | )
257 | print(f"Export saved to: {export_result.get('file_path')}")
258 |
259 | # Or get the image data directly without saving
260 | image_data = await client.export_object(
261 | file_id="your-file-id",
262 | page_id="your-page-id",
263 | object_id="your-object-id"
264 | )
265 | print(f"Received image in format: {image_data.get('format')}")
266 | print(f"Image size: {len(image_data.get('data'))} bytes")
267 | """
268 | finally:
269 | # Disconnect from the server
270 | await client.disconnect()
271 |
272 |
273 | def main():
274 | """Run the client example."""
275 | asyncio.run(run_client_example())
276 |
277 |
278 | if __name__ == "__main__":
279 | main()
280 |
```
--------------------------------------------------------------------------------
/lint.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """Script to run linters with auto-fix capabilities.
3 |
4 | Run with: python lint.py [--autofix]
5 | """
6 |
7 | import argparse
8 | import importlib.util
9 | import subprocess
10 | import sys
11 | from pathlib import Path
12 |
13 |
14 | def is_venv():
15 | """Check if running in a virtual environment."""
16 | return (hasattr(sys, 'real_prefix') or
17 | (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))
18 |
19 |
20 | def check_dependencies():
21 | """Check if all required dependencies are installed."""
22 | missing_deps = []
23 |
24 | # Check for required modules
25 | required_modules = ["flake8", "isort", "autopep8", "pyflakes"]
26 |
27 | # In Python 3.12+, also check for pycodestyle as a fallback
28 | if sys.version_info >= (3, 12):
29 | required_modules.append("pycodestyle")
30 |
31 | for module in required_modules:
32 | if importlib.util.find_spec(module) is None:
33 | missing_deps.append(module)
34 |
35 | # Special check for autopep8 compatibility with Python 3.12+
36 | if sys.version_info >= (3, 12) and importlib.util.find_spec("autopep8") is not None:
37 | try:
38 | import autopep8
39 |
40 | # Try to access a function that would use lib2to3
41 | # Will throw an error if lib2to3 is missing and not handled properly
42 | autopep8_version = autopep8.__version__
43 | print(f"Using autopep8 version: {autopep8_version}")
44 | except ImportError as e:
45 | if "lib2to3" in str(e):
46 | print("WARNING: You're using Python 3.12+ where lib2to3 is no longer included.")
47 | print("Your installed version of autopep8 may not work correctly.")
48 | print("Consider using a version of autopep8 compatible with Python 3.12+")
49 | print("or run this script with Python 3.11 or earlier.")
50 |
51 | if missing_deps:
52 | print("ERROR: Missing required dependencies:")
53 | for dep in missing_deps:
54 | print(f" - {dep}")
55 |
56 | if not is_venv():
57 | print("\nYou are using the system Python environment.")
58 | print("It's recommended to use a virtual environment:")
59 | print("\n1. Create a virtual environment:")
60 | print(" python3 -m venv .venv")
61 | print("\n2. Activate the virtual environment:")
62 | print(" source .venv/bin/activate # On Linux/macOS")
63 | print(" .venv\\Scripts\\activate # On Windows")
64 | print("\n3. Install dependencies:")
65 | print(" pip install -r requirements-dev.txt")
66 | else:
67 | print("\nPlease install these dependencies with:")
68 | print(" pip install -r requirements-dev.txt")
69 |
70 | return False
71 |
72 | return True
73 |
74 |
75 | def run_command(cmd, cwd=None):
76 | """Run a shell command and return the exit code."""
77 | try:
78 | process = subprocess.run(cmd, shell=True, cwd=cwd)
79 | return process.returncode
80 | except Exception as e:
81 | print(f"Error executing command '{cmd}': {e}")
82 | return 1
83 |
84 |
85 | def fix_unused_imports(root_dir):
86 | """Fix unused imports using pyflakes and autoflake."""
87 | try:
88 | if importlib.util.find_spec("autoflake") is not None:
89 | print("Running autoflake to remove unused imports...")
90 | cmd = "autoflake --remove-all-unused-imports --recursive --in-place penpot_mcp/ tests/"
91 | return run_command(cmd, cwd=root_dir)
92 | else:
93 | print("autoflake not found. To automatically remove unused imports, install:")
94 | print(" pip install autoflake")
95 | return 0
96 | except Exception as e:
97 | print(f"Error with autoflake: {e}")
98 | return 0
99 |
100 |
101 | def fix_whitespace_and_docstring_issues(root_dir):
102 | """Attempt to fix whitespace and simple docstring issues."""
103 | # Find Python files that need fixing
104 | try:
105 | filelist_cmd = "find penpot_mcp tests setup.py -name '*.py' -type f"
106 | process = subprocess.run(
107 | filelist_cmd, shell=True, cwd=root_dir,
108 | capture_output=True, text=True
109 | )
110 |
111 | if process.returncode != 0:
112 | print("Error finding Python files")
113 | return 1
114 |
115 | files = process.stdout.strip().split('\n')
116 | fixed_count = 0
117 |
118 | for file_path in files:
119 | if not file_path:
120 | continue
121 |
122 | full_path = Path(root_dir) / file_path
123 |
124 | try:
125 | with open(full_path, 'r', encoding='utf-8') as f:
126 | content = f.read()
127 |
128 | # Fix trailing whitespace
129 | fixed_content = '\n'.join(line.rstrip() for line in content.split('\n'))
130 |
131 | # Ensure final newline
132 | if not fixed_content.endswith('\n'):
133 | fixed_content += '\n'
134 |
135 | # Add basic docstrings to empty modules, classes, functions
136 | if '__init__.py' in file_path and '"""' not in fixed_content:
137 | package_name = file_path.split('/')[-2]
138 | fixed_content = f'"""Package {package_name}."""\n' + fixed_content
139 |
140 | # Write back if changes were made
141 | if fixed_content != content:
142 | with open(full_path, 'w', encoding='utf-8') as f:
143 | f.write(fixed_content)
144 | fixed_count += 1
145 |
146 | except Exception as e:
147 | print(f"Error processing {file_path}: {e}")
148 |
149 | if fixed_count > 0:
150 | print(f"Fixed whitespace and newlines in {fixed_count} files")
151 |
152 | return 0
153 | except Exception as e:
154 | print(f"Error in whitespace fixing: {e}")
155 | return 0
156 |
157 |
158 | def main():
159 | """Main entry point for the linter script."""
160 | parser = argparse.ArgumentParser(description="Run linters with optional auto-fix")
161 | parser.add_argument(
162 | "--autofix", "-a", action="store_true", help="Automatically fix linting issues"
163 | )
164 | args = parser.parse_args()
165 |
166 | # Verify dependencies before proceeding
167 | if not check_dependencies():
168 | return 1
169 |
170 | root_dir = Path(__file__).parent.absolute()
171 |
172 | print("Running linters...")
173 |
174 | # Run isort
175 | isort_cmd = "isort --profile black ."
176 | if args.autofix:
177 | print("Running isort with auto-fix...")
178 | exit_code = run_command(isort_cmd, cwd=root_dir)
179 | else:
180 | print("Checking imports with isort...")
181 | exit_code = run_command(f"{isort_cmd} --check", cwd=root_dir)
182 |
183 | if exit_code != 0 and not args.autofix:
184 | print("isort found issues. Run with --autofix to fix automatically.")
185 |
186 | # Run additional fixers when in autofix mode
187 | if args.autofix:
188 | # Fix unused imports
189 | fix_unused_imports(root_dir)
190 |
191 | # Fix whitespace and newline issues
192 | fix_whitespace_and_docstring_issues(root_dir)
193 |
194 | # Run autopep8
195 | print("Running autopep8 with auto-fix...")
196 |
197 | if sys.version_info >= (3, 12):
198 | print("Detected Python 3.12+. Using compatible code formatting approach...")
199 | # Use a more compatible approach for Python 3.12+
200 | # First try autopep8 (newer versions may have fixed lib2to3 dependency)
201 | autopep8_cmd = "autopep8 --recursive --aggressive --aggressive --in-place --select E,W penpot_mcp/ tests/ setup.py"
202 | try:
203 | exit_code = run_command(autopep8_cmd, cwd=root_dir)
204 | if exit_code != 0:
205 | print("Warning: autopep8 encountered issues. Some files may not have been fixed.")
206 | except Exception as e:
207 | if "lib2to3" in str(e):
208 | print("Error with autopep8 due to missing lib2to3 module in Python 3.12+")
209 | print("Using pycodestyle for checking only (no auto-fix is possible)")
210 | exit_code = run_command("pycodestyle penpot_mcp/ tests/", cwd=root_dir)
211 | else:
212 | raise
213 | else:
214 | # Normal execution for Python < 3.12
215 | autopep8_cmd = "autopep8 --recursive --aggressive --aggressive --in-place --select E,W penpot_mcp/ tests/ setup.py"
216 | exit_code = run_command(autopep8_cmd, cwd=root_dir)
217 | if exit_code != 0:
218 | print("Warning: autopep8 encountered issues. Some files may not have been fixed.")
219 |
220 | # Run flake8 (check only, no auto-fix)
221 | print("Running flake8...")
222 | flake8_cmd = "flake8 --exclude=.venv,venv,__pycache__,.git,build,dist,*.egg-info,node_modules"
223 | flake8_result = run_command(flake8_cmd, cwd=root_dir)
224 |
225 | if flake8_result != 0:
226 | print("flake8 found issues that need to be fixed manually.")
227 | print("Common issues and how to fix them:")
228 | print("- F401 (unused import): Remove the import or use it")
229 | print("- D1XX (missing docstring): Add a docstring to the module/class/function")
230 | print("- E501 (line too long): Break the line or use line continuation")
231 | print("- F841 (unused variable): Remove or use the variable")
232 |
233 | if args.autofix:
234 | print("Auto-fix completed! Run flake8 again to see if there are any remaining issues.")
235 | elif exit_code != 0 or flake8_result != 0:
236 | print("Linting issues found. Run with --autofix to fix automatically where possible.")
237 | return 1
238 | else:
239 | print("All linting checks passed!")
240 |
241 | return 0
242 |
243 |
244 | if __name__ == "__main__":
245 | sys.exit(main())
246 |
```
--------------------------------------------------------------------------------
/penpot_mcp/resources/penpot-tree-schema.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "type": "object",
4 | "required": ["colors", "typographies", "pages", "components", "id", "tokensLib", "pagesIndex"],
5 | "properties": {
6 | "colors": {
7 | "type": "object",
8 | "patternProperties": {
9 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
10 | "type": "object",
11 | "required": ["path", "color", "name", "modifiedAt", "opacity", "id"],
12 | "properties": {
13 | "path": {"type": "string"},
14 | "color": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"},
15 | "name": {"type": "string"},
16 | "modifiedAt": {"type": "string", "format": "date-time"},
17 | "opacity": {"type": "number", "minimum": 0, "maximum": 1},
18 | "id": {"type": "string", "format": "uuid"}
19 | }
20 | }
21 | }
22 | },
23 | "typographies": {
24 | "type": "object",
25 | "patternProperties": {
26 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
27 | "type": "object",
28 | "required": ["lineHeight", "path", "fontStyle", "textTransform", "fontId", "fontSize", "fontWeight", "name", "modifiedAt", "fontVariantId", "id", "letterSpacing", "fontFamily"],
29 | "properties": {
30 | "lineHeight": {"type": "string"},
31 | "path": {"type": "string"},
32 | "fontStyle": {"type": "string", "enum": ["normal"]},
33 | "textTransform": {"type": "string", "enum": ["uppercase", "none"]},
34 | "fontId": {"type": "string"},
35 | "fontSize": {"type": "string"},
36 | "fontWeight": {"type": "string"},
37 | "name": {"type": "string"},
38 | "modifiedAt": {"type": "string", "format": "date-time"},
39 | "fontVariantId": {"type": "string"},
40 | "id": {"type": "string", "format": "uuid"},
41 | "letterSpacing": {"type": "string"},
42 | "fontFamily": {"type": "string"}
43 | }
44 | }
45 | }
46 | },
47 | "components": {
48 | "type": "object",
49 | "patternProperties": {
50 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
51 | "type": "object",
52 | "required": ["id", "name", "path", "modifiedAt", "mainInstanceId", "mainInstancePage"],
53 | "properties": {
54 | "id": {"type": "string", "format": "uuid"},
55 | "name": {"type": "string"},
56 | "path": {"type": "string"},
57 | "modifiedAt": {"type": "string", "format": "date-time"},
58 | "mainInstanceId": {"type": "string", "format": "uuid"},
59 | "mainInstancePage": {"type": "string", "format": "uuid"},
60 | "annotation": {"type": "string"}
61 | }
62 | }
63 | }
64 | },
65 | "id": {"type": "string", "format": "uuid"},
66 | "tokensLib": {
67 | "type": "object",
68 | "required": ["sets", "themes", "activeThemes"],
69 | "properties": {
70 | "sets": {
71 | "type": "object",
72 | "patternProperties": {
73 | "^S-[a-z]+$": {
74 | "type": "object",
75 | "required": ["name", "description", "modifiedAt", "tokens"],
76 | "properties": {
77 | "name": {"type": "string"},
78 | "description": {"type": "string"},
79 | "modifiedAt": {"type": "string", "format": "date-time"},
80 | "tokens": {
81 | "type": "object",
82 | "patternProperties": {
83 | "^[a-z][a-z0-9.-]*$": {
84 | "type": "object",
85 | "required": ["name", "type", "value", "description", "modifiedAt"],
86 | "properties": {
87 | "name": {"type": "string"},
88 | "type": {"type": "string", "enum": ["dimensions", "sizing", "color", "border-radius", "spacing", "stroke-width", "rotation", "opacity"]},
89 | "value": {"type": "string"},
90 | "description": {"type": "string"},
91 | "modifiedAt": {"type": "string", "format": "date-time"}
92 | }
93 | }
94 | }
95 | }
96 | }
97 | }
98 | }
99 | },
100 | "themes": {
101 | "type": "object",
102 | "patternProperties": {
103 | ".*": {
104 | "type": "object",
105 | "patternProperties": {
106 | ".*": {
107 | "type": "object",
108 | "required": ["name", "group", "description", "isSource", "id", "modifiedAt", "sets"],
109 | "properties": {
110 | "name": {"type": "string"},
111 | "group": {"type": "string"},
112 | "description": {"type": "string"},
113 | "isSource": {"type": "boolean"},
114 | "id": {"type": "string", "format": "uuid"},
115 | "modifiedAt": {"type": "string", "format": "date-time"},
116 | "sets": {"type": "array", "items": {"type": "string"}}
117 | }
118 | }
119 | }
120 | }
121 | }
122 | },
123 | "activeThemes": {
124 | "type": "array",
125 | "items": {"type": "string"}
126 | }
127 | }
128 | },
129 | "options": {
130 | "type": "object",
131 | "properties": {
132 | "componentsV2": {"type": "boolean"}
133 | }
134 | },
135 | "objects": {
136 | "type": "object",
137 | "patternProperties": {
138 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
139 | "type": "object",
140 | "required": ["options", "objects", "id", "name"],
141 | "properties": {
142 | "options": {"type": "object"},
143 | "objects": {
144 | "type": "object",
145 | "patternProperties": {
146 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
147 | "type": "object",
148 | "required": ["id", "name", "type"],
149 | "properties": {
150 | "id": {"type": "string", "format": "uuid"},
151 | "name": {"type": "string"},
152 | "type": {"type": "string", "enum": ["frame", "rect", "text"]},
153 | "x": {"type": "number"},
154 | "y": {"type": "number"},
155 | "width": {"type": "number"},
156 | "height": {"type": "number"},
157 | "rotation": {"type": "number"},
158 | "selrect": {
159 | "type": "object",
160 | "properties": {
161 | "x": {"type": "number"},
162 | "y": {"type": "number"},
163 | "width": {"type": "number"},
164 | "height": {"type": "number"},
165 | "x1": {"type": "number"},
166 | "y1": {"type": "number"},
167 | "x2": {"type": "number"},
168 | "y2": {"type": "number"}
169 | }
170 | },
171 | "points": {
172 | "type": "array",
173 | "items": {
174 | "type": "object",
175 | "properties": {
176 | "x": {"type": "number"},
177 | "y": {"type": "number"}
178 | }
179 | }
180 | },
181 | "transform": {
182 | "type": "object",
183 | "properties": {
184 | "a": {"type": "number"},
185 | "b": {"type": "number"},
186 | "c": {"type": "number"},
187 | "d": {"type": "number"},
188 | "e": {"type": "number"},
189 | "f": {"type": "number"}
190 | }
191 | },
192 | "transformInverse": {
193 | "type": "object",
194 | "properties": {
195 | "a": {"type": "number"},
196 | "b": {"type": "number"},
197 | "c": {"type": "number"},
198 | "d": {"type": "number"},
199 | "e": {"type": "number"},
200 | "f": {"type": "number"}
201 | }
202 | },
203 | "parentId": {"type": "string", "format": "uuid"},
204 | "frameId": {"type": "string", "format": "uuid"},
205 | "flipX": {"type": ["null", "boolean"]},
206 | "flipY": {"type": ["null", "boolean"]},
207 | "hideFillOnExport": {"type": "boolean"},
208 | "growType": {"type": "string", "enum": ["fixed", "auto-height"]},
209 | "hideInViewer": {"type": "boolean"},
210 | "r1": {"type": "number"},
211 | "r2": {"type": "number"},
212 | "r3": {"type": "number"},
213 | "r4": {"type": "number"},
214 | "proportion": {"type": "number"},
215 | "proportionLock": {"type": "boolean"},
216 | "componentRoot": {"type": "boolean"},
217 | "componentId": {"type": "string", "format": "uuid"},
218 | "mainInstance": {"type": "boolean"},
219 | "componentFile": {"type": "string", "format": "uuid"},
220 | "strokes": {
221 | "type": "array",
222 | "items": {
223 | "type": "object",
224 | "properties": {
225 | "strokeStyle": {"type": "string"},
226 | "strokeAlignment": {"type": "string"},
227 | "strokeWidth": {"type": "number"},
228 | "strokeColor": {"type": "string"},
229 | "strokeOpacity": {"type": "number"}
230 | }
231 | }
232 | },
233 | "fills": {
234 | "type": "array",
235 | "items": {
236 | "type": "object",
237 | "properties": {
238 | "fillColor": {"type": "string"},
239 | "fillOpacity": {"type": "number"},
240 | "fillImage": {
241 | "type": "object",
242 | "properties": {
243 | "name": {"type": "string"},
244 | "width": {"type": "number"},
245 | "height": {"type": "number"},
246 | "mtype": {"type": "string"},
247 | "id": {"type": "string", "format": "uuid"},
248 | "keepAspectRatio": {"type": "boolean"}
249 | }
250 | }
251 | }
252 | }
253 | },
254 | "shapes": {
255 | "type": "array",
256 | "items": {"type": "string", "format": "uuid"}
257 | },
258 | "content": {
259 | "type": "object",
260 | "properties": {
261 | "type": {"type": "string"},
262 | "children": {"type": "array"}
263 | }
264 | },
265 | "appliedTokens": {"type": "object"},
266 | "positionData": {"type": "array"},
267 | "layoutItemMarginType": {"type": "string"},
268 | "constraintsV": {"type": "string"},
269 | "constraintsH": {"type": "string"},
270 | "layoutItemMargin": {"type": "object"},
271 | "layoutGapType": {"type": "string"},
272 | "layoutPadding": {"type": "object"},
273 | "layoutWrapType": {"type": "string"},
274 | "layout": {"type": "string"},
275 | "layoutAlignItems": {"type": "string"},
276 | "layoutPaddingType": {"type": "string"},
277 | "layoutItemHSizing": {"type": "string"},
278 | "layoutGap": {"type": "object"},
279 | "layoutItemVSizing": {"type": "string"},
280 | "layoutJustifyContent": {"type": "string"},
281 | "layoutFlexDir": {"type": "string"},
282 | "layoutAlignContent": {"type": "string"},
283 | "shapeRef": {"type": "string", "format": "uuid"}
284 | }
285 | }
286 | }
287 | },
288 | "id": {"type": "string", "format": "uuid"},
289 | "name": {"type": "string"}
290 | }
291 | }
292 | }
293 | }
294 | }
295 | }
```
--------------------------------------------------------------------------------
/penpot_mcp/resources/penpot-schema.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "type": "object",
4 | "required": ["colors", "typographies", "pages", "components", "id", "tokensLib", "pagesIndex"],
5 | "properties": {
6 | "colors": {
7 | "type": "object",
8 | "patternProperties": {
9 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
10 | "type": "object",
11 | "required": ["path", "color", "name", "modifiedAt", "opacity", "id"],
12 | "properties": {
13 | "path": {"type": "string"},
14 | "color": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"},
15 | "name": {"type": "string"},
16 | "modifiedAt": {"type": "string", "format": "date-time"},
17 | "opacity": {"type": "number", "minimum": 0, "maximum": 1},
18 | "id": {"type": "string", "format": "uuid"}
19 | }
20 | }
21 | }
22 | },
23 | "typographies": {
24 | "type": "object",
25 | "patternProperties": {
26 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
27 | "type": "object",
28 | "required": ["lineHeight", "path", "fontStyle", "textTransform", "fontId", "fontSize", "fontWeight", "name", "modifiedAt", "fontVariantId", "id", "letterSpacing", "fontFamily"],
29 | "properties": {
30 | "lineHeight": {"type": "string"},
31 | "path": {"type": "string"},
32 | "fontStyle": {"type": "string", "enum": ["normal"]},
33 | "textTransform": {"type": "string", "enum": ["uppercase", "none"]},
34 | "fontId": {"type": "string"},
35 | "fontSize": {"type": "string"},
36 | "fontWeight": {"type": "string"},
37 | "name": {"type": "string"},
38 | "modifiedAt": {"type": "string", "format": "date-time"},
39 | "fontVariantId": {"type": "string"},
40 | "id": {"type": "string", "format": "uuid"},
41 | "letterSpacing": {"type": "string"},
42 | "fontFamily": {"type": "string"}
43 | }
44 | }
45 | }
46 | },
47 | "pages": {
48 | "type": "array",
49 | "items": {"type": "string", "format": "uuid"}
50 | },
51 | "components": {
52 | "type": "object",
53 | "patternProperties": {
54 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
55 | "type": "object",
56 | "required": ["id", "name", "path", "modifiedAt", "mainInstanceId", "mainInstancePage"],
57 | "properties": {
58 | "id": {"type": "string", "format": "uuid"},
59 | "name": {"type": "string"},
60 | "path": {"type": "string"},
61 | "modifiedAt": {"type": "string", "format": "date-time"},
62 | "mainInstanceId": {"type": "string", "format": "uuid"},
63 | "mainInstancePage": {"type": "string", "format": "uuid"},
64 | "annotation": {"type": "string"}
65 | }
66 | }
67 | }
68 | },
69 | "id": {"type": "string", "format": "uuid"},
70 | "tokensLib": {
71 | "type": "object",
72 | "required": ["sets", "themes", "activeThemes"],
73 | "properties": {
74 | "sets": {
75 | "type": "object",
76 | "patternProperties": {
77 | "^S-[a-z]+$": {
78 | "type": "object",
79 | "required": ["name", "description", "modifiedAt", "tokens"],
80 | "properties": {
81 | "name": {"type": "string"},
82 | "description": {"type": "string"},
83 | "modifiedAt": {"type": "string", "format": "date-time"},
84 | "tokens": {
85 | "type": "object",
86 | "patternProperties": {
87 | "^[a-z][a-z0-9.-]*$": {
88 | "type": "object",
89 | "required": ["name", "type", "value", "description", "modifiedAt"],
90 | "properties": {
91 | "name": {"type": "string"},
92 | "type": {"type": "string", "enum": ["dimensions", "sizing", "color", "border-radius", "spacing", "stroke-width", "rotation", "opacity"]},
93 | "value": {"type": "string"},
94 | "description": {"type": "string"},
95 | "modifiedAt": {"type": "string", "format": "date-time"}
96 | }
97 | }
98 | }
99 | }
100 | }
101 | }
102 | }
103 | },
104 | "themes": {
105 | "type": "object",
106 | "patternProperties": {
107 | ".*": {
108 | "type": "object",
109 | "patternProperties": {
110 | ".*": {
111 | "type": "object",
112 | "required": ["name", "group", "description", "isSource", "id", "modifiedAt", "sets"],
113 | "properties": {
114 | "name": {"type": "string"},
115 | "group": {"type": "string"},
116 | "description": {"type": "string"},
117 | "isSource": {"type": "boolean"},
118 | "id": {"type": "string", "format": "uuid"},
119 | "modifiedAt": {"type": "string", "format": "date-time"},
120 | "sets": {"type": "array", "items": {"type": "string"}}
121 | }
122 | }
123 | }
124 | }
125 | }
126 | },
127 | "activeThemes": {
128 | "type": "array",
129 | "items": {"type": "string"}
130 | }
131 | }
132 | },
133 | "options": {
134 | "type": "object",
135 | "properties": {
136 | "componentsV2": {"type": "boolean"}
137 | }
138 | },
139 | "pagesIndex": {
140 | "type": "object",
141 | "patternProperties": {
142 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
143 | "type": "object",
144 | "required": ["options", "objects", "id", "name"],
145 | "properties": {
146 | "options": {"type": "object"},
147 | "objects": {
148 | "type": "object",
149 | "patternProperties": {
150 | "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$": {
151 | "type": "object",
152 | "required": ["id", "name", "type"],
153 | "properties": {
154 | "id": {"type": "string", "format": "uuid"},
155 | "name": {"type": "string"},
156 | "type": {"type": "string", "enum": ["frame", "rect", "text"]},
157 | "x": {"type": "number"},
158 | "y": {"type": "number"},
159 | "width": {"type": "number"},
160 | "height": {"type": "number"},
161 | "rotation": {"type": "number"},
162 | "selrect": {
163 | "type": "object",
164 | "properties": {
165 | "x": {"type": "number"},
166 | "y": {"type": "number"},
167 | "width": {"type": "number"},
168 | "height": {"type": "number"},
169 | "x1": {"type": "number"},
170 | "y1": {"type": "number"},
171 | "x2": {"type": "number"},
172 | "y2": {"type": "number"}
173 | }
174 | },
175 | "points": {
176 | "type": "array",
177 | "items": {
178 | "type": "object",
179 | "properties": {
180 | "x": {"type": "number"},
181 | "y": {"type": "number"}
182 | }
183 | }
184 | },
185 | "transform": {
186 | "type": "object",
187 | "properties": {
188 | "a": {"type": "number"},
189 | "b": {"type": "number"},
190 | "c": {"type": "number"},
191 | "d": {"type": "number"},
192 | "e": {"type": "number"},
193 | "f": {"type": "number"}
194 | }
195 | },
196 | "transformInverse": {
197 | "type": "object",
198 | "properties": {
199 | "a": {"type": "number"},
200 | "b": {"type": "number"},
201 | "c": {"type": "number"},
202 | "d": {"type": "number"},
203 | "e": {"type": "number"},
204 | "f": {"type": "number"}
205 | }
206 | },
207 | "parentId": {"type": "string", "format": "uuid"},
208 | "frameId": {"type": "string", "format": "uuid"},
209 | "flipX": {"type": ["null", "boolean"]},
210 | "flipY": {"type": ["null", "boolean"]},
211 | "hideFillOnExport": {"type": "boolean"},
212 | "growType": {"type": "string", "enum": ["fixed", "auto-height"]},
213 | "hideInViewer": {"type": "boolean"},
214 | "r1": {"type": "number"},
215 | "r2": {"type": "number"},
216 | "r3": {"type": "number"},
217 | "r4": {"type": "number"},
218 | "proportion": {"type": "number"},
219 | "proportionLock": {"type": "boolean"},
220 | "componentRoot": {"type": "boolean"},
221 | "componentId": {"type": "string", "format": "uuid"},
222 | "mainInstance": {"type": "boolean"},
223 | "componentFile": {"type": "string", "format": "uuid"},
224 | "strokes": {
225 | "type": "array",
226 | "items": {
227 | "type": "object",
228 | "properties": {
229 | "strokeStyle": {"type": "string"},
230 | "strokeAlignment": {"type": "string"},
231 | "strokeWidth": {"type": "number"},
232 | "strokeColor": {"type": "string"},
233 | "strokeOpacity": {"type": "number"}
234 | }
235 | }
236 | },
237 | "fills": {
238 | "type": "array",
239 | "items": {
240 | "type": "object",
241 | "properties": {
242 | "fillColor": {"type": "string"},
243 | "fillOpacity": {"type": "number"},
244 | "fillImage": {
245 | "type": "object",
246 | "properties": {
247 | "name": {"type": "string"},
248 | "width": {"type": "number"},
249 | "height": {"type": "number"},
250 | "mtype": {"type": "string"},
251 | "id": {"type": "string", "format": "uuid"},
252 | "keepAspectRatio": {"type": "boolean"}
253 | }
254 | }
255 | }
256 | }
257 | },
258 | "shapes": {
259 | "type": "array",
260 | "items": {"type": "string", "format": "uuid"}
261 | },
262 | "content": {
263 | "type": "object",
264 | "properties": {
265 | "type": {"type": "string"},
266 | "children": {"type": "array"}
267 | }
268 | },
269 | "appliedTokens": {"type": "object"},
270 | "positionData": {"type": "array"},
271 | "layoutItemMarginType": {"type": "string"},
272 | "constraintsV": {"type": "string"},
273 | "constraintsH": {"type": "string"},
274 | "layoutItemMargin": {"type": "object"},
275 | "layoutGapType": {"type": "string"},
276 | "layoutPadding": {"type": "object"},
277 | "layoutWrapType": {"type": "string"},
278 | "layout": {"type": "string"},
279 | "layoutAlignItems": {"type": "string"},
280 | "layoutPaddingType": {"type": "string"},
281 | "layoutItemHSizing": {"type": "string"},
282 | "layoutGap": {"type": "object"},
283 | "layoutItemVSizing": {"type": "string"},
284 | "layoutJustifyContent": {"type": "string"},
285 | "layoutFlexDir": {"type": "string"},
286 | "layoutAlignContent": {"type": "string"},
287 | "shapeRef": {"type": "string", "format": "uuid"}
288 | }
289 | }
290 | }
291 | },
292 | "id": {"type": "string", "format": "uuid"},
293 | "name": {"type": "string"}
294 | }
295 | }
296 | }
297 | }
298 | }
299 | }
```