Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
11 changes: 11 additions & 0 deletions packages/parity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<org>/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
Expand Down
3 changes: 2 additions & 1 deletion packages/parity/docs/migrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://<studio host>
# 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/<org>/mcp/self
export PARITY_STUDIO_TOKEN=<token>
parity plan board --dir <target>/.parity --board studio
```
Expand Down
2 changes: 1 addition & 1 deletion packages/parity/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
15 changes: 13 additions & 2 deletions packages/parity/src/board/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/<org>/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`;
}

/**
Expand Down Expand Up @@ -103,7 +114,7 @@ export const callTool: ToolCaller = async <T>(
name: string,
args: unknown,
): Promise<T> => {
const res = await fetch(`${cfg.url}/mcp/self`, {
const res = await fetch(cfg.url, {
method: "POST",
headers: {
"content-type": "application/json",
Expand Down
27 changes: 25 additions & 2 deletions packages/parity/tests/board/studio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
cardDescription,
cardTitle,
fetchClientNotes,
mcpEndpoint,
parseRpcBody,
postParityComment,
studioConfigFromEnv,
Expand Down Expand Up @@ -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");
});
});

Expand Down Expand Up @@ -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);
});
});
Loading