mlola-ui 1.9.2 → 1.10.0

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/src/mcp.js CHANGED
@@ -2,7 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import readline from "node:readline";
4
4
  import { projectContract, projectThemes } from "./check.js";
5
- import { checkMarkup, describeItem, designData, designGuide, searchItems, suggest, themes, tokens } from "./knowledge.js";
5
+ import { checkMarkup, describeItem, designData, designGuide, renderApp, renderCatalog, renderRules, searchItems, suggest, themes, tokens } from "./knowledge.js";
6
+ import { formatRenderReport, renderCatalogGuide, validateRender } from "../registry/render.js";
6
7
 
7
8
  /**
8
9
  * `mlola-ui mcp` — a Model Context Protocol server over stdio.
@@ -21,11 +22,28 @@ import { checkMarkup, describeItem, designData, designGuide, searchItems, sugges
21
22
 
22
23
  const PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
23
24
 
25
+ const RENDER_INSTRUCTIONS = "To show UI at runtime instead of writing code (a form or a status card inside a chat or an agent's app), compose A2UI messages with Mlola Render: read get_render_catalog once, then run check_render on the messages and fix every error before sending them. In a host that shows MCP Apps, render_ui shows the surface in the conversation, and what the person presses comes back to you as their next message.";
26
+
27
+ /** Mlola Render's MCP App: the view render_ui names (scripts/build-render-app.mjs). */
28
+ const RENDER_APP = { uri: "ui://mlola/render", name: "Mlola Render", description: "Shows the A2UI surfaces render_ui is called with, drawn with Mlola components; a pressed button comes back as a message.", mimeType: "text/html;profile=mcp-app" };
29
+
30
+ /** The resources both servers serve, with what reading each answers: its text, type and metadata. */
31
+ const SHARED_RESOURCES = {
32
+ "mlola://guide": { listed: { uri: "mlola://guide", name: "Mlola UI design guide", description: "Classes, attributes, tokens and rules, generated from the stylesheet.", mimeType: "text/markdown" }, read: () => designGuide() ?? "" },
33
+ [RENDER_APP.uri]: {
34
+ listed: RENDER_APP,
35
+ // No connection, no outside resource: the page holds everything it draws with. No border: a surface lays its own ground.
36
+ read: () => ({ text: renderApp() ?? "", mimeType: RENDER_APP.mimeType, _meta: { ui: { prefersBorder: false } } }),
37
+ },
38
+ };
39
+
24
40
  const INSTRUCTIONS = `This project's UI is Mlola UI: native CSS classes (ml-*), data-* attributes for state and variant, and --ml-* tokens. There is no Tailwind.
25
- Before writing UI: call get_design_rules once, then search_components for what you need and get_component for how to use it. Read tokens with get_tokens instead of writing colors, sizes, shadows or durations. After writing markup, run check_markup on it and fix what it reports.`;
41
+ Before writing UI: call get_design_rules once, then search_components for what you need and get_component for how to use it. Read tokens with get_tokens instead of writing colors, sizes, shadows or durations. After writing markup, run check_markup on it and fix what it reports.
42
+ ${RENDER_INSTRUCTIONS}`;
26
43
 
27
44
  const REMOTE_INSTRUCTIONS = `Mlola UI is a component system on native CSS classes (ml-*), data-* attributes for state and variant, and --ml-* tokens. There is no Tailwind.
28
- Before writing UI: call get_design_rules once, then search_components for what you need and get_component for how to use it. Read tokens with get_tokens. After writing markup, run check_markup and fix what it reports. This server cannot write files: get_install_command gives the commands to run in the project, and Mlola Pro items need a license token (npx mlola-ui login). For a page with no build step (one HTML file), get_design_rules gives the stylesheet link and the script to paste.`;
45
+ Before writing UI: call get_design_rules once, then search_components for what you need and get_component for how to use it. Read tokens with get_tokens. After writing markup, run check_markup and fix what it reports. This server cannot write files: get_install_command gives the commands to run in the project, and Mlola Pro items need a license token (npx mlola-ui login). For a page with no build step (one HTML file), get_design_rules gives the stylesheet link and the script to paste.
46
+ ${RENDER_INSTRUCTIONS}`;
29
47
 
30
48
  const text = (value) => ({ content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }] });
31
49
 
@@ -114,6 +132,68 @@ function readTools({ cwd, version, project = false }) {
114
132
  return text(issues.length ? issues : "No issues: every Mlola class exists and every data-* value is one its element reacts to.");
115
133
  },
116
134
  },
135
+ {
136
+ name: "get_render_catalog",
137
+ title: "Mlola Render catalog",
138
+ description: "The components an agent may compose at runtime with Mlola Render, as A2UI v0.9 messages: each component's props and allowed values, how surfaces, data binding and actions work, and complete examples. Read once before composing a surface. format \"schema\" returns the A2UI catalog (JSON Schema) instead.",
139
+ inputSchema: { type: "object", properties: { format: { type: "string", enum: ["guide", "schema"], default: "guide" } }, additionalProperties: false },
140
+ annotations: { readOnlyHint: true },
141
+ handler: ({ format = "guide" }) => {
142
+ const rules = renderRules();
143
+ if (!rules) throw new Error("This build of mlola-ui carries no Mlola Render catalog.");
144
+ return text(format === "schema" ? renderCatalog() : renderCatalogGuide(rules));
145
+ },
146
+ },
147
+ {
148
+ name: "check_render",
149
+ title: "Check Mlola Render messages",
150
+ description: "Checks A2UI messages (createSurface, updateComponents, updateDataModel, deleteSurface) against the Mlola Render catalog before they render: components and props that do not exist, values a component does not offer, colors or styles set by hand, fields and controls without a name, a missing root, children that do not exist or form a loop, headings that skip a level, values outside a range. Each error names its JSON Pointer path and the fix. Pass every message of the surface: one object, an array, or JSON Lines.",
151
+ inputSchema: {
152
+ type: "object",
153
+ properties: {
154
+ messages: {
155
+ anyOf: [{ type: "array", items: { type: "object" } }, { type: "object" }, { type: "string" }],
156
+ description: "The A2UI messages: an array of message objects, one message, or their JSON text (JSON Lines works too).",
157
+ },
158
+ },
159
+ required: ["messages"],
160
+ additionalProperties: false,
161
+ },
162
+ annotations: { readOnlyHint: true },
163
+ handler: ({ messages }) => {
164
+ const rules = renderRules();
165
+ if (!rules) throw new Error("This build of mlola-ui carries no Mlola Render catalog.");
166
+ return text(formatRenderReport(validateRender(messages, rules)));
167
+ },
168
+ },
169
+ {
170
+ name: "render_ui",
171
+ title: "Show Mlola Render UI",
172
+ description: "Shows an interface in the conversation: a form, a status card, a table of results. Pass A2UI messages composed from the Mlola Render catalog (read get_render_catalog first); they are checked, and drawn with Mlola components where the host shows MCP Apps. When the person presses a button, their next message carries the A2UI action with what they entered. Where the host shows no apps, this answers with the check's report.",
173
+ inputSchema: {
174
+ type: "object",
175
+ properties: {
176
+ messages: {
177
+ anyOf: [{ type: "array", items: { type: "object" } }, { type: "object" }, { type: "string" }],
178
+ description: "The A2UI messages of the surface: createSurface, updateComponents, updateDataModel.",
179
+ },
180
+ },
181
+ required: ["messages"],
182
+ additionalProperties: false,
183
+ },
184
+ annotations: { readOnlyHint: true },
185
+ _meta: { ui: { resourceUri: RENDER_APP.uri } },
186
+ handler: ({ messages }) => {
187
+ const rules = renderRules();
188
+ if (!rules) throw new Error("This build of mlola-ui carries no Mlola Render catalog.");
189
+ const result = validateRender(messages, rules);
190
+ const report = formatRenderReport(result);
191
+ return {
192
+ ...text(result.valid ? `${report}\nShown to the person. What they press comes back as their next message, with the A2UI action.` : `${report}\nNothing was shown. Fix the errors and call render_ui again.`),
193
+ structuredContent: { valid: result.valid, surfaces: result.surfaces, errors: result.errors },
194
+ };
195
+ },
196
+ },
117
197
  ];
118
198
  }
119
199
 
@@ -159,7 +239,7 @@ function tools({ cwd, run, version }) {
159
239
  }
160
240
 
161
241
  function resources(cwd) {
162
- const list = [{ uri: "mlola://guide", name: "Mlola UI design guide", description: "Classes, attributes, tokens and rules, generated from the stylesheet.", mimeType: "text/markdown" }];
242
+ const list = Object.values(SHARED_RESOURCES).map((resource) => resource.listed);
163
243
  if (fs.existsSync(path.join(cwd, "mlola-pro.agents.md"))) {
164
244
  list.push({ uri: "mlola://guide/pro", name: "Mlola Pro design guide", description: "The classes and attributes of the Pro items installed here.", mimeType: "text/markdown" });
165
245
  }
@@ -167,7 +247,7 @@ function resources(cwd) {
167
247
  }
168
248
 
169
249
  function readResource(uri, cwd) {
170
- if (uri === "mlola://guide") return designGuide() ?? "";
250
+ if (Object.hasOwn(SHARED_RESOURCES, uri)) return SHARED_RESOURCES[uri].read();
171
251
  if (uri === "mlola://guide/pro") return fs.readFileSync(path.join(cwd, "mlola-pro.agents.md"), "utf8");
172
252
  throw Object.assign(new Error(`Unknown resource ${uri}`), { code: -32002 });
173
253
  }
@@ -265,8 +345,11 @@ export function createMcpHandler({ tools: available, listResources, readResource
265
345
  return reply(id, { resources: listResources() });
266
346
  case "resources/templates/list":
267
347
  return reply(id, { resourceTemplates: [] });
268
- case "resources/read":
269
- return reply(id, { contents: [{ uri: params?.uri, mimeType: "text/markdown", text: read(params?.uri) }] });
348
+ case "resources/read": {
349
+ // A resource answers with its text, or with its text, type and metadata (the MCP App).
350
+ const content = read(params?.uri);
351
+ return reply(id, { contents: [typeof content === "string" ? { uri: params?.uri, mimeType: "text/markdown", text: content } : { uri: params?.uri, ...content }] });
352
+ }
270
353
  case "prompts/list":
271
354
  return reply(id, { prompts: PROMPTS });
272
355
  case "prompts/get":
@@ -316,9 +399,9 @@ export function remoteMcpHandler({ version }) {
316
399
  };
317
400
  return createMcpHandler({
318
401
  tools: [...readTools({ cwd, version }), install],
319
- listResources: () => [{ uri: "mlola://guide", name: "Mlola UI design guide", description: "Classes, attributes, tokens and rules, generated from the stylesheet.", mimeType: "text/markdown" }],
402
+ listResources: () => Object.values(SHARED_RESOURCES).map((resource) => resource.listed),
320
403
  readResource: (uri) => {
321
- if (uri === "mlola://guide") return designGuide() ?? "";
404
+ if (Object.hasOwn(SHARED_RESOURCES, uri)) return SHARED_RESOURCES[uri].read();
322
405
  throw Object.assign(new Error(`Unknown resource ${uri}`), { code: -32002 });
323
406
  },
324
407
  version,
@@ -0,0 +1,5 @@
1
+ /** Types for mlola-ui/render/rules: the catalog the check reads (registry/render-rules.json). */
2
+ import type { RenderRules } from "../registry/render.js";
3
+
4
+ declare const rules: RenderRules;
5
+ export default rules;