mirror of
https://github.com/penpot/penpot-mcp.git
synced 2026-04-25 11:18:37 +00:00
Merge pull request #23
This commit is contained in:
commit
b27ff03fbb
41
README.md
41
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=<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).
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
) {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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";
|
||||
|
||||
3
penpot-plugin/src/vite-env.d.ts
vendored
3
penpot-plugin/src/vite-env.d.ts
vendored
@ -1 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const IS_MULTI_USER_MODE: boolean;
|
||||
declare const PENPOT_MCP_WEBSOCKET_URL: string;
|
||||
|
||||
@ -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),
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user