From 67573ac86aaca5ee67c3c34643686a64c79c3ecc Mon Sep 17 00:00:00 2001 From: Jonas Jesus Date: Mon, 24 Aug 2026 16:11:28 -0300 Subject: [PATCH] fix(board): PARITY_STUDIO_URL accepts a full MCP endpoint (v0.28.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Studio deployments are org-scoped (https://studio.decocms.com/api//mcp/self) and the client appended /mcp/self to whatever was set, hitting the root path, which 404s on a real deployment. Use the value as-is when it already points at an /mcp/ endpoint; only append for a bare host. On an org-scoped URL the organization comes from the URL, not only the token — corrects the earlier note. Verified against the live Electrolux org: TASK_BOARD_ITEM_LIST returns {items: [], repos: ["ELX-LATAM-DevOps/electrolux-poc"]}. Co-Authored-By: Claude Opus 5 (1M context) --- .claude-plugin/plugin.json | 2 +- packages/parity/CHANGELOG.md | 11 +++++++++ packages/parity/docs/migrate.md | 3 ++- packages/parity/package.json | 2 +- packages/parity/src/board/studio.ts | 15 ++++++++++-- packages/parity/tests/board/studio.test.ts | 27 ++++++++++++++++++++-- 6 files changed, 53 insertions(+), 7 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 5006955..2bbed37 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "parity", "description": "Orchestrates site migrations (VTEX IO → FastStore v4, deco/Fresh → TanStack) end-to-end: capture prod, reconcile against the candidate, port components, measure parity score, file issues, fix, repeat.", - "version": "0.28.0", + "version": "0.28.1", "author": { "name": "deco CMS", "email": "hi@deco.cx" diff --git a/packages/parity/CHANGELOG.md b/packages/parity/CHANGELOG.md index 62661e7..dcae719 100644 --- a/packages/parity/CHANGELOG.md +++ b/packages/parity/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [0.28.1] — 2026-08-24 + +### Fixed + +* **`PARITY_STUDIO_URL` accepts a full MCP endpoint.** Studio deployments are + org-scoped (`https://studio.decocms.com/api//mcp/self`), and the client + appended `/mcp/self` to whatever was set — hitting the root path instead, which + 404s on a real deployment. It now uses the value as-is when it already points at + an `/mcp/` endpoint and only appends for a bare host. Note that on an org-scoped + URL the **organization comes from the URL**, not only from the token. + ## [0.28.0] — 2026-08-24 The Studio board stops being a one-way report: the client can now see the fixes diff --git a/packages/parity/docs/migrate.md b/packages/parity/docs/migrate.md index e5028ae..9192e89 100644 --- a/packages/parity/docs/migrate.md +++ b/packages/parity/docs/migrate.md @@ -325,7 +325,8 @@ The board covers the pages the capture **sampled**, not every URL on the site. #### Mirroring the board into the deco Studio ```bash -export PARITY_STUDIO_URL=https:// +# Full org-scoped endpoint (the org lives in the URL), or a bare host for the root endpoint: +export PARITY_STUDIO_URL=https://studio.decocms.com/api//mcp/self export PARITY_STUDIO_TOKEN= parity plan board --dir /.parity --board studio ``` diff --git a/packages/parity/package.json b/packages/parity/package.json index 36ea77b..d9fc7d8 100644 --- a/packages/parity/package.json +++ b/packages/parity/package.json @@ -1,6 +1,6 @@ { "name": "@decocms/parity", - "version": "0.28.0", + "version": "0.28.1", "description": "E2E parity validator for site migrations. Compares prod vs cand and reports UI, functional, SEO, visual, and Web Vitals deltas with an LLM-ranked HTML report.", "type": "module", "license": "MIT", diff --git a/packages/parity/src/board/studio.ts b/packages/parity/src/board/studio.ts index 0ed1b02..7ba6a3d 100644 --- a/packages/parity/src/board/studio.ts +++ b/packages/parity/src/board/studio.ts @@ -41,7 +41,18 @@ export function studioConfigFromEnv(env: NodeJS.ProcessEnv = process.env): Studi const url = env.PARITY_STUDIO_URL; const token = env.PARITY_STUDIO_TOKEN; if (!url || !token) return null; - return { url: url.replace(/\/+$/, ""), token }; + return { url: mcpEndpoint(url), token }; +} + +/** + * Accept either a host or a full MCP endpoint. Deployments are org-scoped + * (`https://studio.decocms.com/api//mcp/self`), so appending `/mcp/self` to whatever the + * user pasted would hit the wrong path — and there the ORG lives in the URL, not only in the + * token. A bare host still gets the root endpoint appended. + */ +export function mcpEndpoint(url: string): string { + const trimmed = url.replace(/\/+$/, ""); + return /\/mcp(\/|$)/.test(trimmed) ? trimmed : `${trimmed}/mcp/self`; } /** @@ -103,7 +114,7 @@ export const callTool: ToolCaller = async ( name: string, args: unknown, ): Promise => { - const res = await fetch(`${cfg.url}/mcp/self`, { + const res = await fetch(cfg.url, { method: "POST", headers: { "content-type": "application/json", diff --git a/packages/parity/tests/board/studio.test.ts b/packages/parity/tests/board/studio.test.ts index 392b8bf..67e994b 100644 --- a/packages/parity/tests/board/studio.test.ts +++ b/packages/parity/tests/board/studio.test.ts @@ -7,6 +7,7 @@ import { cardDescription, cardTitle, fetchClientNotes, + mcpEndpoint, parseRpcBody, postParityComment, studioConfigFromEnv, @@ -63,12 +64,20 @@ describe("studioConfigFromEnv", () => { expect(studioConfigFromEnv({ PARITY_STUDIO_TOKEN: "y" } as NodeJS.ProcessEnv)).toBeNull(); }); - it("tira a barra final da url", () => { + it("completa um host simples com o endpoint raiz", () => { const c = studioConfigFromEnv({ PARITY_STUDIO_URL: "https://s.example/", PARITY_STUDIO_TOKEN: "y", } as NodeJS.ProcessEnv); - expect(c).toEqual({ url: "https://s.example", token: "y" }); + expect(c).toEqual({ url: "https://s.example/mcp/self", token: "y" }); + }); + + it("respeita um endpoint org-scoped completo — a org vive na URL, não só no token", () => { + const c = studioConfigFromEnv({ + PARITY_STUDIO_URL: "https://studio.decocms.com/api/electrolux/mcp/self", + PARITY_STUDIO_TOKEN: "y", + } as NodeJS.ProcessEnv); + expect(c?.url).toBe("https://studio.decocms.com/api/electrolux/mcp/self"); }); }); @@ -228,3 +237,17 @@ describe("fetchClientNotes / postParityComment", () => { expect(body.startsWith(PARITY_COMMENT_PREFIX)).toBe(true); }); }); + +describe("mcpEndpoint", () => { + it.each([ + ["https://studio.decocms.com", "https://studio.decocms.com/mcp/self"], + ["https://studio.decocms.com/", "https://studio.decocms.com/mcp/self"], + [ + "https://studio.decocms.com/api/electrolux/mcp/self", + "https://studio.decocms.com/api/electrolux/mcp/self", + ], + ["https://studio.decocms.com/api/org/mcp/self/", "https://studio.decocms.com/api/org/mcp/self"], + ])("%s -> %s", (input, expected) => { + expect(mcpEndpoint(input)).toBe(expected); + }); +});