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,