@plitzi/sdk-server 0.32.17 → 0.32.19

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.
Files changed (80) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +18 -0
  3. package/dist/modules/mcp/apps/render/index.js +2 -1
  4. package/dist/modules/mcp/apps/render/view/heldBatch.ts +107 -0
  5. package/dist/modules/mcp/apps/render/view/index.tsx +212 -0
  6. package/dist/modules/mcp/apps/shared/assets.js +10 -0
  7. package/dist/modules/mcp/apps/shared/bundle.js +15 -14
  8. package/dist/modules/mcp/handler.js +1 -1
  9. package/dist/modules/mcp/helpers/guide.js +11 -3
  10. package/dist/modules/mcp/helpers/space.js +13 -1
  11. package/dist/modules/mcp/resources/register.js +8 -3
  12. package/dist/modules/mcp/resources/renderGuide.js +138 -24
  13. package/dist/modules/mcp/resources/router.js +13 -1
  14. package/dist/modules/mcp/server.js +31 -19
  15. package/dist/modules/mcp/tools/apply/dispatch.js +2 -0
  16. package/dist/modules/mcp/tools/apply/index.js +17 -3
  17. package/dist/modules/mcp/tools/operations/index.js +4 -0
  18. package/dist/modules/mcp/tools/operations/schema/elements/patchElement.js +1 -1
  19. package/dist/modules/mcp/tools/operations/schema/elements/repeatElement.js +148 -0
  20. package/dist/modules/mcp/tools/operations/schema/interactions/deleteInteraction.js +1 -1
  21. package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +1 -1
  22. package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.js +1 -1
  23. package/dist/modules/mcp/tools/operations/schema/operations.js +2 -0
  24. package/dist/modules/mcp/tools/operations/schema/pages/upsertPage.js +1 -1
  25. package/dist/modules/mcp/tools/operations/schema/shared.js +8 -5
  26. package/dist/modules/mcp/tools/operations/schemaIds.js +54 -0
  27. package/dist/modules/mcp/tools/operations/style/definitions/patchDefinition.js +1 -1
  28. package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinition.js +1 -1
  29. package/dist/modules/mcp/tools/operations/style/definitions/upsertDefinitions.js +40 -0
  30. package/dist/modules/mcp/tools/operations/style/globalStyles/patchGlobalStyle.js +1 -1
  31. package/dist/modules/mcp/tools/operations/style/globalStyles/upsertGlobalStyle.js +1 -1
  32. package/dist/modules/mcp/tools/operations/style/idStyles/patchIdStyle.js +1 -1
  33. package/dist/modules/mcp/tools/operations/style/idStyles/upsertIdStyle.js +1 -1
  34. package/dist/modules/mcp/tools/operations/style/operations.js +2 -0
  35. package/dist/modules/mcp/tools/operations/style/shared.js +1 -1
  36. package/dist/modules/mcp/tools/read.js +21 -3
  37. package/dist/modules/mcp/tools/render.js +70 -8
  38. package/dist/modules/mcp/tools/shared/expandOperations.js +44 -0
  39. package/dist/modules/mcp/tools/shared/tool.js +16 -11
  40. package/dist/modules/mcp/tools/shared/validator/audit.js +3 -0
  41. package/dist/modules/mcp/tools/shared/validator/batch.js +13 -4
  42. package/dist/modules/mcp/tools/shared/validator/index.js +15 -0
  43. package/dist/modules/mcp/tools/validate.js +11 -3
  44. package/dist/src/modules/mcp/apps/shared/assets.d.ts +10 -0
  45. package/dist/src/modules/mcp/apps/shared/bundle.d.ts +3 -0
  46. package/dist/src/modules/mcp/apps/shared/index.d.ts +1 -0
  47. package/dist/src/modules/mcp/e2e/index.d.ts +2 -2
  48. package/dist/src/modules/mcp/e2e/mcpEndpoint.d.ts +6 -1
  49. package/dist/src/modules/mcp/e2e/renderingHost.d.ts +16 -1
  50. package/dist/src/modules/mcp/helpers/guide.d.ts +2 -1
  51. package/dist/src/modules/mcp/helpers/space.d.ts +9 -0
  52. package/dist/src/modules/mcp/resources/index.d.ts +2 -1
  53. package/dist/src/modules/mcp/resources/register.d.ts +6 -2
  54. package/dist/src/modules/mcp/resources/renderGuide.d.ts +5 -0
  55. package/dist/src/modules/mcp/resources/router.d.ts +4 -0
  56. package/dist/src/modules/mcp/server.d.ts +6 -5
  57. package/dist/src/modules/mcp/tools/apply/index.d.ts +50 -0
  58. package/dist/src/modules/mcp/tools/operations/index.d.ts +100 -0
  59. package/dist/src/modules/mcp/tools/operations/schema/elements/repeatElement.d.ts +55 -0
  60. package/dist/src/modules/mcp/tools/operations/schema/index.d.ts +1 -0
  61. package/dist/src/modules/mcp/tools/operations/schema/operations.d.ts +19 -0
  62. package/dist/src/modules/mcp/tools/operations/schema/shared.d.ts +15 -0
  63. package/dist/src/modules/mcp/tools/operations/schemaIds.d.ts +6 -0
  64. package/dist/src/modules/mcp/tools/operations/style/definitions/upsertDefinitions.d.ts +41 -0
  65. package/dist/src/modules/mcp/tools/operations/style/index.d.ts +1 -0
  66. package/dist/src/modules/mcp/tools/operations/style/operations.d.ts +33 -0
  67. package/dist/src/modules/mcp/tools/operations/style/shared.d.ts +4 -3
  68. package/dist/src/modules/mcp/tools/preview.d.ts +50 -0
  69. package/dist/src/modules/mcp/tools/read.d.ts +1 -0
  70. package/dist/src/modules/mcp/tools/render.d.ts +421 -363
  71. package/dist/src/modules/mcp/tools/screenshot.d.ts +50 -0
  72. package/dist/src/modules/mcp/tools/shared/expandOperations.d.ts +13 -0
  73. package/dist/src/modules/mcp/tools/shared/tool.d.ts +8 -0
  74. package/dist/src/modules/mcp/tools/validate.d.ts +50 -0
  75. package/package.json +6 -5
  76. package/skills/plitzi-render/SKILL.md +170 -0
  77. package/dist/modules/mcp/apps/render/view.tsx +0 -108
  78. /package/dist/modules/mcp/apps/example/{view.tsx → view/index.tsx} +0 -0
  79. /package/dist/src/modules/mcp/{apps/example/view.d.ts → tests/schemaIds.test.d.ts} +0 -0
  80. /package/dist/src/modules/mcp/{apps/render/view.d.ts → tests/skill.test.d.ts} +0 -0
@@ -11,6 +11,24 @@ export declare const screenshotShape: {
11
11
  before: "before";
12
12
  after: "after";
13
13
  }>>;
14
+ }, z.core.$strip>, z.ZodObject<{
15
+ type: z.ZodLiteral<"repeatElement">;
16
+ pageRef: z.ZodString;
17
+ ref: z.ZodString;
18
+ elementType: z.ZodOptional<z.ZodString>;
19
+ label: z.ZodOptional<z.ZodString>;
20
+ style: z.ZodOptional<z.ZodObject<{
21
+ base: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
+ slots: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
23
+ }, z.core.$strip>>;
24
+ parentRef: z.ZodOptional<z.ZodString>;
25
+ position: z.ZodOptional<z.ZodEnum<{
26
+ inside: "inside";
27
+ before: "before";
28
+ after: "after";
29
+ }>>;
30
+ template: z.ZodType<import('./operations/schema').TemplateElement, unknown, z.core.$ZodTypeInternals<import('./operations/schema').TemplateElement, unknown>>;
31
+ items: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
14
32
  }, z.core.$strip>, z.ZodObject<{
15
33
  type: z.ZodLiteral<"patchElement">;
16
34
  pageRef: z.ZodString;
@@ -223,6 +241,38 @@ export declare const screenshotShape: {
223
241
  }, z.core.$strip>>>;
224
242
  type: z.ZodLiteral<"upsertDefinition">;
225
243
  ref: z.ZodString;
244
+ }, z.core.$strip>, z.ZodObject<{
245
+ type: z.ZodLiteral<"upsertDefinitions">;
246
+ definitions: z.ZodRecord<z.ZodString, z.ZodObject<{
247
+ desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
248
+ tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
249
+ mobile: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
250
+ states: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
251
+ desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
252
+ tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
253
+ mobile: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
254
+ }, z.core.$strip>>>;
255
+ variants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
256
+ desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
257
+ tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
258
+ mobile: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
259
+ }, z.core.$strip>>>;
260
+ slots: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
261
+ desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
262
+ tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
263
+ mobile: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
264
+ states: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
265
+ desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
266
+ tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
267
+ mobile: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
268
+ }, z.core.$strip>>>;
269
+ variants: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
270
+ desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
271
+ tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
272
+ mobile: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
273
+ }, z.core.$strip>>>;
274
+ }, z.core.$strip>>>;
275
+ }, z.core.$strip>>;
226
276
  }, z.core.$strip>, z.ZodObject<{
227
277
  desktop: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>>;
228
278
  tablet: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodNull]>>>;
@@ -0,0 +1,13 @@
1
+ import { ValidationError } from '../../types';
2
+ import { Operation } from '../operations';
3
+ /** Rewrites the sugar ops into the vocabulary the rest of the pipeline knows, BEFORE validation — today that is
4
+ * `repeatElement`, which stands for the `upsertElement` its template + rows expand to. Everything downstream
5
+ * (validator, dispatch, audit, versions) therefore stays unaware of it, and a row that renders a bad element is
6
+ * reported by the ordinary element checks.
7
+ *
8
+ * One op in, one op out: the indices the agent sent are the indices every error path names, so `operations[3]`
9
+ * still means the fourth operation it wrote. */
10
+ export declare const expandOperations: (ops: Operation[]) => {
11
+ operations: Operation[];
12
+ errors: ValidationError[];
13
+ };
@@ -39,6 +39,11 @@ export interface ToolSpec<Shape extends ZodRawShape> {
39
39
  * iframe, receiving the tool result). Registered as the tool's `_meta.ui` — see registerAppTool. */
40
40
  ui?: McpUiToolMeta;
41
41
  run: (input: z.infer<ZodObject<Shape>>, ctx: ToolContext) => unknown;
42
+ /** What the tool still does on a connection that carries NO space (a guest / widgets-only grant): it keeps only
43
+ * the part that needs none — plitzi_read serves the public documents and says so for the rest. A tool that
44
+ * defines this stays advertised there; one that does not is not registered at all, so the agent never calls
45
+ * something that cannot work. */
46
+ runPublic?: (input: z.infer<ZodObject<Shape>>, env: Env) => unknown;
42
47
  }
43
48
  /** What the registry holds and the hosts register from: the same metadata plus a type-erased `execute` that
44
49
  * validates raw args against the shape and delegates to the typed `run`. Produced by `defineTool`. */
@@ -52,6 +57,9 @@ export interface ToolDef {
52
57
  spaceless?: boolean;
53
58
  ui?: McpUiToolMeta;
54
59
  execute: (args: unknown, ctx: ToolContext) => unknown;
60
+ /** Present when the tool degrades to a space-less connection (see ToolSpec.runPublic); its absence is what tells
61
+ * the host not to advertise the tool there. */
62
+ executePublic?: (args: unknown, env: Env) => unknown;
55
63
  }
56
64
  /** Author a tool: give it its metadata, its input shape and a typed `run`. The returned descriptor parses the
57
65
  * raw args against the shape before handing them to `run`, so `run` is fully typed and no cast is needed.
@@ -12,6 +12,24 @@ export declare const validateShape: {
12
12
  before: "before";
13
13
  after: "after";
14
14
  }>>;
15
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
16
+ type: import('zod').ZodLiteral<"repeatElement">;
17
+ pageRef: import('zod').ZodString;
18
+ ref: import('zod').ZodString;
19
+ elementType: import('zod').ZodOptional<import('zod').ZodString>;
20
+ label: import('zod').ZodOptional<import('zod').ZodString>;
21
+ style: import('zod').ZodOptional<import('zod').ZodObject<{
22
+ base: import('zod').ZodOptional<import('zod').ZodArray<import('zod').ZodString>>;
23
+ slots: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodArray<import('zod').ZodString>>>;
24
+ }, import('zod/v4/core').$strip>>;
25
+ parentRef: import('zod').ZodOptional<import('zod').ZodString>;
26
+ position: import('zod').ZodOptional<import('zod').ZodEnum<{
27
+ inside: "inside";
28
+ before: "before";
29
+ after: "after";
30
+ }>>;
31
+ template: import('zod').ZodType<import('./operations/schema').TemplateElement, unknown, import('zod/v4/core').$ZodTypeInternals<import('./operations/schema').TemplateElement, unknown>>;
32
+ items: import('zod').ZodArray<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnknown>>;
15
33
  }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
16
34
  type: import('zod').ZodLiteral<"patchElement">;
17
35
  pageRef: import('zod').ZodString;
@@ -224,6 +242,38 @@ export declare const validateShape: {
224
242
  }, import('zod/v4/core').$strip>>>;
225
243
  type: import('zod').ZodLiteral<"upsertDefinition">;
226
244
  ref: import('zod').ZodString;
245
+ }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
246
+ type: import('zod').ZodLiteral<"upsertDefinitions">;
247
+ definitions: import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodObject<{
248
+ desktop: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
249
+ tablet: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
250
+ mobile: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
251
+ states: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodObject<{
252
+ desktop: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
253
+ tablet: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
254
+ mobile: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
255
+ }, import('zod/v4/core').$strip>>>;
256
+ variants: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodObject<{
257
+ desktop: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
258
+ tablet: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
259
+ mobile: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
260
+ }, import('zod/v4/core').$strip>>>;
261
+ slots: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodObject<{
262
+ desktop: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
263
+ tablet: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
264
+ mobile: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
265
+ states: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodObject<{
266
+ desktop: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
267
+ tablet: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
268
+ mobile: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
269
+ }, import('zod/v4/core').$strip>>>;
270
+ variants: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodObject<{
271
+ desktop: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
272
+ tablet: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
273
+ mobile: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber]>>>;
274
+ }, import('zod/v4/core').$strip>>>;
275
+ }, import('zod/v4/core').$strip>>>;
276
+ }, import('zod/v4/core').$strip>>;
227
277
  }, import('zod/v4/core').$strip>, import('zod').ZodObject<{
228
278
  desktop: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber, import('zod').ZodNull]>>>;
229
279
  tablet: import('zod').ZodOptional<import('zod').ZodRecord<import('zod').ZodString, import('zod').ZodUnion<readonly [import('zod').ZodString, import('zod').ZodNumber, import('zod').ZodNull]>>>;
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@plitzi/sdk-server",
3
- "version": "0.32.17",
3
+ "version": "0.32.19",
4
4
  "license": "AGPL-3.0",
5
5
  "files": [
6
- "dist"
6
+ "dist",
7
+ "skills"
7
8
  ],
8
9
  "main": "dist/index.js",
9
10
  "module": "dist/index.js",
@@ -29,9 +30,9 @@
29
30
  "dependencies": {
30
31
  "@modelcontextprotocol/ext-apps": "^1.7.5",
31
32
  "@modelcontextprotocol/sdk": "^1.30.0",
32
- "@plitzi/plitzi-sdk": "0.32.17",
33
- "@plitzi/sdk-schema": "0.32.17",
34
- "@plitzi/sdk-shared": "0.32.17",
33
+ "@plitzi/plitzi-sdk": "0.32.19",
34
+ "@plitzi/sdk-schema": "0.32.19",
35
+ "@plitzi/sdk-shared": "0.32.19",
35
36
  "ejs": "^6.0.1",
36
37
  "esbuild": "^0.28.1",
37
38
  "zod": "^4.4.3"
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: plitzi-render
3
+ description: >-
4
+ Show answers as a rendered visual widget instead of plain text, using the Plitzi MCP's plitzi_render tool.
5
+ Use whenever the reply is naturally visual or structured — a recipe, a comparison, pricing tiers, a profile,
6
+ a menu, steps/checklist, a product card, a dashboard-like summary — or whenever the user asks to design,
7
+ build, show, or "make it look nice". Requires the Plitzi MCP server (the plitzi_render tool) to be connected.
8
+ ---
9
+
10
+ # Rendering answers as Plitzi widgets
11
+
12
+ `plitzi_render` renders a small, self-contained UI widget the user can SEE. It runs the Plitzi SDK offline — no
13
+ backend, no account, no space, so it works even on a connection with no credentials. Prefer **showing** a widget
14
+ over a long text answer whenever the content is naturally visual.
15
+
16
+ ## When to reach for it
17
+
18
+ - **Something to design or build**: a card, hero, banner, form, pricing table — anything the user asks you to
19
+ "create / design / build / show / make look nice".
20
+ - **A naturally visual answer**: a recipe → a card with photo + ingredients; a comparison → side-by-side tiles;
21
+ steps → a checklist; a product, place or person → a profile card; options → a tiled menu.
22
+
23
+ Keep writing plain text when the answer is genuinely textual — an explanation, code, a discussion. One good widget
24
+ beats a wall of text, but a widget wrapping a paragraph helps no one.
25
+
26
+ ## Before the first call
27
+
28
+ Read the MCP resource **`plitzi://render/guide`** once per conversation. It is the authoritative authoring model:
29
+ every operation, the element types and their props, the styling system, worked examples. `plitzi://render/types`
30
+ lists every element type with its description. Don't reconstruct the schema from memory — read the guide.
31
+
32
+ ## The shape of a good call
33
+
34
+ Everything the widget needs travels in ONE call, as `operations`. Three ops carry almost every widget:
35
+
36
+ - **`upsertDefinitions`** — declare ALL the CSS classes at once, one class per *look* (not per property).
37
+ - **`upsertElement`** — build the whole tree in a single op, nesting with `children` under `pageRef: "render"`.
38
+ - **`repeatElement`** — the moment two siblings differ only in data, write the row ONCE as a template with
39
+ `{{item.field}}` placeholders and pass `items`. Rows come out numbered (`tile-1`, `tile-2`…). A list inside each
40
+ row is the same op: give a template node `repeat: { items: "{{item.<field>}}", template: … }`.
41
+
42
+ ```json
43
+ {
44
+ "operations": [
45
+ {
46
+ "type": "upsertDefinitions",
47
+ "definitions": {
48
+ "panel": {
49
+ "desktop": {
50
+ "display": "flex",
51
+ "flex-direction": "column",
52
+ "gap": "12px",
53
+ "padding": "16px",
54
+ "min-width": "0",
55
+ "color": "var(--color-text-primary, light-dark(#0f172a, #e8eaed))"
56
+ }
57
+ },
58
+ "row": {
59
+ "desktop": { "display": "flex", "flex-wrap": "wrap", "gap": "12px", "min-width": "0" },
60
+ "mobile": { "flex-direction": "column" }
61
+ },
62
+ "tile": {
63
+ "desktop": {
64
+ "flex": "1 1 0%",
65
+ "min-width": "160px",
66
+ "padding": "12px",
67
+ "border-radius": "10px",
68
+ "border": "1px solid var(--color-border-primary, light-dark(#e2e8f0, #333a48))",
69
+ "background-color": "var(--color-background-secondary, light-dark(#ffffff, #1f2430))"
70
+ }
71
+ },
72
+ "tile-title": { "desktop": { "margin": "0", "font-size": "15px" } },
73
+ "tile-price": { "desktop": { "margin": "0", "font-size": "13px", "opacity": "0.75" } }
74
+ }
75
+ },
76
+ {
77
+ "type": "upsertElement",
78
+ "pageRef": "render",
79
+ "element": {
80
+ "ref": "panel",
81
+ "type": "container",
82
+ "style": { "base": ["panel"] },
83
+ "children": [{ "ref": "title", "type": "heading", "subType": "h3", "props": { "content": "Plans" } }]
84
+ }
85
+ },
86
+ {
87
+ "type": "repeatElement",
88
+ "pageRef": "render",
89
+ "parentRef": "panel",
90
+ "ref": "plans",
91
+ "style": { "base": ["row"] },
92
+ "template": {
93
+ "ref": "tile",
94
+ "type": "container",
95
+ "style": { "base": ["tile"] },
96
+ "children": [
97
+ {
98
+ "ref": "name",
99
+ "type": "heading",
100
+ "subType": "h4",
101
+ "props": { "content": "{{item.name}}" },
102
+ "style": { "base": ["tile-title"] }
103
+ },
104
+ {
105
+ "ref": "price",
106
+ "type": "paragraph",
107
+ "props": { "content": "{{item.price}}" },
108
+ "style": { "base": ["tile-price"] }
109
+ }
110
+ ]
111
+ },
112
+ "items": [
113
+ { "name": "Starter", "price": "$0 / month" },
114
+ { "name": "Team", "price": "$19 / month" },
115
+ { "name": "Business", "price": "$49 / month" }
116
+ ]
117
+ }
118
+ ]
119
+ }
120
+ ```
121
+
122
+ ## The five things that decide whether it looks good
123
+
124
+ 1. **Width is free, height is scarce.** The widget renders in a side panel. A plain container stacks its children
125
+ vertically — that is the tall, half-empty default to avoid. Put peers (metrics, plans, options, image + text) in
126
+ a wrapping row (`display:flex` + `flex-wrap:wrap`, children `flex:1 1 0%` with a `min-width` as the wrap
127
+ threshold) or a grid with `grid-template-columns: repeat(auto-fit, minmax(160px, 1fr))`. Stack only what reads in
128
+ order: heading over paragraph, forms, steps, prose.
129
+ 2. **The host may be in dark mode.** Never hardcode a light palette. Take colours from the host variables with a
130
+ `light-dark()` fallback, and always set `color` wherever you set `background-color`.
131
+ 3. **Watch the SDK defaults.** Every container has `min-width`/`min-height: 50px` — set them to `0` for rails,
132
+ dividers, dots and any flex child that must shrink. Headings and paragraphs keep the browser's margins; zero
133
+ them and space with the parent's `gap`.
134
+ 4. **Never hand-draw a scene in a `data:` URI.** An inline SVG illustration costs more tokens than the entire rest
135
+ of the widget. Use an `https` image, a flat colour, or a CSS gradient.
136
+ 5. **Write CSS plainly.** Kebab-case properties, shorthands welcome (`padding: 8px 16px`, `border: 1px solid red`,
137
+ `font: bold 16px/1.5 Arial`) — they are expanded and stored as longhands, so a breakpoint or state can override
138
+ one property on its own.
139
+
140
+ ## Iterating on a widget you already rendered
141
+
142
+ Every render answers with a **`renderId`**. To change that widget, do NOT rebuild it: call again with
143
+ `patch: true`, that `renderId`, and only the operations that differ (`patchElement`, `patchDefinition`,
144
+ `deleteElement`, a new `repeatElement`). Address rows by the refs you already know (`tile-1`, `tile-2`). The widget
145
+ merges the delta into the batch it was built from and reports back what it applied, errors included.
146
+
147
+ Patch **only** to modify that widget. A different subject, or a different kind of widget, is a fresh render with no
148
+ `patch` — the delta is merged into the previous batch, so patching a new idea leaves the user looking at both at
149
+ once. Rebuilding when you could have patched only costs tokens; patching when you should have rebuilt costs the
150
+ user a wrong widget.
151
+
152
+ If the answer says the widget could not be recovered (a surface that renders no widgets, a host that keeps no
153
+ storage, a conversation resumed elsewhere), send the whole batch again without `patch`.
154
+
155
+ ## When a call fails
156
+
157
+ `plitzi_render` answers with `rendered: false` and `errors: [{ path, message, hint }]`, plus `warnings` for smaller
158
+ issues. The `path` names the operation, so fix that one and call again — you never lose the rest of the batch. An
159
+ unknown prop comes back as a warning naming the right one, so probing is safe.
160
+
161
+ ## After it renders
162
+
163
+ The widget is shown to the user; you get a compact summary. Don't re-describe what they can already see — a short
164
+ caption or a follow-up question is enough.
165
+
166
+ ## Going further
167
+
168
+ Widgets can be data-driven and interactive, not only static: an `apiContainer` fetches at runtime, `upsertBinding`
169
+ wires that data into elements, and `upsertInteractionFlow` makes them react to clicks. See the "Data &
170
+ interactivity" section of `plitzi://render/guide`.
@@ -1,108 +0,0 @@
1
- /* eslint-disable react-refresh/only-export-components -- one bundled entry: components cannot move out. */
2
- import { useApp, useHostStyles } from '@modelcontextprotocol/ext-apps/react';
3
- import PlitziSdk from '@plitzi/plitzi-sdk';
4
- import { Component, useEffect, useState } from 'react';
5
- import { createRoot } from 'react-dom/client';
6
-
7
- import type { McpUiHostContext } from '@modelcontextprotocol/ext-apps';
8
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
9
- import type { OfflineDataRaw } from '@plitzi/sdk-shared';
10
- import type { ReactNode } from 'react';
11
-
12
- /** The view for plitzi_render: the entry the server bundles (React, the MCP Apps runtime and the Plitzi SDK
13
- * included) and inlines in the shared ui:// page shell — which is what lets it run in a sandbox that can fetch
14
- * nothing. It mounts on `#app`, the shell's root. */
15
-
16
- const panelStyle = { padding: 16, font: '13px/1.5 system-ui, sans-serif', color: '#b91c1c' } as const;
17
-
18
- // Every failure ends up here: an iframe left on its placeholder would hide the reason in the sandbox's console.
19
- const ErrorPanel = ({ title, details }: { title: string; details: string }) => (
20
- <div style={panelStyle}>
21
- <strong>{title}</strong>
22
- <pre style={{ margin: '8px 0 0', whiteSpace: 'pre-wrap', color: '#7f1d1d', fontSize: 12 }}>{details}</pre>
23
- </div>
24
- );
25
-
26
- class RenderBoundary extends Component<{ children: ReactNode }, { error?: Error }> {
27
- state: { error?: Error } = {};
28
-
29
- static getDerivedStateFromError(error: Error) {
30
- return { error };
31
- }
32
-
33
- render() {
34
- if (this.state.error) {
35
- return <ErrorPanel title="Widget failed to render" details={this.state.error.message} />;
36
- }
37
-
38
- return this.props.children;
39
- }
40
- }
41
-
42
- const RenderApp = () => {
43
- const [result, setResult] = useState<CallToolResult | null>(null);
44
- const [cancelled, setCancelled] = useState<string | undefined>(undefined);
45
- const [context, setContext] = useState<McpUiHostContext | undefined>(undefined);
46
-
47
- // useApp creates the App, runs onAppCreated so every handler is in place BEFORE the handshake, and connects.
48
- const { app, error } = useApp({
49
- appInfo: { name: 'Plitzi Widget', version: '1.0.0' },
50
- capabilities: {},
51
- onAppCreated: instance => {
52
- instance.ontoolresult = setResult;
53
- instance.ontoolcancelled = params => setCancelled(params.reason ?? 'The host cancelled the render.');
54
- instance.onhostcontextchanged = params => setContext(previous => ({ ...previous, ...params }));
55
- instance.onteardown = () => ({});
56
- }
57
- });
58
-
59
- // The context the host sent in its initialize result predates any change notification.
60
- useEffect(() => {
61
- if (app) {
62
- setContext(app.getHostContext());
63
- }
64
- }, [app]);
65
-
66
- useHostStyles(app, context);
67
-
68
- const failure = error?.message ?? cancelled;
69
- if (failure) {
70
- return <ErrorPanel title="Could not render the widget" details={failure} />;
71
- }
72
-
73
- // Still connecting, or connected and waiting for the result: the page's CSS placeholder covers both.
74
- if (!result) {
75
- return null;
76
- }
77
-
78
- // The payload rides in structuredContent, which the protocol types as unknown-valued; plitzi_render is the one
79
- // guaranteeing the shape, and the SDK validates it again when it renders.
80
- const offlineData = result.structuredContent?.offlineData as OfflineDataRaw | undefined;
81
- if (!offlineData) {
82
- // A failed render answers with its (already compact) reasons as the tool's JSON text summary.
83
- const text = result.content.find(entry => entry.type === 'text');
84
-
85
- return <ErrorPanel title="Render failed" details={text?.text ?? 'The tool returned no widget data.'} />;
86
- }
87
-
88
- const insets = context?.safeAreaInsets;
89
-
90
- return (
91
- <div
92
- style={{
93
- padding: `${insets?.top ?? 0}px ${insets?.right ?? 0}px ${insets?.bottom ?? 0}px ${insets?.left ?? 0}px`
94
- }}
95
- >
96
- <RenderBoundary>
97
- <PlitziSdk offlineData={offlineData} offlineMode environment="main" renderMode="raw" />
98
- </RenderBoundary>
99
- </div>
100
- );
101
- };
102
-
103
- const root = document.getElementById('app');
104
- if (root) {
105
- // The SDK styles its tree from this container class down.
106
- root.className = 'plitzi-root-container';
107
- createRoot(root).render(<RenderApp />);
108
- }