Files
Oleg Isonen dde30ffb54 feat: Improve MCP authoring, compact output, and agent onboarding (#5883)
## Summary

This improves how agents discover, author, inspect, and verify Webstudio
projects while reducing unnecessary MCP context usage.

## What changed

- Adds an **Agents** section to Project Settings with ready-to-run setup
commands and client configuration for Claude Code, Codex, Cursor, and VS
Code.
- Makes large list and schema responses compact and paginated by
default, with **verbose** available when complete records or diagnostics
are needed.
- Applies consistent compact output to assets, instances, styles, design
tokens, CLI schemas, and project-session metadata.
- Returns actionable expression warnings for props, resources, and
Collection bindings, including unknown variables and risky render-time
mutation resources.
- Improves Collection expression guidance so agents can bind array items
with the correct scoped variables.
- Reports rendered audit outcomes explicitly as complete, partial,
confirmation-required, or failed.
- Expands runtime output schemas so MCP clients can validate responses
reliably.
- Strengthens the packaged CLI smoke workflow with a realistic
seven-page stdio MCP scenario.

## Architecture

- Consolidates `project-build` from 65 file-oriented entrypoints to 11
architecture-owned surfaces.
- Separates the core model, contracts, runtime authoring, transfer,
state, project sessions, MCP, visual tooling, persistence codecs, and
authenticated server services.
- Isolates Node-only JSX evaluation from browser-safe transfer utilities
and authenticated server modules.
- Keeps persistence codecs independent from authenticated database
services, preventing unrelated module initialization.
- Defines shared instance-path types once and reuses them across runtime
modules.
- Preserves Immer initialization through explicit package side-effect
metadata.
- Adds package tests that prevent a return to source-file entrypoints or
generic exposure of Node-only surfaces.

## Validation

- Full repository checks, tests, typechecks, generated-file checks, and
fixtures
- All 163 Builder test files and 2,141 tests
- All 71 project-build test files and 1,794 tests
- Lint and package-boundary validation
- Full packed CLI release smoke test, including install, preview, agent
setup, seven-page stdio MCP workflow, rendered audit, and structured
commands
2026-07-15 18:11:28 +02:00

150 lines
4.0 KiB
TypeScript

// Data utilities own access to Webstudio's instance-related stores and
// transaction boundaries. Put generic store reads/writes and content-mode data
// guards here, not tree-shape mutations.
import { toast } from "@webstudio-is/design-system";
import { type WebstudioData, isPageTemplate } from "@webstudio-is/sdk";
import {
executeBuilderRuntimeOperation,
type BuilderRuntimeOperationInput,
type BuilderRuntimeOperationId,
type BuilderRuntimeOperationResult,
} from "@webstudio-is/project-build/runtime";
import { builderRuntimeContext } from "@webstudio-is/project-build/runtime";
import { type BuilderRuntimeMutation } from "@webstudio-is/project-build/runtime";
import { $canOpenPageTemplates, $selectedPage } from "../nano-states";
import { createTransactionFromBuilderPatchPayload } from "../sync/builder-patch";
import {
$assets,
$breakpoints,
$dataSources,
$instances,
$marketplaceProduct,
$pages,
$projectSettings,
$project,
$props,
$resources,
$styles,
$styleSourceSelections,
$styleSources,
} from "../sync/data-stores";
type RuntimeMutationResult<Id extends BuilderRuntimeOperationId> = Extract<
BuilderRuntimeOperationResult<Id>,
BuilderRuntimeMutation
>;
export type WebstudioInstanceData = Pick<
WebstudioData,
| "instances"
| "props"
| "styleSourceSelections"
| "styleSources"
| "styles"
| "dataSources"
| "resources"
>;
const canCommitWebstudioData = () => {
const selectedPage = $selectedPage.get();
return (
isPageTemplate(selectedPage) === false ||
$canOpenPageTemplates.get() === true
);
};
export const migrateLoadedWebstudioData = () => {
const result = executeRuntimeMutation({
id: "system.migrateLoadedData",
input: {},
});
if (result?.result.didBreakCycles === true) {
toast.info("Detected and fixed cycles in the instance tree.");
}
};
const createRuntimeMutationArgs = <Id extends BuilderRuntimeOperationId>({
id,
input,
}: {
id: Id;
input: BuilderRuntimeOperationInput<Id>;
}) => ({
id,
state: getWebstudioData(),
input,
context: {
createId: builderRuntimeContext.createId,
projectId: $project.get()?.id,
},
});
const commitRuntimeMutation = <Mutation extends BuilderRuntimeMutation>(
result: Mutation
): Mutation => {
createTransactionFromBuilderPatchPayload({
data: getWebstudioData(),
payload: result.payload,
});
return result;
};
export const executeRuntimeMutation = <Id extends BuilderRuntimeOperationId>({
id,
input,
}: {
id: Id;
input: BuilderRuntimeOperationInput<Id>;
}): RuntimeMutationResult<Id> | undefined => {
if (canCommitWebstudioData() === false) {
return;
}
const operationResult = executeBuilderRuntimeOperation<
RuntimeMutationResult<Id>
>(createRuntimeMutationArgs({ id, input }));
if (operationResult instanceof Promise) {
throw Error(`Builder runtime operation "${id}" must be synchronous.`);
}
return commitRuntimeMutation(operationResult);
};
export const executeRuntimeMutationAsync = async <
Id extends BuilderRuntimeOperationId,
>({
id,
input,
}: {
id: Id;
input: BuilderRuntimeOperationInput<Id>;
}): Promise<RuntimeMutationResult<Id> | undefined> => {
if (canCommitWebstudioData() === false) {
return;
}
const result = await executeBuilderRuntimeOperation<
RuntimeMutationResult<Id>
>(createRuntimeMutationArgs({ id, input }));
return commitRuntimeMutation(result);
};
export const getWebstudioData = () => {
const pages = $pages.get();
if (pages === undefined) {
throw Error(`Cannot get webstudio data with empty pages`);
}
const projectSettings = $projectSettings.get() ?? { meta: {}, compiler: {} };
return {
pages,
instances: $instances.get(),
props: $props.get(),
dataSources: $dataSources.get(),
resources: $resources.get(),
breakpoints: $breakpoints.get(),
styleSourceSelections: $styleSourceSelections.get(),
styleSources: $styleSources.get(),
styles: $styles.get(),
assets: $assets.get(),
marketplaceProduct: $marketplaceProduct.get(),
projectSettings,
};
};