penpot-api-mcp 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- penpot_api_mcp-0.1.1/.crackerjack/adapter_learning.dhara +0 -0
- penpot_api_mcp-0.1.1/.crackerjack/oneiric_cache/workflow_checkpoints.sqlite +0 -0
- penpot_api_mcp-0.1.1/.crackerjack/uv/cache/.gitignore +1 -0
- penpot_api_mcp-0.1.1/.crackerjack/uv/cache/.lock +0 -0
- penpot_api_mcp-0.1.1/.crackerjack/uv/cache/CACHEDIR.TAG +1 -0
- penpot_api_mcp-0.1.1/.crackerjack/uv/cache/interpreter-v4/7590ff3983a44196/3b61eef48c418b3e.msgpack +0 -0
- penpot_api_mcp-0.1.1/.crackerjack/uv/cache/sdists-v9/.git +0 -0
- penpot_api_mcp-0.1.1/.crackerjack/uv/cache/sdists-v9/.gitignore +0 -0
- penpot_api_mcp-0.1.1/.envrc +1 -0
- penpot_api_mcp-0.1.1/.gitignore +18 -0
- penpot_api_mcp-0.1.1/.mcp.json +18 -0
- penpot_api_mcp-0.1.1/AGENTS.md +42 -0
- penpot_api_mcp-0.1.1/CHANGELOG.md +16 -0
- penpot_api_mcp-0.1.1/CLAUDE.md +79 -0
- penpot_api_mcp-0.1.1/LICENSE +28 -0
- penpot_api_mcp-0.1.1/PKG-INFO +126 -0
- penpot_api_mcp-0.1.1/README.md +102 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/__init__.py +5 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/__main__.py +94 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/clients/__init__.py +5 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/clients/base_client.py +61 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/clients/penpot_client.py +222 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/config/__init__.py +5 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/config/settings.py +73 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/models/__init__.py +14 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/models/file.py +24 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/models/object.py +32 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/models/project.py +22 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/server.py +65 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/tools/__init__.py +17 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/tools/files.py +23 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/tools/objects.py +51 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/tools/projects.py +18 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/utils/__init__.py +5 -0
- penpot_api_mcp-0.1.1/penpot_api_mcp/utils/transit.py +50 -0
- penpot_api_mcp-0.1.1/pyproject.toml +65 -0
- penpot_api_mcp-0.1.1/settings/penpot_api_mcp.yaml +18 -0
- penpot_api_mcp-0.1.1/tests/__init__.py +0 -0
- penpot_api_mcp-0.1.1/tests/test_client.py +357 -0
- penpot_api_mcp-0.1.1/tests/test_models.py +48 -0
- penpot_api_mcp-0.1.1/tests/test_transit.py +54 -0
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Signature: 8a477f597d28d172789f06886806bc55
|
penpot_api_mcp-0.1.1/.crackerjack/uv/cache/interpreter-v4/7590ff3983a44196/3b61eef48c418b3e.msgpack
ADDED
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
layout uv
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"crackerjack": {
|
|
4
|
+
"env": {
|
|
5
|
+
"BASH_DEFAULT_TIMEOUT_MS": "300000",
|
|
6
|
+
"BASH_MAX_TIMEOUT_MS": "1800000",
|
|
7
|
+
"MCP_TIMEOUT": "60000",
|
|
8
|
+
"MCP_TOOL_TIMEOUT": "1800000"
|
|
9
|
+
},
|
|
10
|
+
"type": "http",
|
|
11
|
+
"url": "http://localhost:8676/mcp"
|
|
12
|
+
},
|
|
13
|
+
"session-buddy": {
|
|
14
|
+
"type": "http",
|
|
15
|
+
"url": "http://localhost:8678/mcp"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Bootstrap reference for AI agents and tools working with `penpot-api-mcp`.
|
|
4
|
+
|
|
5
|
+
## What This Is
|
|
6
|
+
|
|
7
|
+
MCP server (Python 3.13, FastMCP, port 3051) wrapping the Penpot REST API. Headless — no browser required. Complements the official TypeScript `@penpot/mcp` (which needs a browser plugin for live canvas manipulation).
|
|
8
|
+
|
|
9
|
+
## Quick Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync # Install deps
|
|
13
|
+
uv run pytest # Tests
|
|
14
|
+
uv run crackerjack # Quality gates
|
|
15
|
+
uv run python -m penpot_api_mcp start --force # Start HTTP server
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Key Files
|
|
19
|
+
|
|
20
|
+
| File | Purpose |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `penpot_api_mcp/utils/transit.py` | Transit+JSON encode/decode — Penpot's wire format |
|
|
23
|
+
| `penpot_api_mcp/clients/penpot_client.py` | Core API client with auth and RPC |
|
|
24
|
+
| `penpot_api_mcp/config/settings.py` | `PENPOT_*` env var configuration |
|
|
25
|
+
| `penpot_api_mcp/tools/` | FastMCP tool registrations (6 tools) |
|
|
26
|
+
| `settings/penpot_api_mcp.yaml` | YAML config (Oneiric-compatible) |
|
|
27
|
+
| `~/Library/LaunchAgents/com.mcp.penpot-api.plist` | launchctl service definition |
|
|
28
|
+
|
|
29
|
+
## MCP Tools
|
|
30
|
+
|
|
31
|
+
`list_projects` · `get_project_files` · `get_file` · `get_object_tree` · `search_objects` · `export_object`
|
|
32
|
+
|
|
33
|
+
## Auth
|
|
34
|
+
|
|
35
|
+
Set `PENPOT_ACCESS_TOKEN` (preferred) or `PENPOT_EMAIL` + `PENPOT_PASSWORD`. Never use password-auth session cookie values as Bearer tokens.
|
|
36
|
+
|
|
37
|
+
## Critical Invariants
|
|
38
|
+
|
|
39
|
+
1. All `_rpc()` calls use `transit=True` — Penpot speaks Transit+JSON
|
|
40
|
+
1. `encode()` / `decode()` are the only correct way to build/parse Penpot payloads
|
|
41
|
+
1. After password login, session is cookie-jar only — `self._api_token` stays empty
|
|
42
|
+
1. FastMCP lifespan via constructor `lifespan=` param, not `_mcp_server.lifespan` patch
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
## [0.1.1] - 2026-05-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial implementation of penpot-api-mcp
|
|
13
|
+
|
|
14
|
+
### Internal
|
|
15
|
+
|
|
16
|
+
- Add LICENSE, README, CLAUDE.md, AGENTS.md, .gitignore
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Guidance for Claude Code working with `penpot-api-mcp`. Start with `AGENTS.md` for a shorter bootstrap.
|
|
4
|
+
|
|
5
|
+
## Ecosystem Context
|
|
6
|
+
|
|
7
|
+
Part of the **Bodai Ecosystem**:
|
|
8
|
+
|
|
9
|
+
| Component | Role | Port |
|
|
10
|
+
|-----------|------|------|
|
|
11
|
+
| Mahavishnu | Orchestrator | 8680 |
|
|
12
|
+
| Akosha | Seer | 8682 |
|
|
13
|
+
| Dhara | Curator | 8683 |
|
|
14
|
+
| Session-Buddy | Builder | 8678 |
|
|
15
|
+
| Crackerjack | Inspector | 8676 |
|
|
16
|
+
| **penpot-api-mcp** | Penpot REST API | 3051 |
|
|
17
|
+
|
|
18
|
+
## Project Overview
|
|
19
|
+
|
|
20
|
+
FastMCP MCP server wrapping the Penpot REST API. Provides headless (no-browser) read and export access for design automation workflows. Complements the official `@penpot/mcp` (TypeScript, browser plugin required).
|
|
21
|
+
|
|
22
|
+
**Tech stack:** Python 3.13, FastMCP, httpx, Pydantic, mcp-common, Oneiric
|
|
23
|
+
|
|
24
|
+
## Common Commands
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv sync # Install/sync all dependencies
|
|
28
|
+
uv run python -m penpot_api_mcp # Run (stdio mode)
|
|
29
|
+
uv run python -m penpot_api_mcp start --force # Run (HTTP mode)
|
|
30
|
+
|
|
31
|
+
uv run pytest # Run tests
|
|
32
|
+
uv run crackerjack # Full quality suite
|
|
33
|
+
uv run ruff check --fix # Lint
|
|
34
|
+
uv run mypy . # Type check
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Architecture
|
|
38
|
+
|
|
39
|
+
### Key Design Decisions
|
|
40
|
+
|
|
41
|
+
**Transit+JSON**: Penpot's RPC API uses Transit+JSON (Clojure serialization). All encoding/decoding is centralized in `penpot_api_mcp/utils/transit.py`. Never manually construct `~:key` or `~uUUID` payloads elsewhere — use `encode()` / `decode()`.
|
|
42
|
+
|
|
43
|
+
**Auth separation**: Two auth paths must stay separate:
|
|
44
|
+
|
|
45
|
+
- API token (`PENPOT_ACCESS_TOKEN`): stored in `self._api_token`, sent as `Authorization: Token` header
|
|
46
|
+
- Password auth: session cookie only, stored in the httpx cookie jar, never converted to a Bearer token
|
|
47
|
+
|
|
48
|
+
**Consistent transit**: All `_rpc()` calls use `transit=True` (default). The `transit=False` path exists only for edge cases where the server doesn't speak Transit. Do not add new `transit=False` calls without verifying the server response format.
|
|
49
|
+
|
|
50
|
+
**FastMCP lifespan**: Use the `lifespan=` constructor parameter — never monkey-patch `app._mcp_server.lifespan`.
|
|
51
|
+
|
|
52
|
+
### Module Layout
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
penpot_api_mcp/
|
|
56
|
+
├── utils/transit.py # encode() / decode() for Transit+JSON
|
|
57
|
+
├── config/settings.py # PenpotSettings (PENPOT_* env vars)
|
|
58
|
+
├── clients/
|
|
59
|
+
│ ├── base_client.py # httpx.AsyncClient lifecycle + cookie persistence
|
|
60
|
+
│ └── penpot_client.py # Typed Penpot RPC wrapper
|
|
61
|
+
├── models/ # Pydantic: Project, File, Object, ObjectTree
|
|
62
|
+
├── tools/ # FastMCP @app.tool() registrations
|
|
63
|
+
├── server.py # create_app() factory
|
|
64
|
+
└── __main__.py # Oneiric MCPServerCLIFactory entrypoint
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Coding Standards
|
|
68
|
+
|
|
69
|
+
- Python 3.13+, strict typing, 88-char lines (Ruff)
|
|
70
|
+
- `from __future__ import annotations` in every module
|
|
71
|
+
- Pydantic models for all API response shapes
|
|
72
|
+
- `httpx` (async) — never `requests`
|
|
73
|
+
- Tests in `tests/`, mirrors package layout
|
|
74
|
+
|
|
75
|
+
## Security
|
|
76
|
+
|
|
77
|
+
- Credentials only via environment variables — never hardcoded
|
|
78
|
+
- Path: `PENPOT_ACCESS_TOKEN` > `PENPOT_EMAIL` + `PENPOT_PASSWORD`
|
|
79
|
+
- No secrets in logs — use structured logging with redaction
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Les Leslie
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: penpot-api-mcp
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: MCP server wrapping the Penpot REST API for headless design automation
|
|
5
|
+
Author-email: Les Leslie <les@wedgwoodwebworks.com>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: automation,design,mcp,model-context-protocol,penpot
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Requires-Dist: fastmcp>=2.12.0
|
|
16
|
+
Requires-Dist: httpx>=0.27.0
|
|
17
|
+
Requires-Dist: mcp-common>=0.15.0
|
|
18
|
+
Requires-Dist: oneiric>=0.3.6
|
|
19
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
20
|
+
Requires-Dist: pydantic>=2.12.0
|
|
21
|
+
Requires-Dist: structlog>=25.0.0
|
|
22
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# penpot-api-mcp
|
|
26
|
+
|
|
27
|
+
MCP server wrapping the [Penpot](https://penpot.app) REST API for headless design automation. Provides read, search, and export access to Penpot projects, files, and design objects — without requiring a browser session.
|
|
28
|
+
|
|
29
|
+
Part of the **Bodai Ecosystem** alongside Mahavishnu, Akosha, Dhara, Session-Buddy, and Crackerjack.
|
|
30
|
+
|
|
31
|
+
## Why this exists
|
|
32
|
+
|
|
33
|
+
The official `@penpot/mcp` (TypeScript) requires a live browser plugin to operate — it is the right tool for interactive canvas manipulation. This server targets the complementary use case: background automation, asset export pipelines, and AI-driven design queries that run without a browser.
|
|
34
|
+
|
|
35
|
+
## Tools
|
|
36
|
+
|
|
37
|
+
| Tool | Description |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `list_projects` | List all projects for the authenticated user |
|
|
40
|
+
| `get_project_files` | List design files in a project |
|
|
41
|
+
| `get_file` | Fetch the full content of a design file |
|
|
42
|
+
| `get_object_tree` | Return the design object hierarchy for a file |
|
|
43
|
+
| `search_objects` | Search objects by name or type |
|
|
44
|
+
| `export_object` | Export a design object as PNG/SVG (base64-encoded) |
|
|
45
|
+
|
|
46
|
+
## Setup
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv sync
|
|
50
|
+
cp .env.example .env # fill in credentials
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
Environment variables (prefix `PENPOT_`):
|
|
56
|
+
|
|
57
|
+
| Variable | Description | Default |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `PENPOT_ACCESS_TOKEN` | API access token (preferred) | — |
|
|
60
|
+
| `PENPOT_EMAIL` | Email for password auth (fallback) | — |
|
|
61
|
+
| `PENPOT_PASSWORD` | Password for password auth (fallback) | — |
|
|
62
|
+
| `PENPOT_BASE_URL` | API base URL for self-hosted instances | `https://design.penpot.app/api` |
|
|
63
|
+
|
|
64
|
+
Either `PENPOT_ACCESS_TOKEN` or `PENPOT_EMAIL` + `PENPOT_PASSWORD` must be set.
|
|
65
|
+
|
|
66
|
+
## Running
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# HTTP mode (default — Claude Code compatible)
|
|
70
|
+
uv run python -m penpot_api_mcp start --force
|
|
71
|
+
|
|
72
|
+
# stdio mode
|
|
73
|
+
uv run python -m penpot_api_mcp
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Server listens on `http://localhost:3051/mcp`.
|
|
77
|
+
|
|
78
|
+
## MCP configuration
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"penpot-api": {
|
|
84
|
+
"type": "http",
|
|
85
|
+
"url": "http://localhost:3051/mcp"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv run pytest # Run tests
|
|
95
|
+
uv run crackerjack # Full quality suite (ruff + mypy + pytest + bandit)
|
|
96
|
+
uv run ruff check --fix # Lint
|
|
97
|
+
uv run mypy . # Type check
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Architecture
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
penpot_api_mcp/
|
|
104
|
+
├── utils/transit.py # Transit+JSON encode/decode (Penpot's wire format)
|
|
105
|
+
├── config/settings.py # Pydantic settings (PENPOT_* env vars)
|
|
106
|
+
├── clients/ # httpx async client with dual auth
|
|
107
|
+
├── models/ # Pydantic models: Project, File, Object, ObjectTree
|
|
108
|
+
├── tools/ # FastMCP tool registrations
|
|
109
|
+
├── server.py # FastMCP app + health endpoints
|
|
110
|
+
└── __main__.py # MCPServerCLIFactory entrypoint (Oneiric)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Transit+JSON
|
|
114
|
+
|
|
115
|
+
Penpot's RPC layer uses [Transit+JSON](https://github.com/cognitect/transit-format) — a Clojure serialization format where map keys are `~:keyword` and UUIDs are `~uUUID`. The `utils/transit.py` module handles encode/decode at the API boundary, keeping all Python models clean.
|
|
116
|
+
|
|
117
|
+
### Authentication
|
|
118
|
+
|
|
119
|
+
Two modes are supported:
|
|
120
|
+
|
|
121
|
+
- **API token** (`PENPOT_ACCESS_TOKEN`): sent as `Authorization: Token <token>` header
|
|
122
|
+
- **Email + password**: authenticates via `/rpc/command/login-with-password`, then relies on the httpx cookie jar (`auth-token` cookie) for all subsequent requests
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
BSD 3-Clause. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# penpot-api-mcp
|
|
2
|
+
|
|
3
|
+
MCP server wrapping the [Penpot](https://penpot.app) REST API for headless design automation. Provides read, search, and export access to Penpot projects, files, and design objects — without requiring a browser session.
|
|
4
|
+
|
|
5
|
+
Part of the **Bodai Ecosystem** alongside Mahavishnu, Akosha, Dhara, Session-Buddy, and Crackerjack.
|
|
6
|
+
|
|
7
|
+
## Why this exists
|
|
8
|
+
|
|
9
|
+
The official `@penpot/mcp` (TypeScript) requires a live browser plugin to operate — it is the right tool for interactive canvas manipulation. This server targets the complementary use case: background automation, asset export pipelines, and AI-driven design queries that run without a browser.
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
| Tool | Description |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `list_projects` | List all projects for the authenticated user |
|
|
16
|
+
| `get_project_files` | List design files in a project |
|
|
17
|
+
| `get_file` | Fetch the full content of a design file |
|
|
18
|
+
| `get_object_tree` | Return the design object hierarchy for a file |
|
|
19
|
+
| `search_objects` | Search objects by name or type |
|
|
20
|
+
| `export_object` | Export a design object as PNG/SVG (base64-encoded) |
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv sync
|
|
26
|
+
cp .env.example .env # fill in credentials
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
Environment variables (prefix `PENPOT_`):
|
|
32
|
+
|
|
33
|
+
| Variable | Description | Default |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `PENPOT_ACCESS_TOKEN` | API access token (preferred) | — |
|
|
36
|
+
| `PENPOT_EMAIL` | Email for password auth (fallback) | — |
|
|
37
|
+
| `PENPOT_PASSWORD` | Password for password auth (fallback) | — |
|
|
38
|
+
| `PENPOT_BASE_URL` | API base URL for self-hosted instances | `https://design.penpot.app/api` |
|
|
39
|
+
|
|
40
|
+
Either `PENPOT_ACCESS_TOKEN` or `PENPOT_EMAIL` + `PENPOT_PASSWORD` must be set.
|
|
41
|
+
|
|
42
|
+
## Running
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# HTTP mode (default — Claude Code compatible)
|
|
46
|
+
uv run python -m penpot_api_mcp start --force
|
|
47
|
+
|
|
48
|
+
# stdio mode
|
|
49
|
+
uv run python -m penpot_api_mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Server listens on `http://localhost:3051/mcp`.
|
|
53
|
+
|
|
54
|
+
## MCP configuration
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"penpot-api": {
|
|
60
|
+
"type": "http",
|
|
61
|
+
"url": "http://localhost:3051/mcp"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Development
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
uv run pytest # Run tests
|
|
71
|
+
uv run crackerjack # Full quality suite (ruff + mypy + pytest + bandit)
|
|
72
|
+
uv run ruff check --fix # Lint
|
|
73
|
+
uv run mypy . # Type check
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Architecture
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
penpot_api_mcp/
|
|
80
|
+
├── utils/transit.py # Transit+JSON encode/decode (Penpot's wire format)
|
|
81
|
+
├── config/settings.py # Pydantic settings (PENPOT_* env vars)
|
|
82
|
+
├── clients/ # httpx async client with dual auth
|
|
83
|
+
├── models/ # Pydantic models: Project, File, Object, ObjectTree
|
|
84
|
+
├── tools/ # FastMCP tool registrations
|
|
85
|
+
├── server.py # FastMCP app + health endpoints
|
|
86
|
+
└── __main__.py # MCPServerCLIFactory entrypoint (Oneiric)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Transit+JSON
|
|
90
|
+
|
|
91
|
+
Penpot's RPC layer uses [Transit+JSON](https://github.com/cognitect/transit-format) — a Clojure serialization format where map keys are `~:keyword` and UUIDs are `~uUUID`. The `utils/transit.py` module handles encode/decode at the API boundary, keeping all Python models clean.
|
|
92
|
+
|
|
93
|
+
### Authentication
|
|
94
|
+
|
|
95
|
+
Two modes are supported:
|
|
96
|
+
|
|
97
|
+
- **API token** (`PENPOT_ACCESS_TOKEN`): sent as `Authorization: Token <token>` header
|
|
98
|
+
- **Email + password**: authenticates via `/rpc/command/login-with-password`, then relies on the httpx cookie jar (`auth-token` cookie) for all subsequent requests
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
BSD 3-Clause. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Oneiric CLI entry point for penpot-api-mcp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from mcp_common.cli import MCPServerCLIFactory
|
|
8
|
+
from mcp_common.server import BaseOneiricServerMixin, create_runtime_components
|
|
9
|
+
from oneiric.core.config import OneiricMCPConfig
|
|
10
|
+
from oneiric.runtime.mcp_health import HealthStatus
|
|
11
|
+
|
|
12
|
+
from penpot_api_mcp.config import get_settings
|
|
13
|
+
from penpot_api_mcp.server import create_app
|
|
14
|
+
|
|
15
|
+
OneiricMCPConfigType = (
|
|
16
|
+
OneiricMCPConfig if not TYPE_CHECKING else "TypedOneiricMCPConfig"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PenpotApiConfig(OneiricMCPConfig): # type: ignore[misc]
|
|
21
|
+
http_port: int = 3051
|
|
22
|
+
http_host: str = "127.0.0.1"
|
|
23
|
+
enable_http_transport: bool = True
|
|
24
|
+
|
|
25
|
+
class Config:
|
|
26
|
+
env_prefix = "PENPOT_MCP_"
|
|
27
|
+
env_file = ".env"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PenpotApiMCPServer(BaseOneiricServerMixin):
|
|
31
|
+
def __init__(self, config: PenpotApiConfig) -> None:
|
|
32
|
+
self.config = config # type: ignore[assignment]
|
|
33
|
+
self.app = create_app()
|
|
34
|
+
self.runtime = create_runtime_components(
|
|
35
|
+
server_name="penpot-api-mcp",
|
|
36
|
+
cache_dir=config.cache_dir or ".oneiric_cache",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def snapshot_manager(self) -> object:
|
|
41
|
+
return self.runtime.snapshot_manager
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def cache_manager(self) -> object:
|
|
45
|
+
return self.runtime.cache_manager
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def health_monitor(self) -> object:
|
|
49
|
+
return self.runtime.health_monitor
|
|
50
|
+
|
|
51
|
+
async def startup(self) -> None:
|
|
52
|
+
_ = get_settings()
|
|
53
|
+
await self.runtime.initialize()
|
|
54
|
+
await self._create_startup_snapshot(
|
|
55
|
+
custom_components={"penpot-api": {"status": "initialized"}}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def shutdown(self) -> None:
|
|
59
|
+
await self._create_shutdown_snapshot()
|
|
60
|
+
await self.runtime.cleanup()
|
|
61
|
+
|
|
62
|
+
async def health_check(self) -> object:
|
|
63
|
+
base = await self._build_health_components()
|
|
64
|
+
settings = get_settings()
|
|
65
|
+
base.append(
|
|
66
|
+
self.runtime.health_monitor.create_component_health(
|
|
67
|
+
name="penpot-api",
|
|
68
|
+
status=HealthStatus.HEALTHY
|
|
69
|
+
if settings.is_configured
|
|
70
|
+
else HealthStatus.UNHEALTHY,
|
|
71
|
+
details={
|
|
72
|
+
"configured": settings.is_configured,
|
|
73
|
+
"auth_method": "token" if settings.has_token_auth else "password",
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
return self.runtime.health_monitor.create_health_response(base)
|
|
78
|
+
|
|
79
|
+
def get_app(self) -> object:
|
|
80
|
+
return self.app.http_app
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main() -> None:
|
|
84
|
+
cli_factory = MCPServerCLIFactory.create_server_cli(
|
|
85
|
+
server_class=PenpotApiMCPServer,
|
|
86
|
+
config_class=PenpotApiConfig,
|
|
87
|
+
name="penpot-api-mcp",
|
|
88
|
+
_description="Penpot API MCP Server — headless design automation via REST API",
|
|
89
|
+
)
|
|
90
|
+
cli_factory.create_app()()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
main()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Base async HTTP client with httpx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from penpot_api_mcp.config.settings import PenpotSettings
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_TRANSIT_HEADERS = {
|
|
15
|
+
"Content-Type": "application/transit+json",
|
|
16
|
+
"Accept": "application/transit+json",
|
|
17
|
+
}
|
|
18
|
+
_JSON_HEADERS = {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"Accept": "application/json",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseHTTPClient:
|
|
25
|
+
def __init__(self, settings: PenpotSettings) -> None:
|
|
26
|
+
self._settings = settings
|
|
27
|
+
self._base_url = str(settings.base_url).rstrip("/")
|
|
28
|
+
self._client: httpx.AsyncClient | None = None
|
|
29
|
+
|
|
30
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
31
|
+
if self._client is None or self._client.is_closed:
|
|
32
|
+
# Carry over any cookies from a closed client so session auth
|
|
33
|
+
# survives a transport error + reconnect.
|
|
34
|
+
prior_cookies = dict(self._client.cookies) if self._client else {}
|
|
35
|
+
self._client = httpx.AsyncClient(
|
|
36
|
+
base_url=self._base_url,
|
|
37
|
+
timeout=self._settings.request_timeout,
|
|
38
|
+
limits=httpx.Limits(max_connections=self._settings.max_connections),
|
|
39
|
+
cookies=prior_cookies,
|
|
40
|
+
)
|
|
41
|
+
return self._client
|
|
42
|
+
|
|
43
|
+
async def close(self) -> None:
|
|
44
|
+
if self._client and not self._client.is_closed:
|
|
45
|
+
await self._client.aclose()
|
|
46
|
+
|
|
47
|
+
async def _post(
|
|
48
|
+
self,
|
|
49
|
+
path: str,
|
|
50
|
+
body: dict[str, Any],
|
|
51
|
+
*,
|
|
52
|
+
transit: bool = True,
|
|
53
|
+
extra_headers: dict[str, str] | None = None,
|
|
54
|
+
) -> Any:
|
|
55
|
+
client = await self._get_client()
|
|
56
|
+
headers = dict(_TRANSIT_HEADERS if transit else _JSON_HEADERS)
|
|
57
|
+
if extra_headers:
|
|
58
|
+
headers.update(extra_headers)
|
|
59
|
+
response = await client.post(path, json=body, headers=headers)
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
return response.json()
|