Files
Oleg Isonen d291e4b671 feat: Builder api runtime contracts guardrails for MCP (#5857)
This PR expands Webstudio MCP and CLI into a more complete agent
authoring interface. Agents can discover project capabilities, edit real
Webstudio content, use data/resources, verify visual output, and report
useful issues when workflows fail.

## MCP User Capabilities

- Create, update, duplicate, reorder, and delete pages.
- Create pages from templates and manage reusable page templates.
- Inspect project pages, instances, props, styles, assets, variables,
resources, and metadata.
- Insert Webstudio components and authored JSX fragments into pages.
- Edit text, props, styles, breakpoints, design tokens, and CSS
variables.
- Update page metadata, SEO fields, redirects, marketplace metadata, and
project settings.
- Create and use static data variables.
- Create and use dynamic resources fetched over HTTP.
- Use REST, GraphQL, and built-in system resources in authored pages.
- Bind data variables and resources to visible page content.
- Create resource-backed pages such as sitemap XML, structured SEO data,
and API-driven content sections.
- Preview current project state without requiring a publish step.
- Capture screenshots for visual verification.
- Verify responsive designs across desktop, tablet, and mobile
breakpoints.
- Use screenshot diff and OCR-style checks to inspect rendered output.
- Reset and refresh local MCP project state when needed.
- Discover available CLI/MCP commands from generated manuals.
- Use clearer examples for resources, data variables, JSX authoring,
previewing, and visual checks.
- Read marketplace product metadata without requiring a full project
snapshot.
- Get safer guidance when using low-level patch tools.
- Get a ready-to-copy Discord report template when MCP/CLI behavior is
confusing or broken.
2026-07-10 02:13:22 +02:00

132 lines
3.0 KiB
TypeScript

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import {
createClient,
type Database,
} from "@webstudio-is/postgrest/index.server";
import env from "../app/env/env.server";
const execFileAsync = promisify(execFile);
const composeBaseFile = fileURLToPath(
new URL("../docker-compose.yaml", import.meta.url)
);
const composeFile = fileURLToPath(
new URL("../docker-compose.e2e.yaml", import.meta.url)
);
const postgrest = createClient(env.POSTGREST_URL, env.POSTGREST_API_KEY);
const throwIfError = <Data>({
error,
data,
}: {
error: { message: string } | null;
data: Data;
}): NonNullable<Data> => {
if (error) {
throw new Error(error.message);
}
if (data === null || data === undefined) {
throw new Error("Expected PostgREST response data");
}
return data;
};
export const resetDatabase = async () => {
const sql = `
DO $$
DECLARE
table_names text;
BEGIN
SELECT string_agg(format('%I.%I', schemaname, tablename), ', ')
INTO table_names
FROM pg_tables
WHERE schemaname = 'public'
AND tablename <> '_prisma_migrations';
IF table_names IS NOT NULL THEN
EXECUTE 'TRUNCATE TABLE ' || table_names || ' RESTART IDENTITY CASCADE';
END IF;
END $$;
`;
await execFileAsync(
"docker",
[
"compose",
"-f",
composeBaseFile,
"-f",
composeFile,
"exec",
"-T",
"db",
"sh",
"-c",
'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=1 -c "$1"',
"sh",
sql,
],
{ maxBuffer: 1024 * 1024 }
);
};
type Tables = Database["public"]["Tables"];
export const updateBuild = async (
id: string,
row: Tables["Build"]["Update"]
) => {
return throwIfError(
await postgrest.from("Build").update(row).eq("id", id).select().single()
);
};
export const updateProject = async (
id: string,
row: Tables["Project"]["Update"]
) => {
return throwIfError(
await postgrest.from("Project").update(row).eq("id", id).select().single()
);
};
export const insertAuthorizationToken = async (
row: Tables["AuthorizationToken"]["Insert"]
) => {
return throwIfError(
await postgrest.from("AuthorizationToken").insert(row).select().single()
);
};
export const insertFile = async (row: Tables["File"]["Insert"]) => {
return throwIfError(
await postgrest.from("File").insert(row).select().single()
);
};
export const insertAsset = async (row: Tables["Asset"]["Insert"]) => {
return throwIfError(
await postgrest.from("Asset").insert(row).select().single()
);
};
export const loadDevBuild = async ({
projectId,
}: {
projectId: string;
}): Promise<Database["public"]["Tables"]["Build"]["Row"]> => {
return throwIfError(
await postgrest
.from("Build")
.select("*")
.eq("projectId", projectId)
.is("deployment", null)
.order("createdAt", { ascending: false })
.limit(1)
.single()
);
};