From dc3c407ac5fc898198de0abd3aba312af30b09d8 Mon Sep 17 00:00:00 2001 From: Fernando Basello Date: Sat, 10 Jan 2026 20:41:16 -0300 Subject: [PATCH 1/6] feat(plugin): support running the MCP server on a remote host (not just localhost) --- penpot-plugin/src/main.ts | 4 +++- penpot-plugin/vite.config.ts | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/penpot-plugin/src/main.ts b/penpot-plugin/src/main.ts index ce01d0a..fe28cd1 100644 --- a/penpot-plugin/src/main.ts +++ b/penpot-plugin/src/main.ts @@ -42,7 +42,9 @@ function connectToMcpServer(): void { } try { - ws = new WebSocket("ws://localhost:4402"); + // Use environment variable for MCP WebSocket URL, fallback to localhost for local development + const mcpWsUrl = import.meta.env.VITE_MCP_WS_URL || "ws://localhost:4402"; + ws = new WebSocket(mcpWsUrl); updateConnectionStatus("Connecting...", false); ws.onopen = () => { diff --git a/penpot-plugin/vite.config.ts b/penpot-plugin/vite.config.ts index adfe58b..3607bf2 100644 --- a/penpot-plugin/vite.config.ts +++ b/penpot-plugin/vite.config.ts @@ -26,5 +26,8 @@ export default defineConfig({ preview: { port: 4400, cors: true, + allowedHosts: process.env.VITE_ALLOWED_HOSTS + ? process.env.VITE_ALLOWED_HOSTS.split(",").map((h) => h.trim()) + : ["localhost", "0.0.0.0"], }, }); From b67e6abdd5c2642b5c58114a91cad8780b15fff4 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 12 Jan 2026 20:17:17 +0100 Subject: [PATCH 2/6] Use more specific environment variable names * VITE_ALLOWED_HOSTS -> PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS * VITE_MCP_WS_URL -> PENPOT_MCP_WEBSOCKET_URL --- penpot-plugin/src/main.ts | 2 +- penpot-plugin/vite.config.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/penpot-plugin/src/main.ts b/penpot-plugin/src/main.ts index fe28cd1..2a44977 100644 --- a/penpot-plugin/src/main.ts +++ b/penpot-plugin/src/main.ts @@ -43,7 +43,7 @@ function connectToMcpServer(): void { try { // Use environment variable for MCP WebSocket URL, fallback to localhost for local development - const mcpWsUrl = import.meta.env.VITE_MCP_WS_URL || "ws://localhost:4402"; + const mcpWsUrl = import.meta.env.PENPOT_MCP_WEBSOCKET_URL || "ws://localhost:4402"; ws = new WebSocket(mcpWsUrl); updateConnectionStatus("Connecting...", false); diff --git a/penpot-plugin/vite.config.ts b/penpot-plugin/vite.config.ts index 3607bf2..1807e12 100644 --- a/penpot-plugin/vite.config.ts +++ b/penpot-plugin/vite.config.ts @@ -26,8 +26,8 @@ export default defineConfig({ preview: { port: 4400, cors: true, - allowedHosts: process.env.VITE_ALLOWED_HOSTS - ? process.env.VITE_ALLOWED_HOSTS.split(",").map((h) => h.trim()) + allowedHosts: process.env.PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS + ? process.env.PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS.split(",").map((h) => h.trim()) : ["localhost", "0.0.0.0"], }, }); From 4c875ba7363ddb19c0d71e247ea465cacea8904f Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 12 Jan 2026 20:20:57 +0100 Subject: [PATCH 3/6] Change default allowedHosts to [], allowing only local connections (as before) --- penpot-plugin/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/penpot-plugin/vite.config.ts b/penpot-plugin/vite.config.ts index 1807e12..92af45e 100644 --- a/penpot-plugin/vite.config.ts +++ b/penpot-plugin/vite.config.ts @@ -28,6 +28,6 @@ export default defineConfig({ cors: true, allowedHosts: process.env.PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS ? process.env.PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS.split(",").map((h) => h.trim()) - : ["localhost", "0.0.0.0"], + : [], }, }); From 2598a57080ea3100be8920631bdaa36f987c3b1b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 12 Jan 2026 20:51:32 +0100 Subject: [PATCH 4/6] Add new concept of 'remote mode' (configurable via env var) In particular, remote mode disables file system access --- mcp-server/src/PenpotMcpServer.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/mcp-server/src/PenpotMcpServer.ts b/mcp-server/src/PenpotMcpServer.ts index eb2c367..08df41b 100644 --- a/mcp-server/src/PenpotMcpServer.ts +++ b/mcp-server/src/PenpotMcpServer.ts @@ -75,13 +75,27 @@ export class PenpotMcpServer { return this.isMultiUser; } + /** + * Indicates whether the server is running in remote mode. + * + * In remote mode, the server is not assumed to be accessed only by a local user on the same machine, + * with corresponding limitations being enforced. + * Remote mode can be explicitly enabled by setting the environment variable PENPOT_MCP_REMOTE_MODE + * to "true". Enabling multi-user mode forces remote mode, regardless of the value of the environment + * variable. + */ + public isRemoteMode(): boolean { + const isRemoteModeRequested: boolean = process.env.PENPOT_MCP_REMOTE_MODE === "true"; + return this.isMultiUserMode() || isRemoteModeRequested; + } + /** * Indicates whether file system access is enabled for MCP tools. - * Access is enabled only in single-user mode, where the file system is assumed + * Access is enabled only in local mode, where the file system is assumed * to belong to the user running the server locally. */ public isFileSystemAccessEnabled(): boolean { - return !this.isMultiUserMode(); + return !this.isRemoteMode(); } public getInitialInstructions(): string { @@ -211,6 +225,8 @@ export class PenpotMcpServer { return new Promise((resolve) => { this.app.listen(this.port, async () => { this.logger.info(`Penpot MCP Server started on port ${this.port}`); + this.logger.info(`Multi-user mode: ${this.isMultiUserMode()}`); + this.logger.info(`Remote mode: ${this.isRemoteMode()}`); this.logger.info(`Modern Streamable HTTP endpoint: http://localhost:${this.port}/mcp`); this.logger.info(`Legacy SSE endpoint: http://localhost:${this.port}/sse`); this.logger.info(`WebSocket server is on ws://localhost:${this.webSocketPort}`); From ab97a625e695afc26dedba8235c4ca3ae7393955 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 12 Jan 2026 21:14:29 +0100 Subject: [PATCH 5/6] Add documentation on environment variables for remote mode --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 8d3e49c..67803e5 100644 --- a/README.md +++ b/README.md @@ -196,3 +196,15 @@ The above instructions describe how to run the MCP server and plugin server loca 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. + +To run the server remotely (even for a single user), +you may set the following environment variables to configure the two servers +(MCP server & plugin server) appropriately: + * `PENPOT_MCP_REMOTE_MODE=true`: This ensures that the MCP server is operating + in remote mode, with local file system access disabled. + * `PENPOT_MCP_WEBSOCKET_URL=ws://:4402`: This informs the + Penpot MCP Plugin about the address of the server to connect to. + * `PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS`: Set this to a comma-separated list + of listen addresses for the plugin web server. + To accept connections from any address, use `0.0.0.0` (use caution in + untrusted networks). From 055f7172075a2b4ca320d9867cb3be6909c4dd8e Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Mon, 12 Jan 2026 22:39:36 +0100 Subject: [PATCH 6/6] Standardise configuration with environment variables Replace CLI parameters with environment variables, keeping only --multi-user and --help Environment variables: - PENPOT_MCP_SERVER_PORT (new, replaces CLI param) - PENPOT_MCP_WEBSOCKET_PORT (new) - PENPOT_MCP_REPL_PORT (new) - PENPOT_MCP_SERVER_ADDRESS (new) - PENPOT_MCP_REMOTE_MODE (existing) - PENPOT_MCP_LOG_LEVEL (renamed from LOG_LEVEL, replaces CLI param) - PENPOT_MCP_LOG_DIR (renamed from LOG_DIR, replaces CLI param) - PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS (renamed from PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS) Additional changes: - Plugin now constructs WebSocket URL from server address and port (replaces PENPOT_MCP_WEBSOCKET_URL) - Use configured server address in all startup log messages - Document all configuration options in README.md --- README.md | 37 +++++++++++++++++++++++++++---- mcp-server/src/PenpotMcpServer.ts | 29 ++++++++++++++---------- mcp-server/src/PluginBridge.ts | 2 +- mcp-server/src/ReplServer.ts | 4 +++- mcp-server/src/index.ts | 32 +++++--------------------- mcp-server/src/logger.ts | 4 ++-- penpot-plugin/src/main.ts | 2 +- penpot-plugin/src/vite-env.d.ts | 3 +++ penpot-plugin/vite.config.ts | 12 +++++++--- 9 files changed, 75 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 67803e5..7a94fbe 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,34 @@ This repository is a monorepo containing four main components: The core components are written in TypeScript, rendering interactions with the Penpot Plugin API both natural and type-safe. +## Configuration + +The Penpot MCP server can be configured using environment variables. All configuration +options use the `PENPOT_MCP_` prefix for consistency. + +### Server Configuration + +| Environment Variable | Description | Default | +|-----------------------------|------------- --------------------------------------------------------------|---------| +| `PENPOT_MCP_SERVER_PORT` | Port for the HTTP/SSE server | `4401` | +| `PENPOT_MCP_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` | +| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` | +| `PENPOT_MCP_SERVER_ADDRESS` | Hostname or IP address where the MCP server can be reached | `localhost` | +| `PENPOT_MCP_REMOTE_MODE` | Enable remote mode (disables file system access). Set to `true` to enable. | `false` | + +### Logging Configuration + +| Environment Variable | Description | Default | +|------------------------|------------------------------------------------------|---------| +| `PENPOT_MCP_LOG_LEVEL` | Log level: `trace`, `debug`, `info`, `warn`, `error` | `info` | +| `PENPOT_MCP_LOG_DIR` | Directory for log files | `logs` | + +### Plugin Server Configuration + +| Environment Variable | Description | Default | +|-------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| +| `PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS` | Address on which the plugin web server listens. Can be a single address or a comma-separated list. For example, use `0.0.0.0` to accept connections from any address (use caution in untrusted networks). | (local only) | + ## Beyond Local Execution The above instructions describe how to run the MCP server and plugin server locally. @@ -202,9 +230,10 @@ you may set the following environment variables to configure the two servers (MCP server & plugin server) appropriately: * `PENPOT_MCP_REMOTE_MODE=true`: This ensures that the MCP server is operating in remote mode, with local file system access disabled. - * `PENPOT_MCP_WEBSOCKET_URL=ws://:4402`: This informs the - Penpot MCP Plugin about the address of the server to connect to. - * `PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS`: Set this to a comma-separated list - of listen addresses for the plugin web server. + * `PENPOT_MCP_SERVER_ADDRESS=`: This sets the hostname or IP address + where the MCP server can be reached. The Penpot MCP Plugin uses this to construct + the WebSocket URL as `ws://:` (default port: `4402`). + * `PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS`: Set this to the address (or a + comma-separated list of addresses) on which the plugin web server listens. To accept connections from any address, use `0.0.0.0` (use caution in untrusted networks). diff --git a/mcp-server/src/PenpotMcpServer.ts b/mcp-server/src/PenpotMcpServer.ts index 08df41b..3ea1222 100644 --- a/mcp-server/src/PenpotMcpServer.ts +++ b/mcp-server/src/PenpotMcpServer.ts @@ -41,12 +41,18 @@ export class PenpotMcpServer { sse: {} as Record, }; - constructor( - private port: number = 4401, - private webSocketPort: number = 4402, - replPort: number = 4403, - private isMultiUser: boolean = false - ) { + private readonly port: number; + private readonly webSocketPort: number; + private readonly replPort: number; + public readonly serverAddress: string; + + constructor(private isMultiUser: boolean = false) { + // read port configuration from environment variables + this.port = parseInt(process.env.PENPOT_MCP_SERVER_PORT ?? "4401", 10); + this.webSocketPort = parseInt(process.env.PENPOT_MCP_WEBSOCKET_PORT ?? "4402", 10); + this.replPort = parseInt(process.env.PENPOT_MCP_REPL_PORT ?? "4403", 10); + this.serverAddress = process.env.PENPOT_MCP_SERVER_ADDRESS ?? "localhost"; + this.configLoader = new ConfigurationLoader(); this.apiDocs = new ApiDocs(); @@ -61,8 +67,8 @@ export class PenpotMcpServer { ); this.tools = new Map>(); - this.pluginBridge = new PluginBridge(this, webSocketPort); - this.replServer = new ReplServer(this.pluginBridge, replPort); + this.pluginBridge = new PluginBridge(this, this.webSocketPort); + this.replServer = new ReplServer(this.pluginBridge, this.replPort); this.registerTools(); } @@ -224,12 +230,11 @@ export class PenpotMcpServer { return new Promise((resolve) => { this.app.listen(this.port, async () => { - this.logger.info(`Penpot MCP Server started on port ${this.port}`); this.logger.info(`Multi-user mode: ${this.isMultiUserMode()}`); this.logger.info(`Remote mode: ${this.isRemoteMode()}`); - this.logger.info(`Modern Streamable HTTP endpoint: http://localhost:${this.port}/mcp`); - this.logger.info(`Legacy SSE endpoint: http://localhost:${this.port}/sse`); - this.logger.info(`WebSocket server is on ws://localhost:${this.webSocketPort}`); + this.logger.info(`Modern Streamable HTTP endpoint: http://${this.serverAddress}:${this.port}/mcp`); + this.logger.info(`Legacy SSE endpoint: http://${this.serverAddress}:${this.port}/sse`); + this.logger.info(`WebSocket server URL: ws://${this.serverAddress}:${this.webSocketPort}`); // start the REPL server await this.replServer.start(); diff --git a/mcp-server/src/PluginBridge.ts b/mcp-server/src/PluginBridge.ts index 92aa667..0089a53 100644 --- a/mcp-server/src/PluginBridge.ts +++ b/mcp-server/src/PluginBridge.ts @@ -23,7 +23,7 @@ export class PluginBridge { private readonly taskTimeouts: Map = new Map(); constructor( - private mcpServer: PenpotMcpServer, + public readonly mcpServer: PenpotMcpServer, private port: number, private taskTimeoutSecs: number = 30 ) { diff --git a/mcp-server/src/ReplServer.ts b/mcp-server/src/ReplServer.ts index 90bcba4..978f0e2 100644 --- a/mcp-server/src/ReplServer.ts +++ b/mcp-server/src/ReplServer.ts @@ -88,7 +88,9 @@ export class ReplServer { return new Promise((resolve) => { this.server = this.app.listen(this.port, () => { this.logger.info(`REPL server started on port ${this.port}`); - this.logger.info(`REPL interface available at: http://localhost:${this.port}`); + this.logger.info( + `REPL interface URL: http://${this.pluginBridge.mcpServer.serverAddress}:${this.port}` + ); resolve(); }); }); diff --git a/mcp-server/src/index.ts b/mcp-server/src/index.ts index 5eea0ad..356c2ce 100644 --- a/mcp-server/src/index.ts +++ b/mcp-server/src/index.ts @@ -9,9 +9,7 @@ import { createLogger, logFilePath } from "./logger"; * Creates and starts the MCP server instance, handling any startup errors * gracefully and ensuring proper process termination. * - * Usage: - * - Help: node dist/index.js --help - * - Default configuration: runs on port 4401, logs to mcp-server/logs at info level + * Configuration via environment variables (see README). */ async function main(): Promise { const logger = createLogger("main"); @@ -21,43 +19,25 @@ async function main(): Promise { try { const args = process.argv.slice(2); - let port = 4401; // default port let multiUser = false; // default to single-user mode // parse command line arguments for (let i = 0; i < args.length; i++) { - if (args[i] === "--port" || args[i] === "-p") { - if (i + 1 < args.length) { - const portArg = parseInt(args[i + 1], 10); - if (!isNaN(portArg) && portArg > 0 && portArg <= 65535) { - port = portArg; - } else { - logger.info("Invalid port number. Using default port 4401."); - } - } - } else if (args[i] === "--log-level" || args[i] === "-l") { - if (i + 1 < args.length) { - process.env.LOG_LEVEL = args[i + 1]; - } - } else if (args[i] === "--log-dir") { - if (i + 1 < args.length) { - process.env.LOG_DIR = args[i + 1]; - } - } else if (args[i] === "--multi-user") { + if (args[i] === "--multi-user") { multiUser = true; } else if (args[i] === "--help" || args[i] === "-h") { logger.info("Usage: node dist/index.js [options]"); logger.info("Options:"); - logger.info(" --port, -p Port number for the HTTP/SSE server (default: 4401)"); - logger.info(" --log-level, -l Log level: trace, debug, info, warn, error (default: info)"); - logger.info(" --log-dir Directory for log files (default: mcp-server/logs)"); logger.info(" --multi-user Enable multi-user mode (default: single-user)"); logger.info(" --help, -h Show this help message"); + logger.info(""); + logger.info("Note that configuration is mostly handled through environment variables."); + logger.info("Refer to the README for more information."); process.exit(0); } } - const server = new PenpotMcpServer(port, undefined, undefined, multiUser); + const server = new PenpotMcpServer(multiUser); await server.start(); // keep the process alive diff --git a/mcp-server/src/logger.ts b/mcp-server/src/logger.ts index 05580c6..1e8f96e 100644 --- a/mcp-server/src/logger.ts +++ b/mcp-server/src/logger.ts @@ -4,8 +4,8 @@ import { join, resolve } from "path"; /** * Configuration for log file location and level. */ -const LOG_DIR = process.env.LOG_DIR || "logs"; -const LOG_LEVEL = process.env.LOG_LEVEL || "info"; +const LOG_DIR = process.env.PENPOT_MCP_LOG_DIR || "logs"; +const LOG_LEVEL = process.env.PENPOT_MCP_LOG_LEVEL || "info"; /** * Generates a timestamped log file name. diff --git a/penpot-plugin/src/main.ts b/penpot-plugin/src/main.ts index 7a31b0d..18877d3 100644 --- a/penpot-plugin/src/main.ts +++ b/penpot-plugin/src/main.ts @@ -51,7 +51,7 @@ function connectToMcpServer(): void { } try { - let wsUrl = import.meta.env.PENPOT_MCP_WEBSOCKET_URL || "ws://localhost:4402"; + let wsUrl = PENPOT_MCP_WEBSOCKET_URL; if (isMultiUserMode) { // TODO obtain proper userToken from penpot const userToken = "dummyToken"; diff --git a/penpot-plugin/src/vite-env.d.ts b/penpot-plugin/src/vite-env.d.ts index 11f02fe..ddbf746 100644 --- a/penpot-plugin/src/vite-env.d.ts +++ b/penpot-plugin/src/vite-env.d.ts @@ -1 +1,4 @@ /// + +declare const IS_MULTI_USER_MODE: boolean; +declare const PENPOT_MCP_WEBSOCKET_URL: string; diff --git a/penpot-plugin/vite.config.ts b/penpot-plugin/vite.config.ts index 191827c..d610b7d 100644 --- a/penpot-plugin/vite.config.ts +++ b/penpot-plugin/vite.config.ts @@ -1,10 +1,15 @@ import { defineConfig } from "vite"; import livePreview from "vite-live-preview"; -// Debug: Log the environment variable +// Debug: Log the environment variables console.log("MULTI_USER_MODE env:", process.env.MULTI_USER_MODE); console.log("Will define IS_MULTI_USER_MODE as:", JSON.stringify(process.env.MULTI_USER_MODE === "true")); +const serverAddress = process.env.PENPOT_MCP_SERVER_ADDRESS || "localhost"; +const websocketPort = process.env.PENPOT_MCP_WEBSOCKET_PORT || "4402"; +const websocketUrl = `ws://${serverAddress}:${websocketPort}`; +console.log("Will define PENPOT_MCP_WEBSOCKET_URL as:", JSON.stringify(websocketUrl)); + export default defineConfig({ plugins: [ livePreview({ @@ -30,11 +35,12 @@ export default defineConfig({ preview: { port: 4400, cors: true, - allowedHosts: process.env.PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS - ? process.env.PENPOT_MCP_PLUGIN_SERVER_ALLOWED_HOSTS.split(",").map((h) => h.trim()) + allowedHosts: process.env.PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS + ? process.env.PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS.split(",").map((h) => h.trim()) : [], }, define: { IS_MULTI_USER_MODE: JSON.stringify(process.env.MULTI_USER_MODE === "true"), + PENPOT_MCP_WEBSOCKET_URL: JSON.stringify(websocketUrl), }, });