Add PENPOT_MCP_REPL_ENABLE env var for explicit REPL control

Allow the REPL server to be enabled independently of the devenv
setting via a new PENPOT_MCP_REPL_ENABLE environment variable.
When set to "true", the REPL server starts regardless of
PENPOT_MCP_DEVENV; when set to any other value, it is disabled.
When unset, the previous isDevEnv fallback applies.

Addresses review feedback on PR #11282.

AI-assisted-by: mimo-v2.5
This commit is contained in:
Andrey Antukh 2026-08-21 12:48:08 +00:00
parent eb21d4e899
commit bd83ab076a
3 changed files with 78 additions and 3 deletions

View File

@ -265,6 +265,7 @@ The Penpot MCP server can be configured using environment variables.
| `PENPOT_MCP_SERVER_PORT` | Port for the HTTP/SSE server | `4401` | | `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_WEBSOCKET_PORT` | Port for the WebSocket server (plugin connection) | `4402` |
| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` | | `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` |
| `PENPOT_MCP_REPL_ENABLE` | Explicitly enable/disable the REPL server. Set to `true` to enable. When unset, defaults to the value of `PENPOT_MCP_DEVENV`. | (unset) |
| `PENPOT_MCP_REMOTE_MODE` | Enable remote mode (disables file system access). Set to `true` to enable. | `false` | | `PENPOT_MCP_REMOTE_MODE` | Enable remote mode (disables file system access). Set to `true` to enable. | `false` |
| `PENPOT_MCP_DEVENV` | Enable Penpot development environment tools. Set to `true` to enable. | `false` | | `PENPOT_MCP_DEVENV` | Enable Penpot development environment tools. Set to `true` to enable. | `false` |
| `PENPOT_MCP_TOOL_TIMEOUT_S` | Timeout, in seconds, for tool calls dispatched to the Penpot plugin | `120` | | `PENPOT_MCP_TOOL_TIMEOUT_S` | Timeout, in seconds, for tool calls dispatched to the Penpot plugin | `120` |

View File

@ -16,6 +16,28 @@ test("isDevEnvEnabled returns true when PENPOT_MCP_DEVENV is 'true'", () => {
assert.equal(PenpotMcpServer.isDevEnvEnabled({ PENPOT_MCP_DEVENV: "true" }), true); assert.equal(PenpotMcpServer.isDevEnvEnabled({ PENPOT_MCP_DEVENV: "true" }), true);
}); });
// ── Pure function tests: isReplEnabled ──────────────────────────
test("isReplEnabled returns false when neither env var is set", () => {
assert.equal(PenpotMcpServer.isReplEnabled({}), false);
});
test("isReplEnabled returns true when PENPOT_MCP_DEVENV is 'true' (fallback)", () => {
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_DEVENV: "true" }), true);
});
test("isReplEnabled returns true when PENPOT_MCP_REPL_ENABLE is 'true'", () => {
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_REPL_ENABLE: "true" }), true);
});
test("isReplEnabled returns false when PENPOT_MCP_REPL_ENABLE is 'false' even if DEVENV is true", () => {
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_REPL_ENABLE: "false", PENPOT_MCP_DEVENV: "true" }), false);
});
test("isReplEnabled returns true when PENPOT_MCP_REPL_ENABLE is 'true' regardless of DEVENV", () => {
assert.equal(PenpotMcpServer.isReplEnabled({ PENPOT_MCP_REPL_ENABLE: "true" }), true);
});
// ── Integration tests: constructor gating ────────────────────── // ── Integration tests: constructor gating ──────────────────────
// //
// Each test uses unique ports to avoid conflicts when tests run // Each test uses unique ports to avoid conflicts when tests run
@ -57,6 +79,40 @@ test("constructor creates ReplServer when PENPOT_MCP_DEVENV is 'true'", async ()
} }
}); });
test("constructor creates ReplServer when PENPOT_MCP_REPL_ENABLE is 'true' without DEVENV", async () => {
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
const prevReplEnable = process.env.PENPOT_MCP_REPL_ENABLE;
const prevPorts = setUniqueEnv();
delete process.env.PENPOT_MCP_DEVENV;
process.env.PENPOT_MCP_REPL_ENABLE = "true";
let server: PenpotMcpServer | undefined;
try {
server = new PenpotMcpServer(false);
assert.equal(server.hasReplServer(), true);
} finally {
await server?.stop();
restoreEnv(prevDevEnv, prevPorts);
restoreOrDelete("PENPOT_MCP_REPL_ENABLE", prevReplEnable);
}
});
test("constructor does not create ReplServer when PENPOT_MCP_REPL_ENABLE is 'false' even with DEVENV", async () => {
const prevDevEnv = process.env.PENPOT_MCP_DEVENV;
const prevReplEnable = process.env.PENPOT_MCP_REPL_ENABLE;
const prevPorts = setUniqueEnv();
process.env.PENPOT_MCP_DEVENV = "true";
process.env.PENPOT_MCP_REPL_ENABLE = "false";
let server: PenpotMcpServer | undefined;
try {
server = new PenpotMcpServer(false);
assert.equal(server.hasReplServer(), false);
} finally {
await server?.stop();
restoreEnv(prevDevEnv, prevPorts);
restoreOrDelete("PENPOT_MCP_REPL_ENABLE", prevReplEnable);
}
});
// ── Helpers ──────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────
function setUniqueEnv() { function setUniqueEnv() {

View File

@ -66,6 +66,20 @@ export class PenpotMcpServer {
return env.PENPOT_MCP_DEVENV === "true"; return env.PENPOT_MCP_DEVENV === "true";
} }
/**
* Determines whether the REPL server should be enabled.
*
* If ``PENPOT_MCP_REPL_ENABLE`` is set, its value controls the result
* (``"true"`` enables, any other value disables). When the variable is
* not set, the result falls back to {@link isDevEnvEnabled}.
*/
public static isReplEnabled(env: Record<string, string | undefined>): boolean {
if (env.PENPOT_MCP_REPL_ENABLE !== undefined) {
return env.PENPOT_MCP_REPL_ENABLE === "true";
}
return PenpotMcpServer.isDevEnvEnabled(env);
}
/** /**
* Returns a short, non-reversible fingerprint of a user token, suitable for * Returns a short, non-reversible fingerprint of a user token, suitable for
* correlating log lines without exposing the full credential. * correlating log lines without exposing the full credential.
@ -160,7 +174,7 @@ export class PenpotMcpServer {
this.pluginBridge = new PluginBridge(this, this.webSocketPort, toolTimeoutSecs, this.redisBridge); this.pluginBridge = new PluginBridge(this, this.webSocketPort, toolTimeoutSecs, this.redisBridge);
if (PenpotMcpServer.isDevEnvEnabled(process.env)) { if (PenpotMcpServer.isReplEnabled(process.env)) {
this.replServer = new ReplServer(this.pluginBridge, this.replPort, this.host); this.replServer = new ReplServer(this.pluginBridge, this.replPort, this.host);
} else { } else {
this.replServer = null; this.replServer = null;
@ -211,7 +225,9 @@ export class PenpotMcpServer {
/** /**
* Indicates whether the REPL server was created. * Indicates whether the REPL server was created.
* *
* The REPL server is created only in development environment mode. * The REPL server is created when {@link isReplEnabled} returns true,
* which means either ``PENPOT_MCP_REPL_ENABLE=true`` or, when that
* variable is unset, ``PENPOT_MCP_DEVENV=true``.
*/ */
public hasReplServer(): boolean { public hasReplServer(): boolean {
return this.replServer !== null; return this.replServer !== null;
@ -449,7 +465,9 @@ export class PenpotMcpServer {
if (this.replServer) { if (this.replServer) {
await this.replServer.start(); await this.replServer.start();
} else { } else {
this.logger.info("REPL server disabled (set PENPOT_MCP_DEVENV=true to enable)"); this.logger.info(
"REPL server disabled (set PENPOT_MCP_REPL_ENABLE=true or PENPOT_MCP_DEVENV=true to enable)"
);
} }
this.startSessionTimeoutChecker(); this.startSessionTimeoutChecker();