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
|
The core components are written in TypeScript, rendering interactions with the
|
||||||
Penpot Plugin API both natural and type-safe.
|
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
|
## Beyond Local Execution
|
||||||
|
|
||||||
The above instructions describe how to run the MCP server and plugin server locally.
|
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
|
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
|
in [multi-user mode](docs/multi-user-mode.md), where multiple Penpot users will
|
||||||
be able to connect to the same MCP server instance.
|
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 }>,
|
sse: {} as Record<string, { transport: SSEServerTransport; userToken?: string }>,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
private readonly port: number;
|
||||||
private port: number = 4401,
|
private readonly webSocketPort: number;
|
||||||
private webSocketPort: number = 4402,
|
private readonly replPort: number;
|
||||||
replPort: number = 4403,
|
public readonly serverAddress: string;
|
||||||
private isMultiUser: boolean = false
|
|
||||||
) {
|
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.configLoader = new ConfigurationLoader();
|
||||||
this.apiDocs = new ApiDocs();
|
this.apiDocs = new ApiDocs();
|
||||||
|
|
||||||
@ -61,8 +67,8 @@ export class PenpotMcpServer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.tools = new Map<string, Tool<any>>();
|
this.tools = new Map<string, Tool<any>>();
|
||||||
this.pluginBridge = new PluginBridge(this, webSocketPort);
|
this.pluginBridge = new PluginBridge(this, this.webSocketPort);
|
||||||
this.replServer = new ReplServer(this.pluginBridge, replPort);
|
this.replServer = new ReplServer(this.pluginBridge, this.replPort);
|
||||||
|
|
||||||
this.registerTools();
|
this.registerTools();
|
||||||
}
|
}
|
||||||
@ -75,13 +81,27 @@ export class PenpotMcpServer {
|
|||||||
return this.isMultiUser;
|
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.
|
* 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.
|
* to belong to the user running the server locally.
|
||||||
*/
|
*/
|
||||||
public isFileSystemAccessEnabled(): boolean {
|
public isFileSystemAccessEnabled(): boolean {
|
||||||
return !this.isMultiUserMode();
|
return !this.isRemoteMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getInitialInstructions(): string {
|
public getInitialInstructions(): string {
|
||||||
@ -210,10 +230,11 @@ export class PenpotMcpServer {
|
|||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.app.listen(this.port, async () => {
|
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(`Modern Streamable HTTP endpoint: http://localhost:${this.port}/mcp`);
|
this.logger.info(`Remote mode: ${this.isRemoteMode()}`);
|
||||||
this.logger.info(`Legacy SSE endpoint: http://localhost:${this.port}/sse`);
|
this.logger.info(`Modern Streamable HTTP endpoint: http://${this.serverAddress}:${this.port}/mcp`);
|
||||||
this.logger.info(`WebSocket server is on ws://localhost:${this.webSocketPort}`);
|
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
|
// start the REPL server
|
||||||
await this.replServer.start();
|
await this.replServer.start();
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export class PluginBridge {
|
|||||||
private readonly taskTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
private readonly taskTimeouts: Map<string, NodeJS.Timeout> = new Map();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private mcpServer: PenpotMcpServer,
|
public readonly mcpServer: PenpotMcpServer,
|
||||||
private port: number,
|
private port: number,
|
||||||
private taskTimeoutSecs: number = 30
|
private taskTimeoutSecs: number = 30
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -88,7 +88,9 @@ export class ReplServer {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.server = this.app.listen(this.port, () => {
|
this.server = this.app.listen(this.port, () => {
|
||||||
this.logger.info(`REPL server started on port ${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();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,9 +9,7 @@ import { createLogger, logFilePath } from "./logger";
|
|||||||
* Creates and starts the MCP server instance, handling any startup errors
|
* Creates and starts the MCP server instance, handling any startup errors
|
||||||
* gracefully and ensuring proper process termination.
|
* gracefully and ensuring proper process termination.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Configuration via environment variables (see README).
|
||||||
* - Help: node dist/index.js --help
|
|
||||||
* - Default configuration: runs on port 4401, logs to mcp-server/logs at info level
|
|
||||||
*/
|
*/
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const logger = createLogger("main");
|
const logger = createLogger("main");
|
||||||
@ -21,43 +19,25 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
let port = 4401; // default port
|
|
||||||
let multiUser = false; // default to single-user mode
|
let multiUser = false; // default to single-user mode
|
||||||
|
|
||||||
// parse command line arguments
|
// parse command line arguments
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
if (args[i] === "--port" || args[i] === "-p") {
|
if (args[i] === "--multi-user") {
|
||||||
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") {
|
|
||||||
multiUser = true;
|
multiUser = true;
|
||||||
} else if (args[i] === "--help" || args[i] === "-h") {
|
} else if (args[i] === "--help" || args[i] === "-h") {
|
||||||
logger.info("Usage: node dist/index.js [options]");
|
logger.info("Usage: node dist/index.js [options]");
|
||||||
logger.info("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(" --multi-user Enable multi-user mode (default: single-user)");
|
||||||
logger.info(" --help, -h Show this help message");
|
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);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new PenpotMcpServer(port, undefined, undefined, multiUser);
|
const server = new PenpotMcpServer(multiUser);
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
||||||
// keep the process alive
|
// keep the process alive
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { join, resolve } from "path";
|
|||||||
/**
|
/**
|
||||||
* Configuration for log file location and level.
|
* Configuration for log file location and level.
|
||||||
*/
|
*/
|
||||||
const LOG_DIR = process.env.LOG_DIR || "logs";
|
const LOG_DIR = process.env.PENPOT_MCP_LOG_DIR || "logs";
|
||||||
const LOG_LEVEL = process.env.LOG_LEVEL || "info";
|
const LOG_LEVEL = process.env.PENPOT_MCP_LOG_LEVEL || "info";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a timestamped log file name.
|
* Generates a timestamped log file name.
|
||||||
|
|||||||
@ -51,7 +51,7 @@ function connectToMcpServer(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let wsUrl = "ws://localhost:4402";
|
let wsUrl = PENPOT_MCP_WEBSOCKET_URL;
|
||||||
if (isMultiUserMode) {
|
if (isMultiUserMode) {
|
||||||
// TODO obtain proper userToken from penpot
|
// TODO obtain proper userToken from penpot
|
||||||
const userToken = "dummyToken";
|
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" />
|
/// <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 { defineConfig } from "vite";
|
||||||
import livePreview from "vite-live-preview";
|
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("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"));
|
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({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
livePreview({
|
livePreview({
|
||||||
@ -30,8 +35,12 @@ export default defineConfig({
|
|||||||
preview: {
|
preview: {
|
||||||
port: 4400,
|
port: 4400,
|
||||||
cors: true,
|
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: {
|
define: {
|
||||||
IS_MULTI_USER_MODE: JSON.stringify(process.env.MULTI_USER_MODE === "true"),
|
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