diff --git a/README.md b/README.md index 8d3e49c..7a94fbe 100644 --- a/README.md +++ b/README.md @@ -190,9 +190,50 @@ 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. 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_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 eb2c367..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(); } @@ -75,13 +81,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 { @@ -210,10 +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(`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(`Multi-user mode: ${this.isMultiUserMode()}`); + this.logger.info(`Remote mode: ${this.isRemoteMode()}`); + 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 2404346..18877d3 100644 --- a/penpot-plugin/src/main.ts +++ b/penpot-plugin/src/main.ts @@ -51,7 +51,7 @@ function connectToMcpServer(): void { } try { - let wsUrl = "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 afa7319..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,8 +35,12 @@ export default defineConfig({ preview: { port: 4400, cors: true, + 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), }, });