Skip to content

Commit b27ff03

Browse files
authored
Merge pull request #23
2 parents c51255c + 055f717 commit b27ff03

File tree

9 files changed

+102
-46
lines changed

9 files changed

+102
-46
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,50 @@ This repository is a monorepo containing four main components:
190190
The core components are written in TypeScript, rendering interactions with the
191191
Penpot Plugin API both natural and type-safe.
192192

193+
## Configuration
194+
195+
The Penpot MCP server can be configured using environment variables. All configuration
196+
options use the `PENPOT_MCP_` prefix for consistency.
197+
198+
### Server Configuration
199+
200+
| Environment Variable | Description | Default |
201+
|-----------------------------|------------- --------------------------------------------------------------|---------|
202+
| `PENPOT_MCP_SERVER_PORT` | Port for the HTTP/SSE server | `4401` |
203+
| `PENPOT_MCP_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` |
204+
| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` |
205+
| `PENPOT_MCP_SERVER_ADDRESS` | Hostname or IP address where the MCP server can be reached | `localhost` |
206+
| `PENPOT_MCP_REMOTE_MODE` | Enable remote mode (disables file system access). Set to `true` to enable. | `false` |
207+
208+
### Logging Configuration
209+
210+
| Environment Variable | Description | Default |
211+
|------------------------|------------------------------------------------------|---------|
212+
| `PENPOT_MCP_LOG_LEVEL` | Log level: `trace`, `debug`, `info`, `warn`, `error` | `info` |
213+
| `PENPOT_MCP_LOG_DIR` | Directory for log files | `logs` |
214+
215+
### Plugin Server Configuration
216+
217+
| Environment Variable | Description | Default |
218+
|-------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|
219+
| `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) |
220+
193221
## Beyond Local Execution
194222

195223
The above instructions describe how to run the MCP server and plugin server locally.
196224
We are working on enabling remote deployments of the MCP server, particularly
197225
in [multi-user mode](docs/multi-user-mode.md), where multiple Penpot users will
198226
be able to connect to the same MCP server instance.
227+
228+
To run the server remotely (even for a single user),
229+
you may set the following environment variables to configure the two servers
230+
(MCP server & plugin server) appropriately:
231+
* `PENPOT_MCP_REMOTE_MODE=true`: This ensures that the MCP server is operating
232+
in remote mode, with local file system access disabled.
233+
* `PENPOT_MCP_SERVER_ADDRESS=<your-address>`: This sets the hostname or IP address
234+
where the MCP server can be reached. The Penpot MCP Plugin uses this to construct
235+
the WebSocket URL as `ws://<your-address>:<port>` (default port: `4402`).
236+
* `PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS`: Set this to the address (or a
237+
comma-separated list of addresses) on which the plugin web server listens.
238+
To accept connections from any address, use `0.0.0.0` (use caution in
239+
untrusted networks).

mcp-server/src/PenpotMcpServer.ts

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,18 @@ export class PenpotMcpServer {
4141
sse: {} as Record<string, { transport: SSEServerTransport; userToken?: string }>,
4242
};
4343

44-
constructor(
45-
private port: number = 4401,
46-
private webSocketPort: number = 4402,
47-
replPort: number = 4403,
48-
private isMultiUser: boolean = false
49-
) {
44+
private readonly port: number;
45+
private readonly webSocketPort: number;
46+
private readonly replPort: number;
47+
public readonly serverAddress: string;
48+
49+
constructor(private isMultiUser: boolean = false) {
50+
// read port configuration from environment variables
51+
this.port = parseInt(process.env.PENPOT_MCP_SERVER_PORT ?? "4401", 10);
52+
this.webSocketPort = parseInt(process.env.PENPOT_MCP_WEBSOCKET_PORT ?? "4402", 10);
53+
this.replPort = parseInt(process.env.PENPOT_MCP_REPL_PORT ?? "4403", 10);
54+
this.serverAddress = process.env.PENPOT_MCP_SERVER_ADDRESS ?? "localhost";
55+
5056
this.configLoader = new ConfigurationLoader();
5157
this.apiDocs = new ApiDocs();
5258

@@ -61,8 +67,8 @@ export class PenpotMcpServer {
6167
);
6268

6369
this.tools = new Map<string, Tool<any>>();
64-
this.pluginBridge = new PluginBridge(this, webSocketPort);
65-
this.replServer = new ReplServer(this.pluginBridge, replPort);
70+
this.pluginBridge = new PluginBridge(this, this.webSocketPort);
71+
this.replServer = new ReplServer(this.pluginBridge, this.replPort);
6672

6773
this.registerTools();
6874
}
@@ -75,13 +81,27 @@ export class PenpotMcpServer {
7581
return this.isMultiUser;
7682
}
7783

84+
/**
85+
* Indicates whether the server is running in remote mode.
86+
*
87+
* In remote mode, the server is not assumed to be accessed only by a local user on the same machine,
88+
* with corresponding limitations being enforced.
89+
* Remote mode can be explicitly enabled by setting the environment variable PENPOT_MCP_REMOTE_MODE
90+
* to "true". Enabling multi-user mode forces remote mode, regardless of the value of the environment
91+
* variable.
92+
*/
93+
public isRemoteMode(): boolean {
94+
const isRemoteModeRequested: boolean = process.env.PENPOT_MCP_REMOTE_MODE === "true";
95+
return this.isMultiUserMode() || isRemoteModeRequested;
96+
}
97+
7898
/**
7999
* Indicates whether file system access is enabled for MCP tools.
80-
* Access is enabled only in single-user mode, where the file system is assumed
100+
* Access is enabled only in local mode, where the file system is assumed
81101
* to belong to the user running the server locally.
82102
*/
83103
public isFileSystemAccessEnabled(): boolean {
84-
return !this.isMultiUserMode();
104+
return !this.isRemoteMode();
85105
}
86106

87107
public getInitialInstructions(): string {
@@ -210,10 +230,11 @@ export class PenpotMcpServer {
210230

211231
return new Promise((resolve) => {
212232
this.app.listen(this.port, async () => {
213-
this.logger.info(`Penpot MCP Server started on port ${this.port}`);
214-
this.logger.info(`Modern Streamable HTTP endpoint: http://localhost:${this.port}/mcp`);
215-
this.logger.info(`Legacy SSE endpoint: http://localhost:${this.port}/sse`);
216-
this.logger.info(`WebSocket server is on ws://localhost:${this.webSocketPort}`);
233+
this.logger.info(`Multi-user mode: ${this.isMultiUserMode()}`);
234+
this.logger.info(`Remote mode: ${this.isRemoteMode()}`);
235+
this.logger.info(`Modern Streamable HTTP endpoint: http://${this.serverAddress}:${this.port}/mcp`);
236+
this.logger.info(`Legacy SSE endpoint: http://${this.serverAddress}:${this.port}/sse`);
237+
this.logger.info(`WebSocket server URL: ws://${this.serverAddress}:${this.webSocketPort}`);
217238

218239
// start the REPL server
219240
await this.replServer.start();

mcp-server/src/PluginBridge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class PluginBridge {
2323
private readonly taskTimeouts: Map<string, NodeJS.Timeout> = new Map();
2424

2525
constructor(
26-
private mcpServer: PenpotMcpServer,
26+
public readonly mcpServer: PenpotMcpServer,
2727
private port: number,
2828
private taskTimeoutSecs: number = 30
2929
) {

mcp-server/src/ReplServer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ export class ReplServer {
8888
return new Promise((resolve) => {
8989
this.server = this.app.listen(this.port, () => {
9090
this.logger.info(`REPL server started on port ${this.port}`);
91-
this.logger.info(`REPL interface available at: http://localhost:${this.port}`);
91+
this.logger.info(
92+
`REPL interface URL: http://${this.pluginBridge.mcpServer.serverAddress}:${this.port}`
93+
);
9294
resolve();
9395
});
9496
});

mcp-server/src/index.ts

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import { createLogger, logFilePath } from "./logger";
99
* Creates and starts the MCP server instance, handling any startup errors
1010
* gracefully and ensuring proper process termination.
1111
*
12-
* Usage:
13-
* - Help: node dist/index.js --help
14-
* - Default configuration: runs on port 4401, logs to mcp-server/logs at info level
12+
* Configuration via environment variables (see README).
1513
*/
1614
async function main(): Promise<void> {
1715
const logger = createLogger("main");
@@ -21,43 +19,25 @@ async function main(): Promise<void> {
2119

2220
try {
2321
const args = process.argv.slice(2);
24-
let port = 4401; // default port
2522
let multiUser = false; // default to single-user mode
2623

2724
// parse command line arguments
2825
for (let i = 0; i < args.length; i++) {
29-
if (args[i] === "--port" || args[i] === "-p") {
30-
if (i + 1 < args.length) {
31-
const portArg = parseInt(args[i + 1], 10);
32-
if (!isNaN(portArg) && portArg > 0 && portArg <= 65535) {
33-
port = portArg;
34-
} else {
35-
logger.info("Invalid port number. Using default port 4401.");
36-
}
37-
}
38-
} else if (args[i] === "--log-level" || args[i] === "-l") {
39-
if (i + 1 < args.length) {
40-
process.env.LOG_LEVEL = args[i + 1];
41-
}
42-
} else if (args[i] === "--log-dir") {
43-
if (i + 1 < args.length) {
44-
process.env.LOG_DIR = args[i + 1];
45-
}
46-
} else if (args[i] === "--multi-user") {
26+
if (args[i] === "--multi-user") {
4727
multiUser = true;
4828
} else if (args[i] === "--help" || args[i] === "-h") {
4929
logger.info("Usage: node dist/index.js [options]");
5030
logger.info("Options:");
51-
logger.info(" --port, -p <number> Port number for the HTTP/SSE server (default: 4401)");
52-
logger.info(" --log-level, -l <level> Log level: trace, debug, info, warn, error (default: info)");
53-
logger.info(" --log-dir <path> Directory for log files (default: mcp-server/logs)");
5431
logger.info(" --multi-user Enable multi-user mode (default: single-user)");
5532
logger.info(" --help, -h Show this help message");
33+
logger.info("");
34+
logger.info("Note that configuration is mostly handled through environment variables.");
35+
logger.info("Refer to the README for more information.");
5636
process.exit(0);
5737
}
5838
}
5939

60-
const server = new PenpotMcpServer(port, undefined, undefined, multiUser);
40+
const server = new PenpotMcpServer(multiUser);
6141
await server.start();
6242

6343
// keep the process alive

mcp-server/src/logger.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { join, resolve } from "path";
44
/**
55
* Configuration for log file location and level.
66
*/
7-
const LOG_DIR = process.env.LOG_DIR || "logs";
8-
const LOG_LEVEL = process.env.LOG_LEVEL || "info";
7+
const LOG_DIR = process.env.PENPOT_MCP_LOG_DIR || "logs";
8+
const LOG_LEVEL = process.env.PENPOT_MCP_LOG_LEVEL || "info";
99

1010
/**
1111
* Generates a timestamped log file name.

penpot-plugin/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function connectToMcpServer(): void {
5151
}
5252

5353
try {
54-
let wsUrl = "ws://localhost:4402";
54+
let wsUrl = PENPOT_MCP_WEBSOCKET_URL;
5555
if (isMultiUserMode) {
5656
// TODO obtain proper userToken from penpot
5757
const userToken = "dummyToken";

penpot-plugin/src/vite-env.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
/// <reference types="vite/client" />
2+
3+
declare const IS_MULTI_USER_MODE: boolean;
4+
declare const PENPOT_MCP_WEBSOCKET_URL: string;

penpot-plugin/vite.config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { defineConfig } from "vite";
22
import livePreview from "vite-live-preview";
33

4-
// Debug: Log the environment variable
4+
// Debug: Log the environment variables
55
console.log("MULTI_USER_MODE env:", process.env.MULTI_USER_MODE);
66
console.log("Will define IS_MULTI_USER_MODE as:", JSON.stringify(process.env.MULTI_USER_MODE === "true"));
77

8+
const serverAddress = process.env.PENPOT_MCP_SERVER_ADDRESS || "localhost";
9+
const websocketPort = process.env.PENPOT_MCP_WEBSOCKET_PORT || "4402";
10+
const websocketUrl = `ws://${serverAddress}:${websocketPort}`;
11+
console.log("Will define PENPOT_MCP_WEBSOCKET_URL as:", JSON.stringify(websocketUrl));
12+
813
export default defineConfig({
914
plugins: [
1015
livePreview({
@@ -30,8 +35,12 @@ export default defineConfig({
3035
preview: {
3136
port: 4400,
3237
cors: true,
38+
allowedHosts: process.env.PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS
39+
? process.env.PENPOT_MCP_PLUGIN_SERVER_LISTEN_ADDRESS.split(",").map((h) => h.trim())
40+
: [],
3341
},
3442
define: {
3543
IS_MULTI_USER_MODE: JSON.stringify(process.env.MULTI_USER_MODE === "true"),
44+
PENPOT_MCP_WEBSOCKET_URL: JSON.stringify(websocketUrl),
3645
},
3746
});

0 commit comments

Comments
 (0)