From 8888540601bc80e02882476380b2ca36d0d208f9 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Mon, 20 Jul 2026 15:26:58 +0100 Subject: [PATCH 01/14] feat: create and bind text assets --- .../app/builder/features/assets/assets.tsx | 24 ++- .../settings-panel/resource-panel.tsx | 30 +++- .../create-text-file-dialog.test.ts | 35 ++++ .../create-text-file-dialog.tsx | 170 ++++++++++++++++++ .../asset-manager-item-menu.test.ts | 4 + .../asset-manager/asset-manager-item-menu.tsx | 2 + .../shared/asset-manager/asset-manager.tsx | 2 +- .../builder/shared/assets/asset-utils.test.ts | 11 ++ .../app/builder/shared/assets/asset-utils.ts | 3 + .../shared/assets/replace-asset.test.ts | 6 +- .../builder/shared/assets/replace-asset.ts | 6 +- .../builder/shared/assets/upload-assets.tsx | 4 +- .../app/routes/rest.resources-loader.ts | 2 +- .../app/shared/$resources/assets.server.ts | 25 ++- apps/builder/app/shared/resources.ts | 21 +-- packages/cli/src/docs.generated.ts | 4 +- packages/cli/src/docs/api-use-cases.md | 15 ++ packages/cli/src/docs/manual-mcp.md | 1 + packages/cli/src/prebuild.test.ts | 3 + packages/cli/src/prebuild.ts | 8 +- .../defaults/app/route-templates/html.tsx | 13 +- .../defaults/app/route-templates/text.tsx | 18 +- .../defaults/app/route-templates/xml.tsx | 18 +- .../react-router/app/route-templates/html.tsx | 13 +- .../react-router/app/route-templates/text.tsx | 18 +- .../react-router/app/route-templates/xml.tsx | 18 +- .../ssg/app/route-templates/html/+data.ts | 18 +- packages/react-sdk/placeholder.d.ts | 4 +- packages/sdk/src/asset-resource.test.ts | 148 +++++++++++++++ packages/sdk/src/asset-resource.ts | 130 ++++++++++++++ packages/sdk/src/index.ts | 1 + packages/sdk/src/resource-loader.test.ts | 25 ++- packages/sdk/src/resource-loader.ts | 13 +- packages/sdk/src/runtime.ts | 1 + 34 files changed, 733 insertions(+), 81 deletions(-) create mode 100644 apps/builder/app/builder/features/text-file-editor/create-text-file-dialog.test.ts create mode 100644 apps/builder/app/builder/features/text-file-editor/create-text-file-dialog.tsx create mode 100644 packages/sdk/src/asset-resource.test.ts create mode 100644 packages/sdk/src/asset-resource.ts diff --git a/apps/builder/app/builder/features/assets/assets.tsx b/apps/builder/app/builder/features/assets/assets.tsx index 4c6cec0011d0..f9243c5a69dd 100644 --- a/apps/builder/app/builder/features/assets/assets.tsx +++ b/apps/builder/app/builder/features/assets/assets.tsx @@ -4,7 +4,11 @@ import { Separator, Tooltip, } from "@webstudio-is/design-system"; -import { BrushCleaningIcon, NewFolderIcon } from "@webstudio-is/icons"; +import { + BrushCleaningIcon, + NewFolderIcon, + NewPageIcon, +} from "@webstudio-is/icons"; import { useRef, useState } from "react"; import { useStore } from "@nanostores/react"; import { isTextFileAsset } from "@webstudio-is/sdk"; @@ -17,6 +21,7 @@ import { $assets } from "~/shared/sync/data-stores"; import type { Publish } from "~/shared/pubsub"; import { useImageAssetCanvasDrag } from "./use-image-asset-canvas-drag"; import { TextFileEditor } from "~/builder/features/text-file-editor/text-file-editor"; +import { CreateTextFileDialog } from "~/builder/features/text-file-editor/create-text-file-dialog"; import { getAssetUrl } from "~/builder/shared/assets/asset-utils"; export const AssetsPanel = ({ @@ -27,6 +32,7 @@ export const AssetsPanel = ({ }) => { const [folderId, setFolderId] = useState(); const [createFolderOpen, setCreateFolderOpen] = useState(false); + const [createTextFileOpen, setCreateTextFileOpen] = useState(false); const [openedTextAssetId, setOpenedTextAssetId] = useState(); const uploadRef = useRef(null); const authPermit = useStore($authPermit); @@ -60,6 +66,15 @@ export const AssetsPanel = ({ + + setCreateTextFileOpen(true)} + > + + + @@ -82,6 +97,7 @@ export const AssetsPanel = ({ ? {} : { createFolder: () => setCreateFolderOpen(true), + createFile: () => setCreateTextFileOpen(true), upload: () => uploadRef.current?.open(), }), deleteUnusedAssets: openDeleteUnusedAssetsDialog, @@ -92,6 +108,12 @@ export const AssetsPanel = ({ onOpenChange={setCreateFolderOpen} currentFolderId={folderId} /> + {openedTextAssetId !== undefined && ( (({ variable }, ref) => { + const { scope, aliases } = useResourceScope({ variable }); const resources = useStore($resources); const resource = @@ -946,6 +949,9 @@ export const SystemResourceForm = forwardRef< ) ?? localResources[0] ); }); + const [searchParams, setSearchParams] = useState( + resource?.searchParams ?? [] + ); useImperativeHandle(ref, () => ({ save: (formData) => { @@ -992,8 +998,30 @@ export const SystemResourceForm = forwardRef< ); }} value={localResource} - onChange={setLocalResource} + onChange={(value) => { + setLocalResource(value); + if (value.value !== resource?.url) { + setSearchParams([]); + } + }} /> + {localResource.value === JSON.stringify(assetsResourceUrl) && ( + <> + + Filter with filename or extension. Use comma-separated extensions + and include=content to load editable text. Content is limited to{" "} + {maxAssetResourceContentItems} assets and{" "} + {maxAssetResourceContentBytes / 1024} KiB per asset. JSON content + is exposed as structured data. + + + + )} ); diff --git a/apps/builder/app/builder/features/text-file-editor/create-text-file-dialog.test.ts b/apps/builder/app/builder/features/text-file-editor/create-text-file-dialog.test.ts new file mode 100644 index 000000000000..908c0377d756 --- /dev/null +++ b/apps/builder/app/builder/features/text-file-editor/create-text-file-dialog.test.ts @@ -0,0 +1,35 @@ +import { expect, test } from "vitest"; +import type { Asset } from "@webstudio-is/sdk"; +import { getTextFileNameError } from "./create-text-file-dialog"; + +const existing: Asset = { + id: "existing", + projectId: "project", + name: "readme_hash.md", + filename: "Readme.md", + folderId: "docs", + type: "file", + format: "md", + size: 0, + createdAt: "2026-07-20T00:00:00.000Z", + meta: {}, +}; + +test("accepts supported text names and rejects invalid or duplicate names", () => { + expect( + getTextFileNameError({ name: "data.json", assets: [existing] }) + ).toBeUndefined(); + expect(getTextFileNameError({ name: "image.png", assets: [] })).toBe( + "Use a supported editable text extension." + ); + expect(getTextFileNameError({ name: "bad/name.md", assets: [] })).toBe( + "Enter a valid file name." + ); + expect( + getTextFileNameError({ + name: "readme.md", + folderId: "docs", + assets: [existing], + }) + ).toBe("A file with this name already exists here."); +}); diff --git a/apps/builder/app/builder/features/text-file-editor/create-text-file-dialog.tsx b/apps/builder/app/builder/features/text-file-editor/create-text-file-dialog.tsx new file mode 100644 index 000000000000..3633337cd821 --- /dev/null +++ b/apps/builder/app/builder/features/text-file-editor/create-text-file-dialog.tsx @@ -0,0 +1,170 @@ +import { useLayoutEffect, useState, type KeyboardEvent } from "react"; +import isValidFilename from "valid-filename"; +import { + Button, + Dialog, + DialogContent, + DialogTitle, + Flex, + Grid, + InputField, + Label, + Text, + theme, +} from "@webstudio-is/design-system"; +import { + getMimeTypeByExtension, + isTextFileAsset, + type Asset, +} from "@webstudio-is/sdk"; +import { $assets } from "~/shared/sync/data-stores"; +import { uploadAssets } from "~/builder/shared/assets/upload-assets"; +import { waitForAsset } from "~/builder/shared/assets/replace-asset"; + +const getFormat = (name: string) => name.split(".").at(-1)?.toLowerCase() ?? ""; + +export const getTextFileNameError = ({ + name, + folderId, + assets, +}: { + name: string; + folderId?: string; + assets: Iterable; +}) => { + if (isValidFilename(name) === false) { + return "Enter a valid file name."; + } + if (isTextFileAsset({ format: getFormat(name) }) === false) { + return "Use a supported editable text extension."; + } + const duplicate = Array.from(assets).some( + (asset) => + asset.folderId === folderId && + (asset.filename ?? asset.name).toLowerCase() === name.toLowerCase() + ); + if (duplicate) { + return "A file with this name already exists here."; + } +}; + +export const createTextFile = async ({ + name, + folderId, +}: { + name: string; + folderId?: string; +}): Promise => { + const format = getFormat(name); + if (isTextFileAsset({ format }) === false) { + return; + } + const file = new File([""], name, { + type: getMimeTypeByExtension(format), + }); + const assetId = (await uploadAssets("file", [file], { folderId })).get(file); + return assetId === undefined ? undefined : await waitForAsset(assetId); +}; + +const stopEscapePropagation = (event: KeyboardEvent) => { + if (event.key === "Escape") { + event.stopPropagation(); + } +}; + +export const CreateTextFileDialog = ({ + open, + folderId, + onOpenChange, + onCreated, +}: { + open: boolean; + folderId?: string; + onOpenChange: (open: boolean) => void; + onCreated: (assetId: string) => void; +}) => { + const [name, setName] = useState("untitled.md"); + const [error, setError] = useState(); + const [creating, setCreating] = useState(false); + + useLayoutEffect(() => { + if (open) { + setName("untitled.md"); + setError(undefined); + setCreating(false); + } + }, [open]); + + const normalizedName = name.trim(); + const submit = async () => { + const validationError = getTextFileNameError({ + name: normalizedName, + folderId, + assets: $assets.get().values(), + }); + setError(validationError); + if (validationError !== undefined) { + return; + } + setCreating(true); + try { + const asset = await createTextFile({ name: normalizedName, folderId }); + if (asset === undefined) { + setError("The file could not be created."); + return; + } + onOpenChange(false); + onCreated(asset.id); + } catch (error) { + setError( + error instanceof Error + ? error.message + : "The file could not be created." + ); + } finally { + setCreating(false); + } + }; + + return ( + + + New text file + + + + { + setName(event.target.value); + setError(undefined); + }} + onKeyDown={(event) => { + if (event.key === "Enter") { + void submit(); + } + }} + /> + {error !== undefined && ( + + {error} + + )} + + + + + + + + ); +}; diff --git a/apps/builder/app/builder/shared/asset-manager/asset-manager-item-menu.test.ts b/apps/builder/app/builder/shared/asset-manager/asset-manager-item-menu.test.ts index 7e99d4509c53..93dfdb0f833f 100644 --- a/apps/builder/app/builder/shared/asset-manager/asset-manager-item-menu.test.ts +++ b/apps/builder/app/builder/shared/asset-manager/asset-manager-item-menu.test.ts @@ -7,6 +7,7 @@ test("uses one ordered command model for context and dropdown menus", () => { open: action, settings: action, createFolder: action, + createFile: action, upload: action, cut: action, copy: action, @@ -21,6 +22,7 @@ test("uses one ordered command model for context and dropdown menus", () => { expect(items.map(({ name }) => name)).toEqual([ "createFolder", + "createFile", "upload", "open", "settings", @@ -60,6 +62,7 @@ test("orders panel actions independently of unavailable item actions", () => { const action = vi.fn(); const items = getAssetManagerItemMenuItems({ createFolder: action, + createFile: action, upload: action, paste: action, deleteUnusedAssets: action, @@ -67,6 +70,7 @@ test("orders panel actions independently of unavailable item actions", () => { expect(items.map(({ name }) => name)).toEqual([ "createFolder", + "createFile", "upload", "paste", "deleteUnusedAssets", diff --git a/apps/builder/app/builder/shared/asset-manager/asset-manager-item-menu.tsx b/apps/builder/app/builder/shared/asset-manager/asset-manager-item-menu.tsx index b6ef0127e1b4..8682d73f74c2 100644 --- a/apps/builder/app/builder/shared/asset-manager/asset-manager-item-menu.tsx +++ b/apps/builder/app/builder/shared/asset-manager/asset-manager-item-menu.tsx @@ -28,6 +28,7 @@ export type AssetManagerItemActions = Partial< | "download" | "replace" | "createFolder" + | "createFile" | "upload" | "deleteUnusedAssets" | "delete", @@ -46,6 +47,7 @@ type ItemDefinition = { const itemDefinitions: readonly ItemDefinition[] = [ { name: "createFolder", label: "Create folder" }, + { name: "createFile", label: "Create text file" }, { name: "upload", label: "Upload asset" }, { name: "open", label: "Open" }, { name: "settings", label: "Settings" }, diff --git a/apps/builder/app/builder/shared/asset-manager/asset-manager.tsx b/apps/builder/app/builder/shared/asset-manager/asset-manager.tsx index 7f816bcbd858..0b9c5facc6cb 100644 --- a/apps/builder/app/builder/shared/asset-manager/asset-manager.tsx +++ b/apps/builder/app/builder/shared/asset-manager/asset-manager.tsx @@ -110,7 +110,7 @@ type AssetManagerProps = FolderNavigationProps & { panelActions?: Partial< Pick< AssetManagerItemActions, - "createFolder" | "upload" | "deleteUnusedAssets" + "createFolder" | "createFile" | "upload" | "deleteUnusedAssets" > >; }; diff --git a/apps/builder/app/builder/shared/assets/asset-utils.test.ts b/apps/builder/app/builder/shared/assets/asset-utils.test.ts index ad88e39da2a1..bdb9458e2d12 100644 --- a/apps/builder/app/builder/shared/assets/asset-utils.test.ts +++ b/apps/builder/app/builder/shared/assets/asset-utils.test.ts @@ -2,10 +2,21 @@ import { expect, test, describe } from "vitest"; import { getImageNameAndType, getSha256Hash, + getFileUploadFingerprint, detectAssetType, uploadingFileDataToAsset, } from "./asset-utils"; +test("distinguishes upload fingerprints for equal files with different names", async () => { + const first = new File([""], "first.md"); + const second = new File([""], "second.md"); + + const firstFingerprint = await getFileUploadFingerprint(first); + const secondFingerprint = await getFileUploadFingerprint(second); + + expect(firstFingerprint).not.toBe(secondFingerprint); +}); + describe("getImageNameAndType", () => { test("returns MIME type and filename for valid image", () => { const result = getImageNameAndType("photo.jpg"); diff --git a/apps/builder/app/builder/shared/assets/asset-utils.ts b/apps/builder/app/builder/shared/assets/asset-utils.ts index 6f7e767b84c1..5d2de2e1cac5 100644 --- a/apps/builder/app/builder/shared/assets/asset-utils.ts +++ b/apps/builder/app/builder/shared/assets/asset-utils.ts @@ -83,6 +83,9 @@ export const getSha256HashOfFile = async (file: File) => { return bufferToHex(hashBuffer); }; +export const getFileUploadFingerprint = async (file: File) => + JSON.stringify([file.name, await getSha256HashOfFile(file)]); + export const getMimeType = (file: File | URL) => { if (file instanceof File) { return file.type; diff --git a/apps/builder/app/builder/shared/assets/replace-asset.test.ts b/apps/builder/app/builder/shared/assets/replace-asset.test.ts index d6c006c083db..ca393d8c97de 100644 --- a/apps/builder/app/builder/shared/assets/replace-asset.test.ts +++ b/apps/builder/app/builder/shared/assets/replace-asset.test.ts @@ -2,9 +2,7 @@ import { beforeEach, expect, test } from "vitest"; import type { Asset } from "@webstudio-is/sdk"; import { $uploadingFilesDataStore } from "~/shared/nano-states"; import { $assets } from "~/shared/sync/data-stores"; -import { __testing__ } from "./replace-asset"; - -const { waitForAsset } = __testing__; +import { waitForAsset } from "./replace-asset"; const asset = { id: "replacement", @@ -43,5 +41,5 @@ test("rejects when a replacement upload fails", async () => { $uploadingFilesDataStore.set([]); - await expect(result).rejects.toThrow("Failed to upload replacement asset"); + await expect(result).rejects.toThrow("Failed to upload asset"); }); diff --git a/apps/builder/app/builder/shared/assets/replace-asset.ts b/apps/builder/app/builder/shared/assets/replace-asset.ts index 1c6be26e2327..76f4c2d9f80c 100644 --- a/apps/builder/app/builder/shared/assets/replace-asset.ts +++ b/apps/builder/app/builder/shared/assets/replace-asset.ts @@ -68,7 +68,7 @@ export const replaceAsset = async ( toast.success("Asset replaced successfully"); }; -const waitForAsset = (assetId: string): Promise => { +export const waitForAsset = (assetId: string): Promise => { // Check if asset already exists (avoids TDZ with synchronous subscribe callback) const existingAsset = $assets.get().get(assetId); if (existingAsset !== undefined) { @@ -94,7 +94,7 @@ const waitForAsset = (assetId: string): Promise => { .some((fileData) => fileData.assetId === assetId); if (isUploading === false) { cleanup(); - reject(new Error("Failed to upload replacement asset")); + reject(new Error("Failed to upload asset")); } }; const unsubscribeAssets = $assets.listen(check); @@ -102,5 +102,3 @@ const waitForAsset = (assetId: string): Promise => { check(); }); }; - -export const __testing__ = { waitForAsset }; diff --git a/apps/builder/app/builder/shared/assets/upload-assets.tsx b/apps/builder/app/builder/shared/assets/upload-assets.tsx index d2f414c195ba..040f097feebd 100644 --- a/apps/builder/app/builder/shared/assets/upload-assets.tsx +++ b/apps/builder/app/builder/shared/assets/upload-assets.tsx @@ -21,9 +21,9 @@ import { executeRuntimeMutation } from "~/shared/instance-utils/data"; import { formatAssetName } from "@webstudio-is/project-build/runtime"; import { getFileName, + getFileUploadFingerprint, getMimeType, getSha256Hash, - getSha256HashOfFile, } from "./asset-utils"; const safeDeleteAssets = (assetIds: Asset["id"][], projectId: string) => { @@ -76,7 +76,7 @@ const getFilesData = async ( const filesData: UploadingFileData[] = []; for (const fileOrUrl of filesOrUrls) { if (fileOrUrl instanceof File) { - const fingerprintId = await getSha256HashOfFile(fileOrUrl); + const fingerprintId = await getFileUploadFingerprint(fileOrUrl); filesData.push({ source: "file" as const, assetId: "", diff --git a/apps/builder/app/routes/rest.resources-loader.ts b/apps/builder/app/routes/rest.resources-loader.ts index 1d35d0bf4936..45fa0d882ff9 100644 --- a/apps/builder/app/routes/rest.resources-loader.ts +++ b/apps/builder/app/routes/rest.resources-loader.ts @@ -29,7 +29,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { } if (isLocalResource(input, "assets")) { - return assetsLoader({ request }); + return assetsLoader({ request, resourceUrl: input }); } return fetch(input, init); diff --git a/apps/builder/app/shared/$resources/assets.server.ts b/apps/builder/app/shared/$resources/assets.server.ts index bffdd290b240..6f9b824b8046 100644 --- a/apps/builder/app/shared/$resources/assets.server.ts +++ b/apps/builder/app/shared/$resources/assets.server.ts @@ -1,7 +1,10 @@ import { json } from "@remix-run/server-runtime"; import { parseBuilderUrl } from "@webstudio-is/protocol"; import { loadAssetsByProject } from "@webstudio-is/asset-uploader/index.server"; -import { toRuntimeAsset } from "@webstudio-is/sdk"; +import { + loadAssetResource, + toAssetResourceItem, +} from "@webstudio-is/sdk/runtime"; import { isBuilder } from "../router-utils"; import { createContext } from "../context.server"; @@ -9,7 +12,13 @@ import { createContext } from "../context.server"; * System Resource that provides the list of assets for the current project. * This allows assets to be dynamically referenced in the builder using the expression editor. */ -export const loader = async ({ request }: { request: Request }) => { +export const loader = async ({ + request, + resourceUrl = request.url, +}: { + request: Request; + resourceUrl?: string; +}) => { if (isBuilder(request) === false) { throw new Error( "Asset resource loader can only be accessed from the builder interface" @@ -31,11 +40,15 @@ export const loader = async ({ request }: { request: Request }) => { const requestUrl = new URL(request.url); const origin = `${requestUrl.protocol}//${requestUrl.host}`; - // Convert array to object with asset IDs as keys - // Use /cgi/ endpoint URLs (relative paths) const assetsById = Object.fromEntries( - assets.map((asset) => [asset.id, toRuntimeAsset(asset, origin)]) + assets.map((asset) => [asset.id, toAssetResourceItem(asset, origin)]) ); - return json(assetsById); + return json( + await loadAssetResource({ + assets: assetsById, + requestUrl: resourceUrl, + fetchAsset: (url) => fetch(new URL(url, origin)), + }) + ); }; diff --git a/apps/builder/app/shared/resources.ts b/apps/builder/app/shared/resources.ts index b2a6eed53b13..1f9bf056d5cc 100644 --- a/apps/builder/app/shared/resources.ts +++ b/apps/builder/app/shared/resources.ts @@ -12,6 +12,7 @@ export { getResourceKey }; const queue = new Map(); const pending = new Map(); const cache = new Map(); +const cachedRequests = new Map(); export const $resourcesCache = atom(cache); @@ -46,6 +47,10 @@ const loadResources = async () => { const results = new Map(await response.json()); for (const [key, result] of results) { cache.set(key, result); + const request = pending.get(key); + if (request !== undefined) { + cachedRequests.set(key, request); + } pending.delete(key); } } @@ -82,6 +87,7 @@ export const preloadResource = (resource: ResourceRequest) => { export const invalidateResource = (resource: ResourceRequest) => { const key = getResourceKey(resource); cache.delete(key); + cachedRequests.delete(key); preloadResource(resource); }; @@ -90,16 +96,11 @@ export const invalidateResource = (resource: ResourceRequest) => { * Call this when assets are uploaded, deleted, or modified to refresh expressions using assets. */ export const invalidateAssets = () => { - const url = "/$resources/assets"; - // System resources always use GET with no params/headers/body - const systemResourceRequest: ResourceRequest = { - name: "assets", - method: "get", - url, - searchParams: [], - headers: [], - }; - invalidateResource(systemResourceRequest); + for (const request of cachedRequests.values()) { + if (request.url === "/$resources/assets") { + invalidateResource(request); + } + } }; export const computeResourceRequest = ( diff --git a/packages/cli/src/docs.generated.ts b/packages/cli/src/docs.generated.ts index 481c3694da96..3b5572307224 100644 --- a/packages/cli/src/docs.generated.ts +++ b/packages/cli/src/docs.generated.ts @@ -2,13 +2,13 @@ // Edit markdown files in src/docs instead. export const cliDocs = { "api-use-cases": - '# CLI API Use Cases\n\n## Link/configure one project\n\nCommands:\n\n- webstudio init --link --json\n\nNotes:\n\n- Writes local project id and global origin/token config.\n\n## Import synced project bundle into another project\n\nCommands:\n\n- webstudio sync\n- webstudio import --to \n- MCP tool: import {"to":""}\n\nNotes:\n\n- Imports local `.webstudio/data.json` into the destination project.\n- Destination share link must allow build/import access.\n- Use `--skip-assets` only when asset rows and files should not be imported.\n\n## Identify current token\n\nCommands:\n\n- MCP tool: whoami {}\n\n## Check token permissions\n\nCommands:\n\n- webstudio permissions --json\n\n## Inspect project/build/version\n\nCommands:\n\n- MCP tool: inspect {}\n- MCP tool: snapshot {"include":["pages","instances","styles"]}\n\n## Discover CLI/API capabilities\n\nCommands:\n\n- webstudio schema api\n- webstudio schema mcp\n- webstudio man --json\n- webstudio man llm --json\n- MCP tool: meta.index {}\n- MCP tool: meta.guide {"brief":"Create a pricing page"}\n- MCP tool: meta.get_more_tools {"brief":"update-styles"}\n- webstudio mcp list-resources\n- webstudio mcp read-resource webstudio://project/guide\n- webstudio mcp read-resource webstudio://project/expressions\n\nNotes:\n\n- Use `webstudio schema mcp` for a compact machine-readable MCP tool overview. Add `--verbose` or use focused `meta.get_more_tools` calls only when exact input schemas are needed.\n- Use focused MCP tools for discovery first: `meta.index`, `meta.guide`, `meta.get_more_tools`, `components.list`, `components.summary`, `components.search`, `components.get`, `templates.list`, and `templates.get`. Protocol clients can use `resources/list` and `resources/read`; shell agents can use `webstudio mcp list-resources` and `webstudio mcp read-resource `. Read longer resources such as `webstudio://project/tools` and `webstudio://project/components` only when focused tools are insufficient.\n- `components.summary` returns counts by default; request `{"detail":"components","limit":20}` for paginated entries. Registry list tools return compact metadata, while `components.get` and `templates.get` return focused full details.\n- Read `webstudio://project/expressions` before authoring unfamiliar computed text, prop bindings, resource expressions, actions, or Collection item bindings.\n- From a shell, call one MCP tool with the shortcut form `webstudio \'\'`, for example `webstudio components.summary`. The explicit equivalent is `webstudio mcp single-op-call \'\'`. Use `--input-file` for large payloads.\n\n## Inspect external shadcn registry items\n\nCommands:\n\n- webstudio registry inspect --source https://example.com/r/registry.json --item button --json\n- webstudio registry inspect --source ./registry.json --item dialog --json\n- webstudio registry inspect --source https://example.com/r/button.json --json\n\nNotes:\n\n- Reads a local or remote registry item without installing files or changing the configured Webstudio project.\n- Returns the item name, description, package and registry dependencies, file paths/targets, available docs, and a read-only compatibility report.\n- The report explicitly says whether installation or editable-component conversion is supported, lists declared requirements and manual steps, and says when arbitrary source code was not analyzed.\n- This is an inspection step only. It does not install files or change the configured project.\n\n## Understand what MCP can do\n\nCommands:\n\n- webstudio man mcp\n- MCP tool: meta.index {}\n- MCP tool: meta.guide {"brief":"What can Webstudio MCP do?"}\n\nMCP lets agents work on one configured Webstudio project. Agents can:\n\n- Inspect the linked project, token permissions, and latest editable build.\n- Read selected project data for audits, migrations, and repair.\n- Search labels, text, props, resource URLs, asset metadata, and styles.\n- Audit accessibility, security, SEO, performance settings, unused assets, ineffective Collection styles, and unused or duplicate style data.\n- Create and edit pages, folders, redirects, breakpoints, and page templates.\n- Create pages from reusable templates.\n- Update page metadata, SEO fields, auth settings, and marketplace metadata.\n- Insert components and styled JSX sections.\n- Create data-driven lists, grids, cards, and similar repeated UI from array or object data in one Collection operation.\n- Move, copy, wrap, unwrap, convert, rename, retag, and delete elements.\n- Update text, rich text, props, bindings, and actions.\n- Create and update local styles, design tokens, style sources, and CSS variables.\n- Create static data variables and JSON variables.\n- Create HTTP, GraphQL, and system resources.\n- Use system resources for sitemap, current date, and assets.\n- Bind resources to rendered data or form/action props.\n- Manage nested asset folders and upload, inspect, move, duplicate, download, replace, delete, and inspect usage for assets.\n- Publish, unpublish, inspect publish jobs, and manage custom domains.\n- Start preview, capture screenshots, compare screenshot diffs, and use OCR when installed.\n\n## Inspect and refresh MCP session cache\n\nCommands:\n\n- MCP tool: status {}\n- MCP tool: status {"verbose":true}\n- MCP tool: refresh {"namespaces":["pages","instances","styles"]}\n- MCP tool: reset-session {}\n\nNotes:\n\n- Use status before a task to understand the cached ProjectSession state.\n- Use status with `{"verbose":true}` only when debugging full namespaces, freshness, compatibility, or diagnostics.\n- Use refresh when project data may have changed outside the current MCP session.\n- Use reset-session when local cached state is corrupt or incompatible.\n\n## Visually verify rendered work with AI vision\n\nCommands:\n\n- MCP tool: preview.start {"host":"127.0.0.1","port":5173}\n- MCP tool: preview.status {}\n- MCP tool: preview.stop {}\n- MCP tool: screenshot {"path":"/","output":".webstudio/screenshots/home-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"path":"/pricing","output":".webstudio/screenshots/pricing-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"baseUrl":"http://127.0.0.1:5177","path":"/pricing","output":".webstudio/screenshots/pricing-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff"}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff","expectedText":["Pricing","Start free"]}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff","expectedVisual":{"maxMismatchPercentage":2,"maxChangedRegions":3,"dominantColorChange":{"channel":"luminance","direction":"increase","minMagnitude":10}}}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/pricing-before.png","currentPath":".webstudio/screenshots/pricing-current.png","outputDir":".webstudio/screenshots/diff"}\n- MCP tool: vision.install-ocr {"confirm":true}\n\nNotes:\n\n- Use this after page/content/style mutations and after generated project files are current so a vision-capable AI can see the production-like generated site.\n- For multi-page work, capture every changed page by `path` through the same preview server; no click navigation is required.\n- After MCP mutations, path screenshots regenerate/restart preview as needed before capture; when preview is fresh, repeated path screenshots reuse the running server.\n- Do not call `preview.start` through one-shot `webstudio mcp single-op-call`: it is long-lived. From a shell, use `webstudio mcp run` with preview.start, screenshot, and preview.stop in one shared process, or use a real long-running MCP client.\n- From one-shot shell calls or another process, pass `baseUrl` with `path` to capture an already-running preview/site without generating, building, starting, or restarting preview.\n- Use preview.stop only in the same long-running MCP server or `webstudio mcp run` process that started preview. A separate one-shot `single-op-call` process does not own another process\'s preview controller.\n- Use waitForSelector when the rendered app has a reliable ready marker, waitUntil:"networkidle" for network-heavy pages, and waitForTimeout only for final visual settling.\n- Preview installs generated app dependencies under `.webstudio/preview` and reuses them across regenerations.\n- Do not add generated-preview dependencies to the repository root `package.json` or `pnpm-lock.yaml`.\n- If dependency installation fails, check npm and network configuration, then reinstall or update the Webstudio CLI if the problem persists.\n- When a baseline exists, use screenshot.diff once per baseline/current page or viewport pair to get changed regions, OCR textAnalysis, and diff artifact paths before deciding whether the result matches. Pass expectedText for explicit pass/fail current-screen text assertions with found and missing text. Pass expectedVisual for pass/fail limits on pixel mismatch percentage, changed-region count, or the overall dominant color/brightness direction.\n- If screenshot.diff reports OCR unavailable and the user agrees to install it, call vision.install-ocr {"confirm":true}; otherwise continue with pixel diff and visual inspection.\n- Compare the PNG, OCR text evidence, and diff artifacts against the user\'s intent for layout, typography, colors, spacing, imagery, and responsive framing; then iterate with focused mutations.\n- Root CLI equivalent: `webstudio screenshot --path /pricing --output pricing.png` generates a temporary production preview, captures that route, and stops the server. For repeated captures, keep `webstudio preview` running and pass its absolute URL to `webstudio screenshot`.\n\n## List pages\n\nCommands:\n\n- MCP tool: list-pages {}\n- MCP tool: list-folders {}\n\n## Read page by id\n\nCommands:\n\n- MCP tool: get-page {"pageId":""}\n\n## Read page by path\n\nCommands:\n\n- MCP tool: get-page-by-path {"path":"/pricing"}\n\n## Create page\n\nCommands:\n\n- MCP tool: create-page {"name":"Pricing","path":"/pricing"}\n- MCP tool: create-page {"name":"Pricing","path":"/pricing","title":"Pricing","meta":{"description":"Plans for teams"}}\n\nNotes:\n\n- `name`, `path`, page `title`, and metadata text fields accept plain fixed values.\n- For computed page titles or metadata, send JavaScript expression code such as `pageTitle ?? "Pricing"`.\n\n## Update page settings/metadata\n\nCommands:\n\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans","status":200}}}\n- MCP tool: update-page {"pageId":"","values":{"meta":{"auth":{"method":"basic","login":"","password":""}}}}\n\nNotes:\n\n- Page `title` and metadata text fields accept plain fixed values.\n- For computed page titles or metadata, send JavaScript expression code such as `pageTitle ?? "Pricing"`.\n- Page `status` accepts a fixed HTTP status code as a number from 200 through 599 or a JavaScript expression string for a dynamic status.\n\n## Read project settings\n\nCommands:\n\n- MCP tool: get-project-settings {}\n\nNotes:\n\n- Read `meta.agentInstructions` before making project changes. It contains the project\'s own guidance for AI agents.\n- Agent instructions are shared project guidance. Do not store credentials or other secrets there.\n\n## Update project settings\n\nCommands:\n\n- MCP tool: update-project-settings {"meta":{"siteName":"Acme"}}\n- MCP tool: update-project-settings {"meta":{"agentInstructions":"Use existing design tokens and keep product copy concise."}}\n\n## Read marketplace product\n\nCommands:\n\n- MCP tool: get-marketplace-product {}\n\n## Update marketplace product\n\nCommands:\n\n- MCP tool: update-marketplace-product {"category":"pageTemplates","name":"Acme Template","thumbnailAssetId":"asset-id","author":"Acme Studio","email":"hello@example.com","website":"https://example.com","issues":"","description":"Reusable template project for Acme landing pages."}\n\n## List redirects\n\nCommands:\n\n- MCP tool: list-redirects {}\n\n## Create redirect\n\nCommands:\n\n- MCP tool: create-redirect {"old":"/old","new":"/new","status":301}\n\n## Update redirect\n\nCommands:\n\n- MCP tool: update-redirect {"old":"/old","values":{"new":"/newer","status":302}}\n- MCP tool: update-redirect {"old":"/old","values":{"status":null}}\n\n## Delete redirect\n\nCommands:\n\n- MCP tool: delete-redirect {"old":"/old"}\n\n## Set redirects\n\nCommands:\n\n- MCP tool: set-redirects {"redirects":[{"old":"/old","new":"/new","status":"301"}]}\n\n## List breakpoints\n\nCommands:\n\n- MCP tool: list-breakpoints {}\n\n## Create breakpoint\n\nCommands:\n\n- MCP tool: create-breakpoint {"label":"Tablet","maxWidth":991}\n\n## Update breakpoint\n\nCommands:\n\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"label":"Tablet","maxWidth":1023}}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"condition":null,"minWidth":768}}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"minWidth":null,"maxWidth":null,"condition":"(hover: hover)"}}\n\n## Delete breakpoint\n\nCommands:\n\n- MCP tool: delete-breakpoint {"breakpointId":"tablet"}\n\n## Duplicate page\n\nCommands:\n\n- MCP tool: duplicate-page {"pageId":"","name":"Pricing Copy","path":"/pricing-copy"}\n- MCP tool: duplicate-page {"pageId":"","name":"Paris","path":"/paris","substitutions":{"text":{"London":"Paris"},"variables":{"city":{"type":"string","value":"Paris"}}}}\n- webstudio duplicate-page --page --name Paris --path /paris --substitutions \'{"text":{"London":"Paris"},"variables":{"city":{"type":"string","value":"Paris"}}}\' --json\n\nNotes:\n\n- Text substitutions replace exact fixed text only in the duplicated page\'s text children, string props, title, and metadata.\n- Variable substitutions are keyed by copied source-variable name and use typed variable values. The operation rejects missing or ambiguous names without committing a partial duplicate.\n- Existing expressions and cloned variable/resource references keep their remapped ids.\n\n## List page templates\n\nCommands:\n\n- MCP tool: list-page-templates {}\n\n## Create page template\n\nCommands:\n\n- MCP tool: create-page-template {"name":"Landing Template","title":"Landing"}\n\n## Update page template\n\nCommands:\n\n- MCP tool: update-page-template {"templateId":"","values":{"name":"Article Template","meta":{"description":"Reusable article layout"}}}\n\n## Delete page template\n\nCommands:\n\n- MCP tool: delete-page-template {"templateId":""}\n\n## Duplicate page template\n\nCommands:\n\n- MCP tool: duplicate-page-template {"templateId":""}\n\n## Reorder page template\n\nCommands:\n\n- MCP tool: reorder-page-template {"sourceTemplateId":"","targetTemplateId":"","position":"before"}\n\n## Create page from template\n\nCommands:\n\n- MCP tool: create-page-from-template {"templateId":"","name":"Landing","path":"/landing"}\n\n## Delete page\n\nCommands:\n\n- MCP tool: delete-page {"pageId":""}\n\n## List folders\n\nCommands:\n\n- MCP tool: list-folders {}\n- MCP tool: list-pages {}\n\n## Create folder\n\nCommands:\n\n- MCP tool: create-folder {"name":"Blog","slug":"blog"}\n\n## Update folder\n\nCommands:\n\n- MCP tool: update-folder {"folderId":"","values":{"name":"Blog","slug":"blog"}}\n\n## Delete folder\n\nCommands:\n\n- MCP tool: delete-folder {"folderId":""}\n\n## List element instances\n\nCommands:\n\n- MCP tool: list-instances {"pagePath":"/","maxDepth":3}\n\n## Inspect one element instance\n\nCommands:\n\n- MCP tool: inspect-instance {"instanceId":"","include":["props","styles","children"]}\n\n## Insert authored JSX or one component template\n\nCommands:\n\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"Product OS"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"@webstudio-is/sdk-components-react-radix:Switch"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"Form"}\n\nNotes:\n\n- Use MCP `insert-fragment` as the default way to author styled component trees. It converts JSX to a structured fragment before mutation.\n- Use only exact component ids returned by `components.search`, `components.get`, or `templates.get`. Never derive or guess component ids.\n- The `ws:` namespace contains specific Webstudio core components; it is not HTML-tag shorthand. Use `` for a native `div` and `` for a native form, never `` or ``.\n- For Webstudio\'s complete form structure, discover the Form component and insert its automatic template with `insert-component` using component `"Form"`.\n- MCP receives JSX as a JSON string because MCP arguments are JSON. The CLI converts it locally before the runtime mutation, so the project session receives structured Webstudio data, not JSX source.\n- In `insert-fragment` JSX, use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n- Do not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`.\n- Use Webstudio prop names such as `class` and `for`; do not use React aliases `className` or `htmlFor`.\n- Use Webstudio actions for event/action props, for example `onClick={new ActionValue(["event"], expression\\`console.log(event)\\`)}`. Do not pass JavaScript functions such as `onClick={() => ...}`.\n- Plain prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n- Template-backed components used in JSX must include required child/part components explicitly under the same parent structure as the template, for example ``.\n- Webstudio applies a registered template automatically when using `insert-component`, so composed components such as Switch include required child parts and styles.\n- Use `components.list`, `components.summary`, `components.search`, `components.get`, `templates.list`, and `templates.get` to discover known registry items, component ids, props, templates, insertability, and content model. Read `webstudio://project/components` only when those focused tools are insufficient.\n- Component/template registry items use a shadcn-compatible top-level shape plus Webstudio-specific superset metadata in `meta`. They are for Builder/MCP discovery, not a published shadcn install registry yet.\n- Known components with `contentModel.category: "none"` are not standalone-insertable; insert their root component template instead so required providers/parents are included.\n- Unknown custom component ids are a low-level extension mechanism, not a discovery fallback. Agents must not synthesize them.\n\n## Make a region editable in Content mode\n\nCommands:\n\n- MCP tool: insert-component {"parentInstanceId":"","component":"ws:block"}\n- MCP tool: inspect-instance {"instanceId":"","include":["children"]}\n\nNotes:\n\n- When a page will be handed to a Content-mode editor, wrap every region they should be able to edit in a Content Block (`ws:block`). Content-mode editors can edit text and supported props only in Content Block descendants. Content outside those blocks remains read-only, even when it looks like ordinary editable text.\n- Put reusable insertable options inside the Content Block\'s `ws:block-template` child. A template is source material, not editor content: editors cannot edit or delete it directly. When an editor inserts a template, its copy becomes a direct child of the Content Block and is editable.\n- Before handing off a page, verify with `inspect-instance` that the intended text, images, and links are inside a Content Block, and that templates include all required styling because Content-mode editors cannot use the Style panel.\n\n## Move elements\n\nCommands:\n\n- MCP tool: move-instance {"moves":"moves.json contents"}\n\nNotes:\n\n- Use `position: "end"` to append an instance. Repeating this for A and then B preserves the final order A, B.\n- A numeric `insertIndex` addresses the target parent\'s children before the moved instance is removed. Use it for exact placement; do not calculate the last index to append.\n- Moves in one `moves` array are applied sequentially in array order.\n\n## Clone element subtree\n\nCommands:\n\n- MCP tool: clone-instance {"sourceInstanceId":"","targetParentInstanceId":""}\n\n## Delete element subtree\n\nCommands:\n\n- MCP tool: delete-instance {"instanceIds":[""]}\n\n## List text/expression children\n\nCommands:\n\n- MCP tool: list-texts {"pagePath":"/"}\n\n## Update text child\n\nCommands:\n\n- MCP tool: update-text {"instanceId":"","childIndex":0,"text":"Launch faster"}\n\n## Replace bounded literal text\n\nCommands:\n\n- MCP tool: replace-text {"find":"Start free","replace":"Get started","match":"exact","pagePath":"/pricing","limit":20}\n\nNotes:\n\n- This changes only literal text children, never expression children. Scope it to pagePath or pageId and set a limit before a broad replacement.\n\n## Replace bounded static prop text\n\nCommands:\n\n- MCP tool: replace-prop-text {"find":"old.example.com","replace":"www.example.com","match":"substring","names":["href","code"],"limit":20}\n\nNotes:\n\n- This changes only static string props such as href, alt, aria-label, title, and HTML embed code. It never changes expressions, resources, actions, assets, or other dynamic bindings. Use names or instanceIds and a limit to narrow the change.\n\n## Replace bounded resource text\n\nCommands:\n\n- MCP tool: replace-resource-text {"find":"api.old.example.com","replace":"api.example.com","fields":["url"],"limit":20}\n\nNotes:\n\n- This changes resource names and fixed URL literals only. It skips dynamic URL expressions, headers, search parameters, request bodies, and GraphQL query code.\n\n## Update props\n\nCommands:\n\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: replace-prop-text {"find":"Old label","replace":"New label","names":["aria-label","title"],"limit":20}\n\nNotes:\n\n- Use this for fixed prop values such as `aria-label`, `alt`, `id`, static `href`, and other direct string/number/boolean/json prop values.\n\n## Add JSON-LD structured data\n\nCommands:\n\n- MCP tool: components.get {"component":"JsonLd"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"JsonLd"}\n- MCP tool: update-props {"updates":[{"instanceId":"","name":"code","type":"string","value":"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Organization\\",\\"name\\":\\"Acme\\"}"}]}\n- MCP tool: audit {"scopes":["seo"],"pagePath":"/"}\n\nNotes:\n\n- Prefer placing `JsonLd` inside `HeadSlot`.\n- Store `code` as a JSON object or array encoded as a compact string. The Builder formats it for editing.\n- The semantic prop update rejects malformed JSON and structurally invalid fixed JSON-LD with a precise JSON path.\n- The SEO audit also warns about a missing top-level `@context`, unknown or superseded Schema.org terms, properties unsupported by the supplied type, and incompatible primitive value types.\n- Schema.org vocabulary findings are warnings because custom vocabularies and extensions remain valid. Dynamic JSON-LD is marked as skipped for rendered validation.\n- Do not use bindings just to set static text.\n\n## Delete props\n\nCommands:\n\n- MCP tool: delete-props {"deletions":"props.json contents"}\n\n## Bind props to expressions/resources/actions\n\nCommands:\n\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n\nNotes:\n\n- Use this only when the prop should remain dynamic: expression, resource, action, or an existing scoped runtime context value such as `system`.\n- For a fixed string value, use `update-props` with `type:"string"` and a direct `value` instead.\n\n## Read styles\n\nCommands:\n\n- MCP tool: get-styles {"instanceIds":[""],"includeTokens":true}\n\n## Update local styles\n\nCommands:\n\n- MCP tool: update-styles {"updates":"styles.json contents"}\n\n## Delete local styles\n\nCommands:\n\n- MCP tool: delete-styles {"deletions":"styles.json contents"}\n\n## Replace matching style values\n\nCommands:\n\n- MCP tool: replace-styles {"property":"color","fromValue":{"type":"keyword","value":"red"},"toValue":{"type":"keyword","value":"blue"}}\n\n## List design tokens\n\nCommands:\n\n- MCP tool: list-design-tokens {}\n- MCP tool: list-design-tokens {"withUsage":true}\n- MCP tool: list-design-tokens {"verbose":true}\n\nNotes:\n\n- The default response is compact and includes token id, name, declaration count, and optional usage count.\n- Use `verbose:true` only when you need the full inline style declarations.\n\n## Create design tokens\n\nCommands:\n\n- MCP tool: create-design-token {"tokens":"tokens.json contents"}\n\n## Update design token styles\n\nCommands:\n\n- MCP tool: update-design-token-styles {"designTokenId":"","updates":"styles.json contents"}\n\n## Delete design token styles\n\nCommands:\n\n- MCP tool: delete-design-token-styles {"designTokenId":"","deletions":"styles.json contents"}\n\n## Attach design token to instances\n\nCommands:\n\n- MCP tool: attach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n\n## Detach design token from instances\n\nCommands:\n\n- MCP tool: detach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n\n## Extract design token from local styles\n\nCommands:\n\n- MCP tool: extract-design-token {"instanceIds":[""],"name":"Brand Primary","removeLocalProps":["color"]}\n\n## List CSS variables\n\nCommands:\n\n- MCP tool: list-css-variables {"withUsage":true}\n\n## Define CSS variables\n\nCommands:\n\n- MCP tool: define-css-variable {"vars":"vars.json contents"}\n\n## Delete CSS variables\n\nCommands:\n\n- MCP tool: delete-css-variable {"names":"names.json contents","force":true}\n\n## Rewrite CSS variable references\n\nCommands:\n\n- MCP tool: rewrite-css-variable-refs {"map":"variables.json contents"}\n\n## List data variables\n\nCommands:\n\n- MCP tool: list-variables {}\n- MCP tool: list-variables {"scopeInstanceId":""}\n\nNotes:\n\n- Data variables live in the internal `dataSources` namespace.\n- For raw `snapshot`, request the public `variables` namespace rather than the internal `dataSources` name. Raw patch payloads still use `dataSources` when applying direct changes.\n- Scope variables to the instance where they should become available. Descendants can use them in expressions, and nested variables with the same name mask outer variables.\n\n## Create data variable\n\nCommands:\n\n- MCP tool: create-variable {"scopeInstanceId":"","name":"title","value":{"type":"string","value":"Hello"}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"count","value":{"type":"number","value":3}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"featured","value":{"type":"boolean","value":true}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"tags","value":{"type":"string[]","value":["news","product"]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"filters","value":{"type":"json","value":{"tag":"news"}}}\n\nNotes:\n\n- Data variable values support `string`, `number`, `boolean`, `string[]`, and `json`.\n- Parameters are internal scoped runtime values provided by pages, collections, or components. They are not a public authoring surface: do not create, update, or delete parameter records. Use data variables/resources for user-authored data, and reference documented context values such as `system` only where they are already in scope.\n\n## Update data variable\n\nCommands:\n\n- MCP tool: update-variable {"dataSourceId":"","values":{"value":{"type":"json","value":{"count":1}}}}\n\n## Delete data variable\n\nCommands:\n\n- MCP tool: delete-variable {"dataSourceId":""}\n\n## List resources\n\nCommands:\n\n- MCP tool: list-resources {}\n- MCP tool: list-resources {"scopeInstanceId":""}\n\n## Create resource\n\nCommands:\n\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"https://api.example.com/posts","headers":[]}}\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"\\"https://api.example.com/posts?tag=\\" + filters.tag","headers":[]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Filtered Posts","method":"get","url":"https://api.example.com/posts","searchParams":[{"name":"tag","value":"filters.tag"},{"name":"source","value":{"type":"literal","value":"website"}}],"headers":[{"name":"Authorization","value":"\\"Bearer \\" + auth.token"}]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Post GraphQL","control":"graphql","method":"post","url":"https://api.example.com/graphql","headers":[{"name":"Content-Type","value":{"type":"literal","value":"application/json"}}],"body":"{ query: \\"query Post($slug: String!) { post(slug: $slug) { title } }\\", variables: { slug: system.params.slug } }"},"scopeInstanceId":"","dataSourceName":"post"}\n- MCP tool: create-resource {"resource":{"name":"Current Date","control":"system","method":"get","url":"/$resources/current-date","headers":[]},"scopeInstanceId":"","dataSourceName":"currentDate"}\n\nNotes:\n\n- Resource `url` accepts plain fixed URLs and paths such as `https://api.example.com/posts` and `/$resources/current-date`.\n- Resource `url` can also be a JavaScript expression when it is computed, such as `"https://api.example.com/posts?tag=" + filters.tag`.\n- Header values, search parameter values, and body accept expressions for dynamic values. For fixed text, use `{"type":"literal","value":"application/json"}`; Webstudio stores the required string expression for you.\n- Search parameter values, header values, and body expressions can read scoped variables and documented runtime context values such as `system` when they are available at the resource scope.\n- Add `scopeInstanceId` and `dataSourceName` when the resource result should be exposed as a scoped read data variable. Scoped resources are generated into the page resource `data` map and may be loaded during page rendering. Use this for read-oriented resources such as GET CMS/API data.\n- For submit/write/action resources, create the resource without `scopeInstanceId`, then bind a component prop such as a Form `action` with `bind-props` and `binding.type: "resource"`. Prop-bound resources are generated into the page resource `action` map instead of the read `data` map. Use this for POST, PUT, DELETE, webhooks, GraphQL submissions, and other explicit action flows.\n- Resource `method` can be `get`, `post`, `put`, or `delete`. Use GET for read data, POST for creates/GraphQL/webhooks/form submissions, PUT for full updates or replacements, and DELETE for deletion actions.\n- Optional `control` values are `graphql` and `system`. Use `graphql` for GraphQL-style requests, usually POST with a query body. Use `system` for built-in resources such as `"/$resources/sitemap.xml"`, `"/$resources/current-date"`, and `"/$resources/assets"` and when the resource should use the built-in `system` parameter. System fields are `system.origin`, `system.pathname`, `system.params`, and `system.search`.\n\n## Update resource\n\nCommands:\n\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: replace-resource-text {"find":"api.old.example.com","replace":"api.example.com","fields":["url"],"limit":20}\n\n## Delete resource\n\nCommands:\n\n- MCP tool: delete-resource {"resourceId":""}\n\n## List assets\n\nCommands:\n\n- MCP tool: list-assets {"withUsage":true}\n- MCP tool: list-assets {"verbose":true}\n\nNotes:\n\n- Compact results include each asset\'s folder id. Use `verbose:true` to include complete records for a page of assets, or `get-asset` to read one complete record including description, folder, creation time, and image/font metadata.\n- Image asset descriptions are the default alt text for asset-backed Image components.\n- To generate missing descriptions, inspect the image in its rendered page or asset source, write a concise description of its purpose, and save it on the asset rather than duplicating it on each Image instance.\n\n## Get asset\n\nCommands:\n\n- MCP tool: get-asset {"assetId":""}\n\n## List asset folders\n\nCommands:\n\n- MCP tool: list-asset-folders {}\n\n## Create asset folder\n\nCommands:\n\n- MCP tool: create-asset-folder {"name":"Marketing"}\n- MCP tool: create-asset-folder {"name":"Photos","parentId":""}\n\n## Update asset folder\n\nCommands:\n\n- MCP tool: update-asset-folder {"folderId":"","values":{"name":"Brand"}}\n- MCP tool: update-asset-folder {"folderId":"","values":{"parentId":""}}\n- MCP tool: update-asset-folder {"folderId":"","values":{"parentId":null}}\n\nNotes:\n\n- Updating `parentId` is the folder equivalent of cut and paste. Use `null` to move a folder to Root.\n\n## Duplicate asset folder\n\nCommands:\n\n- MCP tool: duplicate-asset-folder {"folderId":""}\n- MCP tool: duplicate-asset-folder {"folderId":"","parentId":""}\n\nNotes:\n\n- Duplication recursively copies descendant folders and assets. This is the folder equivalent of copy and paste.\n\n## Delete asset folder\n\nCommands:\n\n- MCP tool: delete-asset-folder {"folderId":""}\n\nNotes:\n\n- Deleting a folder recursively deletes its descendant folders and assets.\n\n## Update asset metadata\n\nCommands:\n\n- MCP tool: update-asset {"assetId":"","values":{"description":"Team collaborating around a whiteboard"}}\n\nNotes:\n\n- Use an empty description only when the image is intentionally decorative.\n- Updating an image asset description updates the default alt text wherever that asset is used with an asset-backed alt prop.\n\n## Generate missing image descriptions with an agent\n\nCommands:\n\n- MCP tool: audit {"scopes":["accessibility"],"verbose":true}\n- MCP tool: set-image-descriptions {"updates":[{"assetId":"hero-id","description":"Team collaborating around a whiteboard"},{"assetId":"texture-id","decorative":true}]}\n- MCP tool: audit {"scopes":["accessibility"]}\n\nNotes:\n\n- Start from `missing-image-description` findings. Inspect each image in its rendered page context before writing text.\n- The vision-capable agent generates the wording; the CLI validates and stores it but does not contain its own vision model.\n- Use `decorative:true` only when the image adds no information. This intentionally stores an empty description so later audits do not report it as missing.\n- Re-run the accessibility audit after the update. Asset-backed Image components use the saved asset description as their default alt text.\n\n## Manage fonts\n\nCommands:\n\n- MCP tool: list-fonts {"includeSystem":true}\n- MCP tool: list-assets {"type":"font"}\n- MCP tool: upload-asset {"asset":{"name":"acme-sans.woff2","type":"font","format":"woff2","meta":{"family":"Acme Sans","style":"normal","weight":400}},"assetsDir":".webstudio/assets"}\n- MCP tool: update-styles {"updates":"styles.json contents"}\n\nNotes:\n\n- Use `list-fonts` to discover uploaded families and system stacks. Upload/delete fonts through the existing asset tools, then apply a family with a `font-family` style declaration.\n\n## Upload one asset\n\nCommands:\n\n- MCP tool: upload-asset {"asset":{"name":"image.png","type":"image","format":"png","meta":{"width":1200,"height":630}},"assetsDir":".webstudio/assets"}\n- MCP tool: upload-asset {"asset":{"name":"image.png","type":"image","format":"png","folderId":"","meta":{"width":1200,"height":630}},"assetsDir":".webstudio/assets"}\n\n## Upload asset batch\n\nCommands:\n\n- MCP tool: upload-assets {"assets":[{"name":"image.png","type":"image","format":"png","meta":{"width":1200,"height":630}}],"assetsDir":".webstudio/assets"}\n\n## Duplicate asset\n\nCommands:\n\n- MCP tool: duplicate-asset {"assetId":""}\n- MCP tool: duplicate-asset {"assetId":"","folderId":""}\n- MCP tool: duplicate-asset {"assetId":"","folderId":null}\n\nNotes:\n\n- Duplication is the asset equivalent of copy and paste. Updating `folderId` is the equivalent of cut and paste; use `null` for Root.\n\n## Download asset\n\nCommands:\n\n- MCP tool: download-asset {"assetId":""}\n\n## Find asset usage\n\nCommands:\n\n- MCP tool: find-asset-usage {"assetId":""}\n\n## Replace asset references\n\nCommands:\n\n- MCP tool: replace-asset {"fromAssetId":"","toAssetId":""}\n\n## Delete assets\n\nCommands:\n\n- MCP tool: delete-asset {"assetIdsOrPrefixes":[""],"force":true}\n\n## Publish project\n\nCommands:\n\n- webstudio publish deploy --target production --json\n\n## List publishes\n\nCommands:\n\n- webstudio publish list --json\n\n## Check publish job\n\nCommands:\n\n- webstudio publish status --job --json\n\n## Unpublish\n\nCommands:\n\n- webstudio publish unpublish --target production --confirm --json\n\n## List domains\n\nCommands:\n\n- webstudio domains list --json\n\n## Create domain\n\nCommands:\n\n- webstudio domains create --domain example.com --json\n\n## Update domain\n\nCommands:\n\n- webstudio domains update --domain-id --domain www.example.com --json\n\n## Delete domain\n\nCommands:\n\n- webstudio domains delete --domain-id --confirm --json\n\n## Verify domain\n\nCommands:\n\n- webstudio domains verify --domain-id --json\n\n## Make arbitrary store-level changes\n\nCommands:\n\n- MCP tool: inspect {}\n- MCP tool: snapshot {"include":[""]}\n- MCP tool: apply-patch {"baseVersion":"","transactions":"patch.json contents"}\n\nNotes:\n\n- Use only when no semantic command exists.\n\n## Manage marketplace metadata\n\nCommands:\n\n- MCP tool: get-marketplace-product {}\n- MCP tool: update-marketplace-product {"category":"pageTemplates","name":"Acme Template","thumbnailAssetId":"asset-id","author":"Acme Studio","email":"hello@example.com","website":"https://example.com","issues":"","description":"Reusable template project for Acme landing pages."}\n\nPatch namespaces:\n\n- marketplaceProduct\n\n## Search and inspect safely\n\nCommands:\n\n- MCP tool: search-project {"query":"pricing"}\n- MCP tool: search-project {"query":"api.example.com","scopes":["resources"]}\n- MCP tool: list-instances {"pagePath":"/","maxDepth":5}\n- MCP tool: inspect-instance {"instanceId":"","include":["props","styles","children"]}\n- MCP tool: list-texts {"pagePath":"/"}\n- MCP tool: list-assets {"withUsage":true}\n- MCP tool: find-asset-usage {"assetId":""}\n- MCP tool: snapshot {"include":["pages","instances","props","resources","assets"]}\n\nNotes:\n\n- Use `search-project` for query-driven lookup across labels, text, prop values, resource URLs, asset metadata, and styles. Use `audit` for project health findings.\n\n## Audit project quality\n\nCommands:\n\n- webstudio audit --json\n- webstudio audit --scopes accessibility --scopes seo --json\n- webstudio audit --page-path /pricing --json\n- webstudio audit --scopes accessibility --verbose --json\n- webstudio audit --rendered --page-path /pricing --json\n- webstudio audit --rendered --route-example post=/blog/hello --json\n- webstudio audit --rendered --image-domain images.example.com --json\n- MCP tool: audit {}\n- MCP tool: audit {"scopes":["accessibility","security"],"severities":["error","warning"]}\n- MCP tool: audit {"scopes":["accessibility"],"verbose":true}\n- MCP tool: audit {"scopes":["craft"],"verbose":true}\n- MCP tool: audit {"rendered":true,"verbose":true}\n\nNotes:\n\n- With no scopes, `audit` checks accessibility, security, SEO, performance settings, unused assets, ineffective Collection styles, non-GET resources exposed as render-time data, and unused or duplicate style data.\n- Craft is opt-in and read-only. Run `audit` with `scopes:["craft"]` to detect whether the project is not using Craft, partially compatible, or compatible with the versioned Craft 1.2 profile. `profileStatuses` includes the University-doc provenance and the smallest safe next action. The audit never installs Craft or changes a non-Craft project.\n- The `performance` scope reports disabled atomic CSS generation. A rendered audit also measures broken, eager below-fold, and oversized images, browser-marked render-blocking resources, and legacy font formats.\n- Rendered image and resource metrics run only when the selected scopes include `performance`; responsive layout dimensions remain available whenever `rendered:true` is requested.\n- Compact findings include stable ids, severity, message, and location. Use `--verbose` or `{"verbose":true}` for evidence, explanation, suggested remediation, skipped-check details, and manual-check workflows.\n- `summary` counts all findings before severity filtering and pagination.\n- `contractVersion` identifies the audit response contract. Handle a new value before assuming existing fields retain the same meaning.\n- Expression-, resource-, and parameter-backed values that cannot be checked reliably appear in `skippedChecks`; they are not treated as passing or failing.\n- Page filters apply to page-owned accessibility, security, and SEO checks. Asset and style usage remain project-wide to avoid false unused findings.\n- Continue paginated results with `cursor`. Restart the audit if the project version changes.\n- Verbose skipped-check and manual-check details are included on the first findings page only; their total counts remain available on every page.\n- `manualChecks` describes responsive, hierarchy, and contrast checks that require preview screenshots and vision.\n- Focused audits return only manual checks relevant to their selected scopes.\n- In a long-lived MCP session, `{"rendered":true}` reuses preview and screenshot\n tools to capture every static page at mobile, desktop, and Builder breakpoint\n edges. Compact output reports rendered check/issue/failure counts; verbose\n output includes screenshot paths and measured layout dimensions.\n- Dynamic route templates are skipped unless `--route-example =`\n (or MCP `routeExamples`) supplies a concrete path. The path must not contain\n unresolved `:` or `*` parameters.\n- Plans above 120 captures return a short-lived confirmation token. Review the\n unchanged plan, then rerun with `--confirm-large-run` and\n `--confirmation-token`.\n- Detailed rendered evidence is stored in a versioned manifest under\n `.webstudio/audits`; compact output includes its path and screenshot count.\n- Rendered checks also report broken images, eager images below the fold, and\n image sources more than 2x their rendered dimensions in both axes, including\n Webstudio instance ids and measured dimensions when available.\n- Rendered checks include sanitized Resource Timing evidence and report\n browser-marked render-blocking resources plus legacy `.ttf`, `.otf`, and\n `.woff` fonts without applying a universal transfer-size threshold.\n- Fix findings through semantic mutation commands, then rerun `audit` to confirm their deterministic finding ids disappeared.\n\n## Verify dynamic bindings\n\nCommands:\n\n- webstudio verify-bindings --json\n- MCP tool: verify-bindings {"pagePath":"/pricing"}\n- MCP tool: verify-bindings {"instanceId":"","limit":50}\n\nNotes:\n\n- Statically checks persisted text expressions, expression/action/resource/parameter props, resource expressions, and page metadata.\n- Findings distinguish invalid syntax, unknown or out-of-scope variables, stale internal data-source ids, and missing resource or parameter references.\n- Page and instance filters can be combined when the instance belongs to the selected page. Continue findings with `cursor`.\n- This operation does not resolve rendered values or execute external resources. Preview representative loading, empty, error, and populated states after static findings are fixed.\n\n## Refactor targeted content\n\nCommands:\n\n- MCP tool: list-instances {"pagePath":"/"}\n- MCP tool: list-texts {"pagePath":"/"}\n- MCP tool: update-text {"instanceId":"","childIndex":0,"text":"Launch faster"}\n- MCP tool: replace-text {"find":"Old headline","replace":"New headline","match":"exact","pagePath":"/pricing","limit":20}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans"}}}\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: replace-asset {"fromAssetId":"","toAssetId":""}\n- MCP tool: replace-styles {"property":"color","fromValue":{"type":"keyword","value":"red"},"toValue":{"type":"keyword","value":"blue"}}\n- MCP tool: rewrite-css-variable-refs {"map":"variables.json contents"}\n\nNotes:\n\n- Use focused reads first, then mutate only matching instances, props, metadata, resource URLs, assets, or style references. Use `replace-text` for bounded literal text changes, `replace-prop-text` for bounded static prop text, `replace-resource-text` for fixed resource names/URLs, and `update-text` for one known child or expressions.\n\n## Optimize existing project\n\nCommands:\n\n- MCP tool: list-pages {}\n- MCP tool: list-folders {}\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans"}}}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: list-breakpoints {}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"maxWidth":1023}}\n- MCP tool: get-styles {"instanceIds":[""],"includeTokens":true}\n- MCP tool: update-styles {"updates":"styles.json contents"}\n- MCP tool: attach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n- MCP tool: update-project-settings {"meta":{"siteName":"Acme"}}\n\nNotes:\n\n- Use this for SEO metadata, accessibility labels, responsive behavior, token consistency, and project settings.\n\n## Connect external data\n\nCommands:\n\n- MCP tool: create-variable {"scopeInstanceId":"","name":"title","value":{"type":"string","value":"Hello"}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"tags","value":{"type":"string[]","value":["news","product"]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"filters","value":{"type":"json","value":{"tag":"news"}}}\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"https://api.example.com/posts","searchParams":[{"name":"tag","value":"filters.tag"},{"name":"source","value":{"type":"literal","value":"website"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Post GraphQL","control":"graphql","method":"post","url":"https://api.example.com/graphql","headers":[{"name":"Content-Type","value":{"type":"literal","value":"application/json"}}],"body":"{ query: \\"query Post($slug: String!) { post(slug: $slug) { title } }\\", variables: { slug: system.params.slug } }"},"scopeInstanceId":"","dataSourceName":"post"}\n- MCP tool: create-resource {"resource":{"name":"Current Date","control":"system","method":"get","url":"/$resources/current-date","headers":[]},"scopeInstanceId":"","dataSourceName":"currentDate"}\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"{/_ collection content _/}"}\n\nNotes:\n\n- Use this for CMS sections, blog listings, Ghost/headless CMS pages, n8n-style integrations, and API URLs built from variables.\n- For read data, expose GET resources as scoped data variables with `scopeInstanceId`/`dataSourceName` and read the loaded result from the resource result wrapper, usually `.data`.\n- For writes, webhooks, GraphQL submissions, and deletes, prefer unscoped resources bound to Form `action` props so they become action resources instead of auto-loaded read resources.\n- Use direct props for fixed values and prop bindings only when a prop must read a data variable, resource, action, or documented runtime context value such as `system`.\n\n## Render an array or object as repeated content\n\nCommands:\n\n- MCP tool: insert-collection {"parentInstanceId":"","data":{"type":"expression","value":"posts.data.items"},"itemFragment":"{expression`collectionItem.title`}"}\n- MCP tool: inspect-instance {"instanceId":"","include":["props","bindings","children"]}\n\nNotes:\n\n- Use Collection whenever an array or object from a resource or data variable should render a list, grid, cards, table rows, options, tabs, or other repeated UI.\n- Pass `insert-collection` the complete array or object. Do not pass the resource response wrapper or one indexed item. External resource arrays are commonly nested under the scoped resource result\'s `data` field or deeper.\n- Pass one repeated-item Webstudio JSX root. The command creates the Collection, its private current-item/current-key parameters, the iterable binding, and descendant item bindings atomically.\n- Collection renders the item root once for every entry. Use `expression` values such as `collectionItem.title` in descendant text and props. Object iteration also exposes `collectionItemKey`.\n- Wrap multiple repeated sibling instances in one Element inside Collection.\n- For repeated Radix items such as accordion items, tabs, or menu options, bind a stable unique id or slug to every required `value` prop.\n- See the [Collection documentation](https://docs.webstudio.is/university/core-components/collection) for the equivalent Builder workflow.\n\n## Support dynamic runtime behavior\n\nCommands:\n\n- MCP tool: integrate-runtime-ui {"parentInstanceId":"","resources":[{"resource":{"name":"Seats","method":"get","url":"https://api.example.com/seats","headers":[]},"dataSourceName":"Seats","exposeAsDataSource":true}],"structure":{"type":"collection","data":{"type":"expression","value":"Seats.data"},"itemFragment":{"children":[{"type":"id","value":"seat"}],"instances":[{"type":"instance","id":"seat","component":"Text","children":[{"type":"expression","value":"collectionItem.label"}]}],"props":[],"dataSources":[],"resources":[],"styleSources":[],"styleSourceSelections":[],"styles":[],"breakpoints":[],"assets":[]}},"retainedBehavior":[{"instanceId":"","responsibility":"Seat selection behavior"}]}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n- MCP tool: create-resource {"resource":{"name":"Seats","method":"get","url":"https://api.example.com/seats","headers":[]}}\n- MCP tool: snapshot {"include":["instances","props","resources"]}\n- MCP tool: apply-patch {"baseVersion":"","transactions":"patch.json contents"}\n\nNotes:\n\n- Use `integrate-runtime-ui` to create variables/resources, insert one editable fragment or Collection, and add safe data bindings in one transaction.\n- List existing script-owned responsibilities under `retainedBehavior`. The operation preserves those instances and never evaluates or accepts replacement script bodies.\n- `unsupportedConversions` records behavior that cannot be represented safely. Dry-run returns the complete transaction and the same retained/unsupported report without changing the project.\n- New actions and HtmlEmbed scripts are intentionally rejected. Create normal editable components and data bindings; keep opaque runtime behavior in existing script instances.\n\n## Build authenticated pages\n\nCommands:\n\n- MCP tool: meta.guide {"brief":"Build a Supabase-authenticated account page"}\n- MCP tool: create-page {"name":"Account","path":"/account"}\n- MCP tool: update-page {"pageId":"","values":{"meta":{"auth":{"method":"basic","login":"","password":""}}}}\n- MCP tool: create-resource {"resource":{"name":"Session","method":"get","url":"https://api.example.com/session","headers":[]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"user","value":{"type":"json","value":{}}}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n\nNotes:\n\n- Inspect and reuse the project\'s existing auth convention before authoring. Do\n not add a second provider or session model implicitly.\n- Model signed-out, loading, signed-in, and failed-auth states explicitly.\n- Never store credentials, service-role keys, refresh tokens, private session\n values, or authenticated response bodies in project data, command output,\n screenshots, agent instructions, or error reports. Privileged provider calls\n and authorization enforcement belong server-side.\n- Basic auth is semantic today. Provider-specific Supabase/Firebase setup still\n uses the existing resource, variable, prop, binding, and embed tools; there is\n no provider-specific installer.\n\n## Generate from design input\n\nCommands:\n\n- MCP tool: meta.guide {"brief":"Recreate this Figma design as a responsive page"}\n- MCP tool: create-page {"name":"Landing","path":"/landing"}\n- MCP tool: create-design-token {"tokens":"tokens.json contents"}\n- MCP tool: define-css-variable {"vars":"vars.json contents"}\n- MCP tool: list-breakpoints {}\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"Section copy"}\n- MCP tool: update-styles {"updates":[{"instanceId":"","breakpointId":"","property":"padding-left","value":{"type":"unit","unit":"px","value":24}}]}\n- MCP tool: preview.start {"host":"127.0.0.1","port":5173}\n- MCP tool: screenshot {"path":"/landing","output":"landing-desktop.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"path":"/landing","output":"landing-mobile.png","viewport":{"width":390,"height":844},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"baseUrl":"http://127.0.0.1:5177","path":"/landing","output":"landing-desktop.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n\nNotes:\n\n- Use this after the agent can inspect the supplied design. There is no direct\n Figma, screenshot, Inception, or `design.md` import command.\n- Inspect and reuse existing variables, tokens, styles, components, assets, and\n page patterns before authoring. Build semantic editable structure rather than\n flattening the design into an image or absolute-positioned approximation.\n- Verify one familiar viewport inside every distinct Builder breakpoint range,\n then run rendered audit and inspect the screenshots before completion.\n\n## Cross-project maintenance\n\nCommands:\n\n- webstudio mcp run .temp/projects.json\n- webstudio mcp run .temp/projects.json --dry-run\n- webstudio mcp run .temp/projects.json --approve-mutations --concurrency 2\n\nNotes:\n\n- Put shared `calls` and a `projects` array of independently linked project roots in the existing `mcp run` manifest. Project roots are relative to the manifest file.\n- Each project uses its own config, authentication, ProjectSession storage, checkpoint, and failure boundary. Confirmed successful calls are checkpointed. Reads can resume automatically; a mutation interrupted after dispatch is reported as ambiguous and is not replayed automatically, preventing silent duplicate writes.\n- Focus the manifest on bounded reads or audits first. Use per-call `dryRun`, global `--dry-run`, or explicitly approve committed mutations with `--approve-mutations` after reviewing the manifest.\n\n# Known CLI Gaps\n\n## Provider-specific authenticated pages\n\nMissing:\nCLI supports page basic auth and generic resources/props/embeds, but not guided Supabase/Firebase auth setup.\n\nCurrent fallback:\nCall `meta.guide` with the provider-authenticated page goal, then create the\npage, resources, variables, props, bindings, and embeds with existing semantic\ntools.\n\nSuggested commands:\n\n- setup-auth-page\n\n## Generate from design input\n\nMissing:\nNo command imports Figma, screenshots, Inception output, or design.md and turns it into pages/tokens/layout.\n\nCurrent fallback:\nCall `meta.guide` with the design-input goal, let the agent inspect the supplied\ndesign, then use semantic page, token, asset, fragment, style, preview,\nscreenshot, and audit tools. Use `apply-patch` only when no semantic operation\nfits.\n\nSuggested commands:\n\n- generate-from-design\n\n## Built-in cross-project maintenance\n\nMissing:\nPublic API and CLI intentionally operate on one configured project at a time; there is no built-in multi-project discovery or loop runner.\n\nCurrent fallback:\nRun the CLI from an external script that reconfigures one project/session at a time.\n\nSuggested commands:\n\n- none\n', + '# CLI API Use Cases\n\n## Link/configure one project\n\nCommands:\n\n- webstudio init --link --json\n\nNotes:\n\n- Writes local project id and global origin/token config.\n\n## Import synced project bundle into another project\n\nCommands:\n\n- webstudio sync\n- webstudio import --to \n- MCP tool: import {"to":""}\n\nNotes:\n\n- Imports local `.webstudio/data.json` into the destination project.\n- Destination share link must allow build/import access.\n- Use `--skip-assets` only when asset rows and files should not be imported.\n\n## Identify current token\n\nCommands:\n\n- MCP tool: whoami {}\n\n## Check token permissions\n\nCommands:\n\n- webstudio permissions --json\n\n## Inspect project/build/version\n\nCommands:\n\n- MCP tool: inspect {}\n- MCP tool: snapshot {"include":["pages","instances","styles"]}\n\n## Discover CLI/API capabilities\n\nCommands:\n\n- webstudio schema api\n- webstudio schema mcp\n- webstudio man --json\n- webstudio man llm --json\n- MCP tool: meta.index {}\n- MCP tool: meta.guide {"brief":"Create a pricing page"}\n- MCP tool: meta.get_more_tools {"brief":"update-styles"}\n- webstudio mcp list-resources\n- webstudio mcp read-resource webstudio://project/guide\n- webstudio mcp read-resource webstudio://project/expressions\n\nNotes:\n\n- Use `webstudio schema mcp` for a compact machine-readable MCP tool overview. Add `--verbose` or use focused `meta.get_more_tools` calls only when exact input schemas are needed.\n- Use focused MCP tools for discovery first: `meta.index`, `meta.guide`, `meta.get_more_tools`, `components.list`, `components.summary`, `components.search`, `components.get`, `templates.list`, and `templates.get`. Protocol clients can use `resources/list` and `resources/read`; shell agents can use `webstudio mcp list-resources` and `webstudio mcp read-resource `. Read longer resources such as `webstudio://project/tools` and `webstudio://project/components` only when focused tools are insufficient.\n- `components.summary` returns counts by default; request `{"detail":"components","limit":20}` for paginated entries. Registry list tools return compact metadata, while `components.get` and `templates.get` return focused full details.\n- Read `webstudio://project/expressions` before authoring unfamiliar computed text, prop bindings, resource expressions, actions, or Collection item bindings.\n- From a shell, call one MCP tool with the shortcut form `webstudio \'\'`, for example `webstudio components.summary`. The explicit equivalent is `webstudio mcp single-op-call \'\'`. Use `--input-file` for large payloads.\n\n## Inspect external shadcn registry items\n\nCommands:\n\n- webstudio registry inspect --source https://example.com/r/registry.json --item button --json\n- webstudio registry inspect --source ./registry.json --item dialog --json\n- webstudio registry inspect --source https://example.com/r/button.json --json\n\nNotes:\n\n- Reads a local or remote registry item without installing files or changing the configured Webstudio project.\n- Returns the item name, description, package and registry dependencies, file paths/targets, available docs, and a read-only compatibility report.\n- The report explicitly says whether installation or editable-component conversion is supported, lists declared requirements and manual steps, and says when arbitrary source code was not analyzed.\n- This is an inspection step only. It does not install files or change the configured project.\n\n## Understand what MCP can do\n\nCommands:\n\n- webstudio man mcp\n- MCP tool: meta.index {}\n- MCP tool: meta.guide {"brief":"What can Webstudio MCP do?"}\n\nMCP lets agents work on one configured Webstudio project. Agents can:\n\n- Inspect the linked project, token permissions, and latest editable build.\n- Read selected project data for audits, migrations, and repair.\n- Search labels, text, props, resource URLs, asset metadata, and styles.\n- Audit accessibility, security, SEO, performance settings, unused assets, ineffective Collection styles, and unused or duplicate style data.\n- Create and edit pages, folders, redirects, breakpoints, and page templates.\n- Create pages from reusable templates.\n- Update page metadata, SEO fields, auth settings, and marketplace metadata.\n- Insert components and styled JSX sections.\n- Create data-driven lists, grids, cards, and similar repeated UI from array or object data in one Collection operation.\n- Move, copy, wrap, unwrap, convert, rename, retag, and delete elements.\n- Update text, rich text, props, bindings, and actions.\n- Create and update local styles, design tokens, style sources, and CSS variables.\n- Create static data variables and JSON variables.\n- Create HTTP, GraphQL, and system resources.\n- Use system resources for sitemap, current date, and assets.\n- Bind resources to rendered data or form/action props.\n- Manage nested asset folders and upload, inspect, move, duplicate, download, replace, delete, and inspect usage for assets.\n- Publish, unpublish, inspect publish jobs, and manage custom domains.\n- Start preview, capture screenshots, compare screenshot diffs, and use OCR when installed.\n\n## Query and bind asset data\n\nCommands:\n\n- MCP tool: create-resource {"resource":{"control":"system","name":"Markdown assets","method":"get","url":"/$resources/assets","searchParams":[{"name":"extension","value":{"type":"literal","value":"md"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"markdownAssets"}\n- MCP tool: create-resource {"resource":{"control":"system","name":"Site data","method":"get","url":"/$resources/assets","searchParams":[{"name":"filename","value":{"type":"literal","value":"site-data.json"}},{"name":"include","value":{"type":"literal","value":"content"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"siteData"}\n\nNotes:\n\n- The Assets system resource returns an object keyed by asset id. Each item includes its URL, filename, description, folder, type, format, size, and timestamps.\n- Filter by a case-insensitive filename substring with `filename`. Filter by one or more extensions with `extension`; separate extensions with commas or repeat the parameter.\n- Asset contents are omitted by default. Set `include=content` to include editable text contents and parse valid JSON contents into structured values.\n- A content query accepts at most 20 matching assets and 256 KiB per asset. Binary and oversized assets include `contentError` instead of content.\n- Bind the resource data or any nested metadata/content field with the existing resource binding controls.\n\n## Inspect and refresh MCP session cache\n\nCommands:\n\n- MCP tool: status {}\n- MCP tool: status {"verbose":true}\n- MCP tool: refresh {"namespaces":["pages","instances","styles"]}\n- MCP tool: reset-session {}\n\nNotes:\n\n- Use status before a task to understand the cached ProjectSession state.\n- Use status with `{"verbose":true}` only when debugging full namespaces, freshness, compatibility, or diagnostics.\n- Use refresh when project data may have changed outside the current MCP session.\n- Use reset-session when local cached state is corrupt or incompatible.\n\n## Visually verify rendered work with AI vision\n\nCommands:\n\n- MCP tool: preview.start {"host":"127.0.0.1","port":5173}\n- MCP tool: preview.status {}\n- MCP tool: preview.stop {}\n- MCP tool: screenshot {"path":"/","output":".webstudio/screenshots/home-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"path":"/pricing","output":".webstudio/screenshots/pricing-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"baseUrl":"http://127.0.0.1:5177","path":"/pricing","output":".webstudio/screenshots/pricing-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff"}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff","expectedText":["Pricing","Start free"]}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff","expectedVisual":{"maxMismatchPercentage":2,"maxChangedRegions":3,"dominantColorChange":{"channel":"luminance","direction":"increase","minMagnitude":10}}}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/pricing-before.png","currentPath":".webstudio/screenshots/pricing-current.png","outputDir":".webstudio/screenshots/diff"}\n- MCP tool: vision.install-ocr {"confirm":true}\n\nNotes:\n\n- Use this after page/content/style mutations and after generated project files are current so a vision-capable AI can see the production-like generated site.\n- For multi-page work, capture every changed page by `path` through the same preview server; no click navigation is required.\n- After MCP mutations, path screenshots regenerate/restart preview as needed before capture; when preview is fresh, repeated path screenshots reuse the running server.\n- Do not call `preview.start` through one-shot `webstudio mcp single-op-call`: it is long-lived. From a shell, use `webstudio mcp run` with preview.start, screenshot, and preview.stop in one shared process, or use a real long-running MCP client.\n- From one-shot shell calls or another process, pass `baseUrl` with `path` to capture an already-running preview/site without generating, building, starting, or restarting preview.\n- Use preview.stop only in the same long-running MCP server or `webstudio mcp run` process that started preview. A separate one-shot `single-op-call` process does not own another process\'s preview controller.\n- Use waitForSelector when the rendered app has a reliable ready marker, waitUntil:"networkidle" for network-heavy pages, and waitForTimeout only for final visual settling.\n- Preview installs generated app dependencies under `.webstudio/preview` and reuses them across regenerations.\n- Do not add generated-preview dependencies to the repository root `package.json` or `pnpm-lock.yaml`.\n- If dependency installation fails, check npm and network configuration, then reinstall or update the Webstudio CLI if the problem persists.\n- When a baseline exists, use screenshot.diff once per baseline/current page or viewport pair to get changed regions, OCR textAnalysis, and diff artifact paths before deciding whether the result matches. Pass expectedText for explicit pass/fail current-screen text assertions with found and missing text. Pass expectedVisual for pass/fail limits on pixel mismatch percentage, changed-region count, or the overall dominant color/brightness direction.\n- If screenshot.diff reports OCR unavailable and the user agrees to install it, call vision.install-ocr {"confirm":true}; otherwise continue with pixel diff and visual inspection.\n- Compare the PNG, OCR text evidence, and diff artifacts against the user\'s intent for layout, typography, colors, spacing, imagery, and responsive framing; then iterate with focused mutations.\n- Root CLI equivalent: `webstudio screenshot --path /pricing --output pricing.png` generates a temporary production preview, captures that route, and stops the server. For repeated captures, keep `webstudio preview` running and pass its absolute URL to `webstudio screenshot`.\n\n## List pages\n\nCommands:\n\n- MCP tool: list-pages {}\n- MCP tool: list-folders {}\n\n## Read page by id\n\nCommands:\n\n- MCP tool: get-page {"pageId":""}\n\n## Read page by path\n\nCommands:\n\n- MCP tool: get-page-by-path {"path":"/pricing"}\n\n## Create page\n\nCommands:\n\n- MCP tool: create-page {"name":"Pricing","path":"/pricing"}\n- MCP tool: create-page {"name":"Pricing","path":"/pricing","title":"Pricing","meta":{"description":"Plans for teams"}}\n\nNotes:\n\n- `name`, `path`, page `title`, and metadata text fields accept plain fixed values.\n- For computed page titles or metadata, send JavaScript expression code such as `pageTitle ?? "Pricing"`.\n\n## Update page settings/metadata\n\nCommands:\n\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans","status":200}}}\n- MCP tool: update-page {"pageId":"","values":{"meta":{"auth":{"method":"basic","login":"","password":""}}}}\n\nNotes:\n\n- Page `title` and metadata text fields accept plain fixed values.\n- For computed page titles or metadata, send JavaScript expression code such as `pageTitle ?? "Pricing"`.\n- Page `status` accepts a fixed HTTP status code as a number from 200 through 599 or a JavaScript expression string for a dynamic status.\n\n## Read project settings\n\nCommands:\n\n- MCP tool: get-project-settings {}\n\nNotes:\n\n- Read `meta.agentInstructions` before making project changes. It contains the project\'s own guidance for AI agents.\n- Agent instructions are shared project guidance. Do not store credentials or other secrets there.\n\n## Update project settings\n\nCommands:\n\n- MCP tool: update-project-settings {"meta":{"siteName":"Acme"}}\n- MCP tool: update-project-settings {"meta":{"agentInstructions":"Use existing design tokens and keep product copy concise."}}\n\n## Read marketplace product\n\nCommands:\n\n- MCP tool: get-marketplace-product {}\n\n## Update marketplace product\n\nCommands:\n\n- MCP tool: update-marketplace-product {"category":"pageTemplates","name":"Acme Template","thumbnailAssetId":"asset-id","author":"Acme Studio","email":"hello@example.com","website":"https://example.com","issues":"","description":"Reusable template project for Acme landing pages."}\n\n## List redirects\n\nCommands:\n\n- MCP tool: list-redirects {}\n\n## Create redirect\n\nCommands:\n\n- MCP tool: create-redirect {"old":"/old","new":"/new","status":301}\n\n## Update redirect\n\nCommands:\n\n- MCP tool: update-redirect {"old":"/old","values":{"new":"/newer","status":302}}\n- MCP tool: update-redirect {"old":"/old","values":{"status":null}}\n\n## Delete redirect\n\nCommands:\n\n- MCP tool: delete-redirect {"old":"/old"}\n\n## Set redirects\n\nCommands:\n\n- MCP tool: set-redirects {"redirects":[{"old":"/old","new":"/new","status":"301"}]}\n\n## List breakpoints\n\nCommands:\n\n- MCP tool: list-breakpoints {}\n\n## Create breakpoint\n\nCommands:\n\n- MCP tool: create-breakpoint {"label":"Tablet","maxWidth":991}\n\n## Update breakpoint\n\nCommands:\n\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"label":"Tablet","maxWidth":1023}}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"condition":null,"minWidth":768}}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"minWidth":null,"maxWidth":null,"condition":"(hover: hover)"}}\n\n## Delete breakpoint\n\nCommands:\n\n- MCP tool: delete-breakpoint {"breakpointId":"tablet"}\n\n## Duplicate page\n\nCommands:\n\n- MCP tool: duplicate-page {"pageId":"","name":"Pricing Copy","path":"/pricing-copy"}\n- MCP tool: duplicate-page {"pageId":"","name":"Paris","path":"/paris","substitutions":{"text":{"London":"Paris"},"variables":{"city":{"type":"string","value":"Paris"}}}}\n- webstudio duplicate-page --page --name Paris --path /paris --substitutions \'{"text":{"London":"Paris"},"variables":{"city":{"type":"string","value":"Paris"}}}\' --json\n\nNotes:\n\n- Text substitutions replace exact fixed text only in the duplicated page\'s text children, string props, title, and metadata.\n- Variable substitutions are keyed by copied source-variable name and use typed variable values. The operation rejects missing or ambiguous names without committing a partial duplicate.\n- Existing expressions and cloned variable/resource references keep their remapped ids.\n\n## List page templates\n\nCommands:\n\n- MCP tool: list-page-templates {}\n\n## Create page template\n\nCommands:\n\n- MCP tool: create-page-template {"name":"Landing Template","title":"Landing"}\n\n## Update page template\n\nCommands:\n\n- MCP tool: update-page-template {"templateId":"","values":{"name":"Article Template","meta":{"description":"Reusable article layout"}}}\n\n## Delete page template\n\nCommands:\n\n- MCP tool: delete-page-template {"templateId":""}\n\n## Duplicate page template\n\nCommands:\n\n- MCP tool: duplicate-page-template {"templateId":""}\n\n## Reorder page template\n\nCommands:\n\n- MCP tool: reorder-page-template {"sourceTemplateId":"","targetTemplateId":"","position":"before"}\n\n## Create page from template\n\nCommands:\n\n- MCP tool: create-page-from-template {"templateId":"","name":"Landing","path":"/landing"}\n\n## Delete page\n\nCommands:\n\n- MCP tool: delete-page {"pageId":""}\n\n## List folders\n\nCommands:\n\n- MCP tool: list-folders {}\n- MCP tool: list-pages {}\n\n## Create folder\n\nCommands:\n\n- MCP tool: create-folder {"name":"Blog","slug":"blog"}\n\n## Update folder\n\nCommands:\n\n- MCP tool: update-folder {"folderId":"","values":{"name":"Blog","slug":"blog"}}\n\n## Delete folder\n\nCommands:\n\n- MCP tool: delete-folder {"folderId":""}\n\n## List element instances\n\nCommands:\n\n- MCP tool: list-instances {"pagePath":"/","maxDepth":3}\n\n## Inspect one element instance\n\nCommands:\n\n- MCP tool: inspect-instance {"instanceId":"","include":["props","styles","children"]}\n\n## Insert authored JSX or one component template\n\nCommands:\n\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"Product OS"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"@webstudio-is/sdk-components-react-radix:Switch"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"Form"}\n\nNotes:\n\n- Use MCP `insert-fragment` as the default way to author styled component trees. It converts JSX to a structured fragment before mutation.\n- Use only exact component ids returned by `components.search`, `components.get`, or `templates.get`. Never derive or guess component ids.\n- The `ws:` namespace contains specific Webstudio core components; it is not HTML-tag shorthand. Use `` for a native `div` and `` for a native form, never `` or ``.\n- For Webstudio\'s complete form structure, discover the Form component and insert its automatic template with `insert-component` using component `"Form"`.\n- MCP receives JSX as a JSON string because MCP arguments are JSON. The CLI converts it locally before the runtime mutation, so the project session receives structured Webstudio data, not JSX source.\n- In `insert-fragment` JSX, use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n- Do not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`.\n- Use Webstudio prop names such as `class` and `for`; do not use React aliases `className` or `htmlFor`.\n- Use Webstudio actions for event/action props, for example `onClick={new ActionValue(["event"], expression\\`console.log(event)\\`)}`. Do not pass JavaScript functions such as `onClick={() => ...}`.\n- Plain prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n- Template-backed components used in JSX must include required child/part components explicitly under the same parent structure as the template, for example ``.\n- Webstudio applies a registered template automatically when using `insert-component`, so composed components such as Switch include required child parts and styles.\n- Use `components.list`, `components.summary`, `components.search`, `components.get`, `templates.list`, and `templates.get` to discover known registry items, component ids, props, templates, insertability, and content model. Read `webstudio://project/components` only when those focused tools are insufficient.\n- Component/template registry items use a shadcn-compatible top-level shape plus Webstudio-specific superset metadata in `meta`. They are for Builder/MCP discovery, not a published shadcn install registry yet.\n- Known components with `contentModel.category: "none"` are not standalone-insertable; insert their root component template instead so required providers/parents are included.\n- Unknown custom component ids are a low-level extension mechanism, not a discovery fallback. Agents must not synthesize them.\n\n## Make a region editable in Content mode\n\nCommands:\n\n- MCP tool: insert-component {"parentInstanceId":"","component":"ws:block"}\n- MCP tool: inspect-instance {"instanceId":"","include":["children"]}\n\nNotes:\n\n- When a page will be handed to a Content-mode editor, wrap every region they should be able to edit in a Content Block (`ws:block`). Content-mode editors can edit text and supported props only in Content Block descendants. Content outside those blocks remains read-only, even when it looks like ordinary editable text.\n- Put reusable insertable options inside the Content Block\'s `ws:block-template` child. A template is source material, not editor content: editors cannot edit or delete it directly. When an editor inserts a template, its copy becomes a direct child of the Content Block and is editable.\n- Before handing off a page, verify with `inspect-instance` that the intended text, images, and links are inside a Content Block, and that templates include all required styling because Content-mode editors cannot use the Style panel.\n\n## Move elements\n\nCommands:\n\n- MCP tool: move-instance {"moves":"moves.json contents"}\n\nNotes:\n\n- Use `position: "end"` to append an instance. Repeating this for A and then B preserves the final order A, B.\n- A numeric `insertIndex` addresses the target parent\'s children before the moved instance is removed. Use it for exact placement; do not calculate the last index to append.\n- Moves in one `moves` array are applied sequentially in array order.\n\n## Clone element subtree\n\nCommands:\n\n- MCP tool: clone-instance {"sourceInstanceId":"","targetParentInstanceId":""}\n\n## Delete element subtree\n\nCommands:\n\n- MCP tool: delete-instance {"instanceIds":[""]}\n\n## List text/expression children\n\nCommands:\n\n- MCP tool: list-texts {"pagePath":"/"}\n\n## Update text child\n\nCommands:\n\n- MCP tool: update-text {"instanceId":"","childIndex":0,"text":"Launch faster"}\n\n## Replace bounded literal text\n\nCommands:\n\n- MCP tool: replace-text {"find":"Start free","replace":"Get started","match":"exact","pagePath":"/pricing","limit":20}\n\nNotes:\n\n- This changes only literal text children, never expression children. Scope it to pagePath or pageId and set a limit before a broad replacement.\n\n## Replace bounded static prop text\n\nCommands:\n\n- MCP tool: replace-prop-text {"find":"old.example.com","replace":"www.example.com","match":"substring","names":["href","code"],"limit":20}\n\nNotes:\n\n- This changes only static string props such as href, alt, aria-label, title, and HTML embed code. It never changes expressions, resources, actions, assets, or other dynamic bindings. Use names or instanceIds and a limit to narrow the change.\n\n## Replace bounded resource text\n\nCommands:\n\n- MCP tool: replace-resource-text {"find":"api.old.example.com","replace":"api.example.com","fields":["url"],"limit":20}\n\nNotes:\n\n- This changes resource names and fixed URL literals only. It skips dynamic URL expressions, headers, search parameters, request bodies, and GraphQL query code.\n\n## Update props\n\nCommands:\n\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: replace-prop-text {"find":"Old label","replace":"New label","names":["aria-label","title"],"limit":20}\n\nNotes:\n\n- Use this for fixed prop values such as `aria-label`, `alt`, `id`, static `href`, and other direct string/number/boolean/json prop values.\n\n## Add JSON-LD structured data\n\nCommands:\n\n- MCP tool: components.get {"component":"JsonLd"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"JsonLd"}\n- MCP tool: update-props {"updates":[{"instanceId":"","name":"code","type":"string","value":"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Organization\\",\\"name\\":\\"Acme\\"}"}]}\n- MCP tool: audit {"scopes":["seo"],"pagePath":"/"}\n\nNotes:\n\n- Prefer placing `JsonLd` inside `HeadSlot`.\n- Store `code` as a JSON object or array encoded as a compact string. The Builder formats it for editing.\n- The semantic prop update rejects malformed JSON and structurally invalid fixed JSON-LD with a precise JSON path.\n- The SEO audit also warns about a missing top-level `@context`, unknown or superseded Schema.org terms, properties unsupported by the supplied type, and incompatible primitive value types.\n- Schema.org vocabulary findings are warnings because custom vocabularies and extensions remain valid. Dynamic JSON-LD is marked as skipped for rendered validation.\n- Do not use bindings just to set static text.\n\n## Delete props\n\nCommands:\n\n- MCP tool: delete-props {"deletions":"props.json contents"}\n\n## Bind props to expressions/resources/actions\n\nCommands:\n\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n\nNotes:\n\n- Use this only when the prop should remain dynamic: expression, resource, action, or an existing scoped runtime context value such as `system`.\n- For a fixed string value, use `update-props` with `type:"string"` and a direct `value` instead.\n\n## Read styles\n\nCommands:\n\n- MCP tool: get-styles {"instanceIds":[""],"includeTokens":true}\n\n## Update local styles\n\nCommands:\n\n- MCP tool: update-styles {"updates":"styles.json contents"}\n\n## Delete local styles\n\nCommands:\n\n- MCP tool: delete-styles {"deletions":"styles.json contents"}\n\n## Replace matching style values\n\nCommands:\n\n- MCP tool: replace-styles {"property":"color","fromValue":{"type":"keyword","value":"red"},"toValue":{"type":"keyword","value":"blue"}}\n\n## List design tokens\n\nCommands:\n\n- MCP tool: list-design-tokens {}\n- MCP tool: list-design-tokens {"withUsage":true}\n- MCP tool: list-design-tokens {"verbose":true}\n\nNotes:\n\n- The default response is compact and includes token id, name, declaration count, and optional usage count.\n- Use `verbose:true` only when you need the full inline style declarations.\n\n## Create design tokens\n\nCommands:\n\n- MCP tool: create-design-token {"tokens":"tokens.json contents"}\n\n## Update design token styles\n\nCommands:\n\n- MCP tool: update-design-token-styles {"designTokenId":"","updates":"styles.json contents"}\n\n## Delete design token styles\n\nCommands:\n\n- MCP tool: delete-design-token-styles {"designTokenId":"","deletions":"styles.json contents"}\n\n## Attach design token to instances\n\nCommands:\n\n- MCP tool: attach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n\n## Detach design token from instances\n\nCommands:\n\n- MCP tool: detach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n\n## Extract design token from local styles\n\nCommands:\n\n- MCP tool: extract-design-token {"instanceIds":[""],"name":"Brand Primary","removeLocalProps":["color"]}\n\n## List CSS variables\n\nCommands:\n\n- MCP tool: list-css-variables {"withUsage":true}\n\n## Define CSS variables\n\nCommands:\n\n- MCP tool: define-css-variable {"vars":"vars.json contents"}\n\n## Delete CSS variables\n\nCommands:\n\n- MCP tool: delete-css-variable {"names":"names.json contents","force":true}\n\n## Rewrite CSS variable references\n\nCommands:\n\n- MCP tool: rewrite-css-variable-refs {"map":"variables.json contents"}\n\n## List data variables\n\nCommands:\n\n- MCP tool: list-variables {}\n- MCP tool: list-variables {"scopeInstanceId":""}\n\nNotes:\n\n- Data variables live in the internal `dataSources` namespace.\n- For raw `snapshot`, request the public `variables` namespace rather than the internal `dataSources` name. Raw patch payloads still use `dataSources` when applying direct changes.\n- Scope variables to the instance where they should become available. Descendants can use them in expressions, and nested variables with the same name mask outer variables.\n\n## Create data variable\n\nCommands:\n\n- MCP tool: create-variable {"scopeInstanceId":"","name":"title","value":{"type":"string","value":"Hello"}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"count","value":{"type":"number","value":3}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"featured","value":{"type":"boolean","value":true}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"tags","value":{"type":"string[]","value":["news","product"]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"filters","value":{"type":"json","value":{"tag":"news"}}}\n\nNotes:\n\n- Data variable values support `string`, `number`, `boolean`, `string[]`, and `json`.\n- Parameters are internal scoped runtime values provided by pages, collections, or components. They are not a public authoring surface: do not create, update, or delete parameter records. Use data variables/resources for user-authored data, and reference documented context values such as `system` only where they are already in scope.\n\n## Update data variable\n\nCommands:\n\n- MCP tool: update-variable {"dataSourceId":"","values":{"value":{"type":"json","value":{"count":1}}}}\n\n## Delete data variable\n\nCommands:\n\n- MCP tool: delete-variable {"dataSourceId":""}\n\n## List resources\n\nCommands:\n\n- MCP tool: list-resources {}\n- MCP tool: list-resources {"scopeInstanceId":""}\n\n## Create resource\n\nCommands:\n\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"https://api.example.com/posts","headers":[]}}\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"\\"https://api.example.com/posts?tag=\\" + filters.tag","headers":[]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Filtered Posts","method":"get","url":"https://api.example.com/posts","searchParams":[{"name":"tag","value":"filters.tag"},{"name":"source","value":{"type":"literal","value":"website"}}],"headers":[{"name":"Authorization","value":"\\"Bearer \\" + auth.token"}]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Post GraphQL","control":"graphql","method":"post","url":"https://api.example.com/graphql","headers":[{"name":"Content-Type","value":{"type":"literal","value":"application/json"}}],"body":"{ query: \\"query Post($slug: String!) { post(slug: $slug) { title } }\\", variables: { slug: system.params.slug } }"},"scopeInstanceId":"","dataSourceName":"post"}\n- MCP tool: create-resource {"resource":{"name":"Current Date","control":"system","method":"get","url":"/$resources/current-date","headers":[]},"scopeInstanceId":"","dataSourceName":"currentDate"}\n\nNotes:\n\n- Resource `url` accepts plain fixed URLs and paths such as `https://api.example.com/posts` and `/$resources/current-date`.\n- Resource `url` can also be a JavaScript expression when it is computed, such as `"https://api.example.com/posts?tag=" + filters.tag`.\n- Header values, search parameter values, and body accept expressions for dynamic values. For fixed text, use `{"type":"literal","value":"application/json"}`; Webstudio stores the required string expression for you.\n- Search parameter values, header values, and body expressions can read scoped variables and documented runtime context values such as `system` when they are available at the resource scope.\n- Add `scopeInstanceId` and `dataSourceName` when the resource result should be exposed as a scoped read data variable. Scoped resources are generated into the page resource `data` map and may be loaded during page rendering. Use this for read-oriented resources such as GET CMS/API data.\n- For submit/write/action resources, create the resource without `scopeInstanceId`, then bind a component prop such as a Form `action` with `bind-props` and `binding.type: "resource"`. Prop-bound resources are generated into the page resource `action` map instead of the read `data` map. Use this for POST, PUT, DELETE, webhooks, GraphQL submissions, and other explicit action flows.\n- Resource `method` can be `get`, `post`, `put`, or `delete`. Use GET for read data, POST for creates/GraphQL/webhooks/form submissions, PUT for full updates or replacements, and DELETE for deletion actions.\n- Optional `control` values are `graphql` and `system`. Use `graphql` for GraphQL-style requests, usually POST with a query body. Use `system` for built-in resources such as `"/$resources/sitemap.xml"`, `"/$resources/current-date"`, and `"/$resources/assets"` and when the resource should use the built-in `system` parameter. System fields are `system.origin`, `system.pathname`, `system.params`, and `system.search`.\n\n## Update resource\n\nCommands:\n\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: replace-resource-text {"find":"api.old.example.com","replace":"api.example.com","fields":["url"],"limit":20}\n\n## Delete resource\n\nCommands:\n\n- MCP tool: delete-resource {"resourceId":""}\n\n## List assets\n\nCommands:\n\n- MCP tool: list-assets {"withUsage":true}\n- MCP tool: list-assets {"verbose":true}\n\nNotes:\n\n- Compact results include each asset\'s folder id. Use `verbose:true` to include complete records for a page of assets, or `get-asset` to read one complete record including description, folder, creation time, and image/font metadata.\n- Image asset descriptions are the default alt text for asset-backed Image components.\n- To generate missing descriptions, inspect the image in its rendered page or asset source, write a concise description of its purpose, and save it on the asset rather than duplicating it on each Image instance.\n\n## Get asset\n\nCommands:\n\n- MCP tool: get-asset {"assetId":""}\n\n## List asset folders\n\nCommands:\n\n- MCP tool: list-asset-folders {}\n\n## Create asset folder\n\nCommands:\n\n- MCP tool: create-asset-folder {"name":"Marketing"}\n- MCP tool: create-asset-folder {"name":"Photos","parentId":""}\n\n## Update asset folder\n\nCommands:\n\n- MCP tool: update-asset-folder {"folderId":"","values":{"name":"Brand"}}\n- MCP tool: update-asset-folder {"folderId":"","values":{"parentId":""}}\n- MCP tool: update-asset-folder {"folderId":"","values":{"parentId":null}}\n\nNotes:\n\n- Updating `parentId` is the folder equivalent of cut and paste. Use `null` to move a folder to Root.\n\n## Duplicate asset folder\n\nCommands:\n\n- MCP tool: duplicate-asset-folder {"folderId":""}\n- MCP tool: duplicate-asset-folder {"folderId":"","parentId":""}\n\nNotes:\n\n- Duplication recursively copies descendant folders and assets. This is the folder equivalent of copy and paste.\n\n## Delete asset folder\n\nCommands:\n\n- MCP tool: delete-asset-folder {"folderId":""}\n\nNotes:\n\n- Deleting a folder recursively deletes its descendant folders and assets.\n\n## Update asset metadata\n\nCommands:\n\n- MCP tool: update-asset {"assetId":"","values":{"description":"Team collaborating around a whiteboard"}}\n\nNotes:\n\n- Use an empty description only when the image is intentionally decorative.\n- Updating an image asset description updates the default alt text wherever that asset is used with an asset-backed alt prop.\n\n## Generate missing image descriptions with an agent\n\nCommands:\n\n- MCP tool: audit {"scopes":["accessibility"],"verbose":true}\n- MCP tool: set-image-descriptions {"updates":[{"assetId":"hero-id","description":"Team collaborating around a whiteboard"},{"assetId":"texture-id","decorative":true}]}\n- MCP tool: audit {"scopes":["accessibility"]}\n\nNotes:\n\n- Start from `missing-image-description` findings. Inspect each image in its rendered page context before writing text.\n- The vision-capable agent generates the wording; the CLI validates and stores it but does not contain its own vision model.\n- Use `decorative:true` only when the image adds no information. This intentionally stores an empty description so later audits do not report it as missing.\n- Re-run the accessibility audit after the update. Asset-backed Image components use the saved asset description as their default alt text.\n\n## Manage fonts\n\nCommands:\n\n- MCP tool: list-fonts {"includeSystem":true}\n- MCP tool: list-assets {"type":"font"}\n- MCP tool: upload-asset {"asset":{"name":"acme-sans.woff2","type":"font","format":"woff2","meta":{"family":"Acme Sans","style":"normal","weight":400}},"assetsDir":".webstudio/assets"}\n- MCP tool: update-styles {"updates":"styles.json contents"}\n\nNotes:\n\n- Use `list-fonts` to discover uploaded families and system stacks. Upload/delete fonts through the existing asset tools, then apply a family with a `font-family` style declaration.\n\n## Upload one asset\n\nCommands:\n\n- MCP tool: upload-asset {"asset":{"name":"image.png","type":"image","format":"png","meta":{"width":1200,"height":630}},"assetsDir":".webstudio/assets"}\n- MCP tool: upload-asset {"asset":{"name":"image.png","type":"image","format":"png","folderId":"","meta":{"width":1200,"height":630}},"assetsDir":".webstudio/assets"}\n\n## Upload asset batch\n\nCommands:\n\n- MCP tool: upload-assets {"assets":[{"name":"image.png","type":"image","format":"png","meta":{"width":1200,"height":630}}],"assetsDir":".webstudio/assets"}\n\n## Duplicate asset\n\nCommands:\n\n- MCP tool: duplicate-asset {"assetId":""}\n- MCP tool: duplicate-asset {"assetId":"","folderId":""}\n- MCP tool: duplicate-asset {"assetId":"","folderId":null}\n\nNotes:\n\n- Duplication is the asset equivalent of copy and paste. Updating `folderId` is the equivalent of cut and paste; use `null` for Root.\n\n## Download asset\n\nCommands:\n\n- MCP tool: download-asset {"assetId":""}\n\n## Find asset usage\n\nCommands:\n\n- MCP tool: find-asset-usage {"assetId":""}\n\n## Replace asset references\n\nCommands:\n\n- MCP tool: replace-asset {"fromAssetId":"","toAssetId":""}\n\n## Delete assets\n\nCommands:\n\n- MCP tool: delete-asset {"assetIdsOrPrefixes":[""],"force":true}\n\n## Publish project\n\nCommands:\n\n- webstudio publish deploy --target production --json\n\n## List publishes\n\nCommands:\n\n- webstudio publish list --json\n\n## Check publish job\n\nCommands:\n\n- webstudio publish status --job --json\n\n## Unpublish\n\nCommands:\n\n- webstudio publish unpublish --target production --confirm --json\n\n## List domains\n\nCommands:\n\n- webstudio domains list --json\n\n## Create domain\n\nCommands:\n\n- webstudio domains create --domain example.com --json\n\n## Update domain\n\nCommands:\n\n- webstudio domains update --domain-id --domain www.example.com --json\n\n## Delete domain\n\nCommands:\n\n- webstudio domains delete --domain-id --confirm --json\n\n## Verify domain\n\nCommands:\n\n- webstudio domains verify --domain-id --json\n\n## Make arbitrary store-level changes\n\nCommands:\n\n- MCP tool: inspect {}\n- MCP tool: snapshot {"include":[""]}\n- MCP tool: apply-patch {"baseVersion":"","transactions":"patch.json contents"}\n\nNotes:\n\n- Use only when no semantic command exists.\n\n## Manage marketplace metadata\n\nCommands:\n\n- MCP tool: get-marketplace-product {}\n- MCP tool: update-marketplace-product {"category":"pageTemplates","name":"Acme Template","thumbnailAssetId":"asset-id","author":"Acme Studio","email":"hello@example.com","website":"https://example.com","issues":"","description":"Reusable template project for Acme landing pages."}\n\nPatch namespaces:\n\n- marketplaceProduct\n\n## Search and inspect safely\n\nCommands:\n\n- MCP tool: search-project {"query":"pricing"}\n- MCP tool: search-project {"query":"api.example.com","scopes":["resources"]}\n- MCP tool: list-instances {"pagePath":"/","maxDepth":5}\n- MCP tool: inspect-instance {"instanceId":"","include":["props","styles","children"]}\n- MCP tool: list-texts {"pagePath":"/"}\n- MCP tool: list-assets {"withUsage":true}\n- MCP tool: find-asset-usage {"assetId":""}\n- MCP tool: snapshot {"include":["pages","instances","props","resources","assets"]}\n\nNotes:\n\n- Use `search-project` for query-driven lookup across labels, text, prop values, resource URLs, asset metadata, and styles. Use `audit` for project health findings.\n\n## Audit project quality\n\nCommands:\n\n- webstudio audit --json\n- webstudio audit --scopes accessibility --scopes seo --json\n- webstudio audit --page-path /pricing --json\n- webstudio audit --scopes accessibility --verbose --json\n- webstudio audit --rendered --page-path /pricing --json\n- webstudio audit --rendered --route-example post=/blog/hello --json\n- webstudio audit --rendered --image-domain images.example.com --json\n- MCP tool: audit {}\n- MCP tool: audit {"scopes":["accessibility","security"],"severities":["error","warning"]}\n- MCP tool: audit {"scopes":["accessibility"],"verbose":true}\n- MCP tool: audit {"scopes":["craft"],"verbose":true}\n- MCP tool: audit {"rendered":true,"verbose":true}\n\nNotes:\n\n- With no scopes, `audit` checks accessibility, security, SEO, performance settings, unused assets, ineffective Collection styles, non-GET resources exposed as render-time data, and unused or duplicate style data.\n- Craft is opt-in and read-only. Run `audit` with `scopes:["craft"]` to detect whether the project is not using Craft, partially compatible, or compatible with the versioned Craft 1.2 profile. `profileStatuses` includes the University-doc provenance and the smallest safe next action. The audit never installs Craft or changes a non-Craft project.\n- The `performance` scope reports disabled atomic CSS generation. A rendered audit also measures broken, eager below-fold, and oversized images, browser-marked render-blocking resources, and legacy font formats.\n- Rendered image and resource metrics run only when the selected scopes include `performance`; responsive layout dimensions remain available whenever `rendered:true` is requested.\n- Compact findings include stable ids, severity, message, and location. Use `--verbose` or `{"verbose":true}` for evidence, explanation, suggested remediation, skipped-check details, and manual-check workflows.\n- `summary` counts all findings before severity filtering and pagination.\n- `contractVersion` identifies the audit response contract. Handle a new value before assuming existing fields retain the same meaning.\n- Expression-, resource-, and parameter-backed values that cannot be checked reliably appear in `skippedChecks`; they are not treated as passing or failing.\n- Page filters apply to page-owned accessibility, security, and SEO checks. Asset and style usage remain project-wide to avoid false unused findings.\n- Continue paginated results with `cursor`. Restart the audit if the project version changes.\n- Verbose skipped-check and manual-check details are included on the first findings page only; their total counts remain available on every page.\n- `manualChecks` describes responsive, hierarchy, and contrast checks that require preview screenshots and vision.\n- Focused audits return only manual checks relevant to their selected scopes.\n- In a long-lived MCP session, `{"rendered":true}` reuses preview and screenshot\n tools to capture every static page at mobile, desktop, and Builder breakpoint\n edges. Compact output reports rendered check/issue/failure counts; verbose\n output includes screenshot paths and measured layout dimensions.\n- Dynamic route templates are skipped unless `--route-example =`\n (or MCP `routeExamples`) supplies a concrete path. The path must not contain\n unresolved `:` or `*` parameters.\n- Plans above 120 captures return a short-lived confirmation token. Review the\n unchanged plan, then rerun with `--confirm-large-run` and\n `--confirmation-token`.\n- Detailed rendered evidence is stored in a versioned manifest under\n `.webstudio/audits`; compact output includes its path and screenshot count.\n- Rendered checks also report broken images, eager images below the fold, and\n image sources more than 2x their rendered dimensions in both axes, including\n Webstudio instance ids and measured dimensions when available.\n- Rendered checks include sanitized Resource Timing evidence and report\n browser-marked render-blocking resources plus legacy `.ttf`, `.otf`, and\n `.woff` fonts without applying a universal transfer-size threshold.\n- Fix findings through semantic mutation commands, then rerun `audit` to confirm their deterministic finding ids disappeared.\n\n## Verify dynamic bindings\n\nCommands:\n\n- webstudio verify-bindings --json\n- MCP tool: verify-bindings {"pagePath":"/pricing"}\n- MCP tool: verify-bindings {"instanceId":"","limit":50}\n\nNotes:\n\n- Statically checks persisted text expressions, expression/action/resource/parameter props, resource expressions, and page metadata.\n- Findings distinguish invalid syntax, unknown or out-of-scope variables, stale internal data-source ids, and missing resource or parameter references.\n- Page and instance filters can be combined when the instance belongs to the selected page. Continue findings with `cursor`.\n- This operation does not resolve rendered values or execute external resources. Preview representative loading, empty, error, and populated states after static findings are fixed.\n\n## Refactor targeted content\n\nCommands:\n\n- MCP tool: list-instances {"pagePath":"/"}\n- MCP tool: list-texts {"pagePath":"/"}\n- MCP tool: update-text {"instanceId":"","childIndex":0,"text":"Launch faster"}\n- MCP tool: replace-text {"find":"Old headline","replace":"New headline","match":"exact","pagePath":"/pricing","limit":20}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans"}}}\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: replace-asset {"fromAssetId":"","toAssetId":""}\n- MCP tool: replace-styles {"property":"color","fromValue":{"type":"keyword","value":"red"},"toValue":{"type":"keyword","value":"blue"}}\n- MCP tool: rewrite-css-variable-refs {"map":"variables.json contents"}\n\nNotes:\n\n- Use focused reads first, then mutate only matching instances, props, metadata, resource URLs, assets, or style references. Use `replace-text` for bounded literal text changes, `replace-prop-text` for bounded static prop text, `replace-resource-text` for fixed resource names/URLs, and `update-text` for one known child or expressions.\n\n## Optimize existing project\n\nCommands:\n\n- MCP tool: list-pages {}\n- MCP tool: list-folders {}\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans"}}}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: list-breakpoints {}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"maxWidth":1023}}\n- MCP tool: get-styles {"instanceIds":[""],"includeTokens":true}\n- MCP tool: update-styles {"updates":"styles.json contents"}\n- MCP tool: attach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n- MCP tool: update-project-settings {"meta":{"siteName":"Acme"}}\n\nNotes:\n\n- Use this for SEO metadata, accessibility labels, responsive behavior, token consistency, and project settings.\n\n## Connect external data\n\nCommands:\n\n- MCP tool: create-variable {"scopeInstanceId":"","name":"title","value":{"type":"string","value":"Hello"}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"tags","value":{"type":"string[]","value":["news","product"]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"filters","value":{"type":"json","value":{"tag":"news"}}}\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"https://api.example.com/posts","searchParams":[{"name":"tag","value":"filters.tag"},{"name":"source","value":{"type":"literal","value":"website"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Post GraphQL","control":"graphql","method":"post","url":"https://api.example.com/graphql","headers":[{"name":"Content-Type","value":{"type":"literal","value":"application/json"}}],"body":"{ query: \\"query Post($slug: String!) { post(slug: $slug) { title } }\\", variables: { slug: system.params.slug } }"},"scopeInstanceId":"","dataSourceName":"post"}\n- MCP tool: create-resource {"resource":{"name":"Current Date","control":"system","method":"get","url":"/$resources/current-date","headers":[]},"scopeInstanceId":"","dataSourceName":"currentDate"}\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"{/_ collection content _/}"}\n\nNotes:\n\n- Use this for CMS sections, blog listings, Ghost/headless CMS pages, n8n-style integrations, and API URLs built from variables.\n- For read data, expose GET resources as scoped data variables with `scopeInstanceId`/`dataSourceName` and read the loaded result from the resource result wrapper, usually `.data`.\n- For writes, webhooks, GraphQL submissions, and deletes, prefer unscoped resources bound to Form `action` props so they become action resources instead of auto-loaded read resources.\n- Use direct props for fixed values and prop bindings only when a prop must read a data variable, resource, action, or documented runtime context value such as `system`.\n\n## Render an array or object as repeated content\n\nCommands:\n\n- MCP tool: insert-collection {"parentInstanceId":"","data":{"type":"expression","value":"posts.data.items"},"itemFragment":"{expression`collectionItem.title`}"}\n- MCP tool: inspect-instance {"instanceId":"","include":["props","bindings","children"]}\n\nNotes:\n\n- Use Collection whenever an array or object from a resource or data variable should render a list, grid, cards, table rows, options, tabs, or other repeated UI.\n- Pass `insert-collection` the complete array or object. Do not pass the resource response wrapper or one indexed item. External resource arrays are commonly nested under the scoped resource result\'s `data` field or deeper.\n- Pass one repeated-item Webstudio JSX root. The command creates the Collection, its private current-item/current-key parameters, the iterable binding, and descendant item bindings atomically.\n- Collection renders the item root once for every entry. Use `expression` values such as `collectionItem.title` in descendant text and props. Object iteration also exposes `collectionItemKey`.\n- Wrap multiple repeated sibling instances in one Element inside Collection.\n- For repeated Radix items such as accordion items, tabs, or menu options, bind a stable unique id or slug to every required `value` prop.\n- See the [Collection documentation](https://docs.webstudio.is/university/core-components/collection) for the equivalent Builder workflow.\n\n## Support dynamic runtime behavior\n\nCommands:\n\n- MCP tool: integrate-runtime-ui {"parentInstanceId":"","resources":[{"resource":{"name":"Seats","method":"get","url":"https://api.example.com/seats","headers":[]},"dataSourceName":"Seats","exposeAsDataSource":true}],"structure":{"type":"collection","data":{"type":"expression","value":"Seats.data"},"itemFragment":{"children":[{"type":"id","value":"seat"}],"instances":[{"type":"instance","id":"seat","component":"Text","children":[{"type":"expression","value":"collectionItem.label"}]}],"props":[],"dataSources":[],"resources":[],"styleSources":[],"styleSourceSelections":[],"styles":[],"breakpoints":[],"assets":[]}},"retainedBehavior":[{"instanceId":"","responsibility":"Seat selection behavior"}]}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n- MCP tool: create-resource {"resource":{"name":"Seats","method":"get","url":"https://api.example.com/seats","headers":[]}}\n- MCP tool: snapshot {"include":["instances","props","resources"]}\n- MCP tool: apply-patch {"baseVersion":"","transactions":"patch.json contents"}\n\nNotes:\n\n- Use `integrate-runtime-ui` to create variables/resources, insert one editable fragment or Collection, and add safe data bindings in one transaction.\n- List existing script-owned responsibilities under `retainedBehavior`. The operation preserves those instances and never evaluates or accepts replacement script bodies.\n- `unsupportedConversions` records behavior that cannot be represented safely. Dry-run returns the complete transaction and the same retained/unsupported report without changing the project.\n- New actions and HtmlEmbed scripts are intentionally rejected. Create normal editable components and data bindings; keep opaque runtime behavior in existing script instances.\n\n## Build authenticated pages\n\nCommands:\n\n- MCP tool: meta.guide {"brief":"Build a Supabase-authenticated account page"}\n- MCP tool: create-page {"name":"Account","path":"/account"}\n- MCP tool: update-page {"pageId":"","values":{"meta":{"auth":{"method":"basic","login":"","password":""}}}}\n- MCP tool: create-resource {"resource":{"name":"Session","method":"get","url":"https://api.example.com/session","headers":[]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"user","value":{"type":"json","value":{}}}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n\nNotes:\n\n- Inspect and reuse the project\'s existing auth convention before authoring. Do\n not add a second provider or session model implicitly.\n- Model signed-out, loading, signed-in, and failed-auth states explicitly.\n- Never store credentials, service-role keys, refresh tokens, private session\n values, or authenticated response bodies in project data, command output,\n screenshots, agent instructions, or error reports. Privileged provider calls\n and authorization enforcement belong server-side.\n- Basic auth is semantic today. Provider-specific Supabase/Firebase setup still\n uses the existing resource, variable, prop, binding, and embed tools; there is\n no provider-specific installer.\n\n## Generate from design input\n\nCommands:\n\n- MCP tool: meta.guide {"brief":"Recreate this Figma design as a responsive page"}\n- MCP tool: create-page {"name":"Landing","path":"/landing"}\n- MCP tool: create-design-token {"tokens":"tokens.json contents"}\n- MCP tool: define-css-variable {"vars":"vars.json contents"}\n- MCP tool: list-breakpoints {}\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"Section copy"}\n- MCP tool: update-styles {"updates":[{"instanceId":"","breakpointId":"","property":"padding-left","value":{"type":"unit","unit":"px","value":24}}]}\n- MCP tool: preview.start {"host":"127.0.0.1","port":5173}\n- MCP tool: screenshot {"path":"/landing","output":"landing-desktop.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"path":"/landing","output":"landing-mobile.png","viewport":{"width":390,"height":844},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"baseUrl":"http://127.0.0.1:5177","path":"/landing","output":"landing-desktop.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n\nNotes:\n\n- Use this after the agent can inspect the supplied design. There is no direct\n Figma, screenshot, Inception, or `design.md` import command.\n- Inspect and reuse existing variables, tokens, styles, components, assets, and\n page patterns before authoring. Build semantic editable structure rather than\n flattening the design into an image or absolute-positioned approximation.\n- Verify one familiar viewport inside every distinct Builder breakpoint range,\n then run rendered audit and inspect the screenshots before completion.\n\n## Cross-project maintenance\n\nCommands:\n\n- webstudio mcp run .temp/projects.json\n- webstudio mcp run .temp/projects.json --dry-run\n- webstudio mcp run .temp/projects.json --approve-mutations --concurrency 2\n\nNotes:\n\n- Put shared `calls` and a `projects` array of independently linked project roots in the existing `mcp run` manifest. Project roots are relative to the manifest file.\n- Each project uses its own config, authentication, ProjectSession storage, checkpoint, and failure boundary. Confirmed successful calls are checkpointed. Reads can resume automatically; a mutation interrupted after dispatch is reported as ambiguous and is not replayed automatically, preventing silent duplicate writes.\n- Focus the manifest on bounded reads or audits first. Use per-call `dryRun`, global `--dry-run`, or explicitly approve committed mutations with `--approve-mutations` after reviewing the manifest.\n\n# Known CLI Gaps\n\n## Provider-specific authenticated pages\n\nMissing:\nCLI supports page basic auth and generic resources/props/embeds, but not guided Supabase/Firebase auth setup.\n\nCurrent fallback:\nCall `meta.guide` with the provider-authenticated page goal, then create the\npage, resources, variables, props, bindings, and embeds with existing semantic\ntools.\n\nSuggested commands:\n\n- setup-auth-page\n\n## Generate from design input\n\nMissing:\nNo command imports Figma, screenshots, Inception output, or design.md and turns it into pages/tokens/layout.\n\nCurrent fallback:\nCall `meta.guide` with the design-input goal, let the agent inspect the supplied\ndesign, then use semantic page, token, asset, fragment, style, preview,\nscreenshot, and audit tools. Use `apply-patch` only when no semantic operation\nfits.\n\nSuggested commands:\n\n- generate-from-design\n\n## Built-in cross-project maintenance\n\nMissing:\nPublic API and CLI intentionally operate on one configured project at a time; there is no built-in multi-project discovery or loop runner.\n\nCurrent fallback:\nRun the CLI from an external script that reconfigures one project/session at a time.\n\nSuggested commands:\n\n- none\n', "manual-api": '# Webstudio API CLI Manual\n\nThe API commands operate on the single project configured by:\n\n- .webstudio/config.json: projectId\n- global Webstudio config: origin and token\n\n- Pass --json to API/discovery commands that support it. Do not add --json to top-level commands unless their help/schema documents it.\n- Never pass a project id. Commands use configured project only.\n- Read ids before writing. Do not invent ids for existing records.\n- stdout is one JSON object. stderr is diagnostics.\n- Prefer MCP semantic tools for detailed project edits. Use MCP apply-patch only when no semantic tool exists.\n\n## Start\n\n{{start}}\n\n## Read First\n\n{{readFirst}}\n\n## Project Session Cache\n\n- CLI commands use one local ProjectSession snapshot for the configured project.\n- Local-capable reads use cached namespaces when compatible and fetch only missing or stale namespaces.\n- Local-capable mutations build patches from the local snapshot, then commit with the cached build version.\n- Successful mutation commits update the local snapshot only after the remote commit succeeds.\n- Server-only commands run remotely and invalidate/refetch namespaces declared by the operation catalog.\n- Use --refresh on local-capable commands to refresh required namespaces before running.\n- Successful JSON responses include compact meta.session with operationId, buildId, version, source, committed, namespaceCounts, diagnosticCount, non-empty diagnostic summaries, and optional compatibilityVersion.\n\n## CLI Capability Inventory\n\nFor a short end-consumer summary of what MCP can do, see\n`manual mcp` / `webstudio man mcp`. The MCP inventory describes the same\nproject, page, element, style, data, asset, publish, domain, and visual\nverification capabilities without internal command names.\n\n### Top-Level Commands\n\n{{topLevelCapabilityIndex}}\n\n### High-Level API Commands By Area\n\n{{apiCapabilityIndex}}\n\n### MCP Tool Operations\n\nThese are MCP tools. From a shell, call them with the shortcut form `webstudio \'\'` or with the explicit form `webstudio mcp single-op-call \'\'`:\n\n{{mcpOnlyCommandIndex}}\n\n## Task Recipes\n\n{{taskRecipeIndex}}\n\n## Use Case Index\n\n{{useCaseIndex}}\n\n## Known CLI Gaps\n\n{{knownCliGapIndex}}\n\n## Input File Shapes\n\n{{inputFileShapeIndex}}\n\n## Raw Patch Fallback\n\napply-patch accepts either BuildPatchTransaction[] or { "transactions": BuildPatchTransaction[] }.\n\nEach transaction has:\n\n{\n"id": "patch-transaction-label",\n"payload": [\n{\n"namespace": "projectSettings",\n"patches": [\n{ "op": "replace", "path": ["meta", "siteName"], "value": "New Site" }\n]\n}\n]\n}\n\nThe transaction id is a patch label used for optimistic synchronization. It is\nnot a Builder record id. Do not invent ids for pages, instances, props,\nbreakpoints, resources, variables, folders, assets, or other project records.\n\nPatch paths are JSON-patch-like paths into Builder store data. Map-like namespaces use ids as the first path item.\n\nSupported namespaces:\n\n- pages: redirects, page records, and folders\n- projectSettings: project-wide metadata and compiler settings\n- instances: element instances and children, including text/expression children\n- props: element props, bindings, page references, resource bindings\n- styles: CSS declarations keyed by style declaration key\n- styleSources: local style sources and reusable design tokens\n- styleSourceSelections: instance-to-style-source connections\n- dataSources: data variables, parameters, and resource data sources\n- resources: data resource definitions\n- assets: project asset records handled by the existing asset patch path\n- breakpoints: responsive breakpoints\n- marketplaceProduct: marketplace metadata\n\n## Data Sources\n\n`dataSources` is the internal Builder namespace for variables. Public API, CLI,\nand MCP tools expose it through two user-facing groups:\n\n- data variables: `list-variables`, `create-variable`, `update-variable`, and\n `delete-variable`\n- data resources: `list-resources`, `create-resource`, `update-resource`, and\n `delete-resource`\n\nFor raw `snapshot`, request the public `variables` namespace rather than the\ninternal `dataSources` name. Raw patch payloads still use `dataSources` when\napplying direct changes.\n\nVariables can be scoped to an instance. Expressions under that instance can use\nthe variable by name; nested variables with the same name mask outer variables.\nVariable values support `string`, `number`, `boolean`, `string[]`, and `json`.\nUse `string[]` for lists of strings such as tags or selected categories; use\n`json` for objects, arrays with mixed shapes, or nested API filter state.\nParameters are internal scoped runtime values provided by pages, collections,\nor components. They are not a public authoring surface: do not create, update,\nor delete parameter records. Public tools should preserve existing parameter\nrecords and may reference documented context values such as `system` in\nexpressions where they are already in scope.\n\nResource `url` accepts plain fixed URLs and paths, for example\n`https://api.example.com/posts` or `/$resources/current-date`. Dynamic URLs can\ncombine strings and variables, for example\n`"https://api.example.com/posts?tag=" + filters.tag`. Prefer `searchParams` for\nquery parameters that should be encoded separately:\n`[{ "name": "tag", "value": "filters.tag" }]`. Header values, search parameter\nvalues, and bodies are expressions for dynamic content. For fixed text, use\n`{ "type": "literal", "value": "application/json" }`; Webstudio stores the\nrequired string expression. Headers can still read variables such as\n`"Bearer " + auth.token`, and GraphQL bodies can return objects such as\n`{ query: "...", variables: { slug: system.params.slug } }`.\n\nCreate a GET resource with `scopeInstanceId` when the fetched resource result\nshould be available as a read data variable. Scoped GET resources default to\n`exposeAsDataSource: true`, are generated into the page resource `data` map,\nand may be loaded during page rendering. Use `dataSourceName` to choose the\nvariable name.\n\nFor submit/write/action resources, create the resource without\n`scopeInstanceId`, then bind a component prop such as a Form `action` to the\nresource with `bind-props` and `binding.type: "resource"`. Prop-bound resources\nare generated into the page resource `action` map instead of the read `data`\nmap. Use this shape for POST, PUT, DELETE, webhook, and other resources that\nshould run only from an explicit form/action flow, not merely because the page\nrendered.\n\nPOST, PUT, and DELETE resources default to `exposeAsDataSource: false` even\nwhen a scope is supplied. Set `exposeAsDataSource: true` only when a write-method\nresource intentionally provides render-time data, such as a read-only GraphQL\nPOST query. A scope is required, and the result includes a warning because the\nrequest may execute during page rendering. Set `exposeAsDataSource: false` on\n`update-resource` to detach an existing render-time data source.\n\nResource `method` can be `get`, `post`, `put`, or `delete`. Use GET for read\ndata. Use POST for creates, GraphQL requests, webhooks, and form submissions.\nUse PUT for full updates/replacements. Use DELETE for deletion actions.\nOptional `control` values are `graphql` and `system`: `graphql` marks a\nGraphQL-style resource, usually POST with a query body; `system` marks a\nresource intended to use the built-in `system` parameter or one of the built-in\nlocal resource URLs: `"/$resources/sitemap.xml"`,\n`"/$resources/current-date"`, and `"/$resources/assets"`. The system parameter\nfields are `system.origin`, `system.pathname`, `system.params`, and\n`system.search`.\n\nUse prop bindings for dynamic values that read variables or resources; use\ndirect props for static values.\n\nCommit raw patch:\n\nMCP tool: apply-patch\n\n## Raw Patch Examples\n\nRename the site:\n\n[\n{\n"id": "patch-site-name",\n"payload": [\n{\n"namespace": "projectSettings",\n"patches": [\n{ "op": "add", "path": ["meta", "siteName"], "value": "Acme Studio" }\n]\n}\n]\n}\n]\n\nUpdate page title metadata:\n\n[\n{\n"id": "patch-page-title",\n"payload": [\n{\n"namespace": "pages",\n"patches": [\n{ "op": "replace", "path": ["pages", "page-id", "meta", "title"], "value": "Pricing" }\n]\n}\n]\n}\n]\n\nUpdate a text child on an element:\n\n[\n{\n"id": "patch-text",\n"payload": [\n{\n"namespace": "instances",\n"patches": [\n{ "op": "replace", "path": ["instance-id", "children", 0, "value"], "value": "Launch faster" }\n]\n}\n]\n}\n]\n\nCreate records with semantic operations such as create-variable,\ncreate-resource, create-design-token, create-page, create-folder,\nand create-breakpoint. Raw patch rejects generated record\ncreation, collection replacement, record replacement with a different `id`, and\nrecord id field mutations in id-keyed namespaces because Webstudio must generate\nand preserve record ids.\n\n## Safety Rules\n\n- For MCP apply-patch, read the latest version with MCP snapshot before writing.\n- Reuse ids from MCP snapshot output when updating existing records.\n- Do not create generated records, replace generated record collections, replace records with different ids, or mutate record id fields with raw patch. Use semantic create operations so Webstudio generates ids.\n- If apply-patch reports a version conflict, read the latest build and regenerate the patch.\n- Prefer semantic MCP read tools for discovery, then use MCP snapshot for exact patch paths.\n\n## Command Index\n\n{{commandIndex}}\n', "manual-llm": '# Webstudio CLI Manual for LLMs\n\nUse this order. Stop only when a command returns ok:false.\n\nIf you are inside the Webstudio monorepo, the first command discovery should use\nthe local CLI exactly as `node packages/cli/local.js ...` from the repo root. Do\nnot use `packages/cli/bin.js` for local source-tree work; it is the packaged\nbuild entry and may use stale built output. Do not use `pnpm exec webstudio`,\n`pnpm --filter webstudio exec webstudio`, or a global `webstudio`: they can\nresolve an older binary.\n\nFor delegated design-system or “use every component” tasks, skip the generic warm-up sequence and start with exactly one MCP command: `webstudio workflow.next \'{"goal":"design-system-page"}\'`. Report that returned checkpoint to the parent/user and stop until continued.\n\n## Connect an MCP client\n\nWhen the user asks to connect the current folder to Webstudio, run the command\nfor the agent client you are currently using:\n\n- Claude Code: `webstudio connect claude`\n- Codex: `webstudio connect codex`\n- Cursor: `webstudio connect cursor`\n- VS Code or GitHub Copilot: `webstudio connect vscode`\n\nRun the command from the linked project root. If the folder is not linked, ask\nfor an editable Builder share link and run\n`webstudio init --link --json`, followed by `webstudio sync`, then\nretry `webstudio connect `. Treat the share link as a credential and do\nnot include it in committed files, logs, screenshots, or issue reports.\n\n`connect` verifies project access before changing client configuration. For\nClaude Code, Cursor, and VS Code it safely merges the `webstudio` server into\nthe client\'s project configuration. For Codex it runs both `codex mcp add` and\n`codex mcp get webstudio`; do not repeat those commands separately. Follow the\nreload, restart, or approval instruction printed by `connect`, then verify the\nloaded MCP connection by asking the client to use Webstudio MCP and list the\nproject pages. Use `--print` only to inspect the generated setup without\nchanging configuration or requiring project access.\n\n## Always\n\n1. webstudio permissions --json\n2. For bounded shell workflows, call MCP tools directly through the CLI shortcut, for example `webstudio meta.index` or `webstudio insert-fragment \'\' --dry-run`. The explicit form `webstudio mcp single-op-call \'\'` is equivalent and useful when you need to make the MCP boundary obvious. Use `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` for multiple calls in one shared CLI session. Use a normal JSON file path for large batches. Use long-running `webstudio mcp` only when your environment is a real MCP client. Do not manually send raw JSON-RPC to `webstudio mcp` from a shell or PTY.\n3. Read MCP `meta.index`, for example `webstudio meta.index`.\n4. Use focused MCP calls with concrete JSON: `webstudio meta.guide \'{"brief":"Create a design system page using every component"}\'`, `webstudio meta.get_more_tools \'{"tools":["insert-fragment"]}\'`, `webstudio components.list \'{"source":"all"}\'`, `webstudio components.coverage-plan`, `webstudio components.search \'{"brief":"radix select"}\'`, `webstudio components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'`, `webstudio templates.list`, and `webstudio templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'`.\n5. Read overview resources `webstudio://project/tools-overview` or `webstudio://project/components-overview` when useful. Read full resources `webstudio://project/tools` or `webstudio://project/components` only when focused tools are insufficient.\n6. Pick focused MCP read tool.\n7. Pick semantic MCP write tool.\n\nUse `webstudio schema mcp` for a compact MCP tool overview. Add `--verbose` only when exact input schemas for all tools are truly needed; otherwise prefer focused `meta.get_more_tools` and `components.*` calls.\n\nRun these commands from the linked project root. Use the MCP startup status line\'s absolute root for local files; write temporary scripts and artifacts under `/.temp`, not under a parent workspace.\n\nMonorepo quick path for a simple styled section:\n\n```sh\nnode packages/cli/local.js mcp single-op-call meta.index\nnode packages/cli/local.js mcp single-op-call meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nnode packages/cli/local.js mcp single-op-call insert-fragment \'{"parentInstanceId":"parent-id","fragment":"Launch KitA focused section created with Webstudio JSX.Get started"}\' --dry-run\n```\n\nThe same local shortcut form is shorter and preferred for simple shell steps:\n\n```sh\nnode packages/cli/local.js meta.index\nnode packages/cli/local.js meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nnode packages/cli/local.js insert-fragment \'{"parentInstanceId":"parent-id","fragment":"Launch KitA focused section created with Webstudio JSX.Get started"}\' --dry-run\n```\n\nFor this simple path, do not grep source files, dump full MCP resources, or write parser scripts first. Use `list-pages`, `get-page-by-path`, or `list-instances` only to get the target `parentInstanceId`.\n\nWhen authoring JSX for `insert-fragment`, use Webstudio component helpers and Webstudio style syntax. Use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n\nWhen the task says another user will edit a page in Content mode, use a Content Block (`ws:block`) around every editable region. Content-mode users can edit text and supported props only in descendants of that block; content outside it is read-only. Put reusable insertable options in the block\'s `ws:block-template` child. Do not put intended editor content inside that template container: templates are protected source material, while an inserted template copy becomes an editable direct child of the Content Block. Verify this structure before handoff.\n\nDo not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`. JSX fragments are declarative project data; use the built-in Webstudio helpers instead.\n\nUse Webstudio prop names in JSX: `class`, `for`, `aria-label`, and other HTML/Webstudio names. Do not use React-only aliases such as `className` or `htmlFor`; the runtime rejects them with the Webstudio prop name to use.\n\nUse Webstudio actions for event/action props. Do not pass JavaScript functions such as `onClick={() => ...}`; the runtime rejects them because functions cannot be persisted as Webstudio project data.\n\n```tsx\n\n Open\n\n```\n\nPlain JSX prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n\nIf a component has a registered template with required parts, JSX must include those parts explicitly under the same parent structure as the template, for example ``. Use `insert-component` when you want Webstudio to apply one component template automatically.\n\n## Animation Components\n\nBefore creating animation examples, inspect the exact components with focused discovery:\n\n```sh\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-animation:AnimateChildren"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-animation:AnimateText"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-animation:StaggerAnimation"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-animation:VideoAnimation"}\'\n```\n\nUse Animation Group (`AnimateChildren`) as the controller. Put normal instances directly inside it, or put Text Animation, Stagger Animation, or Video Animation directly inside it. Text, Stagger, and Video are helper components with `contentModel.category: "none"` and should not be used as standalone section roots.\n\nDefine timing and CSS changes on the Animation Group `action` prop. Use `type:"view"` for viewport entry/exit progress and `type:"scroll"` for scroll-progress timelines. For in animations, keep the canvas styles as the final state and use `fill:"backwards"` with keyframes that describe the starting state. For out animations, use `fill:"forwards"` with keyframes that describe the ending state.\n\nText Animation settings: `slidingWindow` defaults to `5`, `easing` defaults to `linear`, and `splitBy` defaults to `char`. Use `splitBy:"space"` for word-by-word animation. The parent Animation Group keyframes provide the actual opacity, translate, scale, or other styles.\n\nStagger Animation settings: `slidingWindow` defaults to `1` and `easing` defaults to `linear`. It applies parent Animation Group progress across its direct children. Use `slidingWindow:0` for instant sequential steps, `1` for one child at a time, and values above `1` for overlapping waves.\n\nVideo Animation settings: `timeline` is a boolean. Prefer `insert-component` for Video Animation so the Video child template is inserted, then configure the Video child asset/source. Use short, seek-friendly videos for smooth scroll-linked playback.\n\nUse JSX fragments for authored animation structures when you need styled, editable examples. Put the final visual state in `ws:style` and put the starting or ending animated state in the Animation Group `action` keyframes. Include an explicit `offset` on every keyframe: use `offset: 0` for starting-state keyframes with `fill:"backwards"` and `offset: 1` for ending-state keyframes with `fill:"forwards"`.\n\n```tsx\n\n \n Launch metrics\n \n A polished card that fades up as it enters the viewport.\n \n \n\n```\n\nFor Text Animation, keep `animation.AnimateText` as the direct child of Animation Group and place the text-containing element inside it:\n\n```tsx\n\n \n Animate words with controlled rhythm\n \n\n```\n\nFor Stagger Animation, put the repeated cards or rows directly inside `animation.StaggerAnimation`:\n\n```tsx\n\n \n \n Plan\n \n \n Build\n \n \n Launch\n \n \n\n```\n\nFor Video Animation, use the registered template via `insert-component` when possible. If you author JSX, include the Video child explicitly:\n\n```tsx\n\n \n <$.Video\n preload="auto"\n autoPlay={true}\n muted={true}\n playsInline={true}\n crossOrigin="anonymous"\n />\n \n\n```\n\n## Command Surface Boundary\n\n- Use top-level `webstudio ...` shell commands for setup, sync/import/build/preview/screenshot, permissions, publish/domains, schema, registry inspection, man, and starting MCP.\n- Use MCP tools for Builder project data manipulation: pages, instances/components, props, text, styles, tokens, variables, resources, assets, breakpoints, redirects, and raw patches.\n- From a shell, call MCP tools with the shortcut form `webstudio \'\'`, for example `webstudio insert-fragment \'\' --dry-run`. The explicit equivalent is `webstudio mcp single-op-call \'\'`. Use `--input-file` for large payloads.\n- Inside the Webstudio monorepo, call the local CLI as its own command: `node packages/cli/local.js ...`. Do not wrap the CLI call in `pwd && ...`, command substitution, `pnpm exec webstudio`, `pnpm --filter webstudio exec webstudio`, or a global `webstudio`.\n- For experiments, pass `--dry-run` to local-capable mutation calls. Read the computed transaction from `meta.session.transaction` and its base build version from `meta.session.version`. Copying a `.webstudio` folder is not an isolated project clone; `.webstudio/config.json` still points to the same remote project, so non-dry-run mutations can commit to that project.\n- For bounded multi-step shell work, run inline JSON with `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'`; this reuses one CLI session without raw JSON-RPC. For large batches, write `{ "calls": [{ "tool": "..." }] }` to a normal JSON file and run `webstudio mcp run .temp/mcp-calls.json`.\n- Use JSON strings for `brief` fields. Never pass boolean flags such as `{"brief":true}`.\n- Treat `webstudio mcp single-op-call` and `webstudio mcp run` stderr lines as progress checkpoints; stdout remains JSON on both success and failure. On failure, parse stdout for `{ "ok": false, "error": { "code": "...", "message": "..." } }` before deciding what to fix.\n- If a CLI/MCP tool crashes, hangs, gives a confusing error, needs an undocumented workaround, or forces source-code inspection for normal usage, ask the user to report it in Discord `#help` at https://wstd.us/community. Give them a complete copy-paste report with the goal, expected behavior, actual error, exact command/tool call, stdout JSON, stderr/lifecycle logs, environment, workaround, and secrets redacted.\n- Run one-shot `webstudio mcp single-op-call` commands sequentially against a linked `.webstudio` folder. If a command returns `PROJECT_SESSION_BUSY`, another CLI/MCP process is updating the local session; wait a moment and retry sequentially.\n- In delegated or non-streaming agent environments, do not batch many MCP calls silently and do not wrap many shortcut or `webstudio mcp single-op-call` commands in a shell loop. Treat each parent-visible checkpoint as the unit of work. If the parent asks for status within 30 seconds, run exactly one shortcut command such as `webstudio meta.index` or one explicit `webstudio mcp single-op-call` command, report that command/result, then wait for the parent to continue. Do not take a broad task such as creating a full design-system page as one execution unit. Call `workflow.next {"goal":"design-system-page"}`, report the returned phase/checkpoint, wait until the parent continues, call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`, complete exactly that bounded phase, and return: discovery, page creation, one dry-run JSX section, one committed JSX section, one `components.coverage-insert-next` call, or one presentation pass. Phase commands do not include nextPhase in their own output. After the parent continues, acknowledge the previous checkpoint first, then call `workflow.next` with the next phase. For all-component design-system pages, checkpoint after workflow planning, discovery, page creation, call `components.coverage-insert-next` once before checkpointing again, then finish with `workflow.next {"goal":"design-system-page","phase":"presentation-pass"}`. Coverage 72/72 is necessary but not sufficient: the page must be organized into styled, real-world examples, not raw unstyled component dumps.\n- For design-system or “use every component” tasks, start with compact `webstudio components.coverage-plan`, checkpoint, then request component coverage details with `webstudio components.coverage-plan \'{"detail":"roots","offset":20}\'` or `{"detail":"parts"}` only when needed. Do not pass `detail` to `list-pages`; use `list-pages {}` or `get-page-by-path` for page lookup.\n- MCP tool shortcuts are only for MCP tools. If a shortcut is ambiguous with a real top-level command, the real top-level command wins; use `webstudio mcp single-op-call \'\'` to force the MCP path.\n\n## LLM Implementation Process\n\nUse this process for user requests that change Webstudio content, layout, styles, assets, pages, redirects, resources, or publishing state:\n\n1. Discover capabilities with `webstudio man --json`, `webstudio schema api`, `webstudio schema mcp`, MCP `meta.index`, `meta.guide`, `meta.get_more_tools`, `components.list`, `components.summary`, `components.coverage-plan`, `components.search`, `components.get`, `templates.list`, and `templates.get`. From a shell, prefer shortcut calls such as `webstudio meta.index` and `webstudio components.search \'{"brief":"button"}\'` for these focused tool calls; use `webstudio mcp single-op-call` when you need the explicit MCP form. Read full resources such as `webstudio://project/tools` and `webstudio://project/components` only when needed. Do not write scripts to parse full MCP discovery JSON for normal lookup.\n2. Inspect current project state with semantic reads such as `get-project-settings`, `list-pages`, `get-page-by-path`, `list-instances`, `inspect-instance`, `get-styles`, `list-assets`, `list-breakpoints`, and `snapshot` only when needed. Before changing a project, read `get-project-settings` and follow any non-empty `meta.agentInstructions`. These are shared project instructions, not a place for secrets.\n3. Mutate the Webstudio project with semantic MCP write tools first. Prefer MCP `insert-fragment` for authored/styled sections, use `insert-component` only for one automatic component template, then `update-text`, `update-props`, `update-styles`, `upload-asset`, `create-page`, and page/project settings tools over raw patches.\n4. Use `apply-patch` only when no semantic tool covers the required change, and only after reading the latest snapshot/version.\n5. For visual/design work, regenerate or preview the generated app, capture a screenshot, inspect it with vision, and iterate before final response.\n6. Report what changed and what verification ran.\n\n## Visual Design Workflow\n\nFor requests involving visible HTML/CSS, layout, typography, colors, imagery, responsive behavior, or screenshots:\n\n1. Read editable Webstudio structure first: pages, instances, props, styles, breakpoints, assets, and relevant text.\n2. Do not use generated route/component files as the source of truth for editable content.\n3. Make edits through Webstudio semantic commands/MCP tools so the result stays editable in Builder and survives the next `webstudio build`.\n4. Keep generated project files current, start preview, and capture the changed page with `screenshot`.\n5. Use `screenshot.diff` when a baseline exists and inspect screenshot/diff artifacts with vision before finishing.\n6. If vision or screenshot tooling is unavailable, state that explicitly and explain what fallback verification was used.\n\n## Responsive Verification Workflow\n\nFor responsive page work, use Builder breakpoints as the source of truth:\n\n1. Read breakpoints with `list-breakpoints` before deciding responsive behavior.\n2. Apply responsive styles with existing Builder breakpoint ids; do not invent CSS media queries or breakpoint names when Webstudio breakpoint data exists.\n3. Pick screenshot viewport widths from the project breakpoints: include a desktop width, each defined max-width or min-width edge, and a narrow mobile width.\n4. Capture each viewport with `screenshot`, for example `{"path":"/","output":"home-375.png","viewport":{"width":375,"height":812}}` and `{"path":"/","output":"home-1440.png","viewport":{"width":1440,"height":900}}`.\n5. Inspect every viewport screenshot with vision before finishing, checking layout, overflow, hidden content, text wrapping, and breakpoint-specific style changes.\n6. If any viewport fails, update styles through semantic Webstudio tools and repeat screenshots for the affected breakpoints.\n\n## Generated Files Guardrails\n\n- Do not edit `app/__generated__`, generated route files, generated page files, generated CSS, or build output for normal Webstudio content/design requests.\n- Do not replace generated page components with handcrafted app code unless the user explicitly asks for code-only export customization.\n- Generated files are build artifacts and may be overwritten by `webstudio build`.\n- If a task truly requires generated app customization, keep it outside `app/__generated__` where possible and explain that it is not editable Webstudio content.\n\n## Values vs Bindings\n\nBefore authoring unfamiliar expressions, read `webstudio://project/expressions` with MCP `resources/read` or `webstudio mcp read-resource webstudio://project/expressions`. It documents the supported expression subset, method allowlist, scope, Collection context, and validation limits.\n\n- Use direct value tools for fixed content. For one visible text child, use `update-text` with plain `text`. For a bounded multi-instance literal replacement, use `replace-text` with `find`, `replace`, `pagePath` or `pageId`, and `limit`; it does not change expression children. Use `replace-prop-text` for bounded changes inside static string props, optionally limited to prop names or instance ids; it never changes dynamic bindings. For static props such as `aria-label`, `alt`, `id`, `class`, `href`, or button labels stored as props, use `update-props` with the prop\'s direct type/value.\n- Use `bind-props` only when the prop must stay dynamic: an expression, resource result, action, or existing scoped runtime context such as `system`. Do not use `bind-props` just to set a fixed string.\n- Direct prop string example: `{"updates":[{"instanceId":"button-id","name":"aria-label","type":"string","value":"Open menu"}]}`.\n- Expression binding example: `{"bindings":[{"instanceId":"link-id","name":"href","binding":{"type":"expression","value":"currentPost.url"}}]}`.\n- Page metadata fields such as `title`, `description`, `language`, `redirect`, and custom meta content accept plain fixed text. For computed values, pass JavaScript expression code such as `pageTitle ?? "Pricing | Acme"`.\n- Page `status` accepts a fixed HTTP status code as a number from 200 through 599, for example `302`. For a dynamic status, pass JavaScript expression code such as `system.status`.\n- Page metadata update example: use `update-page` with `{"pageId":"page-id","values":{"title":"Pricing | Acme","meta":{"description":"Plans for teams"}}}`.\n- Draft a page with `update-page` and `{"pageId":"page-id","values":{"isDraft":true}}`. It remains editable and previewable but is omitted from every publish target, including staging, and from sitemap output.\n- Stage a draft page for a future publish with `{"pageId":"page-id","values":{"isDraft":false}}`. This clears draft state but does not deploy the site. The home page and `/*` catch-all page cannot be drafts.\n- Resource `url` accepts plain fixed URLs and paths. For computed URLs, pass JavaScript expression code such as `"https://api.example.com/items?tag=" + filters.tag`. Resource header values, search parameter values, and text bodies accept expressions for dynamic values; for fixed text, use `{ "type": "literal", "value": "application/json" }`.\n- Resource update example: use `update-resource` with `{"resourceId":"resource-id","values":{"url":"https://api.example.com/items"}}`.\n- Data variable values support `string`, `number`, `boolean`, `string[]`, and `json`. Use `string[]` only for arrays where every item is a string; use `json` for objects, mixed arrays, filters, and nested data.\n- Parameters are internal scoped runtime values from pages, collections, or components. They are not a public authoring surface: do not create, update, or delete parameter records. Public tools should preserve existing parameter records and may reference documented context values such as `system` in expressions where they are already in scope.\n- Use scoped resources for read data. A GET resource created with `scopeInstanceId`/`dataSourceName` defaults to `exposeAsDataSource:true`, becomes a scoped resource data variable, is generated into the page resource `data` map, and may be loaded while rendering the page. Read the loaded resource result from its wrapper, usually `.data`.\n- Use prop-bound resources for actions. A resource created without `scopeInstanceId` and bound to a component prop such as Form `action` with `bind-props` and `binding.type: "resource"` becomes an action resource in the page resource `action` map. Use this for POST, PUT, DELETE, webhooks, GraphQL submissions, and anything that should run only from an explicit form/action flow.\n- POST, PUT, and DELETE resources default to `exposeAsDataSource:false`, even with a scope. Set `exposeAsDataSource:true` only for an intentional render-time read such as a GraphQL POST query; provide `scopeInstanceId` and inspect the returned warning. Set it to `false` during `update-resource` to detach existing render-time exposure.\n- For dynamic resource query parameters prefer `searchParams`, for example `{"name":"tag","value":"filters.tag"}`. Use `{"type":"literal","value":"website"}` for fixed request text. Header values can be expressions such as `"\\"Bearer \\" + auth.token"`. Body can be an object expression, including GraphQL payloads such as `{ query: "...", variables: { slug: system.params.slug } }`.\n- Resource methods are `get`, `post`, `put`, and `delete`. Optional resource controls are `graphql` and `system`. Use `control:"graphql"` for GraphQL POST resources with query bodies. Use `control:"system"` for built-in local resource URLs such as `"/$resources/current-date"` and for resources reading the built-in `system` parameter. The built-in system fields are `system.origin`, `system.pathname`, `system.params`, and `system.search`; do not use `system.path`.\n- Whenever an array or object from a resource or data variable should render repeated UI, call `insert-collection` with the complete iterable and one repeated-item JSX root. The command creates the Collection, private item parameters, iterable binding, and descendant item bindings atomically. Use `collectionItem` and `collectionItemKey` expressions in the item JSX. Wrap multiple repeated siblings in one Element, and give repeated Radix items stable unique `value` bindings.\n- Expressions are single JavaScript expressions, not statements or functions. Functions, arrow functions, classes, `new`, `this`, `await`, imports, arbitrary calls, increment/decrement, and assignment outside actions are unsupported. Prefer optional chaining, nullish coalescing, ternaries, property/index access, operators, and the documented string/array methods.\n\n## Pick Read Command\n\n{{readFirst}}\n\n## Pick Write Command\n\n{{taskRecipeIndex}}\n\n## Raw Patch Only If Needed\n\n1. Use MCP tool: snapshot.\n2. Write BuildPatchTransaction[].\n3. Use MCP tool: apply-patch.\n\n## MCP Argument Examples\n\nMCP tools receive JSON argument objects, not CLI flags. Use these shapes:\n\n{{mcpArgumentExampleIndex}}\n\n## Rules\n\n- Never guess ids for existing records. Read them first.\n- Never use project ids from user input. Commands use the configured project.\n- Use --refresh before a local-capable command when cached data may be stale.\n- Pass --json only to commands whose help/schema documents it. Do not add --json to top-level commands such as sync unless supported.\n- On VERSION_CONFLICT, read MCP snapshot again, regenerate the patch, then retry.\n- Treat stdout JSON as the API contract and stderr as diagnostics.\n- For visual/design work, verify the rendered result with vision before finishing.\n- Do not edit generated files for normal Webstudio content/design requests.\n- Use direct values for static strings and bindings only for dynamic expressions/resources/actions.\n- Use plain fixed text where documented. Only encode a quoted JavaScript string literal when a field is explicitly documented as an expression-only value.\n- Confirm destructive commands with --confirm only when user requested deletion/unpublish/replacement.\n- Use webstudio schema api for machine-readable top-level command metadata and webstudio schema mcp for MCP tool schemas.\n\n## Known Gaps\n\n{{knownCliGapIndex}}\n', "manual-mcp": - '# Webstudio MCP Manual\n\n`webstudio mcp` starts a stdio MCP server for real MCP clients. Shell users can call MCP tools with the shortcut form `webstudio \'\'`, for example `webstudio meta.index` or `webstudio insert-fragment \'\' --dry-run`. `webstudio mcp single-op-call` is the explicit equivalent and prints the structured JSON result. `webstudio mcp run` runs multiple MCP tool calls from inline JSON or a normal JSON file in one shared CLI session. Do not manually type or pipe raw JSON-RPC frames into `webstudio mcp` from an interactive shell or PTY.\n\n## Startup\n\nIf you are already working with an agent, ask it to connect the current folder\nto Webstudio. Give the editable Builder share link only when the trusted agent\nasks for it. The agent should perform the steps below, follow any\nclient-specific output from `webstudio connect`, and verify the connection by\nlisting the project pages. Treat the share link as a credential: do not include\nit in committed files, screenshots, logs, or issue reports.\n\n1. Configure a project with `webstudio init --link --json`.\n2. Synchronize it with `webstudio sync`.\n3. Generate the client configuration with `webstudio connect claude`,\n `webstudio connect codex`, `webstudio connect cursor`, or\n `webstudio connect vscode`. Use `--print` to preview the generated setup.\n4. Check capabilities with `webstudio permissions --json`.\n5. For shell-driven agents, use shortcut calls such as `webstudio meta.index` and `webstudio insert-fragment \'\' --dry-run` for individual MCP tool calls. Use the explicit equivalent `webstudio mcp single-op-call \'\'` when you need to force the MCP path, or `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` for bounded multi-call workflows. Use `webstudio mcp run .temp/mcp-calls.json` for large batches.\n6. For MCP clients, start the server with `webstudio mcp`.\n7. Start discovery with `meta.index`, then call focused tools with concrete JSON, for example `webstudio mcp single-op-call meta.guide \'{"brief":"Create a design system page using every component"}\'`.\n\nAfter linking and synchronization, run `webstudio connect `, reload or\nrestart the client, then ask the agent to use Webstudio MCP to list the project\npages. The supported client names are `claude`, `codex`, `cursor`, and `vscode`.\nFor Codex, `connect` registers and verifies the server through the Codex CLI;\nthe resulting server appears in Codex settings. Use `--print` to inspect the\nexact registration command without changing configuration or requiring project\naccess. Before changing client configuration, `connect` verifies that the saved\nproject endpoint is reachable and its credential is accepted. If verification\nfails, follow the specific connection, compatibility, or relinking guidance in\nthe error.\n\nStart MCP from the linked Webstudio project root. The lifecycle status line prints that absolute root; create local scripts, screenshots, and temporary artifacts under that root, for example `/.temp/script.mjs`. If the shell starts in a parent workspace, `cd` into the project root first or use absolute paths.\n\nWhen developing inside the Webstudio monorepo, start the local CLI exactly as `node packages/cli/local.js mcp` from the repo root. Do not use `pnpm exec webstudio`, `pnpm --filter webstudio exec webstudio`, or a global `webstudio`: they can resolve an older binary.\n\nWhile the server is running, stdout is reserved for MCP JSON-RPC messages. Do not print human text from the server process. The server advertises MCP `logging` capability and emits sparse `notifications/message` logs for ready state and tool lifecycle checkpoints such as `tool preview.start started`, `tool preview.start still running after 10000ms`, and `tool preview.start succeeded in 1234ms`; stderr also mirrors these sparse lifecycle fallback lines prefixed with `[webstudio mcp]`.\n\n## One-Shot Tool Calls\n\nUse the shortcut `webstudio \'\'` when you are operating from a shell and need one MCP tool result. The explicit form `webstudio mcp single-op-call \'\'` is equivalent and avoids writing temporary Node.js stdio client scripts.\n\nExamples:\n\n```sh\nwebstudio mcp single-op-call meta.index\nwebstudio mcp single-op-call meta.guide \'{"brief":"Create a design system page using every component"}\'\nwebstudio mcp single-op-call meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nwebstudio mcp single-op-call components.list \'{"source":"all"}\'\nwebstudio mcp single-op-call components.coverage-plan\nwebstudio mcp single-op-call components.search \'{"brief":"radix select"}\'\nwebstudio mcp single-op-call components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio mcp single-op-call templates.list\nwebstudio mcp single-op-call templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio mcp single-op-call insert-fragment --input-file .temp/insert-fragment.json\n```\n\nShortcut equivalents:\n\n```sh\nwebstudio meta.index\nwebstudio meta.guide \'{"brief":"Create a design system page using every component"}\'\nwebstudio meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nwebstudio components.list \'{"source":"all"}\'\nwebstudio components.coverage-plan\nwebstudio components.search \'{"brief":"radix select"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio templates.list\nwebstudio templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio insert-fragment --input-file .temp/insert-fragment.json\n```\n\nRules:\n\n- Inside the Webstudio monorepo, replace `webstudio` in the examples above with `node packages/cli/local.js`, for example `node packages/cli/local.js meta.index`.\n- For a simple authored/styled section, run `meta.index`, then `meta.get_more_tools \'{"tools":["insert-fragment"]}\'`, then `insert-fragment`. Do not grep source files, dump full MCP resources, or write parser scripts first.\n- In `insert-fragment` JSX, use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n- Prefer JSX for authored/styled content. Common `insert-fragment` inputs:\n\n```jsonl\n{"parentInstanceId":"root-id","fragment":"Northstar Product OSReusable patterns for teams."}\n{"parentInstanceId":"root-id","fragment":"Operations ConsoleReact-style object styles become editable Webstudio styles."}\n{"parentInstanceId":"root-id","fragment":"Track launch"}\n{"parentInstanceId":"root-id","fragment":""}\n```\n\n- Do not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`.\n- Use Webstudio prop names such as `class` and `for`; do not use React aliases `className` or `htmlFor`.\n- Use Webstudio actions for event/action props, for example `onClick={new ActionValue(["event"], expression\\`console.log(event)\\`)}`. Do not pass JavaScript functions such as `onClick={() => ...}`.\n- Plain prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n- Template-backed components used in JSX must include required child/part components explicitly under the same parent structure as the template, for example ``. Use `insert-component` when you want one automatic registered component template.\n- The positional input is JSON and defaults to `{}`.\n- Use `--input-file` for large mutation payloads.\n- Use `--dry-run` with local-capable mutation tools when you need a patch plan without committing. The computed transaction is returned in `meta.session.transaction`, and `meta.session.version` is its base build version. Copying a `.webstudio` folder is not an isolated project clone; `.webstudio/config.json` still points to the same remote project, so non-dry-run mutations can commit to that project.\n- The command prints JSON to stdout for both success and failure. Success uses the same `structuredContent` shape MCP tools return: `{ "ok": true, "data": ..., "meta": ... }`. Failure prints `{ "ok": false, "error": { "code": "...", "message": "..." }, "meta": ... }` and exits nonzero.\n- The command writes sparse progress to stderr, including start, success/failure, elapsed time, and committed status when the tool returns session metadata.\n- Invalid argument types fail loudly with path-specific messages, for example `meta.guide input.brief must be a string when provided`.\n- Run one-shot shortcut or `mcp single-op-call` commands sequentially against the same linked `.webstudio` folder. If you receive `PROJECT_SESSION_BUSY`, another CLI/MCP process is updating the local session; wait a moment and retry sequentially.\n- To work with another previously linked project without changing the directory\'s default link, start MCP or a shell call with `--project `, for example `webstudio mcp --project ` or `webstudio mcp single-op-call list-pages --project `. Selected projects use isolated local session and checkpoint files.\n- If you are a delegated agent and your parent cannot see live stderr/stdout, do not run a long sequence of shortcut or `mcp single-op-call` commands silently and do not wrap many calls in a shell loop. Treat each parent-visible checkpoint as the unit of work. If the parent asks for status within 30 seconds, run exactly one `webstudio ` or `webstudio mcp single-op-call` command, report that command/result, then wait before the next MCP command. For all-component design-system pages, checkpoint after discovery, checkpoint after page creation, call `components.coverage-insert-next` once before checkpointing again, then finish with the `presentation-pass` workflow phase. Coverage alone is not completion; organize examples into styled sections/cards.\n\n## Reporting CLI/MCP Issues\n\nIf a CLI/MCP tool gives a confusing error, crashes, hangs, produces invalid output, requires an undocumented workaround, or makes you inspect source code to understand normal usage, ask the user to report it in the Webstudio Discord `#help` channel: https://wstd.us/community.\n\nGive the user a complete copy-paste report. Include only non-secret values: never include auth tokens, private URLs, cookies, API keys, passwords, or proprietary project data. Redact them as ``.\n\nCopy-paste template:\n\n````md\nWebstudio CLI/MCP issue report\n\nWhat I was trying to do:\n\n\nWhat I expected:\n\n\nWhat happened instead:\n\n\nCommand/tool used:\n\n```sh\n\n```\n\nStructured output / error:\n\n```json\n\n```\n\nStderr / lifecycle logs:\n\n```txt\n\n```\n\nEnvironment:\n\n- CLI command path: \n- Webstudio CLI version: \n- OS: \n- Node version: \n- Project/session state: \n\nWorkaround tried:\n\n\nWhy this should be improved:\n\n````\n\n## Shared-Session Shell Runs\n\nUse `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` when you are operating from a shell and need several MCP tool calls to share one CLI session without hand-writing JSON-RPC. For large batches, pass a normal JSON file path such as `.temp/mcp-calls.json`. Do not use shell process substitution like `<(...)`; use inline JSON or a real file.\n\nUse `mcp run` for long-lived tools such as `preview.start`. A one-shot `mcp single-op-call preview.start` cannot keep ownership of a preview server for a later screenshot or stop call. Put `preview.start`, `screenshot`, and `preview.stop` in one shared `mcp run` process, or use a real long-running MCP client.\n\nInput shape:\n\n```json\n{\n "calls": [\n { "tool": "meta.index" },\n { "tool": "components.find", "input": { "brief": "radix select" } }\n ]\n}\n```\n\nRules:\n\n- The command prints JSON to stdout for both success and failure. It stops at the first failed call and prints partial results in `{ "ok": false, "error": ..., "data": { "completedCalls": ..., "results": [...] }, "meta": ... }`, then exits nonzero.\n- If a call returns `checkpoint.required`, read-only discovery and inspection remain available, but mutations and state-changing session tools return `CHECKPOINT_REQUIRED`. Stop and report the checkpoint to the parent/user. Only after the parent/user continues, call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}` before continuing mutations.\n- For `mcp single-op-call`, checkpoint requirements persist across later one-shot CLI processes until you call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`.\n- Use this instead of manually sending JSON-RPC frames to `webstudio mcp` from a shell.\n\n### Cross-project batches\n\nAdd `projects` to the same `mcp run` manifest to run focused reads, audits, or dry runs across independently linked project roots:\n\n```json\n{\n "concurrency": 2,\n "calls": [\n { "tool": "status" },\n { "tool": "audit", "input": {} },\n {\n "tool": "update-project-settings",\n "input": { "meta": { "siteName": "Reviewed" } },\n "dryRun": true\n }\n ],\n "projects": [\n { "id": "site-a", "root": "../site-a" },\n { "id": "site-b", "root": "../site-b" }\n ]\n}\n```\n\nProject roots and an optional `progressFile` are resolved relative to the manifest file. Each project may provide its own `calls` instead of using the top-level calls. Each root must already be linked with its own `.webstudio/config.json`; the runner creates an independently authenticated ProjectSession and uses root-scoped session, audit, preview-data, and checkpoint paths without changing the process working directory.\n\nConcurrency defaults to 2, is capped at 16, and can be set in the manifest or overridden with `--concurrency`. A failure is reported for that project while other projects continue. Progress is saved after every successful call; rerunning with the default `--resume` skips completed projects and starts failed projects after their last confirmed successful call. Reads and dry runs may be retried. A committed mutation interrupted after dispatch is marked `AMBIGUOUS_MUTATION_RESULT` and is never replayed automatically; inspect that project before deciding how to continue. Use `--no-resume` only to intentionally start the complete manifest over.\n\nCommitted mutation tools are rejected in a projects batch unless the command includes `--approve-mutations`. Review the complete manifest before granting approval. `--dry-run` applies to every call and does not require mutation approval. The final stdout object is compact: project counts, one status/error record per project, elapsed time, and the progress-file path rather than every tool result.\n\n## Discovery\n\nUse MCP itself after startup, or call the same tools with `webstudio mcp single-op-call`:\n\n- `tools/list`: machine-readable available tools\n- `resources/list`: available overview and full JSON resources\n- `meta.index`: concise capability catalog\n- `meta.guide`: workflow for a user goal; call with a string brief such as `{"brief":"Create a pricing page"}`\n- `meta.get_more_tools`: detailed params, examples, namespaces, and local/server behavior; prefer exact names such as `{"tools":["insert-fragment"]}` when you know them\n- `components.list`: compact registry metadata for visible components and templates; use a focused get tool for complete details\n- `components.summary`: component counts by default; use `{"detail":"components","limit":20}` for paginated entries\n- `components.coverage-plan`: compact paged plan for design-system coverage tasks that need every component; default returns counts plus the first root page, use `{"detail":"roots"}`, `{"detail":"parts"}`, or `{"detail":"full"}` for more\n- `components.coverage-status`: page-specific covered/missing component report with `missingRoots` and `missingParts`\n- `components.search`: focused component/template search by id, namespace, label, category, or content model\n- `components.find`: compatibility alias for focused component search\n- `components.get`: full metadata for one component id\n- `templates.list`: compact metadata for template-backed insertions only\n- `templates.get`: full registry item and payload metadata for one template\n\nComponent and template registry items use a shadcn-compatible top-level shape plus Webstudio-specific superset metadata in `meta`. Use `meta.runtime` for component ids, props, states, content model, and source identity; `meta.authoring` for composition and accessibility guidance; and `meta.builder` for template insertion details and expected project-data namespaces. These items are for Builder/MCP discovery and are not a published shadcn install registry yet.\n\nPrefer the focused `components.*` tools over dumping `webstudio://project/components`. Do not write local scripts to parse full MCP discovery JSON for common component lookup.\nFor “use every component” or design-system pages, start with compact `components.coverage-plan`, checkpoint, then page through roots/parts instead of dumping the full catalog.\n\n## Consumer Capabilities\n\nMCP lets agents work on one configured Webstudio project at a time. In consumer\nterms, agents can:\n\n- Check which project they are connected to.\n- Check what the share link is allowed to do.\n- Inspect project metadata and the latest editable build.\n- Read selected project data for audits and repair.\n- Apply precise project changes against a known version.\n- List, inspect, create, update, delete, duplicate, copy, and reorder pages.\n- Set the home page.\n- Preserve old page paths for redirects or history.\n- Read and update page titles, descriptions, metadata, auth settings, and SEO fields.\n- List, create, update, duplicate, move, and delete page folders.\n- List, create, update, delete, duplicate, reorder, and reuse page templates.\n- Create pages from reusable templates.\n- Read and update project site settings.\n- Read and update marketplace product metadata.\n- List, create, update, delete, and replace redirects.\n- List, create, update, and delete responsive breakpoints.\n- List and inspect page elements.\n- Insert registered components.\n- Insert styled JSX fragments.\n- Move, reparent, clone, duplicate, wrap, unwrap, convert, rename, retag, and delete elements.\n- Fill grid cells.\n- List and update text children.\n- Update plain text and expression text.\n- Update structured rich text.\n- Add, update, delete, and bind element props.\n- Bind props to expressions, resources, actions, and runtime system values.\n- Read, add, update, delete, and replace local styles.\n- Update selected style-source styles.\n- List, create, update, attach, detach, extract, duplicate, rename, lock, unlock, reorder, clear, and delete design tokens and style sources.\n- List, define, rename, delete, and rewrite CSS variables.\n- List, create, update, and delete static data variables.\n- Create string, number, boolean, string list, and JSON variables.\n- Delete unused data variables.\n- List, create, update, upsert, bind, and delete resources.\n- Create HTTP resources.\n- Create GraphQL resources.\n- Create system resources.\n- Use built-in system resources for sitemap, current date, and assets.\n- List and inspect complete asset metadata; upload, download, update, move, duplicate, find usage for, replace, and delete assets.\n- List, create, rename, move, recursively duplicate, and recursively delete nested asset folders.\n- Publish to staging or production.\n- Publish to selected domains.\n- List publish builds.\n- Check publish job status.\n- Unpublish staging or production deployments.\n- List, create, update, delete, and verify custom domains.\n- Start and stop preview.\n- Capture screenshots of generated pages.\n- Compare screenshots against baselines.\n- Install OCR support for richer visual checks.\n\nUseful resources:\n\n- `webstudio://project/status`: compact current ProjectSession status\n- `webstudio://project/tools-overview`: small operation overview by capability area\n- `webstudio://project/components-overview`: small component overview with ids, labels, namespaces, and categories\n- `webstudio://project/tools`: full operation catalog; read only when focused metadata is insufficient\n- `webstudio://project/components`: full component catalog with props, states, and content model composition constraints; read only when `components.summary`, `components.find`, and `components.get` are insufficient\n- `webstudio://project/guide`: concise discovery guide\n- `webstudio://project/expressions`: expression syntax, scope, supported methods, bindings, Collection iteration context, and verification\n- `webstudio://project/accessibility-review`: evidence-based LLM accessibility-review workflow using project checks, preview, and screenshots\n\n## MCP SDK Client Imports\n\nWhen writing a local Node.js MCP client script, use the official MCP SDK package and these exact ESM imports:\n\nInside the Webstudio monorepo this package is available at the repo root. In another project, install it first with `pnpm add -D @modelcontextprotocol/sdk`.\n\n```js\nimport { Client } from "@modelcontextprotocol/sdk/client/index.js";\nimport { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";\nimport { LoggingMessageNotificationSchema } from "@modelcontextprotocol/sdk/types.js";\n```\n\nMinimal stdio client for the local Webstudio CLI:\n\n```js\nconst client = new Client({ name: "webstudio-agent", version: "1.0.0" });\n\nclient.setNotificationHandler(\n LoggingMessageNotificationSchema,\n (notification) => {\n console.error(`[mcp] ${notification.params.data}`);\n }\n);\n\nconst transport = new StdioClientTransport({\n command: "node",\n args: ["packages/cli/local.js", "mcp"],\n cwd: process.cwd(),\n stderr: "inherit",\n});\n\nawait client.connect(transport);\n\nconst index = await client.callTool({\n name: "meta.index",\n arguments: {},\n});\nconsole.log(JSON.stringify(index.structuredContent, null, 2));\n\nawait client.close();\n```\n\nUse `node packages/cli/local.js mcp` from the Webstudio monorepo root for local development, or `webstudio mcp` from a linked project where the CLI is installed. Keep stdout for JSON-RPC/structured results and surface MCP logging notifications or stderr lifecycle lines as progress.\n\n## Core Rules\n\n- stdout is reserved for MCP JSON-RPC while the server is running.\n- Operate on the configured project only.\n- Read ids before writing.\n- Prefer semantic tools over `apply-patch`.\n- Use `status` and `refresh` when cached namespaces may be stale. Pass `status {"verbose":true}` only when debugging full namespace arrays, freshness, compatibility, or diagnostic details.\n- A mutation is durable only when `meta.session.committed` is true.\n- For visual/design work, verify the rendered result with vision before finishing.\n\n## Vision Verification Loop\n\nVision-capable AI can use MCP to see what it is building:\n\n{{mcpVisionVerificationLoopMarkdown}}\n\nGenerated app setup:\n\n{{mcpGeneratedAppDependencyNotes}}\n\n## MCP Argument Examples\n\nMCP tools receive JSON argument objects:\n\n{{mcpArgumentExampleIndex}}\n\n## Screenshot Verification\n\n{{screenshotVerificationSummary}}\n', + '# Webstudio MCP Manual\n\n`webstudio mcp` starts a stdio MCP server for real MCP clients. Shell users can call MCP tools with the shortcut form `webstudio \'\'`, for example `webstudio meta.index` or `webstudio insert-fragment \'\' --dry-run`. `webstudio mcp single-op-call` is the explicit equivalent and prints the structured JSON result. `webstudio mcp run` runs multiple MCP tool calls from inline JSON or a normal JSON file in one shared CLI session. Do not manually type or pipe raw JSON-RPC frames into `webstudio mcp` from an interactive shell or PTY.\n\n## Startup\n\nIf you are already working with an agent, ask it to connect the current folder\nto Webstudio. Give the editable Builder share link only when the trusted agent\nasks for it. The agent should perform the steps below, follow any\nclient-specific output from `webstudio connect`, and verify the connection by\nlisting the project pages. Treat the share link as a credential: do not include\nit in committed files, screenshots, logs, or issue reports.\n\n1. Configure a project with `webstudio init --link --json`.\n2. Synchronize it with `webstudio sync`.\n3. Generate the client configuration with `webstudio connect claude`,\n `webstudio connect codex`, `webstudio connect cursor`, or\n `webstudio connect vscode`. Use `--print` to preview the generated setup.\n4. Check capabilities with `webstudio permissions --json`.\n5. For shell-driven agents, use shortcut calls such as `webstudio meta.index` and `webstudio insert-fragment \'\' --dry-run` for individual MCP tool calls. Use the explicit equivalent `webstudio mcp single-op-call \'\'` when you need to force the MCP path, or `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` for bounded multi-call workflows. Use `webstudio mcp run .temp/mcp-calls.json` for large batches.\n6. For MCP clients, start the server with `webstudio mcp`.\n7. Start discovery with `meta.index`, then call focused tools with concrete JSON, for example `webstudio mcp single-op-call meta.guide \'{"brief":"Create a design system page using every component"}\'`.\n\nAfter linking and synchronization, run `webstudio connect `, reload or\nrestart the client, then ask the agent to use Webstudio MCP to list the project\npages. The supported client names are `claude`, `codex`, `cursor`, and `vscode`.\nFor Codex, `connect` registers and verifies the server through the Codex CLI;\nthe resulting server appears in Codex settings. Use `--print` to inspect the\nexact registration command without changing configuration or requiring project\naccess. Before changing client configuration, `connect` verifies that the saved\nproject endpoint is reachable and its credential is accepted. If verification\nfails, follow the specific connection, compatibility, or relinking guidance in\nthe error.\n\nStart MCP from the linked Webstudio project root. The lifecycle status line prints that absolute root; create local scripts, screenshots, and temporary artifacts under that root, for example `/.temp/script.mjs`. If the shell starts in a parent workspace, `cd` into the project root first or use absolute paths.\n\nWhen developing inside the Webstudio monorepo, start the local CLI exactly as `node packages/cli/local.js mcp` from the repo root. Do not use `pnpm exec webstudio`, `pnpm --filter webstudio exec webstudio`, or a global `webstudio`: they can resolve an older binary.\n\nWhile the server is running, stdout is reserved for MCP JSON-RPC messages. Do not print human text from the server process. The server advertises MCP `logging` capability and emits sparse `notifications/message` logs for ready state and tool lifecycle checkpoints such as `tool preview.start started`, `tool preview.start still running after 10000ms`, and `tool preview.start succeeded in 1234ms`; stderr also mirrors these sparse lifecycle fallback lines prefixed with `[webstudio mcp]`.\n\n## One-Shot Tool Calls\n\nUse the shortcut `webstudio \'\'` when you are operating from a shell and need one MCP tool result. The explicit form `webstudio mcp single-op-call \'\'` is equivalent and avoids writing temporary Node.js stdio client scripts.\n\nExamples:\n\n```sh\nwebstudio mcp single-op-call meta.index\nwebstudio mcp single-op-call meta.guide \'{"brief":"Create a design system page using every component"}\'\nwebstudio mcp single-op-call meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nwebstudio mcp single-op-call components.list \'{"source":"all"}\'\nwebstudio mcp single-op-call components.coverage-plan\nwebstudio mcp single-op-call components.search \'{"brief":"radix select"}\'\nwebstudio mcp single-op-call components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio mcp single-op-call templates.list\nwebstudio mcp single-op-call templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio mcp single-op-call insert-fragment --input-file .temp/insert-fragment.json\n```\n\nShortcut equivalents:\n\n```sh\nwebstudio meta.index\nwebstudio meta.guide \'{"brief":"Create a design system page using every component"}\'\nwebstudio meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nwebstudio components.list \'{"source":"all"}\'\nwebstudio components.coverage-plan\nwebstudio components.search \'{"brief":"radix select"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio templates.list\nwebstudio templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio insert-fragment --input-file .temp/insert-fragment.json\n```\n\nRules:\n\n- Inside the Webstudio monorepo, replace `webstudio` in the examples above with `node packages/cli/local.js`, for example `node packages/cli/local.js meta.index`.\n- For a simple authored/styled section, run `meta.index`, then `meta.get_more_tools \'{"tools":["insert-fragment"]}\'`, then `insert-fragment`. Do not grep source files, dump full MCP resources, or write parser scripts first.\n- In `insert-fragment` JSX, use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n- Prefer JSX for authored/styled content. Common `insert-fragment` inputs:\n\n```jsonl\n{"parentInstanceId":"root-id","fragment":"Northstar Product OSReusable patterns for teams."}\n{"parentInstanceId":"root-id","fragment":"Operations ConsoleReact-style object styles become editable Webstudio styles."}\n{"parentInstanceId":"root-id","fragment":"Track launch"}\n{"parentInstanceId":"root-id","fragment":""}\n```\n\n- Do not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`.\n- Use Webstudio prop names such as `class` and `for`; do not use React aliases `className` or `htmlFor`.\n- Use Webstudio actions for event/action props, for example `onClick={new ActionValue(["event"], expression\\`console.log(event)\\`)}`. Do not pass JavaScript functions such as `onClick={() => ...}`.\n- Plain prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n- Template-backed components used in JSX must include required child/part components explicitly under the same parent structure as the template, for example ``. Use `insert-component` when you want one automatic registered component template.\n- The positional input is JSON and defaults to `{}`.\n- Use `--input-file` for large mutation payloads.\n- Use `--dry-run` with local-capable mutation tools when you need a patch plan without committing. The computed transaction is returned in `meta.session.transaction`, and `meta.session.version` is its base build version. Copying a `.webstudio` folder is not an isolated project clone; `.webstudio/config.json` still points to the same remote project, so non-dry-run mutations can commit to that project.\n- The command prints JSON to stdout for both success and failure. Success uses the same `structuredContent` shape MCP tools return: `{ "ok": true, "data": ..., "meta": ... }`. Failure prints `{ "ok": false, "error": { "code": "...", "message": "..." }, "meta": ... }` and exits nonzero.\n- The command writes sparse progress to stderr, including start, success/failure, elapsed time, and committed status when the tool returns session metadata.\n- Invalid argument types fail loudly with path-specific messages, for example `meta.guide input.brief must be a string when provided`.\n- Run one-shot shortcut or `mcp single-op-call` commands sequentially against the same linked `.webstudio` folder. If you receive `PROJECT_SESSION_BUSY`, another CLI/MCP process is updating the local session; wait a moment and retry sequentially.\n- To work with another previously linked project without changing the directory\'s default link, start MCP or a shell call with `--project `, for example `webstudio mcp --project ` or `webstudio mcp single-op-call list-pages --project `. Selected projects use isolated local session and checkpoint files.\n- If you are a delegated agent and your parent cannot see live stderr/stdout, do not run a long sequence of shortcut or `mcp single-op-call` commands silently and do not wrap many calls in a shell loop. Treat each parent-visible checkpoint as the unit of work. If the parent asks for status within 30 seconds, run exactly one `webstudio ` or `webstudio mcp single-op-call` command, report that command/result, then wait before the next MCP command. For all-component design-system pages, checkpoint after discovery, checkpoint after page creation, call `components.coverage-insert-next` once before checkpointing again, then finish with the `presentation-pass` workflow phase. Coverage alone is not completion; organize examples into styled sections/cards.\n\n## Reporting CLI/MCP Issues\n\nIf a CLI/MCP tool gives a confusing error, crashes, hangs, produces invalid output, requires an undocumented workaround, or makes you inspect source code to understand normal usage, ask the user to report it in the Webstudio Discord `#help` channel: https://wstd.us/community.\n\nGive the user a complete copy-paste report. Include only non-secret values: never include auth tokens, private URLs, cookies, API keys, passwords, or proprietary project data. Redact them as ``.\n\nCopy-paste template:\n\n````md\nWebstudio CLI/MCP issue report\n\nWhat I was trying to do:\n\n\nWhat I expected:\n\n\nWhat happened instead:\n\n\nCommand/tool used:\n\n```sh\n\n```\n\nStructured output / error:\n\n```json\n\n```\n\nStderr / lifecycle logs:\n\n```txt\n\n```\n\nEnvironment:\n\n- CLI command path: \n- Webstudio CLI version: \n- OS: \n- Node version: \n- Project/session state: \n\nWorkaround tried:\n\n\nWhy this should be improved:\n\n````\n\n## Shared-Session Shell Runs\n\nUse `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` when you are operating from a shell and need several MCP tool calls to share one CLI session without hand-writing JSON-RPC. For large batches, pass a normal JSON file path such as `.temp/mcp-calls.json`. Do not use shell process substitution like `<(...)`; use inline JSON or a real file.\n\nUse `mcp run` for long-lived tools such as `preview.start`. A one-shot `mcp single-op-call preview.start` cannot keep ownership of a preview server for a later screenshot or stop call. Put `preview.start`, `screenshot`, and `preview.stop` in one shared `mcp run` process, or use a real long-running MCP client.\n\nInput shape:\n\n```json\n{\n "calls": [\n { "tool": "meta.index" },\n { "tool": "components.find", "input": { "brief": "radix select" } }\n ]\n}\n```\n\nRules:\n\n- The command prints JSON to stdout for both success and failure. It stops at the first failed call and prints partial results in `{ "ok": false, "error": ..., "data": { "completedCalls": ..., "results": [...] }, "meta": ... }`, then exits nonzero.\n- If a call returns `checkpoint.required`, read-only discovery and inspection remain available, but mutations and state-changing session tools return `CHECKPOINT_REQUIRED`. Stop and report the checkpoint to the parent/user. Only after the parent/user continues, call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}` before continuing mutations.\n- For `mcp single-op-call`, checkpoint requirements persist across later one-shot CLI processes until you call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`.\n- Use this instead of manually sending JSON-RPC frames to `webstudio mcp` from a shell.\n\n### Cross-project batches\n\nAdd `projects` to the same `mcp run` manifest to run focused reads, audits, or dry runs across independently linked project roots:\n\n```json\n{\n "concurrency": 2,\n "calls": [\n { "tool": "status" },\n { "tool": "audit", "input": {} },\n {\n "tool": "update-project-settings",\n "input": { "meta": { "siteName": "Reviewed" } },\n "dryRun": true\n }\n ],\n "projects": [\n { "id": "site-a", "root": "../site-a" },\n { "id": "site-b", "root": "../site-b" }\n ]\n}\n```\n\nProject roots and an optional `progressFile` are resolved relative to the manifest file. Each project may provide its own `calls` instead of using the top-level calls. Each root must already be linked with its own `.webstudio/config.json`; the runner creates an independently authenticated ProjectSession and uses root-scoped session, audit, preview-data, and checkpoint paths without changing the process working directory.\n\nConcurrency defaults to 2, is capped at 16, and can be set in the manifest or overridden with `--concurrency`. A failure is reported for that project while other projects continue. Progress is saved after every successful call; rerunning with the default `--resume` skips completed projects and starts failed projects after their last confirmed successful call. Reads and dry runs may be retried. A committed mutation interrupted after dispatch is marked `AMBIGUOUS_MUTATION_RESULT` and is never replayed automatically; inspect that project before deciding how to continue. Use `--no-resume` only to intentionally start the complete manifest over.\n\nCommitted mutation tools are rejected in a projects batch unless the command includes `--approve-mutations`. Review the complete manifest before granting approval. `--dry-run` applies to every call and does not require mutation approval. The final stdout object is compact: project counts, one status/error record per project, elapsed time, and the progress-file path rather than every tool result.\n\n## Discovery\n\nUse MCP itself after startup, or call the same tools with `webstudio mcp single-op-call`:\n\n- `tools/list`: machine-readable available tools\n- `resources/list`: available overview and full JSON resources\n- `meta.index`: concise capability catalog\n- `meta.guide`: workflow for a user goal; call with a string brief such as `{"brief":"Create a pricing page"}`\n- `meta.get_more_tools`: detailed params, examples, namespaces, and local/server behavior; prefer exact names such as `{"tools":["insert-fragment"]}` when you know them\n- `components.list`: compact registry metadata for visible components and templates; use a focused get tool for complete details\n- `components.summary`: component counts by default; use `{"detail":"components","limit":20}` for paginated entries\n- `components.coverage-plan`: compact paged plan for design-system coverage tasks that need every component; default returns counts plus the first root page, use `{"detail":"roots"}`, `{"detail":"parts"}`, or `{"detail":"full"}` for more\n- `components.coverage-status`: page-specific covered/missing component report with `missingRoots` and `missingParts`\n- `components.search`: focused component/template search by id, namespace, label, category, or content model\n- `components.find`: compatibility alias for focused component search\n- `components.get`: full metadata for one component id\n- `templates.list`: compact metadata for template-backed insertions only\n- `templates.get`: full registry item and payload metadata for one template\n\nComponent and template registry items use a shadcn-compatible top-level shape plus Webstudio-specific superset metadata in `meta`. Use `meta.runtime` for component ids, props, states, content model, and source identity; `meta.authoring` for composition and accessibility guidance; and `meta.builder` for template insertion details and expected project-data namespaces. These items are for Builder/MCP discovery and are not a published shadcn install registry yet.\n\nPrefer the focused `components.*` tools over dumping `webstudio://project/components`. Do not write local scripts to parse full MCP discovery JSON for common component lookup.\nFor “use every component” or design-system pages, start with compact `components.coverage-plan`, checkpoint, then page through roots/parts instead of dumping the full catalog.\n\n## Consumer Capabilities\n\nMCP lets agents work on one configured Webstudio project at a time. In consumer\nterms, agents can:\n\n- Check which project they are connected to.\n- Check what the share link is allowed to do.\n- Inspect project metadata and the latest editable build.\n- Read selected project data for audits and repair.\n- Apply precise project changes against a known version.\n- List, inspect, create, update, delete, duplicate, copy, and reorder pages.\n- Set the home page.\n- Preserve old page paths for redirects or history.\n- Read and update page titles, descriptions, metadata, auth settings, and SEO fields.\n- List, create, update, duplicate, move, and delete page folders.\n- List, create, update, delete, duplicate, reorder, and reuse page templates.\n- Create pages from reusable templates.\n- Read and update project site settings.\n- Read and update marketplace product metadata.\n- List, create, update, delete, and replace redirects.\n- List, create, update, and delete responsive breakpoints.\n- List and inspect page elements.\n- Insert registered components.\n- Insert styled JSX fragments.\n- Move, reparent, clone, duplicate, wrap, unwrap, convert, rename, retag, and delete elements.\n- Fill grid cells.\n- List and update text children.\n- Update plain text and expression text.\n- Update structured rich text.\n- Add, update, delete, and bind element props.\n- Bind props to expressions, resources, actions, and runtime system values.\n- Read, add, update, delete, and replace local styles.\n- Update selected style-source styles.\n- List, create, update, attach, detach, extract, duplicate, rename, lock, unlock, reorder, clear, and delete design tokens and style sources.\n- List, define, rename, delete, and rewrite CSS variables.\n- List, create, update, and delete static data variables.\n- Create string, number, boolean, string list, and JSON variables.\n- Delete unused data variables.\n- List, create, update, upsert, bind, and delete resources.\n- Create HTTP resources.\n- Create GraphQL resources.\n- Create system resources.\n- Use built-in system resources for sitemap, current date, and assets.\n- Filter the Assets system resource by `filename` or `extension`, opt into bounded text content with `include=content`, and bind its metadata or parsed JSON content.\n- List and inspect complete asset metadata; upload, download, update, move, duplicate, find usage for, replace, and delete assets.\n- List, create, rename, move, recursively duplicate, and recursively delete nested asset folders.\n- Publish to staging or production.\n- Publish to selected domains.\n- List publish builds.\n- Check publish job status.\n- Unpublish staging or production deployments.\n- List, create, update, delete, and verify custom domains.\n- Start and stop preview.\n- Capture screenshots of generated pages.\n- Compare screenshots against baselines.\n- Install OCR support for richer visual checks.\n\nUseful resources:\n\n- `webstudio://project/status`: compact current ProjectSession status\n- `webstudio://project/tools-overview`: small operation overview by capability area\n- `webstudio://project/components-overview`: small component overview with ids, labels, namespaces, and categories\n- `webstudio://project/tools`: full operation catalog; read only when focused metadata is insufficient\n- `webstudio://project/components`: full component catalog with props, states, and content model composition constraints; read only when `components.summary`, `components.find`, and `components.get` are insufficient\n- `webstudio://project/guide`: concise discovery guide\n- `webstudio://project/expressions`: expression syntax, scope, supported methods, bindings, Collection iteration context, and verification\n- `webstudio://project/accessibility-review`: evidence-based LLM accessibility-review workflow using project checks, preview, and screenshots\n\n## MCP SDK Client Imports\n\nWhen writing a local Node.js MCP client script, use the official MCP SDK package and these exact ESM imports:\n\nInside the Webstudio monorepo this package is available at the repo root. In another project, install it first with `pnpm add -D @modelcontextprotocol/sdk`.\n\n```js\nimport { Client } from "@modelcontextprotocol/sdk/client/index.js";\nimport { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";\nimport { LoggingMessageNotificationSchema } from "@modelcontextprotocol/sdk/types.js";\n```\n\nMinimal stdio client for the local Webstudio CLI:\n\n```js\nconst client = new Client({ name: "webstudio-agent", version: "1.0.0" });\n\nclient.setNotificationHandler(\n LoggingMessageNotificationSchema,\n (notification) => {\n console.error(`[mcp] ${notification.params.data}`);\n }\n);\n\nconst transport = new StdioClientTransport({\n command: "node",\n args: ["packages/cli/local.js", "mcp"],\n cwd: process.cwd(),\n stderr: "inherit",\n});\n\nawait client.connect(transport);\n\nconst index = await client.callTool({\n name: "meta.index",\n arguments: {},\n});\nconsole.log(JSON.stringify(index.structuredContent, null, 2));\n\nawait client.close();\n```\n\nUse `node packages/cli/local.js mcp` from the Webstudio monorepo root for local development, or `webstudio mcp` from a linked project where the CLI is installed. Keep stdout for JSON-RPC/structured results and surface MCP logging notifications or stderr lifecycle lines as progress.\n\n## Core Rules\n\n- stdout is reserved for MCP JSON-RPC while the server is running.\n- Operate on the configured project only.\n- Read ids before writing.\n- Prefer semantic tools over `apply-patch`.\n- Use `status` and `refresh` when cached namespaces may be stale. Pass `status {"verbose":true}` only when debugging full namespace arrays, freshness, compatibility, or diagnostic details.\n- A mutation is durable only when `meta.session.committed` is true.\n- For visual/design work, verify the rendered result with vision before finishing.\n\n## Vision Verification Loop\n\nVision-capable AI can use MCP to see what it is building:\n\n{{mcpVisionVerificationLoopMarkdown}}\n\nGenerated app setup:\n\n{{mcpGeneratedAppDependencyNotes}}\n\n## MCP Argument Examples\n\nMCP tools receive JSON argument objects:\n\n{{mcpArgumentExampleIndex}}\n\n## Screenshot Verification\n\n{{screenshotVerificationSummary}}\n', "mcp-startup-epilogue": 'If you are inside the Webstudio monorepo, use the local CLI exactly as `node packages/cli/local.js ...` as your first command path. Do not use `packages/cli/bin.js` for local source-tree work; it is the packaged build entry and may use stale built output.\n\nPlain `webstudio mcp` starts the stdio MCP server for real MCP clients. Do not manually type or pipe raw JSON-RPC frames into it from an interactive shell or PTY. From a shell, use shortcut calls such as `webstudio meta.index` and `webstudio insert-fragment \'\' --dry-run` for one bounded tool call. The explicit equivalent is `webstudio mcp single-op-call \'\'`. Use `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` for small multi-call batches in one shared CLI session. For large batches, pass a normal JSON file path such as `.temp/mcp-calls.json`. Do not use shell process substitution like `<(...)`; use inline JSON or a real file.\n\nPut each local CLI call in its own shell command; do not chain helper commands with `&&`, `;`, command substitution, or shell wrappers around the CLI when reporting a CLI step. Do not use `pnpm exec webstudio`, `pnpm --filter webstudio exec webstudio`, or a global `webstudio`; those can resolve an older binary. For example:\n\n```sh\nnode packages/cli/local.js mcp single-op-call meta.index\nnode packages/cli/local.js mcp single-op-call meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nnode packages/cli/local.js mcp single-op-call insert-fragment \'{"parentInstanceId":"parent-id","fragment":"Launch KitA focused section created with Webstudio JSX.Get started"}\' --dry-run\n```\n\nThe shorter local shortcut form is preferred for simple shell steps:\n\n```sh\nnode packages/cli/local.js meta.index\nnode packages/cli/local.js meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nnode packages/cli/local.js insert-fragment \'{"parentInstanceId":"parent-id","fragment":"Launch KitA focused section created with Webstudio JSX.Get started"}\' --dry-run\n```\n\nFor simple authored/styled sections, run the three commands above. Do not grep source files, dump full MCP resources, or write parser scripts first. Use `list-pages`, `get-page-by-path`, or `list-instances` only when you still need the target `parentInstanceId`.\n\nWhen building for a Content-mode editor, use a Content Block (`ws:block`) around every region that editor must change. Content-mode text and supported props are editable only in Content Block descendants; content outside is read-only. Keep reusable insertable source options in the block\'s `ws:block-template` child. Templates themselves are protected; an editor\'s inserted copy becomes an editable direct child of the Content Block. Verify this structure before handoff.\n\nRun it from the linked Webstudio project root. The startup status line prints that absolute root; use it for local files such as `/.temp/script.mjs`, screenshots, and generated artifacts.\n\nFor experiments, pass `--dry-run` to local-capable mutation calls. Read the computed transaction from `meta.session.transaction` and its base build version from `meta.session.version`. Copying a `.webstudio` folder is not an isolated project clone; `.webstudio/config.json` still points to the same remote project, so non-dry-run mutations can commit to that project.\n\nStartup marks cached ProjectSession data stale so MCP tools read the current Builder dev build.\n\nAfter startup, use focused discovery tools with concrete JSON. For delegated design-system or “use every component” tasks, start with exactly one command: `webstudio workflow.next \'{"goal":"design-system-page"}\'`; report the returned checkpoint to the parent/user and stop until continued. For other tasks, read `meta.index` first. From a shell, `webstudio mcp list-tools` is a concise alias for the initial tool catalog. Then call shortcuts such as `webstudio meta.guide \'{"brief":"Create a design system page using every component"}\'`, `webstudio workflow.next \'{"goal":"design-system-page"}\'`, `webstudio meta.get_more_tools \'{"tools":["insert-fragment"]}\'`, `webstudio components.list \'{"source":"all"}\'`, `webstudio components.summary`, `webstudio components.search \'{"brief":"radix select"}\'`, `webstudio components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'`, `webstudio templates.list`, and `webstudio templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'`. Use `webstudio mcp single-op-call` when you need the explicit MCP form. Prefer `insert-fragment` for authored/styled sections; use `insert-component` only when inserting one automatic component template. In JSX, use `ws:style={css\\`...\\`}`for styles,`class`/`for`instead of`className`/`htmlFor`, `ActionValue`for actions instead of JavaScript functions such as`onClick={() => ...}`, only JSON-compatible plain prop values, and no host globals or dynamic code APIs. For bounded multi-step shell work, run inline JSON like `webstudio mcp run \'[{"tool":"meta.index"},{"tool":"components.search","input":{"brief":"button"}}]\'`; this reuses one CLI session without raw JSON-RPC. For larger batches, write `{ "calls": [{ "tool": "..." }] }`to`.temp/mcp-calls.json`and run`webstudio mcp run .temp/mcp-calls.json`. If any call returns `checkpoint.required`, `mcp run`stops immediately before later calls; report the checkpoint and call`checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`before continuing. For design-system or “use every component” tasks, call`workflow.next {"goal":"design-system-page"}`for the next bounded phase, report its checkpoint, wait until the parent/user continues, call`checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`, then run only that phase. Start coverage phases with compact`webstudio components.coverage-plan`, checkpoint, then request component coverage details with `components.coverage-plan {"detail":"roots"}`or`components.coverage-plan {"detail":"parts"}`. Do not pass `detail`to`list-pages`; use `list-pages {}`or`get-page-by-path`for page lookup. Read MCP overview resources`webstudio://project/guide`, `webstudio://project/tools-overview`, and `webstudio://project/components-overview`when useful. Read full resources`webstudio://project/tools`and`webstudio://project/components` only when focused tools are insufficient; do not dump or parse full discovery JSON for common lookup.\n\nAfter startup, MCP clients discover capabilities with `tools/list`, `resources/list`, `meta.index`, `meta.guide`, and `meta.get_more_tools`.\n\nstdout is reserved for MCP JSON-RPC messages while the server is running.\n\nThe server advertises MCP `logging` capability and sends sparse `notifications/message` logs for ready state and tool lifecycle checkpoints such as `tool preview.start started`, `tool preview.start still running after 10000ms`, and `tool preview.start succeeded in 1234ms`. It also writes the same sparse lifecycle status lines to stderr, prefixed with `[webstudio mcp]`, including a final ready line. Treat the process as healthy and long-running after either ready signal; surface tool lifecycle logs as progress checkpoints and interact through MCP JSON-RPC on stdin/stdout.\n\n`webstudio mcp single-op-call` also writes sparse lifecycle lines to stderr for each one-shot call. Use those stderr lines as progress checkpoints and parse stdout as JSON on both success and failure. Failed one-shot calls print `{ "ok": false, "error": { "code": "...", "message": "..." }, "meta": ... }` to stdout and exit nonzero. If a one-shot call returns checkpoint.required, read-only discovery remains available, while later mutations fail with `CHECKPOINT_REQUIRED` until you report the checkpoint and call `webstudio mcp single-op-call checkpoint.ack \'{"reported":true,"continueAfterReport":true,"summary":""}\'`.\n\n`webstudio mcp run` also writes sparse lifecycle lines to stderr and prints one JSON result on success or failure. It stops at the first failed call or checkpoint requirement, including `CHECKPOINT_REQUIRED`, and preserves partial results so delegated agents can report exactly where they stopped.\n\nIf a CLI/MCP tool crashes, hangs, gives a confusing error, needs an undocumented workaround, or forces source-code inspection for normal usage, ask the user to report it in the Webstudio Discord `#help` channel at https://wstd.us/community. Give the user a complete copy-paste report with the goal, expected behavior, actual error, exact command/tool call, stdout JSON, stderr/lifecycle logs, environment, workaround, and secrets redacted.\n\nIf you are running as a delegated or non-streaming agent whose parent cannot see live stderr/stdout, do not batch many MCP calls silently and do not run long shell loops of shortcut or `webstudio mcp single-op-call` commands. Treat each parent-visible checkpoint as the unit of work. If the parent asks for status within 30 seconds, run exactly one shortcut command such as `webstudio meta.index` or one explicit `webstudio mcp single-op-call` command, return a concise checkpoint with that command/result, and wait for the parent to continue. Do not take a broad task such as creating a full design-system page as one execution unit. Call `workflow.next {"goal":"design-system-page"}`, report the returned phase/checkpoint, wait until the parent/user continues, call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`, complete exactly that bounded phase, and return: discovery, page creation, one dry-run JSX section, one committed JSX section, one `components.coverage-insert-next` call, or one presentation pass. Phase commands do not include nextPhase in their own output. After the parent continues, acknowledge the previous checkpoint first, then call `workflow.next` with the next phase. For all-component design-system pages, checkpoint after workflow planning, discovery, page creation, call `components.coverage-insert-next` once before checkpointing again, then finish with the `presentation-pass` workflow phase.\n', "mcp-vision": diff --git a/packages/cli/src/docs/api-use-cases.md b/packages/cli/src/docs/api-use-cases.md index 72eb43fddd00..9a8c8d4ae22d 100644 --- a/packages/cli/src/docs/api-use-cases.md +++ b/packages/cli/src/docs/api-use-cases.md @@ -111,6 +111,21 @@ MCP lets agents work on one configured Webstudio project. Agents can: - Publish, unpublish, inspect publish jobs, and manage custom domains. - Start preview, capture screenshots, compare screenshot diffs, and use OCR when installed. +## Query and bind asset data + +Commands: + +- MCP tool: create-resource {"resource":{"control":"system","name":"Markdown assets","method":"get","url":"/$resources/assets","searchParams":[{"name":"extension","value":{"type":"literal","value":"md"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"markdownAssets"} +- MCP tool: create-resource {"resource":{"control":"system","name":"Site data","method":"get","url":"/$resources/assets","searchParams":[{"name":"filename","value":{"type":"literal","value":"site-data.json"}},{"name":"include","value":{"type":"literal","value":"content"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"siteData"} + +Notes: + +- The Assets system resource returns an object keyed by asset id. Each item includes its URL, filename, description, folder, type, format, size, and timestamps. +- Filter by a case-insensitive filename substring with `filename`. Filter by one or more extensions with `extension`; separate extensions with commas or repeat the parameter. +- Asset contents are omitted by default. Set `include=content` to include editable text contents and parse valid JSON contents into structured values. +- A content query accepts at most 20 matching assets and 256 KiB per asset. Binary and oversized assets include `contentError` instead of content. +- Bind the resource data or any nested metadata/content field with the existing resource binding controls. + ## Inspect and refresh MCP session cache Commands: diff --git a/packages/cli/src/docs/manual-mcp.md b/packages/cli/src/docs/manual-mcp.md index 65afa9694789..4eda887e9853 100644 --- a/packages/cli/src/docs/manual-mcp.md +++ b/packages/cli/src/docs/manual-mcp.md @@ -274,6 +274,7 @@ terms, agents can: - Create GraphQL resources. - Create system resources. - Use built-in system resources for sitemap, current date, and assets. +- Filter the Assets system resource by `filename` or `extension`, opt into bounded text content with `include=content`, and bind its metadata or parsed JSON content. - List and inspect complete asset metadata; upload, download, update, move, duplicate, find usage for, replace, and delete assets. - List, create, rename, move, recursively duplicate, and recursively delete nested asset folders. - Publish to staging or production. diff --git a/packages/cli/src/prebuild.test.ts b/packages/cli/src/prebuild.test.ts index 1d7a5c2caf42..5bf095366558 100644 --- a/packages/cli/src/prebuild.test.ts +++ b/packages/cli/src/prebuild.test.ts @@ -410,6 +410,9 @@ describe("prebuild", () => { await expect( readFile("app/__generated__/$resources.assets.ts", "utf8") ).resolves.toContain("image.png"); + await expect( + readFile("app/__generated__/$resources.assets.ts", "utf8") + ).resolves.toContain('"format": "png"'); await expect( readFile("app/__generated__/$resources.sitemap.xml.ts", "utf8") ).resolves.toContain('"path": "/"'); diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index 9d83bafd6943..7f611f111138 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -40,7 +40,7 @@ import { generateCss, ROOT_INSTANCE_ID, elementComponent, - toRuntimeAsset, + toAssetResourceItem, } from "@webstudio-is/sdk"; import { migratePages } from "@webstudio-is/project-migrations/pages"; import { collectFontFamiliesFromStyleDecls } from "@webstudio-is/project-build/runtime"; @@ -798,14 +798,16 @@ export const prebuild = async (options: { const assetsById = Object.fromEntries( siteData.assets.map((asset) => [ asset.id, - toRuntimeAsset(asset, "https://placeholder.local"), + toAssetResourceItem(asset, "https://placeholder.local"), ]) ); await createFileIfNotExists( join(generatedDir, "$resources.assets.ts"), ` - export const assets = ${JSON.stringify(assetsById, null, 2)}; + import type { AssetResourceItem } from "@webstudio-is/sdk/runtime"; + + export const assets = ${JSON.stringify(assetsById, null, 2)} satisfies Record; ` ); diff --git a/packages/cli/templates/defaults/app/route-templates/html.tsx b/packages/cli/templates/defaults/app/route-templates/html.tsx index d4fa64509859..b950fb67fb53 100644 --- a/packages/cli/templates/defaults/app/route-templates/html.tsx +++ b/packages/cli/templates/defaults/app/route-templates/html.tsx @@ -15,6 +15,7 @@ import { loadResource, loadResources, cachedFetch, + loadAssetResource, formIdFieldName, formBotFieldName, } from "@webstudio-is/sdk/runtime"; @@ -65,7 +66,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const customFetch: typeof fetch = (input, init) => { +const createCustomFetch = (origin: string): typeof fetch => (input, init) => { if (typeof input !== "string") { return cachedFetch(projectId, input, init); } @@ -96,9 +97,11 @@ const customFetch: typeof fetch = (input, init) => { } if (isLocalResource(input, "assets")) { - const response = new Response(JSON.stringify(assets)); - response.headers.set("content-type", "application/json; charset=utf-8"); - return Promise.resolve(response); + return loadAssetResource({ + assets, + requestUrl: input, + fetchAsset: (url) => cachedFetch(projectId, new URL(url, origin)), + }).then(Response.json); } return cachedFetch(projectId, input, init); @@ -124,7 +127,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - customFetch, + createCustomFetch(url.origin), getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/defaults/app/route-templates/text.tsx b/packages/cli/templates/defaults/app/route-templates/text.tsx index 25da6ce73d4b..1f1ebe4f3e6b 100644 --- a/packages/cli/templates/defaults/app/route-templates/text.tsx +++ b/packages/cli/templates/defaults/app/route-templates/text.tsx @@ -1,5 +1,9 @@ import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; -import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; +import { + isLocalResource, + loadAssetResource, + loadResources, +} from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { projectDomain } from "__CLIENT__"; import { getPageMeta, getRemixParams, getResources } from "__SERVER__"; @@ -25,7 +29,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const customFetch: typeof fetch = (input, init) => { +const createCustomFetch = (origin: string): typeof fetch => (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -54,9 +58,11 @@ const customFetch: typeof fetch = (input, init) => { } if (isLocalResource(input, "assets")) { - const response = new Response(JSON.stringify(assets)); - response.headers.set("content-type", "application/json; charset=utf-8"); - return Promise.resolve(response); + return loadAssetResource({ + assets, + requestUrl: input, + fetchAsset: (url) => fetch(new URL(url, origin)), + }).then(Response.json); } return fetch(input, init); @@ -83,7 +89,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - customFetch, + createCustomFetch(url.origin), getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/defaults/app/route-templates/xml.tsx b/packages/cli/templates/defaults/app/route-templates/xml.tsx index 17cecc3c6c2e..7311faf48fa9 100644 --- a/packages/cli/templates/defaults/app/route-templates/xml.tsx +++ b/packages/cli/templates/defaults/app/route-templates/xml.tsx @@ -1,6 +1,10 @@ import { renderToString } from "react-dom/server"; import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; -import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; +import { + isLocalResource, + loadAssetResource, + loadResources, +} from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { ReactSdkContext, @@ -31,7 +35,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const customFetch: typeof fetch = (input, init) => { +const createCustomFetch = (origin: string): typeof fetch => (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -62,9 +66,11 @@ const customFetch: typeof fetch = (input, init) => { } if (isLocalResource(input, "assets")) { - const response = new Response(JSON.stringify(assets)); - response.headers.set("content-type", "application/json; charset=utf-8"); - return Promise.resolve(response); + return loadAssetResource({ + assets, + requestUrl: input, + fetchAsset: (url) => fetch(new URL(url, origin)), + }).then(Response.json); } return fetch(input, init); @@ -91,7 +97,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - customFetch, + createCustomFetch(url.origin), getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/react-router/app/route-templates/html.tsx b/packages/cli/templates/react-router/app/route-templates/html.tsx index bb66055fa7ae..db3a62ec63d3 100644 --- a/packages/cli/templates/react-router/app/route-templates/html.tsx +++ b/packages/cli/templates/react-router/app/route-templates/html.tsx @@ -17,6 +17,7 @@ import { formIdFieldName, formBotFieldName, cachedFetch, + loadAssetResource, } from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { @@ -64,7 +65,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const customFetch: typeof fetch = (input, init) => { +const createCustomFetch = (origin: string): typeof fetch => (input, init) => { if (typeof input !== "string") { return cachedFetch(projectId, input, init); } @@ -95,9 +96,11 @@ const customFetch: typeof fetch = (input, init) => { } if (isLocalResource(input, "assets")) { - const response = new Response(JSON.stringify(assets)); - response.headers.set("content-type", "application/json; charset=utf-8"); - return Promise.resolve(response); + return loadAssetResource({ + assets, + requestUrl: input, + fetchAsset: (url) => cachedFetch(projectId, new URL(url, origin)), + }).then(Response.json); } return cachedFetch(projectId, input, init); @@ -123,7 +126,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - customFetch, + createCustomFetch(url.origin), getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/react-router/app/route-templates/text.tsx b/packages/cli/templates/react-router/app/route-templates/text.tsx index 3229cb1f40fd..df57065a42ba 100644 --- a/packages/cli/templates/react-router/app/route-templates/text.tsx +++ b/packages/cli/templates/react-router/app/route-templates/text.tsx @@ -1,5 +1,9 @@ import { type LoaderFunctionArgs, redirect } from "react-router"; -import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; +import { + isLocalResource, + loadAssetResource, + loadResources, +} from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { projectDomain } from "__CLIENT__"; import { getPageMeta, getRemixParams, getResources } from "__SERVER__"; @@ -25,7 +29,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const customFetch: typeof fetch = (input, init) => { +const createCustomFetch = (origin: string): typeof fetch => (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -54,9 +58,11 @@ const customFetch: typeof fetch = (input, init) => { } if (isLocalResource(input, "assets")) { - const response = new Response(JSON.stringify(assets)); - response.headers.set("content-type", "application/json; charset=utf-8"); - return Promise.resolve(response); + return loadAssetResource({ + assets, + requestUrl: input, + fetchAsset: (url) => fetch(new URL(url, origin)), + }).then(Response.json); } return fetch(input, init); @@ -83,7 +89,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - customFetch, + createCustomFetch(url.origin), getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/react-router/app/route-templates/xml.tsx b/packages/cli/templates/react-router/app/route-templates/xml.tsx index ccc93691c43c..7a5da6c2fef9 100644 --- a/packages/cli/templates/react-router/app/route-templates/xml.tsx +++ b/packages/cli/templates/react-router/app/route-templates/xml.tsx @@ -1,6 +1,10 @@ import { renderToString } from "react-dom/server"; import { type LoaderFunctionArgs, redirect } from "react-router"; -import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; +import { + isLocalResource, + loadAssetResource, + loadResources, +} from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { ReactSdkContext, @@ -31,7 +35,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const customFetch: typeof fetch = (input, init) => { +const createCustomFetch = (origin: string): typeof fetch => (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -62,9 +66,11 @@ const customFetch: typeof fetch = (input, init) => { } if (isLocalResource(input, "assets")) { - const response = new Response(JSON.stringify(assets)); - response.headers.set("content-type", "application/json; charset=utf-8"); - return Promise.resolve(response); + return loadAssetResource({ + assets, + requestUrl: input, + fetchAsset: (url) => fetch(new URL(url, origin)), + }).then(Response.json); } return fetch(input, init); @@ -91,7 +97,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - customFetch, + createCustomFetch(url.origin), getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/ssg/app/route-templates/html/+data.ts b/packages/cli/templates/ssg/app/route-templates/html/+data.ts index 058735e51e85..990db1f6ea57 100644 --- a/packages/cli/templates/ssg/app/route-templates/html/+data.ts +++ b/packages/cli/templates/ssg/app/route-templates/html/+data.ts @@ -1,9 +1,13 @@ import type { PageContextServer } from "vike/types"; -import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; +import { + isLocalResource, + loadAssetResource, + loadResources, +} from "@webstudio-is/sdk/runtime"; import { getPageMeta, getResources } from "__SERVER__"; import { assets } from "__ASSETS__"; -const customFetch: typeof fetch = (input, init) => { +const createCustomFetch = (origin: string): typeof fetch => (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -27,9 +31,11 @@ const customFetch: typeof fetch = (input, init) => { } if (isLocalResource(input, "assets")) { - const response = new Response(JSON.stringify(assets)); - response.headers.set("content-type", "application/json; charset=utf-8"); - return Promise.resolve(response); + return loadAssetResource({ + assets, + requestUrl: input, + fetchAsset: (url) => fetch(new URL(url, origin)), + }).then(Response.json); } return fetch(input, init); @@ -51,7 +57,7 @@ export const data = async (pageContext: PageContextServer) => { }; const resources = await loadResources( - customFetch, + createCustomFetch(url.origin), getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/react-sdk/placeholder.d.ts b/packages/react-sdk/placeholder.d.ts index 572686dc064e..f89514302f3c 100644 --- a/packages/react-sdk/placeholder.d.ts +++ b/packages/react-sdk/placeholder.d.ts @@ -66,8 +66,8 @@ declare module "__SITEMAP__" { } declare module "__ASSETS__" { - import type { RuntimeAsset } from "@webstudio-is/sdk"; - export const assets: Record; + import type { AssetResourceItem } from "@webstudio-is/sdk"; + export const assets: Record; } declare module "__REDIRECT__" { diff --git a/packages/sdk/src/asset-resource.test.ts b/packages/sdk/src/asset-resource.test.ts new file mode 100644 index 000000000000..ff74a926c36f --- /dev/null +++ b/packages/sdk/src/asset-resource.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, test, vi } from "vitest"; +import type { Asset } from "./schema/assets"; +import { + filterAssetResource, + loadAssetResource, + maxAssetResourceContentBytes, + maxAssetResourceContentItems, + toAssetResourceItem, +} from "./asset-resource"; + +const fileAsset = ({ + id, + filename, + format, + size = 10, +}: { + id: string; + filename: string; + format: string; + size?: number; +}): Asset => ({ + id, + projectId: "project", + name: `${id}.${format}`, + filename, + type: "file", + format, + size, + createdAt: "2026-07-20T00:00:00.000Z", + meta: {}, +}); + +const assets = Object.fromEntries( + [ + fileAsset({ id: "readme", filename: "Readme.md", format: "md" }), + fileAsset({ id: "settings", filename: "settings.json", format: "json" }), + fileAsset({ id: "notes", filename: "release-notes.txt", format: "txt" }), + ].map((asset) => [ + asset.id, + toAssetResourceItem(asset, "https://example.com"), + ]) +); + +describe("asset resource", () => { + test("filters by filename and one or more extensions", () => { + expect( + Object.keys( + filterAssetResource( + assets, + new URLSearchParams({ filename: "set", extension: ".json,md" }) + ) + ) + ).toEqual(["settings"]); + }); + + test("includes text and parsed JSON content", async () => { + const fetchAsset = vi.fn( + async (url: string) => + new Response(url.includes("settings") ? '{"theme":"dark"}' : "# Readme") + ); + + await expect( + loadAssetResource({ + assets, + requestUrl: "/$resources/assets?extension=md,json&include=content", + fetchAsset, + }) + ).resolves.toMatchObject({ + readme: { content: "# Readme" }, + settings: { content: { theme: "dark" } }, + }); + }); + + test("reports invalid JSON without losing its source text", async () => { + await expect( + loadAssetResource({ + assets: { settings: assets.settings! }, + requestUrl: "/$resources/assets?include=content", + fetchAsset: async () => new Response("{invalid"), + }) + ).resolves.toMatchObject({ + settings: { + content: "{invalid", + contentError: "Asset contains invalid JSON.", + }, + }); + }); + + test("bounds content by asset size and match count", async () => { + const oversized = toAssetResourceItem( + fileAsset({ + id: "large", + filename: "large.md", + format: "md", + size: maxAssetResourceContentBytes + 1, + }), + "https://example.com" + ); + const fetchAsset = vi.fn(async () => new Response("unused")); + await expect( + loadAssetResource({ + assets: { large: oversized }, + requestUrl: "/$resources/assets?include=content", + fetchAsset, + }) + ).resolves.toMatchObject({ + large: { contentError: expect.stringContaining("exceeds") }, + }); + expect(fetchAsset).not.toHaveBeenCalled(); + + const tooMany = Object.fromEntries( + Array.from({ length: maxAssetResourceContentItems + 1 }, (_, index) => [ + String(index), + assets.readme!, + ]) + ); + await expect( + loadAssetResource({ + assets: tooMany, + requestUrl: "/$resources/assets?include=content", + fetchAsset, + }) + ).rejects.toThrow(`at most ${maxAssetResourceContentItems}`); + }); + + test("does not fetch binary asset content", async () => { + const binary = toAssetResourceItem( + fileAsset({ + id: "document", + filename: "document.pdf", + format: "pdf", + }), + "https://example.com" + ); + const fetchAsset = vi.fn(async () => new Response("unused")); + + await expect( + loadAssetResource({ + assets: { document: binary }, + requestUrl: "/$resources/assets?include=content", + fetchAsset, + }) + ).resolves.toMatchObject({ + document: { contentError: "Asset format is not editable text." }, + }); + expect(fetchAsset).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/sdk/src/asset-resource.ts b/packages/sdk/src/asset-resource.ts new file mode 100644 index 000000000000..9134537fc7cd --- /dev/null +++ b/packages/sdk/src/asset-resource.ts @@ -0,0 +1,130 @@ +import type { Asset } from "./schema/assets"; +import { getAssetTextEditorLanguage, toRuntimeAsset } from "./assets"; + +export const maxAssetResourceContentBytes = 256 * 1024; +export const maxAssetResourceContentItems = 20; + +export type AssetResourceItem = ReturnType & { + content?: unknown; + contentError?: string; +}; + +export const toAssetResourceItem = (asset: Asset, origin: string) => ({ + ...toRuntimeAsset(asset, origin), + name: asset.name, + filename: asset.filename ?? asset.name, + description: asset.description ?? undefined, + folderId: asset.folderId, + type: asset.type, + format: asset.format, + size: asset.size, + createdAt: asset.createdAt, + updatedAt: asset.updatedAt, +}); + +const getValues = (searchParams: URLSearchParams, name: string) => + searchParams + .getAll(name) + .flatMap((value) => value.split(",")) + .map((value) => value.trim().toLowerCase()) + .filter(Boolean); + +const matchesAssetResourceQuery = ( + asset: AssetResourceItem, + searchParams: URLSearchParams +) => { + const filenames = getValues(searchParams, "filename"); + const extensions = getValues(searchParams, "extension").map((extension) => + extension.startsWith(".") ? extension.slice(1) : extension + ); + const normalizedName = asset.filename.toLowerCase(); + return ( + (filenames.length === 0 || + filenames.some((filename) => normalizedName.includes(filename))) && + (extensions.length === 0 || extensions.includes(asset.format.toLowerCase())) + ); +}; + +export const filterAssetResource = ( + assets: Record, + searchParams: URLSearchParams +) => + Object.fromEntries( + Object.entries(assets).filter(([, asset]) => + matchesAssetResourceQuery(asset, searchParams) + ) + ); + +const getAssetContent = async ({ + asset, + fetchAsset, +}: { + asset: AssetResourceItem; + fetchAsset: (url: string) => Promise; +}) => { + if (getAssetTextEditorLanguage(asset) === undefined) { + return { contentError: "Asset format is not editable text." }; + } + if (asset.size > maxAssetResourceContentBytes) { + return { + contentError: `Asset content exceeds ${maxAssetResourceContentBytes} bytes.`, + }; + } + try { + const response = await fetchAsset(asset.url); + if (response.ok === false) { + return { contentError: `Asset request failed with ${response.status}.` }; + } + const bytes = await response.arrayBuffer(); + if (bytes.byteLength > maxAssetResourceContentBytes) { + return { + contentError: `Asset content exceeds ${maxAssetResourceContentBytes} bytes.`, + }; + } + const content = new TextDecoder().decode(bytes); + if (asset.format.toLowerCase() !== "json") { + return { content }; + } + try { + return { content: JSON.parse(content) as unknown }; + } catch { + return { content, contentError: "Asset contains invalid JSON." }; + } + } catch (error) { + return { + contentError: + error instanceof Error ? error.message : "Asset request failed.", + }; + } +}; + +export const loadAssetResource = async ({ + assets, + requestUrl, + fetchAsset, +}: { + assets: Record; + requestUrl: string; + fetchAsset: (url: string) => Promise; +}) => { + const searchParams = new URL(requestUrl, "https://webstudio.local") + .searchParams; + const filtered = filterAssetResource(assets, searchParams); + if (searchParams.get("include") !== "content") { + return filtered; + } + const entries = Object.entries(filtered); + if (entries.length > maxAssetResourceContentItems) { + throw new Error( + `Asset content queries support at most ${maxAssetResourceContentItems} matching assets.` + ); + } + return Object.fromEntries( + await Promise.all( + entries.map(async ([id, asset]) => [ + id, + { ...asset, ...(await getAssetContent({ asset, fetchAsset })) }, + ]) + ) + ); +}; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index f502ca499ef6..d84aeb265260 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -17,6 +17,7 @@ export * from "./schema/component-meta"; export * from "./assets"; export * from "./asset-folder-hierarchy"; export * from "./asset-folder-normalization"; +export * from "./asset-resource"; export * from "./core-metas"; export * from "./instances-utils"; export * from "./page-utils"; diff --git a/packages/sdk/src/resource-loader.test.ts b/packages/sdk/src/resource-loader.test.ts index 1d11d305a940..374def58b335 100644 --- a/packages/sdk/src/resource-loader.test.ts +++ b/packages/sdk/src/resource-loader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, beforeEach, vi, type Mock } from "vitest"; -import { loadResource } from "./resource-loader"; +import { isLocalResource, loadResource } from "./resource-loader"; import type { ResourceRequest } from "./schema/resources"; // Mock the fetch function @@ -104,6 +104,29 @@ describe("loadResource", () => { ); }); + test("appends search params to local resources", async () => { + const fetchResource = vi.fn(async () => Response.json({})); + + await loadResource(fetchResource, { + name: "assets", + url: "/$resources/assets", + searchParams: [{ name: "extension", value: "json,md" }], + method: "get", + headers: [], + }); + + expect(fetchResource).toHaveBeenCalledWith( + "/$resources/assets?extension=json%2Cmd", + expect.any(Object) + ); + expect( + isLocalResource("/$resources/assets?extension=json%2Cmd", "assets") + ).toBe(true); + expect( + isLocalResource("https://example.com/$resources/assets", "assets") + ).toBe(false); + }); + test("should fetch resource with JSON search params", async () => { const mockResponse = new Response(JSON.stringify({ key: "value" }), { status: 200, diff --git a/packages/sdk/src/resource-loader.ts b/packages/sdk/src/resource-loader.ts index 7aa7a7cd187d..782666400abc 100644 --- a/packages/sdk/src/resource-loader.ts +++ b/packages/sdk/src/resource-loader.ts @@ -3,12 +3,17 @@ import type { ResourceRequest } from "./schema/resources"; import { serializeValue } from "./to-string"; const LOCAL_RESOURCE_PREFIX = "$resources"; +const localResourceBaseUrl = new URL("https://webstudio.local"); /** * Prevents fetch cycles by prefixing local resources. */ export const isLocalResource = (pathname: string, resourceName?: string) => { - const segments = pathname.split("/").filter(Boolean); + const url = new URL(pathname, localResourceBaseUrl); + if (url.origin !== localResourceBaseUrl.origin) { + return false; + } + const segments = url.pathname.split("/").filter(Boolean); if (resourceName === undefined) { return segments[0] === LOCAL_RESOURCE_PREFIX; @@ -31,13 +36,15 @@ export const loadResource = async ( try { // cloudflare workers fail when fetching url contains spaces // even though new URL suppose to trim them on parsing by spec - const url = new URL(resourceRequest.url.trim()); + const resourceUrl = resourceRequest.url.trim(); + const isRelative = URL.canParse(resourceUrl) === false; + const url = new URL(resourceUrl, localResourceBaseUrl); if (searchParams) { for (const { name, value } of searchParams) { url.searchParams.append(name, serializeValue(value)); } } - href = url.href; + href = isRelative ? `${url.pathname}${url.search}${url.hash}` : url.href; } catch { // empty block } diff --git a/packages/sdk/src/runtime.ts b/packages/sdk/src/runtime.ts index dd766a7bd164..505ece627bfb 100644 --- a/packages/sdk/src/runtime.ts +++ b/packages/sdk/src/runtime.ts @@ -1,4 +1,5 @@ export * from "./resource-loader"; +export * from "./asset-resource"; export * from "./to-string"; export * from "./form-fields"; export * from "./json-ld"; From d34a2be88dd7dcc603f8f504ef6354fd54c00eaa Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Mon, 20 Jul 2026 16:06:11 +0100 Subject: [PATCH 02/14] refactor: defer asset queries to dedicated resource --- .../settings-panel/resource-panel.tsx | 30 +--- .../app/routes/rest.resources-loader.ts | 2 +- .../app/shared/$resources/assets.server.ts | 25 +-- apps/builder/app/shared/resources.ts | 21 ++- packages/cli/src/docs.generated.ts | 4 +- packages/cli/src/docs/api-use-cases.md | 15 -- packages/cli/src/docs/manual-mcp.md | 1 - packages/cli/src/prebuild.test.ts | 3 - packages/cli/src/prebuild.ts | 8 +- .../defaults/app/route-templates/html.tsx | 13 +- .../defaults/app/route-templates/text.tsx | 18 +-- .../defaults/app/route-templates/xml.tsx | 18 +-- .../react-router/app/route-templates/html.tsx | 13 +- .../react-router/app/route-templates/text.tsx | 18 +-- .../react-router/app/route-templates/xml.tsx | 18 +-- .../ssg/app/route-templates/html/+data.ts | 18 +-- packages/react-sdk/placeholder.d.ts | 4 +- packages/sdk/src/asset-resource.test.ts | 148 ------------------ packages/sdk/src/asset-resource.ts | 130 --------------- packages/sdk/src/index.ts | 1 - packages/sdk/src/resource-loader.test.ts | 25 +-- packages/sdk/src/resource-loader.ts | 13 +- packages/sdk/src/runtime.ts | 1 - 23 files changed, 69 insertions(+), 478 deletions(-) delete mode 100644 packages/sdk/src/asset-resource.test.ts delete mode 100644 packages/sdk/src/asset-resource.ts diff --git a/apps/builder/app/builder/features/settings-panel/resource-panel.tsx b/apps/builder/app/builder/features/settings-panel/resource-panel.tsx index b900f0c30930..0adc756f6244 100644 --- a/apps/builder/app/builder/features/settings-panel/resource-panel.tsx +++ b/apps/builder/app/builder/features/settings-panel/resource-panel.tsx @@ -30,8 +30,6 @@ import { sitemapResourceUrl, currentDateResourceUrl, assetsResourceUrl, - maxAssetResourceContentBytes, - maxAssetResourceContentItems, } from "@webstudio-is/sdk/runtime"; import { Box, @@ -914,7 +912,6 @@ export const SystemResourceForm = forwardRef< undefined | PanelApi, { variable?: DataSource } >(({ variable }, ref) => { - const { scope, aliases } = useResourceScope({ variable }); const resources = useStore($resources); const resource = @@ -949,9 +946,6 @@ export const SystemResourceForm = forwardRef< ) ?? localResources[0] ); }); - const [searchParams, setSearchParams] = useState( - resource?.searchParams ?? [] - ); useImperativeHandle(ref, () => ({ save: (formData) => { @@ -998,30 +992,8 @@ export const SystemResourceForm = forwardRef< ); }} value={localResource} - onChange={(value) => { - setLocalResource(value); - if (value.value !== resource?.url) { - setSearchParams([]); - } - }} + onChange={setLocalResource} /> - {localResource.value === JSON.stringify(assetsResourceUrl) && ( - <> - - Filter with filename or extension. Use comma-separated extensions - and include=content to load editable text. Content is limited to{" "} - {maxAssetResourceContentItems} assets and{" "} - {maxAssetResourceContentBytes / 1024} KiB per asset. JSON content - is exposed as structured data. - - - - )} ); diff --git a/apps/builder/app/routes/rest.resources-loader.ts b/apps/builder/app/routes/rest.resources-loader.ts index 45fa0d882ff9..1d35d0bf4936 100644 --- a/apps/builder/app/routes/rest.resources-loader.ts +++ b/apps/builder/app/routes/rest.resources-loader.ts @@ -29,7 +29,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { } if (isLocalResource(input, "assets")) { - return assetsLoader({ request, resourceUrl: input }); + return assetsLoader({ request }); } return fetch(input, init); diff --git a/apps/builder/app/shared/$resources/assets.server.ts b/apps/builder/app/shared/$resources/assets.server.ts index 6f9b824b8046..bffdd290b240 100644 --- a/apps/builder/app/shared/$resources/assets.server.ts +++ b/apps/builder/app/shared/$resources/assets.server.ts @@ -1,10 +1,7 @@ import { json } from "@remix-run/server-runtime"; import { parseBuilderUrl } from "@webstudio-is/protocol"; import { loadAssetsByProject } from "@webstudio-is/asset-uploader/index.server"; -import { - loadAssetResource, - toAssetResourceItem, -} from "@webstudio-is/sdk/runtime"; +import { toRuntimeAsset } from "@webstudio-is/sdk"; import { isBuilder } from "../router-utils"; import { createContext } from "../context.server"; @@ -12,13 +9,7 @@ import { createContext } from "../context.server"; * System Resource that provides the list of assets for the current project. * This allows assets to be dynamically referenced in the builder using the expression editor. */ -export const loader = async ({ - request, - resourceUrl = request.url, -}: { - request: Request; - resourceUrl?: string; -}) => { +export const loader = async ({ request }: { request: Request }) => { if (isBuilder(request) === false) { throw new Error( "Asset resource loader can only be accessed from the builder interface" @@ -40,15 +31,11 @@ export const loader = async ({ const requestUrl = new URL(request.url); const origin = `${requestUrl.protocol}//${requestUrl.host}`; + // Convert array to object with asset IDs as keys + // Use /cgi/ endpoint URLs (relative paths) const assetsById = Object.fromEntries( - assets.map((asset) => [asset.id, toAssetResourceItem(asset, origin)]) + assets.map((asset) => [asset.id, toRuntimeAsset(asset, origin)]) ); - return json( - await loadAssetResource({ - assets: assetsById, - requestUrl: resourceUrl, - fetchAsset: (url) => fetch(new URL(url, origin)), - }) - ); + return json(assetsById); }; diff --git a/apps/builder/app/shared/resources.ts b/apps/builder/app/shared/resources.ts index 1f9bf056d5cc..b2a6eed53b13 100644 --- a/apps/builder/app/shared/resources.ts +++ b/apps/builder/app/shared/resources.ts @@ -12,7 +12,6 @@ export { getResourceKey }; const queue = new Map(); const pending = new Map(); const cache = new Map(); -const cachedRequests = new Map(); export const $resourcesCache = atom(cache); @@ -47,10 +46,6 @@ const loadResources = async () => { const results = new Map(await response.json()); for (const [key, result] of results) { cache.set(key, result); - const request = pending.get(key); - if (request !== undefined) { - cachedRequests.set(key, request); - } pending.delete(key); } } @@ -87,7 +82,6 @@ export const preloadResource = (resource: ResourceRequest) => { export const invalidateResource = (resource: ResourceRequest) => { const key = getResourceKey(resource); cache.delete(key); - cachedRequests.delete(key); preloadResource(resource); }; @@ -96,11 +90,16 @@ export const invalidateResource = (resource: ResourceRequest) => { * Call this when assets are uploaded, deleted, or modified to refresh expressions using assets. */ export const invalidateAssets = () => { - for (const request of cachedRequests.values()) { - if (request.url === "/$resources/assets") { - invalidateResource(request); - } - } + const url = "/$resources/assets"; + // System resources always use GET with no params/headers/body + const systemResourceRequest: ResourceRequest = { + name: "assets", + method: "get", + url, + searchParams: [], + headers: [], + }; + invalidateResource(systemResourceRequest); }; export const computeResourceRequest = ( diff --git a/packages/cli/src/docs.generated.ts b/packages/cli/src/docs.generated.ts index 3b5572307224..481c3694da96 100644 --- a/packages/cli/src/docs.generated.ts +++ b/packages/cli/src/docs.generated.ts @@ -2,13 +2,13 @@ // Edit markdown files in src/docs instead. export const cliDocs = { "api-use-cases": - '# CLI API Use Cases\n\n## Link/configure one project\n\nCommands:\n\n- webstudio init --link --json\n\nNotes:\n\n- Writes local project id and global origin/token config.\n\n## Import synced project bundle into another project\n\nCommands:\n\n- webstudio sync\n- webstudio import --to \n- MCP tool: import {"to":""}\n\nNotes:\n\n- Imports local `.webstudio/data.json` into the destination project.\n- Destination share link must allow build/import access.\n- Use `--skip-assets` only when asset rows and files should not be imported.\n\n## Identify current token\n\nCommands:\n\n- MCP tool: whoami {}\n\n## Check token permissions\n\nCommands:\n\n- webstudio permissions --json\n\n## Inspect project/build/version\n\nCommands:\n\n- MCP tool: inspect {}\n- MCP tool: snapshot {"include":["pages","instances","styles"]}\n\n## Discover CLI/API capabilities\n\nCommands:\n\n- webstudio schema api\n- webstudio schema mcp\n- webstudio man --json\n- webstudio man llm --json\n- MCP tool: meta.index {}\n- MCP tool: meta.guide {"brief":"Create a pricing page"}\n- MCP tool: meta.get_more_tools {"brief":"update-styles"}\n- webstudio mcp list-resources\n- webstudio mcp read-resource webstudio://project/guide\n- webstudio mcp read-resource webstudio://project/expressions\n\nNotes:\n\n- Use `webstudio schema mcp` for a compact machine-readable MCP tool overview. Add `--verbose` or use focused `meta.get_more_tools` calls only when exact input schemas are needed.\n- Use focused MCP tools for discovery first: `meta.index`, `meta.guide`, `meta.get_more_tools`, `components.list`, `components.summary`, `components.search`, `components.get`, `templates.list`, and `templates.get`. Protocol clients can use `resources/list` and `resources/read`; shell agents can use `webstudio mcp list-resources` and `webstudio mcp read-resource `. Read longer resources such as `webstudio://project/tools` and `webstudio://project/components` only when focused tools are insufficient.\n- `components.summary` returns counts by default; request `{"detail":"components","limit":20}` for paginated entries. Registry list tools return compact metadata, while `components.get` and `templates.get` return focused full details.\n- Read `webstudio://project/expressions` before authoring unfamiliar computed text, prop bindings, resource expressions, actions, or Collection item bindings.\n- From a shell, call one MCP tool with the shortcut form `webstudio \'\'`, for example `webstudio components.summary`. The explicit equivalent is `webstudio mcp single-op-call \'\'`. Use `--input-file` for large payloads.\n\n## Inspect external shadcn registry items\n\nCommands:\n\n- webstudio registry inspect --source https://example.com/r/registry.json --item button --json\n- webstudio registry inspect --source ./registry.json --item dialog --json\n- webstudio registry inspect --source https://example.com/r/button.json --json\n\nNotes:\n\n- Reads a local or remote registry item without installing files or changing the configured Webstudio project.\n- Returns the item name, description, package and registry dependencies, file paths/targets, available docs, and a read-only compatibility report.\n- The report explicitly says whether installation or editable-component conversion is supported, lists declared requirements and manual steps, and says when arbitrary source code was not analyzed.\n- This is an inspection step only. It does not install files or change the configured project.\n\n## Understand what MCP can do\n\nCommands:\n\n- webstudio man mcp\n- MCP tool: meta.index {}\n- MCP tool: meta.guide {"brief":"What can Webstudio MCP do?"}\n\nMCP lets agents work on one configured Webstudio project. Agents can:\n\n- Inspect the linked project, token permissions, and latest editable build.\n- Read selected project data for audits, migrations, and repair.\n- Search labels, text, props, resource URLs, asset metadata, and styles.\n- Audit accessibility, security, SEO, performance settings, unused assets, ineffective Collection styles, and unused or duplicate style data.\n- Create and edit pages, folders, redirects, breakpoints, and page templates.\n- Create pages from reusable templates.\n- Update page metadata, SEO fields, auth settings, and marketplace metadata.\n- Insert components and styled JSX sections.\n- Create data-driven lists, grids, cards, and similar repeated UI from array or object data in one Collection operation.\n- Move, copy, wrap, unwrap, convert, rename, retag, and delete elements.\n- Update text, rich text, props, bindings, and actions.\n- Create and update local styles, design tokens, style sources, and CSS variables.\n- Create static data variables and JSON variables.\n- Create HTTP, GraphQL, and system resources.\n- Use system resources for sitemap, current date, and assets.\n- Bind resources to rendered data or form/action props.\n- Manage nested asset folders and upload, inspect, move, duplicate, download, replace, delete, and inspect usage for assets.\n- Publish, unpublish, inspect publish jobs, and manage custom domains.\n- Start preview, capture screenshots, compare screenshot diffs, and use OCR when installed.\n\n## Query and bind asset data\n\nCommands:\n\n- MCP tool: create-resource {"resource":{"control":"system","name":"Markdown assets","method":"get","url":"/$resources/assets","searchParams":[{"name":"extension","value":{"type":"literal","value":"md"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"markdownAssets"}\n- MCP tool: create-resource {"resource":{"control":"system","name":"Site data","method":"get","url":"/$resources/assets","searchParams":[{"name":"filename","value":{"type":"literal","value":"site-data.json"}},{"name":"include","value":{"type":"literal","value":"content"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"siteData"}\n\nNotes:\n\n- The Assets system resource returns an object keyed by asset id. Each item includes its URL, filename, description, folder, type, format, size, and timestamps.\n- Filter by a case-insensitive filename substring with `filename`. Filter by one or more extensions with `extension`; separate extensions with commas or repeat the parameter.\n- Asset contents are omitted by default. Set `include=content` to include editable text contents and parse valid JSON contents into structured values.\n- A content query accepts at most 20 matching assets and 256 KiB per asset. Binary and oversized assets include `contentError` instead of content.\n- Bind the resource data or any nested metadata/content field with the existing resource binding controls.\n\n## Inspect and refresh MCP session cache\n\nCommands:\n\n- MCP tool: status {}\n- MCP tool: status {"verbose":true}\n- MCP tool: refresh {"namespaces":["pages","instances","styles"]}\n- MCP tool: reset-session {}\n\nNotes:\n\n- Use status before a task to understand the cached ProjectSession state.\n- Use status with `{"verbose":true}` only when debugging full namespaces, freshness, compatibility, or diagnostics.\n- Use refresh when project data may have changed outside the current MCP session.\n- Use reset-session when local cached state is corrupt or incompatible.\n\n## Visually verify rendered work with AI vision\n\nCommands:\n\n- MCP tool: preview.start {"host":"127.0.0.1","port":5173}\n- MCP tool: preview.status {}\n- MCP tool: preview.stop {}\n- MCP tool: screenshot {"path":"/","output":".webstudio/screenshots/home-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"path":"/pricing","output":".webstudio/screenshots/pricing-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"baseUrl":"http://127.0.0.1:5177","path":"/pricing","output":".webstudio/screenshots/pricing-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff"}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff","expectedText":["Pricing","Start free"]}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff","expectedVisual":{"maxMismatchPercentage":2,"maxChangedRegions":3,"dominantColorChange":{"channel":"luminance","direction":"increase","minMagnitude":10}}}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/pricing-before.png","currentPath":".webstudio/screenshots/pricing-current.png","outputDir":".webstudio/screenshots/diff"}\n- MCP tool: vision.install-ocr {"confirm":true}\n\nNotes:\n\n- Use this after page/content/style mutations and after generated project files are current so a vision-capable AI can see the production-like generated site.\n- For multi-page work, capture every changed page by `path` through the same preview server; no click navigation is required.\n- After MCP mutations, path screenshots regenerate/restart preview as needed before capture; when preview is fresh, repeated path screenshots reuse the running server.\n- Do not call `preview.start` through one-shot `webstudio mcp single-op-call`: it is long-lived. From a shell, use `webstudio mcp run` with preview.start, screenshot, and preview.stop in one shared process, or use a real long-running MCP client.\n- From one-shot shell calls or another process, pass `baseUrl` with `path` to capture an already-running preview/site without generating, building, starting, or restarting preview.\n- Use preview.stop only in the same long-running MCP server or `webstudio mcp run` process that started preview. A separate one-shot `single-op-call` process does not own another process\'s preview controller.\n- Use waitForSelector when the rendered app has a reliable ready marker, waitUntil:"networkidle" for network-heavy pages, and waitForTimeout only for final visual settling.\n- Preview installs generated app dependencies under `.webstudio/preview` and reuses them across regenerations.\n- Do not add generated-preview dependencies to the repository root `package.json` or `pnpm-lock.yaml`.\n- If dependency installation fails, check npm and network configuration, then reinstall or update the Webstudio CLI if the problem persists.\n- When a baseline exists, use screenshot.diff once per baseline/current page or viewport pair to get changed regions, OCR textAnalysis, and diff artifact paths before deciding whether the result matches. Pass expectedText for explicit pass/fail current-screen text assertions with found and missing text. Pass expectedVisual for pass/fail limits on pixel mismatch percentage, changed-region count, or the overall dominant color/brightness direction.\n- If screenshot.diff reports OCR unavailable and the user agrees to install it, call vision.install-ocr {"confirm":true}; otherwise continue with pixel diff and visual inspection.\n- Compare the PNG, OCR text evidence, and diff artifacts against the user\'s intent for layout, typography, colors, spacing, imagery, and responsive framing; then iterate with focused mutations.\n- Root CLI equivalent: `webstudio screenshot --path /pricing --output pricing.png` generates a temporary production preview, captures that route, and stops the server. For repeated captures, keep `webstudio preview` running and pass its absolute URL to `webstudio screenshot`.\n\n## List pages\n\nCommands:\n\n- MCP tool: list-pages {}\n- MCP tool: list-folders {}\n\n## Read page by id\n\nCommands:\n\n- MCP tool: get-page {"pageId":""}\n\n## Read page by path\n\nCommands:\n\n- MCP tool: get-page-by-path {"path":"/pricing"}\n\n## Create page\n\nCommands:\n\n- MCP tool: create-page {"name":"Pricing","path":"/pricing"}\n- MCP tool: create-page {"name":"Pricing","path":"/pricing","title":"Pricing","meta":{"description":"Plans for teams"}}\n\nNotes:\n\n- `name`, `path`, page `title`, and metadata text fields accept plain fixed values.\n- For computed page titles or metadata, send JavaScript expression code such as `pageTitle ?? "Pricing"`.\n\n## Update page settings/metadata\n\nCommands:\n\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans","status":200}}}\n- MCP tool: update-page {"pageId":"","values":{"meta":{"auth":{"method":"basic","login":"","password":""}}}}\n\nNotes:\n\n- Page `title` and metadata text fields accept plain fixed values.\n- For computed page titles or metadata, send JavaScript expression code such as `pageTitle ?? "Pricing"`.\n- Page `status` accepts a fixed HTTP status code as a number from 200 through 599 or a JavaScript expression string for a dynamic status.\n\n## Read project settings\n\nCommands:\n\n- MCP tool: get-project-settings {}\n\nNotes:\n\n- Read `meta.agentInstructions` before making project changes. It contains the project\'s own guidance for AI agents.\n- Agent instructions are shared project guidance. Do not store credentials or other secrets there.\n\n## Update project settings\n\nCommands:\n\n- MCP tool: update-project-settings {"meta":{"siteName":"Acme"}}\n- MCP tool: update-project-settings {"meta":{"agentInstructions":"Use existing design tokens and keep product copy concise."}}\n\n## Read marketplace product\n\nCommands:\n\n- MCP tool: get-marketplace-product {}\n\n## Update marketplace product\n\nCommands:\n\n- MCP tool: update-marketplace-product {"category":"pageTemplates","name":"Acme Template","thumbnailAssetId":"asset-id","author":"Acme Studio","email":"hello@example.com","website":"https://example.com","issues":"","description":"Reusable template project for Acme landing pages."}\n\n## List redirects\n\nCommands:\n\n- MCP tool: list-redirects {}\n\n## Create redirect\n\nCommands:\n\n- MCP tool: create-redirect {"old":"/old","new":"/new","status":301}\n\n## Update redirect\n\nCommands:\n\n- MCP tool: update-redirect {"old":"/old","values":{"new":"/newer","status":302}}\n- MCP tool: update-redirect {"old":"/old","values":{"status":null}}\n\n## Delete redirect\n\nCommands:\n\n- MCP tool: delete-redirect {"old":"/old"}\n\n## Set redirects\n\nCommands:\n\n- MCP tool: set-redirects {"redirects":[{"old":"/old","new":"/new","status":"301"}]}\n\n## List breakpoints\n\nCommands:\n\n- MCP tool: list-breakpoints {}\n\n## Create breakpoint\n\nCommands:\n\n- MCP tool: create-breakpoint {"label":"Tablet","maxWidth":991}\n\n## Update breakpoint\n\nCommands:\n\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"label":"Tablet","maxWidth":1023}}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"condition":null,"minWidth":768}}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"minWidth":null,"maxWidth":null,"condition":"(hover: hover)"}}\n\n## Delete breakpoint\n\nCommands:\n\n- MCP tool: delete-breakpoint {"breakpointId":"tablet"}\n\n## Duplicate page\n\nCommands:\n\n- MCP tool: duplicate-page {"pageId":"","name":"Pricing Copy","path":"/pricing-copy"}\n- MCP tool: duplicate-page {"pageId":"","name":"Paris","path":"/paris","substitutions":{"text":{"London":"Paris"},"variables":{"city":{"type":"string","value":"Paris"}}}}\n- webstudio duplicate-page --page --name Paris --path /paris --substitutions \'{"text":{"London":"Paris"},"variables":{"city":{"type":"string","value":"Paris"}}}\' --json\n\nNotes:\n\n- Text substitutions replace exact fixed text only in the duplicated page\'s text children, string props, title, and metadata.\n- Variable substitutions are keyed by copied source-variable name and use typed variable values. The operation rejects missing or ambiguous names without committing a partial duplicate.\n- Existing expressions and cloned variable/resource references keep their remapped ids.\n\n## List page templates\n\nCommands:\n\n- MCP tool: list-page-templates {}\n\n## Create page template\n\nCommands:\n\n- MCP tool: create-page-template {"name":"Landing Template","title":"Landing"}\n\n## Update page template\n\nCommands:\n\n- MCP tool: update-page-template {"templateId":"","values":{"name":"Article Template","meta":{"description":"Reusable article layout"}}}\n\n## Delete page template\n\nCommands:\n\n- MCP tool: delete-page-template {"templateId":""}\n\n## Duplicate page template\n\nCommands:\n\n- MCP tool: duplicate-page-template {"templateId":""}\n\n## Reorder page template\n\nCommands:\n\n- MCP tool: reorder-page-template {"sourceTemplateId":"","targetTemplateId":"","position":"before"}\n\n## Create page from template\n\nCommands:\n\n- MCP tool: create-page-from-template {"templateId":"","name":"Landing","path":"/landing"}\n\n## Delete page\n\nCommands:\n\n- MCP tool: delete-page {"pageId":""}\n\n## List folders\n\nCommands:\n\n- MCP tool: list-folders {}\n- MCP tool: list-pages {}\n\n## Create folder\n\nCommands:\n\n- MCP tool: create-folder {"name":"Blog","slug":"blog"}\n\n## Update folder\n\nCommands:\n\n- MCP tool: update-folder {"folderId":"","values":{"name":"Blog","slug":"blog"}}\n\n## Delete folder\n\nCommands:\n\n- MCP tool: delete-folder {"folderId":""}\n\n## List element instances\n\nCommands:\n\n- MCP tool: list-instances {"pagePath":"/","maxDepth":3}\n\n## Inspect one element instance\n\nCommands:\n\n- MCP tool: inspect-instance {"instanceId":"","include":["props","styles","children"]}\n\n## Insert authored JSX or one component template\n\nCommands:\n\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"Product OS"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"@webstudio-is/sdk-components-react-radix:Switch"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"Form"}\n\nNotes:\n\n- Use MCP `insert-fragment` as the default way to author styled component trees. It converts JSX to a structured fragment before mutation.\n- Use only exact component ids returned by `components.search`, `components.get`, or `templates.get`. Never derive or guess component ids.\n- The `ws:` namespace contains specific Webstudio core components; it is not HTML-tag shorthand. Use `` for a native `div` and `` for a native form, never `` or ``.\n- For Webstudio\'s complete form structure, discover the Form component and insert its automatic template with `insert-component` using component `"Form"`.\n- MCP receives JSX as a JSON string because MCP arguments are JSON. The CLI converts it locally before the runtime mutation, so the project session receives structured Webstudio data, not JSX source.\n- In `insert-fragment` JSX, use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n- Do not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`.\n- Use Webstudio prop names such as `class` and `for`; do not use React aliases `className` or `htmlFor`.\n- Use Webstudio actions for event/action props, for example `onClick={new ActionValue(["event"], expression\\`console.log(event)\\`)}`. Do not pass JavaScript functions such as `onClick={() => ...}`.\n- Plain prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n- Template-backed components used in JSX must include required child/part components explicitly under the same parent structure as the template, for example ``.\n- Webstudio applies a registered template automatically when using `insert-component`, so composed components such as Switch include required child parts and styles.\n- Use `components.list`, `components.summary`, `components.search`, `components.get`, `templates.list`, and `templates.get` to discover known registry items, component ids, props, templates, insertability, and content model. Read `webstudio://project/components` only when those focused tools are insufficient.\n- Component/template registry items use a shadcn-compatible top-level shape plus Webstudio-specific superset metadata in `meta`. They are for Builder/MCP discovery, not a published shadcn install registry yet.\n- Known components with `contentModel.category: "none"` are not standalone-insertable; insert their root component template instead so required providers/parents are included.\n- Unknown custom component ids are a low-level extension mechanism, not a discovery fallback. Agents must not synthesize them.\n\n## Make a region editable in Content mode\n\nCommands:\n\n- MCP tool: insert-component {"parentInstanceId":"","component":"ws:block"}\n- MCP tool: inspect-instance {"instanceId":"","include":["children"]}\n\nNotes:\n\n- When a page will be handed to a Content-mode editor, wrap every region they should be able to edit in a Content Block (`ws:block`). Content-mode editors can edit text and supported props only in Content Block descendants. Content outside those blocks remains read-only, even when it looks like ordinary editable text.\n- Put reusable insertable options inside the Content Block\'s `ws:block-template` child. A template is source material, not editor content: editors cannot edit or delete it directly. When an editor inserts a template, its copy becomes a direct child of the Content Block and is editable.\n- Before handing off a page, verify with `inspect-instance` that the intended text, images, and links are inside a Content Block, and that templates include all required styling because Content-mode editors cannot use the Style panel.\n\n## Move elements\n\nCommands:\n\n- MCP tool: move-instance {"moves":"moves.json contents"}\n\nNotes:\n\n- Use `position: "end"` to append an instance. Repeating this for A and then B preserves the final order A, B.\n- A numeric `insertIndex` addresses the target parent\'s children before the moved instance is removed. Use it for exact placement; do not calculate the last index to append.\n- Moves in one `moves` array are applied sequentially in array order.\n\n## Clone element subtree\n\nCommands:\n\n- MCP tool: clone-instance {"sourceInstanceId":"","targetParentInstanceId":""}\n\n## Delete element subtree\n\nCommands:\n\n- MCP tool: delete-instance {"instanceIds":[""]}\n\n## List text/expression children\n\nCommands:\n\n- MCP tool: list-texts {"pagePath":"/"}\n\n## Update text child\n\nCommands:\n\n- MCP tool: update-text {"instanceId":"","childIndex":0,"text":"Launch faster"}\n\n## Replace bounded literal text\n\nCommands:\n\n- MCP tool: replace-text {"find":"Start free","replace":"Get started","match":"exact","pagePath":"/pricing","limit":20}\n\nNotes:\n\n- This changes only literal text children, never expression children. Scope it to pagePath or pageId and set a limit before a broad replacement.\n\n## Replace bounded static prop text\n\nCommands:\n\n- MCP tool: replace-prop-text {"find":"old.example.com","replace":"www.example.com","match":"substring","names":["href","code"],"limit":20}\n\nNotes:\n\n- This changes only static string props such as href, alt, aria-label, title, and HTML embed code. It never changes expressions, resources, actions, assets, or other dynamic bindings. Use names or instanceIds and a limit to narrow the change.\n\n## Replace bounded resource text\n\nCommands:\n\n- MCP tool: replace-resource-text {"find":"api.old.example.com","replace":"api.example.com","fields":["url"],"limit":20}\n\nNotes:\n\n- This changes resource names and fixed URL literals only. It skips dynamic URL expressions, headers, search parameters, request bodies, and GraphQL query code.\n\n## Update props\n\nCommands:\n\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: replace-prop-text {"find":"Old label","replace":"New label","names":["aria-label","title"],"limit":20}\n\nNotes:\n\n- Use this for fixed prop values such as `aria-label`, `alt`, `id`, static `href`, and other direct string/number/boolean/json prop values.\n\n## Add JSON-LD structured data\n\nCommands:\n\n- MCP tool: components.get {"component":"JsonLd"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"JsonLd"}\n- MCP tool: update-props {"updates":[{"instanceId":"","name":"code","type":"string","value":"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Organization\\",\\"name\\":\\"Acme\\"}"}]}\n- MCP tool: audit {"scopes":["seo"],"pagePath":"/"}\n\nNotes:\n\n- Prefer placing `JsonLd` inside `HeadSlot`.\n- Store `code` as a JSON object or array encoded as a compact string. The Builder formats it for editing.\n- The semantic prop update rejects malformed JSON and structurally invalid fixed JSON-LD with a precise JSON path.\n- The SEO audit also warns about a missing top-level `@context`, unknown or superseded Schema.org terms, properties unsupported by the supplied type, and incompatible primitive value types.\n- Schema.org vocabulary findings are warnings because custom vocabularies and extensions remain valid. Dynamic JSON-LD is marked as skipped for rendered validation.\n- Do not use bindings just to set static text.\n\n## Delete props\n\nCommands:\n\n- MCP tool: delete-props {"deletions":"props.json contents"}\n\n## Bind props to expressions/resources/actions\n\nCommands:\n\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n\nNotes:\n\n- Use this only when the prop should remain dynamic: expression, resource, action, or an existing scoped runtime context value such as `system`.\n- For a fixed string value, use `update-props` with `type:"string"` and a direct `value` instead.\n\n## Read styles\n\nCommands:\n\n- MCP tool: get-styles {"instanceIds":[""],"includeTokens":true}\n\n## Update local styles\n\nCommands:\n\n- MCP tool: update-styles {"updates":"styles.json contents"}\n\n## Delete local styles\n\nCommands:\n\n- MCP tool: delete-styles {"deletions":"styles.json contents"}\n\n## Replace matching style values\n\nCommands:\n\n- MCP tool: replace-styles {"property":"color","fromValue":{"type":"keyword","value":"red"},"toValue":{"type":"keyword","value":"blue"}}\n\n## List design tokens\n\nCommands:\n\n- MCP tool: list-design-tokens {}\n- MCP tool: list-design-tokens {"withUsage":true}\n- MCP tool: list-design-tokens {"verbose":true}\n\nNotes:\n\n- The default response is compact and includes token id, name, declaration count, and optional usage count.\n- Use `verbose:true` only when you need the full inline style declarations.\n\n## Create design tokens\n\nCommands:\n\n- MCP tool: create-design-token {"tokens":"tokens.json contents"}\n\n## Update design token styles\n\nCommands:\n\n- MCP tool: update-design-token-styles {"designTokenId":"","updates":"styles.json contents"}\n\n## Delete design token styles\n\nCommands:\n\n- MCP tool: delete-design-token-styles {"designTokenId":"","deletions":"styles.json contents"}\n\n## Attach design token to instances\n\nCommands:\n\n- MCP tool: attach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n\n## Detach design token from instances\n\nCommands:\n\n- MCP tool: detach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n\n## Extract design token from local styles\n\nCommands:\n\n- MCP tool: extract-design-token {"instanceIds":[""],"name":"Brand Primary","removeLocalProps":["color"]}\n\n## List CSS variables\n\nCommands:\n\n- MCP tool: list-css-variables {"withUsage":true}\n\n## Define CSS variables\n\nCommands:\n\n- MCP tool: define-css-variable {"vars":"vars.json contents"}\n\n## Delete CSS variables\n\nCommands:\n\n- MCP tool: delete-css-variable {"names":"names.json contents","force":true}\n\n## Rewrite CSS variable references\n\nCommands:\n\n- MCP tool: rewrite-css-variable-refs {"map":"variables.json contents"}\n\n## List data variables\n\nCommands:\n\n- MCP tool: list-variables {}\n- MCP tool: list-variables {"scopeInstanceId":""}\n\nNotes:\n\n- Data variables live in the internal `dataSources` namespace.\n- For raw `snapshot`, request the public `variables` namespace rather than the internal `dataSources` name. Raw patch payloads still use `dataSources` when applying direct changes.\n- Scope variables to the instance where they should become available. Descendants can use them in expressions, and nested variables with the same name mask outer variables.\n\n## Create data variable\n\nCommands:\n\n- MCP tool: create-variable {"scopeInstanceId":"","name":"title","value":{"type":"string","value":"Hello"}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"count","value":{"type":"number","value":3}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"featured","value":{"type":"boolean","value":true}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"tags","value":{"type":"string[]","value":["news","product"]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"filters","value":{"type":"json","value":{"tag":"news"}}}\n\nNotes:\n\n- Data variable values support `string`, `number`, `boolean`, `string[]`, and `json`.\n- Parameters are internal scoped runtime values provided by pages, collections, or components. They are not a public authoring surface: do not create, update, or delete parameter records. Use data variables/resources for user-authored data, and reference documented context values such as `system` only where they are already in scope.\n\n## Update data variable\n\nCommands:\n\n- MCP tool: update-variable {"dataSourceId":"","values":{"value":{"type":"json","value":{"count":1}}}}\n\n## Delete data variable\n\nCommands:\n\n- MCP tool: delete-variable {"dataSourceId":""}\n\n## List resources\n\nCommands:\n\n- MCP tool: list-resources {}\n- MCP tool: list-resources {"scopeInstanceId":""}\n\n## Create resource\n\nCommands:\n\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"https://api.example.com/posts","headers":[]}}\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"\\"https://api.example.com/posts?tag=\\" + filters.tag","headers":[]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Filtered Posts","method":"get","url":"https://api.example.com/posts","searchParams":[{"name":"tag","value":"filters.tag"},{"name":"source","value":{"type":"literal","value":"website"}}],"headers":[{"name":"Authorization","value":"\\"Bearer \\" + auth.token"}]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Post GraphQL","control":"graphql","method":"post","url":"https://api.example.com/graphql","headers":[{"name":"Content-Type","value":{"type":"literal","value":"application/json"}}],"body":"{ query: \\"query Post($slug: String!) { post(slug: $slug) { title } }\\", variables: { slug: system.params.slug } }"},"scopeInstanceId":"","dataSourceName":"post"}\n- MCP tool: create-resource {"resource":{"name":"Current Date","control":"system","method":"get","url":"/$resources/current-date","headers":[]},"scopeInstanceId":"","dataSourceName":"currentDate"}\n\nNotes:\n\n- Resource `url` accepts plain fixed URLs and paths such as `https://api.example.com/posts` and `/$resources/current-date`.\n- Resource `url` can also be a JavaScript expression when it is computed, such as `"https://api.example.com/posts?tag=" + filters.tag`.\n- Header values, search parameter values, and body accept expressions for dynamic values. For fixed text, use `{"type":"literal","value":"application/json"}`; Webstudio stores the required string expression for you.\n- Search parameter values, header values, and body expressions can read scoped variables and documented runtime context values such as `system` when they are available at the resource scope.\n- Add `scopeInstanceId` and `dataSourceName` when the resource result should be exposed as a scoped read data variable. Scoped resources are generated into the page resource `data` map and may be loaded during page rendering. Use this for read-oriented resources such as GET CMS/API data.\n- For submit/write/action resources, create the resource without `scopeInstanceId`, then bind a component prop such as a Form `action` with `bind-props` and `binding.type: "resource"`. Prop-bound resources are generated into the page resource `action` map instead of the read `data` map. Use this for POST, PUT, DELETE, webhooks, GraphQL submissions, and other explicit action flows.\n- Resource `method` can be `get`, `post`, `put`, or `delete`. Use GET for read data, POST for creates/GraphQL/webhooks/form submissions, PUT for full updates or replacements, and DELETE for deletion actions.\n- Optional `control` values are `graphql` and `system`. Use `graphql` for GraphQL-style requests, usually POST with a query body. Use `system` for built-in resources such as `"/$resources/sitemap.xml"`, `"/$resources/current-date"`, and `"/$resources/assets"` and when the resource should use the built-in `system` parameter. System fields are `system.origin`, `system.pathname`, `system.params`, and `system.search`.\n\n## Update resource\n\nCommands:\n\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: replace-resource-text {"find":"api.old.example.com","replace":"api.example.com","fields":["url"],"limit":20}\n\n## Delete resource\n\nCommands:\n\n- MCP tool: delete-resource {"resourceId":""}\n\n## List assets\n\nCommands:\n\n- MCP tool: list-assets {"withUsage":true}\n- MCP tool: list-assets {"verbose":true}\n\nNotes:\n\n- Compact results include each asset\'s folder id. Use `verbose:true` to include complete records for a page of assets, or `get-asset` to read one complete record including description, folder, creation time, and image/font metadata.\n- Image asset descriptions are the default alt text for asset-backed Image components.\n- To generate missing descriptions, inspect the image in its rendered page or asset source, write a concise description of its purpose, and save it on the asset rather than duplicating it on each Image instance.\n\n## Get asset\n\nCommands:\n\n- MCP tool: get-asset {"assetId":""}\n\n## List asset folders\n\nCommands:\n\n- MCP tool: list-asset-folders {}\n\n## Create asset folder\n\nCommands:\n\n- MCP tool: create-asset-folder {"name":"Marketing"}\n- MCP tool: create-asset-folder {"name":"Photos","parentId":""}\n\n## Update asset folder\n\nCommands:\n\n- MCP tool: update-asset-folder {"folderId":"","values":{"name":"Brand"}}\n- MCP tool: update-asset-folder {"folderId":"","values":{"parentId":""}}\n- MCP tool: update-asset-folder {"folderId":"","values":{"parentId":null}}\n\nNotes:\n\n- Updating `parentId` is the folder equivalent of cut and paste. Use `null` to move a folder to Root.\n\n## Duplicate asset folder\n\nCommands:\n\n- MCP tool: duplicate-asset-folder {"folderId":""}\n- MCP tool: duplicate-asset-folder {"folderId":"","parentId":""}\n\nNotes:\n\n- Duplication recursively copies descendant folders and assets. This is the folder equivalent of copy and paste.\n\n## Delete asset folder\n\nCommands:\n\n- MCP tool: delete-asset-folder {"folderId":""}\n\nNotes:\n\n- Deleting a folder recursively deletes its descendant folders and assets.\n\n## Update asset metadata\n\nCommands:\n\n- MCP tool: update-asset {"assetId":"","values":{"description":"Team collaborating around a whiteboard"}}\n\nNotes:\n\n- Use an empty description only when the image is intentionally decorative.\n- Updating an image asset description updates the default alt text wherever that asset is used with an asset-backed alt prop.\n\n## Generate missing image descriptions with an agent\n\nCommands:\n\n- MCP tool: audit {"scopes":["accessibility"],"verbose":true}\n- MCP tool: set-image-descriptions {"updates":[{"assetId":"hero-id","description":"Team collaborating around a whiteboard"},{"assetId":"texture-id","decorative":true}]}\n- MCP tool: audit {"scopes":["accessibility"]}\n\nNotes:\n\n- Start from `missing-image-description` findings. Inspect each image in its rendered page context before writing text.\n- The vision-capable agent generates the wording; the CLI validates and stores it but does not contain its own vision model.\n- Use `decorative:true` only when the image adds no information. This intentionally stores an empty description so later audits do not report it as missing.\n- Re-run the accessibility audit after the update. Asset-backed Image components use the saved asset description as their default alt text.\n\n## Manage fonts\n\nCommands:\n\n- MCP tool: list-fonts {"includeSystem":true}\n- MCP tool: list-assets {"type":"font"}\n- MCP tool: upload-asset {"asset":{"name":"acme-sans.woff2","type":"font","format":"woff2","meta":{"family":"Acme Sans","style":"normal","weight":400}},"assetsDir":".webstudio/assets"}\n- MCP tool: update-styles {"updates":"styles.json contents"}\n\nNotes:\n\n- Use `list-fonts` to discover uploaded families and system stacks. Upload/delete fonts through the existing asset tools, then apply a family with a `font-family` style declaration.\n\n## Upload one asset\n\nCommands:\n\n- MCP tool: upload-asset {"asset":{"name":"image.png","type":"image","format":"png","meta":{"width":1200,"height":630}},"assetsDir":".webstudio/assets"}\n- MCP tool: upload-asset {"asset":{"name":"image.png","type":"image","format":"png","folderId":"","meta":{"width":1200,"height":630}},"assetsDir":".webstudio/assets"}\n\n## Upload asset batch\n\nCommands:\n\n- MCP tool: upload-assets {"assets":[{"name":"image.png","type":"image","format":"png","meta":{"width":1200,"height":630}}],"assetsDir":".webstudio/assets"}\n\n## Duplicate asset\n\nCommands:\n\n- MCP tool: duplicate-asset {"assetId":""}\n- MCP tool: duplicate-asset {"assetId":"","folderId":""}\n- MCP tool: duplicate-asset {"assetId":"","folderId":null}\n\nNotes:\n\n- Duplication is the asset equivalent of copy and paste. Updating `folderId` is the equivalent of cut and paste; use `null` for Root.\n\n## Download asset\n\nCommands:\n\n- MCP tool: download-asset {"assetId":""}\n\n## Find asset usage\n\nCommands:\n\n- MCP tool: find-asset-usage {"assetId":""}\n\n## Replace asset references\n\nCommands:\n\n- MCP tool: replace-asset {"fromAssetId":"","toAssetId":""}\n\n## Delete assets\n\nCommands:\n\n- MCP tool: delete-asset {"assetIdsOrPrefixes":[""],"force":true}\n\n## Publish project\n\nCommands:\n\n- webstudio publish deploy --target production --json\n\n## List publishes\n\nCommands:\n\n- webstudio publish list --json\n\n## Check publish job\n\nCommands:\n\n- webstudio publish status --job --json\n\n## Unpublish\n\nCommands:\n\n- webstudio publish unpublish --target production --confirm --json\n\n## List domains\n\nCommands:\n\n- webstudio domains list --json\n\n## Create domain\n\nCommands:\n\n- webstudio domains create --domain example.com --json\n\n## Update domain\n\nCommands:\n\n- webstudio domains update --domain-id --domain www.example.com --json\n\n## Delete domain\n\nCommands:\n\n- webstudio domains delete --domain-id --confirm --json\n\n## Verify domain\n\nCommands:\n\n- webstudio domains verify --domain-id --json\n\n## Make arbitrary store-level changes\n\nCommands:\n\n- MCP tool: inspect {}\n- MCP tool: snapshot {"include":[""]}\n- MCP tool: apply-patch {"baseVersion":"","transactions":"patch.json contents"}\n\nNotes:\n\n- Use only when no semantic command exists.\n\n## Manage marketplace metadata\n\nCommands:\n\n- MCP tool: get-marketplace-product {}\n- MCP tool: update-marketplace-product {"category":"pageTemplates","name":"Acme Template","thumbnailAssetId":"asset-id","author":"Acme Studio","email":"hello@example.com","website":"https://example.com","issues":"","description":"Reusable template project for Acme landing pages."}\n\nPatch namespaces:\n\n- marketplaceProduct\n\n## Search and inspect safely\n\nCommands:\n\n- MCP tool: search-project {"query":"pricing"}\n- MCP tool: search-project {"query":"api.example.com","scopes":["resources"]}\n- MCP tool: list-instances {"pagePath":"/","maxDepth":5}\n- MCP tool: inspect-instance {"instanceId":"","include":["props","styles","children"]}\n- MCP tool: list-texts {"pagePath":"/"}\n- MCP tool: list-assets {"withUsage":true}\n- MCP tool: find-asset-usage {"assetId":""}\n- MCP tool: snapshot {"include":["pages","instances","props","resources","assets"]}\n\nNotes:\n\n- Use `search-project` for query-driven lookup across labels, text, prop values, resource URLs, asset metadata, and styles. Use `audit` for project health findings.\n\n## Audit project quality\n\nCommands:\n\n- webstudio audit --json\n- webstudio audit --scopes accessibility --scopes seo --json\n- webstudio audit --page-path /pricing --json\n- webstudio audit --scopes accessibility --verbose --json\n- webstudio audit --rendered --page-path /pricing --json\n- webstudio audit --rendered --route-example post=/blog/hello --json\n- webstudio audit --rendered --image-domain images.example.com --json\n- MCP tool: audit {}\n- MCP tool: audit {"scopes":["accessibility","security"],"severities":["error","warning"]}\n- MCP tool: audit {"scopes":["accessibility"],"verbose":true}\n- MCP tool: audit {"scopes":["craft"],"verbose":true}\n- MCP tool: audit {"rendered":true,"verbose":true}\n\nNotes:\n\n- With no scopes, `audit` checks accessibility, security, SEO, performance settings, unused assets, ineffective Collection styles, non-GET resources exposed as render-time data, and unused or duplicate style data.\n- Craft is opt-in and read-only. Run `audit` with `scopes:["craft"]` to detect whether the project is not using Craft, partially compatible, or compatible with the versioned Craft 1.2 profile. `profileStatuses` includes the University-doc provenance and the smallest safe next action. The audit never installs Craft or changes a non-Craft project.\n- The `performance` scope reports disabled atomic CSS generation. A rendered audit also measures broken, eager below-fold, and oversized images, browser-marked render-blocking resources, and legacy font formats.\n- Rendered image and resource metrics run only when the selected scopes include `performance`; responsive layout dimensions remain available whenever `rendered:true` is requested.\n- Compact findings include stable ids, severity, message, and location. Use `--verbose` or `{"verbose":true}` for evidence, explanation, suggested remediation, skipped-check details, and manual-check workflows.\n- `summary` counts all findings before severity filtering and pagination.\n- `contractVersion` identifies the audit response contract. Handle a new value before assuming existing fields retain the same meaning.\n- Expression-, resource-, and parameter-backed values that cannot be checked reliably appear in `skippedChecks`; they are not treated as passing or failing.\n- Page filters apply to page-owned accessibility, security, and SEO checks. Asset and style usage remain project-wide to avoid false unused findings.\n- Continue paginated results with `cursor`. Restart the audit if the project version changes.\n- Verbose skipped-check and manual-check details are included on the first findings page only; their total counts remain available on every page.\n- `manualChecks` describes responsive, hierarchy, and contrast checks that require preview screenshots and vision.\n- Focused audits return only manual checks relevant to their selected scopes.\n- In a long-lived MCP session, `{"rendered":true}` reuses preview and screenshot\n tools to capture every static page at mobile, desktop, and Builder breakpoint\n edges. Compact output reports rendered check/issue/failure counts; verbose\n output includes screenshot paths and measured layout dimensions.\n- Dynamic route templates are skipped unless `--route-example =`\n (or MCP `routeExamples`) supplies a concrete path. The path must not contain\n unresolved `:` or `*` parameters.\n- Plans above 120 captures return a short-lived confirmation token. Review the\n unchanged plan, then rerun with `--confirm-large-run` and\n `--confirmation-token`.\n- Detailed rendered evidence is stored in a versioned manifest under\n `.webstudio/audits`; compact output includes its path and screenshot count.\n- Rendered checks also report broken images, eager images below the fold, and\n image sources more than 2x their rendered dimensions in both axes, including\n Webstudio instance ids and measured dimensions when available.\n- Rendered checks include sanitized Resource Timing evidence and report\n browser-marked render-blocking resources plus legacy `.ttf`, `.otf`, and\n `.woff` fonts without applying a universal transfer-size threshold.\n- Fix findings through semantic mutation commands, then rerun `audit` to confirm their deterministic finding ids disappeared.\n\n## Verify dynamic bindings\n\nCommands:\n\n- webstudio verify-bindings --json\n- MCP tool: verify-bindings {"pagePath":"/pricing"}\n- MCP tool: verify-bindings {"instanceId":"","limit":50}\n\nNotes:\n\n- Statically checks persisted text expressions, expression/action/resource/parameter props, resource expressions, and page metadata.\n- Findings distinguish invalid syntax, unknown or out-of-scope variables, stale internal data-source ids, and missing resource or parameter references.\n- Page and instance filters can be combined when the instance belongs to the selected page. Continue findings with `cursor`.\n- This operation does not resolve rendered values or execute external resources. Preview representative loading, empty, error, and populated states after static findings are fixed.\n\n## Refactor targeted content\n\nCommands:\n\n- MCP tool: list-instances {"pagePath":"/"}\n- MCP tool: list-texts {"pagePath":"/"}\n- MCP tool: update-text {"instanceId":"","childIndex":0,"text":"Launch faster"}\n- MCP tool: replace-text {"find":"Old headline","replace":"New headline","match":"exact","pagePath":"/pricing","limit":20}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans"}}}\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: replace-asset {"fromAssetId":"","toAssetId":""}\n- MCP tool: replace-styles {"property":"color","fromValue":{"type":"keyword","value":"red"},"toValue":{"type":"keyword","value":"blue"}}\n- MCP tool: rewrite-css-variable-refs {"map":"variables.json contents"}\n\nNotes:\n\n- Use focused reads first, then mutate only matching instances, props, metadata, resource URLs, assets, or style references. Use `replace-text` for bounded literal text changes, `replace-prop-text` for bounded static prop text, `replace-resource-text` for fixed resource names/URLs, and `update-text` for one known child or expressions.\n\n## Optimize existing project\n\nCommands:\n\n- MCP tool: list-pages {}\n- MCP tool: list-folders {}\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans"}}}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: list-breakpoints {}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"maxWidth":1023}}\n- MCP tool: get-styles {"instanceIds":[""],"includeTokens":true}\n- MCP tool: update-styles {"updates":"styles.json contents"}\n- MCP tool: attach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n- MCP tool: update-project-settings {"meta":{"siteName":"Acme"}}\n\nNotes:\n\n- Use this for SEO metadata, accessibility labels, responsive behavior, token consistency, and project settings.\n\n## Connect external data\n\nCommands:\n\n- MCP tool: create-variable {"scopeInstanceId":"","name":"title","value":{"type":"string","value":"Hello"}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"tags","value":{"type":"string[]","value":["news","product"]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"filters","value":{"type":"json","value":{"tag":"news"}}}\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"https://api.example.com/posts","searchParams":[{"name":"tag","value":"filters.tag"},{"name":"source","value":{"type":"literal","value":"website"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Post GraphQL","control":"graphql","method":"post","url":"https://api.example.com/graphql","headers":[{"name":"Content-Type","value":{"type":"literal","value":"application/json"}}],"body":"{ query: \\"query Post($slug: String!) { post(slug: $slug) { title } }\\", variables: { slug: system.params.slug } }"},"scopeInstanceId":"","dataSourceName":"post"}\n- MCP tool: create-resource {"resource":{"name":"Current Date","control":"system","method":"get","url":"/$resources/current-date","headers":[]},"scopeInstanceId":"","dataSourceName":"currentDate"}\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"{/_ collection content _/}"}\n\nNotes:\n\n- Use this for CMS sections, blog listings, Ghost/headless CMS pages, n8n-style integrations, and API URLs built from variables.\n- For read data, expose GET resources as scoped data variables with `scopeInstanceId`/`dataSourceName` and read the loaded result from the resource result wrapper, usually `.data`.\n- For writes, webhooks, GraphQL submissions, and deletes, prefer unscoped resources bound to Form `action` props so they become action resources instead of auto-loaded read resources.\n- Use direct props for fixed values and prop bindings only when a prop must read a data variable, resource, action, or documented runtime context value such as `system`.\n\n## Render an array or object as repeated content\n\nCommands:\n\n- MCP tool: insert-collection {"parentInstanceId":"","data":{"type":"expression","value":"posts.data.items"},"itemFragment":"{expression`collectionItem.title`}"}\n- MCP tool: inspect-instance {"instanceId":"","include":["props","bindings","children"]}\n\nNotes:\n\n- Use Collection whenever an array or object from a resource or data variable should render a list, grid, cards, table rows, options, tabs, or other repeated UI.\n- Pass `insert-collection` the complete array or object. Do not pass the resource response wrapper or one indexed item. External resource arrays are commonly nested under the scoped resource result\'s `data` field or deeper.\n- Pass one repeated-item Webstudio JSX root. The command creates the Collection, its private current-item/current-key parameters, the iterable binding, and descendant item bindings atomically.\n- Collection renders the item root once for every entry. Use `expression` values such as `collectionItem.title` in descendant text and props. Object iteration also exposes `collectionItemKey`.\n- Wrap multiple repeated sibling instances in one Element inside Collection.\n- For repeated Radix items such as accordion items, tabs, or menu options, bind a stable unique id or slug to every required `value` prop.\n- See the [Collection documentation](https://docs.webstudio.is/university/core-components/collection) for the equivalent Builder workflow.\n\n## Support dynamic runtime behavior\n\nCommands:\n\n- MCP tool: integrate-runtime-ui {"parentInstanceId":"","resources":[{"resource":{"name":"Seats","method":"get","url":"https://api.example.com/seats","headers":[]},"dataSourceName":"Seats","exposeAsDataSource":true}],"structure":{"type":"collection","data":{"type":"expression","value":"Seats.data"},"itemFragment":{"children":[{"type":"id","value":"seat"}],"instances":[{"type":"instance","id":"seat","component":"Text","children":[{"type":"expression","value":"collectionItem.label"}]}],"props":[],"dataSources":[],"resources":[],"styleSources":[],"styleSourceSelections":[],"styles":[],"breakpoints":[],"assets":[]}},"retainedBehavior":[{"instanceId":"","responsibility":"Seat selection behavior"}]}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n- MCP tool: create-resource {"resource":{"name":"Seats","method":"get","url":"https://api.example.com/seats","headers":[]}}\n- MCP tool: snapshot {"include":["instances","props","resources"]}\n- MCP tool: apply-patch {"baseVersion":"","transactions":"patch.json contents"}\n\nNotes:\n\n- Use `integrate-runtime-ui` to create variables/resources, insert one editable fragment or Collection, and add safe data bindings in one transaction.\n- List existing script-owned responsibilities under `retainedBehavior`. The operation preserves those instances and never evaluates or accepts replacement script bodies.\n- `unsupportedConversions` records behavior that cannot be represented safely. Dry-run returns the complete transaction and the same retained/unsupported report without changing the project.\n- New actions and HtmlEmbed scripts are intentionally rejected. Create normal editable components and data bindings; keep opaque runtime behavior in existing script instances.\n\n## Build authenticated pages\n\nCommands:\n\n- MCP tool: meta.guide {"brief":"Build a Supabase-authenticated account page"}\n- MCP tool: create-page {"name":"Account","path":"/account"}\n- MCP tool: update-page {"pageId":"","values":{"meta":{"auth":{"method":"basic","login":"","password":""}}}}\n- MCP tool: create-resource {"resource":{"name":"Session","method":"get","url":"https://api.example.com/session","headers":[]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"user","value":{"type":"json","value":{}}}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n\nNotes:\n\n- Inspect and reuse the project\'s existing auth convention before authoring. Do\n not add a second provider or session model implicitly.\n- Model signed-out, loading, signed-in, and failed-auth states explicitly.\n- Never store credentials, service-role keys, refresh tokens, private session\n values, or authenticated response bodies in project data, command output,\n screenshots, agent instructions, or error reports. Privileged provider calls\n and authorization enforcement belong server-side.\n- Basic auth is semantic today. Provider-specific Supabase/Firebase setup still\n uses the existing resource, variable, prop, binding, and embed tools; there is\n no provider-specific installer.\n\n## Generate from design input\n\nCommands:\n\n- MCP tool: meta.guide {"brief":"Recreate this Figma design as a responsive page"}\n- MCP tool: create-page {"name":"Landing","path":"/landing"}\n- MCP tool: create-design-token {"tokens":"tokens.json contents"}\n- MCP tool: define-css-variable {"vars":"vars.json contents"}\n- MCP tool: list-breakpoints {}\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"Section copy"}\n- MCP tool: update-styles {"updates":[{"instanceId":"","breakpointId":"","property":"padding-left","value":{"type":"unit","unit":"px","value":24}}]}\n- MCP tool: preview.start {"host":"127.0.0.1","port":5173}\n- MCP tool: screenshot {"path":"/landing","output":"landing-desktop.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"path":"/landing","output":"landing-mobile.png","viewport":{"width":390,"height":844},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"baseUrl":"http://127.0.0.1:5177","path":"/landing","output":"landing-desktop.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n\nNotes:\n\n- Use this after the agent can inspect the supplied design. There is no direct\n Figma, screenshot, Inception, or `design.md` import command.\n- Inspect and reuse existing variables, tokens, styles, components, assets, and\n page patterns before authoring. Build semantic editable structure rather than\n flattening the design into an image or absolute-positioned approximation.\n- Verify one familiar viewport inside every distinct Builder breakpoint range,\n then run rendered audit and inspect the screenshots before completion.\n\n## Cross-project maintenance\n\nCommands:\n\n- webstudio mcp run .temp/projects.json\n- webstudio mcp run .temp/projects.json --dry-run\n- webstudio mcp run .temp/projects.json --approve-mutations --concurrency 2\n\nNotes:\n\n- Put shared `calls` and a `projects` array of independently linked project roots in the existing `mcp run` manifest. Project roots are relative to the manifest file.\n- Each project uses its own config, authentication, ProjectSession storage, checkpoint, and failure boundary. Confirmed successful calls are checkpointed. Reads can resume automatically; a mutation interrupted after dispatch is reported as ambiguous and is not replayed automatically, preventing silent duplicate writes.\n- Focus the manifest on bounded reads or audits first. Use per-call `dryRun`, global `--dry-run`, or explicitly approve committed mutations with `--approve-mutations` after reviewing the manifest.\n\n# Known CLI Gaps\n\n## Provider-specific authenticated pages\n\nMissing:\nCLI supports page basic auth and generic resources/props/embeds, but not guided Supabase/Firebase auth setup.\n\nCurrent fallback:\nCall `meta.guide` with the provider-authenticated page goal, then create the\npage, resources, variables, props, bindings, and embeds with existing semantic\ntools.\n\nSuggested commands:\n\n- setup-auth-page\n\n## Generate from design input\n\nMissing:\nNo command imports Figma, screenshots, Inception output, or design.md and turns it into pages/tokens/layout.\n\nCurrent fallback:\nCall `meta.guide` with the design-input goal, let the agent inspect the supplied\ndesign, then use semantic page, token, asset, fragment, style, preview,\nscreenshot, and audit tools. Use `apply-patch` only when no semantic operation\nfits.\n\nSuggested commands:\n\n- generate-from-design\n\n## Built-in cross-project maintenance\n\nMissing:\nPublic API and CLI intentionally operate on one configured project at a time; there is no built-in multi-project discovery or loop runner.\n\nCurrent fallback:\nRun the CLI from an external script that reconfigures one project/session at a time.\n\nSuggested commands:\n\n- none\n', + '# CLI API Use Cases\n\n## Link/configure one project\n\nCommands:\n\n- webstudio init --link --json\n\nNotes:\n\n- Writes local project id and global origin/token config.\n\n## Import synced project bundle into another project\n\nCommands:\n\n- webstudio sync\n- webstudio import --to \n- MCP tool: import {"to":""}\n\nNotes:\n\n- Imports local `.webstudio/data.json` into the destination project.\n- Destination share link must allow build/import access.\n- Use `--skip-assets` only when asset rows and files should not be imported.\n\n## Identify current token\n\nCommands:\n\n- MCP tool: whoami {}\n\n## Check token permissions\n\nCommands:\n\n- webstudio permissions --json\n\n## Inspect project/build/version\n\nCommands:\n\n- MCP tool: inspect {}\n- MCP tool: snapshot {"include":["pages","instances","styles"]}\n\n## Discover CLI/API capabilities\n\nCommands:\n\n- webstudio schema api\n- webstudio schema mcp\n- webstudio man --json\n- webstudio man llm --json\n- MCP tool: meta.index {}\n- MCP tool: meta.guide {"brief":"Create a pricing page"}\n- MCP tool: meta.get_more_tools {"brief":"update-styles"}\n- webstudio mcp list-resources\n- webstudio mcp read-resource webstudio://project/guide\n- webstudio mcp read-resource webstudio://project/expressions\n\nNotes:\n\n- Use `webstudio schema mcp` for a compact machine-readable MCP tool overview. Add `--verbose` or use focused `meta.get_more_tools` calls only when exact input schemas are needed.\n- Use focused MCP tools for discovery first: `meta.index`, `meta.guide`, `meta.get_more_tools`, `components.list`, `components.summary`, `components.search`, `components.get`, `templates.list`, and `templates.get`. Protocol clients can use `resources/list` and `resources/read`; shell agents can use `webstudio mcp list-resources` and `webstudio mcp read-resource `. Read longer resources such as `webstudio://project/tools` and `webstudio://project/components` only when focused tools are insufficient.\n- `components.summary` returns counts by default; request `{"detail":"components","limit":20}` for paginated entries. Registry list tools return compact metadata, while `components.get` and `templates.get` return focused full details.\n- Read `webstudio://project/expressions` before authoring unfamiliar computed text, prop bindings, resource expressions, actions, or Collection item bindings.\n- From a shell, call one MCP tool with the shortcut form `webstudio \'\'`, for example `webstudio components.summary`. The explicit equivalent is `webstudio mcp single-op-call \'\'`. Use `--input-file` for large payloads.\n\n## Inspect external shadcn registry items\n\nCommands:\n\n- webstudio registry inspect --source https://example.com/r/registry.json --item button --json\n- webstudio registry inspect --source ./registry.json --item dialog --json\n- webstudio registry inspect --source https://example.com/r/button.json --json\n\nNotes:\n\n- Reads a local or remote registry item without installing files or changing the configured Webstudio project.\n- Returns the item name, description, package and registry dependencies, file paths/targets, available docs, and a read-only compatibility report.\n- The report explicitly says whether installation or editable-component conversion is supported, lists declared requirements and manual steps, and says when arbitrary source code was not analyzed.\n- This is an inspection step only. It does not install files or change the configured project.\n\n## Understand what MCP can do\n\nCommands:\n\n- webstudio man mcp\n- MCP tool: meta.index {}\n- MCP tool: meta.guide {"brief":"What can Webstudio MCP do?"}\n\nMCP lets agents work on one configured Webstudio project. Agents can:\n\n- Inspect the linked project, token permissions, and latest editable build.\n- Read selected project data for audits, migrations, and repair.\n- Search labels, text, props, resource URLs, asset metadata, and styles.\n- Audit accessibility, security, SEO, performance settings, unused assets, ineffective Collection styles, and unused or duplicate style data.\n- Create and edit pages, folders, redirects, breakpoints, and page templates.\n- Create pages from reusable templates.\n- Update page metadata, SEO fields, auth settings, and marketplace metadata.\n- Insert components and styled JSX sections.\n- Create data-driven lists, grids, cards, and similar repeated UI from array or object data in one Collection operation.\n- Move, copy, wrap, unwrap, convert, rename, retag, and delete elements.\n- Update text, rich text, props, bindings, and actions.\n- Create and update local styles, design tokens, style sources, and CSS variables.\n- Create static data variables and JSON variables.\n- Create HTTP, GraphQL, and system resources.\n- Use system resources for sitemap, current date, and assets.\n- Bind resources to rendered data or form/action props.\n- Manage nested asset folders and upload, inspect, move, duplicate, download, replace, delete, and inspect usage for assets.\n- Publish, unpublish, inspect publish jobs, and manage custom domains.\n- Start preview, capture screenshots, compare screenshot diffs, and use OCR when installed.\n\n## Inspect and refresh MCP session cache\n\nCommands:\n\n- MCP tool: status {}\n- MCP tool: status {"verbose":true}\n- MCP tool: refresh {"namespaces":["pages","instances","styles"]}\n- MCP tool: reset-session {}\n\nNotes:\n\n- Use status before a task to understand the cached ProjectSession state.\n- Use status with `{"verbose":true}` only when debugging full namespaces, freshness, compatibility, or diagnostics.\n- Use refresh when project data may have changed outside the current MCP session.\n- Use reset-session when local cached state is corrupt or incompatible.\n\n## Visually verify rendered work with AI vision\n\nCommands:\n\n- MCP tool: preview.start {"host":"127.0.0.1","port":5173}\n- MCP tool: preview.status {}\n- MCP tool: preview.stop {}\n- MCP tool: screenshot {"path":"/","output":".webstudio/screenshots/home-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"path":"/pricing","output":".webstudio/screenshots/pricing-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"baseUrl":"http://127.0.0.1:5177","path":"/pricing","output":".webstudio/screenshots/pricing-current.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff"}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff","expectedText":["Pricing","Start free"]}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/home-before.png","currentPath":".webstudio/screenshots/home-current.png","outputDir":".webstudio/screenshots/diff","expectedVisual":{"maxMismatchPercentage":2,"maxChangedRegions":3,"dominantColorChange":{"channel":"luminance","direction":"increase","minMagnitude":10}}}\n- MCP tool: screenshot.diff {"baselinePath":".webstudio/screenshots/pricing-before.png","currentPath":".webstudio/screenshots/pricing-current.png","outputDir":".webstudio/screenshots/diff"}\n- MCP tool: vision.install-ocr {"confirm":true}\n\nNotes:\n\n- Use this after page/content/style mutations and after generated project files are current so a vision-capable AI can see the production-like generated site.\n- For multi-page work, capture every changed page by `path` through the same preview server; no click navigation is required.\n- After MCP mutations, path screenshots regenerate/restart preview as needed before capture; when preview is fresh, repeated path screenshots reuse the running server.\n- Do not call `preview.start` through one-shot `webstudio mcp single-op-call`: it is long-lived. From a shell, use `webstudio mcp run` with preview.start, screenshot, and preview.stop in one shared process, or use a real long-running MCP client.\n- From one-shot shell calls or another process, pass `baseUrl` with `path` to capture an already-running preview/site without generating, building, starting, or restarting preview.\n- Use preview.stop only in the same long-running MCP server or `webstudio mcp run` process that started preview. A separate one-shot `single-op-call` process does not own another process\'s preview controller.\n- Use waitForSelector when the rendered app has a reliable ready marker, waitUntil:"networkidle" for network-heavy pages, and waitForTimeout only for final visual settling.\n- Preview installs generated app dependencies under `.webstudio/preview` and reuses them across regenerations.\n- Do not add generated-preview dependencies to the repository root `package.json` or `pnpm-lock.yaml`.\n- If dependency installation fails, check npm and network configuration, then reinstall or update the Webstudio CLI if the problem persists.\n- When a baseline exists, use screenshot.diff once per baseline/current page or viewport pair to get changed regions, OCR textAnalysis, and diff artifact paths before deciding whether the result matches. Pass expectedText for explicit pass/fail current-screen text assertions with found and missing text. Pass expectedVisual for pass/fail limits on pixel mismatch percentage, changed-region count, or the overall dominant color/brightness direction.\n- If screenshot.diff reports OCR unavailable and the user agrees to install it, call vision.install-ocr {"confirm":true}; otherwise continue with pixel diff and visual inspection.\n- Compare the PNG, OCR text evidence, and diff artifacts against the user\'s intent for layout, typography, colors, spacing, imagery, and responsive framing; then iterate with focused mutations.\n- Root CLI equivalent: `webstudio screenshot --path /pricing --output pricing.png` generates a temporary production preview, captures that route, and stops the server. For repeated captures, keep `webstudio preview` running and pass its absolute URL to `webstudio screenshot`.\n\n## List pages\n\nCommands:\n\n- MCP tool: list-pages {}\n- MCP tool: list-folders {}\n\n## Read page by id\n\nCommands:\n\n- MCP tool: get-page {"pageId":""}\n\n## Read page by path\n\nCommands:\n\n- MCP tool: get-page-by-path {"path":"/pricing"}\n\n## Create page\n\nCommands:\n\n- MCP tool: create-page {"name":"Pricing","path":"/pricing"}\n- MCP tool: create-page {"name":"Pricing","path":"/pricing","title":"Pricing","meta":{"description":"Plans for teams"}}\n\nNotes:\n\n- `name`, `path`, page `title`, and metadata text fields accept plain fixed values.\n- For computed page titles or metadata, send JavaScript expression code such as `pageTitle ?? "Pricing"`.\n\n## Update page settings/metadata\n\nCommands:\n\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans","status":200}}}\n- MCP tool: update-page {"pageId":"","values":{"meta":{"auth":{"method":"basic","login":"","password":""}}}}\n\nNotes:\n\n- Page `title` and metadata text fields accept plain fixed values.\n- For computed page titles or metadata, send JavaScript expression code such as `pageTitle ?? "Pricing"`.\n- Page `status` accepts a fixed HTTP status code as a number from 200 through 599 or a JavaScript expression string for a dynamic status.\n\n## Read project settings\n\nCommands:\n\n- MCP tool: get-project-settings {}\n\nNotes:\n\n- Read `meta.agentInstructions` before making project changes. It contains the project\'s own guidance for AI agents.\n- Agent instructions are shared project guidance. Do not store credentials or other secrets there.\n\n## Update project settings\n\nCommands:\n\n- MCP tool: update-project-settings {"meta":{"siteName":"Acme"}}\n- MCP tool: update-project-settings {"meta":{"agentInstructions":"Use existing design tokens and keep product copy concise."}}\n\n## Read marketplace product\n\nCommands:\n\n- MCP tool: get-marketplace-product {}\n\n## Update marketplace product\n\nCommands:\n\n- MCP tool: update-marketplace-product {"category":"pageTemplates","name":"Acme Template","thumbnailAssetId":"asset-id","author":"Acme Studio","email":"hello@example.com","website":"https://example.com","issues":"","description":"Reusable template project for Acme landing pages."}\n\n## List redirects\n\nCommands:\n\n- MCP tool: list-redirects {}\n\n## Create redirect\n\nCommands:\n\n- MCP tool: create-redirect {"old":"/old","new":"/new","status":301}\n\n## Update redirect\n\nCommands:\n\n- MCP tool: update-redirect {"old":"/old","values":{"new":"/newer","status":302}}\n- MCP tool: update-redirect {"old":"/old","values":{"status":null}}\n\n## Delete redirect\n\nCommands:\n\n- MCP tool: delete-redirect {"old":"/old"}\n\n## Set redirects\n\nCommands:\n\n- MCP tool: set-redirects {"redirects":[{"old":"/old","new":"/new","status":"301"}]}\n\n## List breakpoints\n\nCommands:\n\n- MCP tool: list-breakpoints {}\n\n## Create breakpoint\n\nCommands:\n\n- MCP tool: create-breakpoint {"label":"Tablet","maxWidth":991}\n\n## Update breakpoint\n\nCommands:\n\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"label":"Tablet","maxWidth":1023}}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"condition":null,"minWidth":768}}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"minWidth":null,"maxWidth":null,"condition":"(hover: hover)"}}\n\n## Delete breakpoint\n\nCommands:\n\n- MCP tool: delete-breakpoint {"breakpointId":"tablet"}\n\n## Duplicate page\n\nCommands:\n\n- MCP tool: duplicate-page {"pageId":"","name":"Pricing Copy","path":"/pricing-copy"}\n- MCP tool: duplicate-page {"pageId":"","name":"Paris","path":"/paris","substitutions":{"text":{"London":"Paris"},"variables":{"city":{"type":"string","value":"Paris"}}}}\n- webstudio duplicate-page --page --name Paris --path /paris --substitutions \'{"text":{"London":"Paris"},"variables":{"city":{"type":"string","value":"Paris"}}}\' --json\n\nNotes:\n\n- Text substitutions replace exact fixed text only in the duplicated page\'s text children, string props, title, and metadata.\n- Variable substitutions are keyed by copied source-variable name and use typed variable values. The operation rejects missing or ambiguous names without committing a partial duplicate.\n- Existing expressions and cloned variable/resource references keep their remapped ids.\n\n## List page templates\n\nCommands:\n\n- MCP tool: list-page-templates {}\n\n## Create page template\n\nCommands:\n\n- MCP tool: create-page-template {"name":"Landing Template","title":"Landing"}\n\n## Update page template\n\nCommands:\n\n- MCP tool: update-page-template {"templateId":"","values":{"name":"Article Template","meta":{"description":"Reusable article layout"}}}\n\n## Delete page template\n\nCommands:\n\n- MCP tool: delete-page-template {"templateId":""}\n\n## Duplicate page template\n\nCommands:\n\n- MCP tool: duplicate-page-template {"templateId":""}\n\n## Reorder page template\n\nCommands:\n\n- MCP tool: reorder-page-template {"sourceTemplateId":"","targetTemplateId":"","position":"before"}\n\n## Create page from template\n\nCommands:\n\n- MCP tool: create-page-from-template {"templateId":"","name":"Landing","path":"/landing"}\n\n## Delete page\n\nCommands:\n\n- MCP tool: delete-page {"pageId":""}\n\n## List folders\n\nCommands:\n\n- MCP tool: list-folders {}\n- MCP tool: list-pages {}\n\n## Create folder\n\nCommands:\n\n- MCP tool: create-folder {"name":"Blog","slug":"blog"}\n\n## Update folder\n\nCommands:\n\n- MCP tool: update-folder {"folderId":"","values":{"name":"Blog","slug":"blog"}}\n\n## Delete folder\n\nCommands:\n\n- MCP tool: delete-folder {"folderId":""}\n\n## List element instances\n\nCommands:\n\n- MCP tool: list-instances {"pagePath":"/","maxDepth":3}\n\n## Inspect one element instance\n\nCommands:\n\n- MCP tool: inspect-instance {"instanceId":"","include":["props","styles","children"]}\n\n## Insert authored JSX or one component template\n\nCommands:\n\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"Product OS"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"@webstudio-is/sdk-components-react-radix:Switch"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"Form"}\n\nNotes:\n\n- Use MCP `insert-fragment` as the default way to author styled component trees. It converts JSX to a structured fragment before mutation.\n- Use only exact component ids returned by `components.search`, `components.get`, or `templates.get`. Never derive or guess component ids.\n- The `ws:` namespace contains specific Webstudio core components; it is not HTML-tag shorthand. Use `` for a native `div` and `` for a native form, never `` or ``.\n- For Webstudio\'s complete form structure, discover the Form component and insert its automatic template with `insert-component` using component `"Form"`.\n- MCP receives JSX as a JSON string because MCP arguments are JSON. The CLI converts it locally before the runtime mutation, so the project session receives structured Webstudio data, not JSX source.\n- In `insert-fragment` JSX, use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n- Do not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`.\n- Use Webstudio prop names such as `class` and `for`; do not use React aliases `className` or `htmlFor`.\n- Use Webstudio actions for event/action props, for example `onClick={new ActionValue(["event"], expression\\`console.log(event)\\`)}`. Do not pass JavaScript functions such as `onClick={() => ...}`.\n- Plain prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n- Template-backed components used in JSX must include required child/part components explicitly under the same parent structure as the template, for example ``.\n- Webstudio applies a registered template automatically when using `insert-component`, so composed components such as Switch include required child parts and styles.\n- Use `components.list`, `components.summary`, `components.search`, `components.get`, `templates.list`, and `templates.get` to discover known registry items, component ids, props, templates, insertability, and content model. Read `webstudio://project/components` only when those focused tools are insufficient.\n- Component/template registry items use a shadcn-compatible top-level shape plus Webstudio-specific superset metadata in `meta`. They are for Builder/MCP discovery, not a published shadcn install registry yet.\n- Known components with `contentModel.category: "none"` are not standalone-insertable; insert their root component template instead so required providers/parents are included.\n- Unknown custom component ids are a low-level extension mechanism, not a discovery fallback. Agents must not synthesize them.\n\n## Make a region editable in Content mode\n\nCommands:\n\n- MCP tool: insert-component {"parentInstanceId":"","component":"ws:block"}\n- MCP tool: inspect-instance {"instanceId":"","include":["children"]}\n\nNotes:\n\n- When a page will be handed to a Content-mode editor, wrap every region they should be able to edit in a Content Block (`ws:block`). Content-mode editors can edit text and supported props only in Content Block descendants. Content outside those blocks remains read-only, even when it looks like ordinary editable text.\n- Put reusable insertable options inside the Content Block\'s `ws:block-template` child. A template is source material, not editor content: editors cannot edit or delete it directly. When an editor inserts a template, its copy becomes a direct child of the Content Block and is editable.\n- Before handing off a page, verify with `inspect-instance` that the intended text, images, and links are inside a Content Block, and that templates include all required styling because Content-mode editors cannot use the Style panel.\n\n## Move elements\n\nCommands:\n\n- MCP tool: move-instance {"moves":"moves.json contents"}\n\nNotes:\n\n- Use `position: "end"` to append an instance. Repeating this for A and then B preserves the final order A, B.\n- A numeric `insertIndex` addresses the target parent\'s children before the moved instance is removed. Use it for exact placement; do not calculate the last index to append.\n- Moves in one `moves` array are applied sequentially in array order.\n\n## Clone element subtree\n\nCommands:\n\n- MCP tool: clone-instance {"sourceInstanceId":"","targetParentInstanceId":""}\n\n## Delete element subtree\n\nCommands:\n\n- MCP tool: delete-instance {"instanceIds":[""]}\n\n## List text/expression children\n\nCommands:\n\n- MCP tool: list-texts {"pagePath":"/"}\n\n## Update text child\n\nCommands:\n\n- MCP tool: update-text {"instanceId":"","childIndex":0,"text":"Launch faster"}\n\n## Replace bounded literal text\n\nCommands:\n\n- MCP tool: replace-text {"find":"Start free","replace":"Get started","match":"exact","pagePath":"/pricing","limit":20}\n\nNotes:\n\n- This changes only literal text children, never expression children. Scope it to pagePath or pageId and set a limit before a broad replacement.\n\n## Replace bounded static prop text\n\nCommands:\n\n- MCP tool: replace-prop-text {"find":"old.example.com","replace":"www.example.com","match":"substring","names":["href","code"],"limit":20}\n\nNotes:\n\n- This changes only static string props such as href, alt, aria-label, title, and HTML embed code. It never changes expressions, resources, actions, assets, or other dynamic bindings. Use names or instanceIds and a limit to narrow the change.\n\n## Replace bounded resource text\n\nCommands:\n\n- MCP tool: replace-resource-text {"find":"api.old.example.com","replace":"api.example.com","fields":["url"],"limit":20}\n\nNotes:\n\n- This changes resource names and fixed URL literals only. It skips dynamic URL expressions, headers, search parameters, request bodies, and GraphQL query code.\n\n## Update props\n\nCommands:\n\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: replace-prop-text {"find":"Old label","replace":"New label","names":["aria-label","title"],"limit":20}\n\nNotes:\n\n- Use this for fixed prop values such as `aria-label`, `alt`, `id`, static `href`, and other direct string/number/boolean/json prop values.\n\n## Add JSON-LD structured data\n\nCommands:\n\n- MCP tool: components.get {"component":"JsonLd"}\n- MCP tool: insert-component {"parentInstanceId":"","component":"JsonLd"}\n- MCP tool: update-props {"updates":[{"instanceId":"","name":"code","type":"string","value":"{\\"@context\\":\\"https://schema.org\\",\\"@type\\":\\"Organization\\",\\"name\\":\\"Acme\\"}"}]}\n- MCP tool: audit {"scopes":["seo"],"pagePath":"/"}\n\nNotes:\n\n- Prefer placing `JsonLd` inside `HeadSlot`.\n- Store `code` as a JSON object or array encoded as a compact string. The Builder formats it for editing.\n- The semantic prop update rejects malformed JSON and structurally invalid fixed JSON-LD with a precise JSON path.\n- The SEO audit also warns about a missing top-level `@context`, unknown or superseded Schema.org terms, properties unsupported by the supplied type, and incompatible primitive value types.\n- Schema.org vocabulary findings are warnings because custom vocabularies and extensions remain valid. Dynamic JSON-LD is marked as skipped for rendered validation.\n- Do not use bindings just to set static text.\n\n## Delete props\n\nCommands:\n\n- MCP tool: delete-props {"deletions":"props.json contents"}\n\n## Bind props to expressions/resources/actions\n\nCommands:\n\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n\nNotes:\n\n- Use this only when the prop should remain dynamic: expression, resource, action, or an existing scoped runtime context value such as `system`.\n- For a fixed string value, use `update-props` with `type:"string"` and a direct `value` instead.\n\n## Read styles\n\nCommands:\n\n- MCP tool: get-styles {"instanceIds":[""],"includeTokens":true}\n\n## Update local styles\n\nCommands:\n\n- MCP tool: update-styles {"updates":"styles.json contents"}\n\n## Delete local styles\n\nCommands:\n\n- MCP tool: delete-styles {"deletions":"styles.json contents"}\n\n## Replace matching style values\n\nCommands:\n\n- MCP tool: replace-styles {"property":"color","fromValue":{"type":"keyword","value":"red"},"toValue":{"type":"keyword","value":"blue"}}\n\n## List design tokens\n\nCommands:\n\n- MCP tool: list-design-tokens {}\n- MCP tool: list-design-tokens {"withUsage":true}\n- MCP tool: list-design-tokens {"verbose":true}\n\nNotes:\n\n- The default response is compact and includes token id, name, declaration count, and optional usage count.\n- Use `verbose:true` only when you need the full inline style declarations.\n\n## Create design tokens\n\nCommands:\n\n- MCP tool: create-design-token {"tokens":"tokens.json contents"}\n\n## Update design token styles\n\nCommands:\n\n- MCP tool: update-design-token-styles {"designTokenId":"","updates":"styles.json contents"}\n\n## Delete design token styles\n\nCommands:\n\n- MCP tool: delete-design-token-styles {"designTokenId":"","deletions":"styles.json contents"}\n\n## Attach design token to instances\n\nCommands:\n\n- MCP tool: attach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n\n## Detach design token from instances\n\nCommands:\n\n- MCP tool: detach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n\n## Extract design token from local styles\n\nCommands:\n\n- MCP tool: extract-design-token {"instanceIds":[""],"name":"Brand Primary","removeLocalProps":["color"]}\n\n## List CSS variables\n\nCommands:\n\n- MCP tool: list-css-variables {"withUsage":true}\n\n## Define CSS variables\n\nCommands:\n\n- MCP tool: define-css-variable {"vars":"vars.json contents"}\n\n## Delete CSS variables\n\nCommands:\n\n- MCP tool: delete-css-variable {"names":"names.json contents","force":true}\n\n## Rewrite CSS variable references\n\nCommands:\n\n- MCP tool: rewrite-css-variable-refs {"map":"variables.json contents"}\n\n## List data variables\n\nCommands:\n\n- MCP tool: list-variables {}\n- MCP tool: list-variables {"scopeInstanceId":""}\n\nNotes:\n\n- Data variables live in the internal `dataSources` namespace.\n- For raw `snapshot`, request the public `variables` namespace rather than the internal `dataSources` name. Raw patch payloads still use `dataSources` when applying direct changes.\n- Scope variables to the instance where they should become available. Descendants can use them in expressions, and nested variables with the same name mask outer variables.\n\n## Create data variable\n\nCommands:\n\n- MCP tool: create-variable {"scopeInstanceId":"","name":"title","value":{"type":"string","value":"Hello"}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"count","value":{"type":"number","value":3}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"featured","value":{"type":"boolean","value":true}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"tags","value":{"type":"string[]","value":["news","product"]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"filters","value":{"type":"json","value":{"tag":"news"}}}\n\nNotes:\n\n- Data variable values support `string`, `number`, `boolean`, `string[]`, and `json`.\n- Parameters are internal scoped runtime values provided by pages, collections, or components. They are not a public authoring surface: do not create, update, or delete parameter records. Use data variables/resources for user-authored data, and reference documented context values such as `system` only where they are already in scope.\n\n## Update data variable\n\nCommands:\n\n- MCP tool: update-variable {"dataSourceId":"","values":{"value":{"type":"json","value":{"count":1}}}}\n\n## Delete data variable\n\nCommands:\n\n- MCP tool: delete-variable {"dataSourceId":""}\n\n## List resources\n\nCommands:\n\n- MCP tool: list-resources {}\n- MCP tool: list-resources {"scopeInstanceId":""}\n\n## Create resource\n\nCommands:\n\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"https://api.example.com/posts","headers":[]}}\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"\\"https://api.example.com/posts?tag=\\" + filters.tag","headers":[]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Filtered Posts","method":"get","url":"https://api.example.com/posts","searchParams":[{"name":"tag","value":"filters.tag"},{"name":"source","value":{"type":"literal","value":"website"}}],"headers":[{"name":"Authorization","value":"\\"Bearer \\" + auth.token"}]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Post GraphQL","control":"graphql","method":"post","url":"https://api.example.com/graphql","headers":[{"name":"Content-Type","value":{"type":"literal","value":"application/json"}}],"body":"{ query: \\"query Post($slug: String!) { post(slug: $slug) { title } }\\", variables: { slug: system.params.slug } }"},"scopeInstanceId":"","dataSourceName":"post"}\n- MCP tool: create-resource {"resource":{"name":"Current Date","control":"system","method":"get","url":"/$resources/current-date","headers":[]},"scopeInstanceId":"","dataSourceName":"currentDate"}\n\nNotes:\n\n- Resource `url` accepts plain fixed URLs and paths such as `https://api.example.com/posts` and `/$resources/current-date`.\n- Resource `url` can also be a JavaScript expression when it is computed, such as `"https://api.example.com/posts?tag=" + filters.tag`.\n- Header values, search parameter values, and body accept expressions for dynamic values. For fixed text, use `{"type":"literal","value":"application/json"}`; Webstudio stores the required string expression for you.\n- Search parameter values, header values, and body expressions can read scoped variables and documented runtime context values such as `system` when they are available at the resource scope.\n- Add `scopeInstanceId` and `dataSourceName` when the resource result should be exposed as a scoped read data variable. Scoped resources are generated into the page resource `data` map and may be loaded during page rendering. Use this for read-oriented resources such as GET CMS/API data.\n- For submit/write/action resources, create the resource without `scopeInstanceId`, then bind a component prop such as a Form `action` with `bind-props` and `binding.type: "resource"`. Prop-bound resources are generated into the page resource `action` map instead of the read `data` map. Use this for POST, PUT, DELETE, webhooks, GraphQL submissions, and other explicit action flows.\n- Resource `method` can be `get`, `post`, `put`, or `delete`. Use GET for read data, POST for creates/GraphQL/webhooks/form submissions, PUT for full updates or replacements, and DELETE for deletion actions.\n- Optional `control` values are `graphql` and `system`. Use `graphql` for GraphQL-style requests, usually POST with a query body. Use `system` for built-in resources such as `"/$resources/sitemap.xml"`, `"/$resources/current-date"`, and `"/$resources/assets"` and when the resource should use the built-in `system` parameter. System fields are `system.origin`, `system.pathname`, `system.params`, and `system.search`.\n\n## Update resource\n\nCommands:\n\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: replace-resource-text {"find":"api.old.example.com","replace":"api.example.com","fields":["url"],"limit":20}\n\n## Delete resource\n\nCommands:\n\n- MCP tool: delete-resource {"resourceId":""}\n\n## List assets\n\nCommands:\n\n- MCP tool: list-assets {"withUsage":true}\n- MCP tool: list-assets {"verbose":true}\n\nNotes:\n\n- Compact results include each asset\'s folder id. Use `verbose:true` to include complete records for a page of assets, or `get-asset` to read one complete record including description, folder, creation time, and image/font metadata.\n- Image asset descriptions are the default alt text for asset-backed Image components.\n- To generate missing descriptions, inspect the image in its rendered page or asset source, write a concise description of its purpose, and save it on the asset rather than duplicating it on each Image instance.\n\n## Get asset\n\nCommands:\n\n- MCP tool: get-asset {"assetId":""}\n\n## List asset folders\n\nCommands:\n\n- MCP tool: list-asset-folders {}\n\n## Create asset folder\n\nCommands:\n\n- MCP tool: create-asset-folder {"name":"Marketing"}\n- MCP tool: create-asset-folder {"name":"Photos","parentId":""}\n\n## Update asset folder\n\nCommands:\n\n- MCP tool: update-asset-folder {"folderId":"","values":{"name":"Brand"}}\n- MCP tool: update-asset-folder {"folderId":"","values":{"parentId":""}}\n- MCP tool: update-asset-folder {"folderId":"","values":{"parentId":null}}\n\nNotes:\n\n- Updating `parentId` is the folder equivalent of cut and paste. Use `null` to move a folder to Root.\n\n## Duplicate asset folder\n\nCommands:\n\n- MCP tool: duplicate-asset-folder {"folderId":""}\n- MCP tool: duplicate-asset-folder {"folderId":"","parentId":""}\n\nNotes:\n\n- Duplication recursively copies descendant folders and assets. This is the folder equivalent of copy and paste.\n\n## Delete asset folder\n\nCommands:\n\n- MCP tool: delete-asset-folder {"folderId":""}\n\nNotes:\n\n- Deleting a folder recursively deletes its descendant folders and assets.\n\n## Update asset metadata\n\nCommands:\n\n- MCP tool: update-asset {"assetId":"","values":{"description":"Team collaborating around a whiteboard"}}\n\nNotes:\n\n- Use an empty description only when the image is intentionally decorative.\n- Updating an image asset description updates the default alt text wherever that asset is used with an asset-backed alt prop.\n\n## Generate missing image descriptions with an agent\n\nCommands:\n\n- MCP tool: audit {"scopes":["accessibility"],"verbose":true}\n- MCP tool: set-image-descriptions {"updates":[{"assetId":"hero-id","description":"Team collaborating around a whiteboard"},{"assetId":"texture-id","decorative":true}]}\n- MCP tool: audit {"scopes":["accessibility"]}\n\nNotes:\n\n- Start from `missing-image-description` findings. Inspect each image in its rendered page context before writing text.\n- The vision-capable agent generates the wording; the CLI validates and stores it but does not contain its own vision model.\n- Use `decorative:true` only when the image adds no information. This intentionally stores an empty description so later audits do not report it as missing.\n- Re-run the accessibility audit after the update. Asset-backed Image components use the saved asset description as their default alt text.\n\n## Manage fonts\n\nCommands:\n\n- MCP tool: list-fonts {"includeSystem":true}\n- MCP tool: list-assets {"type":"font"}\n- MCP tool: upload-asset {"asset":{"name":"acme-sans.woff2","type":"font","format":"woff2","meta":{"family":"Acme Sans","style":"normal","weight":400}},"assetsDir":".webstudio/assets"}\n- MCP tool: update-styles {"updates":"styles.json contents"}\n\nNotes:\n\n- Use `list-fonts` to discover uploaded families and system stacks. Upload/delete fonts through the existing asset tools, then apply a family with a `font-family` style declaration.\n\n## Upload one asset\n\nCommands:\n\n- MCP tool: upload-asset {"asset":{"name":"image.png","type":"image","format":"png","meta":{"width":1200,"height":630}},"assetsDir":".webstudio/assets"}\n- MCP tool: upload-asset {"asset":{"name":"image.png","type":"image","format":"png","folderId":"","meta":{"width":1200,"height":630}},"assetsDir":".webstudio/assets"}\n\n## Upload asset batch\n\nCommands:\n\n- MCP tool: upload-assets {"assets":[{"name":"image.png","type":"image","format":"png","meta":{"width":1200,"height":630}}],"assetsDir":".webstudio/assets"}\n\n## Duplicate asset\n\nCommands:\n\n- MCP tool: duplicate-asset {"assetId":""}\n- MCP tool: duplicate-asset {"assetId":"","folderId":""}\n- MCP tool: duplicate-asset {"assetId":"","folderId":null}\n\nNotes:\n\n- Duplication is the asset equivalent of copy and paste. Updating `folderId` is the equivalent of cut and paste; use `null` for Root.\n\n## Download asset\n\nCommands:\n\n- MCP tool: download-asset {"assetId":""}\n\n## Find asset usage\n\nCommands:\n\n- MCP tool: find-asset-usage {"assetId":""}\n\n## Replace asset references\n\nCommands:\n\n- MCP tool: replace-asset {"fromAssetId":"","toAssetId":""}\n\n## Delete assets\n\nCommands:\n\n- MCP tool: delete-asset {"assetIdsOrPrefixes":[""],"force":true}\n\n## Publish project\n\nCommands:\n\n- webstudio publish deploy --target production --json\n\n## List publishes\n\nCommands:\n\n- webstudio publish list --json\n\n## Check publish job\n\nCommands:\n\n- webstudio publish status --job --json\n\n## Unpublish\n\nCommands:\n\n- webstudio publish unpublish --target production --confirm --json\n\n## List domains\n\nCommands:\n\n- webstudio domains list --json\n\n## Create domain\n\nCommands:\n\n- webstudio domains create --domain example.com --json\n\n## Update domain\n\nCommands:\n\n- webstudio domains update --domain-id --domain www.example.com --json\n\n## Delete domain\n\nCommands:\n\n- webstudio domains delete --domain-id --confirm --json\n\n## Verify domain\n\nCommands:\n\n- webstudio domains verify --domain-id --json\n\n## Make arbitrary store-level changes\n\nCommands:\n\n- MCP tool: inspect {}\n- MCP tool: snapshot {"include":[""]}\n- MCP tool: apply-patch {"baseVersion":"","transactions":"patch.json contents"}\n\nNotes:\n\n- Use only when no semantic command exists.\n\n## Manage marketplace metadata\n\nCommands:\n\n- MCP tool: get-marketplace-product {}\n- MCP tool: update-marketplace-product {"category":"pageTemplates","name":"Acme Template","thumbnailAssetId":"asset-id","author":"Acme Studio","email":"hello@example.com","website":"https://example.com","issues":"","description":"Reusable template project for Acme landing pages."}\n\nPatch namespaces:\n\n- marketplaceProduct\n\n## Search and inspect safely\n\nCommands:\n\n- MCP tool: search-project {"query":"pricing"}\n- MCP tool: search-project {"query":"api.example.com","scopes":["resources"]}\n- MCP tool: list-instances {"pagePath":"/","maxDepth":5}\n- MCP tool: inspect-instance {"instanceId":"","include":["props","styles","children"]}\n- MCP tool: list-texts {"pagePath":"/"}\n- MCP tool: list-assets {"withUsage":true}\n- MCP tool: find-asset-usage {"assetId":""}\n- MCP tool: snapshot {"include":["pages","instances","props","resources","assets"]}\n\nNotes:\n\n- Use `search-project` for query-driven lookup across labels, text, prop values, resource URLs, asset metadata, and styles. Use `audit` for project health findings.\n\n## Audit project quality\n\nCommands:\n\n- webstudio audit --json\n- webstudio audit --scopes accessibility --scopes seo --json\n- webstudio audit --page-path /pricing --json\n- webstudio audit --scopes accessibility --verbose --json\n- webstudio audit --rendered --page-path /pricing --json\n- webstudio audit --rendered --route-example post=/blog/hello --json\n- webstudio audit --rendered --image-domain images.example.com --json\n- MCP tool: audit {}\n- MCP tool: audit {"scopes":["accessibility","security"],"severities":["error","warning"]}\n- MCP tool: audit {"scopes":["accessibility"],"verbose":true}\n- MCP tool: audit {"scopes":["craft"],"verbose":true}\n- MCP tool: audit {"rendered":true,"verbose":true}\n\nNotes:\n\n- With no scopes, `audit` checks accessibility, security, SEO, performance settings, unused assets, ineffective Collection styles, non-GET resources exposed as render-time data, and unused or duplicate style data.\n- Craft is opt-in and read-only. Run `audit` with `scopes:["craft"]` to detect whether the project is not using Craft, partially compatible, or compatible with the versioned Craft 1.2 profile. `profileStatuses` includes the University-doc provenance and the smallest safe next action. The audit never installs Craft or changes a non-Craft project.\n- The `performance` scope reports disabled atomic CSS generation. A rendered audit also measures broken, eager below-fold, and oversized images, browser-marked render-blocking resources, and legacy font formats.\n- Rendered image and resource metrics run only when the selected scopes include `performance`; responsive layout dimensions remain available whenever `rendered:true` is requested.\n- Compact findings include stable ids, severity, message, and location. Use `--verbose` or `{"verbose":true}` for evidence, explanation, suggested remediation, skipped-check details, and manual-check workflows.\n- `summary` counts all findings before severity filtering and pagination.\n- `contractVersion` identifies the audit response contract. Handle a new value before assuming existing fields retain the same meaning.\n- Expression-, resource-, and parameter-backed values that cannot be checked reliably appear in `skippedChecks`; they are not treated as passing or failing.\n- Page filters apply to page-owned accessibility, security, and SEO checks. Asset and style usage remain project-wide to avoid false unused findings.\n- Continue paginated results with `cursor`. Restart the audit if the project version changes.\n- Verbose skipped-check and manual-check details are included on the first findings page only; their total counts remain available on every page.\n- `manualChecks` describes responsive, hierarchy, and contrast checks that require preview screenshots and vision.\n- Focused audits return only manual checks relevant to their selected scopes.\n- In a long-lived MCP session, `{"rendered":true}` reuses preview and screenshot\n tools to capture every static page at mobile, desktop, and Builder breakpoint\n edges. Compact output reports rendered check/issue/failure counts; verbose\n output includes screenshot paths and measured layout dimensions.\n- Dynamic route templates are skipped unless `--route-example =`\n (or MCP `routeExamples`) supplies a concrete path. The path must not contain\n unresolved `:` or `*` parameters.\n- Plans above 120 captures return a short-lived confirmation token. Review the\n unchanged plan, then rerun with `--confirm-large-run` and\n `--confirmation-token`.\n- Detailed rendered evidence is stored in a versioned manifest under\n `.webstudio/audits`; compact output includes its path and screenshot count.\n- Rendered checks also report broken images, eager images below the fold, and\n image sources more than 2x their rendered dimensions in both axes, including\n Webstudio instance ids and measured dimensions when available.\n- Rendered checks include sanitized Resource Timing evidence and report\n browser-marked render-blocking resources plus legacy `.ttf`, `.otf`, and\n `.woff` fonts without applying a universal transfer-size threshold.\n- Fix findings through semantic mutation commands, then rerun `audit` to confirm their deterministic finding ids disappeared.\n\n## Verify dynamic bindings\n\nCommands:\n\n- webstudio verify-bindings --json\n- MCP tool: verify-bindings {"pagePath":"/pricing"}\n- MCP tool: verify-bindings {"instanceId":"","limit":50}\n\nNotes:\n\n- Statically checks persisted text expressions, expression/action/resource/parameter props, resource expressions, and page metadata.\n- Findings distinguish invalid syntax, unknown or out-of-scope variables, stale internal data-source ids, and missing resource or parameter references.\n- Page and instance filters can be combined when the instance belongs to the selected page. Continue findings with `cursor`.\n- This operation does not resolve rendered values or execute external resources. Preview representative loading, empty, error, and populated states after static findings are fixed.\n\n## Refactor targeted content\n\nCommands:\n\n- MCP tool: list-instances {"pagePath":"/"}\n- MCP tool: list-texts {"pagePath":"/"}\n- MCP tool: update-text {"instanceId":"","childIndex":0,"text":"Launch faster"}\n- MCP tool: replace-text {"find":"Old headline","replace":"New headline","match":"exact","pagePath":"/pricing","limit":20}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans"}}}\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: replace-asset {"fromAssetId":"","toAssetId":""}\n- MCP tool: replace-styles {"property":"color","fromValue":{"type":"keyword","value":"red"},"toValue":{"type":"keyword","value":"blue"}}\n- MCP tool: rewrite-css-variable-refs {"map":"variables.json contents"}\n\nNotes:\n\n- Use focused reads first, then mutate only matching instances, props, metadata, resource URLs, assets, or style references. Use `replace-text` for bounded literal text changes, `replace-prop-text` for bounded static prop text, `replace-resource-text` for fixed resource names/URLs, and `update-text` for one known child or expressions.\n\n## Optimize existing project\n\nCommands:\n\n- MCP tool: list-pages {}\n- MCP tool: list-folders {}\n- MCP tool: update-page {"pageId":"","values":{"title":"Pricing","meta":{"description":"Plans"}}}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: list-breakpoints {}\n- MCP tool: update-breakpoint {"breakpointId":"tablet","values":{"maxWidth":1023}}\n- MCP tool: get-styles {"instanceIds":[""],"includeTokens":true}\n- MCP tool: update-styles {"updates":"styles.json contents"}\n- MCP tool: attach-design-token {"designTokenId":"","instanceIds":"instances.json contents"}\n- MCP tool: update-project-settings {"meta":{"siteName":"Acme"}}\n\nNotes:\n\n- Use this for SEO metadata, accessibility labels, responsive behavior, token consistency, and project settings.\n\n## Connect external data\n\nCommands:\n\n- MCP tool: create-variable {"scopeInstanceId":"","name":"title","value":{"type":"string","value":"Hello"}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"tags","value":{"type":"string[]","value":["news","product"]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"filters","value":{"type":"json","value":{"tag":"news"}}}\n- MCP tool: create-resource {"resource":{"name":"Posts","method":"get","url":"https://api.example.com/posts","searchParams":[{"name":"tag","value":"filters.tag"},{"name":"source","value":{"type":"literal","value":"website"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"posts"}\n- MCP tool: create-resource {"resource":{"name":"Post GraphQL","control":"graphql","method":"post","url":"https://api.example.com/graphql","headers":[{"name":"Content-Type","value":{"type":"literal","value":"application/json"}}],"body":"{ query: \\"query Post($slug: String!) { post(slug: $slug) { title } }\\", variables: { slug: system.params.slug } }"},"scopeInstanceId":"","dataSourceName":"post"}\n- MCP tool: create-resource {"resource":{"name":"Current Date","control":"system","method":"get","url":"/$resources/current-date","headers":[]},"scopeInstanceId":"","dataSourceName":"currentDate"}\n- MCP tool: update-resource {"resourceId":"","values":{"url":"https://api.example.com/posts"}}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"{/_ collection content _/}"}\n\nNotes:\n\n- Use this for CMS sections, blog listings, Ghost/headless CMS pages, n8n-style integrations, and API URLs built from variables.\n- For read data, expose GET resources as scoped data variables with `scopeInstanceId`/`dataSourceName` and read the loaded result from the resource result wrapper, usually `.data`.\n- For writes, webhooks, GraphQL submissions, and deletes, prefer unscoped resources bound to Form `action` props so they become action resources instead of auto-loaded read resources.\n- Use direct props for fixed values and prop bindings only when a prop must read a data variable, resource, action, or documented runtime context value such as `system`.\n\n## Render an array or object as repeated content\n\nCommands:\n\n- MCP tool: insert-collection {"parentInstanceId":"","data":{"type":"expression","value":"posts.data.items"},"itemFragment":"{expression`collectionItem.title`}"}\n- MCP tool: inspect-instance {"instanceId":"","include":["props","bindings","children"]}\n\nNotes:\n\n- Use Collection whenever an array or object from a resource or data variable should render a list, grid, cards, table rows, options, tabs, or other repeated UI.\n- Pass `insert-collection` the complete array or object. Do not pass the resource response wrapper or one indexed item. External resource arrays are commonly nested under the scoped resource result\'s `data` field or deeper.\n- Pass one repeated-item Webstudio JSX root. The command creates the Collection, its private current-item/current-key parameters, the iterable binding, and descendant item bindings atomically.\n- Collection renders the item root once for every entry. Use `expression` values such as `collectionItem.title` in descendant text and props. Object iteration also exposes `collectionItemKey`.\n- Wrap multiple repeated sibling instances in one Element inside Collection.\n- For repeated Radix items such as accordion items, tabs, or menu options, bind a stable unique id or slug to every required `value` prop.\n- See the [Collection documentation](https://docs.webstudio.is/university/core-components/collection) for the equivalent Builder workflow.\n\n## Support dynamic runtime behavior\n\nCommands:\n\n- MCP tool: integrate-runtime-ui {"parentInstanceId":"","resources":[{"resource":{"name":"Seats","method":"get","url":"https://api.example.com/seats","headers":[]},"dataSourceName":"Seats","exposeAsDataSource":true}],"structure":{"type":"collection","data":{"type":"expression","value":"Seats.data"},"itemFragment":{"children":[{"type":"id","value":"seat"}],"instances":[{"type":"instance","id":"seat","component":"Text","children":[{"type":"expression","value":"collectionItem.label"}]}],"props":[],"dataSources":[],"resources":[],"styleSources":[],"styleSourceSelections":[],"styles":[],"breakpoints":[],"assets":[]}},"retainedBehavior":[{"instanceId":"","responsibility":"Seat selection behavior"}]}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n- MCP tool: create-resource {"resource":{"name":"Seats","method":"get","url":"https://api.example.com/seats","headers":[]}}\n- MCP tool: snapshot {"include":["instances","props","resources"]}\n- MCP tool: apply-patch {"baseVersion":"","transactions":"patch.json contents"}\n\nNotes:\n\n- Use `integrate-runtime-ui` to create variables/resources, insert one editable fragment or Collection, and add safe data bindings in one transaction.\n- List existing script-owned responsibilities under `retainedBehavior`. The operation preserves those instances and never evaluates or accepts replacement script bodies.\n- `unsupportedConversions` records behavior that cannot be represented safely. Dry-run returns the complete transaction and the same retained/unsupported report without changing the project.\n- New actions and HtmlEmbed scripts are intentionally rejected. Create normal editable components and data bindings; keep opaque runtime behavior in existing script instances.\n\n## Build authenticated pages\n\nCommands:\n\n- MCP tool: meta.guide {"brief":"Build a Supabase-authenticated account page"}\n- MCP tool: create-page {"name":"Account","path":"/account"}\n- MCP tool: update-page {"pageId":"","values":{"meta":{"auth":{"method":"basic","login":"","password":""}}}}\n- MCP tool: create-resource {"resource":{"name":"Session","method":"get","url":"https://api.example.com/session","headers":[]}}\n- MCP tool: create-variable {"scopeInstanceId":"","name":"user","value":{"type":"json","value":{}}}\n- MCP tool: update-props {"updates":"props.json contents"}\n- MCP tool: bind-props {"bindings":"bindings.json contents"}\n\nNotes:\n\n- Inspect and reuse the project\'s existing auth convention before authoring. Do\n not add a second provider or session model implicitly.\n- Model signed-out, loading, signed-in, and failed-auth states explicitly.\n- Never store credentials, service-role keys, refresh tokens, private session\n values, or authenticated response bodies in project data, command output,\n screenshots, agent instructions, or error reports. Privileged provider calls\n and authorization enforcement belong server-side.\n- Basic auth is semantic today. Provider-specific Supabase/Firebase setup still\n uses the existing resource, variable, prop, binding, and embed tools; there is\n no provider-specific installer.\n\n## Generate from design input\n\nCommands:\n\n- MCP tool: meta.guide {"brief":"Recreate this Figma design as a responsive page"}\n- MCP tool: create-page {"name":"Landing","path":"/landing"}\n- MCP tool: create-design-token {"tokens":"tokens.json contents"}\n- MCP tool: define-css-variable {"vars":"vars.json contents"}\n- MCP tool: list-breakpoints {}\n- MCP tool: insert-fragment {"parentInstanceId":"","fragment":"Section copy"}\n- MCP tool: update-styles {"updates":[{"instanceId":"","breakpointId":"","property":"padding-left","value":{"type":"unit","unit":"px","value":24}}]}\n- MCP tool: preview.start {"host":"127.0.0.1","port":5173}\n- MCP tool: screenshot {"path":"/landing","output":"landing-desktop.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"path":"/landing","output":"landing-mobile.png","viewport":{"width":390,"height":844},"waitUntil":"load","waitForTimeout":250}\n- MCP tool: screenshot {"baseUrl":"http://127.0.0.1:5177","path":"/landing","output":"landing-desktop.png","viewport":{"width":1440,"height":900},"waitUntil":"load","waitForTimeout":250}\n\nNotes:\n\n- Use this after the agent can inspect the supplied design. There is no direct\n Figma, screenshot, Inception, or `design.md` import command.\n- Inspect and reuse existing variables, tokens, styles, components, assets, and\n page patterns before authoring. Build semantic editable structure rather than\n flattening the design into an image or absolute-positioned approximation.\n- Verify one familiar viewport inside every distinct Builder breakpoint range,\n then run rendered audit and inspect the screenshots before completion.\n\n## Cross-project maintenance\n\nCommands:\n\n- webstudio mcp run .temp/projects.json\n- webstudio mcp run .temp/projects.json --dry-run\n- webstudio mcp run .temp/projects.json --approve-mutations --concurrency 2\n\nNotes:\n\n- Put shared `calls` and a `projects` array of independently linked project roots in the existing `mcp run` manifest. Project roots are relative to the manifest file.\n- Each project uses its own config, authentication, ProjectSession storage, checkpoint, and failure boundary. Confirmed successful calls are checkpointed. Reads can resume automatically; a mutation interrupted after dispatch is reported as ambiguous and is not replayed automatically, preventing silent duplicate writes.\n- Focus the manifest on bounded reads or audits first. Use per-call `dryRun`, global `--dry-run`, or explicitly approve committed mutations with `--approve-mutations` after reviewing the manifest.\n\n# Known CLI Gaps\n\n## Provider-specific authenticated pages\n\nMissing:\nCLI supports page basic auth and generic resources/props/embeds, but not guided Supabase/Firebase auth setup.\n\nCurrent fallback:\nCall `meta.guide` with the provider-authenticated page goal, then create the\npage, resources, variables, props, bindings, and embeds with existing semantic\ntools.\n\nSuggested commands:\n\n- setup-auth-page\n\n## Generate from design input\n\nMissing:\nNo command imports Figma, screenshots, Inception output, or design.md and turns it into pages/tokens/layout.\n\nCurrent fallback:\nCall `meta.guide` with the design-input goal, let the agent inspect the supplied\ndesign, then use semantic page, token, asset, fragment, style, preview,\nscreenshot, and audit tools. Use `apply-patch` only when no semantic operation\nfits.\n\nSuggested commands:\n\n- generate-from-design\n\n## Built-in cross-project maintenance\n\nMissing:\nPublic API and CLI intentionally operate on one configured project at a time; there is no built-in multi-project discovery or loop runner.\n\nCurrent fallback:\nRun the CLI from an external script that reconfigures one project/session at a time.\n\nSuggested commands:\n\n- none\n', "manual-api": '# Webstudio API CLI Manual\n\nThe API commands operate on the single project configured by:\n\n- .webstudio/config.json: projectId\n- global Webstudio config: origin and token\n\n- Pass --json to API/discovery commands that support it. Do not add --json to top-level commands unless their help/schema documents it.\n- Never pass a project id. Commands use configured project only.\n- Read ids before writing. Do not invent ids for existing records.\n- stdout is one JSON object. stderr is diagnostics.\n- Prefer MCP semantic tools for detailed project edits. Use MCP apply-patch only when no semantic tool exists.\n\n## Start\n\n{{start}}\n\n## Read First\n\n{{readFirst}}\n\n## Project Session Cache\n\n- CLI commands use one local ProjectSession snapshot for the configured project.\n- Local-capable reads use cached namespaces when compatible and fetch only missing or stale namespaces.\n- Local-capable mutations build patches from the local snapshot, then commit with the cached build version.\n- Successful mutation commits update the local snapshot only after the remote commit succeeds.\n- Server-only commands run remotely and invalidate/refetch namespaces declared by the operation catalog.\n- Use --refresh on local-capable commands to refresh required namespaces before running.\n- Successful JSON responses include compact meta.session with operationId, buildId, version, source, committed, namespaceCounts, diagnosticCount, non-empty diagnostic summaries, and optional compatibilityVersion.\n\n## CLI Capability Inventory\n\nFor a short end-consumer summary of what MCP can do, see\n`manual mcp` / `webstudio man mcp`. The MCP inventory describes the same\nproject, page, element, style, data, asset, publish, domain, and visual\nverification capabilities without internal command names.\n\n### Top-Level Commands\n\n{{topLevelCapabilityIndex}}\n\n### High-Level API Commands By Area\n\n{{apiCapabilityIndex}}\n\n### MCP Tool Operations\n\nThese are MCP tools. From a shell, call them with the shortcut form `webstudio \'\'` or with the explicit form `webstudio mcp single-op-call \'\'`:\n\n{{mcpOnlyCommandIndex}}\n\n## Task Recipes\n\n{{taskRecipeIndex}}\n\n## Use Case Index\n\n{{useCaseIndex}}\n\n## Known CLI Gaps\n\n{{knownCliGapIndex}}\n\n## Input File Shapes\n\n{{inputFileShapeIndex}}\n\n## Raw Patch Fallback\n\napply-patch accepts either BuildPatchTransaction[] or { "transactions": BuildPatchTransaction[] }.\n\nEach transaction has:\n\n{\n"id": "patch-transaction-label",\n"payload": [\n{\n"namespace": "projectSettings",\n"patches": [\n{ "op": "replace", "path": ["meta", "siteName"], "value": "New Site" }\n]\n}\n]\n}\n\nThe transaction id is a patch label used for optimistic synchronization. It is\nnot a Builder record id. Do not invent ids for pages, instances, props,\nbreakpoints, resources, variables, folders, assets, or other project records.\n\nPatch paths are JSON-patch-like paths into Builder store data. Map-like namespaces use ids as the first path item.\n\nSupported namespaces:\n\n- pages: redirects, page records, and folders\n- projectSettings: project-wide metadata and compiler settings\n- instances: element instances and children, including text/expression children\n- props: element props, bindings, page references, resource bindings\n- styles: CSS declarations keyed by style declaration key\n- styleSources: local style sources and reusable design tokens\n- styleSourceSelections: instance-to-style-source connections\n- dataSources: data variables, parameters, and resource data sources\n- resources: data resource definitions\n- assets: project asset records handled by the existing asset patch path\n- breakpoints: responsive breakpoints\n- marketplaceProduct: marketplace metadata\n\n## Data Sources\n\n`dataSources` is the internal Builder namespace for variables. Public API, CLI,\nand MCP tools expose it through two user-facing groups:\n\n- data variables: `list-variables`, `create-variable`, `update-variable`, and\n `delete-variable`\n- data resources: `list-resources`, `create-resource`, `update-resource`, and\n `delete-resource`\n\nFor raw `snapshot`, request the public `variables` namespace rather than the\ninternal `dataSources` name. Raw patch payloads still use `dataSources` when\napplying direct changes.\n\nVariables can be scoped to an instance. Expressions under that instance can use\nthe variable by name; nested variables with the same name mask outer variables.\nVariable values support `string`, `number`, `boolean`, `string[]`, and `json`.\nUse `string[]` for lists of strings such as tags or selected categories; use\n`json` for objects, arrays with mixed shapes, or nested API filter state.\nParameters are internal scoped runtime values provided by pages, collections,\nor components. They are not a public authoring surface: do not create, update,\nor delete parameter records. Public tools should preserve existing parameter\nrecords and may reference documented context values such as `system` in\nexpressions where they are already in scope.\n\nResource `url` accepts plain fixed URLs and paths, for example\n`https://api.example.com/posts` or `/$resources/current-date`. Dynamic URLs can\ncombine strings and variables, for example\n`"https://api.example.com/posts?tag=" + filters.tag`. Prefer `searchParams` for\nquery parameters that should be encoded separately:\n`[{ "name": "tag", "value": "filters.tag" }]`. Header values, search parameter\nvalues, and bodies are expressions for dynamic content. For fixed text, use\n`{ "type": "literal", "value": "application/json" }`; Webstudio stores the\nrequired string expression. Headers can still read variables such as\n`"Bearer " + auth.token`, and GraphQL bodies can return objects such as\n`{ query: "...", variables: { slug: system.params.slug } }`.\n\nCreate a GET resource with `scopeInstanceId` when the fetched resource result\nshould be available as a read data variable. Scoped GET resources default to\n`exposeAsDataSource: true`, are generated into the page resource `data` map,\nand may be loaded during page rendering. Use `dataSourceName` to choose the\nvariable name.\n\nFor submit/write/action resources, create the resource without\n`scopeInstanceId`, then bind a component prop such as a Form `action` to the\nresource with `bind-props` and `binding.type: "resource"`. Prop-bound resources\nare generated into the page resource `action` map instead of the read `data`\nmap. Use this shape for POST, PUT, DELETE, webhook, and other resources that\nshould run only from an explicit form/action flow, not merely because the page\nrendered.\n\nPOST, PUT, and DELETE resources default to `exposeAsDataSource: false` even\nwhen a scope is supplied. Set `exposeAsDataSource: true` only when a write-method\nresource intentionally provides render-time data, such as a read-only GraphQL\nPOST query. A scope is required, and the result includes a warning because the\nrequest may execute during page rendering. Set `exposeAsDataSource: false` on\n`update-resource` to detach an existing render-time data source.\n\nResource `method` can be `get`, `post`, `put`, or `delete`. Use GET for read\ndata. Use POST for creates, GraphQL requests, webhooks, and form submissions.\nUse PUT for full updates/replacements. Use DELETE for deletion actions.\nOptional `control` values are `graphql` and `system`: `graphql` marks a\nGraphQL-style resource, usually POST with a query body; `system` marks a\nresource intended to use the built-in `system` parameter or one of the built-in\nlocal resource URLs: `"/$resources/sitemap.xml"`,\n`"/$resources/current-date"`, and `"/$resources/assets"`. The system parameter\nfields are `system.origin`, `system.pathname`, `system.params`, and\n`system.search`.\n\nUse prop bindings for dynamic values that read variables or resources; use\ndirect props for static values.\n\nCommit raw patch:\n\nMCP tool: apply-patch\n\n## Raw Patch Examples\n\nRename the site:\n\n[\n{\n"id": "patch-site-name",\n"payload": [\n{\n"namespace": "projectSettings",\n"patches": [\n{ "op": "add", "path": ["meta", "siteName"], "value": "Acme Studio" }\n]\n}\n]\n}\n]\n\nUpdate page title metadata:\n\n[\n{\n"id": "patch-page-title",\n"payload": [\n{\n"namespace": "pages",\n"patches": [\n{ "op": "replace", "path": ["pages", "page-id", "meta", "title"], "value": "Pricing" }\n]\n}\n]\n}\n]\n\nUpdate a text child on an element:\n\n[\n{\n"id": "patch-text",\n"payload": [\n{\n"namespace": "instances",\n"patches": [\n{ "op": "replace", "path": ["instance-id", "children", 0, "value"], "value": "Launch faster" }\n]\n}\n]\n}\n]\n\nCreate records with semantic operations such as create-variable,\ncreate-resource, create-design-token, create-page, create-folder,\nand create-breakpoint. Raw patch rejects generated record\ncreation, collection replacement, record replacement with a different `id`, and\nrecord id field mutations in id-keyed namespaces because Webstudio must generate\nand preserve record ids.\n\n## Safety Rules\n\n- For MCP apply-patch, read the latest version with MCP snapshot before writing.\n- Reuse ids from MCP snapshot output when updating existing records.\n- Do not create generated records, replace generated record collections, replace records with different ids, or mutate record id fields with raw patch. Use semantic create operations so Webstudio generates ids.\n- If apply-patch reports a version conflict, read the latest build and regenerate the patch.\n- Prefer semantic MCP read tools for discovery, then use MCP snapshot for exact patch paths.\n\n## Command Index\n\n{{commandIndex}}\n', "manual-llm": '# Webstudio CLI Manual for LLMs\n\nUse this order. Stop only when a command returns ok:false.\n\nIf you are inside the Webstudio monorepo, the first command discovery should use\nthe local CLI exactly as `node packages/cli/local.js ...` from the repo root. Do\nnot use `packages/cli/bin.js` for local source-tree work; it is the packaged\nbuild entry and may use stale built output. Do not use `pnpm exec webstudio`,\n`pnpm --filter webstudio exec webstudio`, or a global `webstudio`: they can\nresolve an older binary.\n\nFor delegated design-system or “use every component” tasks, skip the generic warm-up sequence and start with exactly one MCP command: `webstudio workflow.next \'{"goal":"design-system-page"}\'`. Report that returned checkpoint to the parent/user and stop until continued.\n\n## Connect an MCP client\n\nWhen the user asks to connect the current folder to Webstudio, run the command\nfor the agent client you are currently using:\n\n- Claude Code: `webstudio connect claude`\n- Codex: `webstudio connect codex`\n- Cursor: `webstudio connect cursor`\n- VS Code or GitHub Copilot: `webstudio connect vscode`\n\nRun the command from the linked project root. If the folder is not linked, ask\nfor an editable Builder share link and run\n`webstudio init --link --json`, followed by `webstudio sync`, then\nretry `webstudio connect `. Treat the share link as a credential and do\nnot include it in committed files, logs, screenshots, or issue reports.\n\n`connect` verifies project access before changing client configuration. For\nClaude Code, Cursor, and VS Code it safely merges the `webstudio` server into\nthe client\'s project configuration. For Codex it runs both `codex mcp add` and\n`codex mcp get webstudio`; do not repeat those commands separately. Follow the\nreload, restart, or approval instruction printed by `connect`, then verify the\nloaded MCP connection by asking the client to use Webstudio MCP and list the\nproject pages. Use `--print` only to inspect the generated setup without\nchanging configuration or requiring project access.\n\n## Always\n\n1. webstudio permissions --json\n2. For bounded shell workflows, call MCP tools directly through the CLI shortcut, for example `webstudio meta.index` or `webstudio insert-fragment \'\' --dry-run`. The explicit form `webstudio mcp single-op-call \'\'` is equivalent and useful when you need to make the MCP boundary obvious. Use `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` for multiple calls in one shared CLI session. Use a normal JSON file path for large batches. Use long-running `webstudio mcp` only when your environment is a real MCP client. Do not manually send raw JSON-RPC to `webstudio mcp` from a shell or PTY.\n3. Read MCP `meta.index`, for example `webstudio meta.index`.\n4. Use focused MCP calls with concrete JSON: `webstudio meta.guide \'{"brief":"Create a design system page using every component"}\'`, `webstudio meta.get_more_tools \'{"tools":["insert-fragment"]}\'`, `webstudio components.list \'{"source":"all"}\'`, `webstudio components.coverage-plan`, `webstudio components.search \'{"brief":"radix select"}\'`, `webstudio components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'`, `webstudio templates.list`, and `webstudio templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'`.\n5. Read overview resources `webstudio://project/tools-overview` or `webstudio://project/components-overview` when useful. Read full resources `webstudio://project/tools` or `webstudio://project/components` only when focused tools are insufficient.\n6. Pick focused MCP read tool.\n7. Pick semantic MCP write tool.\n\nUse `webstudio schema mcp` for a compact MCP tool overview. Add `--verbose` only when exact input schemas for all tools are truly needed; otherwise prefer focused `meta.get_more_tools` and `components.*` calls.\n\nRun these commands from the linked project root. Use the MCP startup status line\'s absolute root for local files; write temporary scripts and artifacts under `/.temp`, not under a parent workspace.\n\nMonorepo quick path for a simple styled section:\n\n```sh\nnode packages/cli/local.js mcp single-op-call meta.index\nnode packages/cli/local.js mcp single-op-call meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nnode packages/cli/local.js mcp single-op-call insert-fragment \'{"parentInstanceId":"parent-id","fragment":"Launch KitA focused section created with Webstudio JSX.Get started"}\' --dry-run\n```\n\nThe same local shortcut form is shorter and preferred for simple shell steps:\n\n```sh\nnode packages/cli/local.js meta.index\nnode packages/cli/local.js meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nnode packages/cli/local.js insert-fragment \'{"parentInstanceId":"parent-id","fragment":"Launch KitA focused section created with Webstudio JSX.Get started"}\' --dry-run\n```\n\nFor this simple path, do not grep source files, dump full MCP resources, or write parser scripts first. Use `list-pages`, `get-page-by-path`, or `list-instances` only to get the target `parentInstanceId`.\n\nWhen authoring JSX for `insert-fragment`, use Webstudio component helpers and Webstudio style syntax. Use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n\nWhen the task says another user will edit a page in Content mode, use a Content Block (`ws:block`) around every editable region. Content-mode users can edit text and supported props only in descendants of that block; content outside it is read-only. Put reusable insertable options in the block\'s `ws:block-template` child. Do not put intended editor content inside that template container: templates are protected source material, while an inserted template copy becomes an editable direct child of the Content Block. Verify this structure before handoff.\n\nDo not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`. JSX fragments are declarative project data; use the built-in Webstudio helpers instead.\n\nUse Webstudio prop names in JSX: `class`, `for`, `aria-label`, and other HTML/Webstudio names. Do not use React-only aliases such as `className` or `htmlFor`; the runtime rejects them with the Webstudio prop name to use.\n\nUse Webstudio actions for event/action props. Do not pass JavaScript functions such as `onClick={() => ...}`; the runtime rejects them because functions cannot be persisted as Webstudio project data.\n\n```tsx\n\n Open\n\n```\n\nPlain JSX prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n\nIf a component has a registered template with required parts, JSX must include those parts explicitly under the same parent structure as the template, for example ``. Use `insert-component` when you want Webstudio to apply one component template automatically.\n\n## Animation Components\n\nBefore creating animation examples, inspect the exact components with focused discovery:\n\n```sh\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-animation:AnimateChildren"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-animation:AnimateText"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-animation:StaggerAnimation"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-animation:VideoAnimation"}\'\n```\n\nUse Animation Group (`AnimateChildren`) as the controller. Put normal instances directly inside it, or put Text Animation, Stagger Animation, or Video Animation directly inside it. Text, Stagger, and Video are helper components with `contentModel.category: "none"` and should not be used as standalone section roots.\n\nDefine timing and CSS changes on the Animation Group `action` prop. Use `type:"view"` for viewport entry/exit progress and `type:"scroll"` for scroll-progress timelines. For in animations, keep the canvas styles as the final state and use `fill:"backwards"` with keyframes that describe the starting state. For out animations, use `fill:"forwards"` with keyframes that describe the ending state.\n\nText Animation settings: `slidingWindow` defaults to `5`, `easing` defaults to `linear`, and `splitBy` defaults to `char`. Use `splitBy:"space"` for word-by-word animation. The parent Animation Group keyframes provide the actual opacity, translate, scale, or other styles.\n\nStagger Animation settings: `slidingWindow` defaults to `1` and `easing` defaults to `linear`. It applies parent Animation Group progress across its direct children. Use `slidingWindow:0` for instant sequential steps, `1` for one child at a time, and values above `1` for overlapping waves.\n\nVideo Animation settings: `timeline` is a boolean. Prefer `insert-component` for Video Animation so the Video child template is inserted, then configure the Video child asset/source. Use short, seek-friendly videos for smooth scroll-linked playback.\n\nUse JSX fragments for authored animation structures when you need styled, editable examples. Put the final visual state in `ws:style` and put the starting or ending animated state in the Animation Group `action` keyframes. Include an explicit `offset` on every keyframe: use `offset: 0` for starting-state keyframes with `fill:"backwards"` and `offset: 1` for ending-state keyframes with `fill:"forwards"`.\n\n```tsx\n\n \n Launch metrics\n \n A polished card that fades up as it enters the viewport.\n \n \n\n```\n\nFor Text Animation, keep `animation.AnimateText` as the direct child of Animation Group and place the text-containing element inside it:\n\n```tsx\n\n \n Animate words with controlled rhythm\n \n\n```\n\nFor Stagger Animation, put the repeated cards or rows directly inside `animation.StaggerAnimation`:\n\n```tsx\n\n \n \n Plan\n \n \n Build\n \n \n Launch\n \n \n\n```\n\nFor Video Animation, use the registered template via `insert-component` when possible. If you author JSX, include the Video child explicitly:\n\n```tsx\n\n \n <$.Video\n preload="auto"\n autoPlay={true}\n muted={true}\n playsInline={true}\n crossOrigin="anonymous"\n />\n \n\n```\n\n## Command Surface Boundary\n\n- Use top-level `webstudio ...` shell commands for setup, sync/import/build/preview/screenshot, permissions, publish/domains, schema, registry inspection, man, and starting MCP.\n- Use MCP tools for Builder project data manipulation: pages, instances/components, props, text, styles, tokens, variables, resources, assets, breakpoints, redirects, and raw patches.\n- From a shell, call MCP tools with the shortcut form `webstudio \'\'`, for example `webstudio insert-fragment \'\' --dry-run`. The explicit equivalent is `webstudio mcp single-op-call \'\'`. Use `--input-file` for large payloads.\n- Inside the Webstudio monorepo, call the local CLI as its own command: `node packages/cli/local.js ...`. Do not wrap the CLI call in `pwd && ...`, command substitution, `pnpm exec webstudio`, `pnpm --filter webstudio exec webstudio`, or a global `webstudio`.\n- For experiments, pass `--dry-run` to local-capable mutation calls. Read the computed transaction from `meta.session.transaction` and its base build version from `meta.session.version`. Copying a `.webstudio` folder is not an isolated project clone; `.webstudio/config.json` still points to the same remote project, so non-dry-run mutations can commit to that project.\n- For bounded multi-step shell work, run inline JSON with `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'`; this reuses one CLI session without raw JSON-RPC. For large batches, write `{ "calls": [{ "tool": "..." }] }` to a normal JSON file and run `webstudio mcp run .temp/mcp-calls.json`.\n- Use JSON strings for `brief` fields. Never pass boolean flags such as `{"brief":true}`.\n- Treat `webstudio mcp single-op-call` and `webstudio mcp run` stderr lines as progress checkpoints; stdout remains JSON on both success and failure. On failure, parse stdout for `{ "ok": false, "error": { "code": "...", "message": "..." } }` before deciding what to fix.\n- If a CLI/MCP tool crashes, hangs, gives a confusing error, needs an undocumented workaround, or forces source-code inspection for normal usage, ask the user to report it in Discord `#help` at https://wstd.us/community. Give them a complete copy-paste report with the goal, expected behavior, actual error, exact command/tool call, stdout JSON, stderr/lifecycle logs, environment, workaround, and secrets redacted.\n- Run one-shot `webstudio mcp single-op-call` commands sequentially against a linked `.webstudio` folder. If a command returns `PROJECT_SESSION_BUSY`, another CLI/MCP process is updating the local session; wait a moment and retry sequentially.\n- In delegated or non-streaming agent environments, do not batch many MCP calls silently and do not wrap many shortcut or `webstudio mcp single-op-call` commands in a shell loop. Treat each parent-visible checkpoint as the unit of work. If the parent asks for status within 30 seconds, run exactly one shortcut command such as `webstudio meta.index` or one explicit `webstudio mcp single-op-call` command, report that command/result, then wait for the parent to continue. Do not take a broad task such as creating a full design-system page as one execution unit. Call `workflow.next {"goal":"design-system-page"}`, report the returned phase/checkpoint, wait until the parent continues, call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`, complete exactly that bounded phase, and return: discovery, page creation, one dry-run JSX section, one committed JSX section, one `components.coverage-insert-next` call, or one presentation pass. Phase commands do not include nextPhase in their own output. After the parent continues, acknowledge the previous checkpoint first, then call `workflow.next` with the next phase. For all-component design-system pages, checkpoint after workflow planning, discovery, page creation, call `components.coverage-insert-next` once before checkpointing again, then finish with `workflow.next {"goal":"design-system-page","phase":"presentation-pass"}`. Coverage 72/72 is necessary but not sufficient: the page must be organized into styled, real-world examples, not raw unstyled component dumps.\n- For design-system or “use every component” tasks, start with compact `webstudio components.coverage-plan`, checkpoint, then request component coverage details with `webstudio components.coverage-plan \'{"detail":"roots","offset":20}\'` or `{"detail":"parts"}` only when needed. Do not pass `detail` to `list-pages`; use `list-pages {}` or `get-page-by-path` for page lookup.\n- MCP tool shortcuts are only for MCP tools. If a shortcut is ambiguous with a real top-level command, the real top-level command wins; use `webstudio mcp single-op-call \'\'` to force the MCP path.\n\n## LLM Implementation Process\n\nUse this process for user requests that change Webstudio content, layout, styles, assets, pages, redirects, resources, or publishing state:\n\n1. Discover capabilities with `webstudio man --json`, `webstudio schema api`, `webstudio schema mcp`, MCP `meta.index`, `meta.guide`, `meta.get_more_tools`, `components.list`, `components.summary`, `components.coverage-plan`, `components.search`, `components.get`, `templates.list`, and `templates.get`. From a shell, prefer shortcut calls such as `webstudio meta.index` and `webstudio components.search \'{"brief":"button"}\'` for these focused tool calls; use `webstudio mcp single-op-call` when you need the explicit MCP form. Read full resources such as `webstudio://project/tools` and `webstudio://project/components` only when needed. Do not write scripts to parse full MCP discovery JSON for normal lookup.\n2. Inspect current project state with semantic reads such as `get-project-settings`, `list-pages`, `get-page-by-path`, `list-instances`, `inspect-instance`, `get-styles`, `list-assets`, `list-breakpoints`, and `snapshot` only when needed. Before changing a project, read `get-project-settings` and follow any non-empty `meta.agentInstructions`. These are shared project instructions, not a place for secrets.\n3. Mutate the Webstudio project with semantic MCP write tools first. Prefer MCP `insert-fragment` for authored/styled sections, use `insert-component` only for one automatic component template, then `update-text`, `update-props`, `update-styles`, `upload-asset`, `create-page`, and page/project settings tools over raw patches.\n4. Use `apply-patch` only when no semantic tool covers the required change, and only after reading the latest snapshot/version.\n5. For visual/design work, regenerate or preview the generated app, capture a screenshot, inspect it with vision, and iterate before final response.\n6. Report what changed and what verification ran.\n\n## Visual Design Workflow\n\nFor requests involving visible HTML/CSS, layout, typography, colors, imagery, responsive behavior, or screenshots:\n\n1. Read editable Webstudio structure first: pages, instances, props, styles, breakpoints, assets, and relevant text.\n2. Do not use generated route/component files as the source of truth for editable content.\n3. Make edits through Webstudio semantic commands/MCP tools so the result stays editable in Builder and survives the next `webstudio build`.\n4. Keep generated project files current, start preview, and capture the changed page with `screenshot`.\n5. Use `screenshot.diff` when a baseline exists and inspect screenshot/diff artifacts with vision before finishing.\n6. If vision or screenshot tooling is unavailable, state that explicitly and explain what fallback verification was used.\n\n## Responsive Verification Workflow\n\nFor responsive page work, use Builder breakpoints as the source of truth:\n\n1. Read breakpoints with `list-breakpoints` before deciding responsive behavior.\n2. Apply responsive styles with existing Builder breakpoint ids; do not invent CSS media queries or breakpoint names when Webstudio breakpoint data exists.\n3. Pick screenshot viewport widths from the project breakpoints: include a desktop width, each defined max-width or min-width edge, and a narrow mobile width.\n4. Capture each viewport with `screenshot`, for example `{"path":"/","output":"home-375.png","viewport":{"width":375,"height":812}}` and `{"path":"/","output":"home-1440.png","viewport":{"width":1440,"height":900}}`.\n5. Inspect every viewport screenshot with vision before finishing, checking layout, overflow, hidden content, text wrapping, and breakpoint-specific style changes.\n6. If any viewport fails, update styles through semantic Webstudio tools and repeat screenshots for the affected breakpoints.\n\n## Generated Files Guardrails\n\n- Do not edit `app/__generated__`, generated route files, generated page files, generated CSS, or build output for normal Webstudio content/design requests.\n- Do not replace generated page components with handcrafted app code unless the user explicitly asks for code-only export customization.\n- Generated files are build artifacts and may be overwritten by `webstudio build`.\n- If a task truly requires generated app customization, keep it outside `app/__generated__` where possible and explain that it is not editable Webstudio content.\n\n## Values vs Bindings\n\nBefore authoring unfamiliar expressions, read `webstudio://project/expressions` with MCP `resources/read` or `webstudio mcp read-resource webstudio://project/expressions`. It documents the supported expression subset, method allowlist, scope, Collection context, and validation limits.\n\n- Use direct value tools for fixed content. For one visible text child, use `update-text` with plain `text`. For a bounded multi-instance literal replacement, use `replace-text` with `find`, `replace`, `pagePath` or `pageId`, and `limit`; it does not change expression children. Use `replace-prop-text` for bounded changes inside static string props, optionally limited to prop names or instance ids; it never changes dynamic bindings. For static props such as `aria-label`, `alt`, `id`, `class`, `href`, or button labels stored as props, use `update-props` with the prop\'s direct type/value.\n- Use `bind-props` only when the prop must stay dynamic: an expression, resource result, action, or existing scoped runtime context such as `system`. Do not use `bind-props` just to set a fixed string.\n- Direct prop string example: `{"updates":[{"instanceId":"button-id","name":"aria-label","type":"string","value":"Open menu"}]}`.\n- Expression binding example: `{"bindings":[{"instanceId":"link-id","name":"href","binding":{"type":"expression","value":"currentPost.url"}}]}`.\n- Page metadata fields such as `title`, `description`, `language`, `redirect`, and custom meta content accept plain fixed text. For computed values, pass JavaScript expression code such as `pageTitle ?? "Pricing | Acme"`.\n- Page `status` accepts a fixed HTTP status code as a number from 200 through 599, for example `302`. For a dynamic status, pass JavaScript expression code such as `system.status`.\n- Page metadata update example: use `update-page` with `{"pageId":"page-id","values":{"title":"Pricing | Acme","meta":{"description":"Plans for teams"}}}`.\n- Draft a page with `update-page` and `{"pageId":"page-id","values":{"isDraft":true}}`. It remains editable and previewable but is omitted from every publish target, including staging, and from sitemap output.\n- Stage a draft page for a future publish with `{"pageId":"page-id","values":{"isDraft":false}}`. This clears draft state but does not deploy the site. The home page and `/*` catch-all page cannot be drafts.\n- Resource `url` accepts plain fixed URLs and paths. For computed URLs, pass JavaScript expression code such as `"https://api.example.com/items?tag=" + filters.tag`. Resource header values, search parameter values, and text bodies accept expressions for dynamic values; for fixed text, use `{ "type": "literal", "value": "application/json" }`.\n- Resource update example: use `update-resource` with `{"resourceId":"resource-id","values":{"url":"https://api.example.com/items"}}`.\n- Data variable values support `string`, `number`, `boolean`, `string[]`, and `json`. Use `string[]` only for arrays where every item is a string; use `json` for objects, mixed arrays, filters, and nested data.\n- Parameters are internal scoped runtime values from pages, collections, or components. They are not a public authoring surface: do not create, update, or delete parameter records. Public tools should preserve existing parameter records and may reference documented context values such as `system` in expressions where they are already in scope.\n- Use scoped resources for read data. A GET resource created with `scopeInstanceId`/`dataSourceName` defaults to `exposeAsDataSource:true`, becomes a scoped resource data variable, is generated into the page resource `data` map, and may be loaded while rendering the page. Read the loaded resource result from its wrapper, usually `.data`.\n- Use prop-bound resources for actions. A resource created without `scopeInstanceId` and bound to a component prop such as Form `action` with `bind-props` and `binding.type: "resource"` becomes an action resource in the page resource `action` map. Use this for POST, PUT, DELETE, webhooks, GraphQL submissions, and anything that should run only from an explicit form/action flow.\n- POST, PUT, and DELETE resources default to `exposeAsDataSource:false`, even with a scope. Set `exposeAsDataSource:true` only for an intentional render-time read such as a GraphQL POST query; provide `scopeInstanceId` and inspect the returned warning. Set it to `false` during `update-resource` to detach existing render-time exposure.\n- For dynamic resource query parameters prefer `searchParams`, for example `{"name":"tag","value":"filters.tag"}`. Use `{"type":"literal","value":"website"}` for fixed request text. Header values can be expressions such as `"\\"Bearer \\" + auth.token"`. Body can be an object expression, including GraphQL payloads such as `{ query: "...", variables: { slug: system.params.slug } }`.\n- Resource methods are `get`, `post`, `put`, and `delete`. Optional resource controls are `graphql` and `system`. Use `control:"graphql"` for GraphQL POST resources with query bodies. Use `control:"system"` for built-in local resource URLs such as `"/$resources/current-date"` and for resources reading the built-in `system` parameter. The built-in system fields are `system.origin`, `system.pathname`, `system.params`, and `system.search`; do not use `system.path`.\n- Whenever an array or object from a resource or data variable should render repeated UI, call `insert-collection` with the complete iterable and one repeated-item JSX root. The command creates the Collection, private item parameters, iterable binding, and descendant item bindings atomically. Use `collectionItem` and `collectionItemKey` expressions in the item JSX. Wrap multiple repeated siblings in one Element, and give repeated Radix items stable unique `value` bindings.\n- Expressions are single JavaScript expressions, not statements or functions. Functions, arrow functions, classes, `new`, `this`, `await`, imports, arbitrary calls, increment/decrement, and assignment outside actions are unsupported. Prefer optional chaining, nullish coalescing, ternaries, property/index access, operators, and the documented string/array methods.\n\n## Pick Read Command\n\n{{readFirst}}\n\n## Pick Write Command\n\n{{taskRecipeIndex}}\n\n## Raw Patch Only If Needed\n\n1. Use MCP tool: snapshot.\n2. Write BuildPatchTransaction[].\n3. Use MCP tool: apply-patch.\n\n## MCP Argument Examples\n\nMCP tools receive JSON argument objects, not CLI flags. Use these shapes:\n\n{{mcpArgumentExampleIndex}}\n\n## Rules\n\n- Never guess ids for existing records. Read them first.\n- Never use project ids from user input. Commands use the configured project.\n- Use --refresh before a local-capable command when cached data may be stale.\n- Pass --json only to commands whose help/schema documents it. Do not add --json to top-level commands such as sync unless supported.\n- On VERSION_CONFLICT, read MCP snapshot again, regenerate the patch, then retry.\n- Treat stdout JSON as the API contract and stderr as diagnostics.\n- For visual/design work, verify the rendered result with vision before finishing.\n- Do not edit generated files for normal Webstudio content/design requests.\n- Use direct values for static strings and bindings only for dynamic expressions/resources/actions.\n- Use plain fixed text where documented. Only encode a quoted JavaScript string literal when a field is explicitly documented as an expression-only value.\n- Confirm destructive commands with --confirm only when user requested deletion/unpublish/replacement.\n- Use webstudio schema api for machine-readable top-level command metadata and webstudio schema mcp for MCP tool schemas.\n\n## Known Gaps\n\n{{knownCliGapIndex}}\n', "manual-mcp": - '# Webstudio MCP Manual\n\n`webstudio mcp` starts a stdio MCP server for real MCP clients. Shell users can call MCP tools with the shortcut form `webstudio \'\'`, for example `webstudio meta.index` or `webstudio insert-fragment \'\' --dry-run`. `webstudio mcp single-op-call` is the explicit equivalent and prints the structured JSON result. `webstudio mcp run` runs multiple MCP tool calls from inline JSON or a normal JSON file in one shared CLI session. Do not manually type or pipe raw JSON-RPC frames into `webstudio mcp` from an interactive shell or PTY.\n\n## Startup\n\nIf you are already working with an agent, ask it to connect the current folder\nto Webstudio. Give the editable Builder share link only when the trusted agent\nasks for it. The agent should perform the steps below, follow any\nclient-specific output from `webstudio connect`, and verify the connection by\nlisting the project pages. Treat the share link as a credential: do not include\nit in committed files, screenshots, logs, or issue reports.\n\n1. Configure a project with `webstudio init --link --json`.\n2. Synchronize it with `webstudio sync`.\n3. Generate the client configuration with `webstudio connect claude`,\n `webstudio connect codex`, `webstudio connect cursor`, or\n `webstudio connect vscode`. Use `--print` to preview the generated setup.\n4. Check capabilities with `webstudio permissions --json`.\n5. For shell-driven agents, use shortcut calls such as `webstudio meta.index` and `webstudio insert-fragment \'\' --dry-run` for individual MCP tool calls. Use the explicit equivalent `webstudio mcp single-op-call \'\'` when you need to force the MCP path, or `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` for bounded multi-call workflows. Use `webstudio mcp run .temp/mcp-calls.json` for large batches.\n6. For MCP clients, start the server with `webstudio mcp`.\n7. Start discovery with `meta.index`, then call focused tools with concrete JSON, for example `webstudio mcp single-op-call meta.guide \'{"brief":"Create a design system page using every component"}\'`.\n\nAfter linking and synchronization, run `webstudio connect `, reload or\nrestart the client, then ask the agent to use Webstudio MCP to list the project\npages. The supported client names are `claude`, `codex`, `cursor`, and `vscode`.\nFor Codex, `connect` registers and verifies the server through the Codex CLI;\nthe resulting server appears in Codex settings. Use `--print` to inspect the\nexact registration command without changing configuration or requiring project\naccess. Before changing client configuration, `connect` verifies that the saved\nproject endpoint is reachable and its credential is accepted. If verification\nfails, follow the specific connection, compatibility, or relinking guidance in\nthe error.\n\nStart MCP from the linked Webstudio project root. The lifecycle status line prints that absolute root; create local scripts, screenshots, and temporary artifacts under that root, for example `/.temp/script.mjs`. If the shell starts in a parent workspace, `cd` into the project root first or use absolute paths.\n\nWhen developing inside the Webstudio monorepo, start the local CLI exactly as `node packages/cli/local.js mcp` from the repo root. Do not use `pnpm exec webstudio`, `pnpm --filter webstudio exec webstudio`, or a global `webstudio`: they can resolve an older binary.\n\nWhile the server is running, stdout is reserved for MCP JSON-RPC messages. Do not print human text from the server process. The server advertises MCP `logging` capability and emits sparse `notifications/message` logs for ready state and tool lifecycle checkpoints such as `tool preview.start started`, `tool preview.start still running after 10000ms`, and `tool preview.start succeeded in 1234ms`; stderr also mirrors these sparse lifecycle fallback lines prefixed with `[webstudio mcp]`.\n\n## One-Shot Tool Calls\n\nUse the shortcut `webstudio \'\'` when you are operating from a shell and need one MCP tool result. The explicit form `webstudio mcp single-op-call \'\'` is equivalent and avoids writing temporary Node.js stdio client scripts.\n\nExamples:\n\n```sh\nwebstudio mcp single-op-call meta.index\nwebstudio mcp single-op-call meta.guide \'{"brief":"Create a design system page using every component"}\'\nwebstudio mcp single-op-call meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nwebstudio mcp single-op-call components.list \'{"source":"all"}\'\nwebstudio mcp single-op-call components.coverage-plan\nwebstudio mcp single-op-call components.search \'{"brief":"radix select"}\'\nwebstudio mcp single-op-call components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio mcp single-op-call templates.list\nwebstudio mcp single-op-call templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio mcp single-op-call insert-fragment --input-file .temp/insert-fragment.json\n```\n\nShortcut equivalents:\n\n```sh\nwebstudio meta.index\nwebstudio meta.guide \'{"brief":"Create a design system page using every component"}\'\nwebstudio meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nwebstudio components.list \'{"source":"all"}\'\nwebstudio components.coverage-plan\nwebstudio components.search \'{"brief":"radix select"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio templates.list\nwebstudio templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio insert-fragment --input-file .temp/insert-fragment.json\n```\n\nRules:\n\n- Inside the Webstudio monorepo, replace `webstudio` in the examples above with `node packages/cli/local.js`, for example `node packages/cli/local.js meta.index`.\n- For a simple authored/styled section, run `meta.index`, then `meta.get_more_tools \'{"tools":["insert-fragment"]}\'`, then `insert-fragment`. Do not grep source files, dump full MCP resources, or write parser scripts first.\n- In `insert-fragment` JSX, use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n- Prefer JSX for authored/styled content. Common `insert-fragment` inputs:\n\n```jsonl\n{"parentInstanceId":"root-id","fragment":"Northstar Product OSReusable patterns for teams."}\n{"parentInstanceId":"root-id","fragment":"Operations ConsoleReact-style object styles become editable Webstudio styles."}\n{"parentInstanceId":"root-id","fragment":"Track launch"}\n{"parentInstanceId":"root-id","fragment":""}\n```\n\n- Do not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`.\n- Use Webstudio prop names such as `class` and `for`; do not use React aliases `className` or `htmlFor`.\n- Use Webstudio actions for event/action props, for example `onClick={new ActionValue(["event"], expression\\`console.log(event)\\`)}`. Do not pass JavaScript functions such as `onClick={() => ...}`.\n- Plain prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n- Template-backed components used in JSX must include required child/part components explicitly under the same parent structure as the template, for example ``. Use `insert-component` when you want one automatic registered component template.\n- The positional input is JSON and defaults to `{}`.\n- Use `--input-file` for large mutation payloads.\n- Use `--dry-run` with local-capable mutation tools when you need a patch plan without committing. The computed transaction is returned in `meta.session.transaction`, and `meta.session.version` is its base build version. Copying a `.webstudio` folder is not an isolated project clone; `.webstudio/config.json` still points to the same remote project, so non-dry-run mutations can commit to that project.\n- The command prints JSON to stdout for both success and failure. Success uses the same `structuredContent` shape MCP tools return: `{ "ok": true, "data": ..., "meta": ... }`. Failure prints `{ "ok": false, "error": { "code": "...", "message": "..." }, "meta": ... }` and exits nonzero.\n- The command writes sparse progress to stderr, including start, success/failure, elapsed time, and committed status when the tool returns session metadata.\n- Invalid argument types fail loudly with path-specific messages, for example `meta.guide input.brief must be a string when provided`.\n- Run one-shot shortcut or `mcp single-op-call` commands sequentially against the same linked `.webstudio` folder. If you receive `PROJECT_SESSION_BUSY`, another CLI/MCP process is updating the local session; wait a moment and retry sequentially.\n- To work with another previously linked project without changing the directory\'s default link, start MCP or a shell call with `--project `, for example `webstudio mcp --project ` or `webstudio mcp single-op-call list-pages --project `. Selected projects use isolated local session and checkpoint files.\n- If you are a delegated agent and your parent cannot see live stderr/stdout, do not run a long sequence of shortcut or `mcp single-op-call` commands silently and do not wrap many calls in a shell loop. Treat each parent-visible checkpoint as the unit of work. If the parent asks for status within 30 seconds, run exactly one `webstudio ` or `webstudio mcp single-op-call` command, report that command/result, then wait before the next MCP command. For all-component design-system pages, checkpoint after discovery, checkpoint after page creation, call `components.coverage-insert-next` once before checkpointing again, then finish with the `presentation-pass` workflow phase. Coverage alone is not completion; organize examples into styled sections/cards.\n\n## Reporting CLI/MCP Issues\n\nIf a CLI/MCP tool gives a confusing error, crashes, hangs, produces invalid output, requires an undocumented workaround, or makes you inspect source code to understand normal usage, ask the user to report it in the Webstudio Discord `#help` channel: https://wstd.us/community.\n\nGive the user a complete copy-paste report. Include only non-secret values: never include auth tokens, private URLs, cookies, API keys, passwords, or proprietary project data. Redact them as ``.\n\nCopy-paste template:\n\n````md\nWebstudio CLI/MCP issue report\n\nWhat I was trying to do:\n\n\nWhat I expected:\n\n\nWhat happened instead:\n\n\nCommand/tool used:\n\n```sh\n\n```\n\nStructured output / error:\n\n```json\n\n```\n\nStderr / lifecycle logs:\n\n```txt\n\n```\n\nEnvironment:\n\n- CLI command path: \n- Webstudio CLI version: \n- OS: \n- Node version: \n- Project/session state: \n\nWorkaround tried:\n\n\nWhy this should be improved:\n\n````\n\n## Shared-Session Shell Runs\n\nUse `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` when you are operating from a shell and need several MCP tool calls to share one CLI session without hand-writing JSON-RPC. For large batches, pass a normal JSON file path such as `.temp/mcp-calls.json`. Do not use shell process substitution like `<(...)`; use inline JSON or a real file.\n\nUse `mcp run` for long-lived tools such as `preview.start`. A one-shot `mcp single-op-call preview.start` cannot keep ownership of a preview server for a later screenshot or stop call. Put `preview.start`, `screenshot`, and `preview.stop` in one shared `mcp run` process, or use a real long-running MCP client.\n\nInput shape:\n\n```json\n{\n "calls": [\n { "tool": "meta.index" },\n { "tool": "components.find", "input": { "brief": "radix select" } }\n ]\n}\n```\n\nRules:\n\n- The command prints JSON to stdout for both success and failure. It stops at the first failed call and prints partial results in `{ "ok": false, "error": ..., "data": { "completedCalls": ..., "results": [...] }, "meta": ... }`, then exits nonzero.\n- If a call returns `checkpoint.required`, read-only discovery and inspection remain available, but mutations and state-changing session tools return `CHECKPOINT_REQUIRED`. Stop and report the checkpoint to the parent/user. Only after the parent/user continues, call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}` before continuing mutations.\n- For `mcp single-op-call`, checkpoint requirements persist across later one-shot CLI processes until you call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`.\n- Use this instead of manually sending JSON-RPC frames to `webstudio mcp` from a shell.\n\n### Cross-project batches\n\nAdd `projects` to the same `mcp run` manifest to run focused reads, audits, or dry runs across independently linked project roots:\n\n```json\n{\n "concurrency": 2,\n "calls": [\n { "tool": "status" },\n { "tool": "audit", "input": {} },\n {\n "tool": "update-project-settings",\n "input": { "meta": { "siteName": "Reviewed" } },\n "dryRun": true\n }\n ],\n "projects": [\n { "id": "site-a", "root": "../site-a" },\n { "id": "site-b", "root": "../site-b" }\n ]\n}\n```\n\nProject roots and an optional `progressFile` are resolved relative to the manifest file. Each project may provide its own `calls` instead of using the top-level calls. Each root must already be linked with its own `.webstudio/config.json`; the runner creates an independently authenticated ProjectSession and uses root-scoped session, audit, preview-data, and checkpoint paths without changing the process working directory.\n\nConcurrency defaults to 2, is capped at 16, and can be set in the manifest or overridden with `--concurrency`. A failure is reported for that project while other projects continue. Progress is saved after every successful call; rerunning with the default `--resume` skips completed projects and starts failed projects after their last confirmed successful call. Reads and dry runs may be retried. A committed mutation interrupted after dispatch is marked `AMBIGUOUS_MUTATION_RESULT` and is never replayed automatically; inspect that project before deciding how to continue. Use `--no-resume` only to intentionally start the complete manifest over.\n\nCommitted mutation tools are rejected in a projects batch unless the command includes `--approve-mutations`. Review the complete manifest before granting approval. `--dry-run` applies to every call and does not require mutation approval. The final stdout object is compact: project counts, one status/error record per project, elapsed time, and the progress-file path rather than every tool result.\n\n## Discovery\n\nUse MCP itself after startup, or call the same tools with `webstudio mcp single-op-call`:\n\n- `tools/list`: machine-readable available tools\n- `resources/list`: available overview and full JSON resources\n- `meta.index`: concise capability catalog\n- `meta.guide`: workflow for a user goal; call with a string brief such as `{"brief":"Create a pricing page"}`\n- `meta.get_more_tools`: detailed params, examples, namespaces, and local/server behavior; prefer exact names such as `{"tools":["insert-fragment"]}` when you know them\n- `components.list`: compact registry metadata for visible components and templates; use a focused get tool for complete details\n- `components.summary`: component counts by default; use `{"detail":"components","limit":20}` for paginated entries\n- `components.coverage-plan`: compact paged plan for design-system coverage tasks that need every component; default returns counts plus the first root page, use `{"detail":"roots"}`, `{"detail":"parts"}`, or `{"detail":"full"}` for more\n- `components.coverage-status`: page-specific covered/missing component report with `missingRoots` and `missingParts`\n- `components.search`: focused component/template search by id, namespace, label, category, or content model\n- `components.find`: compatibility alias for focused component search\n- `components.get`: full metadata for one component id\n- `templates.list`: compact metadata for template-backed insertions only\n- `templates.get`: full registry item and payload metadata for one template\n\nComponent and template registry items use a shadcn-compatible top-level shape plus Webstudio-specific superset metadata in `meta`. Use `meta.runtime` for component ids, props, states, content model, and source identity; `meta.authoring` for composition and accessibility guidance; and `meta.builder` for template insertion details and expected project-data namespaces. These items are for Builder/MCP discovery and are not a published shadcn install registry yet.\n\nPrefer the focused `components.*` tools over dumping `webstudio://project/components`. Do not write local scripts to parse full MCP discovery JSON for common component lookup.\nFor “use every component” or design-system pages, start with compact `components.coverage-plan`, checkpoint, then page through roots/parts instead of dumping the full catalog.\n\n## Consumer Capabilities\n\nMCP lets agents work on one configured Webstudio project at a time. In consumer\nterms, agents can:\n\n- Check which project they are connected to.\n- Check what the share link is allowed to do.\n- Inspect project metadata and the latest editable build.\n- Read selected project data for audits and repair.\n- Apply precise project changes against a known version.\n- List, inspect, create, update, delete, duplicate, copy, and reorder pages.\n- Set the home page.\n- Preserve old page paths for redirects or history.\n- Read and update page titles, descriptions, metadata, auth settings, and SEO fields.\n- List, create, update, duplicate, move, and delete page folders.\n- List, create, update, delete, duplicate, reorder, and reuse page templates.\n- Create pages from reusable templates.\n- Read and update project site settings.\n- Read and update marketplace product metadata.\n- List, create, update, delete, and replace redirects.\n- List, create, update, and delete responsive breakpoints.\n- List and inspect page elements.\n- Insert registered components.\n- Insert styled JSX fragments.\n- Move, reparent, clone, duplicate, wrap, unwrap, convert, rename, retag, and delete elements.\n- Fill grid cells.\n- List and update text children.\n- Update plain text and expression text.\n- Update structured rich text.\n- Add, update, delete, and bind element props.\n- Bind props to expressions, resources, actions, and runtime system values.\n- Read, add, update, delete, and replace local styles.\n- Update selected style-source styles.\n- List, create, update, attach, detach, extract, duplicate, rename, lock, unlock, reorder, clear, and delete design tokens and style sources.\n- List, define, rename, delete, and rewrite CSS variables.\n- List, create, update, and delete static data variables.\n- Create string, number, boolean, string list, and JSON variables.\n- Delete unused data variables.\n- List, create, update, upsert, bind, and delete resources.\n- Create HTTP resources.\n- Create GraphQL resources.\n- Create system resources.\n- Use built-in system resources for sitemap, current date, and assets.\n- Filter the Assets system resource by `filename` or `extension`, opt into bounded text content with `include=content`, and bind its metadata or parsed JSON content.\n- List and inspect complete asset metadata; upload, download, update, move, duplicate, find usage for, replace, and delete assets.\n- List, create, rename, move, recursively duplicate, and recursively delete nested asset folders.\n- Publish to staging or production.\n- Publish to selected domains.\n- List publish builds.\n- Check publish job status.\n- Unpublish staging or production deployments.\n- List, create, update, delete, and verify custom domains.\n- Start and stop preview.\n- Capture screenshots of generated pages.\n- Compare screenshots against baselines.\n- Install OCR support for richer visual checks.\n\nUseful resources:\n\n- `webstudio://project/status`: compact current ProjectSession status\n- `webstudio://project/tools-overview`: small operation overview by capability area\n- `webstudio://project/components-overview`: small component overview with ids, labels, namespaces, and categories\n- `webstudio://project/tools`: full operation catalog; read only when focused metadata is insufficient\n- `webstudio://project/components`: full component catalog with props, states, and content model composition constraints; read only when `components.summary`, `components.find`, and `components.get` are insufficient\n- `webstudio://project/guide`: concise discovery guide\n- `webstudio://project/expressions`: expression syntax, scope, supported methods, bindings, Collection iteration context, and verification\n- `webstudio://project/accessibility-review`: evidence-based LLM accessibility-review workflow using project checks, preview, and screenshots\n\n## MCP SDK Client Imports\n\nWhen writing a local Node.js MCP client script, use the official MCP SDK package and these exact ESM imports:\n\nInside the Webstudio monorepo this package is available at the repo root. In another project, install it first with `pnpm add -D @modelcontextprotocol/sdk`.\n\n```js\nimport { Client } from "@modelcontextprotocol/sdk/client/index.js";\nimport { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";\nimport { LoggingMessageNotificationSchema } from "@modelcontextprotocol/sdk/types.js";\n```\n\nMinimal stdio client for the local Webstudio CLI:\n\n```js\nconst client = new Client({ name: "webstudio-agent", version: "1.0.0" });\n\nclient.setNotificationHandler(\n LoggingMessageNotificationSchema,\n (notification) => {\n console.error(`[mcp] ${notification.params.data}`);\n }\n);\n\nconst transport = new StdioClientTransport({\n command: "node",\n args: ["packages/cli/local.js", "mcp"],\n cwd: process.cwd(),\n stderr: "inherit",\n});\n\nawait client.connect(transport);\n\nconst index = await client.callTool({\n name: "meta.index",\n arguments: {},\n});\nconsole.log(JSON.stringify(index.structuredContent, null, 2));\n\nawait client.close();\n```\n\nUse `node packages/cli/local.js mcp` from the Webstudio monorepo root for local development, or `webstudio mcp` from a linked project where the CLI is installed. Keep stdout for JSON-RPC/structured results and surface MCP logging notifications or stderr lifecycle lines as progress.\n\n## Core Rules\n\n- stdout is reserved for MCP JSON-RPC while the server is running.\n- Operate on the configured project only.\n- Read ids before writing.\n- Prefer semantic tools over `apply-patch`.\n- Use `status` and `refresh` when cached namespaces may be stale. Pass `status {"verbose":true}` only when debugging full namespace arrays, freshness, compatibility, or diagnostic details.\n- A mutation is durable only when `meta.session.committed` is true.\n- For visual/design work, verify the rendered result with vision before finishing.\n\n## Vision Verification Loop\n\nVision-capable AI can use MCP to see what it is building:\n\n{{mcpVisionVerificationLoopMarkdown}}\n\nGenerated app setup:\n\n{{mcpGeneratedAppDependencyNotes}}\n\n## MCP Argument Examples\n\nMCP tools receive JSON argument objects:\n\n{{mcpArgumentExampleIndex}}\n\n## Screenshot Verification\n\n{{screenshotVerificationSummary}}\n', + '# Webstudio MCP Manual\n\n`webstudio mcp` starts a stdio MCP server for real MCP clients. Shell users can call MCP tools with the shortcut form `webstudio \'\'`, for example `webstudio meta.index` or `webstudio insert-fragment \'\' --dry-run`. `webstudio mcp single-op-call` is the explicit equivalent and prints the structured JSON result. `webstudio mcp run` runs multiple MCP tool calls from inline JSON or a normal JSON file in one shared CLI session. Do not manually type or pipe raw JSON-RPC frames into `webstudio mcp` from an interactive shell or PTY.\n\n## Startup\n\nIf you are already working with an agent, ask it to connect the current folder\nto Webstudio. Give the editable Builder share link only when the trusted agent\nasks for it. The agent should perform the steps below, follow any\nclient-specific output from `webstudio connect`, and verify the connection by\nlisting the project pages. Treat the share link as a credential: do not include\nit in committed files, screenshots, logs, or issue reports.\n\n1. Configure a project with `webstudio init --link --json`.\n2. Synchronize it with `webstudio sync`.\n3. Generate the client configuration with `webstudio connect claude`,\n `webstudio connect codex`, `webstudio connect cursor`, or\n `webstudio connect vscode`. Use `--print` to preview the generated setup.\n4. Check capabilities with `webstudio permissions --json`.\n5. For shell-driven agents, use shortcut calls such as `webstudio meta.index` and `webstudio insert-fragment \'\' --dry-run` for individual MCP tool calls. Use the explicit equivalent `webstudio mcp single-op-call \'\'` when you need to force the MCP path, or `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` for bounded multi-call workflows. Use `webstudio mcp run .temp/mcp-calls.json` for large batches.\n6. For MCP clients, start the server with `webstudio mcp`.\n7. Start discovery with `meta.index`, then call focused tools with concrete JSON, for example `webstudio mcp single-op-call meta.guide \'{"brief":"Create a design system page using every component"}\'`.\n\nAfter linking and synchronization, run `webstudio connect `, reload or\nrestart the client, then ask the agent to use Webstudio MCP to list the project\npages. The supported client names are `claude`, `codex`, `cursor`, and `vscode`.\nFor Codex, `connect` registers and verifies the server through the Codex CLI;\nthe resulting server appears in Codex settings. Use `--print` to inspect the\nexact registration command without changing configuration or requiring project\naccess. Before changing client configuration, `connect` verifies that the saved\nproject endpoint is reachable and its credential is accepted. If verification\nfails, follow the specific connection, compatibility, or relinking guidance in\nthe error.\n\nStart MCP from the linked Webstudio project root. The lifecycle status line prints that absolute root; create local scripts, screenshots, and temporary artifacts under that root, for example `/.temp/script.mjs`. If the shell starts in a parent workspace, `cd` into the project root first or use absolute paths.\n\nWhen developing inside the Webstudio monorepo, start the local CLI exactly as `node packages/cli/local.js mcp` from the repo root. Do not use `pnpm exec webstudio`, `pnpm --filter webstudio exec webstudio`, or a global `webstudio`: they can resolve an older binary.\n\nWhile the server is running, stdout is reserved for MCP JSON-RPC messages. Do not print human text from the server process. The server advertises MCP `logging` capability and emits sparse `notifications/message` logs for ready state and tool lifecycle checkpoints such as `tool preview.start started`, `tool preview.start still running after 10000ms`, and `tool preview.start succeeded in 1234ms`; stderr also mirrors these sparse lifecycle fallback lines prefixed with `[webstudio mcp]`.\n\n## One-Shot Tool Calls\n\nUse the shortcut `webstudio \'\'` when you are operating from a shell and need one MCP tool result. The explicit form `webstudio mcp single-op-call \'\'` is equivalent and avoids writing temporary Node.js stdio client scripts.\n\nExamples:\n\n```sh\nwebstudio mcp single-op-call meta.index\nwebstudio mcp single-op-call meta.guide \'{"brief":"Create a design system page using every component"}\'\nwebstudio mcp single-op-call meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nwebstudio mcp single-op-call components.list \'{"source":"all"}\'\nwebstudio mcp single-op-call components.coverage-plan\nwebstudio mcp single-op-call components.search \'{"brief":"radix select"}\'\nwebstudio mcp single-op-call components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio mcp single-op-call templates.list\nwebstudio mcp single-op-call templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio mcp single-op-call insert-fragment --input-file .temp/insert-fragment.json\n```\n\nShortcut equivalents:\n\n```sh\nwebstudio meta.index\nwebstudio meta.guide \'{"brief":"Create a design system page using every component"}\'\nwebstudio meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nwebstudio components.list \'{"source":"all"}\'\nwebstudio components.coverage-plan\nwebstudio components.search \'{"brief":"radix select"}\'\nwebstudio components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio templates.list\nwebstudio templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'\nwebstudio insert-fragment --input-file .temp/insert-fragment.json\n```\n\nRules:\n\n- Inside the Webstudio monorepo, replace `webstudio` in the examples above with `node packages/cli/local.js`, for example `node packages/cli/local.js meta.index`.\n- For a simple authored/styled section, run `meta.index`, then `meta.get_more_tools \'{"tools":["insert-fragment"]}\'`, then `insert-fragment`. Do not grep source files, dump full MCP resources, or write parser scripts first.\n- In `insert-fragment` JSX, use `ws:style={css\\`...\\`}`for Webstudio-native CSS, or use React-style object syntax such as`style={{ padding: 24 }}` when that is simpler. Both forms create editable Webstudio style data.\n- Prefer JSX for authored/styled content. Common `insert-fragment` inputs:\n\n```jsonl\n{"parentInstanceId":"root-id","fragment":"Northstar Product OSReusable patterns for teams."}\n{"parentInstanceId":"root-id","fragment":"Operations ConsoleReact-style object styles become editable Webstudio styles."}\n{"parentInstanceId":"root-id","fragment":"Track launch"}\n{"parentInstanceId":"root-id","fragment":""}\n```\n\n- Do not access host globals or dynamic code APIs in JSX fragments, including `process`, `globalThis`, `eval`, `Function`, or `constructor`.\n- Use Webstudio prop names such as `class` and `for`; do not use React aliases `className` or `htmlFor`.\n- Use Webstudio actions for event/action props, for example `onClick={new ActionValue(["event"], expression\\`console.log(event)\\`)}`. Do not pass JavaScript functions such as `onClick={() => ...}`.\n- Plain prop values must be JSON-compatible: `null`, strings, booleans, finite numbers, arrays, and plain objects. Do not pass `undefined`, `Symbol`, `BigInt`, `NaN`, `Infinity`, `Date`, `Map`, `Set`, class instances, or circular objects; omit the prop, use plain data, or use `expression`/`ActionValue` when the value is dynamic.\n- Template-backed components used in JSX must include required child/part components explicitly under the same parent structure as the template, for example ``. Use `insert-component` when you want one automatic registered component template.\n- The positional input is JSON and defaults to `{}`.\n- Use `--input-file` for large mutation payloads.\n- Use `--dry-run` with local-capable mutation tools when you need a patch plan without committing. The computed transaction is returned in `meta.session.transaction`, and `meta.session.version` is its base build version. Copying a `.webstudio` folder is not an isolated project clone; `.webstudio/config.json` still points to the same remote project, so non-dry-run mutations can commit to that project.\n- The command prints JSON to stdout for both success and failure. Success uses the same `structuredContent` shape MCP tools return: `{ "ok": true, "data": ..., "meta": ... }`. Failure prints `{ "ok": false, "error": { "code": "...", "message": "..." }, "meta": ... }` and exits nonzero.\n- The command writes sparse progress to stderr, including start, success/failure, elapsed time, and committed status when the tool returns session metadata.\n- Invalid argument types fail loudly with path-specific messages, for example `meta.guide input.brief must be a string when provided`.\n- Run one-shot shortcut or `mcp single-op-call` commands sequentially against the same linked `.webstudio` folder. If you receive `PROJECT_SESSION_BUSY`, another CLI/MCP process is updating the local session; wait a moment and retry sequentially.\n- To work with another previously linked project without changing the directory\'s default link, start MCP or a shell call with `--project `, for example `webstudio mcp --project ` or `webstudio mcp single-op-call list-pages --project `. Selected projects use isolated local session and checkpoint files.\n- If you are a delegated agent and your parent cannot see live stderr/stdout, do not run a long sequence of shortcut or `mcp single-op-call` commands silently and do not wrap many calls in a shell loop. Treat each parent-visible checkpoint as the unit of work. If the parent asks for status within 30 seconds, run exactly one `webstudio ` or `webstudio mcp single-op-call` command, report that command/result, then wait before the next MCP command. For all-component design-system pages, checkpoint after discovery, checkpoint after page creation, call `components.coverage-insert-next` once before checkpointing again, then finish with the `presentation-pass` workflow phase. Coverage alone is not completion; organize examples into styled sections/cards.\n\n## Reporting CLI/MCP Issues\n\nIf a CLI/MCP tool gives a confusing error, crashes, hangs, produces invalid output, requires an undocumented workaround, or makes you inspect source code to understand normal usage, ask the user to report it in the Webstudio Discord `#help` channel: https://wstd.us/community.\n\nGive the user a complete copy-paste report. Include only non-secret values: never include auth tokens, private URLs, cookies, API keys, passwords, or proprietary project data. Redact them as ``.\n\nCopy-paste template:\n\n````md\nWebstudio CLI/MCP issue report\n\nWhat I was trying to do:\n\n\nWhat I expected:\n\n\nWhat happened instead:\n\n\nCommand/tool used:\n\n```sh\n\n```\n\nStructured output / error:\n\n```json\n\n```\n\nStderr / lifecycle logs:\n\n```txt\n\n```\n\nEnvironment:\n\n- CLI command path: \n- Webstudio CLI version: \n- OS: \n- Node version: \n- Project/session state: \n\nWorkaround tried:\n\n\nWhy this should be improved:\n\n````\n\n## Shared-Session Shell Runs\n\nUse `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` when you are operating from a shell and need several MCP tool calls to share one CLI session without hand-writing JSON-RPC. For large batches, pass a normal JSON file path such as `.temp/mcp-calls.json`. Do not use shell process substitution like `<(...)`; use inline JSON or a real file.\n\nUse `mcp run` for long-lived tools such as `preview.start`. A one-shot `mcp single-op-call preview.start` cannot keep ownership of a preview server for a later screenshot or stop call. Put `preview.start`, `screenshot`, and `preview.stop` in one shared `mcp run` process, or use a real long-running MCP client.\n\nInput shape:\n\n```json\n{\n "calls": [\n { "tool": "meta.index" },\n { "tool": "components.find", "input": { "brief": "radix select" } }\n ]\n}\n```\n\nRules:\n\n- The command prints JSON to stdout for both success and failure. It stops at the first failed call and prints partial results in `{ "ok": false, "error": ..., "data": { "completedCalls": ..., "results": [...] }, "meta": ... }`, then exits nonzero.\n- If a call returns `checkpoint.required`, read-only discovery and inspection remain available, but mutations and state-changing session tools return `CHECKPOINT_REQUIRED`. Stop and report the checkpoint to the parent/user. Only after the parent/user continues, call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}` before continuing mutations.\n- For `mcp single-op-call`, checkpoint requirements persist across later one-shot CLI processes until you call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`.\n- Use this instead of manually sending JSON-RPC frames to `webstudio mcp` from a shell.\n\n### Cross-project batches\n\nAdd `projects` to the same `mcp run` manifest to run focused reads, audits, or dry runs across independently linked project roots:\n\n```json\n{\n "concurrency": 2,\n "calls": [\n { "tool": "status" },\n { "tool": "audit", "input": {} },\n {\n "tool": "update-project-settings",\n "input": { "meta": { "siteName": "Reviewed" } },\n "dryRun": true\n }\n ],\n "projects": [\n { "id": "site-a", "root": "../site-a" },\n { "id": "site-b", "root": "../site-b" }\n ]\n}\n```\n\nProject roots and an optional `progressFile` are resolved relative to the manifest file. Each project may provide its own `calls` instead of using the top-level calls. Each root must already be linked with its own `.webstudio/config.json`; the runner creates an independently authenticated ProjectSession and uses root-scoped session, audit, preview-data, and checkpoint paths without changing the process working directory.\n\nConcurrency defaults to 2, is capped at 16, and can be set in the manifest or overridden with `--concurrency`. A failure is reported for that project while other projects continue. Progress is saved after every successful call; rerunning with the default `--resume` skips completed projects and starts failed projects after their last confirmed successful call. Reads and dry runs may be retried. A committed mutation interrupted after dispatch is marked `AMBIGUOUS_MUTATION_RESULT` and is never replayed automatically; inspect that project before deciding how to continue. Use `--no-resume` only to intentionally start the complete manifest over.\n\nCommitted mutation tools are rejected in a projects batch unless the command includes `--approve-mutations`. Review the complete manifest before granting approval. `--dry-run` applies to every call and does not require mutation approval. The final stdout object is compact: project counts, one status/error record per project, elapsed time, and the progress-file path rather than every tool result.\n\n## Discovery\n\nUse MCP itself after startup, or call the same tools with `webstudio mcp single-op-call`:\n\n- `tools/list`: machine-readable available tools\n- `resources/list`: available overview and full JSON resources\n- `meta.index`: concise capability catalog\n- `meta.guide`: workflow for a user goal; call with a string brief such as `{"brief":"Create a pricing page"}`\n- `meta.get_more_tools`: detailed params, examples, namespaces, and local/server behavior; prefer exact names such as `{"tools":["insert-fragment"]}` when you know them\n- `components.list`: compact registry metadata for visible components and templates; use a focused get tool for complete details\n- `components.summary`: component counts by default; use `{"detail":"components","limit":20}` for paginated entries\n- `components.coverage-plan`: compact paged plan for design-system coverage tasks that need every component; default returns counts plus the first root page, use `{"detail":"roots"}`, `{"detail":"parts"}`, or `{"detail":"full"}` for more\n- `components.coverage-status`: page-specific covered/missing component report with `missingRoots` and `missingParts`\n- `components.search`: focused component/template search by id, namespace, label, category, or content model\n- `components.find`: compatibility alias for focused component search\n- `components.get`: full metadata for one component id\n- `templates.list`: compact metadata for template-backed insertions only\n- `templates.get`: full registry item and payload metadata for one template\n\nComponent and template registry items use a shadcn-compatible top-level shape plus Webstudio-specific superset metadata in `meta`. Use `meta.runtime` for component ids, props, states, content model, and source identity; `meta.authoring` for composition and accessibility guidance; and `meta.builder` for template insertion details and expected project-data namespaces. These items are for Builder/MCP discovery and are not a published shadcn install registry yet.\n\nPrefer the focused `components.*` tools over dumping `webstudio://project/components`. Do not write local scripts to parse full MCP discovery JSON for common component lookup.\nFor “use every component” or design-system pages, start with compact `components.coverage-plan`, checkpoint, then page through roots/parts instead of dumping the full catalog.\n\n## Consumer Capabilities\n\nMCP lets agents work on one configured Webstudio project at a time. In consumer\nterms, agents can:\n\n- Check which project they are connected to.\n- Check what the share link is allowed to do.\n- Inspect project metadata and the latest editable build.\n- Read selected project data for audits and repair.\n- Apply precise project changes against a known version.\n- List, inspect, create, update, delete, duplicate, copy, and reorder pages.\n- Set the home page.\n- Preserve old page paths for redirects or history.\n- Read and update page titles, descriptions, metadata, auth settings, and SEO fields.\n- List, create, update, duplicate, move, and delete page folders.\n- List, create, update, delete, duplicate, reorder, and reuse page templates.\n- Create pages from reusable templates.\n- Read and update project site settings.\n- Read and update marketplace product metadata.\n- List, create, update, delete, and replace redirects.\n- List, create, update, and delete responsive breakpoints.\n- List and inspect page elements.\n- Insert registered components.\n- Insert styled JSX fragments.\n- Move, reparent, clone, duplicate, wrap, unwrap, convert, rename, retag, and delete elements.\n- Fill grid cells.\n- List and update text children.\n- Update plain text and expression text.\n- Update structured rich text.\n- Add, update, delete, and bind element props.\n- Bind props to expressions, resources, actions, and runtime system values.\n- Read, add, update, delete, and replace local styles.\n- Update selected style-source styles.\n- List, create, update, attach, detach, extract, duplicate, rename, lock, unlock, reorder, clear, and delete design tokens and style sources.\n- List, define, rename, delete, and rewrite CSS variables.\n- List, create, update, and delete static data variables.\n- Create string, number, boolean, string list, and JSON variables.\n- Delete unused data variables.\n- List, create, update, upsert, bind, and delete resources.\n- Create HTTP resources.\n- Create GraphQL resources.\n- Create system resources.\n- Use built-in system resources for sitemap, current date, and assets.\n- List and inspect complete asset metadata; upload, download, update, move, duplicate, find usage for, replace, and delete assets.\n- List, create, rename, move, recursively duplicate, and recursively delete nested asset folders.\n- Publish to staging or production.\n- Publish to selected domains.\n- List publish builds.\n- Check publish job status.\n- Unpublish staging or production deployments.\n- List, create, update, delete, and verify custom domains.\n- Start and stop preview.\n- Capture screenshots of generated pages.\n- Compare screenshots against baselines.\n- Install OCR support for richer visual checks.\n\nUseful resources:\n\n- `webstudio://project/status`: compact current ProjectSession status\n- `webstudio://project/tools-overview`: small operation overview by capability area\n- `webstudio://project/components-overview`: small component overview with ids, labels, namespaces, and categories\n- `webstudio://project/tools`: full operation catalog; read only when focused metadata is insufficient\n- `webstudio://project/components`: full component catalog with props, states, and content model composition constraints; read only when `components.summary`, `components.find`, and `components.get` are insufficient\n- `webstudio://project/guide`: concise discovery guide\n- `webstudio://project/expressions`: expression syntax, scope, supported methods, bindings, Collection iteration context, and verification\n- `webstudio://project/accessibility-review`: evidence-based LLM accessibility-review workflow using project checks, preview, and screenshots\n\n## MCP SDK Client Imports\n\nWhen writing a local Node.js MCP client script, use the official MCP SDK package and these exact ESM imports:\n\nInside the Webstudio monorepo this package is available at the repo root. In another project, install it first with `pnpm add -D @modelcontextprotocol/sdk`.\n\n```js\nimport { Client } from "@modelcontextprotocol/sdk/client/index.js";\nimport { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";\nimport { LoggingMessageNotificationSchema } from "@modelcontextprotocol/sdk/types.js";\n```\n\nMinimal stdio client for the local Webstudio CLI:\n\n```js\nconst client = new Client({ name: "webstudio-agent", version: "1.0.0" });\n\nclient.setNotificationHandler(\n LoggingMessageNotificationSchema,\n (notification) => {\n console.error(`[mcp] ${notification.params.data}`);\n }\n);\n\nconst transport = new StdioClientTransport({\n command: "node",\n args: ["packages/cli/local.js", "mcp"],\n cwd: process.cwd(),\n stderr: "inherit",\n});\n\nawait client.connect(transport);\n\nconst index = await client.callTool({\n name: "meta.index",\n arguments: {},\n});\nconsole.log(JSON.stringify(index.structuredContent, null, 2));\n\nawait client.close();\n```\n\nUse `node packages/cli/local.js mcp` from the Webstudio monorepo root for local development, or `webstudio mcp` from a linked project where the CLI is installed. Keep stdout for JSON-RPC/structured results and surface MCP logging notifications or stderr lifecycle lines as progress.\n\n## Core Rules\n\n- stdout is reserved for MCP JSON-RPC while the server is running.\n- Operate on the configured project only.\n- Read ids before writing.\n- Prefer semantic tools over `apply-patch`.\n- Use `status` and `refresh` when cached namespaces may be stale. Pass `status {"verbose":true}` only when debugging full namespace arrays, freshness, compatibility, or diagnostic details.\n- A mutation is durable only when `meta.session.committed` is true.\n- For visual/design work, verify the rendered result with vision before finishing.\n\n## Vision Verification Loop\n\nVision-capable AI can use MCP to see what it is building:\n\n{{mcpVisionVerificationLoopMarkdown}}\n\nGenerated app setup:\n\n{{mcpGeneratedAppDependencyNotes}}\n\n## MCP Argument Examples\n\nMCP tools receive JSON argument objects:\n\n{{mcpArgumentExampleIndex}}\n\n## Screenshot Verification\n\n{{screenshotVerificationSummary}}\n', "mcp-startup-epilogue": 'If you are inside the Webstudio monorepo, use the local CLI exactly as `node packages/cli/local.js ...` as your first command path. Do not use `packages/cli/bin.js` for local source-tree work; it is the packaged build entry and may use stale built output.\n\nPlain `webstudio mcp` starts the stdio MCP server for real MCP clients. Do not manually type or pipe raw JSON-RPC frames into it from an interactive shell or PTY. From a shell, use shortcut calls such as `webstudio meta.index` and `webstudio insert-fragment \'\' --dry-run` for one bounded tool call. The explicit equivalent is `webstudio mcp single-op-call \'\'`. Use `webstudio mcp run \'[{"tool":"components.find","input":{"brief":"button"}}]\'` for small multi-call batches in one shared CLI session. For large batches, pass a normal JSON file path such as `.temp/mcp-calls.json`. Do not use shell process substitution like `<(...)`; use inline JSON or a real file.\n\nPut each local CLI call in its own shell command; do not chain helper commands with `&&`, `;`, command substitution, or shell wrappers around the CLI when reporting a CLI step. Do not use `pnpm exec webstudio`, `pnpm --filter webstudio exec webstudio`, or a global `webstudio`; those can resolve an older binary. For example:\n\n```sh\nnode packages/cli/local.js mcp single-op-call meta.index\nnode packages/cli/local.js mcp single-op-call meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nnode packages/cli/local.js mcp single-op-call insert-fragment \'{"parentInstanceId":"parent-id","fragment":"Launch KitA focused section created with Webstudio JSX.Get started"}\' --dry-run\n```\n\nThe shorter local shortcut form is preferred for simple shell steps:\n\n```sh\nnode packages/cli/local.js meta.index\nnode packages/cli/local.js meta.get_more_tools \'{"tools":["insert-fragment"]}\'\nnode packages/cli/local.js insert-fragment \'{"parentInstanceId":"parent-id","fragment":"Launch KitA focused section created with Webstudio JSX.Get started"}\' --dry-run\n```\n\nFor simple authored/styled sections, run the three commands above. Do not grep source files, dump full MCP resources, or write parser scripts first. Use `list-pages`, `get-page-by-path`, or `list-instances` only when you still need the target `parentInstanceId`.\n\nWhen building for a Content-mode editor, use a Content Block (`ws:block`) around every region that editor must change. Content-mode text and supported props are editable only in Content Block descendants; content outside is read-only. Keep reusable insertable source options in the block\'s `ws:block-template` child. Templates themselves are protected; an editor\'s inserted copy becomes an editable direct child of the Content Block. Verify this structure before handoff.\n\nRun it from the linked Webstudio project root. The startup status line prints that absolute root; use it for local files such as `/.temp/script.mjs`, screenshots, and generated artifacts.\n\nFor experiments, pass `--dry-run` to local-capable mutation calls. Read the computed transaction from `meta.session.transaction` and its base build version from `meta.session.version`. Copying a `.webstudio` folder is not an isolated project clone; `.webstudio/config.json` still points to the same remote project, so non-dry-run mutations can commit to that project.\n\nStartup marks cached ProjectSession data stale so MCP tools read the current Builder dev build.\n\nAfter startup, use focused discovery tools with concrete JSON. For delegated design-system or “use every component” tasks, start with exactly one command: `webstudio workflow.next \'{"goal":"design-system-page"}\'`; report the returned checkpoint to the parent/user and stop until continued. For other tasks, read `meta.index` first. From a shell, `webstudio mcp list-tools` is a concise alias for the initial tool catalog. Then call shortcuts such as `webstudio meta.guide \'{"brief":"Create a design system page using every component"}\'`, `webstudio workflow.next \'{"goal":"design-system-page"}\'`, `webstudio meta.get_more_tools \'{"tools":["insert-fragment"]}\'`, `webstudio components.list \'{"source":"all"}\'`, `webstudio components.summary`, `webstudio components.search \'{"brief":"radix select"}\'`, `webstudio components.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'`, `webstudio templates.list`, and `webstudio templates.get \'{"component":"@webstudio-is/sdk-components-react-radix:Select"}\'`. Use `webstudio mcp single-op-call` when you need the explicit MCP form. Prefer `insert-fragment` for authored/styled sections; use `insert-component` only when inserting one automatic component template. In JSX, use `ws:style={css\\`...\\`}`for styles,`class`/`for`instead of`className`/`htmlFor`, `ActionValue`for actions instead of JavaScript functions such as`onClick={() => ...}`, only JSON-compatible plain prop values, and no host globals or dynamic code APIs. For bounded multi-step shell work, run inline JSON like `webstudio mcp run \'[{"tool":"meta.index"},{"tool":"components.search","input":{"brief":"button"}}]\'`; this reuses one CLI session without raw JSON-RPC. For larger batches, write `{ "calls": [{ "tool": "..." }] }`to`.temp/mcp-calls.json`and run`webstudio mcp run .temp/mcp-calls.json`. If any call returns `checkpoint.required`, `mcp run`stops immediately before later calls; report the checkpoint and call`checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`before continuing. For design-system or “use every component” tasks, call`workflow.next {"goal":"design-system-page"}`for the next bounded phase, report its checkpoint, wait until the parent/user continues, call`checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`, then run only that phase. Start coverage phases with compact`webstudio components.coverage-plan`, checkpoint, then request component coverage details with `components.coverage-plan {"detail":"roots"}`or`components.coverage-plan {"detail":"parts"}`. Do not pass `detail`to`list-pages`; use `list-pages {}`or`get-page-by-path`for page lookup. Read MCP overview resources`webstudio://project/guide`, `webstudio://project/tools-overview`, and `webstudio://project/components-overview`when useful. Read full resources`webstudio://project/tools`and`webstudio://project/components` only when focused tools are insufficient; do not dump or parse full discovery JSON for common lookup.\n\nAfter startup, MCP clients discover capabilities with `tools/list`, `resources/list`, `meta.index`, `meta.guide`, and `meta.get_more_tools`.\n\nstdout is reserved for MCP JSON-RPC messages while the server is running.\n\nThe server advertises MCP `logging` capability and sends sparse `notifications/message` logs for ready state and tool lifecycle checkpoints such as `tool preview.start started`, `tool preview.start still running after 10000ms`, and `tool preview.start succeeded in 1234ms`. It also writes the same sparse lifecycle status lines to stderr, prefixed with `[webstudio mcp]`, including a final ready line. Treat the process as healthy and long-running after either ready signal; surface tool lifecycle logs as progress checkpoints and interact through MCP JSON-RPC on stdin/stdout.\n\n`webstudio mcp single-op-call` also writes sparse lifecycle lines to stderr for each one-shot call. Use those stderr lines as progress checkpoints and parse stdout as JSON on both success and failure. Failed one-shot calls print `{ "ok": false, "error": { "code": "...", "message": "..." }, "meta": ... }` to stdout and exit nonzero. If a one-shot call returns checkpoint.required, read-only discovery remains available, while later mutations fail with `CHECKPOINT_REQUIRED` until you report the checkpoint and call `webstudio mcp single-op-call checkpoint.ack \'{"reported":true,"continueAfterReport":true,"summary":""}\'`.\n\n`webstudio mcp run` also writes sparse lifecycle lines to stderr and prints one JSON result on success or failure. It stops at the first failed call or checkpoint requirement, including `CHECKPOINT_REQUIRED`, and preserves partial results so delegated agents can report exactly where they stopped.\n\nIf a CLI/MCP tool crashes, hangs, gives a confusing error, needs an undocumented workaround, or forces source-code inspection for normal usage, ask the user to report it in the Webstudio Discord `#help` channel at https://wstd.us/community. Give the user a complete copy-paste report with the goal, expected behavior, actual error, exact command/tool call, stdout JSON, stderr/lifecycle logs, environment, workaround, and secrets redacted.\n\nIf you are running as a delegated or non-streaming agent whose parent cannot see live stderr/stdout, do not batch many MCP calls silently and do not run long shell loops of shortcut or `webstudio mcp single-op-call` commands. Treat each parent-visible checkpoint as the unit of work. If the parent asks for status within 30 seconds, run exactly one shortcut command such as `webstudio meta.index` or one explicit `webstudio mcp single-op-call` command, return a concise checkpoint with that command/result, and wait for the parent to continue. Do not take a broad task such as creating a full design-system page as one execution unit. Call `workflow.next {"goal":"design-system-page"}`, report the returned phase/checkpoint, wait until the parent/user continues, call `checkpoint.ack {"reported":true,"continueAfterReport":true,"summary":""}`, complete exactly that bounded phase, and return: discovery, page creation, one dry-run JSX section, one committed JSX section, one `components.coverage-insert-next` call, or one presentation pass. Phase commands do not include nextPhase in their own output. After the parent continues, acknowledge the previous checkpoint first, then call `workflow.next` with the next phase. For all-component design-system pages, checkpoint after workflow planning, discovery, page creation, call `components.coverage-insert-next` once before checkpointing again, then finish with the `presentation-pass` workflow phase.\n', "mcp-vision": diff --git a/packages/cli/src/docs/api-use-cases.md b/packages/cli/src/docs/api-use-cases.md index 9a8c8d4ae22d..72eb43fddd00 100644 --- a/packages/cli/src/docs/api-use-cases.md +++ b/packages/cli/src/docs/api-use-cases.md @@ -111,21 +111,6 @@ MCP lets agents work on one configured Webstudio project. Agents can: - Publish, unpublish, inspect publish jobs, and manage custom domains. - Start preview, capture screenshots, compare screenshot diffs, and use OCR when installed. -## Query and bind asset data - -Commands: - -- MCP tool: create-resource {"resource":{"control":"system","name":"Markdown assets","method":"get","url":"/$resources/assets","searchParams":[{"name":"extension","value":{"type":"literal","value":"md"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"markdownAssets"} -- MCP tool: create-resource {"resource":{"control":"system","name":"Site data","method":"get","url":"/$resources/assets","searchParams":[{"name":"filename","value":{"type":"literal","value":"site-data.json"}},{"name":"include","value":{"type":"literal","value":"content"}}],"headers":[]},"scopeInstanceId":"","dataSourceName":"siteData"} - -Notes: - -- The Assets system resource returns an object keyed by asset id. Each item includes its URL, filename, description, folder, type, format, size, and timestamps. -- Filter by a case-insensitive filename substring with `filename`. Filter by one or more extensions with `extension`; separate extensions with commas or repeat the parameter. -- Asset contents are omitted by default. Set `include=content` to include editable text contents and parse valid JSON contents into structured values. -- A content query accepts at most 20 matching assets and 256 KiB per asset. Binary and oversized assets include `contentError` instead of content. -- Bind the resource data or any nested metadata/content field with the existing resource binding controls. - ## Inspect and refresh MCP session cache Commands: diff --git a/packages/cli/src/docs/manual-mcp.md b/packages/cli/src/docs/manual-mcp.md index 4eda887e9853..65afa9694789 100644 --- a/packages/cli/src/docs/manual-mcp.md +++ b/packages/cli/src/docs/manual-mcp.md @@ -274,7 +274,6 @@ terms, agents can: - Create GraphQL resources. - Create system resources. - Use built-in system resources for sitemap, current date, and assets. -- Filter the Assets system resource by `filename` or `extension`, opt into bounded text content with `include=content`, and bind its metadata or parsed JSON content. - List and inspect complete asset metadata; upload, download, update, move, duplicate, find usage for, replace, and delete assets. - List, create, rename, move, recursively duplicate, and recursively delete nested asset folders. - Publish to staging or production. diff --git a/packages/cli/src/prebuild.test.ts b/packages/cli/src/prebuild.test.ts index 5bf095366558..1d7a5c2caf42 100644 --- a/packages/cli/src/prebuild.test.ts +++ b/packages/cli/src/prebuild.test.ts @@ -410,9 +410,6 @@ describe("prebuild", () => { await expect( readFile("app/__generated__/$resources.assets.ts", "utf8") ).resolves.toContain("image.png"); - await expect( - readFile("app/__generated__/$resources.assets.ts", "utf8") - ).resolves.toContain('"format": "png"'); await expect( readFile("app/__generated__/$resources.sitemap.xml.ts", "utf8") ).resolves.toContain('"path": "/"'); diff --git a/packages/cli/src/prebuild.ts b/packages/cli/src/prebuild.ts index 7f611f111138..9d83bafd6943 100644 --- a/packages/cli/src/prebuild.ts +++ b/packages/cli/src/prebuild.ts @@ -40,7 +40,7 @@ import { generateCss, ROOT_INSTANCE_ID, elementComponent, - toAssetResourceItem, + toRuntimeAsset, } from "@webstudio-is/sdk"; import { migratePages } from "@webstudio-is/project-migrations/pages"; import { collectFontFamiliesFromStyleDecls } from "@webstudio-is/project-build/runtime"; @@ -798,16 +798,14 @@ export const prebuild = async (options: { const assetsById = Object.fromEntries( siteData.assets.map((asset) => [ asset.id, - toAssetResourceItem(asset, "https://placeholder.local"), + toRuntimeAsset(asset, "https://placeholder.local"), ]) ); await createFileIfNotExists( join(generatedDir, "$resources.assets.ts"), ` - import type { AssetResourceItem } from "@webstudio-is/sdk/runtime"; - - export const assets = ${JSON.stringify(assetsById, null, 2)} satisfies Record; + export const assets = ${JSON.stringify(assetsById, null, 2)}; ` ); diff --git a/packages/cli/templates/defaults/app/route-templates/html.tsx b/packages/cli/templates/defaults/app/route-templates/html.tsx index b950fb67fb53..d4fa64509859 100644 --- a/packages/cli/templates/defaults/app/route-templates/html.tsx +++ b/packages/cli/templates/defaults/app/route-templates/html.tsx @@ -15,7 +15,6 @@ import { loadResource, loadResources, cachedFetch, - loadAssetResource, formIdFieldName, formBotFieldName, } from "@webstudio-is/sdk/runtime"; @@ -66,7 +65,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const createCustomFetch = (origin: string): typeof fetch => (input, init) => { +const customFetch: typeof fetch = (input, init) => { if (typeof input !== "string") { return cachedFetch(projectId, input, init); } @@ -97,11 +96,9 @@ const createCustomFetch = (origin: string): typeof fetch => (input, init) => { } if (isLocalResource(input, "assets")) { - return loadAssetResource({ - assets, - requestUrl: input, - fetchAsset: (url) => cachedFetch(projectId, new URL(url, origin)), - }).then(Response.json); + const response = new Response(JSON.stringify(assets)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); } return cachedFetch(projectId, input, init); @@ -127,7 +124,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - createCustomFetch(url.origin), + customFetch, getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/defaults/app/route-templates/text.tsx b/packages/cli/templates/defaults/app/route-templates/text.tsx index 1f1ebe4f3e6b..25da6ce73d4b 100644 --- a/packages/cli/templates/defaults/app/route-templates/text.tsx +++ b/packages/cli/templates/defaults/app/route-templates/text.tsx @@ -1,9 +1,5 @@ import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; -import { - isLocalResource, - loadAssetResource, - loadResources, -} from "@webstudio-is/sdk/runtime"; +import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { projectDomain } from "__CLIENT__"; import { getPageMeta, getRemixParams, getResources } from "__SERVER__"; @@ -29,7 +25,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const createCustomFetch = (origin: string): typeof fetch => (input, init) => { +const customFetch: typeof fetch = (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -58,11 +54,9 @@ const createCustomFetch = (origin: string): typeof fetch => (input, init) => { } if (isLocalResource(input, "assets")) { - return loadAssetResource({ - assets, - requestUrl: input, - fetchAsset: (url) => fetch(new URL(url, origin)), - }).then(Response.json); + const response = new Response(JSON.stringify(assets)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); } return fetch(input, init); @@ -89,7 +83,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - createCustomFetch(url.origin), + customFetch, getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/defaults/app/route-templates/xml.tsx b/packages/cli/templates/defaults/app/route-templates/xml.tsx index 7311faf48fa9..17cecc3c6c2e 100644 --- a/packages/cli/templates/defaults/app/route-templates/xml.tsx +++ b/packages/cli/templates/defaults/app/route-templates/xml.tsx @@ -1,10 +1,6 @@ import { renderToString } from "react-dom/server"; import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; -import { - isLocalResource, - loadAssetResource, - loadResources, -} from "@webstudio-is/sdk/runtime"; +import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { ReactSdkContext, @@ -35,7 +31,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const createCustomFetch = (origin: string): typeof fetch => (input, init) => { +const customFetch: typeof fetch = (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -66,11 +62,9 @@ const createCustomFetch = (origin: string): typeof fetch => (input, init) => { } if (isLocalResource(input, "assets")) { - return loadAssetResource({ - assets, - requestUrl: input, - fetchAsset: (url) => fetch(new URL(url, origin)), - }).then(Response.json); + const response = new Response(JSON.stringify(assets)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); } return fetch(input, init); @@ -97,7 +91,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - createCustomFetch(url.origin), + customFetch, getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/react-router/app/route-templates/html.tsx b/packages/cli/templates/react-router/app/route-templates/html.tsx index db3a62ec63d3..bb66055fa7ae 100644 --- a/packages/cli/templates/react-router/app/route-templates/html.tsx +++ b/packages/cli/templates/react-router/app/route-templates/html.tsx @@ -17,7 +17,6 @@ import { formIdFieldName, formBotFieldName, cachedFetch, - loadAssetResource, } from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { @@ -65,7 +64,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const createCustomFetch = (origin: string): typeof fetch => (input, init) => { +const customFetch: typeof fetch = (input, init) => { if (typeof input !== "string") { return cachedFetch(projectId, input, init); } @@ -96,11 +95,9 @@ const createCustomFetch = (origin: string): typeof fetch => (input, init) => { } if (isLocalResource(input, "assets")) { - return loadAssetResource({ - assets, - requestUrl: input, - fetchAsset: (url) => cachedFetch(projectId, new URL(url, origin)), - }).then(Response.json); + const response = new Response(JSON.stringify(assets)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); } return cachedFetch(projectId, input, init); @@ -126,7 +123,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - createCustomFetch(url.origin), + customFetch, getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/react-router/app/route-templates/text.tsx b/packages/cli/templates/react-router/app/route-templates/text.tsx index df57065a42ba..3229cb1f40fd 100644 --- a/packages/cli/templates/react-router/app/route-templates/text.tsx +++ b/packages/cli/templates/react-router/app/route-templates/text.tsx @@ -1,9 +1,5 @@ import { type LoaderFunctionArgs, redirect } from "react-router"; -import { - isLocalResource, - loadAssetResource, - loadResources, -} from "@webstudio-is/sdk/runtime"; +import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { projectDomain } from "__CLIENT__"; import { getPageMeta, getRemixParams, getResources } from "__SERVER__"; @@ -29,7 +25,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const createCustomFetch = (origin: string): typeof fetch => (input, init) => { +const customFetch: typeof fetch = (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -58,11 +54,9 @@ const createCustomFetch = (origin: string): typeof fetch => (input, init) => { } if (isLocalResource(input, "assets")) { - return loadAssetResource({ - assets, - requestUrl: input, - fetchAsset: (url) => fetch(new URL(url, origin)), - }).then(Response.json); + const response = new Response(JSON.stringify(assets)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); } return fetch(input, init); @@ -89,7 +83,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - createCustomFetch(url.origin), + customFetch, getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/react-router/app/route-templates/xml.tsx b/packages/cli/templates/react-router/app/route-templates/xml.tsx index 7a5da6c2fef9..ccc93691c43c 100644 --- a/packages/cli/templates/react-router/app/route-templates/xml.tsx +++ b/packages/cli/templates/react-router/app/route-templates/xml.tsx @@ -1,10 +1,6 @@ import { renderToString } from "react-dom/server"; import { type LoaderFunctionArgs, redirect } from "react-router"; -import { - isLocalResource, - loadAssetResource, - loadResources, -} from "@webstudio-is/sdk/runtime"; +import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; import { authenticateRequest } from "@webstudio-is/wsauth"; import { ReactSdkContext, @@ -35,7 +31,7 @@ const authenticateProductionRequest = (request: Request) => { return authenticateRequest(request, authRoutes); }; -const createCustomFetch = (origin: string): typeof fetch => (input, init) => { +const customFetch: typeof fetch = (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -66,11 +62,9 @@ const createCustomFetch = (origin: string): typeof fetch => (input, init) => { } if (isLocalResource(input, "assets")) { - return loadAssetResource({ - assets, - requestUrl: input, - fetchAsset: (url) => fetch(new URL(url, origin)), - }).then(Response.json); + const response = new Response(JSON.stringify(assets)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); } return fetch(input, init); @@ -97,7 +91,7 @@ export const loader = async (arg: LoaderFunctionArgs) => { }; const resources = await loadResources( - createCustomFetch(url.origin), + customFetch, getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/cli/templates/ssg/app/route-templates/html/+data.ts b/packages/cli/templates/ssg/app/route-templates/html/+data.ts index 990db1f6ea57..058735e51e85 100644 --- a/packages/cli/templates/ssg/app/route-templates/html/+data.ts +++ b/packages/cli/templates/ssg/app/route-templates/html/+data.ts @@ -1,13 +1,9 @@ import type { PageContextServer } from "vike/types"; -import { - isLocalResource, - loadAssetResource, - loadResources, -} from "@webstudio-is/sdk/runtime"; +import { isLocalResource, loadResources } from "@webstudio-is/sdk/runtime"; import { getPageMeta, getResources } from "__SERVER__"; import { assets } from "__ASSETS__"; -const createCustomFetch = (origin: string): typeof fetch => (input, init) => { +const customFetch: typeof fetch = (input, init) => { if (typeof input !== "string") { return fetch(input, init); } @@ -31,11 +27,9 @@ const createCustomFetch = (origin: string): typeof fetch => (input, init) => { } if (isLocalResource(input, "assets")) { - return loadAssetResource({ - assets, - requestUrl: input, - fetchAsset: (url) => fetch(new URL(url, origin)), - }).then(Response.json); + const response = new Response(JSON.stringify(assets)); + response.headers.set("content-type", "application/json; charset=utf-8"); + return Promise.resolve(response); } return fetch(input, init); @@ -57,7 +51,7 @@ export const data = async (pageContext: PageContextServer) => { }; const resources = await loadResources( - createCustomFetch(url.origin), + customFetch, getResources({ system }).data ); const pageMeta = getPageMeta({ system, resources }); diff --git a/packages/react-sdk/placeholder.d.ts b/packages/react-sdk/placeholder.d.ts index f89514302f3c..572686dc064e 100644 --- a/packages/react-sdk/placeholder.d.ts +++ b/packages/react-sdk/placeholder.d.ts @@ -66,8 +66,8 @@ declare module "__SITEMAP__" { } declare module "__ASSETS__" { - import type { AssetResourceItem } from "@webstudio-is/sdk"; - export const assets: Record; + import type { RuntimeAsset } from "@webstudio-is/sdk"; + export const assets: Record; } declare module "__REDIRECT__" { diff --git a/packages/sdk/src/asset-resource.test.ts b/packages/sdk/src/asset-resource.test.ts deleted file mode 100644 index ff74a926c36f..000000000000 --- a/packages/sdk/src/asset-resource.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { describe, expect, test, vi } from "vitest"; -import type { Asset } from "./schema/assets"; -import { - filterAssetResource, - loadAssetResource, - maxAssetResourceContentBytes, - maxAssetResourceContentItems, - toAssetResourceItem, -} from "./asset-resource"; - -const fileAsset = ({ - id, - filename, - format, - size = 10, -}: { - id: string; - filename: string; - format: string; - size?: number; -}): Asset => ({ - id, - projectId: "project", - name: `${id}.${format}`, - filename, - type: "file", - format, - size, - createdAt: "2026-07-20T00:00:00.000Z", - meta: {}, -}); - -const assets = Object.fromEntries( - [ - fileAsset({ id: "readme", filename: "Readme.md", format: "md" }), - fileAsset({ id: "settings", filename: "settings.json", format: "json" }), - fileAsset({ id: "notes", filename: "release-notes.txt", format: "txt" }), - ].map((asset) => [ - asset.id, - toAssetResourceItem(asset, "https://example.com"), - ]) -); - -describe("asset resource", () => { - test("filters by filename and one or more extensions", () => { - expect( - Object.keys( - filterAssetResource( - assets, - new URLSearchParams({ filename: "set", extension: ".json,md" }) - ) - ) - ).toEqual(["settings"]); - }); - - test("includes text and parsed JSON content", async () => { - const fetchAsset = vi.fn( - async (url: string) => - new Response(url.includes("settings") ? '{"theme":"dark"}' : "# Readme") - ); - - await expect( - loadAssetResource({ - assets, - requestUrl: "/$resources/assets?extension=md,json&include=content", - fetchAsset, - }) - ).resolves.toMatchObject({ - readme: { content: "# Readme" }, - settings: { content: { theme: "dark" } }, - }); - }); - - test("reports invalid JSON without losing its source text", async () => { - await expect( - loadAssetResource({ - assets: { settings: assets.settings! }, - requestUrl: "/$resources/assets?include=content", - fetchAsset: async () => new Response("{invalid"), - }) - ).resolves.toMatchObject({ - settings: { - content: "{invalid", - contentError: "Asset contains invalid JSON.", - }, - }); - }); - - test("bounds content by asset size and match count", async () => { - const oversized = toAssetResourceItem( - fileAsset({ - id: "large", - filename: "large.md", - format: "md", - size: maxAssetResourceContentBytes + 1, - }), - "https://example.com" - ); - const fetchAsset = vi.fn(async () => new Response("unused")); - await expect( - loadAssetResource({ - assets: { large: oversized }, - requestUrl: "/$resources/assets?include=content", - fetchAsset, - }) - ).resolves.toMatchObject({ - large: { contentError: expect.stringContaining("exceeds") }, - }); - expect(fetchAsset).not.toHaveBeenCalled(); - - const tooMany = Object.fromEntries( - Array.from({ length: maxAssetResourceContentItems + 1 }, (_, index) => [ - String(index), - assets.readme!, - ]) - ); - await expect( - loadAssetResource({ - assets: tooMany, - requestUrl: "/$resources/assets?include=content", - fetchAsset, - }) - ).rejects.toThrow(`at most ${maxAssetResourceContentItems}`); - }); - - test("does not fetch binary asset content", async () => { - const binary = toAssetResourceItem( - fileAsset({ - id: "document", - filename: "document.pdf", - format: "pdf", - }), - "https://example.com" - ); - const fetchAsset = vi.fn(async () => new Response("unused")); - - await expect( - loadAssetResource({ - assets: { document: binary }, - requestUrl: "/$resources/assets?include=content", - fetchAsset, - }) - ).resolves.toMatchObject({ - document: { contentError: "Asset format is not editable text." }, - }); - expect(fetchAsset).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/sdk/src/asset-resource.ts b/packages/sdk/src/asset-resource.ts deleted file mode 100644 index 9134537fc7cd..000000000000 --- a/packages/sdk/src/asset-resource.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { Asset } from "./schema/assets"; -import { getAssetTextEditorLanguage, toRuntimeAsset } from "./assets"; - -export const maxAssetResourceContentBytes = 256 * 1024; -export const maxAssetResourceContentItems = 20; - -export type AssetResourceItem = ReturnType & { - content?: unknown; - contentError?: string; -}; - -export const toAssetResourceItem = (asset: Asset, origin: string) => ({ - ...toRuntimeAsset(asset, origin), - name: asset.name, - filename: asset.filename ?? asset.name, - description: asset.description ?? undefined, - folderId: asset.folderId, - type: asset.type, - format: asset.format, - size: asset.size, - createdAt: asset.createdAt, - updatedAt: asset.updatedAt, -}); - -const getValues = (searchParams: URLSearchParams, name: string) => - searchParams - .getAll(name) - .flatMap((value) => value.split(",")) - .map((value) => value.trim().toLowerCase()) - .filter(Boolean); - -const matchesAssetResourceQuery = ( - asset: AssetResourceItem, - searchParams: URLSearchParams -) => { - const filenames = getValues(searchParams, "filename"); - const extensions = getValues(searchParams, "extension").map((extension) => - extension.startsWith(".") ? extension.slice(1) : extension - ); - const normalizedName = asset.filename.toLowerCase(); - return ( - (filenames.length === 0 || - filenames.some((filename) => normalizedName.includes(filename))) && - (extensions.length === 0 || extensions.includes(asset.format.toLowerCase())) - ); -}; - -export const filterAssetResource = ( - assets: Record, - searchParams: URLSearchParams -) => - Object.fromEntries( - Object.entries(assets).filter(([, asset]) => - matchesAssetResourceQuery(asset, searchParams) - ) - ); - -const getAssetContent = async ({ - asset, - fetchAsset, -}: { - asset: AssetResourceItem; - fetchAsset: (url: string) => Promise; -}) => { - if (getAssetTextEditorLanguage(asset) === undefined) { - return { contentError: "Asset format is not editable text." }; - } - if (asset.size > maxAssetResourceContentBytes) { - return { - contentError: `Asset content exceeds ${maxAssetResourceContentBytes} bytes.`, - }; - } - try { - const response = await fetchAsset(asset.url); - if (response.ok === false) { - return { contentError: `Asset request failed with ${response.status}.` }; - } - const bytes = await response.arrayBuffer(); - if (bytes.byteLength > maxAssetResourceContentBytes) { - return { - contentError: `Asset content exceeds ${maxAssetResourceContentBytes} bytes.`, - }; - } - const content = new TextDecoder().decode(bytes); - if (asset.format.toLowerCase() !== "json") { - return { content }; - } - try { - return { content: JSON.parse(content) as unknown }; - } catch { - return { content, contentError: "Asset contains invalid JSON." }; - } - } catch (error) { - return { - contentError: - error instanceof Error ? error.message : "Asset request failed.", - }; - } -}; - -export const loadAssetResource = async ({ - assets, - requestUrl, - fetchAsset, -}: { - assets: Record; - requestUrl: string; - fetchAsset: (url: string) => Promise; -}) => { - const searchParams = new URL(requestUrl, "https://webstudio.local") - .searchParams; - const filtered = filterAssetResource(assets, searchParams); - if (searchParams.get("include") !== "content") { - return filtered; - } - const entries = Object.entries(filtered); - if (entries.length > maxAssetResourceContentItems) { - throw new Error( - `Asset content queries support at most ${maxAssetResourceContentItems} matching assets.` - ); - } - return Object.fromEntries( - await Promise.all( - entries.map(async ([id, asset]) => [ - id, - { ...asset, ...(await getAssetContent({ asset, fetchAsset })) }, - ]) - ) - ); -}; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index d84aeb265260..f502ca499ef6 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -17,7 +17,6 @@ export * from "./schema/component-meta"; export * from "./assets"; export * from "./asset-folder-hierarchy"; export * from "./asset-folder-normalization"; -export * from "./asset-resource"; export * from "./core-metas"; export * from "./instances-utils"; export * from "./page-utils"; diff --git a/packages/sdk/src/resource-loader.test.ts b/packages/sdk/src/resource-loader.test.ts index 374def58b335..1d11d305a940 100644 --- a/packages/sdk/src/resource-loader.test.ts +++ b/packages/sdk/src/resource-loader.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test, beforeEach, vi, type Mock } from "vitest"; -import { isLocalResource, loadResource } from "./resource-loader"; +import { loadResource } from "./resource-loader"; import type { ResourceRequest } from "./schema/resources"; // Mock the fetch function @@ -104,29 +104,6 @@ describe("loadResource", () => { ); }); - test("appends search params to local resources", async () => { - const fetchResource = vi.fn(async () => Response.json({})); - - await loadResource(fetchResource, { - name: "assets", - url: "/$resources/assets", - searchParams: [{ name: "extension", value: "json,md" }], - method: "get", - headers: [], - }); - - expect(fetchResource).toHaveBeenCalledWith( - "/$resources/assets?extension=json%2Cmd", - expect.any(Object) - ); - expect( - isLocalResource("/$resources/assets?extension=json%2Cmd", "assets") - ).toBe(true); - expect( - isLocalResource("https://example.com/$resources/assets", "assets") - ).toBe(false); - }); - test("should fetch resource with JSON search params", async () => { const mockResponse = new Response(JSON.stringify({ key: "value" }), { status: 200, diff --git a/packages/sdk/src/resource-loader.ts b/packages/sdk/src/resource-loader.ts index 782666400abc..7aa7a7cd187d 100644 --- a/packages/sdk/src/resource-loader.ts +++ b/packages/sdk/src/resource-loader.ts @@ -3,17 +3,12 @@ import type { ResourceRequest } from "./schema/resources"; import { serializeValue } from "./to-string"; const LOCAL_RESOURCE_PREFIX = "$resources"; -const localResourceBaseUrl = new URL("https://webstudio.local"); /** * Prevents fetch cycles by prefixing local resources. */ export const isLocalResource = (pathname: string, resourceName?: string) => { - const url = new URL(pathname, localResourceBaseUrl); - if (url.origin !== localResourceBaseUrl.origin) { - return false; - } - const segments = url.pathname.split("/").filter(Boolean); + const segments = pathname.split("/").filter(Boolean); if (resourceName === undefined) { return segments[0] === LOCAL_RESOURCE_PREFIX; @@ -36,15 +31,13 @@ export const loadResource = async ( try { // cloudflare workers fail when fetching url contains spaces // even though new URL suppose to trim them on parsing by spec - const resourceUrl = resourceRequest.url.trim(); - const isRelative = URL.canParse(resourceUrl) === false; - const url = new URL(resourceUrl, localResourceBaseUrl); + const url = new URL(resourceRequest.url.trim()); if (searchParams) { for (const { name, value } of searchParams) { url.searchParams.append(name, serializeValue(value)); } } - href = isRelative ? `${url.pathname}${url.search}${url.hash}` : url.href; + href = url.href; } catch { // empty block } diff --git a/packages/sdk/src/runtime.ts b/packages/sdk/src/runtime.ts index 505ece627bfb..dd766a7bd164 100644 --- a/packages/sdk/src/runtime.ts +++ b/packages/sdk/src/runtime.ts @@ -1,5 +1,4 @@ export * from "./resource-loader"; -export * from "./asset-resource"; export * from "./to-string"; export * from "./form-fields"; export * from "./json-ld"; From 47b500fda079a0fc585e30b8cc1df9cb712e16ac Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Mon, 20 Jul 2026 16:34:14 +0100 Subject: [PATCH 03/14] fix: hide asset storage suffixes --- packages/asset-uploader/src/revision.test.ts | 6 ++++ packages/asset-uploader/src/revision.ts | 13 +++----- .../src/utils/get-unique-filename.ts | 3 +- .../project-build/src/runtime/assets.test.ts | 8 +++++ packages/project-build/src/runtime/assets.ts | 23 ++------------ packages/sdk/src/assets.ts | 30 +++++++++++++++++++ 6 files changed, 52 insertions(+), 31 deletions(-) diff --git a/packages/asset-uploader/src/revision.test.ts b/packages/asset-uploader/src/revision.test.ts index 31830da25716..400365383a81 100644 --- a/packages/asset-uploader/src/revision.test.ts +++ b/packages/asset-uploader/src/revision.test.ts @@ -69,6 +69,12 @@ describe("asset content revisions", () => { filename: "configuration", }) ).toBe("configuration.json"); + expect( + getRevisionFilename({ + name: "test_nCEugJxJwUd_MJcgPodZr.md", + filename: null, + }) + ).toBe("test.md"); }); test("uploads an immutable revision and keeps the asset id", async () => { diff --git a/packages/asset-uploader/src/revision.ts b/packages/asset-uploader/src/revision.ts index f29c6f0c7b7a..527636348aee 100644 --- a/packages/asset-uploader/src/revision.ts +++ b/packages/asset-uploader/src/revision.ts @@ -1,5 +1,4 @@ -import { basename, extname } from "node:path"; -import { isTextFileAsset, type Asset } from "@webstudio-is/sdk"; +import { isTextFileAsset, parseAssetName, type Asset } from "@webstudio-is/sdk"; import { authorizeProject, AuthorizationError, @@ -22,13 +21,9 @@ const getRevisionFilename = ({ name: string; filename: string | null; }) => { - const extension = extname(name); - const storedBasename = basename(name, extension); - const suffixAt = storedBasename.lastIndexOf("_"); - const displayBasename = - filename ?? - (suffixAt === -1 ? storedBasename : storedBasename.slice(0, suffixAt)); - return sanitizeS3Key(`${displayBasename}${extension}`); + const { basename, ext } = parseAssetName(name); + const displayBasename = filename ?? basename; + return sanitizeS3Key(`${displayBasename}${ext === "" ? "" : `.${ext}`}`); }; export const swapAssetFileWithClient = async ( diff --git a/packages/asset-uploader/src/utils/get-unique-filename.ts b/packages/asset-uploader/src/utils/get-unique-filename.ts index 821edaf246aa..e928cab0afa9 100644 --- a/packages/asset-uploader/src/utils/get-unique-filename.ts +++ b/packages/asset-uploader/src/utils/get-unique-filename.ts @@ -1,8 +1,9 @@ import * as path from "node:path"; import { nanoid } from "nanoid"; +import { assetStorageIdLength } from "@webstudio-is/sdk"; export const getUniqueFilename = (filename: string): string => { - const id = nanoid(); + const id = nanoid(assetStorageIdLength); const extension = path.extname(filename); const name = path.basename(filename, extension); return `${name}_${id}${extension}`; diff --git a/packages/project-build/src/runtime/assets.test.ts b/packages/project-build/src/runtime/assets.test.ts index d3217c68bd70..4a13956f0df4 100644 --- a/packages/project-build/src/runtime/assets.test.ts +++ b/packages/project-build/src/runtime/assets.test.ts @@ -130,6 +130,14 @@ describe("asset name helpers", () => { }); }); + test("keeps underscores inside the storage id out of the display name", () => { + expect(parseAssetName("test_nCEugJxJwUd_MJcgPodZr.md")).toEqual({ + basename: "test", + hash: "nCEugJxJwUd_MJcgPodZr", + ext: "md", + }); + }); + test("parses name with hash but no extension", () => { expect(parseAssetName("hello_hash1_hash2")).toEqual({ basename: "hello_hash1", diff --git a/packages/project-build/src/runtime/assets.ts b/packages/project-build/src/runtime/assets.ts index 00453c1e9ca8..6cadf13cd2e0 100644 --- a/packages/project-build/src/runtime/assets.ts +++ b/packages/project-build/src/runtime/assets.ts @@ -7,6 +7,7 @@ import { getAllPages, getStyleDeclKey, isAllowedMimeCategory, + parseAssetName, type Asset, type DataSource, type Pages, @@ -558,27 +559,7 @@ export const findAsset = (assets: Iterable, assetId: Asset["id"]) => { } }; -export type ParsedAssetName = { - basename: string; - hash: string; - ext: string; -}; - -export const parseAssetName = (name: string): ParsedAssetName => { - let hash = ""; - let ext = ""; - const lastDotAt = name.lastIndexOf("."); - if (lastDotAt > -1) { - ext = name.slice(lastDotAt + 1); - name = name.slice(0, lastDotAt); - } - const lastUnderscoreAt = name.lastIndexOf("_"); - if (lastUnderscoreAt > -1) { - hash = name.slice(lastUnderscoreAt + 1); - name = name.slice(0, lastUnderscoreAt); - } - return { basename: name, hash, ext }; -}; +export { parseAssetName }; export const formatAssetName = (asset: Pick) => { const { basename, ext } = parseAssetName(asset.name); diff --git a/packages/sdk/src/assets.ts b/packages/sdk/src/assets.ts index ce7ce8062deb..33d76781371a 100644 --- a/packages/sdk/src/assets.ts +++ b/packages/sdk/src/assets.ts @@ -21,6 +21,36 @@ export type AssetTextEditorLanguage = | "markdown" | "xml"; +export const assetStorageIdLength = 21; + +export type ParsedAssetName = { + basename: string; + hash: string; + ext: string; +}; + +export const parseAssetName = (assetName: string): ParsedAssetName => { + let name = assetName; + let ext = ""; + const lastDotAt = name.lastIndexOf("."); + if (lastDotAt > -1) { + ext = name.slice(lastDotAt + 1); + name = name.slice(0, lastDotAt); + } + + let hash = ""; + let separatorAt = name.length - assetStorageIdLength - 1; + if (separatorAt < 0 || name[separatorAt] !== "_") { + // Support storage names created before Nano ID was introduced. + separatorAt = name.lastIndexOf("_"); + } + if (separatorAt > -1) { + hash = name.slice(separatorAt + 1); + name = name.slice(0, separatorAt); + } + return { basename: name, hash, ext }; +}; + type MimeType = `${MimeCategory}/${string}`; type BinaryMimeType = `${Exclude}/${string}`; From fb40d6a26ccae53c29681110c549b6fe7240dc4d Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Mon, 20 Jul 2026 16:38:43 +0100 Subject: [PATCH 04/14] fix: show fixed asset extensions --- .../builder/shared/asset-manager/asset-settings.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/builder/app/builder/shared/asset-manager/asset-settings.tsx b/apps/builder/app/builder/shared/asset-manager/asset-settings.tsx index 460dfc9ba158..1ba6d89d5fa1 100644 --- a/apps/builder/app/builder/shared/asset-manager/asset-settings.tsx +++ b/apps/builder/app/builder/shared/asset-manager/asset-settings.tsx @@ -456,10 +456,20 @@ const AssetSettingsContent = ({ > + .{ext} + + } onChange={(event) => { setFilename(event.target.value); setFilenameError(undefined); From f92752fcea2e599424eb5f1454f64508eea838e8 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Mon, 20 Jul 2026 16:43:17 +0100 Subject: [PATCH 05/14] refactor: consolidate asset panel actions --- .../app/builder/features/assets/assets.tsx | 60 +++++++++++++------ .../builder/shared/assets/asset-upload.tsx | 26 +++++--- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/apps/builder/app/builder/features/assets/assets.tsx b/apps/builder/app/builder/features/assets/assets.tsx index f9243c5a69dd..020999600014 100644 --- a/apps/builder/app/builder/features/assets/assets.tsx +++ b/apps/builder/app/builder/features/assets/assets.tsx @@ -1,4 +1,8 @@ import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, IconButton, PanelTitle, Separator, @@ -8,6 +12,8 @@ import { BrushCleaningIcon, NewFolderIcon, NewPageIcon, + PlusIcon, + UploadIcon, } from "@webstudio-is/icons"; import { useRef, useState } from "react"; import { useStore } from "@nanostores/react"; @@ -54,33 +60,49 @@ export const AssetsPanel = ({ useImageAssetCanvasDrag(publish); return ( <> + - - setCreateFolderOpen(true)} - > - - - - + setCreateTextFileOpen(true)} + aria-label="Delete unused assets" + onClick={openDeleteUnusedAssetsDialog} > - - - - - - + + + + + + + + + + uploadRef.current?.open()}> + + Upload + + setCreateTextFileOpen(true)}> + + Create text file + + setCreateFolderOpen(true)}> + + Create folder + + + } > diff --git a/apps/builder/app/builder/shared/assets/asset-upload.tsx b/apps/builder/app/builder/shared/assets/asset-upload.tsx index c98c17b6e2b9..69e0d6617795 100644 --- a/apps/builder/app/builder/shared/assets/asset-upload.tsx +++ b/apps/builder/app/builder/shared/assets/asset-upload.tsx @@ -111,6 +111,7 @@ type AssetUploadProps = { type: AssetType; accept?: string; folderId?: string; + showTrigger?: boolean; }; export type AssetUploadHandle = { @@ -118,7 +119,7 @@ export type AssetUploadHandle = { }; const EnabledAssetUpload = forwardRef( - ({ accept, type, folderId }, forwardedRef) => { + ({ accept, type, folderId, showTrigger = true }, forwardedRef) => { const { inputRef, onChange } = useUpload(folderId); useImperativeHandle(forwardedRef, () => ({ open: () => inputRef.current?.click(), @@ -134,13 +135,15 @@ const EnabledAssetUpload = forwardRef( ref={inputRef} style={{ display: "none" }} /> - + {showTrigger && ( +