tabbied-mcp 0.2.3 → 0.2.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/node/render.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,EAAE;AACF,uEAAuE;AACvE,+EAA+E;AAC/E,4EAA4E;AAC5E,yEAAyE;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEhC,8EAA8E;AAC9E,MAAM,iBAAiB,GAAG,OAAO,CAAC;AAElC,+EAA+E;AAC/E,4EAA4E;AAC5E,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAErC,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAEzE,MAAM,SAAS,GAAG,CAAC,OAAe,EAAc,EAAE,CAAC,CAAC;IAClD,OAAO,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxB,OAAO,EAAE,IAAI;CACd,CAAC,CAAC;AAEH,MAAM,UAAU,UAAU,CAAC,OAAgB;IACzC,OAAO;QACL,UAAU,EAAE;YACV,IAAI,EAAE,eAAe;YACrB,KAAK,EAAE,2BAA2B;YAClC,WAAW,EACT,wEAAwE;gBACxE,uEAAuE;gBACvE,yEAAyE;gBACzE,qEAAqE;gBACrE,sEAAsE;gBACtE,qDAAqD;YACvD,WAAW,EAAE;gBACX,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,uBAAuB,EAAE;oBAC9D,MAAM,EAAE;wBACN,IAAI,EAAE,QAAQ;wBACd,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC;wBACpB,WAAW,EACT,+DAA+D;4BAC/D,gCAAgC;qBACnC;oBACD,GAAG,EAAE;wBACH,IAAI,EAAE,QAAQ;wBACd,WAAW,EACT,+DAA+D;4BAC/D,qBAAqB;qBACxB;oBACD,IAAI,EAAE;wBACJ,IAAI,EAAE,QAAQ;wBACd,WAAW,EACT,iEAAiE;4BACjE,eAAe;qBAClB;oBACD,OAAO,EAAE;wBACP,IAAI,EAAE,OAAO;wBACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;wBACzB,WAAW,EACT,uEAAuE;qBAC1E;oBACD,OAAO,EAAE;wBACP,IAAI,EAAE,QAAQ;wBACd,WAAW,EACT,gEAAgE;4BAChE,gCAAgC;qBACnC;oBACD,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,4BAA4B,EAAE;oBACrE,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,4BAA4B,EAAE;oBACtE,GAAG,EAAE;wBACH,IAAI,EAAE,QAAQ;wBACd,IAAI,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC;wBAChC,WAAW,EAAE,6BAA6B;qBAC3C;oBACD,KAAK,EAAE;wBACL,IAAI,EAAE,SAAS;wBACf,WAAW,EACT,iEAAiE;4BACjE,sDAAsD;qBACzD;iBACF;gBACD,QAAQ,EAAE,CAAC,MAAM,EAAE,QAAQ,CAAC;gBAC5B,oBAAoB,EAAE,KAAK;aAC5B;SACF;QAED,KAAK,CAAC,GAAG,CAAC,IAAI;YACZ,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YAC5D,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;YACpE,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO,SAAS,CACd,wBAAwB,IAAI,oCAAoC,CACjE,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;YACrD,IAAI,MAAM,KAAK,KAAK,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC;gBACpD,OAAO,SAAS,CACd,IAAI,IAAI,6DAA6D;oBACnE,mCAAmC,CACtC,CAAC;YACJ,CAAC;YAED,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,SAAS,CACd,qEAAqE;oBACnE,qDAAqD,CACxD,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC;YACrE,oEAAoE;YACpE,uBAAuB;YACvB,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC/E,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,GAAc,CAAC;YACzF,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;YAEpG,MAAM,KAAK,GACT,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;gBAC3D,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;gBACxB,CAAC,CAAC,MAAM;oBACN,CAAC,CAAC,CAAC;oBACH,CAAC,CAAC,CAAC,CAAC;YAEV,MAAM,IAAI,GAAG;gBACX,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC;gBACjC,QAAQ;gBACR,IAAI;gBACJ,OAAO;gBACP,OAAO;gBACP,UAAU;gBACV,MAAM;gBACN,SAAS;gBACT,MAAM,CAAC,KAAK,CAAC;aACd,CAAC;YAEF,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAErE,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI;gBAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1E,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ;gBAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;YAE1D,MAAM,KAAK,GACT,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;YAChE,MAAM,MAAM,GACV,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;YAClE,IAAI,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,MAAM,EAAE,CAAC,CAAC;YAErC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;gBACvD,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5D,CAAC;YAED,IAAI,IAAI,CAAC,OAAO,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;gBACrD,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,OAAkC,CAAC;qBAClE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;qBAC/C,IAAI,CAAC,IAAI,CAAC,CAAC;gBACd,IAAI,KAAK;oBAAE,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;YACtC,CAAC;YAED,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,qEAAqE;gBACrE,iCAAiC;gBACjC,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE;oBAC9C,OAAO,EAAE,iBAAiB;oBAC1B,SAAS,EAAE,CAAC,GAAG,IAAI,GAAG,IAAI;iBAC3B,CAAC,CAAC,CAAC;YACN,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,MAAM,GACV,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,QAAQ,IAAI,KAAK;oBACrD,CAAC,CAAC,MAAM,CAAE,KAA6B,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE;oBACtD,CAAC,CAAC,KAAK,YAAY,KAAK;wBACtB,CAAC,CAAC,KAAK,CAAC,OAAO;wBACf,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACtB,OAAO,SAAS,CACd,qBAAqB,MAAM,IAAI,0BAA0B,MAAM;oBAC7D,+DAA+D;oBAC/D,gEAAgE;oBAChE,kBAAkB,CACrB,CAAC;YACJ,CAAC;YAED,wEAAwE;YACxE,oCAAoC;YACpC,MAAM,KAAK,GAAG,MAAM;iBACjB,KAAK,CAAC,IAAI,CAAC;iBACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;iBACjD,IAAI,CAAC,IAAI,CAAC,CAAC;YAEd,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;gBACrC,OAAO;oBACL,OAAO,EAAE;wBACP,IAAI,CACF,YAAY,IAAI,OAAO,OAAO,KAAK,KAAK,IAAI,MAAM,EAAE;4BAClD,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;4BACvC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,OAAO;4BACnC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAC9B;qBACF;iBACF,CAAC;YACJ,CAAC;YAED,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;gBACrB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC7C,MAAM,OAAO,EAAE,CAAC;gBAChB,OAAO;oBACL,OAAO,EAAE;wBACP,IAAI,CACF,YAAY,IAAI,YAAY,KAAK,IAAI,MAAM,IAAI;4BAC7C,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAC9B;wBACD,IAAI,CAAC,GAAG,CAAC;qBACV;iBACF,CAAC;YACJ,CAAC;YAED,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;YACpC,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACpC,IAAI,IAAI,CAAC,MAAM,GAAG,kBAAkB,EAAE,CAAC;gBACrC,8DAA8D;gBAC9D,OAAO;oBACL,OAAO,EAAE;wBACP,IAAI,CACF,YAAY,IAAI,OAAO,OAAO,MAAM,IAAI,CAAC,KAAK,CAC5C,GAAG,CAAC,UAAU,GAAG,IAAI,CACtB,8DAA8D;4BAC7D,+DAA+D,CAClE;qBACF;iBACF,CAAC;YACJ,CAAC;YAED,MAAM,OAAO,EAAE,CAAC;YAChB,OAAO;gBACL,OAAO,EAAE;oBACP,IAAI,CACF,YAAY,IAAI,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK,KAAK;wBACjD,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAC9B;oBACD,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE;iBAC/C;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["// `render_design`, the one tool that only exists over stdio.\n//\n// It shells out to the `tabbied` CLI: the only faithful renderer for a\n// css-doodle pattern is css-doodle in a real browser (see docs/svg-export.md),\n// and the CLI already owns the headless browser, the SVG converter, and the\n// option parsing. A Worker has no browser, so it doesn't offer the tool.\nimport { execFile } from 'node:child_process';\nimport { mkdtemp, readFile, rm, stat } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport type { Catalog, Tool, ToolResult } from '../types.js';\nimport { tabbiedRoot } from './resources.js';\n\nconst run = promisify(execFile);\n\n// Rendering launches a browser and waits for the first arrangement to settle.\nconst RENDER_TIMEOUT_MS = 180_000;\n\n// A PNG returned inline is spent context. Past this the file stays on disk and\n// the agent gets the path - better a usable pointer than a truncated image.\nconst INLINE_BYTE_BUDGET = 1_500_000;\n\nconst text = (value: string) => ({ type: 'text' as const, text: value });\n\nconst toolError = (message: string): ToolResult => ({\n content: [text(message)],\n isError: true,\n});\n\nexport function renderTool(catalog: Catalog): Tool {\n return {\n definition: {\n name: 'render_design',\n title: 'Render a design to a file',\n description:\n 'Render a design to SVG or PNG with a real browser, at any size, seed, ' +\n 'palette, and option set. Use this to produce an actual asset - or to ' +\n 'preview a *customized* configuration, which preview_design cannot show ' +\n 'you (it only has the stock palette and defaults). Needs Playwright ' +\n 'installed alongside this server. Without \"out\" the image comes back ' +\n 'inline; with it, the file is written where you ask.',\n inputSchema: {\n type: 'object',\n properties: {\n slug: { type: 'string', description: 'The design to render.' },\n format: {\n type: 'string',\n enum: ['svg', 'png'],\n description:\n 'Vector or raster. A few designs cannot be vectorized - check ' +\n 'svgExport on the design first.',\n },\n out: {\n type: 'string',\n description:\n 'Absolute path to write to. Omit to get the image back inline ' +\n 'instead of on disk.',\n },\n seed: {\n type: 'string',\n description:\n 'Fixed seed - the same seed always gives the same image. Random ' +\n 'when omitted.',\n },\n palette: {\n type: 'array',\n items: { type: 'string' },\n description:\n \"CSS colors, background first. Omit for the design's authored palette.\",\n },\n options: {\n type: 'object',\n description:\n 'Option values keyed by option id, e.g. {\"scale\": 3}. Read the ' +\n 'ids and ranges off get_design.',\n },\n width: { type: 'integer', description: 'Pixels wide (default 960).' },\n height: { type: 'integer', description: 'Pixels tall (default 960).' },\n fit: {\n type: 'string',\n enum: ['grid', 'cover', 'fixed'],\n description: 'Sizing mode (default grid).',\n },\n scale: {\n type: 'integer',\n description:\n 'PNG device-scale factor. Defaults to 2 when writing a file and ' +\n '1 when returning inline, to keep the response small.',\n },\n },\n required: ['slug', 'format'],\n additionalProperties: false,\n },\n },\n\n async run(args): Promise<ToolResult> {\n const slug = typeof args.slug === 'string' ? args.slug : '';\n const design = catalog.designs.find((entry) => entry.slug === slug);\n if (!design) {\n return toolError(\n `No design with slug \"${slug}\". Use search_designs to find one.`\n );\n }\n\n const format = args.format === 'png' ? 'png' : 'svg';\n if (format === 'svg' && !design.svgExport.supported) {\n return toolError(\n `\"${slug}\" paints effects SVG cannot represent, so it has no vector ` +\n 'export. Render it as PNG instead.'\n );\n }\n\n const root = tabbiedRoot();\n if (!root) {\n return toolError(\n 'The `tabbied` package is not installed next to this server, so the ' +\n 'renderer is unavailable. Run `npm install tabbied`.'\n );\n }\n\n const inline = typeof args.out !== 'string' || args.out.length === 0;\n // An inline render gets a scratch directory, removed once its bytes\n // have been read back.\n const scratch = inline ? await mkdtemp(path.join(tmpdir(), 'tabbied-')) : null;\n const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : (args.out as string);\n const discard = () => (scratch ? rm(scratch, { recursive: true, force: true }) : Promise.resolve());\n\n const scale =\n typeof args.scale === 'number' && Number.isFinite(args.scale)\n ? Math.trunc(args.scale)\n : inline\n ? 1\n : 2;\n\n const argv = [\n path.join(root, 'dist', 'cli.js'),\n 'render',\n slug,\n '--out',\n outPath,\n '--format',\n format,\n '--scale',\n String(scale),\n ];\n\n const push = (flag: string, value: string) => argv.push(flag, value);\n\n if (typeof args.seed === 'string' && args.seed) push('--seed', args.seed);\n if (typeof args.fit === 'string') push('--fit', args.fit);\n\n const width =\n typeof args.width === 'number' ? Math.trunc(args.width) : 960;\n const height =\n typeof args.height === 'number' ? Math.trunc(args.height) : 960;\n push('--size', `${width}x${height}`);\n\n if (Array.isArray(args.palette) && args.palette.length) {\n push('--palette', args.palette.filter(Boolean).join(','));\n }\n\n if (args.options && typeof args.options === 'object') {\n const pairs = Object.entries(args.options as Record<string, unknown>)\n .map(([id, value]) => `${id}: ${String(value)}`)\n .join('; ');\n if (pairs) push('--options', pairs);\n }\n\n let stderr = '';\n try {\n // No shell: argv goes straight to node, so a palette or option value\n // can't be read as shell syntax.\n ({ stderr } = await run(process.execPath, argv, {\n timeout: RENDER_TIMEOUT_MS,\n maxBuffer: 8 * 1024 * 1024,\n }));\n } catch (error) {\n const detail =\n error && typeof error === 'object' && 'stderr' in error\n ? String((error as { stderr: unknown }).stderr).trim()\n : error instanceof Error\n ? error.message\n : String(error);\n return toolError(\n `Rendering failed: ${detail || 'the CLI exited non-zero.'}\\n\\n` +\n 'If this says a browser is missing, install one alongside the ' +\n 'server (`npm i -D playwright`) or point TABBIED_CHROMIUM at a ' +\n 'Chromium binary.'\n );\n }\n\n // The CLI reports SVG-fidelity caveats on stderr; they matter enough to\n // pass through rather than swallow.\n const notes = stderr\n .split('\\n')\n .filter((line) => line.trim().startsWith('note:'))\n .join('\\n');\n\n if (!inline) {\n const { size } = await stat(outPath);\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} (${width}x${height}` +\n (format === 'png' ? ` @${scale}x` : '') +\n `, ${Math.round(size / 1024)} KB).` +\n (notes ? `\\n${notes}` : '')\n ),\n ],\n };\n }\n\n if (format === 'svg') {\n const svg = await readFile(outPath, 'utf-8');\n await discard();\n return {\n content: [\n text(\n `Rendered ${slug} as SVG (${width}x${height}).` +\n (notes ? `\\n${notes}` : '')\n ),\n text(svg),\n ],\n };\n }\n\n const png = await readFile(outPath);\n const data = png.toString('base64');\n if (data.length > INLINE_BYTE_BUDGET) {\n // Kept: the answer tells the agent to read it from that path.\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} - ${Math.round(\n png.byteLength / 1024\n )} KB, too large to return inline. Read it from that path, or ` +\n 're-render smaller (lower \"scale\", or a smaller width/height).'\n ),\n ],\n };\n }\n\n await discard();\n return {\n content: [\n text(\n `Rendered ${slug} (${width}x${height} @${scale}x).` +\n (notes ? `\\n${notes}` : '')\n ),\n { type: 'image', data, mimeType: 'image/png' },\n ],\n };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/node/render.ts"],"names":[],"mappings":"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","sourcesContent":["// `render_design`, the one tool that only exists over stdio.\n//\n// It shells out to the `tabbied` CLI: the only faithful renderer for a\n// css-doodle pattern is css-doodle in a real browser (see docs/svg-export.md),\n// and the CLI already owns the headless browser, the SVG converter, and the\n// option parsing. A Worker has no browser, so it doesn't offer the tool.\nimport { execFile } from 'node:child_process';\nimport { mkdtemp, readFile, rm, stat } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport type { Catalog, Tool, ToolResult } from '../types.js';\nimport { tabbiedRoot } from './resources.js';\n\nconst run = promisify(execFile);\n\n// Rendering launches a browser and waits for the first arrangement to settle.\nconst RENDER_TIMEOUT_MS = 180_000;\n\n// A PNG returned inline is spent context. Past this the file stays on disk and\n// the agent gets the path - better a usable pointer than a truncated image.\nconst INLINE_BYTE_BUDGET = 1_500_000;\n\n// Past these a PNG is tens of megapixels (or a browser canvas limit), which\n// no asset this tool exists for needs.\nconst SIZE_MAX = 4096;\nconst SCALE_MAX = 4;\n\nconst text = (value: string) => ({ type: 'text' as const, text: value });\n\nconst toolError = (message: string): ToolResult => ({\n content: [text(message)],\n isError: true,\n});\n\n/** A whole number in [min, max], the default when absent, or null when invalid. */\nfunction integerArg(value: unknown, fallback: number, min: number, max: number): number | null {\n if (value === undefined) return fallback;\n return typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max\n ? value\n : null;\n}\n\n/**\n * The CLI's own message out of its stderr: each `tabbied:` line and the\n * indented lines that continue it. Anything else there (Node's warnings, a\n * deprecation notice from a dependency) is noise to the agent. When there is\n * no such line, the CLI died some other way, and the tail is the best guess.\n */\nfunction cliMessage(stderr: string): string {\n const lines = stderr.split('\\n');\n const kept: string[] = [];\n let continuing = false;\n for (const line of lines) {\n if (line.startsWith('tabbied:')) {\n kept.push(line);\n continuing = true;\n } else if (continuing && /^\\s+\\S/.test(line)) {\n kept.push(line);\n } else {\n continuing = false;\n }\n }\n return kept.length > 0\n ? kept.join('\\n')\n : lines.filter((line) => line.trim()).slice(-5).join('\\n');\n}\n\nexport function renderTool(catalog: Catalog): Tool {\n return {\n definition: {\n name: 'render_design',\n title: 'Render a design to a file',\n description:\n 'Render a design to SVG or PNG with a real browser, at any size, seed, ' +\n 'palette, and option set. Use this to produce an actual asset - or to ' +\n 'preview a *customized* configuration, which preview_design cannot show ' +\n 'you (it only has the stock palette and defaults). Needs Playwright ' +\n 'where this server runs (start it as `npx -y -p tabbied-mcp -p ' +\n 'playwright tabbied-mcp`, with `npx playwright install chromium` once). ' +\n 'Without \"out\" the image comes back inline; with it, the file is ' +\n 'written where you ask, replacing any file already there.',\n inputSchema: {\n type: 'object',\n properties: {\n slug: { type: 'string', minLength: 1, description: 'The design to render.' },\n format: {\n type: 'string',\n enum: ['svg', 'png'],\n description:\n 'Vector or raster. A few designs cannot be vectorized - check ' +\n 'svgExport on the design first.',\n },\n out: {\n type: 'string',\n minLength: 1,\n description:\n 'Absolute path to write to, ending in .svg or .png to match ' +\n '\"format\". Omit to get the image back inline instead of on disk.',\n },\n seed: {\n type: 'string',\n minLength: 1,\n description:\n 'Fixed seed - the same seed always gives the same image. Random ' +\n 'when omitted.',\n },\n palette: {\n type: 'array',\n items: { type: 'string', minLength: 1 },\n minItems: 1,\n description:\n \"CSS colors, background first. Omit for the design's authored palette.\",\n },\n options: {\n type: 'object',\n additionalProperties: { type: ['string', 'number', 'boolean'] },\n description:\n 'Option values keyed by option id, e.g. {\"frequency\": 0.6}. Read ' +\n 'the ids, ranges, and choices off get_design.',\n },\n width: {\n type: 'integer',\n minimum: 1,\n maximum: SIZE_MAX,\n description: 'Pixels wide (default 960).',\n },\n height: {\n type: 'integer',\n minimum: 1,\n maximum: SIZE_MAX,\n description: 'Pixels tall (default 960).',\n },\n fit: {\n type: 'string',\n enum: ['grid', 'cover', 'fixed'],\n description: 'Sizing mode (default grid).',\n },\n scale: {\n type: 'integer',\n minimum: 1,\n maximum: SCALE_MAX,\n description:\n 'PNG device-scale factor. Defaults to 2 when writing a file and ' +\n '1 when returning inline, to keep the response small.',\n },\n },\n required: ['slug', 'format'],\n additionalProperties: false,\n },\n // It writes a file, so it is not read-only; it only ever adds or\n // replaces the one file it was asked for.\n annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },\n },\n\n async run(args): Promise<ToolResult> {\n const slug = typeof args.slug === 'string' ? args.slug : '';\n const design = catalog.designs.find((entry) => entry.slug === slug);\n if (!design) {\n return toolError(\n `No design with slug \"${slug}\". Use search_designs to find one.`\n );\n }\n\n const format = args.format === 'png' ? 'png' : 'svg';\n if (format === 'svg' && !design.svgExport.supported) {\n return toolError(\n `\"${slug}\" paints effects SVG cannot represent, so it has no vector ` +\n 'export. Render it as PNG instead.'\n );\n }\n\n // The schema states these, but a host that skips validation (or calls\n // run() directly) must not hand the CLI a path it would resolve against\n // whatever directory the server happened to start in.\n const out = typeof args.out === 'string' && args.out.length > 0 ? args.out : null;\n if (out !== null && !path.isAbsolute(out)) {\n return toolError(\n `\"out\" must be an absolute path, got \"${out}\". A relative one would ` +\n \"land in the server's working directory, not yours.\"\n );\n }\n if (out !== null && path.extname(out).toLowerCase() !== `.${format}`) {\n return toolError(\n `\"out\" must end in .${format} to match format \"${format}\", got \"${out}\".`\n );\n }\n\n const inline = out === null;\n const width = integerArg(args.width, 960, 1, SIZE_MAX);\n const height = integerArg(args.height, 960, 1, SIZE_MAX);\n const scale = integerArg(args.scale, inline ? 1 : 2, 1, SCALE_MAX);\n if (width === null || height === null) {\n return toolError(`\"width\" and \"height\" must be whole pixels from 1 to ${SIZE_MAX}.`);\n }\n if (scale === null) {\n return toolError(`\"scale\" must be a whole number from 1 to ${SCALE_MAX}.`);\n }\n\n // Options reach the CLI as one \"id: value; id: value\" string, so a `;`\n // inside a value would end it and start an option of its own, past\n // every check on the first. Ranges and choices are the CLI's to check.\n let options: string | null = null;\n if (args.options !== undefined) {\n if (!args.options || typeof args.options !== 'object' || Array.isArray(args.options)) {\n return toolError('\"options\" must be an object of option values keyed by id.');\n }\n const ids = design.options.map((option) => option.id);\n const pairs: string[] = [];\n for (const [id, value] of Object.entries(args.options as Record<string, unknown>)) {\n if (!ids.includes(id)) {\n return toolError(\n `\"${slug}\" has no option \"${id}\" (has: ${ids.join(', ') || 'none'}).`\n );\n }\n if (!['string', 'number', 'boolean'].includes(typeof value) || /[;\\n]/.test(String(value))) {\n return toolError(\n `Option \"${id}\" must be a single string, number, or boolean value ` +\n '(no \";\" or line breaks).'\n );\n }\n pairs.push(`${id}: ${String(value)}`);\n }\n if (pairs.length > 0) options = pairs.join('; ');\n }\n\n const root = tabbiedRoot();\n if (!root) {\n return toolError(\n 'The `tabbied` package is not installed next to this server, so the ' +\n 'renderer is unavailable. Run `npm install tabbied`.'\n );\n }\n\n // An inline render gets a scratch directory, removed once its bytes\n // have been read back, or once the render has failed.\n const scratch = inline ? await mkdtemp(path.join(tmpdir(), 'tabbied-')) : null;\n const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : (out as string);\n let keepScratch = false;\n\n // A value that starts with \"-\" (a seed of \"--browser\", say) would read\n // as the next flag, so it goes as --flag=value, which the CLI takes\n // from tabbied 0.7.1. Everything else stays two arguments, which every\n // version of the CLI reads.\n const flag = (name: string, value: string) =>\n value.startsWith('-') ? [`--${name}=${value}`] : [`--${name}`, value];\n const argv = [\n path.join(root, 'dist', 'cli.js'),\n 'render',\n slug,\n ...flag('out', outPath),\n ...flag('format', format),\n ...flag('scale', String(scale)),\n ...flag('size', `${width}x${height}`),\n ];\n if (typeof args.seed === 'string' && args.seed) argv.push(...flag('seed', args.seed));\n if (typeof args.fit === 'string') argv.push(...flag('fit', args.fit));\n if (Array.isArray(args.palette)) {\n const colors = args.palette.filter(\n (color): color is string => typeof color === 'string' && color.trim().length > 0\n );\n if (colors.length > 0) argv.push(...flag('palette', colors.join(',')));\n }\n if (options) argv.push(...flag('options', options));\n\n try {\n let stderr = '';\n try {\n // No shell: argv goes straight to node, so a palette or option value\n // can't be read as shell syntax.\n ({ stderr } = await run(process.execPath, argv, {\n timeout: RENDER_TIMEOUT_MS,\n maxBuffer: 8 * 1024 * 1024,\n }));\n } catch (error) {\n const failure = error as { killed?: boolean; stderr?: unknown; message?: string };\n if (failure.killed) {\n return toolError(\n `Rendering timed out after ${RENDER_TIMEOUT_MS / 1000}s. Try a smaller ` +\n 'width/height or scale.'\n );\n }\n const detail =\n typeof failure.stderr === 'string' && failure.stderr.trim()\n ? cliMessage(failure.stderr)\n : (failure.message ?? String(error));\n return toolError(`Rendering failed:\\n${detail}`);\n }\n\n // The CLI reports SVG-fidelity caveats on stderr; they matter enough to\n // pass through rather than swallow.\n const notes = stderr\n .split('\\n')\n .filter((line) => line.trim().startsWith('note:'))\n .join('\\n');\n\n if (!inline) {\n const { size } = await stat(outPath);\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} (${width}x${height}` +\n (format === 'png' ? ` @${scale}x` : '') +\n `, ${Math.round(size / 1024)} KB).` +\n (notes ? `\\n${notes}` : '')\n ),\n ],\n };\n }\n\n if (format === 'svg') {\n const svg = await readFile(outPath, 'utf-8');\n return {\n content: [\n text(\n `Rendered ${slug} as SVG (${width}x${height}).` +\n (notes ? `\\n${notes}` : '')\n ),\n text(svg),\n ],\n };\n }\n\n const png = await readFile(outPath);\n const data = png.toString('base64');\n if (data.length > INLINE_BYTE_BUDGET) {\n // Kept: the answer tells the agent to read it from that path.\n keepScratch = true;\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} - ${Math.round(\n png.byteLength / 1024\n )} KB, too large to return inline. Read it from that path, or ` +\n 're-render smaller (lower \"scale\", or a smaller width/height).'\n ),\n ],\n };\n }\n\n return {\n content: [\n text(\n `Rendered ${slug} (${width}x${height} @${scale}x).` +\n (notes ? `\\n${notes}` : '')\n ),\n { type: 'image', data, mimeType: 'image/png' },\n ],\n };\n } finally {\n if (scratch && !keepScratch) await rm(scratch, { recursive: true, force: true });\n }\n },\n };\n}\n"]}
@@ -9,6 +9,34 @@ import { readFile } from 'node:fs/promises';
9
9
  import path from 'node:path';
10
10
  const require = createRequire(import.meta.url);
11
11
  const SITE = 'https://tabbied.com';
12
+ // Every read here is a small static file. Without a limit, a network that
13
+ // swallows packets would hold a tool call (or the server's startup) open for
14
+ // as long as the client is willing to wait.
15
+ const FETCH_TIMEOUT_MS = 10_000;
16
+ /**
17
+ * GET a URL, failing with a message that names it. Node's own failure is a
18
+ * bare "fetch failed" (the reason is in `cause`), which tells an agent nothing
19
+ * about what was unreachable.
20
+ */
21
+ async function get(url) {
22
+ let response;
23
+ try {
24
+ response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
25
+ }
26
+ catch (error) {
27
+ const reason = error instanceof Error && error.name === 'TimeoutError'
28
+ ? `no answer within ${FETCH_TIMEOUT_MS / 1000}s`
29
+ : error instanceof Error
30
+ ? [error.message, error.cause?.message]
31
+ .filter(Boolean)
32
+ .join(': ')
33
+ : String(error);
34
+ throw new Error(`${url} could not be reached (${reason})`);
35
+ }
36
+ if (!response.ok)
37
+ throw new Error(`${url} returned ${response.status}`);
38
+ return response;
39
+ }
12
40
  /** Root of the installed `tabbied` package, or null when it isn't resolvable. */
13
41
  export function tabbiedRoot() {
14
42
  try {
@@ -33,22 +61,20 @@ export async function loadCatalog() {
33
61
  `falling back to ${SITE}/catalog.json\n`);
34
62
  }
35
63
  }
36
- const response = await fetch(`${SITE}/catalog.json`);
37
- if (!response.ok) {
38
- throw new Error(`no local catalog and ${SITE}/catalog.json returned ${response.status}. ` +
64
+ try {
65
+ return (await (await get(`${SITE}/catalog.json`)).json());
66
+ }
67
+ catch (error) {
68
+ throw new Error(`no local catalog, and ${error instanceof Error ? error.message : String(error)}. ` +
39
69
  'Install the `tabbied` package (npm i tabbied) or restore network access.');
40
70
  }
41
- return (await response.json());
42
71
  }
43
72
  /**
44
73
  * Preview bytes for one design: the committed @2x renders the site serves, so
45
74
  * the agent sees the same image a human browsing the gallery does.
46
75
  */
47
76
  export async function fetchPreview(design) {
48
- const response = await fetch(design.preview);
49
- if (!response.ok) {
50
- throw new Error(`${design.preview} returned ${response.status}`);
51
- }
77
+ const response = await get(design.preview);
52
78
  const buffer = Buffer.from(await response.arrayBuffer());
53
79
  return {
54
80
  data: buffer.toString('base64'),
@@ -69,11 +95,7 @@ export async function fetchDocs() {
69
95
  // fall through to the network copy
70
96
  }
71
97
  }
72
- const response = await fetch(`${SITE}/llms-full.txt`);
73
- if (!response.ok) {
74
- throw new Error(`${SITE}/llms-full.txt returned ${response.status}`);
75
- }
76
- return await response.text();
98
+ return await (await get(`${SITE}/llms-full.txt`)).text();
77
99
  }
78
100
  /**
79
101
  * The editable-template index and one site's spec. Network only, deliberately:
@@ -81,17 +103,9 @@ export async function fetchDocs() {
81
103
  * (docs/editable-templates.md), and the `tabbied` package does not contain them.
82
104
  */
83
105
  export async function fetchTemplateCatalog() {
84
- const response = await fetch(`${SITE}/editable-catalog.json`);
85
- if (!response.ok) {
86
- throw new Error(`${SITE}/editable-catalog.json returned ${response.status}`);
87
- }
88
- return (await response.json());
106
+ return (await (await get(`${SITE}/editable-catalog.json`)).json());
89
107
  }
90
108
  export async function fetchTemplate(slug) {
91
- const response = await fetch(`${SITE}/editable/${slug}.json`);
92
- if (!response.ok) {
93
- throw new Error(`${SITE}/editable/${slug}.json returned ${response.status}`);
94
- }
95
- return (await response.json());
109
+ return (await (await get(`${SITE}/editable/${encodeURIComponent(slug)}.json`)).json());
96
110
  }
97
111
  //# sourceMappingURL=resources.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/node/resources.ts"],"names":[],"mappings":"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","sourcesContent":["// Where the stdio server gets its catalog, previews, and docs.\n//\n// The catalog is read from the installed `tabbied` package rather than fetched,\n// so the tools describe exactly the version the caller will `npm install`: a\n// design that only exists on the site would give an import that doesn't\n// resolve. The network is the fallback, not the default.\nimport { createRequire } from 'node:module';\nimport { readFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type {\n Catalog,\n CatalogDesign,\n TemplateCatalog,\n TemplateSpec,\n} from '../types.js';\n\nconst require = createRequire(import.meta.url);\n\nconst SITE = 'https://tabbied.com';\n\n/** Root of the installed `tabbied` package, or null when it isn't resolvable. */\nexport function tabbiedRoot(): string | null {\n try {\n return path.dirname(require.resolve('tabbied/package.json'));\n } catch {\n return null;\n }\n}\n\nexport async function loadCatalog(): Promise<Catalog> {\n const root = tabbiedRoot();\n\n if (root) {\n try {\n return JSON.parse(\n await readFile(path.join(root, 'catalog.json'), 'utf-8')\n ) as Catalog;\n } catch (error) {\n // A resolvable package with no catalog.json means a source checkout that\n // hasn't been built. Say so - the network copy would silently disagree\n // with the local patterns the user is about to edit.\n const reason = error instanceof Error ? error.message : String(error);\n process.stderr.write(\n `tabbied-mcp: could not read the local catalog (${reason}); ` +\n `falling back to ${SITE}/catalog.json\\n`\n );\n }\n }\n\n const response = await fetch(`${SITE}/catalog.json`);\n if (!response.ok) {\n throw new Error(\n `no local catalog and ${SITE}/catalog.json returned ${response.status}. ` +\n 'Install the `tabbied` package (npm i tabbied) or restore network access.'\n );\n }\n return (await response.json()) as Catalog;\n}\n\n/**\n * Preview bytes for one design: the committed @2x renders the site serves, so\n * the agent sees the same image a human browsing the gallery does.\n */\nexport async function fetchPreview(\n design: CatalogDesign\n): Promise<{ data: string; mimeType: string }> {\n const response = await fetch(design.preview);\n if (!response.ok) {\n throw new Error(`${design.preview} returned ${response.status}`);\n }\n const buffer = Buffer.from(await response.arrayBuffer());\n return {\n data: buffer.toString('base64'),\n mimeType: response.headers.get('Content-Type') ?? 'image/webp',\n };\n}\n\n/**\n * The full reference. The package's own llms.txt is the *full* text (in\n * node_modules depth beats brevity), so this works offline too.\n */\nexport async function fetchDocs(): Promise<string> {\n const root = tabbiedRoot();\n if (root) {\n try {\n return await readFile(path.join(root, 'llms.txt'), 'utf-8');\n } catch {\n // fall through to the network copy\n }\n }\n\n const response = await fetch(`${SITE}/llms-full.txt`);\n if (!response.ok) {\n throw new Error(`${SITE}/llms-full.txt returned ${response.status}`);\n }\n return await response.text();\n}\n\n/**\n * The editable-template index and one site's spec. Network only, deliberately:\n * they are *site* artifacts generated from the static export\n * (docs/editable-templates.md), and the `tabbied` package does not contain them.\n */\nexport async function fetchTemplateCatalog(): Promise<TemplateCatalog> {\n const response = await fetch(`${SITE}/editable-catalog.json`);\n\n if (!response.ok) {\n throw new Error(`${SITE}/editable-catalog.json returned ${response.status}`);\n }\n\n return (await response.json()) as TemplateCatalog;\n}\n\nexport async function fetchTemplate(slug: string): Promise<TemplateSpec> {\n const response = await fetch(`${SITE}/editable/${slug}.json`);\n\n if (!response.ok) {\n throw new Error(`${SITE}/editable/${slug}.json returned ${response.status}`);\n }\n\n return (await response.json()) as TemplateSpec;\n}\n"]}
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/node/resources.ts"],"names":[],"mappings":"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","sourcesContent":["// Where the stdio server gets its catalog, previews, and docs.\n//\n// The catalog is read from the installed `tabbied` package rather than fetched,\n// so the tools describe exactly the version the caller will `npm install`: a\n// design that only exists on the site would give an import that doesn't\n// resolve. The network is the fallback, not the default.\nimport { createRequire } from 'node:module';\nimport { readFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type {\n Catalog,\n CatalogDesign,\n TemplateCatalog,\n TemplateSpec,\n} from '../types.js';\n\nconst require = createRequire(import.meta.url);\n\nconst SITE = 'https://tabbied.com';\n\n// Every read here is a small static file. Without a limit, a network that\n// swallows packets would hold a tool call (or the server's startup) open for\n// as long as the client is willing to wait.\nconst FETCH_TIMEOUT_MS = 10_000;\n\n/**\n * GET a URL, failing with a message that names it. Node's own failure is a\n * bare \"fetch failed\" (the reason is in `cause`), which tells an agent nothing\n * about what was unreachable.\n */\nasync function get(url: string): Promise<Response> {\n let response: Response;\n try {\n response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });\n } catch (error) {\n const reason =\n error instanceof Error && error.name === 'TimeoutError'\n ? `no answer within ${FETCH_TIMEOUT_MS / 1000}s`\n : error instanceof Error\n ? [error.message, (error.cause as Error | undefined)?.message]\n .filter(Boolean)\n .join(': ')\n : String(error);\n throw new Error(`${url} could not be reached (${reason})`);\n }\n if (!response.ok) throw new Error(`${url} returned ${response.status}`);\n return response;\n}\n\n/** Root of the installed `tabbied` package, or null when it isn't resolvable. */\nexport function tabbiedRoot(): string | null {\n try {\n return path.dirname(require.resolve('tabbied/package.json'));\n } catch {\n return null;\n }\n}\n\nexport async function loadCatalog(): Promise<Catalog> {\n const root = tabbiedRoot();\n\n if (root) {\n try {\n return JSON.parse(\n await readFile(path.join(root, 'catalog.json'), 'utf-8')\n ) as Catalog;\n } catch (error) {\n // A resolvable package with no catalog.json means a source checkout that\n // hasn't been built. Say so - the network copy would silently disagree\n // with the local patterns the user is about to edit.\n const reason = error instanceof Error ? error.message : String(error);\n process.stderr.write(\n `tabbied-mcp: could not read the local catalog (${reason}); ` +\n `falling back to ${SITE}/catalog.json\\n`\n );\n }\n }\n\n try {\n return (await (await get(`${SITE}/catalog.json`)).json()) as Catalog;\n } catch (error) {\n throw new Error(\n `no local catalog, and ${error instanceof Error ? error.message : String(error)}. ` +\n 'Install the `tabbied` package (npm i tabbied) or restore network access.'\n );\n }\n}\n\n/**\n * Preview bytes for one design: the committed @2x renders the site serves, so\n * the agent sees the same image a human browsing the gallery does.\n */\nexport async function fetchPreview(\n design: CatalogDesign\n): Promise<{ data: string; mimeType: string }> {\n const response = await get(design.preview);\n const buffer = Buffer.from(await response.arrayBuffer());\n return {\n data: buffer.toString('base64'),\n mimeType: response.headers.get('Content-Type') ?? 'image/webp',\n };\n}\n\n/**\n * The full reference. The package's own llms.txt is the *full* text (in\n * node_modules depth beats brevity), so this works offline too.\n */\nexport async function fetchDocs(): Promise<string> {\n const root = tabbiedRoot();\n if (root) {\n try {\n return await readFile(path.join(root, 'llms.txt'), 'utf-8');\n } catch {\n // fall through to the network copy\n }\n }\n\n return await (await get(`${SITE}/llms-full.txt`)).text();\n}\n\n/**\n * The editable-template index and one site's spec. Network only, deliberately:\n * they are *site* artifacts generated from the static export\n * (docs/editable-templates.md), and the `tabbied` package does not contain them.\n */\nexport async function fetchTemplateCatalog(): Promise<TemplateCatalog> {\n return (await (await get(`${SITE}/editable-catalog.json`)).json()) as TemplateCatalog;\n}\n\nexport async function fetchTemplate(slug: string): Promise<TemplateSpec> {\n return (await (await get(`${SITE}/editable/${encodeURIComponent(slug)}.json`)).json()) as TemplateSpec;\n}\n"]}
package/dist/server.js CHANGED
@@ -17,12 +17,20 @@ import { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';
17
17
  * values instead of silently matching nothing.
18
18
  */
19
19
  export function buildServer(tools) {
20
- const server = new McpServer({ name: SERVER_NAME, version: VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
20
+ // `listChanged: false` because the list never changes: a server is built
21
+ // per request (per connection on stdio) with its tools fixed, and nothing
22
+ // ever sends the notification. Left unset, the SDK advertises it as true the
23
+ // moment the first tool registers, inviting a modern client to listen for
24
+ // a change that cannot come.
25
+ const server = new McpServer({ name: SERVER_NAME, version: VERSION }, { capabilities: { tools: { listChanged: false } }, instructions: INSTRUCTIONS });
21
26
  for (const tool of tools) {
22
27
  server.registerTool(tool.definition.name, {
23
28
  title: tool.definition.title,
24
29
  description: tool.definition.description,
25
30
  inputSchema: fromJsonSchema(tool.definition.inputSchema),
31
+ ...(tool.definition.annotations
32
+ ? { annotations: tool.definition.annotations }
33
+ : {}),
26
34
  }, async (args) => (await tool.run(args ?? {})));
27
35
  }
28
36
  return server;
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,6EAA6E;AAC7E,sEAAsE;AACtE,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,qBAAqB;AACrB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG/D;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,EACvC,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,CAC5D,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,YAAY,CACjB,IAAI,CAAC,UAAU,CAAC,IAAI,EACpB;YACE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,KAAK;YAC5B,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;YACxC,WAAW,EAAE,cAAc,CACzB,IAAI,CAAC,UAAU,CAAC,WAAW,CAC5B;SACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAU,CACtD,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// Turns the runtime-agnostic toolset into an SDK `McpServer`. The Worker hands\n// the factory to `createMcpHandler` and the bin hands it to `serveStdio`, so\n// both transports get the same tools, instructions, and era handling.\n//\n// A *factory*, not an instance: MCP v2 is stateless and the SDK builds one\n// server per request (per connection on stdio). Nothing here may capture\n// per-request state.\nimport { McpServer, fromJsonSchema } from '@modelcontextprotocol/server';\n\nimport { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';\nimport type { Tool } from './types.js';\n\n/**\n * Register a toolset onto a fresh `McpServer`.\n *\n * Tool schemas stay plain JSON Schema, adapted by `fromJsonSchema`, because\n * `search_designs`'s enums are derived from the catalog being served and so\n * cannot be static Zod. Registering the schema also makes the SDK validate\n * arguments: a bad enum value comes back as a tool error naming the allowed\n * values instead of silently matching nothing.\n */\nexport function buildServer(tools: Tool[]): McpServer {\n const server = new McpServer(\n { name: SERVER_NAME, version: VERSION },\n { capabilities: { tools: {} }, instructions: INSTRUCTIONS }\n );\n\n for (const tool of tools) {\n server.registerTool(\n tool.definition.name,\n {\n title: tool.definition.title,\n description: tool.definition.description,\n inputSchema: fromJsonSchema<Record<string, unknown>>(\n tool.definition.inputSchema\n ),\n },\n async (args) => (await tool.run(args ?? {})) as never\n );\n }\n\n return server;\n}\n"]}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,6EAA6E;AAC7E,sEAAsE;AACtE,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,qBAAqB;AACrB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG/D;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,yEAAyE;IACzE,0EAA0E;IAC1E,6EAA6E;IAC7E,0EAA0E;IAC1E,6BAA6B;IAC7B,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,EACvC,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,CAChF,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,YAAY,CACjB,IAAI,CAAC,UAAU,CAAC,IAAI,EACpB;YACE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,KAAK;YAC5B,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;YACxC,WAAW,EAAE,cAAc,CACzB,IAAI,CAAC,UAAU,CAAC,WAAW,CAC5B;YACD,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW;gBAC7B,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE;gBAC9C,CAAC,CAAC,EAAE,CAAC;SACR,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAU,CACtD,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// Turns the runtime-agnostic toolset into an SDK `McpServer`. The Worker hands\n// the factory to `createMcpHandler` and the bin hands it to `serveStdio`, so\n// both transports get the same tools, instructions, and era handling.\n//\n// A *factory*, not an instance: MCP v2 is stateless and the SDK builds one\n// server per request (per connection on stdio). Nothing here may capture\n// per-request state.\nimport { McpServer, fromJsonSchema } from '@modelcontextprotocol/server';\n\nimport { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';\nimport type { Tool } from './types.js';\n\n/**\n * Register a toolset onto a fresh `McpServer`.\n *\n * Tool schemas stay plain JSON Schema, adapted by `fromJsonSchema`, because\n * `search_designs`'s enums are derived from the catalog being served and so\n * cannot be static Zod. Registering the schema also makes the SDK validate\n * arguments: a bad enum value comes back as a tool error naming the allowed\n * values instead of silently matching nothing.\n */\nexport function buildServer(tools: Tool[]): McpServer {\n // `listChanged: false` because the list never changes: a server is built\n // per request (per connection on stdio) with its tools fixed, and nothing\n // ever sends the notification. Left unset, the SDK advertises it as true the\n // moment the first tool registers, inviting a modern client to listen for\n // a change that cannot come.\n const server = new McpServer(\n { name: SERVER_NAME, version: VERSION },\n { capabilities: { tools: { listChanged: false } }, instructions: INSTRUCTIONS }\n );\n\n for (const tool of tools) {\n server.registerTool(\n tool.definition.name,\n {\n title: tool.definition.title,\n description: tool.definition.description,\n inputSchema: fromJsonSchema<Record<string, unknown>>(\n tool.definition.inputSchema\n ),\n ...(tool.definition.annotations\n ? { annotations: tool.definition.annotations }\n : {}),\n },\n async (args) => (await tool.run(args ?? {})) as never\n );\n }\n\n return server;\n}\n"]}
package/dist/stdio.js CHANGED
@@ -6,6 +6,8 @@
6
6
  //
7
7
  // Configure it in an MCP client as:
8
8
  // { "command": "npx", "args": ["-y", "tabbied-mcp"] }
9
+ // or, for render_design, with Playwright installed beside it:
10
+ // { "command": "npx", "args": ["-y", "-p", "tabbied-mcp", "-p", "playwright", "tabbied-mcp"] }
9
11
  import { serveStdio } from '@modelcontextprotocol/server/stdio';
10
12
  import { buildServer } from './server.js';
11
13
  import { catalogTools } from './tools.js';
package/dist/stdio.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":";AACA,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;AAC3E,uCAAuC;AACvC,EAAE;AACF,oCAAoC;AACpC,wDAAwD;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,SAAS,EACT,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;IAEpC,MAAM,KAAK,GAAG;QACZ,GAAG,YAAY,CAAC;YACd,OAAO;YACP,YAAY;YACZ,SAAS;YACT,oBAAoB;YACpB,aAAa;SACd,CAAC;QACF,qEAAqE;QACrE,UAAU,CAAC,OAAO,CAAC;KACpB,CAAC;IAEF,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAEpD,4EAA4E;IAC5E,uBAAuB;IACvB,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAC7B,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,gEAAgE;IAChE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gBAAgB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC3E,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n// The `tabbied-mcp` bin: the same server the site exposes at /mcp, over stdio.\n// `serveStdio` owns the transport (framing, the era decision, one instance per\n// connection). The catalog is loaded once here rather than in the factory,\n// because it is immutable per process.\n//\n// Configure it in an MCP client as:\n// { \"command\": \"npx\", \"args\": [\"-y\", \"tabbied-mcp\"] }\nimport { serveStdio } from '@modelcontextprotocol/server/stdio';\n\nimport { buildServer } from './server.js';\nimport { catalogTools } from './tools.js';\nimport {\n fetchDocs,\n fetchPreview,\n fetchTemplate,\n fetchTemplateCatalog,\n loadCatalog,\n} from './node/resources.js';\nimport { renderTool } from './node/render.js';\n\nasync function main(): Promise<void> {\n const catalog = await loadCatalog();\n\n const tools = [\n ...catalogTools({\n catalog,\n fetchPreview,\n fetchDocs,\n fetchTemplateCatalog,\n fetchTemplate,\n }),\n // Only the local server can render: it has a browser to render with.\n renderTool(catalog),\n ];\n\n const handle = serveStdio(() => buildServer(tools));\n\n // The client closing stdin is the shutdown signal; without this the process\n // outlives the client.\n process.stdin.on('close', () => {\n void handle.close();\n });\n}\n\nmain().catch((error) => {\n // stdout is the protocol channel and nothing else may touch it.\n process.stderr.write(\n `tabbied-mcp: ${error instanceof Error ? error.message : String(error)}\\n`\n );\n process.exit(1);\n});\n"]}
1
+ {"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":";AACA,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;AAC3E,uCAAuC;AACvC,EAAE;AACF,oCAAoC;AACpC,wDAAwD;AACxD,8DAA8D;AAC9D,iGAAiG;AACjG,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,SAAS,EACT,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;IAEpC,MAAM,KAAK,GAAG;QACZ,GAAG,YAAY,CAAC;YACd,OAAO;YACP,YAAY;YACZ,SAAS;YACT,oBAAoB;YACpB,aAAa;SACd,CAAC;QACF,qEAAqE;QACrE,UAAU,CAAC,OAAO,CAAC;KACpB,CAAC;IAEF,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAEpD,4EAA4E;IAC5E,uBAAuB;IACvB,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAC7B,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,gEAAgE;IAChE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gBAAgB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC3E,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n// The `tabbied-mcp` bin: the same server the site exposes at /mcp, over stdio.\n// `serveStdio` owns the transport (framing, the era decision, one instance per\n// connection). The catalog is loaded once here rather than in the factory,\n// because it is immutable per process.\n//\n// Configure it in an MCP client as:\n// { \"command\": \"npx\", \"args\": [\"-y\", \"tabbied-mcp\"] }\n// or, for render_design, with Playwright installed beside it:\n// { \"command\": \"npx\", \"args\": [\"-y\", \"-p\", \"tabbied-mcp\", \"-p\", \"playwright\", \"tabbied-mcp\"] }\nimport { serveStdio } from '@modelcontextprotocol/server/stdio';\n\nimport { buildServer } from './server.js';\nimport { catalogTools } from './tools.js';\nimport {\n fetchDocs,\n fetchPreview,\n fetchTemplate,\n fetchTemplateCatalog,\n loadCatalog,\n} from './node/resources.js';\nimport { renderTool } from './node/render.js';\n\nasync function main(): Promise<void> {\n const catalog = await loadCatalog();\n\n const tools = [\n ...catalogTools({\n catalog,\n fetchPreview,\n fetchDocs,\n fetchTemplateCatalog,\n fetchTemplate,\n }),\n // Only the local server can render: it has a browser to render with.\n renderTool(catalog),\n ];\n\n const handle = serveStdio(() => buildServer(tools));\n\n // The client closing stdin is the shutdown signal; without this the process\n // outlives the client.\n process.stdin.on('close', () => {\n void handle.close();\n });\n}\n\nmain().catch((error) => {\n // stdout is the protocol channel and nothing else may touch it.\n process.stderr.write(\n `tabbied-mcp: ${error instanceof Error ? error.message : String(error)}\\n`\n );\n process.exit(1);\n});\n"]}
package/dist/templates.js CHANGED
@@ -1,3 +1,6 @@
1
+ const SITE = 'https://tabbied.com';
2
+ const LIST_LIMIT_DEFAULT = 20;
3
+ const LIST_LIMIT_MAX = 100;
1
4
  const text = (value) => ({ type: 'text', text: value });
2
5
  const json = (value) => text(JSON.stringify(value, null, 2));
3
6
  const toolError = (message) => ({
@@ -30,7 +33,8 @@ const USAGE = {
30
33
  programmatic: 'To apply changes programmatically, write an edits document ' +
31
34
  '({specVersion, slug, edits}) and run it through applyEdits() from the ' +
32
35
  '`tabbied-templates` npm package, which validates it against this spec ' +
33
- 'first. Full reference: https://tabbied.com/docs/editable-templates.',
36
+ 'first. Full reference: ' +
37
+ 'https://github.com/tabbied-design/tabbied/blob/main/docs/editable-templates.md',
34
38
  };
35
39
  /**
36
40
  * What an agent should know before it touches a template. Stated, not
@@ -46,16 +50,59 @@ const LICENSE = 'Templates are licensed per Tabbied account, not free to copy. A
46
50
  'pictures from the preview, is not permitted. If you are asked to, tell the ' +
47
51
  'person the template is licensed and point them to its page. Terms: ' +
48
52
  'https://tabbied.com/terms-of-service/#template-license';
53
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
54
+ /**
55
+ * The index, or the tool error that says where it lives. A host that cannot
56
+ * reach it (a local server offline, say) would otherwise answer with the bare
57
+ * "fetch failed" of whatever runtime it is on.
58
+ */
59
+ async function loadIndex(fetchTemplateCatalog) {
60
+ try {
61
+ return await fetchTemplateCatalog();
62
+ }
63
+ catch (error) {
64
+ return toolError(`Could not load the template index (${errorMessage(error)}). It is ` +
65
+ `published at ${SITE}/editable-catalog.json and the templates can be ` +
66
+ `browsed at ${SITE}/templates/; a server running locally needs to reach ` +
67
+ `${SITE} for these two tools.`);
68
+ }
69
+ }
70
+ const isResult = (value) => 'content' in value;
71
+ /** A category as the gallery's URL spells it: "Food & drink" is food-and-drink. */
72
+ const categoryKey = (category) => category
73
+ .toLowerCase()
74
+ .replace(/&/g, 'and')
75
+ .replace(/[^a-z0-9]+/g, '-')
76
+ .replace(/^-|-$/g, '');
77
+ /** One line per template: enough to choose, with get_template for the rest. */
49
78
  function summarize(entry) {
50
79
  return {
51
80
  slug: entry.slug,
52
81
  name: entry.name,
82
+ ...(entry.category ? { category: entry.category } : {}),
83
+ ...(entry.topic ? { topic: entry.topic } : {}),
84
+ };
85
+ }
86
+ function describe(entry) {
87
+ return {
88
+ ...summarize(entry),
53
89
  palette: entry.palette,
54
90
  patterns: entry.patterns,
55
91
  editable: entry.slots,
56
- url: `https://tabbied.com${entry.href}`,
92
+ url: `${SITE}${entry.href}`,
57
93
  };
58
94
  }
95
+ /** Every category in the index with its count, largest first. */
96
+ function categoryCounts(templates) {
97
+ const counts = new Map();
98
+ for (const entry of templates) {
99
+ if (entry.category)
100
+ counts.set(entry.category, (counts.get(entry.category) ?? 0) + 1);
101
+ }
102
+ return [...counts]
103
+ .sort(([a, countA], [b, countB]) => countB - countA || a.localeCompare(b))
104
+ .map(([category, count]) => ({ category, count }));
105
+ }
59
106
  /**
60
107
  * Suggest near-misses for a slug that isn't in the catalog, so a wrong guess
61
108
  * comes back as a correction rather than a dead end.
@@ -63,6 +110,8 @@ function summarize(entry) {
63
110
  function suggestSlugs(catalog, slug) {
64
111
  const needle = slug.toLowerCase();
65
112
  const prefix = needle.slice(0, 3);
113
+ if (!needle)
114
+ return [];
66
115
  return catalog.templates
67
116
  .filter((entry) => entry.slug.startsWith(prefix) ||
68
117
  entry.name.toLowerCase().includes(needle) ||
@@ -79,51 +128,112 @@ function listTool(context) {
79
128
  name: 'list_templates',
80
129
  title: 'List the editable template sites',
81
130
  description: 'The Tabbied template sites that can be customized: finished, ' +
82
- 'single-page brand sites built around a pattern, each downloadable as ' +
83
- 'plain HTML or as a React (Vite) project. Returns each one with its ' +
84
- 'palette, the patterns it uses, and how many text/image/pattern slots ' +
85
- 'are editable. Start here when the task is "build me a site" rather ' +
86
- 'than "pick me a pattern" - then get_template for the one you want. ' +
87
- 'Only sites that have been annotated appear; the rest are not yet ' +
88
- 'customizable. Templates are licensed per Tabbied account: see ' +
89
- '`license` in the result.',
131
+ 'single-page sites for a business, each built around a pattern and ' +
132
+ 'downloadable as plain HTML or as a React (Vite) project. Returns a ' +
133
+ 'page of one-line entries (slug, name, category, what the business ' +
134
+ 'is), the count that matched, and every category with its count; ' +
135
+ 'filter by category or query and page with offset. Start here when ' +
136
+ 'the task is "build me a site" rather than "pick me a pattern" - then ' +
137
+ 'get_template for the one you want. Templates are licensed per ' +
138
+ 'Tabbied account: see `license` in the result.',
90
139
  inputSchema: {
91
140
  type: 'object',
92
141
  properties: {
93
142
  query: {
94
143
  type: 'string',
95
- description: 'Optional substring matched against the slug and the name.',
144
+ description: 'Words matched against the slug, name, category, and what the ' +
145
+ 'business is (e.g. "bakery", "dentist"). Every word must appear.',
146
+ },
147
+ category: {
148
+ type: 'string',
149
+ description: 'A gallery category, e.g. "Food & drink" (or food-and-drink). ' +
150
+ 'Every result lists the categories there are.',
151
+ },
152
+ detail: {
153
+ type: 'boolean',
154
+ description: "Also return each template's palette, patterns, editable slot " +
155
+ 'counts and URL. Off by default to keep the page short.',
156
+ },
157
+ limit: {
158
+ type: 'integer',
159
+ minimum: 1,
160
+ maximum: LIST_LIMIT_MAX,
161
+ description: `Templates per page (default ${LIST_LIMIT_DEFAULT}).`,
162
+ },
163
+ offset: {
164
+ type: 'integer',
165
+ minimum: 0,
166
+ description: 'How many matches to skip, for the next page (default 0).',
96
167
  },
97
168
  },
98
169
  additionalProperties: false,
99
170
  },
171
+ annotations: { readOnlyHint: true, openWorldHint: true },
100
172
  },
101
173
  async run(args) {
102
- const catalog = await fetchTemplateCatalog();
103
- const query = typeof args.query === 'string' ? args.query.trim().toLowerCase() : '';
104
- const matched = query
105
- ? catalog.templates.filter((entry) => entry.slug.toLowerCase().includes(query) ||
106
- entry.name.toLowerCase().includes(query))
107
- : catalog.templates;
174
+ const catalog = await loadIndex(fetchTemplateCatalog);
175
+ if (isResult(catalog))
176
+ return catalog;
177
+ const terms = typeof args.query === 'string'
178
+ ? args.query.toLowerCase().split(/\s+/).filter(Boolean)
179
+ : [];
180
+ const category = typeof args.category === 'string' && args.category.trim()
181
+ ? args.category.trim()
182
+ : null;
183
+ const limit = Math.min(Math.max(typeof args.limit === 'number' && Number.isFinite(args.limit)
184
+ ? Math.trunc(args.limit)
185
+ : LIST_LIMIT_DEFAULT, 1), LIST_LIMIT_MAX);
186
+ const offset = typeof args.offset === 'number' && Number.isFinite(args.offset)
187
+ ? Math.max(Math.trunc(args.offset), 0)
188
+ : 0;
189
+ const matched = catalog.templates.filter((entry) => {
190
+ const haystack = `${entry.slug} ${entry.name} ${entry.category ?? ''} ${entry.topic ?? ''}`.toLowerCase();
191
+ return (terms.every((term) => haystack.includes(term)) &&
192
+ (!category ||
193
+ (entry.category !== undefined &&
194
+ categoryKey(entry.category) === categoryKey(category))));
195
+ });
196
+ const categories = categoryCounts(catalog.templates);
197
+ const total = catalog.templates.length;
108
198
  if (matched.length === 0) {
109
199
  return {
110
200
  content: [
111
201
  json({
112
202
  matched: 0,
113
- total: catalog.templates.length,
114
- slugs: catalog.templates.map((entry) => entry.slug),
115
- hint: 'No name or slug contains that. Every available slug is listed above.',
203
+ total,
204
+ ...(categories.length > 0 ? { categories } : {}),
205
+ hint: category && categories.length === 0
206
+ ? 'This index carries no categories (an older deployment of the ' +
207
+ 'site); filter with query instead.'
208
+ : 'Nothing matches every filter. Check the category against the ' +
209
+ 'list above, use fewer or broader query words, or drop both ' +
210
+ 'to page through everything.',
116
211
  }),
117
212
  ],
118
213
  };
119
214
  }
215
+ const page = matched.slice(offset, offset + limit);
216
+ const nextOffset = offset + page.length;
120
217
  return {
121
218
  content: [
122
219
  json({
123
220
  matched: matched.length,
124
- total: catalog.templates.length,
221
+ total,
222
+ offset,
223
+ returned: page.length,
224
+ ...(page.length === 0
225
+ ? { hint: `offset ${offset} is past the last match; there are ${matched.length}.` }
226
+ : nextOffset < matched.length
227
+ ? {
228
+ next: `Showing ${offset + 1}-${nextOffset} of ${matched.length}. ` +
229
+ `Pass offset ${nextOffset} for the next page, or narrow with ` +
230
+ 'category or query.',
231
+ }
232
+ : {}),
233
+ // Absent from an index written before categories were added.
234
+ ...(categories.length > 0 ? { categories } : {}),
125
235
  license: LICENSE,
126
- templates: matched.map(summarize),
236
+ templates: page.map(args.detail === true ? describe : summarize),
127
237
  }),
128
238
  ],
129
239
  };
@@ -150,18 +260,22 @@ function getTool(context) {
150
260
  properties: {
151
261
  slug: {
152
262
  type: 'string',
263
+ minLength: 1,
153
264
  description: 'Template slug, from list_templates.',
154
265
  },
155
266
  },
156
267
  required: ['slug'],
157
268
  additionalProperties: false,
158
269
  },
270
+ annotations: { readOnlyHint: true, openWorldHint: true },
159
271
  },
160
272
  async run(args) {
161
273
  const slug = typeof args.slug === 'string' ? args.slug.trim() : '';
162
274
  if (!slug)
163
275
  return toolError('get_template needs a `slug`.');
164
- const catalog = await fetchTemplateCatalog();
276
+ const catalog = await loadIndex(fetchTemplateCatalog);
277
+ if (isResult(catalog))
278
+ return catalog;
165
279
  const entry = catalog.templates.find((candidate) => candidate.slug === slug);
166
280
  if (!entry) {
167
281
  const suggestions = suggestSlugs(catalog, slug);
@@ -175,17 +289,19 @@ function getTool(context) {
175
289
  spec = await fetchTemplate(slug);
176
290
  }
177
291
  catch (error) {
178
- return toolError(`Could not load the spec for "${slug}" (${error instanceof Error ? error.message : String(error)}).`);
292
+ return toolError(`Could not load the spec for "${slug}" (${errorMessage(error)}). It ` +
293
+ `is published at ${SITE}${entry.spec}, and the template's page is ` +
294
+ `${SITE}${entry.href}.`);
179
295
  }
180
296
  const downloads = {
181
- html: `https://tabbied.com${entry.downloads.html}`,
182
- react: `https://tabbied.com${entry.downloads.react}`,
297
+ html: `${SITE}${entry.downloads.html}`,
298
+ react: `${SITE}${entry.downloads.react}`,
183
299
  };
184
300
  return {
185
301
  content: [
186
302
  json({
187
303
  ...spec,
188
- url: `https://tabbied.com${entry.href}`,
304
+ url: `${SITE}${entry.href}`,
189
305
  license: LICENSE,
190
306
  downloads,
191
307
  usage: {