@plitzi/sdk-server 0.32.6 → 0.32.8
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/dist/core/http/stages/static.js +2 -0
- package/dist/core/services/mcp.js +1 -1
- package/dist/core/services/registry.js +5 -1
- package/dist/modules/ai/toolkit.js +1 -0
- package/dist/modules/mcp/handler.js +32 -13
- package/dist/modules/mcp/helpers/guide.js +1 -1
- package/dist/modules/mcp/helpers/space.js +27 -1
- package/dist/modules/mcp/resources/register.js +2 -0
- package/dist/modules/mcp/resources/renderApp.js +174 -0
- package/dist/modules/mcp/resources/renderGuide.js +186 -0
- package/dist/modules/mcp/server.js +15 -5
- package/dist/modules/mcp/tools/index.js +2 -0
- package/dist/modules/mcp/tools/render.js +116 -0
- package/dist/modules/mcp/tools/shared/tool.js +2 -0
- package/dist/src/modules/mcp/handler.d.ts +4 -1
- package/dist/src/modules/mcp/helpers/space.d.ts +1 -0
- package/dist/src/modules/mcp/resources/renderApp.d.ts +3 -0
- package/dist/src/modules/mcp/resources/renderGuide.d.ts +4 -0
- package/dist/src/modules/mcp/server.d.ts +9 -1
- package/dist/src/modules/mcp/tools/index.d.ts +1 -0
- package/dist/src/modules/mcp/tools/render.d.ts +434 -0
- package/dist/src/modules/mcp/tools/render.test.d.ts +1 -0
- package/dist/src/modules/mcp/tools/shared/tool.d.ts +12 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @plitzi/sdk-server
|
|
2
2
|
|
|
3
|
+
## 0.32.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- v0.32.8
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @plitzi/plitzi-sdk@0.32.8
|
|
10
|
+
- @plitzi/sdk-schema@0.32.8
|
|
11
|
+
- @plitzi/sdk-shared@0.32.8
|
|
12
|
+
|
|
13
|
+
## 0.32.7
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- v0.32.7
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- @plitzi/plitzi-sdk@0.32.7
|
|
20
|
+
- @plitzi/sdk-schema@0.32.7
|
|
21
|
+
- @plitzi/sdk-shared@0.32.7
|
|
22
|
+
|
|
3
23
|
## 0.32.6
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -18,6 +18,8 @@ var configStaticStage = (ctx) => {
|
|
|
18
18
|
for (const [prefix, rootDir] of Object.entries(mounts)) {
|
|
19
19
|
const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
20
20
|
if (ctx.req.path === prefix || ctx.req.path.startsWith(normalizedPrefix)) {
|
|
21
|
+
ctx.res.setHeader("Access-Control-Allow-Origin", "*");
|
|
22
|
+
ctx.res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
21
23
|
if (serveStatic({
|
|
22
24
|
...ctx.req,
|
|
23
25
|
path: ctx.req.path.slice(prefix.length) || "/"
|
|
@@ -7,7 +7,7 @@ var serveMcp = (ctx) => {
|
|
|
7
7
|
const { previewClient, screenshot } = ctx.config;
|
|
8
8
|
const preview = previewClient ? createHttpPreviewClient(previewClient) : void 0;
|
|
9
9
|
const screenshotClient = screenshot ? createHttpScreenshotClient(screenshot) : void 0;
|
|
10
|
-
return handleMcp(ctx.raw, ctx.rawRes, ctx.req, ctx.config.adapters, preview, screenshotClient, ctx.config.mcpLogger);
|
|
10
|
+
return handleMcp(ctx.raw, ctx.rawRes, ctx.req, ctx.config.adapters, preview, screenshotClient, ctx.config.mcpLogger, ctx.config.renderApp);
|
|
11
11
|
};
|
|
12
12
|
var mcpStage = async (ctx) => {
|
|
13
13
|
if (!ctx.req.path.startsWith(mcpPathOf(ctx.config.mcp?.path))) return false;
|
|
@@ -25,6 +25,10 @@ var buildSSRPipeline = (services) => {
|
|
|
25
25
|
stages.push(services.ssr ? ssrStage : notFoundStage);
|
|
26
26
|
return stages;
|
|
27
27
|
};
|
|
28
|
-
var buildMCPPipeline = () => [
|
|
28
|
+
var buildMCPPipeline = () => [
|
|
29
|
+
healthStage,
|
|
30
|
+
configStaticStage,
|
|
31
|
+
mcpOnlyStage
|
|
32
|
+
];
|
|
29
33
|
//#endregion
|
|
30
34
|
export { buildMCPPipeline, buildSSRPipeline };
|
|
@@ -25,33 +25,52 @@ var serveMcp = async (raw, res, server) => {
|
|
|
25
25
|
res.end();
|
|
26
26
|
return;
|
|
27
27
|
}
|
|
28
|
+
if (raw.url?.startsWith("/.well-known/")) {
|
|
29
|
+
res.writeHead(404);
|
|
30
|
+
res.end();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (raw.method !== "POST") {
|
|
34
|
+
res.writeHead(405, {
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
Allow: "POST, OPTIONS"
|
|
37
|
+
});
|
|
38
|
+
res.end(JSON.stringify({
|
|
39
|
+
jsonrpc: "2.0",
|
|
40
|
+
error: {
|
|
41
|
+
code: -32e3,
|
|
42
|
+
message: "Method not allowed; this MCP endpoint accepts JSON-RPC over POST only."
|
|
43
|
+
},
|
|
44
|
+
id: null
|
|
45
|
+
}));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
28
48
|
const transport = new StreamableHTTPServerTransport({
|
|
29
49
|
sessionIdGenerator: void 0,
|
|
30
50
|
enableJsonResponse: true
|
|
31
51
|
});
|
|
32
52
|
await server.connect(transport);
|
|
33
53
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await transport.handleRequest(raw, res, body);
|
|
44
|
-
} else await transport.handleRequest(raw, res);
|
|
54
|
+
let body = raw.body;
|
|
55
|
+
if (body === void 0) try {
|
|
56
|
+
body = await readMcpBody(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
59
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await transport.handleRequest(raw, res, body);
|
|
45
63
|
} finally {
|
|
46
64
|
await transport.close();
|
|
47
65
|
}
|
|
48
66
|
};
|
|
49
|
-
var handleMcp = (raw, res, req, adapters, preview, screenshot, logger) => serveMcp(raw, res, createMcpServer({
|
|
67
|
+
var handleMcp = (raw, res, req, adapters, preview, screenshot, logger, renderApp) => serveMcp(raw, res, createMcpServer({
|
|
50
68
|
adapters,
|
|
51
69
|
getSpaceId: () => adapters.getSpaceId?.(req) ?? Promise.resolve(void 0),
|
|
52
70
|
preview,
|
|
53
71
|
screenshot,
|
|
54
|
-
logger
|
|
72
|
+
logger,
|
|
73
|
+
renderApp
|
|
55
74
|
}));
|
|
56
75
|
//#endregion
|
|
57
76
|
export { createMcpServer, handleMcp, readMcpBody, serveMcp };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/modules/mcp/helpers/guide.ts
|
|
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 kebab-case and ATOMIC (shorthands like border/padding expanded; compound ones like flex/background/font rejected — use 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).";
|
|
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 kebab-case and ATOMIC (shorthands like border/padding expanded; compound ones like flex/background/font rejected — use 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
3
|
var guideQuickstart = `# Plitzi AI MCP — quickstart
|
|
4
4
|
This is the condensed guide; read \`plitzi://guide\` for the full reference (every resource, op and example).
|
|
5
5
|
|
|
@@ -4,6 +4,32 @@ var cloneSpace = (space) => ({
|
|
|
4
4
|
style: structuredClone(space.style),
|
|
5
5
|
...space.catalog ? { catalog: space.catalog } : {}
|
|
6
6
|
});
|
|
7
|
+
var emptySpace = () => ({
|
|
8
|
+
schema: {
|
|
9
|
+
flat: {},
|
|
10
|
+
definition: {
|
|
11
|
+
name: "",
|
|
12
|
+
permanentUrl: ""
|
|
13
|
+
},
|
|
14
|
+
variables: [],
|
|
15
|
+
settings: { customCss: "" },
|
|
16
|
+
pages: [],
|
|
17
|
+
pageFolders: []
|
|
18
|
+
},
|
|
19
|
+
style: {
|
|
20
|
+
platform: {
|
|
21
|
+
desktop: {},
|
|
22
|
+
tablet: {},
|
|
23
|
+
mobile: {}
|
|
24
|
+
},
|
|
25
|
+
theme: {
|
|
26
|
+
default: "system",
|
|
27
|
+
schemes: ["light", "dark"]
|
|
28
|
+
},
|
|
29
|
+
variables: {},
|
|
30
|
+
cache: ""
|
|
31
|
+
}
|
|
32
|
+
});
|
|
7
33
|
var slugify = (value) => value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "";
|
|
8
34
|
/** A value when it is a string, otherwise undefined — for the many attributes typed as `unknown` that are strings
|
|
9
35
|
* in practice (name, slug, subType, dom id…). */
|
|
@@ -259,4 +285,4 @@ var generateObjectId = () => {
|
|
|
259
285
|
return `${Math.floor(Date.now() / 1e3).toString(16).padStart(8, "0")}${Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("")}`;
|
|
260
286
|
};
|
|
261
287
|
//#endregion
|
|
262
|
-
export { cloneSpace, descendantCount, descendantIds, elementById, elementRefOf, 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 };
|
|
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 };
|
|
@@ -3,6 +3,7 @@ import { guideText } from "../helpers/guide.js";
|
|
|
3
3
|
import { resourceErrorMessage } from "./canonical.js";
|
|
4
4
|
import { envelope, jsonContents } from "./envelope.js";
|
|
5
5
|
import { readResource } from "./router.js";
|
|
6
|
+
import { registerRenderResources } from "./renderGuide.js";
|
|
6
7
|
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp";
|
|
7
8
|
//#region src/modules/mcp/resources/register.ts
|
|
8
9
|
/** Register every resource on the MCP server: fixed listings plus templated per-item reads. The space is
|
|
@@ -32,6 +33,7 @@ var registerResources = (server, getSpace, env, log) => {
|
|
|
32
33
|
description: "Valid kebab-case CSS property keys",
|
|
33
34
|
mimeType: "application/json"
|
|
34
35
|
}, () => jsonContents("plitzi://css-properties", envelope(cssProperties)));
|
|
36
|
+
registerRenderResources(server);
|
|
35
37
|
const fixed = [
|
|
36
38
|
[
|
|
37
39
|
"Primer",
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
//#region src/modules/mcp/resources/renderApp.ts
|
|
2
|
+
var RENDER_APP_URI = "ui://plitzi/render.html";
|
|
3
|
+
var RENDER_APP_MIME = "text/html;profile=mcp-app";
|
|
4
|
+
var RENDER_APP_CSP = {
|
|
5
|
+
resourceDomains: [
|
|
6
|
+
"*",
|
|
7
|
+
"data:",
|
|
8
|
+
"blob:"
|
|
9
|
+
],
|
|
10
|
+
connectDomains: ["*"]
|
|
11
|
+
};
|
|
12
|
+
var appHtml = (sdkBase, devMode) => {
|
|
13
|
+
const js = `${sdkBase}/sdk-assets/plitzi-sdk.js`;
|
|
14
|
+
const vendor = `${sdkBase}/sdk-assets/${devMode ? "plitzi-sdk-dev-vendor.js" : "plitzi-sdk-vendor.js"}`;
|
|
15
|
+
return `<!doctype html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
20
|
+
<title>Plitzi widget</title>
|
|
21
|
+
<link rel="modulepreload" href="${vendor}" crossorigin />
|
|
22
|
+
<script type="importmap">
|
|
23
|
+
{
|
|
24
|
+
"imports": {
|
|
25
|
+
"react": "${vendor}",
|
|
26
|
+
"react-dom": "${vendor}",
|
|
27
|
+
"react-dom/client": "${vendor}",
|
|
28
|
+
"react/jsx-runtime": "${vendor}",
|
|
29
|
+
"react/compiler-runtime": "${vendor}",
|
|
30
|
+
"@plitzi/plitzi-sdk": "${js}"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
<\/script>
|
|
34
|
+
<link href="${`${sdkBase}/sdk-assets/plitzi-sdk.css`}" rel="stylesheet" />
|
|
35
|
+
<style>
|
|
36
|
+
html,
|
|
37
|
+
body {
|
|
38
|
+
margin: 0;
|
|
39
|
+
}
|
|
40
|
+
#plitzi:empty::after {
|
|
41
|
+
content: 'Rendering…';
|
|
42
|
+
display: block;
|
|
43
|
+
padding: 16px;
|
|
44
|
+
font: 14px system-ui, sans-serif;
|
|
45
|
+
color: #64748b;
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<div id="plitzi" class="plitzi-root-container"></div>
|
|
51
|
+
<script type="module">
|
|
52
|
+
const send = msg => window.parent.postMessage(msg, '*');
|
|
53
|
+
const root = document.getElementById('plitzi');
|
|
54
|
+
let mounted = false;
|
|
55
|
+
|
|
56
|
+
// Replace the 'Rendering…' placeholder with an explicit failure panel — otherwise a tool that returns
|
|
57
|
+
// { rendered:false } (no offlineData) or an SDK that fails to load would leave the iframe stuck forever.
|
|
58
|
+
const showError = (title, details) => {
|
|
59
|
+
mounted = true;
|
|
60
|
+
root.textContent = '';
|
|
61
|
+
const box = document.createElement('div');
|
|
62
|
+
box.setAttribute('style', 'padding:16px;font:13px/1.5 system-ui,sans-serif;color:#b91c1c');
|
|
63
|
+
const h = document.createElement('strong');
|
|
64
|
+
h.textContent = title;
|
|
65
|
+
box.appendChild(h);
|
|
66
|
+
if (details) {
|
|
67
|
+
const pre = document.createElement('pre');
|
|
68
|
+
pre.setAttribute('style', 'margin:8px 0 0;white-space:pre-wrap;color:#7f1d1d;font-size:12px');
|
|
69
|
+
pre.textContent = details;
|
|
70
|
+
box.appendChild(pre);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
root.appendChild(box);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// The failed-render text is the tool's JSON summary ({ rendered:false, errors:[{path,message,hint}] }).
|
|
77
|
+
const errorText = params => {
|
|
78
|
+
const item = Array.isArray(params && params.content) ? params.content.find(c => c.type === 'text') : undefined;
|
|
79
|
+
if (!item) {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(item.text);
|
|
85
|
+
if (Array.isArray(parsed.errors)) {
|
|
86
|
+
return parsed.errors
|
|
87
|
+
.map(e => '• ' + (e.path ? e.path + ': ' : '') + e.message + (e.hint ? ' — ' + e.hint : ''))
|
|
88
|
+
.join('\\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return item.text;
|
|
92
|
+
} catch {
|
|
93
|
+
return item.text;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const mount = async params => {
|
|
98
|
+
if (mounted) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const offlineData = params && params.structuredContent && params.structuredContent.offlineData;
|
|
103
|
+
if (!offlineData) {
|
|
104
|
+
showError('Render failed', errorText(params) || 'The tool returned no widget data.');
|
|
105
|
+
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
mounted = true;
|
|
110
|
+
try {
|
|
111
|
+
const { render } = await import('@plitzi/plitzi-sdk');
|
|
112
|
+
render('plitzi', { offlineData, offlineMode: true, environment: 'main', renderMode: 'raw' }, {}, false, false);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
showError('SDK failed to load', String((err && err.message) || err));
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
window.addEventListener('message', event => {
|
|
119
|
+
const msg = event.data;
|
|
120
|
+
if (!msg || msg.jsonrpc !== '2.0') {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Reply to the initialize result: the host withholds tool data until it gets our 'initialized' notification.
|
|
125
|
+
if (msg.id === 1 && 'result' in msg) {
|
|
126
|
+
send({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} });
|
|
127
|
+
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (msg.method === 'ui/notifications/tool-result') {
|
|
132
|
+
void mount(msg.params);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Start the handshake immediately — nothing external blocks it, so the iframe is 'ready' at once.
|
|
137
|
+
send({
|
|
138
|
+
jsonrpc: '2.0',
|
|
139
|
+
id: 1,
|
|
140
|
+
method: 'ui/initialize',
|
|
141
|
+
params: {
|
|
142
|
+
protocolVersion: '2025-06-18',
|
|
143
|
+
appInfo: { name: 'Plitzi Widget', version: '1.0.0' },
|
|
144
|
+
appCapabilities: { availableDisplayModes: ['inline'] }
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
<\/script>
|
|
148
|
+
</body>
|
|
149
|
+
</html>`;
|
|
150
|
+
};
|
|
151
|
+
var appCache = /* @__PURE__ */ new Map();
|
|
152
|
+
var getAppHtml = (sdkBase, devMode) => {
|
|
153
|
+
const key = `${devMode ? "dev" : "prod"}|${sdkBase}`;
|
|
154
|
+
let html = appCache.get(key);
|
|
155
|
+
if (!html) {
|
|
156
|
+
html = appHtml(sdkBase, devMode);
|
|
157
|
+
appCache.set(key, html);
|
|
158
|
+
}
|
|
159
|
+
return html;
|
|
160
|
+
};
|
|
161
|
+
var registerRenderApp = (server, sdkBase, devMode) => {
|
|
162
|
+
const html = getAppHtml(sdkBase, devMode);
|
|
163
|
+
server.registerResource("plitzi-render-app", RENDER_APP_URI, {
|
|
164
|
+
description: "Interactive view that renders a plitzi_render widget with the Plitzi SDK (client-side).",
|
|
165
|
+
mimeType: RENDER_APP_MIME,
|
|
166
|
+
_meta: { ui: { csp: RENDER_APP_CSP } }
|
|
167
|
+
}, () => ({ contents: [{
|
|
168
|
+
uri: RENDER_APP_URI,
|
|
169
|
+
mimeType: RENDER_APP_MIME,
|
|
170
|
+
text: html
|
|
171
|
+
}] }));
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
174
|
+
export { RENDER_APP_URI, registerRenderApp };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { BUILTIN_COMPONENTS } from "../catalogs/builtinComponents.js";
|
|
2
|
+
import { envelope, jsonContents } from "./envelope.js";
|
|
3
|
+
//#region src/modules/mcp/resources/renderGuide.ts
|
|
4
|
+
var RENDER_GUIDE_URI = "plitzi://render/guide";
|
|
5
|
+
var RENDER_TYPES_URI = "plitzi://render/types";
|
|
6
|
+
var RENDER_TYPE_CATEGORIES = /* @__PURE__ */ new Set([
|
|
7
|
+
"structure",
|
|
8
|
+
"basic",
|
|
9
|
+
"media",
|
|
10
|
+
"form",
|
|
11
|
+
"provider"
|
|
12
|
+
]);
|
|
13
|
+
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
|
+
var renderTypes = () => {
|
|
15
|
+
const types = {};
|
|
16
|
+
for (const [name, info] of Object.entries(BUILTIN_COMPONENTS)) if (info && info.category && RENDER_TYPE_CATEGORIES.has(info.category)) types[name] = {
|
|
17
|
+
label: info.label,
|
|
18
|
+
category: info.category,
|
|
19
|
+
description: info.description
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
note: renderTypesNote,
|
|
23
|
+
types
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
var renderGuideText = `# plitzi_render — authoring guide
|
|
27
|
+
|
|
28
|
+
\`plitzi_render\` shows the user a live UI widget. You build it as a list of \`operations\` (applied IN ORDER) that
|
|
29
|
+
assemble an element tree under one pre-seeded root page whose ref is **"render"**. It renders fully offline — no
|
|
30
|
+
space, no backend, no account. Same operation vocabulary as the editing tool plitzi_apply, aimed at one page.
|
|
31
|
+
|
|
32
|
+
Most widgets are **presentation only** — the structure + styling below is all you need, so start there. The widget
|
|
33
|
+
runs the live Plitzi SDK though, so it can also **fetch data** (an \`apiContainer\`) and **react to events**
|
|
34
|
+
(interaction flows); see "Data & interactivity" at the end when a widget needs them.
|
|
35
|
+
|
|
36
|
+
Each call renders a **fresh** widget with no memory of previous calls — always send **every** operation the widget
|
|
37
|
+
needs in the one call. To change a widget, re-send the whole thing.
|
|
38
|
+
|
|
39
|
+
## Build the whole widget in ONE upsertElement (nest with \`children\`)
|
|
40
|
+
|
|
41
|
+
The simplest, least error-prone path: a single \`upsertElement\` whose \`element\` carries a nested \`children\` tree —
|
|
42
|
+
so you describe the layout the way it renders, no ref-juggling. \`pageRef: "render"\` is required; each element in the
|
|
43
|
+
tree needs a unique \`ref\` — a name you choose from letters, digits, \`-\` and \`_\`, **starting with a letter and with
|
|
44
|
+
no dots** (e.g. \`price-row\`, \`cta_button\`).
|
|
45
|
+
|
|
46
|
+
\`\`\`json
|
|
47
|
+
{ "type": "upsertElement", "pageRef": "render", "element": {
|
|
48
|
+
"ref": "card", "type": "container", "style": { "base": ["card"] },
|
|
49
|
+
"children": [
|
|
50
|
+
{ "ref": "plan", "type": "heading", "subType": "h3", "props": { "content": "Pro" } },
|
|
51
|
+
{ "ref": "amount", "type": "text", "props": { "content": "$29/mo" }, "style": { "base": ["price"] } },
|
|
52
|
+
{ "ref": "buy", "type": "button", "props": { "content": "Start free trial" }, "style": { "base": ["cta"] } }
|
|
53
|
+
]
|
|
54
|
+
} }
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
An element is \`{ ref, type, subType?, props?, style?, children? }\`. Children render in array order. (You *can* also
|
|
58
|
+
add elements one-by-one with a top-level \`parentRef: "<existing ref>"\` and optional \`position\` — useful to append to
|
|
59
|
+
or restructure something you already created — but for a fresh widget the inline \`children\` tree is easier.)
|
|
60
|
+
|
|
61
|
+
## Style with reusable classes — upsertDefinition
|
|
62
|
+
|
|
63
|
+
Styling is separate from structure: declare a class, then attach it by ref.
|
|
64
|
+
|
|
65
|
+
\`\`\`json
|
|
66
|
+
{ "type": "upsertDefinition", "ref": "card", "desktop": { "display": "flex", "flex-direction": "column", "gap": "8px", "padding": "24px", "border-radius": "12px" } }
|
|
67
|
+
\`\`\`
|
|
68
|
+
- CSS properties in **kebab-case** (\`background-color\`, \`font-size\`, \`border-radius\`), values as plain strings.
|
|
69
|
+
- Attach to an element via \`style: { "base": ["card"] }\`. Stack classes: \`"base": ["card", "shadow"]\`.
|
|
70
|
+
- One \`ref\` can name both an element and its class (as above) — they live in different namespaces.
|
|
71
|
+
- Lay containers out with flexbox: \`{ "display": "flex", "flex-direction": "column", "gap": "12px" }\`.
|
|
72
|
+
- **Mind the intrinsic display.** Some types start non-block: \`text\` is \`display: inline\`, so to stack or size it,
|
|
73
|
+
wrap it in a \`container\` (or set \`display: block\`). \`heading\` and \`paragraph\` are already block.
|
|
74
|
+
- **Use atomic longhands.** \`padding\`, \`margin\`, \`border\`, \`border-radius\` are fine (they expand cleanly), but
|
|
75
|
+
\`flex\`, \`background\` and \`font\` are rejected — write \`display\`+\`flex-direction\`, \`background-color\`,
|
|
76
|
+
\`font-size\`+\`font-weight\` instead. An unknown property errors with the correct kebab-case key suggested.
|
|
77
|
+
- **Responsive:** add \`tablet\` and/or \`mobile\` blocks next to \`desktop\` (same shape); they override desktop on
|
|
78
|
+
smaller screens — \`{ "desktop": { "font-size": "36px" }, "mobile": { "font-size": "24px" } }\`.
|
|
79
|
+
- **Interactive states:** nest under \`states\` keyed by pseudo-class, each with its own breakpoint block —
|
|
80
|
+
\`{ "desktop": { "background-color": "#3b82f6" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } }\`
|
|
81
|
+
(\`hover\`, \`active\`, \`focus\`).
|
|
82
|
+
|
|
83
|
+
## Element types (type → what to set)
|
|
84
|
+
|
|
85
|
+
| type | renders | set |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| \`container\` | layout box (a div) | nothing — style it with flex/grid |
|
|
88
|
+
| \`heading\` | h1–h6 title | \`props.content\`, element \`subType\`: "h1"…"h6" |
|
|
89
|
+
| \`paragraph\` | body text block (\`<p>\`) | \`props.content\` |
|
|
90
|
+
| \`text\` | inline text | \`props.content\` |
|
|
91
|
+
| \`button\` | clickable button | \`props.content\` |
|
|
92
|
+
| \`link\` | anchor | \`props.content\` (label), \`props.href\`, \`props.target\`: "self" \\| "blank" |
|
|
93
|
+
| \`image\` | \`<img>\` | \`props.src\`, \`props.alt\` |
|
|
94
|
+
| \`video\` | embedded video | \`props.src\` |
|
|
95
|
+
| \`list\` / \`listItem\` | \`<ul>\` / \`<li>\` | nesting only |
|
|
96
|
+
| \`markdown\` | rendered markdown | \`props.content\` |
|
|
97
|
+
|
|
98
|
+
\`subType\` is an element-level field (not a prop). Guessing is safe: an unknown prop for a type comes back as a
|
|
99
|
+
**warning naming the right one**, not an error. This table covers the everyday types; the resource
|
|
100
|
+
**plitzi://render/types** lists every built-in type you can use (with descriptions) — read it when you need one
|
|
101
|
+
that is not here (lists, tabs, dialogs, forms, icons…).
|
|
102
|
+
|
|
103
|
+
\`image\`/\`video\` \`src\` accepts any \`https\` URL, or a \`data:\`/\`blob:\` URI for a fully self-contained graphic
|
|
104
|
+
(e.g. an inline SVG icon or a base64 image) — both render with no extra setup.
|
|
105
|
+
|
|
106
|
+
## Full worked example — a pricing card
|
|
107
|
+
|
|
108
|
+
\`\`\`json
|
|
109
|
+
{
|
|
110
|
+
"operations": [
|
|
111
|
+
{ "type": "upsertDefinition", "ref": "card", "desktop": { "display": "flex", "flex-direction": "column", "gap": "8px", "padding": "24px", "background-color": "#ffffff", "border-radius": "12px", "width": "260px", "text-align": "center", "box-shadow": "0 4px 20px rgba(0,0,0,0.08)" } },
|
|
112
|
+
{ "type": "upsertDefinition", "ref": "price", "desktop": { "font-size": "36px", "font-weight": "800", "color": "#3b82f6" } },
|
|
113
|
+
{ "type": "upsertDefinition", "ref": "cta", "desktop": { "background-color": "#3b82f6", "color": "#ffffff", "padding": "12px 20px", "border-radius": "8px", "font-weight": "600" }, "states": { "hover": { "desktop": { "background-color": "#2563eb" } } } },
|
|
114
|
+
{ "type": "upsertElement", "pageRef": "render", "element": {
|
|
115
|
+
"ref": "card", "type": "container", "style": { "base": ["card"] },
|
|
116
|
+
"children": [
|
|
117
|
+
{ "ref": "plan", "type": "heading", "subType": "h3", "props": { "content": "Pro" } },
|
|
118
|
+
{ "ref": "amount", "type": "text", "props": { "content": "$29/mo" }, "style": { "base": ["price"] } },
|
|
119
|
+
{ "ref": "feat", "type": "paragraph", "props": { "content": "Unlimited projects" } },
|
|
120
|
+
{ "ref": "buy", "type": "button", "props": { "content": "Start free trial" }, "style": { "base": ["cta"] } }
|
|
121
|
+
]
|
|
122
|
+
} }
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
## Data & interactivity (optional)
|
|
128
|
+
|
|
129
|
+
The widget runs the live SDK, so beyond static layout it can fetch data and respond to events. Both are wired by
|
|
130
|
+
\`ref\`, just like styling. (Full reference: **plitzi://guide** — sections Data bindings and Interactions.)
|
|
131
|
+
|
|
132
|
+
### Fetch data — a provider + a binding
|
|
133
|
+
An \`apiContainer\` fetches at runtime and exposes the result as the source **\`apiContainer_<idRef>.data\`**, visible
|
|
134
|
+
to its **DESCENDANTS only** — the bound element must live inside the container's subtree. \`upsertBinding\` then
|
|
135
|
+
connects that source to a descendant's field.
|
|
136
|
+
|
|
137
|
+
\`\`\`json
|
|
138
|
+
{
|
|
139
|
+
"operations": [
|
|
140
|
+
{ "type": "upsertElement", "pageRef": "render", "element": {
|
|
141
|
+
"ref": "products", "type": "apiContainer", "props": { "query": "<your data query — see plitzi://guide>" },
|
|
142
|
+
"children": [ { "ref": "title", "type": "heading", "subType": "h3", "props": { "content": "…" } } ]
|
|
143
|
+
} },
|
|
144
|
+
{ "type": "upsertBinding", "pageRef": "render", "ref": "title", "category": "attributes", "binding": { "to": "content", "source": "apiContainer_products.data.0.name" } }
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
\`\`\`
|
|
148
|
+
- \`category\`: \`"attributes"\` (a prop such as \`content\`/\`src\`), \`"style"\` (a CSS value) or \`"initialState"\`.
|
|
149
|
+
- \`binding.to\` is the target field; \`binding.source\` is the \`<type>_<idRef>.path\` into the provider's data.
|
|
150
|
+
- \`mockData\` on the provider is builder-only — set a real \`query\` for the widget to actually fetch.
|
|
151
|
+
|
|
152
|
+
### React to events — an interaction flow
|
|
153
|
+
\`upsertInteractionFlow\` attaches an ordered \`nodes\` list to an element; the FIRST node is a \`trigger\` (e.g.
|
|
154
|
+
\`onClick\`), the rest run after it. Each following step is one of:
|
|
155
|
+
- **\`globalCallback\`** (OMIT \`elementId\`) — a built-in app action: \`addNotification\` (\`params.content\`),
|
|
156
|
+
\`navigate\`, \`setState\` (\`key\`/\`type\`/\`value\`), \`authLogin\`/\`authLogout\`…
|
|
157
|
+
- **\`callback\`** (\`elementId\` = an element, defaults to the trigger's) — that ELEMENT's OWN callback: e.g. an
|
|
158
|
+
\`apiContainer\` re-fetches, a \`form\` submits. Each type's own callback action names are in plitzi://guide.
|
|
159
|
+
|
|
160
|
+
\`\`\`json
|
|
161
|
+
{ "type": "upsertInteractionFlow", "pageRef": "render", "ref": "buy", "nodes": [
|
|
162
|
+
{ "title": "On click", "nodeType": "trigger", "action": "onClick" },
|
|
163
|
+
{ "title": "Toast", "nodeType": "globalCallback", "action": "addNotification", "params": { "content": "Added to cart" } }
|
|
164
|
+
] }
|
|
165
|
+
\`\`\`
|
|
166
|
+
|
|
167
|
+
## When it fails
|
|
168
|
+
The tool returns \`rendered: false\` with \`errors: [{ path, message, hint }]\`. Read the hint, fix that one op, and
|
|
169
|
+
retry — you never lose the rest of the batch.
|
|
170
|
+
`;
|
|
171
|
+
var registerRenderResources = (server) => {
|
|
172
|
+
server.registerResource("Render guide", RENDER_GUIDE_URI, {
|
|
173
|
+
description: "How to author a plitzi_render widget: operations, element types, styling, examples.",
|
|
174
|
+
mimeType: "text/markdown"
|
|
175
|
+
}, () => ({ contents: [{
|
|
176
|
+
uri: RENDER_GUIDE_URI,
|
|
177
|
+
mimeType: "text/markdown",
|
|
178
|
+
text: renderGuideText
|
|
179
|
+
}] }));
|
|
180
|
+
server.registerResource("Render element types", RENDER_TYPES_URI, {
|
|
181
|
+
description: "Built-in element types a plitzi_render widget can use, with descriptions.",
|
|
182
|
+
mimeType: "application/json"
|
|
183
|
+
}, () => jsonContents(RENDER_TYPES_URI, envelope(renderTypes())));
|
|
184
|
+
};
|
|
185
|
+
//#endregion
|
|
186
|
+
export { RENDER_GUIDE_URI, RENDER_TYPES_URI, registerRenderResources };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { emptySpaceMessage, unauthorizedSpaceMessage } from "./helpers/space.js";
|
|
1
|
+
import { emptySpace, emptySpaceMessage, unauthorizedSpaceMessage } from "./helpers/space.js";
|
|
2
2
|
import { serverInstructions } from "./helpers/guide.js";
|
|
3
3
|
import { createMcpLog } from "./helpers/log.js";
|
|
4
4
|
import { registerResources } from "./resources/register.js";
|
|
5
|
+
import { registerRenderApp } from "./resources/renderApp.js";
|
|
5
6
|
import { tools } from "./tools/index.js";
|
|
6
7
|
import { isCallToolResult } from "../ai/toolkit.js";
|
|
7
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp";
|
|
@@ -11,7 +12,7 @@ var asText = (data) => ({ content: [{
|
|
|
11
12
|
type: "text",
|
|
12
13
|
text: JSON.stringify(data)
|
|
13
14
|
}] });
|
|
14
|
-
var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) => {
|
|
15
|
+
var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger, renderApp }) => {
|
|
15
16
|
const log = createMcpLog(logger);
|
|
16
17
|
let spaceIdPromise;
|
|
17
18
|
const requireSpaceId = () => spaceIdPromise ??= getSpaceId().then((id) => {
|
|
@@ -41,9 +42,10 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
|
|
|
41
42
|
const getSpace = () => spacePromise ??= loadSpace();
|
|
42
43
|
const server = new McpServer({
|
|
43
44
|
name: "plitzi-mcp",
|
|
44
|
-
version: "0.32.
|
|
45
|
+
version: "0.32.8"
|
|
45
46
|
}, { instructions: serverInstructions });
|
|
46
47
|
registerResources(server, getSpace, MCP_ENV, log);
|
|
48
|
+
if (renderApp) registerRenderApp(server, renderApp.sdkBase, renderApp.devMode ?? false);
|
|
47
49
|
const toolContext = async () => ({
|
|
48
50
|
space: await getSpace(),
|
|
49
51
|
env: MCP_ENV,
|
|
@@ -52,16 +54,24 @@ var createMcpServer = ({ adapters, getSpaceId, preview, screenshot, logger }) =>
|
|
|
52
54
|
preview,
|
|
53
55
|
screenshot
|
|
54
56
|
});
|
|
57
|
+
const spacelessContext = () => ({
|
|
58
|
+
space: emptySpace(),
|
|
59
|
+
env: MCP_ENV,
|
|
60
|
+
persisters,
|
|
61
|
+
preview,
|
|
62
|
+
screenshot
|
|
63
|
+
});
|
|
55
64
|
for (const tool of tools) {
|
|
56
65
|
if (tool.requires === "screenshot" && !screenshot) continue;
|
|
57
66
|
server.registerTool(tool.name, {
|
|
58
67
|
title: tool.title,
|
|
59
68
|
description: tool.description,
|
|
60
|
-
inputSchema: tool.inputShape
|
|
69
|
+
inputSchema: tool.inputShape,
|
|
70
|
+
...tool.ui && renderApp ? { _meta: { ui: tool.ui } } : {}
|
|
61
71
|
}, async (args) => {
|
|
62
72
|
const start = performance.now();
|
|
63
73
|
try {
|
|
64
|
-
const result = await tool.execute(args, await toolContext());
|
|
74
|
+
const result = await tool.execute(args, tool.spaceless ? spacelessContext() : await toolContext());
|
|
65
75
|
log.toolCall(tool.name, args, performance.now() - start);
|
|
66
76
|
return isCallToolResult(result) ? result : asText(result);
|
|
67
77
|
} catch (error) {
|
|
@@ -4,6 +4,7 @@ import { validateOperations } from "./shared/validator/index.js";
|
|
|
4
4
|
import { apply, applyShape, applyTool } from "./apply/index.js";
|
|
5
5
|
import { previewTool } from "./preview.js";
|
|
6
6
|
import { read, readShape, readTool } from "./read.js";
|
|
7
|
+
import { renderTool } from "./render.js";
|
|
7
8
|
import { screenshotTool } from "./screenshot.js";
|
|
8
9
|
import { search, searchShape, searchTool } from "./search.js";
|
|
9
10
|
import { validate, validateShape, validateTool } from "./validate.js";
|
|
@@ -15,6 +16,7 @@ var tools = [
|
|
|
15
16
|
validateTool,
|
|
16
17
|
searchTool,
|
|
17
18
|
readTool,
|
|
19
|
+
renderTool,
|
|
18
20
|
previewTool,
|
|
19
21
|
screenshotTool
|
|
20
22
|
];
|