@plitzi/sdk-server 0.32.18 → 0.32.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +18 -0
- package/dist/core/services/mcp.js +2 -1
- package/dist/modules/mcp/apps/index.js +7 -3
- package/dist/modules/mcp/apps/render/index.js +5 -4
- package/dist/modules/mcp/apps/render/styles.js +54 -0
- package/dist/modules/mcp/apps/render/view/heldBatch.ts +107 -0
- package/dist/modules/mcp/apps/render/{view.tsx → view/index.tsx} +106 -4
- package/dist/modules/mcp/apps/render/view/streamProgress.ts +137 -0
- package/dist/modules/mcp/apps/shared/assets.js +10 -0
- package/dist/modules/mcp/apps/shared/bundle.js +15 -14
- package/dist/modules/mcp/apps/shared/page.js +17 -6
- package/dist/modules/mcp/apps/shared/registerApp.js +2 -2
- package/dist/modules/mcp/apps/shared/shell.ejs +5 -0
- package/dist/modules/mcp/handler.js +1 -1
- package/dist/modules/mcp/helpers/guide.js +2 -1
- package/dist/modules/mcp/helpers/space.js +13 -1
- package/dist/modules/mcp/resources/register.js +8 -3
- package/dist/modules/mcp/resources/renderGuide.js +63 -11
- package/dist/modules/mcp/resources/router.js +13 -1
- package/dist/modules/mcp/server.js +32 -20
- package/dist/modules/mcp/tools/read.js +21 -3
- package/dist/modules/mcp/tools/render.js +7 -3
- package/dist/modules/mcp/tools/shared/tool.js +16 -11
- package/dist/modules/mcp/tools/shared/validator/elements.js +11 -1
- package/dist/modules/mcp/tools/shared/validator/index.js +9 -5
- package/dist/src/modules/mcp/apps/index.d.ts +6 -3
- package/dist/src/modules/mcp/apps/render/index.d.ts +1 -0
- package/dist/src/modules/mcp/apps/render/styles.d.ts +10 -0
- package/dist/src/modules/mcp/apps/shared/assets.d.ts +10 -0
- package/dist/src/modules/mcp/apps/shared/bundle.d.ts +3 -0
- package/dist/src/modules/mcp/apps/shared/index.d.ts +1 -0
- package/dist/src/modules/mcp/apps/shared/page.d.ts +2 -2
- package/dist/src/modules/mcp/apps/shared/registerApp.d.ts +2 -2
- package/dist/src/modules/mcp/e2e/mcpEndpoint.d.ts +8 -1
- package/dist/src/modules/mcp/e2e/renderingHost.d.ts +5 -0
- package/dist/src/modules/mcp/handler.d.ts +2 -0
- package/dist/src/modules/mcp/helpers/guide.d.ts +1 -0
- package/dist/src/modules/mcp/helpers/space.d.ts +9 -0
- package/dist/src/modules/mcp/resources/index.d.ts +2 -1
- package/dist/src/modules/mcp/resources/register.d.ts +6 -2
- package/dist/src/modules/mcp/resources/renderGuide.d.ts +5 -0
- package/dist/src/modules/mcp/resources/router.d.ts +4 -0
- package/dist/src/modules/mcp/server.d.ts +9 -5
- package/dist/src/modules/mcp/tools/read.d.ts +1 -0
- package/dist/src/modules/mcp/tools/shared/tool.d.ts +8 -0
- package/dist/src/modules/mcp/tools/shared/validator/context.d.ts +4 -0
- package/dist/src/modules/mcp/tools/shared/validator/elements.d.ts +1 -0
- package/dist/src/modules/mcp/tools/shared/validator/index.d.ts +3 -3
- package/dist/src/modules/mcp/types/appTypes.d.ts +9 -1
- package/package.json +6 -5
- package/skills/plitzi-render/SKILL.md +174 -0
- package/dist/src/modules/mcp/apps/render/heldBatch.d.ts +0 -10
- package/dist/src/modules/mcp/apps/render/view.d.ts +0 -1
- /package/dist/modules/mcp/apps/example/{view.tsx → view/index.tsx} +0 -0
- /package/dist/src/modules/mcp/apps/{example/view.d.ts → render/styles.test.d.ts} +0 -0
- /package/dist/src/modules/mcp/{apps/render/heldBatch.test.d.ts → tests/skill.test.d.ts} +0 -0
|
@@ -11,7 +11,7 @@ var DEFAULT_CSP = {
|
|
|
11
11
|
};
|
|
12
12
|
/** Serves the app as a self-contained page: no import map, no asset mounts, no cross-origin fetches, so the
|
|
13
13
|
* strictest host sandbox runs it and no deployment has to serve anything extra. */
|
|
14
|
-
var registerApp = (server, app) => {
|
|
14
|
+
var registerApp = (server, app, settings) => {
|
|
15
15
|
const meta = { ui: { csp: app.csp ?? DEFAULT_CSP } };
|
|
16
16
|
registerAppResource(server, app.name, app.uri, {
|
|
17
17
|
description: app.description,
|
|
@@ -19,7 +19,7 @@ var registerApp = (server, app) => {
|
|
|
19
19
|
}, async () => ({ contents: [{
|
|
20
20
|
uri: app.uri,
|
|
21
21
|
mimeType: RESOURCE_MIME_TYPE,
|
|
22
|
-
text: await page(app),
|
|
22
|
+
text: await page(app, settings),
|
|
23
23
|
_meta: meta
|
|
24
24
|
}] }));
|
|
25
25
|
};
|
|
@@ -47,6 +47,11 @@
|
|
|
47
47
|
<body>
|
|
48
48
|
<div id="app"></div>
|
|
49
49
|
|
|
50
|
+
<%# Deployment switches, before the app so the view reads them as it mounts. They ride in the page, not the
|
|
51
|
+
bundle, so a server serving both settings still builds the browser bundle once. -%>
|
|
52
|
+
<script id="plitzi-view-settings">
|
|
53
|
+
window.__PLITZI_VIEW__ = <%- settings %>;
|
|
54
|
+
</script>
|
|
50
55
|
<script>
|
|
51
56
|
<%- app %>
|
|
52
57
|
</script>
|
|
@@ -74,7 +74,7 @@ var serveMcp = async (raw, res, server) => {
|
|
|
74
74
|
await transport.close();
|
|
75
75
|
}
|
|
76
76
|
};
|
|
77
|
-
var handleMcp = (raw, res, req, adapters, options = {}) => serveMcp(raw, res, createMcpServer({
|
|
77
|
+
var handleMcp = async (raw, res, req, adapters, options = {}) => serveMcp(raw, res, await createMcpServer({
|
|
78
78
|
adapters,
|
|
79
79
|
getSpaceId: () => adapters.getSpaceId?.(req) ?? Promise.resolve(void 0),
|
|
80
80
|
...options
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
//#region src/modules/mcp/helpers/guide.ts
|
|
2
2
|
var serverInstructions = "Plitzi AI server: read-then-write editing of a Plitzi space. Reads follow a filesystem model — list cheap, read one item in detail on demand; never fetch a whole tree you do not need. Workflow: (1) read plitzi://primer/{env} once — it bundles the guide, types, css-properties and page/definition/variable summaries in a single call; (2) plitzi_search with include:\"detail\" to jump to elements — each hit then carries its uri, stateVersion AND full style/resolvedStyle, so an edit needs no per-element read; open a page skeleton or element only when you need its tree/detail (the skeleton already lists the style classes of each node, and plitzi_read fetches many uris at once); (3) plitzi_apply with dryRun to preview a batch; (4) plitzi_apply to persist, passing expectedResourceVersions to guard against concurrent edits — apply and search both hand back the versions you need for the next edit. Use patchElement / patchDefinition to change only some props / CSS (the upsert variants replace them all). An element read (and search include:\"detail\") inlines the CSS of the definitions it attaches under resolvedStyle, so you rarely need a separate definition read. Refs accept a semantic idRef ([A-Za-z0-9_-] starting with a letter, unique, chosen by you) or the raw id — the idRef is ALSO the runtime wiring key, so a provider source is `<type>_<idRef>.<field>`, visible to the provider’s DESCENDANTS only (bind inside its subtree). CSS is plain kebab-case — write shorthands freely (`border: 1px solid red`, `padding: 8px 16px`, `font: bold 16px/1.5 Arial`), they are expanded to longhands for you and STORED that way, so read-back shows the longhands; style vars are var(--name), schema vars are {{name}}. READERS — do not confuse them: MCP *resources* are the browsable catalog (list them, or open one by URI); plitzi_search FINDS refs by label/type/attribute; plitzi_read BATCH-fetches URIs you already hold. Reach for search/read to work; browse resources to discover. Elements also carry applied style variants + visibility (initialState), data bindings and interaction flows: edit them with patchElement (initialState), upsertBinding/patchBinding/deleteBinding, and upsertInteractionFlow/patchInteractionNode/deleteInteraction. An element read shows all three plus availableVariants (which variant each of its classes offers). Separately, to SHOW the user a small self-contained widget (offline, no space or backend) instead of editing the space — a card, hero, pricing table, a visual answer — use plitzi_render; read plitzi://render/guide for it.";
|
|
3
|
+
var widgetsOnlyInstructions = "Plitzi widget server (widgets-only connection). This connection carries NO Plitzi space: nothing can be read or edited in one, and the space tools are not offered here — do not look for them. What it does do is build self-contained UI widgets fully offline, with no backend, account or setup: call plitzi_render to SHOW the user a real rendered layout (card, hero, pricing table, checklist, form, gallery) instead of describing one. Read plitzi://render/guide first — the element/prop table, the style model and a worked example; plitzi://render/types lists every element type you can author (plitzi_read fetches both). To change a widget you already rendered, call plitzi_render again with patch:true and its renderId. If the user wants to edit their real Plitzi space from here, that is a reconnection they make: the integration must be re-authorized and granted a space (the consent screen lists theirs) — you cannot do it from this connection, and no retry will change it.";
|
|
3
4
|
var guideQuickstart = `# Plitzi AI MCP — quickstart
|
|
4
5
|
This is the condensed guide; read \`plitzi://guide\` for the full reference (every resource, op and example).
|
|
5
6
|
|
|
@@ -463,4 +464,4 @@ Space-level configuration lives in \`plitzi://settings/{env}\` and is edited wit
|
|
|
463
464
|
never omit it "to save a call".
|
|
464
465
|
`;
|
|
465
466
|
//#endregion
|
|
466
|
-
export { guideQuickstart, guideText, serverInstructions };
|
|
467
|
+
export { guideQuickstart, guideText, serverInstructions, widgetsOnlyInstructions };
|
|
@@ -280,9 +280,21 @@ var pageRefOfElement = (schema, el) => {
|
|
|
280
280
|
/** Total number of descendant elements under a subtree (excluding the root). */
|
|
281
281
|
var descendantCount = (schema, rootId) => descendantIds(schema, rootId).length;
|
|
282
282
|
var emptySpaceMessage = "Space data not available";
|
|
283
|
+
/** The code a space-dependent answer carries when the connection reaches no space, so both the agent and a host UI
|
|
284
|
+
* can tell "this connection cannot do that" from "the call failed". */
|
|
285
|
+
var noSpaceError = "NO_SPACE_ATTACHED";
|
|
283
286
|
var unauthorizedSpaceMessage = "This connection has no space attached (a guest or widgets-only grant, or a token that carries no space), so NOTHING in a space can be read or edited — every other space tool will fail the same way, do not retry them. Use plitzi_render instead: it builds a self-contained widget offline, with no space, backend or account (read plitzi://render/guide). To edit a real space, the user must reconnect the integration and grant access to one.";
|
|
287
|
+
/** Raised when a space-dependent operation runs on a connection that resolves no spaceId. A type of its own so the
|
|
288
|
+
* host answers it as a STATE of the connection — a plain result the agent reads — instead of letting it surface as
|
|
289
|
+
* a failed call, which hosts render to the user as "cannot connect to this server". */
|
|
290
|
+
var NoSpaceError = class extends Error {
|
|
291
|
+
constructor() {
|
|
292
|
+
super(unauthorizedSpaceMessage);
|
|
293
|
+
this.name = "NoSpaceError";
|
|
294
|
+
}
|
|
295
|
+
};
|
|
284
296
|
var generateObjectId = () => {
|
|
285
297
|
return `${Math.floor(Date.now() / 1e3).toString(16).padStart(8, "0")}${Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
|
|
286
298
|
};
|
|
287
299
|
//#endregion
|
|
288
|
-
export { cloneSpace, descendantCount, descendantIds, elementById, elementRefOf, emptySpace, emptySpaceMessage, findElementByRef, findFolderByRef, findPageByRef, folderAncestorIds, generateObjectId, getPageElements, indexAddElement, indexAddPage, indexInvalidateDetails, indexReRefElement, indexReRefPage, indexRemoveElements, indexRemovePage, isPageElement, nameOf, orderedChildren, pageFoldersOf, pageRefOf, pageRefOfElement, resolveRef, routeParamNames, slugRouteParams, slugify, sortFolders, spaceIndex, strOr, unauthorizedSpaceMessage };
|
|
300
|
+
export { NoSpaceError, cloneSpace, descendantCount, descendantIds, elementById, elementRefOf, emptySpace, emptySpaceMessage, findElementByRef, findFolderByRef, findPageByRef, folderAncestorIds, generateObjectId, getPageElements, indexAddElement, indexAddPage, indexInvalidateDetails, indexReRefElement, indexReRefPage, indexRemoveElements, indexRemovePage, isPageElement, nameOf, noSpaceError, orderedChildren, pageFoldersOf, pageRefOf, pageRefOfElement, resolveRef, routeParamNames, slugRouteParams, slugify, sortFolders, spaceIndex, strOr, unauthorizedSpaceMessage };
|
|
@@ -2,13 +2,17 @@ import { cssProperties, cssShorthands } from "../catalogs/cssCatalog/index.js";
|
|
|
2
2
|
import { guideText } from "../helpers/guide.js";
|
|
3
3
|
import { resourceErrorMessage } from "./canonical.js";
|
|
4
4
|
import { envelope, jsonContents } from "./envelope.js";
|
|
5
|
-
import { readResource } from "./router.js";
|
|
6
5
|
import { registerRenderResources } from "./renderGuide.js";
|
|
6
|
+
import { readResource } from "./router.js";
|
|
7
7
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
8
|
//#region src/modules/mcp/resources/register.ts
|
|
9
9
|
/** Register every resource on the MCP server: fixed listings plus templated per-item reads. The space is
|
|
10
|
-
* loaded lazily via getSpace, so listing resources never touches the store — only reading one does.
|
|
11
|
-
|
|
10
|
+
* loaded lazily via getSpace, so listing resources never touches the store — only reading one does.
|
|
11
|
+
*
|
|
12
|
+
* `hasSpace` is false when the connection reaches no space (a guest / widgets-only grant): the space-dependent
|
|
13
|
+
* families are then not registered at all, so the catalog the agent browses holds only what it can actually
|
|
14
|
+
* open — no listing that answers every read with the same refusal. */
|
|
15
|
+
var registerResources = (server, getSpace, env, log, hasSpace) => {
|
|
12
16
|
const emit = async (uri) => {
|
|
13
17
|
const start = performance.now();
|
|
14
18
|
try {
|
|
@@ -43,6 +47,7 @@ var registerResources = (server, getSpace, env, log) => {
|
|
|
43
47
|
shorthands: cssShorthands
|
|
44
48
|
}))));
|
|
45
49
|
registerRenderResources(server, log);
|
|
50
|
+
if (!hasSpace) return;
|
|
46
51
|
const fixed = [
|
|
47
52
|
[
|
|
48
53
|
"Primer",
|
|
@@ -10,6 +10,8 @@ var RENDER_TYPE_CATEGORIES = /* @__PURE__ */ new Set([
|
|
|
10
10
|
"form",
|
|
11
11
|
"provider"
|
|
12
12
|
]);
|
|
13
|
+
var RENDER_RAW_HTML_TYPE = "blockHtml";
|
|
14
|
+
var rawHtmlDescription = "Renders a raw HTML string (props.content) — in a widget, the way to draw an inline <svg>: a logo, an icon, a sparkline, a decorative shape. Use fill/stroke \"currentColor\" so it takes the host theme. Markup only: <script>, javascript: URLs and inline on* handlers are rejected.";
|
|
13
15
|
var renderTypesNote = "The built-in element types you can put in a plitzi_render widget, grouped by category. Pick by `description`. For the props of each type and the full authoring model, read plitzi://render/guide. Types that need a backend (providers) or a plugin are omitted because the widget renders offline.";
|
|
14
16
|
var renderTypes = () => {
|
|
15
17
|
const types = {};
|
|
@@ -18,6 +20,12 @@ var renderTypes = () => {
|
|
|
18
20
|
category: info.category,
|
|
19
21
|
description: info.description
|
|
20
22
|
};
|
|
23
|
+
const rawHtml = BUILTIN_COMPONENTS[RENDER_RAW_HTML_TYPE];
|
|
24
|
+
if (rawHtml) types[RENDER_RAW_HTML_TYPE] = {
|
|
25
|
+
label: rawHtml.label,
|
|
26
|
+
category: rawHtml.category,
|
|
27
|
+
description: rawHtmlDescription
|
|
28
|
+
};
|
|
21
29
|
return {
|
|
22
30
|
note: renderTypesNote,
|
|
23
31
|
types
|
|
@@ -161,10 +169,10 @@ and \`patchDefinition\` still changes only some CSS of one class.
|
|
|
161
169
|
- Lay containers out with flexbox or grid — pick the direction on purpose, see **Fit the panel** below.
|
|
162
170
|
- **You are not styling from zero.** Each type lands on the page with CSS you did not write, and it is the usual
|
|
163
171
|
reason a widget does not look like the definitions say:
|
|
164
|
-
- The per-type
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
- The per-type CSS the SDK ships imposes **no minimum size**: a rail, a divider, a dot or a narrow cell is
|
|
173
|
+
exactly as small as you make it, and no \`"min-width": "0"\` escape hatch is needed. The flip side is that an
|
|
174
|
+
element with no content and no size takes no space at all — give a spacer its own \`height\`. (The 50px box
|
|
175
|
+
empty containers show on the builder canvas is a drop target, scoped to it; it never reaches a widget.)
|
|
168
176
|
- The BROWSER's own defaults for the rest: \`heading\` keeps its UA font-size and ~0.67em top/bottom margins,
|
|
169
177
|
\`paragraph\` ~1em margins, \`list\` a 40px \`padding-left\`, \`button\` its native chrome, \`image\` its intrinsic
|
|
170
178
|
size, \`link\` its own colour and underline. In a compact widget set these explicitly — usually
|
|
@@ -256,24 +264,61 @@ The host publishes its palette as CSS variables on the page, so use them for eve
|
|
|
256
264
|
| \`video\` | embedded video | \`props.src\` |
|
|
257
265
|
| \`list\` / \`listItem\` | \`<ul>\` / \`<li>\` | nesting only |
|
|
258
266
|
| \`markdown\` | rendered markdown | \`props.content\` |
|
|
267
|
+
| \`blockHtml\` | a raw HTML string, as written | \`props.content\` — how you draw an inline \`<svg>\`, see below |
|
|
259
268
|
|
|
260
269
|
\`subType\` is an element-level field (not a prop). Guessing is safe: an unknown prop for a type comes back as a
|
|
261
270
|
**warning naming the right one**, not an error. This table covers the everyday types; the resource
|
|
262
271
|
**plitzi://render/types** lists every built-in type you can use (with descriptions) — read it when you need one
|
|
263
272
|
that is not here (lists, tabs, dialogs, forms, icons…).
|
|
264
273
|
|
|
265
|
-
\`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic
|
|
266
|
-
|
|
274
|
+
\`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic (a
|
|
275
|
+
base64 raster) — both render with no extra setup. For a **vector** graphic do not encode a \`data:\` URI: draw it
|
|
276
|
+
inline, as below.
|
|
277
|
+
|
|
278
|
+
## Draw with inline SVG
|
|
279
|
+
|
|
280
|
+
A widget can draw its own graphics, and it is often the difference between a plain block of text and something
|
|
281
|
+
worth showing: a brand mark, a sparkline or donut, a badge, an empty-state glyph, a wave or blob behind a header,
|
|
282
|
+
an arrow between two steps. Put the SVG **inline** in a \`blockHtml\` element — its \`props.content\` is rendered as
|
|
283
|
+
written, so the markup needs no escaping, and the element is styled and laid out like any other box.
|
|
284
|
+
|
|
285
|
+
\`\`\`json
|
|
286
|
+
{
|
|
287
|
+
"operations": [
|
|
288
|
+
{ "type": "upsertDefinitions", "definitions": {
|
|
289
|
+
"mark": { "desktop": { "display": "flex", "width": "32px", "height": "32px", "color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))" } }
|
|
290
|
+
} },
|
|
291
|
+
{ "type": "upsertElement", "pageRef": "render", "element": {
|
|
292
|
+
"ref": "trend", "type": "blockHtml", "style": { "base": ["mark"] },
|
|
293
|
+
"props": { "content": "<svg viewBox='0 0 24 24' width='100%' height='100%' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round'><path d='M3 17l6-6 4 4 8-8'/><path d='M21 7v6h-6'/></svg>" }
|
|
294
|
+
} }
|
|
295
|
+
]
|
|
296
|
+
}
|
|
297
|
+
\`\`\`
|
|
298
|
+
- **Size from the class, not the drawing**: keep a \`viewBox\` and set \`width\`/\`height\` to \`100%\`, then the
|
|
299
|
+
\`blockHtml\`'s own \`width\`/\`height\` decide how big it renders — one drawing, any size.
|
|
300
|
+
- **Colour with \`currentColor\`** (\`fill\` and/or \`stroke\`) and set \`color\` on the class from a host variable. A hex
|
|
301
|
+
buried inside the path is the same mistake as a hardcoded light palette: it cannot follow the theme.
|
|
302
|
+
- **Single quotes inside the markup** keep the JSON free of \`\\"\` escapes, as above.
|
|
303
|
+
- **Markup only.** \`<script>\`, \`javascript:\` URLs and inline \`on*\` handlers are rejected with an error — a widget
|
|
304
|
+
runs inside the host's UI. Animate with CSS (a \`states\` block, a \`transition\`) and wire behaviour with
|
|
305
|
+
interaction flows.
|
|
306
|
+
|
|
307
|
+
**The budget.** Every path is text you write, so an SVG earns its place only while it stays small: a handful of
|
|
308
|
+
paths, a few hundred characters, drawn ONCE and reused (same class, same content, or a \`repeatElement\` row) rather
|
|
309
|
+
than redrawn per card. Skip it entirely when something cheaper does the job — a \`fontAwesome\` icon, an emoji in a
|
|
310
|
+
\`text\`, an \`https\` image the user gave you, a CSS gradient or a \`border-radius\` shape. And a full illustration or
|
|
311
|
+
a photo-real scene (a castle, a skyline, a detailed mascot) is still the wrong trade: it costs more than the entire
|
|
312
|
+
rest of the widget and renders worse than the image it is imitating.
|
|
267
313
|
|
|
268
314
|
## Keep the call small
|
|
269
315
|
|
|
270
316
|
Everything in \`operations\` is text you write, and a widget that takes two calls because the first ran long is a
|
|
271
317
|
widget the user waits twice for. Two habits pay for themselves:
|
|
272
318
|
|
|
273
|
-
- **
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
the decoration. A small self-contained \`data:\` SVG is fine for a **simple** shape (a check, an arrow, a dot).
|
|
319
|
+
- **Keep a drawing to a drawing.** Inline SVG is allowed and often worth it (see above), but a graphic that runs
|
|
320
|
+
longer than the widget around it has stopped paying for itself: a handful of paths, drawn once and reused rather
|
|
321
|
+
than redrawn per card, and an \`https\` image, a flat colour or a two-stop \`linear-gradient\` for anything bigger.
|
|
277
322
|
- **One class per look, not per property.** Classes like \`tone-blue\` + \`tone-blue-text\` + \`tone-blue-bg\` for the
|
|
278
323
|
same card triple the declarations and the attachments. Put everything the look needs in one class, add a second
|
|
279
324
|
only for the part that genuinely varies between siblings (a colour), and reuse it — the whole point of a class.
|
|
@@ -355,6 +400,13 @@ connects that source to a descendant's field.
|
|
|
355
400
|
The tool returns \`rendered: false\` with \`errors: [{ path, message, hint }]\`. Read the hint, fix that one op, and
|
|
356
401
|
retry — you never lose the rest of the batch.
|
|
357
402
|
`;
|
|
403
|
+
/** The same two documents through the READ path (the router, hence plitzi_read), not only as MCP resources: an
|
|
404
|
+
* agent that reaches for the batch-read tool — the reflex the guide itself teaches — must not be told its own
|
|
405
|
+
* authoring guide does not exist. Space-independent, like the core singletons. */
|
|
406
|
+
var readRenderResource = (uri) => {
|
|
407
|
+
if (uri === "plitzi://render/guide") return envelope(renderGuideText);
|
|
408
|
+
if (uri === "plitzi://render/types") return envelope(renderTypes());
|
|
409
|
+
};
|
|
358
410
|
var registerRenderResources = (server, log) => {
|
|
359
411
|
server.registerResource("Render guide", RENDER_GUIDE_URI, {
|
|
360
412
|
description: "How to author a plitzi_render widget: operations, element types, styling, examples.",
|
|
@@ -380,4 +432,4 @@ var registerRenderResources = (server, log) => {
|
|
|
380
432
|
});
|
|
381
433
|
};
|
|
382
434
|
//#endregion
|
|
383
|
-
export { RENDER_GUIDE_URI, RENDER_TYPES_URI, registerRenderResources };
|
|
435
|
+
export { RENDER_GUIDE_URI, RENDER_TYPES_URI, readRenderResource, registerRenderResources };
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import { emptySpace } from "../helpers/space.js";
|
|
2
|
+
import "../helpers/uris.js";
|
|
1
3
|
import { canonicalUri } from "./canonical.js";
|
|
2
4
|
import { readCoreResource } from "./core.js";
|
|
3
5
|
import { readPrimerResource } from "./primer.js";
|
|
6
|
+
import { readRenderResource } from "./renderGuide.js";
|
|
4
7
|
import { readSchemaResource } from "./schema.js";
|
|
5
8
|
import { readStyleResource } from "./style.js";
|
|
6
9
|
//#region src/modules/mcp/resources/router.ts
|
|
7
10
|
var resolvers = [
|
|
8
11
|
(space, _env, uri) => readCoreResource(space, uri),
|
|
12
|
+
(_space, _env, uri) => readRenderResource(uri),
|
|
9
13
|
readPrimerResource,
|
|
10
14
|
readSchemaResource,
|
|
11
15
|
readStyleResource
|
|
@@ -19,7 +23,15 @@ var readResource = (space, env, rawUri) => {
|
|
|
19
23
|
}
|
|
20
24
|
return null;
|
|
21
25
|
};
|
|
26
|
+
/** What a connection carrying NO space (a guest / widgets-only grant) can still read: the space-independent
|
|
27
|
+
* singletons and the render docs. Undefined for everything else, so the caller says the connection has no space
|
|
28
|
+
* instead of answering out of an empty one — an empty page list would read as "this space has no pages". */
|
|
29
|
+
var readPublicResource = (env, rawUri) => {
|
|
30
|
+
const uri = canonicalUri(env, rawUri);
|
|
31
|
+
if (uri === "plitzi://types") return;
|
|
32
|
+
return readRenderResource(uri) ?? readCoreResource(emptySpace(), uri);
|
|
33
|
+
};
|
|
22
34
|
/** Current version of a resource, for optimistic-concurrency checks. Null when the URI is unknown. */
|
|
23
35
|
var resourceVersion = (space, env, uri) => readResource(space, env, uri)?.stateVersion ?? null;
|
|
24
36
|
//#endregion
|
|
25
|
-
export { readResource, resourceVersion };
|
|
37
|
+
export { readPublicResource, readResource, resourceVersion };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registerApps } from "./apps/index.js";
|
|
2
|
-
import { emptySpace, emptySpaceMessage,
|
|
3
|
-
import { serverInstructions } from "./helpers/guide.js";
|
|
2
|
+
import { NoSpaceError, emptySpace, emptySpaceMessage, noSpaceError } from "./helpers/space.js";
|
|
3
|
+
import { serverInstructions, widgetsOnlyInstructions } from "./helpers/guide.js";
|
|
4
4
|
import { createMcpLog } from "./helpers/log.js";
|
|
5
5
|
import { registerResources } from "./resources/register.js";
|
|
6
6
|
import { tools } from "./tools/index.js";
|
|
@@ -13,19 +13,20 @@ var asText = (data) => ({ content: [{
|
|
|
13
13
|
type: "text",
|
|
14
14
|
text: JSON.stringify(data)
|
|
15
15
|
}] });
|
|
16
|
-
var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) => {
|
|
16
|
+
var createMcpServer = async ({ adapters, getSpaceId, preview, screenshot, logger, renderStreaming = true }) => {
|
|
17
17
|
const log = createMcpLog(logger);
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const spaceId = await getSpaceId().catch(() => void 0);
|
|
19
|
+
const hasSpace = spaceId !== void 0;
|
|
20
|
+
const requireSpaceId = () => {
|
|
21
|
+
if (spaceId === void 0) throw new NoSpaceError();
|
|
22
|
+
return spaceId;
|
|
23
|
+
};
|
|
23
24
|
const loadSpace = async () => {
|
|
24
|
-
const
|
|
25
|
+
const id = requireSpaceId();
|
|
25
26
|
const [schema, style, catalog] = await Promise.all([
|
|
26
|
-
adapters.getSchema?.(
|
|
27
|
-
adapters.getStyle?.(
|
|
28
|
-
adapters.getComponentCatalog?.(
|
|
27
|
+
adapters.getSchema?.(id, MCP_ENV),
|
|
28
|
+
adapters.getStyle?.(id, MCP_ENV),
|
|
29
|
+
adapters.getComponentCatalog?.(id, MCP_ENV).catch(() => void 0)
|
|
29
30
|
]);
|
|
30
31
|
if (!schema || !style) throw new Error(emptySpaceMessage);
|
|
31
32
|
return {
|
|
@@ -36,22 +37,22 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
|
|
|
36
37
|
};
|
|
37
38
|
const { saveSchema, saveStyle } = adapters;
|
|
38
39
|
const persisters = {
|
|
39
|
-
schema: saveSchema ?
|
|
40
|
-
style: saveStyle ?
|
|
40
|
+
schema: saveSchema ? (schema) => saveSchema(requireSpaceId(), MCP_ENV, schema) : void 0,
|
|
41
|
+
style: saveStyle ? (style) => saveStyle(requireSpaceId(), MCP_ENV, style) : void 0
|
|
41
42
|
};
|
|
42
43
|
let spacePromise;
|
|
43
44
|
const getSpace = () => spacePromise ??= loadSpace();
|
|
44
45
|
const server = new McpServer({
|
|
45
46
|
name: "plitzi-mcp",
|
|
46
|
-
version: "0.32.
|
|
47
|
-
}, { instructions: serverInstructions });
|
|
48
|
-
registerResources(server, getSpace, MCP_ENV, log);
|
|
49
|
-
registerApps(server);
|
|
47
|
+
version: "0.32.20"
|
|
48
|
+
}, { instructions: hasSpace ? serverInstructions : widgetsOnlyInstructions });
|
|
49
|
+
registerResources(server, getSpace, MCP_ENV, log, hasSpace);
|
|
50
|
+
registerApps(server, { streaming: renderStreaming });
|
|
50
51
|
const toolContext = async () => ({
|
|
51
52
|
space: await getSpace(),
|
|
52
53
|
env: MCP_ENV,
|
|
53
54
|
persisters,
|
|
54
|
-
spaceId:
|
|
55
|
+
spaceId: requireSpaceId(),
|
|
55
56
|
preview,
|
|
56
57
|
screenshot
|
|
57
58
|
});
|
|
@@ -62,16 +63,27 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
|
|
|
62
63
|
preview,
|
|
63
64
|
screenshot
|
|
64
65
|
});
|
|
66
|
+
const behaviorOf = (tool) => {
|
|
67
|
+
if (tool.spaceless) return (args) => tool.execute(args, spacelessContext());
|
|
68
|
+
if (hasSpace) return async (args) => tool.execute(args, await toolContext());
|
|
69
|
+
return tool.executePublic ? (args) => tool.executePublic?.(args, MCP_ENV) : void 0;
|
|
70
|
+
};
|
|
65
71
|
for (const tool of tools) {
|
|
66
72
|
if (tool.requires === "screenshot" && !screenshot) continue;
|
|
73
|
+
const behavior = behaviorOf(tool);
|
|
74
|
+
if (!behavior) continue;
|
|
67
75
|
const run = async (args) => {
|
|
68
76
|
const start = performance.now();
|
|
69
77
|
try {
|
|
70
|
-
const result = await
|
|
78
|
+
const result = await behavior(args);
|
|
71
79
|
log.toolCall(tool.name, args, performance.now() - start);
|
|
72
80
|
return isCallToolResult(result) ? result : asText(result);
|
|
73
81
|
} catch (error) {
|
|
74
82
|
log.toolCall(tool.name, args, performance.now() - start, error);
|
|
83
|
+
if (error instanceof NoSpaceError) return asText({
|
|
84
|
+
error: noSpaceError,
|
|
85
|
+
message: error.message
|
|
86
|
+
});
|
|
75
87
|
throw error;
|
|
76
88
|
}
|
|
77
89
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { noSpaceError, unauthorizedSpaceMessage } from "../helpers/space.js";
|
|
1
2
|
import { resourceErrorMessage } from "../resources/canonical.js";
|
|
2
|
-
import {
|
|
3
|
+
import { RENDER_GUIDE_URI, RENDER_TYPES_URI } from "../resources/renderGuide.js";
|
|
4
|
+
import { readPublicResource, readResource } from "../resources/router.js";
|
|
3
5
|
import { defineTool } from "./shared/tool.js";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
//#region src/modules/mcp/tools/read.ts
|
|
@@ -21,13 +23,29 @@ var read = (input, space, env) => {
|
|
|
21
23
|
};
|
|
22
24
|
}) };
|
|
23
25
|
};
|
|
26
|
+
var publicHint = `Readable on this connection: ${RENDER_GUIDE_URI}, ${RENDER_TYPES_URI}, plitzi://guide, plitzi://css-properties. Everything else lives in a space this connection has none of.`;
|
|
27
|
+
var readPublic = (input, env) => ({ results: input.uris.map((uri) => {
|
|
28
|
+
const found = readPublicResource(env, uri);
|
|
29
|
+
if (found) return {
|
|
30
|
+
uri,
|
|
31
|
+
stateVersion: found.stateVersion,
|
|
32
|
+
data: found.data
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
uri,
|
|
36
|
+
error: noSpaceError,
|
|
37
|
+
message: unauthorizedSpaceMessage,
|
|
38
|
+
hint: publicHint
|
|
39
|
+
};
|
|
40
|
+
}) });
|
|
24
41
|
var readTool = defineTool({
|
|
25
42
|
name: "plitzi_read",
|
|
26
43
|
title: "Batch read",
|
|
27
44
|
description: "Batch-fetch resource content for URIs you ALREADY HOLD (from plitzi_search or a write response) — the tool form of opening MCP resources, for many at once. Pass an array of URIs (pages, elements, definitions, variables) and get them all in one call. NOT for finding things: to locate an element by label/type use plitzi_search; to browse what exists, list the plitzi:// resources. Each result is { uri, stateVersion, data } or a teachable error, so one bad URI never fails the batch. Never hand-build a URI to guess your way to an element — search for it.",
|
|
28
45
|
inputShape: readShape,
|
|
29
46
|
access: "read",
|
|
30
|
-
run: (input, ctx) => read(input, ctx.space, ctx.env)
|
|
47
|
+
run: (input, ctx) => read(input, ctx.space, ctx.env),
|
|
48
|
+
runPublic: readPublic
|
|
31
49
|
});
|
|
32
50
|
//#endregion
|
|
33
|
-
export { read, readShape, readTool };
|
|
51
|
+
export { read, readPublic, readShape, readTool };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { iconFontCss } from "../apps/render/styles.js";
|
|
1
2
|
import { RENDER_APP_URI } from "../apps/render/index.js";
|
|
2
3
|
import { emptySpace } from "../helpers/space.js";
|
|
3
4
|
import { operations } from "./operations/index.js";
|
|
@@ -48,7 +49,7 @@ var render = (input) => {
|
|
|
48
49
|
errors: expansion.errors
|
|
49
50
|
};
|
|
50
51
|
const ops = expansion.operations;
|
|
51
|
-
const validation = validateOperations(space, ops);
|
|
52
|
+
const validation = validateOperations(space, ops, "widget");
|
|
52
53
|
if (!validation.valid) return {
|
|
53
54
|
rendered: false,
|
|
54
55
|
errors: validation.errors,
|
|
@@ -136,6 +137,8 @@ var toPatchResult = (ops, renderId) => {
|
|
|
136
137
|
};
|
|
137
138
|
};
|
|
138
139
|
var newRenderId = () => `r${randomUUID().slice(0, 8)}`;
|
|
140
|
+
var ICON_TYPE = "fontAwesome";
|
|
141
|
+
var drawsIcons = (offlineData) => Object.values(offlineData.schema.flat).some((element) => element.definition.type === ICON_TYPE);
|
|
139
142
|
var toRenderResult = (res, renderId) => {
|
|
140
143
|
if (!res.rendered) return { content: [{
|
|
141
144
|
type: "text",
|
|
@@ -160,14 +163,15 @@ var toRenderResult = (res, renderId) => {
|
|
|
160
163
|
structuredContent: {
|
|
161
164
|
...summary,
|
|
162
165
|
offlineData: res.offlineData,
|
|
163
|
-
operations: res.operations
|
|
166
|
+
operations: res.operations,
|
|
167
|
+
...drawsIcons(res.offlineData) ? { iconCss: iconFontCss() } : {}
|
|
164
168
|
}
|
|
165
169
|
};
|
|
166
170
|
};
|
|
167
171
|
var renderTool = defineTool({
|
|
168
172
|
name: "plitzi_render",
|
|
169
173
|
title: "Render widget",
|
|
170
|
-
description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n1b. REPEATS — the moment two siblings share a shape and differ only in data (list, steps, cards, table, timeline), do NOT copy-paste them: use repeatElement { pageRef, ref (wrapper), style, template, items }. The template is written once with {{item.field}} placeholders and rendered per row; refs come out numbered (\"step-1\", \"step-2\"…). A list INSIDE each row (days with their own steps) is the same op: give the wrapping node repeat:{ items:\"{{item.<list>}}\", template:… } and put the sub-rows in the row data.\n2. STYLE — declare ALL the classes in ONE upsertDefinitions { definitions: { \"<class>\": { desktop:{ …CSS in kebab-case… } }, … } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid. Keep the call small: one class per look
|
|
174
|
+
description: "Show the user a real, rendered UI widget instead of describing one — cards, hero sections, pricing tables, forms, menus, checklists, profiles, galleries. It runs the Plitzi SDK fully offline: no backend, account, or setup. Reach for it whenever a visual layout beats prose: the user asks you to design/build/show something, OR your answer is naturally visual (a recipe → a card, a comparison → a table, steps → a checklist). Prefer showing over telling.\n\nAuthor the widget as an ordered list of `operations` that build an element tree under the pre-seeded root page \"render\". Three rules:\n1. STRUCTURE — one upsertElement builds the whole tree: set pageRef:\"render\" and give element a nested `children` array. Each element is { ref (unique), type, subType?, props?, style?, children? }; children render in order. (To attach to something you already made, use a top-level parentRef:\"<existing ref>\" instead.)\n1b. REPEATS — the moment two siblings share a shape and differ only in data (list, steps, cards, table, timeline), do NOT copy-paste them: use repeatElement { pageRef, ref (wrapper), style, template, items }. The template is written once with {{item.field}} placeholders and rendered per row; refs come out numbered (\"step-1\", \"step-2\"…). A list INSIDE each row (days with their own steps) is the same op: give the wrapping node repeat:{ items:\"{{item.<list>}}\", template:… } and put the sub-rows in the row data.\n2. STYLE — declare ALL the classes in ONE upsertDefinitions { definitions: { \"<class>\": { desktop:{ …CSS in kebab-case… } }, … } }, then attach via the element style:{ base:[\"<class ref>\"] }. Lay containers out with flex/grid. Keep the call small: one class per look, not per property.\n2a. GRAPHICS — a logo, a sparkline, a badge, a decorative shape: draw it as an INLINE <svg> in a blockHtml element (props.content), keeping a viewBox with width/height \"100%\" so its class sizes it, and fill/stroke \"currentColor\" so it follows the host theme. Budget it: a handful of paths, drawn once and reused, never a data: URI and never a full illustration — a photo-real scene costs more than the whole widget, so use an https image, a flat colour or a gradient for that. Markup only: scripts and on* handlers are rejected.\n2b. LAYOUT — it renders in a side panel, so width is free and HEIGHT is scarce. Plain containers stack children vertically, which is the tall half-empty default to avoid: put peers (metrics, plans, options, image + text) in a wrapping row — display:flex, flex-direction:row, flex-wrap:wrap, children flex-grow:\"1\" + flex-basis:\"0%\" + min-width — or a grid with grid-template-columns:\"repeat(auto-fit, minmax(160px, 1fr))\". Keep padding 12-16px and gap 8-12px, and let the outer container fill the panel. Stack only what reads in order (heading over paragraph, forms, steps, prose). Nothing carries a minimum size, so an element with no content and no size takes none — give a spacer or a rail its own height/width — while heading/paragraph do keep the margins the browser gives them (zero them, space with the parent gap).\n2c. THEME — it is embedded in the host UI, which MAY BE DARK, so never hardcode a light palette. Take colours from the host variables with a light-dark() fallback — background-color:\"var(--color-background-secondary, light-dark(#ffffff, #1f2430))\", color:\"var(--color-text-primary, light-dark(#0f172a, #e8eaed))\", border-color:\"var(--color-border-primary, light-dark(#e2e8f0, #333a48))\" — and always set `color` wherever you set `background-color` (a brand accent states its own text colour too).\n3. CONTENT — visible copy goes in props.content (text, heading, paragraph, button); heading level is the element subType (\"h1\"..\"h6\"); image/video take props.src. An unknown prop comes back as a warning naming the right one.\n\nCommon types: container, heading, paragraph, text, button, link, image, video, list, listItem, markdown (plitzi://render/types lists every built-in type with descriptions). Widgets can also be data-driven and interactive — an apiContainer fetches at runtime, upsertBinding wires data into elements, and upsertInteractionFlow makes them react to clicks (see the guide).\nREAD the resource plitzi://render/guide first — it has the element/prop table, the style model and a full worked example, and following it is the difference between a widget that renders and repeated failed calls.\nITERATING — to change a widget you already rendered, do NOT rebuild it: call again with patch:true, the `renderId` that render answered with, and ONLY the operations that differ (patchDefinition, patchElement, deleteElement…). The widget merges them and reports back what it applied; address rows by the refs you already know. Patch ONLY to modify that widget: a different subject or a different kind of widget is a fresh render, without patch — a patch is merged into the previous batch, so patching a new idea leaves you with both.\nReturns a compact summary including the renderId (the widget itself is shown to the user); on failure it returns teachable errors (path + hint) — read them and retry.",
|
|
171
175
|
inputShape: renderShape,
|
|
172
176
|
access: "read",
|
|
173
177
|
spaceless: true,
|
|
@@ -13,16 +13,21 @@ var imageResult = (images, meta) => ({ content: [{
|
|
|
13
13
|
/** Author a tool: give it its metadata, its input shape and a typed `run`. The returned descriptor parses the
|
|
14
14
|
* raw args against the shape before handing them to `run`, so `run` is fully typed and no cast is needed.
|
|
15
15
|
* Adding a tool is: call defineTool in its own file and append it to the `tools` registry. */
|
|
16
|
-
var defineTool = (spec) =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
16
|
+
var defineTool = (spec) => {
|
|
17
|
+
const { runPublic } = spec;
|
|
18
|
+
const parse = (args) => z.object(spec.inputShape).parse(args);
|
|
19
|
+
return {
|
|
20
|
+
name: spec.name,
|
|
21
|
+
title: spec.title,
|
|
22
|
+
description: spec.description,
|
|
23
|
+
inputShape: spec.inputShape,
|
|
24
|
+
access: spec.access,
|
|
25
|
+
requires: spec.requires,
|
|
26
|
+
spaceless: spec.spaceless,
|
|
27
|
+
ui: spec.ui,
|
|
28
|
+
execute: (args, ctx) => spec.run(parse(args), ctx),
|
|
29
|
+
...runPublic ? { executePublic: (args, env) => runPublic(parse(args), env) } : {}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
27
32
|
//#endregion
|
|
28
33
|
export { defineTool, imageResult };
|
|
@@ -32,7 +32,17 @@ var checkVariantApplication = (initialState, path, ctx) => {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
+
var EXECUTABLE_MARKUP = /<script\b|\son[a-z]+\s*=|javascript:/i;
|
|
36
|
+
var checkRawMarkup = (type, props, path, ctx) => {
|
|
37
|
+
if (ctx.mode !== "widget" || !props || !RAW_CODE_TYPES.has(type)) return;
|
|
38
|
+
for (const [key, value] of Object.entries(props)) if (typeof value === "string" && EXECUTABLE_MARKUP.test(value)) ctx.errors.push({
|
|
39
|
+
path: `${path}.props.${key}`,
|
|
40
|
+
message: `Executable markup in a "${type}" prop is not allowed in a widget`,
|
|
41
|
+
hint: "A widget renders inside the host UI, so <script>, javascript: URLs and inline on* handlers are rejected. Keep the markup inert (an inline <svg> is what this type is for) and wire behaviour with upsertInteractionFlow."
|
|
42
|
+
});
|
|
43
|
+
};
|
|
35
44
|
var checkElementProps = (element, path, ctx) => {
|
|
45
|
+
checkRawMarkup(element.type, element.props, path, ctx);
|
|
36
46
|
if (!element.props || RAW_CODE_TYPES.has(element.type)) return;
|
|
37
47
|
for (const [key, value] of Object.entries(element.props)) if (typeof value === "string") checkVarRefs(value, `${path}.props.${key}`, ctx);
|
|
38
48
|
checkTypeProps(element.type, element.props, path, ctx);
|
|
@@ -55,4 +65,4 @@ var checkElementInput = (element, path, ctx, seen) => {
|
|
|
55
65
|
element.children?.forEach((child, i) => checkElementInput(child, `${path}.children[${i}]`, ctx, seen));
|
|
56
66
|
};
|
|
57
67
|
//#endregion
|
|
58
|
-
export { checkElementInput, checkTypeProps, checkVariantApplication };
|
|
68
|
+
export { checkElementInput, checkRawMarkup, checkTypeProps, checkVariantApplication };
|
|
@@ -8,7 +8,7 @@ import { checkObservedName, checkVarRefs, warnOnce } from "./context.js";
|
|
|
8
8
|
import { checkBindingSourceScope, checkBindingTarget, checkBindingTransformers } from "./bindings.js";
|
|
9
9
|
import { checkSlotCss } from "./css.js";
|
|
10
10
|
import { checkRef } from "./refs.js";
|
|
11
|
-
import { checkElementInput, checkTypeProps, checkVariantApplication } from "./elements.js";
|
|
11
|
+
import { checkElementInput, checkRawMarkup, checkTypeProps, checkVariantApplication } from "./elements.js";
|
|
12
12
|
import { checkInteractionNode } from "./interactions.js";
|
|
13
13
|
//#region src/modules/mcp/tools/shared/validator/index.ts
|
|
14
14
|
var STYLE_CATEGORIES = [
|
|
@@ -35,9 +35,10 @@ var buildTypeMeta = (catalog) => {
|
|
|
35
35
|
};
|
|
36
36
|
/** The shared validation context, derived from a space (+ the batch's ops, for batch-declared names). Extracted so
|
|
37
37
|
* the post-apply resource audit (auditResources) can run the same checks against the resulting draft. */
|
|
38
|
-
var buildValidationCtx = (space, ops) => {
|
|
38
|
+
var buildValidationCtx = (space, ops, mode = "space") => {
|
|
39
39
|
const registry = buildTypeRegistry(space.schema, space.catalog);
|
|
40
40
|
return {
|
|
41
|
+
mode,
|
|
41
42
|
errors: [],
|
|
42
43
|
warnings: [],
|
|
43
44
|
warned: /* @__PURE__ */ new Set(),
|
|
@@ -57,11 +58,11 @@ var buildValidationCtx = (space, ops) => {
|
|
|
57
58
|
observedSources: observedDataSources(space.schema)
|
|
58
59
|
};
|
|
59
60
|
};
|
|
60
|
-
var validateOperations = (space, ops) => {
|
|
61
|
+
var validateOperations = (space, ops, mode = "space") => {
|
|
61
62
|
const batchPages = batchDeclaredPages(ops);
|
|
62
63
|
const batchFolders = batchDeclaredFolders(ops);
|
|
63
64
|
const folderRefs = () => pageFoldersOf(space.schema).map((f) => f.id);
|
|
64
|
-
const ctx = buildValidationCtx(space, ops);
|
|
65
|
+
const ctx = buildValidationCtx(space, ops, mode);
|
|
65
66
|
if (ops.length > 1e3) ctx.errors.push({
|
|
66
67
|
path: "operations",
|
|
67
68
|
message: `Batch has ${ops.length} operations (max ${MAX_OPS})`,
|
|
@@ -91,7 +92,10 @@ var validateOperations = (space, ops) => {
|
|
|
91
92
|
const target = page ? resolveRef(space.schema, page, op.ref) : void 0;
|
|
92
93
|
if (op.props) {
|
|
93
94
|
for (const [key, value] of Object.entries(op.props)) if (typeof value === "string") checkVarRefs(value, `${base}.props.${key}`, ctx);
|
|
94
|
-
if (target && target.id !== page?.id)
|
|
95
|
+
if (target && target.id !== page?.id) {
|
|
96
|
+
checkRawMarkup(target.definition.type, op.props, base, ctx);
|
|
97
|
+
checkTypeProps(target.definition.type, op.props, base, ctx);
|
|
98
|
+
}
|
|
95
99
|
}
|
|
96
100
|
checkVariantApplication(op.initialState, `${base}.initialState`, ctx);
|
|
97
101
|
break;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { McpApp } from '../types';
|
|
1
|
+
import { McpApp, McpViewSettings } from '../types';
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
/** What a view gets when the deployment says nothing: streaming on, because a host that streams no arguments
|
|
4
|
+
* paints exactly what it painted before. */
|
|
5
|
+
export declare const DEFAULT_VIEW_SETTINGS: McpViewSettings;
|
|
3
6
|
/** Every MCP App this server serves. A new one is a folder beside `render/` (its definition + its view) and one
|
|
4
7
|
* line here — the tool that opens it points at its `uri` through `ui: { resourceUri }`. Start from `example/`,
|
|
5
8
|
* which is that same shape stripped to the minimum. */
|
|
6
9
|
export declare const apps: McpApp[];
|
|
7
|
-
export declare const registerApps: (server: McpServer) => void;
|
|
8
|
-
export { RENDER_APP_URI } from './render';
|
|
10
|
+
export declare const registerApps: (server: McpServer, settings?: McpViewSettings) => void;
|
|
11
|
+
export { iconFontCss, RENDER_APP_URI } from './render';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Lifts every @font-face block out of a stylesheet. Brace counting rather than a CSS parser: a @font-face body is
|
|
2
|
+
* plain declarations, and base64 payloads and url() values carry no braces of their own. */
|
|
3
|
+
export declare const splitFontFaces: (css: string) => {
|
|
4
|
+
base: string;
|
|
5
|
+
fonts: string;
|
|
6
|
+
};
|
|
7
|
+
/** What the page shell inlines: the SDK stylesheet without the icon fonts. */
|
|
8
|
+
export declare const widgetCss: () => string;
|
|
9
|
+
/** The icon fonts, for the renders that draw one. */
|
|
10
|
+
export declare const iconFontCss: () => string;
|