Add a `client-setup` command to the published `@penpot/mcp` package.
When invoked as `penpot-mcp client-setup`, the bin delegates to the `add-mcp`
CLI against the local MCP server URL (from `PENPOT_MCP_SERVER_PORT`, default
4401), run via npx.
Update docs on MCP client configuration.
Recommend calling `add-mcp` directly for now, since the `client-setup` command
only becomes available once a new `@penpot/mcp` release is published to npm.
AI-assisted-by: Claude
Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
pnpm occasionally detects an incompatible node_modules directory (e.g.
after a store location or pnpm major version change) and interactively
asks whether to remove and recreate it, blocking the MCP bootstrap in
the devenv tmux pane. Set confirmModulesPurge: false in
mcp/pnpm-workspace.yaml so the purge is auto-confirmed; this file is
included in the npm pack tarball (unlike .npmrc) and applies to all
install invocations from a single place.
AI-assisted-by: claude-fable-5
For the npm MCP package, adjust the launcher to specifically prepare
the runtime directory: The package contents are copied once per version
to a runtime directory owned by the launcher.
The npm-owned package directory cannot be used. While installing there
worked for older npm/npx version, we have to assume it is read-only.
Fixes#9947
Read PENPOT_TENANT env var (defaulting to "default") and embed it in
Redis Pub/Sub channel names as penpot.mcp.<tenant>.task.{req,res}.<id>.
This prevents cross-tenant interference when multiple environments share
a Redis instance, matching the backend convention
(e.g. penpot.rlimit.<tenant>.window.<name> in app.rpc.rlimit).
Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>
Add the repository license SPDX identifier to the published @penpot/mcp package metadata.
Signed-off-by: martrend33 <145278472+martrend33@users.noreply.github.com>
* 📎 Ignore .iml files (IntelliJ module files)
* 🎉 Enable multi-instance horizontal scaling for MCP server
Allow the MCP server to run as multiple instances behind a plain
round-robin load balancer, removing the previous requirement that a
user's plugin WebSocket and MCP client connection terminate on the same
instance. Behaviour is unchanged when run as a single instance or
without Redis.
Cross-instance MCP sessions: when a request arrives with an
mcp-session-id that was initialised on another instance, the session is
adopted locally instead of rejected. The user token is read from the
query parameter (present on every request, as the configured endpoint
URL is never rewritten), so no shared session store is needed; the
transport is pre-initialised so the SDK's validateSession() accepts it.
Cross-instance task routing: when a Redis URI is configured in
multi-user mode, plugin task requests are routed via Redis pub/sub keyed
by user token. The instance holding a plugin's WebSocket subscribes to
that token's request channel; any instance handling a tool call
publishes the request and awaits the response on a per-request channel.
RedisBridge is a pure transport for the existing serialised
PluginTaskRequest/Response objects. PluginTask is split into an abstract
base plus a local (promise-backed) PluginTask and a RemotePluginTask
whose resolve/reject publish the outcome back over Redis, so the
existing local dispatch and response-correlation paths are reused
unchanged on the executing instance.
Refs #10000
Memories use a system of progressive disclosure:
Starting from a root memory, memories reference other memories using explicit
references.
The new system of hierarchical memories replaces AGENTS.md files.
GitHub #9215
Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
Co-authored-by: Codex <codex@openai.com>
* ✨ Bound the size of plugin task responses
When using the integrated remote MCP server, bound response size.
All responses are passed to LLMs, which themselves impose bounds.
This is a measure to bound memory usage in the centrally provided
MCP server.
GitHub #9493
* ✨ Bound parallelism in ExportShapeTool
Use an integer semaphore to bound parallel requests to this
memory-intensive tool, thus bounding memory usage.
GitHub #9493
* ✨ Add (manual) integration test script for ExportShapeTool parallelism
Add dependency tsx to facilitate executions.
GitHub #9493
* ✨ Make number of parallel export requests configurable in ExportShapeTool
Use env var PENPOT_MCP_EXPORT_SHAPE_MAX_PARALLEL_REQUESTS to configure
the maximum number of requests in multi-user mode (default 0, no limit).
Adds a new MCP tool (devenv-only) that imports .penpot files into the
running Penpot instance. The tool downloads the file from a given URL,
stages it in the frontend's static directory, and triggers the import
via the ClojureScript REPL using the frontend's web worker infrastructure.
The temporary file is cleaned up after the import completes or fails.
Registered alongside CljsReplTool, sharing the same NreplClient instance.
Github #9217
Co-authored-by: Claude <noreply@anthropic.com>
New tool to evaluate ClojureScript expressions by connecting to the
nREPL service already provided in devenv.
Add dependency 'nrepl-client' and a corresponding client class
as well as types to support this.
Add a new environment variable for 'devenv mode', which enables
the new tool (PENPOT_MCP_DEVENV).
* ✨ Improve MCP server logging
Log only fingerprints of user tokens
* ✨ Add Loki transport support to MCP server logger
Loki logging is enabled iff PENPOT_LOGGERS_LOKI_URI is non-empty.
File logging is now enabled iff PENPOT_MCP_LOG_DIR is set to a non-empty value
(previously defaulted to the "logs" directory when unset).
GitHub #9415
* ✨ Improve MCP server logging
Log only fingerprints of user tokens
* ✨ Add Loki transport support to MCP server logger
Loki logging is enabled iff PENPOT_LOGGERS_LOKI_URI is non-empty.
File logging is now enabled iff PENPOT_MCP_LOG_DIR is set to a non-empty value
(previously defaulted to the "logs" directory when unset).
GitHub #9415
SSE sessions were never included in the periodic inactivity timeout
checker, so a stale connection whose TCP close event never fired would
retain its SSEServerTransport and McpServer indefinitely.
Changes:
- Add lastActiveTime: number to the sseTransports entry type
- Initialise lastActiveTime at SSE session creation (GET /sse)
- Refresh lastActiveTime on every incoming message (POST /messages)
- Extend startSessionTimeoutChecker() to sweep and forcibly close SSE
sessions idle for more than SESSION_TIMEOUT_MINUTES, mirroring the
existing Streamable HTTP logic
- Update the checker log to count both transport maps
The existing res.on('close') cleanup path is preserved unchanged:
it remains the primary cleanup for normal disconnections; the timer
is a safety net for zombie sessions only.
Closes#9432
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>