mirror of
https://github.com/penpot/penpot.git
synced 2026-05-12 19:43:48 +00:00
🎉 Add MCP tool for ClojureScript expression evaluation
New tool to evaluate ClojureScript expressions by connecting to the nREPL service already provided in devenv. Add dependency 'nrepl-client' and a corresponding client class as well as types to support this. Add a new environment variable for 'devenv mode', which enables the new tool (PENPOT_MCP_DEVENV).
This commit is contained in:
parent
e1493de777
commit
66d518f15d
@ -3,15 +3,6 @@
|
||||
## Overview
|
||||
The penpot frontend uses shadow-cljs with `:target :esm` and multi-module code splitting. The CLJS REPL evaluates code in the browser runtime via a websocket connection.
|
||||
|
||||
## Known Pitfall: Rasterizer vs Workspace Runtime
|
||||
The workspace page embeds a rasterizer iframe (`rasterizer.html`) that also loads the `:main` shadow-cljs build. Both runtimes register with shadow-cljs. If the rasterizer connects first, the REPL will target it instead of the workspace — and the rasterizer has an **empty app state** (its own `defonce` store instance).
|
||||
|
||||
**Symptoms:** `@st/state` returns nil, `(.-title js/document)` returns "Penpot - Rasterizer".
|
||||
|
||||
**Fix:** Restart the devenv (`docker restart penpot-devenv-main`) and reload the browser. After a clean restart, the workspace runtime typically connects first.
|
||||
|
||||
**Verification:** Run `(.-title js/document)` — it should show your file name (e.g. "New File 1 - Penpot"), not "Penpot - Rasterizer".
|
||||
|
||||
## Method 1: Interactive REPL (inside container)
|
||||
```bash
|
||||
docker exec -it penpot-devenv-main bash
|
||||
@ -49,27 +40,52 @@ To list connected runtimes and their client-ids:
|
||||
A custom Python nREPL client exists at `tools/nrepl_eval.py`. However, it uses `(shadow/repl :main)` to switch to CLJS mode, which doesn't reliably select the correct runtime. **Prefer Method 2 for automation.**
|
||||
|
||||
## Accessing App State
|
||||
|
||||
The main store is `app.main.store/state`. It contains workspace metadata, selection, UI state, etc.
|
||||
However, **page objects are NOT in the main store atom**. They live behind derived refs.
|
||||
|
||||
### Top-level store keys (subset)
|
||||
`:current-page-id`, `:current-file-id`, `:workspace-local`, `:workspace-global`,
|
||||
`:workspace-trimmed-page`, `:workspace-undo`, `:workspace-guides`, `:workspace-layout`,
|
||||
`:workspace-drawing`, `:workspace-presence`, `:workspace-ready`, `:profile`, `:route`, etc.
|
||||
|
||||
**Notable absence:** There is no `:workspace-data` key in the store. The old path
|
||||
`(get-in state [:workspace-data :pages-index page-id :objects])` does NOT work.
|
||||
|
||||
### Getting page objects — use `app.main.refs/workspace-page-objects`
|
||||
```clojure
|
||||
(require '[app.main.store :as st])
|
||||
(some? @st/state) ;; should be true
|
||||
|
||||
;; Get current page id
|
||||
(:current-page-id @st/state)
|
||||
|
||||
;; Get objects on current page
|
||||
(let [state @st/state
|
||||
page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index page-id :objects])]
|
||||
(count objects))
|
||||
|
||||
;; Get a specific shape
|
||||
(let [state @st/state
|
||||
page-id (:current-page-id state)
|
||||
objects (get-in state [:workspace-data :pages-index page-id :objects])
|
||||
;; This is a derived ref (reactive lens). Deref it directly:
|
||||
(let [objects @app.main.refs/workspace-page-objects
|
||||
shape (get objects (parse-uuid "some-uuid-here"))]
|
||||
(select-keys shape [:name :type :component-id :component-file :component-root]))
|
||||
(select-keys shape [:name :type :x :y :width :height :fills :strokes :rotation :opacity :frame-id :parent-id]))
|
||||
```
|
||||
|
||||
### Getting the current selection
|
||||
```clojure
|
||||
;; Selection is in the main store under :workspace-local :selected
|
||||
(let [state @app.main.store/state
|
||||
selected (get-in state [:workspace-local :selected])]
|
||||
(mapv str selected))
|
||||
;; Returns vector of UUID strings for selected shapes
|
||||
```
|
||||
|
||||
### Other useful store access
|
||||
```clojure
|
||||
;; Current page id
|
||||
(:current-page-id @app.main.store/state)
|
||||
|
||||
;; Verify state is accessible
|
||||
(some? @app.main.store/state) ;; should be true
|
||||
|
||||
;; workspace-local keys: :zoom :selected :hide-toolbar :last-selected :vbox
|
||||
;; :highlighted :vport :expanded :selrect :zoom-inverse
|
||||
```
|
||||
|
||||
### Shape data structure (internal ClojureScript representation)
|
||||
Shape keys use kebab-case keywords (`:fill-color`, `:fill-opacity`, `:parent-id`, `:frame-id`).
|
||||
The shape `:type` is a keyword like `:rect`, `:path`, `:text`, `:ellipse`, `:image`, `:bool`, `:svg-raw`, `:frame`, `:group`.
|
||||
Note `:rect` in CLJS corresponds to "rectangle" in the JS Plugin API, and `:frame` corresponds to "board".
|
||||
|
||||
## Notes
|
||||
- nREPL server runs on port 3447 inside the container, mapped to host
|
||||
- The `:main` build has multiple modules: shared, main, main-workspace, rasterizer, etc.
|
||||
@ -77,3 +93,8 @@ A custom Python nREPL client exists at `tools/nrepl_eval.py`. However, it uses `
|
||||
- Ignore the "WARNING: shadow-cljs not installed in project" message — it works via the running server
|
||||
- Use `timeout` to avoid hanging if the browser is disconnected
|
||||
- `DO NOT` call `shadow.cljs.devtools.api/repl-runtime-select` with a runtime that can't eval — it will jam the REPL until restart
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The REPL may occasionally not connect to the right runtime.
|
||||
Run `(.-title js/document)` to verify — it should show your file name (e.g. "New File 1 - Penpot"), not "Penpot - Rasterizer".
|
||||
@ -50,7 +50,7 @@ if echo "$PENPOT_FLAGS" | grep -q "enable-mcp"; then
|
||||
tmux new-window -t penpot:4 -n 'mcp server'
|
||||
tmux select-window -t penpot:4
|
||||
tmux send-keys -t penpot 'cd penpot/mcp' enter C-l
|
||||
tmux send-keys -t penpot 'PENPOT_MCP_SERVER_HOST=0.0.0.0 PENPOT_MCP_REMOTE_MODE=true pnpm run start' enter
|
||||
tmux send-keys -t penpot './scripts/start-mcp-devenv' enter
|
||||
fi
|
||||
|
||||
tmux -2 attach-session -t penpot
|
||||
|
||||
@ -272,6 +272,7 @@ The Penpot MCP server can be configured using environment variables.
|
||||
| `PENPOT_MCP_REPL_PORT` | Port for the REPL server (development/debugging) | `4403` |
|
||||
| `PENPOT_MCP_SERVER_ADDRESS` | Hostname or IP address via which clients can reach the MCP server | `localhost` |
|
||||
| `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` |
|
||||
|
||||
### Logging Configuration
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build:server": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/* --external:ws --external:express --external:class-transformer --external:class-validator --external:reflect-metadata --external:pino --external:pino-pretty --external:pino-loki --external:js-yaml --external:sharp",
|
||||
"build:server": "esbuild src/index.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/* --external:ws --external:express --external:class-transformer --external:class-validator --external:reflect-metadata --external:pino --external:pino-pretty --external:pino-loki --external:js-yaml --external:sharp --external:nrepl-client",
|
||||
"build": "pnpm run build:server && node scripts/copy-resources.js",
|
||||
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||
"start": "node dist/index.js",
|
||||
@ -29,6 +29,7 @@
|
||||
"class-validator": "^0.14.3",
|
||||
"express": "^5.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"nrepl-client": "^0.3.0",
|
||||
"penpot-mcp": "file:..",
|
||||
"pino": "^9.10.0",
|
||||
"pino-loki": "^2.6.0",
|
||||
|
||||
142
mcp/packages/server/src/NreplClient.ts
Normal file
142
mcp/packages/server/src/NreplClient.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import nreplClient from "nrepl-client";
|
||||
import { createLogger } from "./logger";
|
||||
|
||||
/**
|
||||
* Result of evaluating a ClojureScript expression via nREPL.
|
||||
*/
|
||||
export interface NreplEvalResult {
|
||||
/** the returned value(s) as strings */
|
||||
values: string[];
|
||||
/** captured stdout output */
|
||||
out: string;
|
||||
/** captured stderr output */
|
||||
err: string;
|
||||
/** the namespace after evaluation */
|
||||
ns: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A client for communicating with a shadow-cljs nREPL server.
|
||||
*
|
||||
* This client wraps the nrepl-client library, providing a typed, promise-based
|
||||
* interface for evaluating Clojure and ClojureScript expressions.
|
||||
*/
|
||||
export class NreplClient {
|
||||
private static readonly NREPL_PORT = 3447;
|
||||
private static readonly NREPL_HOST = "localhost";
|
||||
private static readonly EVAL_TIMEOUT_MS = 30_000;
|
||||
|
||||
private readonly logger = createLogger("NreplClient");
|
||||
|
||||
/**
|
||||
* Evaluates a Clojure expression on the nREPL server.
|
||||
*
|
||||
* A new connection is established for each evaluation and closed afterwards.
|
||||
*
|
||||
* @param code - the Clojure expression to evaluate
|
||||
* @returns the evaluation result
|
||||
*/
|
||||
async eval(code: string): Promise<NreplEvalResult> {
|
||||
this.logger.debug("Evaluating Clojure expression: %s", code);
|
||||
return this.withConnection((conn) => {
|
||||
return new Promise<NreplEvalResult>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error(`nREPL evaluation timed out after ${NreplClient.EVAL_TIMEOUT_MS}ms`));
|
||||
}, NreplClient.EVAL_TIMEOUT_MS);
|
||||
|
||||
conn.eval(code, (err: Error | null, result: any[]) => {
|
||||
clearTimeout(timeout);
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(this.parseEvalResult(result));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a ClojureScript expression via the shadow-cljs CLJS eval API.
|
||||
*
|
||||
* The expression is wrapped in a call to `shadow.cljs.devtools.api/cljs-eval`
|
||||
* targeting the `:main` build, so it is evaluated in the browser runtime.
|
||||
*
|
||||
* @param cljsCode - the ClojureScript expression to evaluate
|
||||
* @returns the evaluation result
|
||||
*/
|
||||
async evalCljs(cljsCode: string): Promise<NreplEvalResult> {
|
||||
// escape the CLJS code for embedding in a Clojure string
|
||||
const escapedCode = cljsCode.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
const wrappedCode = `(shadow.cljs.devtools.api/cljs-eval :main "${escapedCode}" {})`;
|
||||
this.logger.debug("Evaluating CLJS expression via shadow-cljs: %s", cljsCode);
|
||||
return this.eval(wrappedCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a connection, executes the given operation, and ensures the connection is closed afterwards.
|
||||
*/
|
||||
private async withConnection<T>(operation: (conn: any) => Promise<T>): Promise<T> {
|
||||
const conn = nreplClient.connect({
|
||||
port: NreplClient.NREPL_PORT,
|
||||
host: NreplClient.NREPL_HOST,
|
||||
});
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
conn.once("connect", async () => {
|
||||
try {
|
||||
const result = await operation(conn);
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
} finally {
|
||||
conn.end();
|
||||
}
|
||||
});
|
||||
|
||||
conn.once("error", (err: Error) => {
|
||||
this.logger.error("nREPL connection error: %s", err);
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to connect to nREPL server at ${NreplClient.NREPL_HOST}:${NreplClient.NREPL_PORT}: ${err.message}`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the raw nREPL response messages into a structured result.
|
||||
*/
|
||||
private parseEvalResult(messages: any[]): NreplEvalResult {
|
||||
const values: string[] = [];
|
||||
const outParts: string[] = [];
|
||||
const errParts: string[] = [];
|
||||
let ns = "user";
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.value !== undefined) {
|
||||
values.push(msg.value);
|
||||
}
|
||||
if (msg.out) {
|
||||
outParts.push(msg.out);
|
||||
}
|
||||
if (msg.err) {
|
||||
errParts.push(msg.err);
|
||||
}
|
||||
if (msg.ns) {
|
||||
ns = msg.ns;
|
||||
}
|
||||
if (msg.ex) {
|
||||
throw new Error(`nREPL evaluation error: ${msg.ex}${msg.err ? "\n" + msg.err : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
values,
|
||||
out: outParts.join(""),
|
||||
err: errParts.join(""),
|
||||
ns,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,8 @@ import { HighLevelOverviewTool } from "./tools/HighLevelOverviewTool";
|
||||
import { PenpotApiInfoTool } from "./tools/PenpotApiInfoTool";
|
||||
import { ExportShapeTool } from "./tools/ExportShapeTool";
|
||||
import { ImportImageTool } from "./tools/ImportImageTool";
|
||||
import { EvalCljsExpressionTool } from "./tools/EvalCljsExpressionTool";
|
||||
import { NreplClient } from "./NreplClient";
|
||||
import { ReplServer } from "./ReplServer";
|
||||
import { ApiDocs } from "./ApiDocs";
|
||||
|
||||
@ -151,6 +153,16 @@ export class PenpotMcpServer {
|
||||
return !this.isRemoteMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the server is running in a Penpot development environment.
|
||||
*
|
||||
* When enabled (by setting the environment variable PENPOT_MCP_DEVENV to "true"),
|
||||
* additional developer tools such as ClojureScript expression evaluation are exposed.
|
||||
*/
|
||||
public isDevEnv(): boolean {
|
||||
return process.env.PENPOT_MCP_DEVENV === "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the high-level overview instructions explaining core Penpot usage.
|
||||
*/
|
||||
@ -177,6 +189,9 @@ export class PenpotMcpServer {
|
||||
if (this.isFileSystemAccessEnabled()) {
|
||||
toolInstances.push(new ImportImageTool(this));
|
||||
}
|
||||
if (this.isDevEnv()) {
|
||||
toolInstances.push(new EvalCljsExpressionTool(this, new NreplClient()));
|
||||
}
|
||||
|
||||
return toolInstances.map((instance) => {
|
||||
this.logger.info(`Registering tool: ${instance.getToolName()}`);
|
||||
@ -341,6 +356,7 @@ export class PenpotMcpServer {
|
||||
this.app.listen(this.port, this.host, async () => {
|
||||
this.logger.info(`Multi-user mode: ${this.isMultiUserMode()}`);
|
||||
this.logger.info(`Remote mode: ${this.isRemoteMode()}`);
|
||||
this.logger.info(`DevEnv mode: ${this.isDevEnv()}`);
|
||||
this.logger.info(`Modern Streamable HTTP endpoint: http://${this.host}:${this.port}/mcp`);
|
||||
this.logger.info(`Legacy SSE endpoint: http://${this.host}:${this.port}/sse`);
|
||||
this.logger.info(`WebSocket server URL: ws://${this.host}:${this.webSocketPort}`);
|
||||
|
||||
74
mcp/packages/server/src/tools/EvalCljsExpressionTool.ts
Normal file
74
mcp/packages/server/src/tools/EvalCljsExpressionTool.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "../Tool";
|
||||
import "reflect-metadata";
|
||||
import type { ToolResponse } from "../ToolResponse";
|
||||
import { TextResponse } from "../ToolResponse";
|
||||
import { PenpotMcpServer } from "../PenpotMcpServer";
|
||||
import { NreplClient } from "../NreplClient";
|
||||
|
||||
/**
|
||||
* Arguments for the EvalCljsExpressionTool.
|
||||
*/
|
||||
export class EvalCljsExpressionArgs {
|
||||
static schema = {
|
||||
expression: z.string().min(1, "Expression cannot be empty"),
|
||||
};
|
||||
|
||||
/**
|
||||
* The ClojureScript expression to evaluate in the frontend runtime.
|
||||
*/
|
||||
expression!: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool for evaluating ClojureScript expressions in the Penpot frontend runtime.
|
||||
*
|
||||
* This tool connects to the shadow-cljs nREPL server and evaluates the given
|
||||
* ClojureScript expression in the context of the running browser application,
|
||||
* providing direct access to the frontend application state and APIs.
|
||||
*/
|
||||
export class EvalCljsExpressionTool extends Tool<EvalCljsExpressionArgs> {
|
||||
private readonly nreplClient: NreplClient;
|
||||
|
||||
/**
|
||||
* Creates a new EvalCljsExpressionTool instance.
|
||||
*
|
||||
* @param mcpServer - the MCP server instance
|
||||
* @param nreplClient - the nREPL client for communicating with shadow-cljs
|
||||
*/
|
||||
constructor(mcpServer: PenpotMcpServer, nreplClient: NreplClient) {
|
||||
super(mcpServer, EvalCljsExpressionArgs.schema);
|
||||
this.nreplClient = nreplClient;
|
||||
}
|
||||
|
||||
public getToolName(): string {
|
||||
return "eval_cljs_expression";
|
||||
}
|
||||
|
||||
public getToolDescription(): string {
|
||||
return (
|
||||
"Evaluates a ClojureScript expression in the Penpot frontend runtime via the shadow-cljs nREPL server. " +
|
||||
"The expression is evaluated in the browser context, providing access to the application state and ClojureScript APIs."
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeCore(args: EvalCljsExpressionArgs): Promise<ToolResponse> {
|
||||
const result = await this.nreplClient.evalCljs(args.expression);
|
||||
|
||||
const parts: string[] = [];
|
||||
if (result.values.length > 0) {
|
||||
parts.push(result.values.join("\n"));
|
||||
}
|
||||
if (result.out) {
|
||||
parts.push(`stdout:\n${result.out}`);
|
||||
}
|
||||
if (result.err) {
|
||||
parts.push(`stderr:\n${result.err}`);
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
parts.push("nil");
|
||||
}
|
||||
|
||||
return new TextResponse(parts.join("\n\n"));
|
||||
}
|
||||
}
|
||||
51
mcp/packages/server/src/types/nrepl-client.d.ts
vendored
Normal file
51
mcp/packages/server/src/types/nrepl-client.d.ts
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
declare module "nrepl-client" {
|
||||
import type { Socket } from "net";
|
||||
|
||||
interface NreplConnection extends Socket {
|
||||
/**
|
||||
* Evaluates the given Clojure expression on the nREPL server.
|
||||
*
|
||||
* @param code - the Clojure expression to evaluate
|
||||
* @param callback - called with an error or array of response messages
|
||||
*/
|
||||
eval(code: string, callback: (err: Error | null, result: NreplMessage[]) => void): void;
|
||||
|
||||
/**
|
||||
* Sends a raw nREPL message to the server.
|
||||
*/
|
||||
send(message: Record<string, unknown>, callback: (err: Error | null, result: NreplMessage[]) => void): void;
|
||||
|
||||
/**
|
||||
* Clones the current session.
|
||||
*/
|
||||
clone(callback: (err: Error | null, result: NreplMessage[]) => void): void;
|
||||
|
||||
/**
|
||||
* Closes the current session.
|
||||
*/
|
||||
close(callback: (err: Error | null, result: NreplMessage[]) => void): void;
|
||||
}
|
||||
|
||||
interface NreplMessage {
|
||||
id?: string;
|
||||
session?: string;
|
||||
ns?: string;
|
||||
value?: string;
|
||||
out?: string;
|
||||
err?: string;
|
||||
ex?: string;
|
||||
status?: string[];
|
||||
}
|
||||
|
||||
interface ConnectOptions {
|
||||
port: number;
|
||||
host?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connection to an nREPL server.
|
||||
*/
|
||||
function connect(options: ConnectOptions): NreplConnection;
|
||||
|
||||
export default { connect };
|
||||
}
|
||||
16
mcp/pnpm-lock.yaml
generated
16
mcp/pnpm-lock.yaml
generated
@ -60,6 +60,9 @@ importers:
|
||||
js-yaml:
|
||||
specifier: ^4.1.1
|
||||
version: 4.1.1
|
||||
nrepl-client:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
penpot-mcp:
|
||||
specifier: file:..
|
||||
version: packages@file:packages
|
||||
@ -881,6 +884,9 @@ packages:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
bencode@2.0.3:
|
||||
resolution: {integrity: sha512-D/vrAD4dLVX23NalHwb8dSvsUsxeRPO8Y7ToKA015JQYq69MLDOMkC0uGZYA/MPpltLO8rt8eqFC2j8DxjTZ/w==}
|
||||
|
||||
body-parser@2.2.2:
|
||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -1221,6 +1227,9 @@ packages:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
nrepl-client@0.3.0:
|
||||
resolution: {integrity: sha512-EcROXUrzlGHKOdu/E/5WB0OESCI0iGHhdXeYk9cULYtd72eFJrM/Q1umvjTBfKWlT62y76cnyLG/3CmSCqT12w==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -2092,6 +2101,8 @@ snapshots:
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
bencode@2.0.3: {}
|
||||
|
||||
body-parser@2.2.2:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@ -2452,6 +2463,11 @@ snapshots:
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
nrepl-client@0.3.0:
|
||||
dependencies:
|
||||
bencode: 2.0.3
|
||||
tree-kill: 1.2.2
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
6
mcp/scripts/start-mcp-devenv
Executable file
6
mcp/scripts/start-mcp-devenv
Executable file
@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This starts the MCP server in a configuration for Penpot development
|
||||
# (assuming devenv)
|
||||
|
||||
PENPOT_MCP_SERVER_HOST=0.0.0.0 PENPOT_MCP_REMOTE_MODE=true PENPOT_MCP_DEVENV=true pnpm run bootstrap
|
||||
Loading…
x
Reference in New Issue
Block a user