diff --git a/mcp/README.md b/mcp/README.md index bc13252c16..32530bc869 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -309,9 +309,10 @@ you may set the following environment variables to configure the two servers * 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. + - Ensure that at least the major and minor components of the version are always up-to-date. + - The MCP plugin assumes that a mismatch between the major.minor components of 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. Patch releases are treated as compatible. * Packaging and publishing: 1. Ensure release version is set correctly in package.json (call `bash scripts/set-version` to update it automatically) 2. Create npm package: `bash scripts/pack` (creates `penpot-mcp-.tgz` for publishing) diff --git a/mcp/packages/plugin/src/VersionCompatibility.test.ts b/mcp/packages/plugin/src/VersionCompatibility.test.ts new file mode 100644 index 0000000000..2e40bb7cf0 --- /dev/null +++ b/mcp/packages/plugin/src/VersionCompatibility.test.ts @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { extractMinorVersion, isCompatibleVersion } from "./VersionCompatibility.ts"; + +test("extracts major.minor from release and pre-release versions", () => { + assert.equal(extractMinorVersion("2.18.1"), "2.18"); + assert.equal(extractMinorVersion("2.17.0"), "2.17"); + assert.equal(extractMinorVersion("2.15.0-rc.1.116"), "2.15"); + assert.equal(extractMinorVersion("2.18.1-dev.140"), "2.18"); + assert.equal(extractMinorVersion("2.18"), "2.18"); +}); + +test("returns non-matching versions unchanged", () => { + assert.equal(extractMinorVersion("<2.15"), "<2.15"); + assert.equal(extractMinorVersion("develop"), "develop"); +}); + +test("treats patch releases of the same minor version as compatible", () => { + assert.equal(isCompatibleVersion(extractMinorVersion("2.17.2"), extractMinorVersion("2.17.0")), true); +}); + +test("reports a different minor version as incompatible", () => { + assert.equal(isCompatibleVersion(extractMinorVersion("2.18.1"), extractMinorVersion("2.17.0")), false); +}); + +test("treats local development builds of Penpot as compatible", () => { + assert.equal(isCompatibleVersion(extractMinorVersion("0.0.0"), extractMinorVersion("2.17.0")), true); +}); diff --git a/mcp/packages/plugin/src/VersionCompatibility.ts b/mcp/packages/plugin/src/VersionCompatibility.ts new file mode 100644 index 0000000000..f529491ea3 --- /dev/null +++ b/mcp/packages/plugin/src/VersionCompatibility.ts @@ -0,0 +1,27 @@ +/** + * Extracts the major.minor prefix from a version string. + * + * Patch releases of Penpot do not change the plugin API the MCP server is + * written against, and the MCP package version is only set per minor + * release, so the patch component is not part of the compatibility check. + * + * @param version - a version string starting with major.minor (e.g. "2.18.1") + * @returns the major.minor prefix, or the original string if it does not match + */ +export function extractMinorVersion(version: string): string { + const match = version.match(/^(\d+\.\d+)(?:\.|$)/); + return match ? match[1] : version; +} + +/** + * Returns whether an MCP build is intended for the running Penpot version. + * + * Local development builds of Penpot report "0.0.0" and are always treated + * as compatible. + * + * @param penpotVersion - the running Penpot version, as major.minor prefix + * @param mcpVersion - the version the MCP build targets, as major.minor prefix + */ +export function isCompatibleVersion(penpotVersion: string, mcpVersion: string): boolean { + return penpotVersion === "0.0" || penpotVersion === mcpVersion; +} diff --git a/mcp/packages/plugin/src/plugin.ts b/mcp/packages/plugin/src/plugin.ts index 502116707f..60f09ccc03 100644 --- a/mcp/packages/plugin/src/plugin.ts +++ b/mcp/packages/plugin/src/plugin.ts @@ -1,6 +1,7 @@ import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler"; import { Task, TaskHandler } from "./TaskHandler"; import { formatTaskError } from "./ErrorUtils"; +import { extractMinorVersion, isCompatibleVersion } from "./VersionCompatibility"; /** * indicates whether the plugin is running in an environment with the Penpot-integrated remote MCP server @@ -8,17 +9,6 @@ import { formatTaskError } from "./ErrorUtils"; */ const isIntegratedRemoteMcp = !!mcp; -/** - * 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"); /** @@ -42,11 +32,10 @@ penpot.ui.onMessage