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.
Files changed (41) hide show
  1. penpot_api_mcp-0.1.1/.crackerjack/adapter_learning.dhara +0 -0
  2. penpot_api_mcp-0.1.1/.crackerjack/oneiric_cache/workflow_checkpoints.sqlite +0 -0
  3. penpot_api_mcp-0.1.1/.crackerjack/uv/cache/.gitignore +1 -0
  4. penpot_api_mcp-0.1.1/.crackerjack/uv/cache/.lock +0 -0
  5. penpot_api_mcp-0.1.1/.crackerjack/uv/cache/CACHEDIR.TAG +1 -0
  6. penpot_api_mcp-0.1.1/.crackerjack/uv/cache/interpreter-v4/7590ff3983a44196/3b61eef48c418b3e.msgpack +0 -0
  7. penpot_api_mcp-0.1.1/.crackerjack/uv/cache/sdists-v9/.git +0 -0
  8. penpot_api_mcp-0.1.1/.crackerjack/uv/cache/sdists-v9/.gitignore +0 -0
  9. penpot_api_mcp-0.1.1/.envrc +1 -0
  10. penpot_api_mcp-0.1.1/.gitignore +18 -0
  11. penpot_api_mcp-0.1.1/.mcp.json +18 -0
  12. penpot_api_mcp-0.1.1/AGENTS.md +42 -0
  13. penpot_api_mcp-0.1.1/CHANGELOG.md +16 -0
  14. penpot_api_mcp-0.1.1/CLAUDE.md +79 -0
  15. penpot_api_mcp-0.1.1/LICENSE +28 -0
  16. penpot_api_mcp-0.1.1/PKG-INFO +126 -0
  17. penpot_api_mcp-0.1.1/README.md +102 -0
  18. penpot_api_mcp-0.1.1/penpot_api_mcp/__init__.py +5 -0
  19. penpot_api_mcp-0.1.1/penpot_api_mcp/__main__.py +94 -0
  20. penpot_api_mcp-0.1.1/penpot_api_mcp/clients/__init__.py +5 -0
  21. penpot_api_mcp-0.1.1/penpot_api_mcp/clients/base_client.py +61 -0
  22. penpot_api_mcp-0.1.1/penpot_api_mcp/clients/penpot_client.py +222 -0
  23. penpot_api_mcp-0.1.1/penpot_api_mcp/config/__init__.py +5 -0
  24. penpot_api_mcp-0.1.1/penpot_api_mcp/config/settings.py +73 -0
  25. penpot_api_mcp-0.1.1/penpot_api_mcp/models/__init__.py +14 -0
  26. penpot_api_mcp-0.1.1/penpot_api_mcp/models/file.py +24 -0
  27. penpot_api_mcp-0.1.1/penpot_api_mcp/models/object.py +32 -0
  28. penpot_api_mcp-0.1.1/penpot_api_mcp/models/project.py +22 -0
  29. penpot_api_mcp-0.1.1/penpot_api_mcp/server.py +65 -0
  30. penpot_api_mcp-0.1.1/penpot_api_mcp/tools/__init__.py +17 -0
  31. penpot_api_mcp-0.1.1/penpot_api_mcp/tools/files.py +23 -0
  32. penpot_api_mcp-0.1.1/penpot_api_mcp/tools/objects.py +51 -0
  33. penpot_api_mcp-0.1.1/penpot_api_mcp/tools/projects.py +18 -0
  34. penpot_api_mcp-0.1.1/penpot_api_mcp/utils/__init__.py +5 -0
  35. penpot_api_mcp-0.1.1/penpot_api_mcp/utils/transit.py +50 -0
  36. penpot_api_mcp-0.1.1/pyproject.toml +65 -0
  37. penpot_api_mcp-0.1.1/settings/penpot_api_mcp.yaml +18 -0
  38. penpot_api_mcp-0.1.1/tests/__init__.py +0 -0
  39. penpot_api_mcp-0.1.1/tests/test_client.py +357 -0
  40. penpot_api_mcp-0.1.1/tests/test_models.py +48 -0
  41. penpot_api_mcp-0.1.1/tests/test_transit.py +54 -0
File without changes
@@ -0,0 +1 @@
1
+ Signature: 8a477f597d28d172789f06886806bc55
@@ -0,0 +1 @@
1
+ layout uv
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .venv/
6
+ .env
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ htmlcov/
11
+ .coverage
12
+ coverage.xml
13
+ *.log
14
+ *.err
15
+ .mypy_cache/
16
+ .ruff_cache/
17
+ .pytest_cache/
18
+ uv.lock
@@ -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,5 @@
1
+ """penpot-api-mcp — MCP server wrapping the Penpot REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.1.0"
@@ -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,5 @@
1
+ from __future__ import annotations
2
+
3
+ from penpot_api_mcp.clients.penpot_client import PenpotClient
4
+
5
+ __all__ = ["PenpotClient"]
@@ -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()