#
tokens: 47501/50000 43/48 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
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 | [![Penpot MCP Demo](https://img.youtube.com/vi/vOMEh-ONN1k/0.jpg)](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 |   }
```
Page 1/2FirstPrevNextLast