From 51c0129485b7c4cce9662f746768966a54ec9b07 Mon Sep 17 00:00:00 2001 From: "Dr. Dominik Jain" Date: Mon, 20 Jul 2026 13:08:51 +0200 Subject: [PATCH] :sparkles: Simplify MCP client setup, adding client-setup command (#10604) 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 --- docs/mcp/index.md | 105 ++++++++-------------------------------- mcp/README.md | 68 +++++++++++--------------- mcp/bin/client-setup.js | 70 +++++++++++++++++++++++++++ mcp/bin/mcp-local.js | 43 +++++++++++----- mcp/package.json | 1 + 5 files changed, 151 insertions(+), 136 deletions(-) create mode 100644 mcp/bin/client-setup.js diff --git a/docs/mcp/index.md b/docs/mcp/index.md index 4d2fa76fc0..8509bb7574 100644 --- a/docs/mcp/index.md +++ b/docs/mcp/index.md @@ -129,17 +129,18 @@ If you just want to try Penpot AI workflows quickly through the MCP, follow this ![MCP Server in Penpot Integrations, copy server url](/img/mcp/mcp-server-url.webp) 4. #### Add the server to your MCP client - In your MCP-aware IDE/agent (Cursor, Claude Code, etc.), add a new server pointing to that URL. - **Example (generic JSON config):** - ```json - { - "mcpServers": { - "penpot": { - "url": "https:///mcp/stream?userToken=YOUR_MCP_KEY" - } - } - } + We recommend using the [add-mcp](https://github.com/neon-solutions/add-mcp) project for connecting your MCP client to the Penpot MCP server. + With `npx` available, call + + ```shell + npx -y add-mcp -g -n penpot ``` + + and follow the interactive setup. Alternatively, follow your client's instructions for connecting + to a remote MCP server. For Claude Desktop, we recommend adding the Penpot MCP Server as a + custom [connector](http://claude.ai/customize/connectors). + See the section **Connect your MCP client** for more details on how to connect. + 5. #### Open a Penpot file and connect MCP In Penpot, open a design file and use **File → MCP Server → Connect** to connect the plugin to your current file. @@ -216,84 +217,21 @@ You can use Penpot MCP server in two main ways: ## Connect your MCP client -Use the same client setup flow for both modes. What changes is the server URL and authentication method. - -### Connection values by mode - * **Remote MCP** - * URL: `https:///mcp/stream?userToken=YOUR_MCP_KEY` - * Auth: MCP key in `userToken` + * URL (copy from your Penpot account overview): `https:///mcp/stream?userToken=YOUR_MCP_KEY` + * Configure your MCP client with `npx -y add-mcp -g -n penpot ` or by following your client's instructions for connecting to a remote MCP server. + For Claude Desktop, the server can be added as a custom [connector](http://claude.ai/customize/connectors). * **Local MCP** - * URL: `http://localhost:4401/mcp` - * Auth: none (uses your active Penpot browser session) + * Configure your MCP client with `npx -y add-mcp -g -n penpot http://localhost:4401/mcp` (adjust the port if you changed `PENPOT_MCP_SERVER_PORT`) or by following your client's instructions for connecting to an HTTP MCP server. -### Cursor - -1. Open Cursor MCP/tool configuration. -2. Add a Penpot MCP server entry: +Note: For clients that do not support HTTP servers directly (like Claude Desktop), the local MCP server can be connected via [mcp-remote](https://github.com/geelen/mcp-remote) as follows: ```json { "mcpServers": { "penpot": { - "url": "REMOTE_OR_LOCAL_URL", - "type": "http" - } - } -} -``` - -Replace `REMOTE_OR_LOCAL_URL` with the URL for your mode. - -### Claude Code - -1. Open MCP configuration in Claude Code. -2. Add a Penpot server with `http` transport and the URL for your mode. -3. Restart Claude Code or reload tools. - -```json -{ - "mcpServers": { - "penpot": { - "transport": "http", - "url": "REMOTE_OR_LOCAL_URL" - } - } -} -``` - - -### VS Code / Copilot - -1. Open external MCP server configuration in your extension/settings. -2. Add Penpot with the URL for your mode. -3. Save and reload tools. - -```json -{ - "mcp.servers": { - "penpot": { - "transport": "http", - "url": "REMOTE_OR_LOCAL_URL" - } - } -} -``` - -### Codex / OpenCode etc - -1. Use your client's "Add MCP server" flow. -2. Set the URL for your mode. -3. Reload tools and verify Penpot tools are available. - -```json -{ - "servers": { - "penpot": { - "url": "REMOTE_OR_LOCAL_URL", - "transport": { - "type": "http" - } + "command": "npx", + "args": ["-y", "mcp-remote", "", "--allow-http"] } } } @@ -326,9 +264,8 @@ Remote MCP is the easiest way to start using AI agents with Penpot. It's hosted ### Connect -For client-specific setup, use the shared section **Connect your MCP client**. - For remote mode, use the URL shown in **Your account → Integrations → MCP Server**, which includes your `userToken`. +See the section **Connect your MCP client** for details on how to connect. ### Setup videos @@ -464,9 +401,7 @@ For advanced or repository-based workflows, see the [MCP README](https://github. ### Connect -For client-specific setup, use the shared section **Connect your MCP client**. - -For local mode, use `http://localhost:4401/mcp` with HTTP transport (no MCP key; authentication uses your active Penpot browser session). +See the section **Connect your MCP client** for details on how to connect. ### Use diff --git a/mcp/README.md b/mcp/README.md index bc6b2205cb..ac99679f28 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -59,14 +59,10 @@ The project requires [Node.js](https://nodejs.org/) (tested with v22.x). The easiest way to launch the servers is to use `npx` to run the appropriate version that matches your Penpot version. -* If you are using the latest Penpot release, e.g. as served on [design.penpot.app](https://design.penpot.app), run: - ```shell - npx -y @penpot/mcp@latest - ``` -* If you are participating in the MCP beta-test, which uses [test-mcp.penpot.dev](https://test-mcp.penpot.dev), run: - ```shell - npx -y @penpot/mcp@beta - ``` +If you are using the latest Penpot release, e.g. as served on [design.penpot.app](https://design.penpot.app), run: +```shell +npx -y @penpot/mcp@latest +``` Once the servers are running, continue with step 2. @@ -78,24 +74,13 @@ On Windows, use the Git Bash terminal to ensure compatibility with the provided ##### Clone the Appropriate Branch of the Repository -> [!IMPORTANT] -> The branches are subject to change in the future. -> Be sure to check the instructions for the latest information on which branch to use. +Clone the Penpot repository, using the proper branch/tag depending on the +version of Penpot you want to use the MCP server with. +For instance, to target the latest development version, use the `develop` branch: -Clone the Penpot repository, using the proper branch depending on the -version of Penpot you want to use the MCP server with. - - * For the current Penpot release 2.14, use the `mcp-prod-2.14.1` branch: - - ```shell - git clone https://github.com/penpot/penpot.git --branch mcp-prod-2.14.1 --depth 1 - ``` - - * For the MCP beta-test, use the `staging` branch: - - ```shell - git clone https://github.com/penpot/penpot.git --branch staging --depth 1 - ``` +```shell +git clone https://github.com/penpot/penpot.git --branch develop --depth 1 +``` Then change into the `mcp` directory: @@ -180,16 +165,27 @@ By default, the server runs on port 4401 and provides: - **Modern Streamable HTTP endpoint**: `http://localhost:4401/mcp` - **Legacy SSE endpoint**: `http://localhost:4401/sse` -These endpoints can be used directly by MCP clients that support them. +You can change the port by setting the `PENPOT_MCP_SERVER_PORT` environment variable +before starting the server. These endpoints can be used directly by MCP clients that support them. Simply configure the client to connect the MCP server by providing the respective URL. -When using a client that only supports stdio transport, -a proxy like `mcp-remote` is required. +#### Configuring your client + +You can configure your client with the [add-mcp](https://github.com/neon-solutions/add-mcp) helper. +Simply call + + npx -y add-mcp -g -n penpot http://localhost:4401/mcp + +and follow the interactive dialogue to configure the clients of your choice. +The config entry name is `penpot` (override it with `-n `) and the URL points to the local http +endpoint (adjust the port if you changed `PENPOT_MCP_SERVER_PORT`). + +When using a client that only supports stdio transport like **Claude Desktop**, +a proxy like [mcp-remote](https://github.com/geelen/mcp-remote) is required. +More information on connecting your client follows below. #### Using a Proxy for stdio Transport -NOTE: only relevant if you are executing this outside of devenv - The `mcp-remote` package can proxy stdio transport to HTTP/SSE, allowing clients that support only stdio to connect to the MCP server indirectly. Use it to provide the launch command for your MCP client as follows: @@ -232,12 +228,6 @@ After updating the configuration file, restart Claude Desktop completely for the After the restart, you should see the MCP server listed when clicking on the "Search and tools" icon at the bottom of the prompt input area. -#### Example: Claude Code - -To add the Penpot MCP server to a Claude Code project, issue the command - - claude mcp add penpot -t http http://localhost:4401/mcp - ## Repository Structure This repository is a monorepo containing four main components: @@ -296,9 +286,9 @@ The Penpot MCP server can be configured using environment variables. ## Beyond Local Execution The above instructions describe how to run the MCP server and plugin server locally. -We are working on enabling remote deployments of the MCP server, particularly -in [multi-user mode](docs/multi-user-mode.md), where multiple Penpot users will -be able to connect to the same MCP server instance. + +The Penpot MCP server can also support multiple remote users simultaneously +in [multi-user mode](docs/multi-user-mode.md). To run the server remotely (even for a single user), you may set the following environment variables to configure the two servers diff --git a/mcp/bin/client-setup.js b/mcp/bin/client-setup.js new file mode 100644 index 0000000000..d078277d2d --- /dev/null +++ b/mcp/bin/client-setup.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node + +// Client-setup entry point of the published @penpot/mcp package. +// Configures an MCP client to talk to the local Penpot MCP server by delegating +// to the add-mcp CLI. This launcher is deliberately independent of the server +// build/bootstrap: it only needs npx to fetch add-mcp on demand, so it does not +// require a source checkout, a prior install, or a running server. + +const { spawnSync } = require("child_process"); + +/** + * Runs the add-mcp CLI against the local Penpot MCP server URL. + * + * The server port is read from PENPOT_MCP_SERVER_PORT (default 4401), matching + * the server's own default. add-mcp is fetched via npx using @latest on + * purpose: MCP client config formats change frequently, so we always want the + * newest add-mcp rather than a pinned version. + * + * forwardedArgs are passed straight through to add-mcp so callers can pick + * agents, set headers, override the name or URL, etc. Two defaults are applied + * conditionally so caller args always win: "penpot" as the server name (dropped + * when the caller passes -n/--name) and the local server URL as the positional + * target (dropped when the caller passes their own URL). Unlike a repeated + * option, the URL is a positional, so add-mcp cannot "last-wins" two of them -- + * we must omit ours rather than append it. A caller URL is detected as an arg + * that starts with a scheme (e.g. http://, https://); matching only at the start + * avoids false positives from URLs embedded in --header/--env values. + * + * @param {string[]} forwardedArgs - additional arguments passed through to add-mcp + */ +function runClientSetup(forwardedArgs) { + const port = process.env.PENPOT_MCP_SERVER_PORT || "4401"; + const defaultUrl = `http://localhost:${port}/mcp`; + + const hasName = forwardedArgs.some( + (arg) => arg === "-n" || arg === "--name" || arg.startsWith("--name="), + ); + const nameArgs = hasName ? [] : ["-n", "penpot"]; + + const isUrl = (arg) => /^[a-zA-Z][\w+.-]*:\/\//.test(arg); + const hasUrl = forwardedArgs.some(isUrl); + const urlArgs = hasUrl ? [] : [defaultUrl]; + + const args = ["-y", "add-mcp@latest", "-g", ...nameArgs, ...forwardedArgs, ...urlArgs]; + + const result = spawnSync("npx", args, { + stdio: "inherit", + shell: process.platform === "win32", + }); + + if (result.error) { + throw result.error; + } + if (typeof result.status === "number" && result.status !== 0) { + const error = new Error(`add-mcp exited with code ${result.status}`); + error.status = result.status; + throw error; + } +} + +module.exports = { runClientSetup }; + +// run directly, e.g. via `pnpm run client-setup` or `node bin/client-setup.js` +if (require.main === module) { + try { + runClientSetup(process.argv.slice(2)); + } catch (error) { + process.exit(error.status ?? 1); + } +} diff --git a/mcp/bin/mcp-local.js b/mcp/bin/mcp-local.js index feecbc9e39..b3baf3326b 100644 --- a/mcp/bin/mcp-local.js +++ b/mcp/bin/mcp-local.js @@ -54,18 +54,37 @@ function prepareRuntimeRoot() { return runtimeRoot; } -const root = prepareRuntimeRoot(); - -// pnpm-lock.yaml is hard-excluded by npm pack; it is shipped as pnpm-lock.dist.yaml -// and restored here before bootstrap runs. -const distLock = path.join(root, "pnpm-lock.dist.yaml"); -const lock = path.join(root, "pnpm-lock.yaml"); -if (fs.existsSync(distLock)) { - fs.copyFileSync(distLock, lock); +// check arguments +const cliArgs = process.argv.slice(2); +if (cliArgs[0] === "client-setup") { + // when invoked as `penpot-mcp client-setup [...]`, configure an MCP client + // instead of bootstrapping/starting the server. This must run before the + // runtime-directory copy and bootstrap, neither of which client setup needs. + try { + require("./client-setup.js").runClientSetup(cliArgs.slice(1)); + process.exit(0); + } catch (error) { + process.exit(error.status ?? 1); + } } +else { + // regular case: run bootstrap, building and starting the servers -try { - execSync(`npx -y pnpm@${pnpmVersion()} run bootstrap`, { cwd: root, stdio: "inherit" }); -} catch (error) { - process.exit(error.status ?? 1); + // get runtime root + const root = prepareRuntimeRoot(); + + // pnpm-lock.yaml is hard-excluded by npm pack; it is shipped as pnpm-lock.dist.yaml + // and restored here before bootstrap runs. + const distLock = path.join(root, "pnpm-lock.dist.yaml"); + const lock = path.join(root, "pnpm-lock.yaml"); + if (fs.existsSync(distLock)) { + fs.copyFileSync(distLock, lock); + } + + // run the bootstrap command + try { + execSync(`npx -y pnpm@${pnpmVersion()} run bootstrap`, { cwd: root, stdio: "inherit" }); + } catch (error) { + process.exit(error.status ?? 1); + } } diff --git a/mcp/package.json b/mcp/package.json index 5f6dea413e..6607dd11b2 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -14,6 +14,7 @@ "start:multi-user": "pnpm -r --parallel run start:multi-user", "bootstrap": "pnpm -r install && pnpm run build && pnpm run start", "bootstrap:multi-user": "pnpm -r install && pnpm run build && pnpm run start:multi-user", + "client-setup": "node bin/client-setup.js", "fmt": "prettier --write packages/", "fmt:check": "prettier --check packages/", "test": "pnpm -r run test"