Files
webstudio/packages/sdk/src/input-json-schema.test.ts
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

177 lines
4.1 KiB
TypeScript

import { expect, test } from "vitest";
import {
getInputJsonSchemaAdditionalProperties,
getInputJsonSchemaMetadata,
getInputJsonSchemaProperties,
inputJsonSchemaAcceptsType,
type InputJsonSchema,
} from "./input-json-schema";
test("derives field summaries from JSON input schemas", () => {
expect(
getInputJsonSchemaMetadata({
type: "object",
properties: {
title: { type: "string" },
tags: { type: "array", items: { type: "string" } },
variants: {
anyOf: [
{ type: "string" },
{ type: "array", items: { type: "string" } },
],
},
pair: {
allOf: [
{ type: "array", items: { type: "string" } },
{ minItems: 2 },
],
},
},
required: ["title", "tags"],
})
).toEqual({
inputFields: ["title", "tags", "variants", "pair"],
requiredInputFields: ["title", "tags"],
inputFieldTypes: {
tags: "array",
variants: "array",
pair: "array",
},
});
});
test("derives field summaries through JSON schema branches", () => {
expect(
getInputJsonSchemaMetadata({
allOf: [
{
properties: {
title: { type: "string" },
},
required: ["title"],
},
{
properties: {
tags: { type: "array", items: { type: "string" } },
},
},
],
})
).toEqual({
inputFields: ["title", "tags"],
requiredInputFields: ["title"],
inputFieldTypes: {
tags: "array",
},
});
});
test("does not require fields that are only required by some anyOf branches", () => {
expect(
getInputJsonSchemaMetadata({
properties: {
title: { type: "string" },
slug: { type: "string" },
kind: { type: "string" },
},
anyOf: [{ required: ["title", "kind"] }, { required: ["slug", "kind"] }],
})
).toEqual({
inputFields: ["title", "slug", "kind"],
requiredInputFields: ["kind"],
inputFieldTypes: {},
});
});
test("can treat unconstrained schemas as accepting structured JSON", () => {
expect(inputJsonSchemaAcceptsType({}, "array")).toBe(false);
expect(
inputJsonSchemaAcceptsType({}, "array", {
treatUnconstrainedAsAny: true,
})
).toBe(true);
expect(
inputJsonSchemaAcceptsType(
{ description: "Any JSON value.", default: {} },
"object",
{
treatUnconstrainedAsAny: true,
}
)
).toBe(true);
expect(
inputJsonSchemaAcceptsType({ format: "uri" } as InputJsonSchema, "object", {
treatUnconstrainedAsAny: true,
})
).toBe(false);
});
test("does not treat empty allOf schemas as every JSON type", () => {
expect(inputJsonSchemaAcceptsType({ allOf: [] }, "array")).toBe(false);
expect(inputJsonSchemaAcceptsType({ allOf: [] }, "object")).toBe(false);
});
test("does not require fields from empty anyOf branches", () => {
expect(
getInputJsonSchemaMetadata({
properties: {
title: { type: "string" },
},
anyOf: [],
}).requiredInputFields
).toEqual([]);
});
test("collects properties through JSON schema branches", () => {
expect(
getInputJsonSchemaProperties({
properties: {
title: { type: "string" },
settings: { type: "string" },
},
allOf: [
{
properties: {
settings: { type: "object" },
enabled: { type: "boolean" },
},
},
],
})
).toEqual({
title: { type: "string" },
settings: {
allOf: [{ type: "string" }, { type: "object" }],
},
enabled: { type: "boolean" },
});
});
test("collects additionalProperties schemas through JSON schema branches", () => {
expect(
getInputJsonSchemaAdditionalProperties({
additionalProperties: true,
allOf: [
{
additionalProperties: {
type: "object",
properties: {
enabled: { type: "boolean" },
},
},
},
],
})
).toEqual({
allOf: [
{},
{
type: "object",
properties: {
enabled: { type: "boolean" },
},
},
],
});
});