tsquare 0.6.1 → 0.7.1

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1
4
+
5
+ - **`share_wireframe` returns only the links asked for.** A new `links` option takes the kinds from the playground's Copy link menu, with the same names: `share`, `svg`, `png`, `markdown`, `html`. The default is `["svg", "share"]`, an image link and the link to edit it; before, every call returned four links. Each link carries the whole wireframe (about 1,200 characters for a three-screen board), and the model has to write out every link it passes on, exactly, so fewer links means less to write and fewer chances to break one. A wireframe too long for an image link gets its share link instead, with a note saying why. New exports: `LINK_KINDS`, `DEFAULT_LINKS`, `LinkKind`.
6
+
7
+ - **Invalid values name the prop and the value.** A wrong `key=value` used to come back as Zod's bare message, which didn't say which prop on the line was wrong: `Input: Invalid option: expected one of "text"|"password"|…`. Now: `Input: type=otp isn't an option; use one of text, password, search, email, date, code`. Likewise `level=4 isn't an option; use one of 1, 2, 3`, `height=tall should be a number`, `columns=Name should be a list like [a, b]`, `range=[1] needs at least 2 items`, `columns=0 is too small (at least 1)`. This matters most in the MCP repair loop, where the model fixes what the messages say.
8
+ - **Fix: the HTML link escapes `&`** (and `<`, `>`) in the board title, in both the playground's Copy link → HTML and the MCP share tool, which now share one helper. A title like `Tom &amp; Jerry` used to show as "Tom & Jerry".
9
+ - **One wrong value is one error.** A check that uses a prop's value is skipped when that value is already invalid: `input type=otp digits=6` no longer also says "digits only applies to type=code". Checks that only notice a prop is set still run (`leadingIcon` on an avatar item is wrong whatever the icon's name).
10
+
11
+ ## 0.7.0
12
+
13
+ ### MCP tools
14
+
15
+ - **Hosted MCP server at `https://tsquare.dev/mcp`** (in the site repo), with three tools: `wireframe_guide` (the language plus a write → render → fix → share workflow), `render_wireframe` (a PNG for the model to look at, at most 1600px on its longest side, or line-numbered problems) and `share_wireframe` (SVG, PNG, playground and Markdown links). Setup for Claude, Claude Code, Cursor and VS Code is in [Using it with AI](docs/ai.md#connect-over-mcp).
16
+ - **The tools live in the library** as plain functions with no MCP SDK, so a local server can reuse them: `mcpTools` (names, descriptions, Zod input schemas, annotations), `MCP_INSTRUCTIONS`, `wireframeGuide()`, `renderWireframeTool()`, `shareWireframeTool()` and `callMcpTool(name, args)`. Results use the MCP tool-result shape; problems come back as `isError` results, not exceptions, so the model can read and fix them. Share links drop a surrounding code fence.
17
+ - `wireframePrompt()` is unchanged; its language part is now shared with the guide.
18
+
19
+ ### Errors
20
+
21
+ - **A value under the wrong prop points at the right one:** `image type=map` now says "for an Image, write kind=map (or just map)"; likewise `input kind=date` → `type=date` and `chart type=pie` → `kind=pie`.
22
+ - Grammar: "a Input" → "an Input" in error messages.
23
+
3
24
  ## 0.6.1
4
25
 
5
26
  - **Fix: the playground opens 0.6.0 share links.** 0.6.0 made share links with the new `y` prefix, but its playground only opened links starting with `z`. Opening a new share link left the editor on whatever it showed before. The prefixes now live in one place that both the library and the playground use, and a test guards it. Links themselves didn't change: every `y` link made by 0.6.0 opens correctly now.
@@ -1892,7 +1892,7 @@ for (const [name, def] of Object.entries(componentDefinitions)) {
1892
1892
  for (const v of ambiguous) enumValues.delete(v);
1893
1893
  COMPONENTS.set(name.toLowerCase(), { name, props: shape, enumValues, ambiguous, booleans: booleans2 });
1894
1894
  }
1895
- function belongsElsewhere(component, word, asProp) {
1895
+ function belongsElsewhere(component, word, asProp, value) {
1896
1896
  const info = COMPONENTS.get(component.toLowerCase());
1897
1897
  if (!info) return "";
1898
1898
  const props = [], options = [];
@@ -1903,7 +1903,9 @@ function belongsElsewhere(component, word, asProp) {
1903
1903
  }
1904
1904
  const names = (xs) => xs.length > 3 ? `${xs.slice(0, 3).join(", ")}\u2026` : xs.join(", ").replace(/, ([^,]*)$/, " and $1");
1905
1905
  const a = (name) => /^[AEIOU]/.test(name) ? `an ${name}` : `a ${name}`;
1906
- let msg = props.length ? `${word} is a ${names(props)} prop, not ${a(info.name)} one` : options.length ? `${word} is a ${names(options)} option, not ${a(info.name)} one` : "";
1906
+ let msg = props.length ? `${word} is ${a(names(props))} prop, not ${a(info.name)} one` : options.length ? `${word} is ${a(names(options))} option, not ${a(info.name)} one` : "";
1907
+ const right = typeof value === "string" ? info.enumValues.get(value) : void 0;
1908
+ if (asProp && right && right !== word) msg += `${msg ? "; " : ""}for ${a(info.name)}, write ${right}=${value} (or just ${value})`;
1907
1909
  const width = info.props.width;
1908
1910
  if (msg && word === "fullWidth" && /fills the space/i.test(width?.description ?? unwrap(width)?.description ?? "")) {
1909
1911
  msg += `; ${a(info.name)} already fills its width (width=\u2026 sets a fixed one), so remove it`;
@@ -2134,11 +2136,12 @@ function parseWireframeText(source) {
2134
2136
  }
2135
2137
  continue;
2136
2138
  }
2139
+ const value = parseValue(c);
2137
2140
  if (!(key in info.props)) {
2138
- const elsewhere = belongsElsewhere(info.name, key, true);
2141
+ const elsewhere = belongsElsewhere(info.name, key, true, value);
2139
2142
  issues.push({ line: lineNo, message: `${info.name} has no prop "${key}"` + (elsewhere ? `: ${elsewhere}` : "") + ` (its props: ${Object.keys(info.props).join(", ")})` });
2140
2143
  }
2141
- props[key] = key in info.props ? textWhereExpected(info.props[key], parseValue(c)) : parseValue(c);
2144
+ props[key] = key in info.props ? textWhereExpected(info.props[key], value) : value;
2142
2145
  continue;
2143
2146
  }
2144
2147
  c.next();
@@ -2681,6 +2684,38 @@ function listItemKindHint(el, path) {
2681
2684
  if (value === "icon" || !kinds.includes(value)) return void 0;
2682
2685
  return `${value} is a ${side} kind, not an icon name: write ${side}=${value} instead of ${prop}=${value}`;
2683
2686
  }
2687
+ function written(v) {
2688
+ if (typeof v === "string") return /^[^\s"[\]{}=,]+$/.test(v) ? v : JSON.stringify(v);
2689
+ if (Array.isArray(v)) return `[${v.map(written).join(", ")}]`;
2690
+ return JSON.stringify(v) ?? String(v);
2691
+ }
2692
+ function propIssueMessage(props, issue) {
2693
+ const path = issue.path ?? [];
2694
+ if (!path.length || issue.code === "custom") return issue.message;
2695
+ const key = path.map(String).join(".");
2696
+ const value = path.reduce((v, k) => v == null ? v : v[k], props);
2697
+ const given = `${key}=${written(value)}`;
2698
+ switch (issue.code) {
2699
+ case "invalid_value":
2700
+ return `${given} isn't an option; use one of ${(issue.values ?? []).map(written).join(", ")}`;
2701
+ case "invalid_union": {
2702
+ const options = (issue.errors ?? []).flat().flatMap((e) => e.code === "invalid_value" ? e.values : []);
2703
+ if (options.length) return `${given} isn't an option; use one of ${options.map(written).join(", ")}`;
2704
+ return `${given} isn't a valid value`;
2705
+ }
2706
+ case "invalid_type":
2707
+ return `${given} should be ${issue.expected === "array" ? "a list like [a, b]" : `a ${issue.expected}`}`;
2708
+ case "too_small":
2709
+ case "too_big": {
2710
+ const [bound, n] = issue.code === "too_small" ? ["at least", issue.minimum] : ["at most", issue.maximum];
2711
+ if (issue.origin === "array") return `${given} needs ${bound} ${n} item${n == 1 ? "" : "s"}`;
2712
+ if (issue.origin === "string") return `${given} needs ${bound} ${n} character${n == 1 ? "" : "s"}`;
2713
+ return `${given} is too ${issue.code === "too_small" ? "small" : "large"} (${bound} ${n})`;
2714
+ }
2715
+ default:
2716
+ return `${given}: ${issue.message}`;
2717
+ }
2718
+ }
2684
2719
  function checkSpec(spec) {
2685
2720
  upgradeSpec(spec);
2686
2721
  const issues = [];
@@ -2702,13 +2737,16 @@ function checkSpec(spec) {
2702
2737
  if (root.type !== "Board") issues.push(`root must be a Board (got ${root.type})`);
2703
2738
  for (const [id, el] of Object.entries(spec.elements)) {
2704
2739
  const def = componentDefinitions[el.type];
2740
+ const bad = /* @__PURE__ */ new Set();
2741
+ const failed = (...keys) => keys.some((k) => bad.has(k));
2705
2742
  if (!def) {
2706
2743
  issues.push(`${id}: ${unknownComponentMessage(el.type, Object.keys(componentDefinitions))}`);
2707
2744
  } else {
2708
2745
  const parsed = def.props.safeParse(el.props ?? {});
2709
2746
  if (!parsed.success) {
2710
2747
  for (const i of parsed.error.issues) {
2711
- issues.push(`${id}.props${i.path.length ? "." + i.path.join(".") : ""}: ${listItemKindHint(el, i.path) ?? i.message}`);
2748
+ if (i.path.length) bad.add(String(i.path[0]));
2749
+ issues.push(`${id}.props${i.path.length ? "." + i.path.join(".") : ""}: ${listItemKindHint(el, i.path) ?? propIssueMessage(el.props ?? {}, i)}`);
2712
2750
  }
2713
2751
  }
2714
2752
  for (const key of Object.keys(el.props ?? {})) {
@@ -2726,7 +2764,7 @@ function checkSpec(spec) {
2726
2764
  continue;
2727
2765
  }
2728
2766
  if (!(key in def.props.shape)) {
2729
- const elsewhere = belongsElsewhere(el.type, key, true);
2767
+ const elsewhere = belongsElsewhere(el.type, key, true, el.props[key]);
2730
2768
  issues.push(`${id}.props: ${el.type} has no prop "${key}"${elsewhere ? `: ${elsewhere}` : ""}`);
2731
2769
  }
2732
2770
  }
@@ -2747,50 +2785,50 @@ function checkSpec(spec) {
2747
2785
  const p = el.props ?? {};
2748
2786
  const { leading, trailing } = listItemEnds(p);
2749
2787
  const conflict = (prop, needs, kind, has) => issues.push(`${id}: ${prop} only shows with ${needs} (this item has ${kind}=${has}); remove one of them`);
2750
- if (p.leadingIcon && leading !== "icon") conflict("leadingIcon", "leading=icon", "leading", leading);
2751
- if (p.trailingIcon && trailing !== "icon") conflict("trailingIcon", "trailing=icon", "trailing", trailing);
2752
- if (p.trailingText && trailing !== "text" && trailing !== "badge") conflict("trailingText", "trailing=text or badge", "trailing", trailing);
2788
+ if (p.leadingIcon && leading !== "icon" && !failed("leading")) conflict("leadingIcon", "leading=icon", "leading", leading);
2789
+ if (p.trailingIcon && trailing !== "icon" && !failed("trailing")) conflict("trailingIcon", "trailing=icon", "trailing", trailing);
2790
+ if (p.trailingText && trailing !== "text" && trailing !== "badge" && !failed("trailing")) conflict("trailingText", "trailing=text or badge", "trailing", trailing);
2753
2791
  }
2754
2792
  {
2755
2793
  const p = el.props ?? {};
2756
- const say = (msg) => issues.push(`${id}: ${msg}`);
2794
+ const say = (msg, ...reads) => !failed(...reads) && issues.push(`${id}: ${msg}`);
2757
2795
  const backwards = (r) => Array.isArray(r) && r.length === 2 && r[0] > r[1];
2758
2796
  if (el.type === "Progress") {
2759
- if (p.step != null && p.steps == null) say(`step only shows with steps (e.g. steps=4 step=${p.step})`);
2760
- if (p.step != null && p.steps != null && p.step > p.steps) say(`step=${p.step} is past the last step (steps=${p.steps})`);
2761
- if (p.steps != null && p.shape === "circle") say("a stepper (steps) can't also be a circle; remove one of them");
2797
+ if (p.step != null && p.steps == null) say(`step only shows with steps (e.g. steps=4 step=${typeof p.step === "number" ? p.step : 2})`);
2798
+ if (p.step != null && p.steps != null && p.step > p.steps) say(`step=${p.step} is past the last step (steps=${p.steps})`, "step", "steps");
2799
+ if (p.steps != null && p.shape === "circle") say("a stepper (steps) can't also be a circle; remove one of them", "shape");
2762
2800
  }
2763
2801
  if (el.type === "Pagination" && p.current != null && p.pages != null && p.current > p.pages) {
2764
- say(`current=${p.current} is past the last page (pages=${p.pages})`);
2802
+ say(`current=${p.current} is past the last page (pages=${p.pages})`, "current", "pages");
2765
2803
  }
2766
- if (el.type === "Slider" && backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the smaller number first`);
2804
+ if (el.type === "Slider" && backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the smaller number first`, "range");
2767
2805
  if (el.type === "Slider" && p.range && p.value != null) say("a slider has value (one handle) or range (two), not both");
2768
2806
  if (el.type === "Calendar") {
2769
2807
  const { days } = monthGrid(p.month);
2770
2808
  const all = [p.selected, ...p.range ?? [], ...p.marked ?? []].filter((d) => typeof d === "number");
2771
2809
  const late = all.find((d) => d > days);
2772
- if (late) say(`day ${late} isn't in ${p.month ?? "the month"} (it has ${days} days)`);
2773
- if (backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the earlier day first`);
2810
+ if (late) say(`day ${late} isn't in ${p.month ?? "the month"} (it has ${days} days)`, "month", "selected", "range", "marked");
2811
+ if (backwards(p.range)) say(`range=[${p.range.join(", ")}] goes backwards; write the earlier day first`, "range");
2774
2812
  if (p.range && p.selected != null) say("a calendar has selected (one day) or range (several), not both");
2775
2813
  }
2776
2814
  if (el.type === "Input" && p.type === "code" && p.value != null && String(p.value).length > (p.digits ?? 6)) {
2777
- say(`value has ${String(p.value).length} characters but the code has ${p.digits ?? 6} boxes (digits)`);
2815
+ say(`value has ${String(p.value).length} characters but the code has ${p.digits ?? 6} boxes (digits)`, "type", "value", "digits");
2778
2816
  }
2779
- if (el.type === "Input" && p.digits != null && p.type !== "code") say("digits only applies to type=code");
2780
- if (el.type === "Input" && p.open && p.type !== "date") say("open shows a date picker, so it needs type=date");
2781
- if ((el.type === "Button" || el.type === "ListItem" || el.type === "NavBar") && p.open && !(Array.isArray(p.menu) && p.menu.length)) say("open shows the menu; add menu=[\u2026] with its items");
2782
- if (el.type === "Select" && p.open && !(Array.isArray(p.options) && p.options.length)) say("open shows the options list; add options=[\u2026] with the choices");
2817
+ if (el.type === "Input" && p.digits != null && p.type !== "code") say("digits only applies to type=code", "type");
2818
+ if (el.type === "Input" && p.open && p.type !== "date") say("open shows a date picker, so it needs type=date", "type");
2819
+ if ((el.type === "Button" || el.type === "ListItem" || el.type === "NavBar") && p.open && !(Array.isArray(p.menu) && p.menu.length)) say("open shows the menu; add menu=[\u2026] with its items", "menu");
2820
+ if (el.type === "Select" && p.open && !(Array.isArray(p.options) && p.options.length)) say("open shows the options list; add options=[\u2026] with the choices", "options");
2783
2821
  }
2784
2822
  if (el.type === "Table") {
2785
2823
  const { columns, data } = el.props ?? {};
2786
2824
  if (Array.isArray(columns) && Array.isArray(data)) {
2787
- const bad = data.map((row, i) => ({ row: i + 1, cells: Array.isArray(row) ? row.length : 1 })).filter((r) => r.cells !== columns.length);
2788
- if (bad.length) {
2825
+ const bad2 = data.map((row, i) => ({ row: i + 1, cells: Array.isArray(row) ? row.length : 1 })).filter((r) => r.cells !== columns.length);
2826
+ if (bad2.length) {
2789
2827
  const cells = (n) => `${n} cell${n === 1 ? "" : "s"}`;
2790
- const rows = bad.length === 1 ? `data row ${bad[0].row} has ${cells(bad[0].cells)}` : `data rows ${bad.map((r) => `${r.row} (${cells(r.cells)})`).join(", ")} don't match`;
2828
+ const rows = bad2.length === 1 ? `data row ${bad2[0].row} has ${cells(bad2[0].cells)}` : `data rows ${bad2.map((r) => `${r.row} (${cells(r.cells)})`).join(", ")} don't match`;
2791
2829
  const hints = [
2792
- bad.some((r) => r.cells > columns.length) && `quote cells that contain a comma, e.g. ["$1,200", Paid]`,
2793
- bad.some((r) => r.cells < columns.length) && `use "" for an empty cell`
2830
+ bad2.some((r) => r.cells > columns.length) && `quote cells that contain a comma, e.g. ["$1,200", Paid]`,
2831
+ bad2.some((r) => r.cells < columns.length) && `use "" for an empty cell`
2794
2832
  ].filter(Boolean);
2795
2833
  issues.push(`${id}: Table has ${columns.length} columns, but ${rows} (${hints.join("; ")})`);
2796
2834
  }
@@ -2805,7 +2843,7 @@ function checkSpec(spec) {
2805
2843
  if (other) issues.push(`${key}: the id #${ref} is also used by ${spec.elements[other.key].type} "${other.key}"${other.id === ref ? "" : ` (as #${other.id}; ids ignore case)`}; ids must be unique`);
2806
2844
  else owners.set(ref.toLowerCase(), { key, id: ref });
2807
2845
  }
2808
- const written = [...owners.values()].map((o2) => o2.id);
2846
+ const written2 = [...owners.values()].map((o2) => o2.id);
2809
2847
  const parentOf = /* @__PURE__ */ new Map();
2810
2848
  for (const [k, el] of Object.entries(spec.elements)) for (const c of el.children ?? []) parentOf.set(c, k);
2811
2849
  for (const [key, el] of Object.entries(spec.elements)) {
@@ -2825,9 +2863,9 @@ function checkSpec(spec) {
2825
2863
  }
2826
2864
  continue;
2827
2865
  }
2828
- const close = closeMatches(p[end], written);
2866
+ const close = closeMatches(p[end], written2);
2829
2867
  issues.push(
2830
- `${key}: no element has the id #${p[end]}` + (close.length ? ` (did you mean ${close.map((c) => "#" + c).join(", ")}?)` : owners.size ? ` (ids on this board: ${written.map((c) => "#" + c).join(", ")})` : "; name an element by writing #name after it")
2868
+ `${key}: no element has the id #${p[end]}` + (close.length ? ` (did you mean ${close.map((c) => "#" + c).join(", ")}?)` : owners.size ? ` (ids on this board: ${written2.map((c) => "#" + c).join(", ")})` : "; name an element by writing #name after it")
2831
2869
  );
2832
2870
  }
2833
2871
  if (typeof p.from === "string" && typeof p.to === "string" && p.from.toLowerCase() === p.to.toLowerCase()) issues.push(`${key}: a flow needs two different ends (both are #${p.from})`);
@@ -3212,6 +3250,11 @@ function wireframePrompt() {
3212
3250
  "You write low-fidelity UI wireframes as specs that a renderer turns into images.",
3213
3251
  "Reply with only the spec in a single code block, with no explanation.",
3214
3252
  "",
3253
+ languageReference()
3254
+ ].join("\n");
3255
+ }
3256
+ function languageReference() {
3257
+ return [
3215
3258
  TEXT_FORMAT,
3216
3259
  "",
3217
3260
  RULES,
@@ -3263,5 +3306,6 @@ export {
3263
3306
  printWireframeText,
3264
3307
  componentDocs,
3265
3308
  wireframePrompt,
3309
+ languageReference,
3266
3310
  repairPrompt
3267
3311
  };
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  printWireframeText,
7
7
  renderWireframe,
8
8
  wireframePrompt
9
- } from "./chunks/chunk-HDSKRKDL.js";
9
+ } from "./chunks/chunk-ZLVSCEWO.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { readFile, writeFile } from "node:fs/promises";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Escaping for HTML attribute values, shared by the playground's Copy link → HTML and the MCP
3
+ * share tool, so the two give the same <img> tag. & first, or the entities it adds get escaped again.
4
+ */
5
+ export declare const escapeAttr: (s: string) => string;
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { renderWireframe, compileWireframe, formatIssues, WireframeError, type C
3
3
  export { wireframePrompt, repairPrompt } from "./prompt.js";
4
4
  export { printWireframeText as formatWireframe } from "./print.js";
5
5
  export { encodeWireframe, decodeWireframe, MAX_SHARED_TEXT } from "./share.js";
6
+ export { mcpTools, MCP_INSTRUCTIONS, LINK_KINDS, DEFAULT_LINKS, type LinkKind, wireframeGuide, renderWireframeTool, shareWireframeTool, callMcpTool, type ToolResult, type ToolContent, type ToolOptions, type McpToolName } from "./mcp.js";
6
7
  export { upgradeWireframe, upgradeSpec, RENAMED_PROPS, type UpgradeOptions } from "./upgrade.js";
7
8
  export { renderWireframeSvg, renderWireframePng, checkSpec, boardSize, boardLayout, type BoardItem, type RenderWireframeOptions } from "./render.js";
8
9
  export { parseWireframeText, type TextIssue, type ParseResult } from "./text.js";
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  compileWireframe,
10
10
  componentDefinitions,
11
11
  formatIssues,
12
+ languageReference,
12
13
  parseWireframeText,
13
14
  printWireframeText,
14
15
  registry,
@@ -20,7 +21,7 @@ import {
20
21
  upgradeSpec,
21
22
  upgradeWireframe,
22
23
  wireframePrompt
23
- } from "./chunks/chunk-HDSKRKDL.js";
24
+ } from "./chunks/chunk-ZLVSCEWO.js";
24
25
 
25
26
  // src/share.ts
26
27
  import { deflateRawSync, inflateRawSync } from "node:zlib";
@@ -32,22 +33,145 @@ var isLinkData = (data) => data.startsWith(LINK_PREFIX) || data.startsWith(LEGAC
32
33
 
33
34
  // src/share.ts
34
35
  var MAX_SHARED_TEXT = 64e3;
35
- function encodeWireframe(text) {
36
- return LINK_PREFIX + deflateRawSync(Buffer.from(text, "utf8"), { level: 9 }).toString("base64url");
36
+ function encodeWireframe(text2) {
37
+ return LINK_PREFIX + deflateRawSync(Buffer.from(text2, "utf8"), { level: 9 }).toString("base64url");
37
38
  }
38
39
  function decodeWireframe(data) {
39
40
  const prefix = data.slice(0, 1);
40
41
  if (!isLinkData(data)) throw new Error(`unknown encoding "${prefix}" (expected a "${LINK_PREFIX}" or "${LEGACY_LINK_PREFIX}" prefix)`);
41
- const text = inflateRawSync(Buffer.from(data.slice(1), "base64url"), { maxOutputLength: MAX_SHARED_TEXT }).toString("utf8");
42
- return upgradeWireframe(text, { fromLink: prefix === LEGACY_LINK_PREFIX });
42
+ const text2 = inflateRawSync(Buffer.from(data.slice(1), "base64url"), { maxOutputLength: MAX_SHARED_TEXT }).toString("utf8");
43
+ return upgradeWireframe(text2, { fromLink: prefix === LEGACY_LINK_PREFIX });
44
+ }
45
+
46
+ // src/mcp.ts
47
+ import { z } from "zod";
48
+
49
+ // src/escape.ts
50
+ var escapeAttr = (s) => s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/[\r\n]+/g, " ");
51
+
52
+ // src/mcp.ts
53
+ var MODEL_IMAGE_MAX = 1600;
54
+ var MAX_IMAGE_LINK_DATA = 16e3;
55
+ var DEFAULT_BASE = "https://tsquare.dev";
56
+ var MCP_INSTRUCTIONS = "tsquare draws low-fidelity UI wireframes from a small text language: multi-screen boards with notes and flow arrows, grayscale by default. Call wireframe_guide once before writing your first wireframe, then render_wireframe to check and see it, and share_wireframe to give the user links.";
57
+ var textInput = z.string().max(MAX_SHARED_TEXT).describe("The wireframe in tsquare text (a ```tsquare code fence around it is fine).");
58
+ var flowsInput = z.boolean().optional().describe("Draw flow arrows (default true). false leaves them out, as if there were no flow lines.");
59
+ var LINK_KINDS = ["share", "svg", "png", "markdown", "html"];
60
+ var DEFAULT_LINKS = ["svg", "share"];
61
+ var LINK_LABELS = {
62
+ share: "Share link (opens it in the playground to view and edit)",
63
+ svg: "Image link (SVG)",
64
+ png: "Image link (PNG)",
65
+ markdown: "Markdown",
66
+ html: "HTML"
67
+ };
68
+ var linksInput = z.array(z.enum(LINK_KINDS)).min(1).optional().describe(
69
+ "Which links to return; ask only for what the user needs, since each link is long. share: open and edit it in the playground. svg: an image link to view or embed (Notion, GitHub, docs). png: an image link for tools that don't show SVG. markdown: an image for READMEs and docs. html: an <img> tag for web pages. Default [svg, share]."
70
+ );
71
+ var GUIDE_HEADER = `# Writing tsquare wireframes
72
+
73
+ Workflow:
74
+ 1. Write the wireframe text for the user's request, following the language below.
75
+ 2. Call render_wireframe with it. If it reports problems, fix every line it names and render again.
76
+ 3. Look at the image for what the checker can't see: content cut off at the bottom of a screen (make the screen taller with height=\u2026 or remove content), cramped rows, text that wraps badly. Fix and render again.
77
+ 4. Call share_wireframe with only the links the user needs (links=[\u2026]; by default an image link and the share link to edit it), and give them to the user exactly as returned: each link carries the whole wireframe, so one changed character breaks it. Show the wireframe text too, in a \`\`\`tsquare code block, so they can keep it.
78
+ `;
79
+ var mcpTools = {
80
+ wireframe_guide: {
81
+ title: "tsquare language guide",
82
+ description: "How to write a tsquare wireframe: the text format, the rules, a worked example and every component with its props. Call it once before writing your first wireframe.",
83
+ inputSchema: z.object({}),
84
+ annotations: { readOnlyHint: true, openWorldHint: false }
85
+ },
86
+ render_wireframe: {
87
+ title: "Render a wireframe",
88
+ description: "Check tsquare wireframe text and render it. Returns a PNG of the board so you can see it, or the problems by line number (with the valid options) if the text isn't valid. Fix every problem and call it again.",
89
+ inputSchema: z.object({ text: textInput, flows: flowsInput }),
90
+ annotations: { readOnlyHint: true, openWorldHint: false }
91
+ },
92
+ share_wireframe: {
93
+ title: "Share a wireframe",
94
+ description: "Links for valid tsquare wireframe text, the same kinds as the playground's Copy link menu: a share link to edit it in the playground, image links (SVG, PNG), Markdown and HTML. Ask only for the links the user needs (default: an SVG image link and the share link). Each link contains the whole wireframe text (compressed), so anyone with a link can read it.",
95
+ inputSchema: z.object({ text: textInput, flows: flowsInput, links: linksInput }),
96
+ annotations: { readOnlyHint: true, openWorldHint: false }
97
+ }
98
+ };
99
+ var text = (t) => ({ type: "text", text: t });
100
+ var problems = (issues) => ({
101
+ content: [text(`The wireframe has ${issues.length} problem${issues.length === 1 ? "" : "s"}. Fix each line and try again.
102
+ ${formatIssues(issues)}`)],
103
+ isError: true
104
+ });
105
+ function wireframeGuide() {
106
+ return { content: [text(`${GUIDE_HEADER}
107
+ ${languageReference()}`)] };
108
+ }
109
+ async function renderWireframeTool(args) {
110
+ const { spec, issues } = compileWireframe(args.text);
111
+ if (!spec || issues.length) return problems(issues);
112
+ const flows = args.flows !== false;
113
+ const { width, height } = boardSize(spec, { flows });
114
+ const scale = Math.min(1, MODEL_IMAGE_MAX / Math.max(width, height));
115
+ const png = await renderWireframePng(spec, { skipValidation: true, flows, scale });
116
+ const shown = scale < 1 ? `, shown at ${Math.round(width * scale)}\xD7${Math.round(height * scale)}` : "";
117
+ return {
118
+ content: [
119
+ text(`Valid. The board is ${width}\xD7${height}${shown}. Check the image for clipped or cramped content, then call share_wireframe for links.`),
120
+ { type: "image", data: Buffer.from(png).toString("base64"), mimeType: "image/png" }
121
+ ]
122
+ };
123
+ }
124
+ function shareWireframeTool(args, opts = {}) {
125
+ const { spec, issues } = compileWireframe(args.text);
126
+ if (!spec || issues.length) return problems(issues);
127
+ const base = (opts.base ?? DEFAULT_BASE).replace(/\/+$/, "");
128
+ const bare = args.text.replace(/^[ \t]*```.*(\r?\n|$)/gm, "").trim() + "\n";
129
+ if (Buffer.byteLength(bare, "utf8") > MAX_SHARED_TEXT)
130
+ return { content: [text(`This wireframe is too long to share as a link (over ${MAX_SHARED_TEXT / 1e3} KB of text). Split it into smaller boards.`)], isError: true };
131
+ const data = encodeWireframe(bare);
132
+ const query = args.flows === false ? "?flows=0" : "";
133
+ const title = String(spec.elements[spec.root]?.props?.title ?? "Wireframe");
134
+ const kinds = [...new Set(args.links?.length ? args.links : DEFAULT_LINKS)];
135
+ const share = `${base}/playground#${data}`;
136
+ const lines = [];
137
+ const notes = ["Give each link exactly as written: it carries the whole wireframe, so anyone with it can read the wireframe, and one changed character breaks it."];
138
+ if (data.length > MAX_IMAGE_LINK_DATA && kinds.some((k) => k !== "share")) {
139
+ if (!kinds.includes("share")) kinds.unshift("share");
140
+ notes.unshift("This wireframe is too long for an image link, so only the share link is included. To get image links, split it into smaller boards.");
141
+ }
142
+ const svg = `${base}/svg/${data}${query}`;
143
+ for (const kind of kinds) {
144
+ if (kind !== "share" && data.length > MAX_IMAGE_LINK_DATA) continue;
145
+ const link = {
146
+ share,
147
+ svg,
148
+ png: `${base}/png/${data}${query}`,
149
+ markdown: `![${title.replace(/[[\]\n]/g, " ")}](${svg})`,
150
+ html: `<img src="${svg}" alt="${escapeAttr(title)}">`
151
+ }[kind];
152
+ lines.push(`${LINK_LABELS[kind]}: ${link}`);
153
+ }
154
+ return { content: [text([...lines, "", ...notes].join("\n"))] };
155
+ }
156
+ async function callMcpTool(name, args = {}, opts = {}) {
157
+ if (name === "wireframe_guide") return wireframeGuide();
158
+ if (name !== "render_wireframe" && name !== "share_wireframe")
159
+ return { content: [text(`Unknown tool "${name}". Tools: ${Object.keys(mcpTools).join(", ")}.`)], isError: true };
160
+ const parsed = mcpTools[name].inputSchema.safeParse(args);
161
+ if (!parsed.success) return { content: [text(`Invalid arguments: ${z.prettifyError(parsed.error)}`)], isError: true };
162
+ return name === "render_wireframe" ? renderWireframeTool(parsed.data) : shareWireframeTool(parsed.data, opts);
43
163
  }
44
164
  export {
165
+ DEFAULT_LINKS,
45
166
  DEVICES,
167
+ LINK_KINDS,
46
168
  MAX_SHARED_TEXT,
169
+ MCP_INSTRUCTIONS,
47
170
  RENAMED_PROPS,
48
171
  WireframeError,
49
172
  boardLayout,
50
173
  boardSize,
174
+ callMcpTool,
51
175
  catalog,
52
176
  checkSpec,
53
177
  compileWireframe,
@@ -56,14 +180,18 @@ export {
56
180
  encodeWireframe,
57
181
  formatIssues,
58
182
  printWireframeText as formatWireframe,
183
+ mcpTools,
59
184
  parseWireframeText,
60
185
  registry,
61
186
  renderWireframe,
62
187
  renderWireframePng,
63
188
  renderWireframeSvg,
189
+ renderWireframeTool,
64
190
  repairPrompt,
191
+ shareWireframeTool,
65
192
  theme,
66
193
  upgradeSpec,
67
194
  upgradeWireframe,
195
+ wireframeGuide,
68
196
  wireframePrompt
69
197
  };
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The MCP tools (guide, render, share) as plain functions, with no MCP SDK: tsquare.dev wraps them
3
+ * in its hosted endpoint (tsquare.dev/mcp), and a local stdio server can reuse them unchanged.
4
+ *
5
+ * Results use the MCP tool-result shape ({ content, isError }), so a server returns them as they are.
6
+ * Problems come back as a result with isError, not a thrown error, so the model reads them and fixes
7
+ * its text: the messages are the same line-numbered ones `tsquare check` prints.
8
+ */
9
+ import { z } from "zod";
10
+ export type ToolContent = {
11
+ type: "text";
12
+ text: string;
13
+ } | {
14
+ type: "image";
15
+ data: string;
16
+ mimeType: string;
17
+ };
18
+ export interface ToolResult {
19
+ content: ToolContent[];
20
+ isError?: boolean;
21
+ [key: string]: unknown;
22
+ }
23
+ export interface ToolOptions {
24
+ /** Where share and image links point (default https://tsquare.dev). */
25
+ base?: string;
26
+ }
27
+ /** Longest side of the PNG a model gets back. Models downscale anything bigger, and it keeps the result small. */
28
+ export declare const MODEL_IMAGE_MAX = 1600;
29
+ /** Longest image link the site serves (its render URLs cap the data at this many characters). */
30
+ export declare const MAX_IMAGE_LINK_DATA = 16000;
31
+ /** Short server instructions: what the tools are for and the order to use them in. */
32
+ export declare const MCP_INSTRUCTIONS: string;
33
+ /** The link kinds share_wireframe offers: the playground's Copy link menu, with the same names. */
34
+ export declare const LINK_KINDS: readonly ["share", "svg", "png", "markdown", "html"];
35
+ export type LinkKind = (typeof LINK_KINDS)[number];
36
+ /** What a plain "share it" gets: one image link and the link to edit it. */
37
+ export declare const DEFAULT_LINKS: LinkKind[];
38
+ export declare const mcpTools: {
39
+ readonly wireframe_guide: {
40
+ readonly title: "tsquare language guide";
41
+ readonly description: "How to write a tsquare wireframe: the text format, the rules, a worked example and every component with its props. Call it once before writing your first wireframe.";
42
+ readonly inputSchema: z.ZodObject<{}, z.core.$strip>;
43
+ readonly annotations: {
44
+ readonly readOnlyHint: true;
45
+ readonly openWorldHint: false;
46
+ };
47
+ };
48
+ readonly render_wireframe: {
49
+ readonly title: "Render a wireframe";
50
+ readonly description: "Check tsquare wireframe text and render it. Returns a PNG of the board so you can see it, or the problems by line number (with the valid options) if the text isn't valid. Fix every problem and call it again.";
51
+ readonly inputSchema: z.ZodObject<{
52
+ text: z.ZodString;
53
+ flows: z.ZodOptional<z.ZodBoolean>;
54
+ }, z.core.$strip>;
55
+ readonly annotations: {
56
+ readonly readOnlyHint: true;
57
+ readonly openWorldHint: false;
58
+ };
59
+ };
60
+ readonly share_wireframe: {
61
+ readonly title: "Share a wireframe";
62
+ readonly description: "Links for valid tsquare wireframe text, the same kinds as the playground's Copy link menu: a share link to edit it in the playground, image links (SVG, PNG), Markdown and HTML. Ask only for the links the user needs (default: an SVG image link and the share link). Each link contains the whole wireframe text (compressed), so anyone with a link can read it.";
63
+ readonly inputSchema: z.ZodObject<{
64
+ text: z.ZodString;
65
+ flows: z.ZodOptional<z.ZodBoolean>;
66
+ links: z.ZodOptional<z.ZodArray<z.ZodEnum<{
67
+ html: "html";
68
+ markdown: "markdown";
69
+ png: "png";
70
+ share: "share";
71
+ svg: "svg";
72
+ }>>>;
73
+ }, z.core.$strip>;
74
+ readonly annotations: {
75
+ readonly readOnlyHint: true;
76
+ readonly openWorldHint: false;
77
+ };
78
+ };
79
+ };
80
+ export type McpToolName = keyof typeof mcpTools;
81
+ /** The wireframe_guide tool: workflow plus the language reference. */
82
+ export declare function wireframeGuide(): ToolResult;
83
+ /** The render_wireframe tool: a PNG sized for a model to look at, or line-numbered problems. */
84
+ export declare function renderWireframeTool(args: {
85
+ text: string;
86
+ flows?: boolean;
87
+ }): Promise<ToolResult>;
88
+ /** The share_wireframe tool: image and playground links, computed locally (nothing is sent anywhere). */
89
+ export declare function shareWireframeTool(args: {
90
+ text: string;
91
+ flows?: boolean;
92
+ links?: LinkKind[];
93
+ }, opts?: ToolOptions): ToolResult;
94
+ /** Run a tool by name, for servers that dispatch by name. Unknown names come back as an error result. */
95
+ export declare function callMcpTool(name: string, args?: Record<string, unknown>, opts?: ToolOptions): Promise<ToolResult>;
@@ -13,7 +13,7 @@ import {
13
13
  renderWireframeSvg,
14
14
  takesUniversalProps,
15
15
  wireframePrompt
16
- } from "../chunks/chunk-HDSKRKDL.js";
16
+ } from "../chunks/chunk-ZLVSCEWO.js";
17
17
 
18
18
  // src/playground/index.ts
19
19
  import { createServer } from "node:http";