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
This commit is contained in:
Dominik Jain 2026-01-12 22:39:36 +01:00
parent ab97a625e6
commit 055f717207
9 changed files with 75 additions and 50 deletions

View File

@ -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://<your-address>: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=<your-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://<your-address>:<port>` (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).

View File

@ -41,12 +41,18 @@ export class PenpotMcpServer {
sse: {} as Record<string, { transport: SSEServerTransport; userToken?: string }>,
};
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<string, Tool<any>>();
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();

View File

@ -23,7 +23,7 @@ export class PluginBridge {
private readonly taskTimeouts: Map<string, NodeJS.Timeout> = new Map();
constructor(
private mcpServer: PenpotMcpServer,
public readonly mcpServer: PenpotMcpServer,
private port: number,
private taskTimeoutSecs: number = 30
) {

View File

@ -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();
});
});

View File

@ -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<void> {
const logger = createLogger("main");
@ -21,43 +19,25 @@ async function main(): Promise<void> {
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 <number> Port number for the HTTP/SSE server (default: 4401)");
logger.info(" --log-level, -l <level> Log level: trace, debug, info, warn, error (default: info)");
logger.info(" --log-dir <path> 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

View File

@ -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.

View File

@ -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";

View File

@ -1 +1,4 @@
/// <reference types="vite/client" />
declare const IS_MULTI_USER_MODE: boolean;
declare const PENPOT_MCP_WEBSOCKET_URL: string;

View File

@ -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),
},
});