diff --git a/mcp/README.md b/mcp/README.md
index 539266ee15..f74a89b6ae 100644
--- a/mcp/README.md
+++ b/mcp/README.md
@@ -302,5 +302,9 @@ you may set the following environment variables to configure the two servers
* The [contribution guidelines for Penpot](../CONTRIBUTING.md) apply
* Auto-formatting: Use `pnpm run fmt`
* Generating API type data: See [types-generator/README.md](types-generator/README.md)
+* Versioning: Use `bash scripts/set-version` to set the version for the MCP package (in `package.json`).
+ - Ensure that at least the major, minor and patch components of the version are always up-to-date.
+ - The MCP plugin assumes that a mismatch between the MCP version and the Penpot version (as returned by the API)
+ indicates incompatibility, resulting in the display of a warning message in the plugin UI.
* Packaging and publishing:
- Create npm package: `bash scripts/pack` (sets version and then calls `npm pack`)
diff --git a/mcp/packages/plugin/index.html b/mcp/packages/plugin/index.html
index fa573c1d0e..de2ff5853c 100644
--- a/mcp/packages/plugin/index.html
+++ b/mcp/packages/plugin/index.html
@@ -7,6 +7,10 @@
+
+
+
+
Not connected
diff --git a/mcp/packages/plugin/src/index.d.ts b/mcp/packages/plugin/src/index.d.ts
index a0eda651e1..42587c8304 100644
--- a/mcp/packages/plugin/src/index.d.ts
+++ b/mcp/packages/plugin/src/index.d.ts
@@ -1,3 +1,12 @@
+import "@penpot/plugin-types";
+
+declare module "@penpot/plugin-types" {
+ interface Penpot {
+ /** The Penpot application version string. */
+ version: string;
+ }
+}
+
interface McpOptions {
getToken(): string;
getServerUrl(): string;
diff --git a/mcp/packages/plugin/src/main.ts b/mcp/packages/plugin/src/main.ts
index e721689563..40b5bd7ba8 100644
--- a/mcp/packages/plugin/src/main.ts
+++ b/mcp/packages/plugin/src/main.ts
@@ -14,6 +14,8 @@ const executedCodeEl = document.getElementById("executed-code") as HTMLTextAreaE
const copyCodeBtn = document.getElementById("copy-code-btn") as HTMLButtonElement;
const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement;
const disconnectBtn = document.getElementById("disconnect-btn") as HTMLButtonElement;
+const versionWarningEl = document.getElementById("version-warning") as HTMLElement;
+const versionWarningTextEl = document.getElementById("version-warning-text") as HTMLElement;
/**
* Updates the status pill and button visibility based on connection state.
@@ -177,6 +179,15 @@ window.addEventListener("message", (event) => {
if (event.data.type === "start-server") {
connectToMcpServer(event.data.url, event.data.token);
}
+ if (event.data.type === "version-mismatch") {
+ if (versionWarningEl && versionWarningTextEl) {
+ versionWarningTextEl.innerHTML =
+ `Version mismatch detected: This version of the MCP server is intended for Penpot ` +
+ `${event.data.mcpVersion} while the current version is ${event.data.penpotVersion}. ` +
+ `Executions may not work or produce suboptimal results.`;
+ versionWarningEl.hidden = false;
+ }
+ }
if (event.data.type === "stop-server") {
ws?.close();
} else if (event.data.source === "penpot") {
diff --git a/mcp/packages/plugin/src/plugin.ts b/mcp/packages/plugin/src/plugin.ts
index e2b5bee38e..3827db70eb 100644
--- a/mcp/packages/plugin/src/plugin.ts
+++ b/mcp/packages/plugin/src/plugin.ts
@@ -1,6 +1,17 @@
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
import { Task, TaskHandler } from "./TaskHandler";
+/**
+ * Extracts the major.minor.patch prefix from a version string.
+ *
+ * @param version - a version string starting with major.minor.patch
+ * @returns the major.minor.patch prefix, or the original string if it does not match
+ */
+function extractVersionPrefix(version: string): string {
+ const match = version.match(/^(\d+\.\d+\.\d+)/);
+ return match ? match[1] : version;
+}
+
mcp?.setMcpStatus("connecting");
/**
@@ -15,18 +26,33 @@ penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`, {
hidden: !!mcp,
} as any);
-// Handle messages
+// Register message handlers
penpot.ui.onMessage((message) => {
- // Handle plugin task requests
- if (mcp && typeof message === "object" && message.type === "ui-initialized") {
- penpot.ui.sendMessage({
- type: "start-server",
- url: mcp?.getServerUrl(),
- token: mcp?.getToken(),
- });
+ if (typeof message === "object" && message.type === "ui-initialized") {
+ // Check Penpot version compatibility
+ const penpotVersionPrefix = penpot.version ? extractVersionPrefix(penpot.version) : "<2.15"; // pre-2.15 versions don't have version info
+ const mcpVersionPrefix = extractVersionPrefix(PENPOT_MCP_VERSION);
+ console.log(`Penpot version: ${penpotVersionPrefix}, MCP version: ${mcpVersionPrefix}`);
+ const isLocalPenpotVersion = penpotVersionPrefix == "0.0.0";
+ if (penpotVersionPrefix !== mcpVersionPrefix && !isLocalPenpotVersion) {
+ penpot.ui.sendMessage({
+ type: "version-mismatch",
+ mcpVersion: mcpVersionPrefix,
+ penpotVersion: penpotVersionPrefix,
+ });
+ }
+ // Initiate connection to remote MCP server (if enabled)
+ if (mcp) {
+ penpot.ui.sendMessage({
+ type: "start-server",
+ url: mcp?.getServerUrl(),
+ token: mcp?.getToken(),
+ });
+ }
} else if (typeof message === "object" && message.type === "update-connection-status") {
mcp?.setMcpStatus(message.status || "unknown");
} else if (typeof message === "object" && message.task && message.id) {
+ // Handle plugin tasks submitted by the MCP server
handlePluginTaskRequest(message).catch((error) => {
console.error("Error in handlePluginTaskRequest:", error);
});
diff --git a/mcp/packages/plugin/src/style.css b/mcp/packages/plugin/src/style.css
index 7061657b33..53e0a9da3d 100644
--- a/mcp/packages/plugin/src/style.css
+++ b/mcp/packages/plugin/src/style.css
@@ -169,6 +169,18 @@ details[open] > .collapsible-header .collapsible-arrow {
border-color: var(--accent-primary);
}
+/* ── Version warning ─────────────────────────────────────────────── */
+
+.version-warning {
+ align-items: flex-start;
+ padding: var(--spacing-8) var(--spacing-12);
+ border-radius: var(--spacing-8);
+ border: 1px solid var(--warning-500, #f59e0b);
+ color: var(--warning-500, #f59e0b);
+ width: 100%;
+ box-sizing: border-box;
+}
+
/* ── Action buttons ──────────────────────────────────────────────── */
#connect-btn,