tabbied-mcp 0.2.2 → 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.
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\r\n// the factory to `createMcpHandler` and the bin hands it to `serveStdio`, so\r\n// both transports get the same tools, instructions, and era handling.\r\n//\r\n// A *factory*, not an instance: MCP v2 is stateless and the SDK builds one\r\n// server per request (per connection on stdio). Nothing here may capture\r\n// per-request state.\r\nimport { McpServer, fromJsonSchema } from '@modelcontextprotocol/server';\r\n\r\nimport { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';\r\nimport type { Tool } from './types.js';\r\n\r\n/**\r\n * Register a toolset onto a fresh `McpServer`.\r\n *\r\n * Tool schemas stay plain JSON Schema, adapted by `fromJsonSchema`, because\r\n * `search_designs`'s enums are derived from the catalog being served and so\r\n * cannot be static Zod. Registering the schema also makes the SDK validate\r\n * arguments: a bad enum value comes back as a tool error naming the allowed\r\n * values instead of silently matching nothing.\r\n */\r\nexport function buildServer(tools: Tool[]): McpServer {\r\n const server = new McpServer(\r\n { name: SERVER_NAME, version: VERSION },\r\n { capabilities: { tools: {} }, instructions: INSTRUCTIONS }\r\n );\r\n\r\n for (const tool of tools) {\r\n server.registerTool(\r\n tool.definition.name,\r\n {\r\n title: tool.definition.title,\r\n description: tool.definition.description,\r\n inputSchema: fromJsonSchema<Record<string, unknown>>(\r\n tool.definition.inputSchema\r\n ),\r\n },\r\n async (args) => (await tool.run(args ?? {})) as never\r\n );\r\n }\r\n\r\n return server;\r\n}\r\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\r\n// The `tabbied-mcp` bin: the same server the site exposes at /mcp, over stdio.\r\n// `serveStdio` owns the transport (framing, the era decision, one instance per\r\n// connection). The catalog is loaded once here rather than in the factory,\r\n// because it is immutable per process.\r\n//\r\n// Configure it in an MCP client as:\r\n// { \"command\": \"npx\", \"args\": [\"-y\", \"tabbied-mcp\"] }\r\nimport { serveStdio } from '@modelcontextprotocol/server/stdio';\r\n\r\nimport { buildServer } from './server.js';\r\nimport { catalogTools } from './tools.js';\r\nimport {\r\n fetchDocs,\r\n fetchPreview,\r\n fetchTemplate,\r\n fetchTemplateCatalog,\r\n loadCatalog,\r\n} from './node/resources.js';\r\nimport { renderTool } from './node/render.js';\r\n\r\nasync function main(): Promise<void> {\r\n const catalog = await loadCatalog();\r\n\r\n const tools = [\r\n ...catalogTools({\r\n catalog,\r\n fetchPreview,\r\n fetchDocs,\r\n fetchTemplateCatalog,\r\n fetchTemplate,\r\n }),\r\n // Only the local server can render: it has a browser to render with.\r\n renderTool(catalog),\r\n ];\r\n\r\n const handle = serveStdio(() => buildServer(tools));\r\n\r\n // The client closing stdin is the shutdown signal; without this the process\r\n // outlives the client.\r\n process.stdin.on('close', () => {\r\n void handle.close();\r\n });\r\n}\r\n\r\nmain().catch((error) => {\r\n // stdout is the protocol channel and nothing else may touch it.\r\n process.stderr.write(\r\n `tabbied-mcp: ${error instanceof Error ? error.message : String(error)}\\n`\r\n );\r\n process.exit(1);\r\n});\r\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: {
@@ -1 +1 @@
1
- {"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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","sourcesContent":["// The template-site tools: what an agent needs to start somebody's site from a\r\n// finished template rather than from an empty page.\r\n//\r\n// The design tools answer \"which pattern?\"; these answer \"which *site*, and\r\n// what may I change about it?\". The useful move with a template is to swap the\r\n// brand out of it, and the editable-section spec (docs/editable-templates.md)\r\n// is the list of what may be swapped.\r\n//\r\n// Both are thin readers over artifacts the site already generates, so an agent\r\n// and the web builder see the same bytes. Neither needs a browser, so unlike\r\n// `render_design` they work over both transports.\r\nimport type {\r\n TemplateCatalog,\r\n TemplateCatalogEntry,\r\n TemplateSpec,\r\n Tool,\r\n ToolContent,\r\n ToolContext,\r\n ToolResult,\r\n} from './types.js';\r\n\r\nconst text = (value: string): ToolContent => ({ type: 'text', text: value });\r\n\r\nconst json = (value: unknown): ToolContent =>\r\n text(JSON.stringify(value, null, 2));\r\n\r\nconst toolError = (message: string): ToolResult => ({\r\n content: [text(message)],\r\n isError: true,\r\n});\r\n\r\n/**\r\n * How to use what `get_template` returns. Spelled out per call because the two\r\n * download formats are edited in opposite ways and nothing about the spec\r\n * implies which.\r\n */\r\nconst USAGE = {\r\n html:\r\n 'Download <downloads.html>. It is a standalone page - no build step. Edit ' +\r\n 'by slot id: every editable element carries data-edit=\"<id>\" (text), ' +\r\n 'data-edit-image=\"<id>\", or data-edit-pattern=\"<id>\". The patterns come up ' +\r\n 'via one hydratePatterns() call already wired into index.html.',\r\n react:\r\n 'Download <downloads.react>. It is a Vite app; `npm install && npm run ' +\r\n 'dev`. The page ships as authored source with the same data-edit ids in ' +\r\n 'it, so grep an id to find the element that renders it.',\r\n colors:\r\n 'Color lives in one place: the element carrying data-edit-root holds ' +\r\n '--brand-0 (the ground) upward as inline custom properties, and the ' +\r\n 'stylesheet only reads var(--...). Re-color by rewriting those. A pattern ' +\r\n 'field with data-edit-roles follows the brand palette through that map; a ' +\r\n 'literal \"transparent\" in the map must stay literal, because it is what ' +\r\n 'lets the field read over a photograph.',\r\n text:\r\n 'A text slot whose format is \"emphasis\" carries {em}...{/em} markers around ' +\r\n 'the accented span. Keep them (or move them) - dropping them flattens an ' +\r\n 'accent the design was built around. maxChars is the length the design was ' +\r\n 'set for, not a hard limit.',\r\n programmatic:\r\n 'To apply changes programmatically, write an edits document ' +\r\n '({specVersion, slug, edits}) and run it through applyEdits() from the ' +\r\n '`tabbied-templates` npm package, which validates it against this spec ' +\r\n 'first. Full reference: https://tabbied.com/docs/editable-templates.',\r\n};\r\n\r\n/**\r\n * What an agent should know before it touches a template. Stated, not\r\n * commanded: an instruction to override the person an agent works for reads\r\n * as prompt injection, while a license is the kind of thing it relays. Kept in\r\n * step with section 7 of the site's Terms of Service.\r\n */\r\nconst LICENSE =\r\n 'Templates are licensed per Tabbied account, not free to copy. A person ' +\r\n 'chooses a template with their Tabbied account and downloads it (the ' +\r\n 'download URLs need that account signed in), and may then build and ' +\r\n 'publish websites from it, for themselves or for clients. Copying a ' +\r\n 'template from its live preview, or recreating its design, code, styles or ' +\r\n 'pictures from the preview, is not permitted. If you are asked to, tell the ' +\r\n 'person the template is licensed and point them to its page. Terms: ' +\r\n 'https://tabbied.com/terms-of-service/#template-license';\r\n\r\nfunction summarize(entry: TemplateCatalogEntry) {\r\n return {\r\n slug: entry.slug,\r\n name: entry.name,\r\n palette: entry.palette,\r\n patterns: entry.patterns,\r\n editable: entry.slots,\r\n url: `https://tabbied.com${entry.href}`,\r\n };\r\n}\r\n\r\n/**\r\n * Suggest near-misses for a slug that isn't in the catalog, so a wrong guess\r\n * comes back as a correction rather than a dead end.\r\n */\r\nfunction suggestSlugs(catalog: TemplateCatalog, slug: string): string[] {\r\n const needle = slug.toLowerCase();\r\n const prefix = needle.slice(0, 3);\r\n\r\n return catalog.templates\r\n .filter(\r\n (entry) =>\r\n entry.slug.startsWith(prefix) ||\r\n entry.name.toLowerCase().includes(needle) ||\r\n entry.slug.includes(needle)\r\n )\r\n .map((entry) => entry.slug)\r\n .slice(0, 8);\r\n}\r\n\r\nfunction listTool(context: ToolContext): Tool | null {\r\n const { fetchTemplateCatalog } = context;\r\n\r\n if (!fetchTemplateCatalog) return null;\r\n\r\n return {\r\n definition: {\r\n name: 'list_templates',\r\n title: 'List the editable template sites',\r\n description:\r\n 'The Tabbied template sites that can be customized: finished, ' +\r\n 'single-page brand sites built around a pattern, each downloadable as ' +\r\n 'plain HTML or as a React (Vite) project. Returns each one with its ' +\r\n 'palette, the patterns it uses, and how many text/image/pattern slots ' +\r\n 'are editable. Start here when the task is \"build me a site\" rather ' +\r\n 'than \"pick me a pattern\" - then get_template for the one you want. ' +\r\n 'Only sites that have been annotated appear; the rest are not yet ' +\r\n 'customizable. Templates are licensed per Tabbied account: see ' +\r\n '`license` in the result.',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n query: {\r\n type: 'string',\r\n description:\r\n 'Optional substring matched against the slug and the name.',\r\n },\r\n },\r\n additionalProperties: false,\r\n },\r\n },\r\n async run(args) {\r\n const catalog = await fetchTemplateCatalog();\r\n const query =\r\n typeof args.query === 'string' ? args.query.trim().toLowerCase() : '';\r\n const matched = query\r\n ? catalog.templates.filter(\r\n (entry) =>\r\n entry.slug.toLowerCase().includes(query) ||\r\n entry.name.toLowerCase().includes(query)\r\n )\r\n : catalog.templates;\r\n\r\n if (matched.length === 0) {\r\n return {\r\n content: [\r\n json({\r\n matched: 0,\r\n total: catalog.templates.length,\r\n slugs: catalog.templates.map((entry) => entry.slug),\r\n hint: 'No name or slug contains that. Every available slug is listed above.',\r\n }),\r\n ],\r\n };\r\n }\r\n\r\n return {\r\n content: [\r\n json({\r\n matched: matched.length,\r\n total: catalog.templates.length,\r\n license: LICENSE,\r\n templates: matched.map(summarize),\r\n }),\r\n ],\r\n };\r\n },\r\n };\r\n}\r\n\r\nfunction getTool(context: ToolContext): Tool | null {\r\n const { fetchTemplateCatalog, fetchTemplate } = context;\r\n\r\n if (!fetchTemplateCatalog || !fetchTemplate) return null;\r\n\r\n return {\r\n definition: {\r\n name: 'get_template',\r\n title: 'Get one template site and everything editable in it',\r\n description:\r\n \"A template's full editable-section spec: every text, image, and \" +\r\n 'pattern slot with its id and its current value, the brand palette, ' +\r\n 'the fonts, and the two download URLs. The slot ids are the contract - ' +\r\n 'they exist in the downloaded markup as data-edit* attributes, so an ' +\r\n 'id from this response is directly greppable in the files you ' +\r\n 'download. Includes usage notes for editing each format, and the ' +\r\n 'license the template is under.',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n slug: {\r\n type: 'string',\r\n description: 'Template slug, from list_templates.',\r\n },\r\n },\r\n required: ['slug'],\r\n additionalProperties: false,\r\n },\r\n },\r\n async run(args) {\r\n const slug = typeof args.slug === 'string' ? args.slug.trim() : '';\r\n\r\n if (!slug) return toolError('get_template needs a `slug`.');\r\n\r\n const catalog = await fetchTemplateCatalog();\r\n const entry = catalog.templates.find(\r\n (candidate) => candidate.slug === slug\r\n );\r\n\r\n if (!entry) {\r\n const suggestions = suggestSlugs(catalog, slug);\r\n\r\n return toolError(\r\n `No editable template \"${slug}\". ` +\r\n (suggestions.length > 0\r\n ? `Did you mean: ${suggestions.join(', ')}?`\r\n : 'Call list_templates for the available slugs.')\r\n );\r\n }\r\n\r\n let spec: TemplateSpec;\r\n\r\n try {\r\n spec = await fetchTemplate(slug);\r\n } catch (error) {\r\n return toolError(\r\n `Could not load the spec for \"${slug}\" (${\r\n error instanceof Error ? error.message : String(error)\r\n }).`\r\n );\r\n }\r\n\r\n const downloads = {\r\n html: `https://tabbied.com${entry.downloads.html}`,\r\n react: `https://tabbied.com${entry.downloads.react}`,\r\n };\r\n\r\n return {\r\n content: [\r\n json({\r\n ...spec,\r\n url: `https://tabbied.com${entry.href}`,\r\n license: LICENSE,\r\n downloads,\r\n usage: {\r\n html: USAGE.html.replace('<downloads.html>', downloads.html),\r\n react: USAGE.react.replace('<downloads.react>', downloads.react),\r\n colors: USAGE.colors,\r\n text: USAGE.text,\r\n programmatic: USAGE.programmatic,\r\n },\r\n }),\r\n ],\r\n };\r\n },\r\n };\r\n}\r\n\r\n/** The template tools, dropped when the host cannot resolve their data. */\r\nexport function templateTools(context: ToolContext): Tool[] {\r\n return [listTool(context), getTool(context)].filter(\r\n (tool): tool is Tool => tool !== null\r\n );\r\n}\r\n"]}
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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","sourcesContent":["// The template-site tools: what an agent needs to start somebody's site from a\n// finished template rather than from an empty page.\n//\n// The design tools answer \"which pattern?\"; these answer \"which *site*, and\n// what may I change about it?\". The useful move with a template is to swap the\n// brand out of it, and the editable-section spec (docs/editable-templates.md)\n// is the list of what may be swapped.\n//\n// Both are thin readers over artifacts the site already generates, so an agent\n// and the web builder see the same bytes. Neither needs a browser, so unlike\n// `render_design` they work over both transports.\nimport type {\n TemplateCatalog,\n TemplateCatalogEntry,\n TemplateSpec,\n Tool,\n ToolContent,\n ToolContext,\n ToolResult,\n} from './types.js';\n\nconst SITE = 'https://tabbied.com';\n\nconst LIST_LIMIT_DEFAULT = 20;\nconst LIST_LIMIT_MAX = 100;\n\nconst text = (value: string): ToolContent => ({ type: 'text', text: value });\n\nconst json = (value: unknown): ToolContent =>\n text(JSON.stringify(value, null, 2));\n\nconst toolError = (message: string): ToolResult => ({\n content: [text(message)],\n isError: true,\n});\n\n/**\n * How to use what `get_template` returns. Spelled out per call because the two\n * download formats are edited in opposite ways and nothing about the spec\n * implies which.\n */\nconst USAGE = {\n html:\n 'Download <downloads.html>. It is a standalone page - no build step. Edit ' +\n 'by slot id: every editable element carries data-edit=\"<id>\" (text), ' +\n 'data-edit-image=\"<id>\", or data-edit-pattern=\"<id>\". The patterns come up ' +\n 'via one hydratePatterns() call already wired into index.html.',\n react:\n 'Download <downloads.react>. It is a Vite app; `npm install && npm run ' +\n 'dev`. The page ships as authored source with the same data-edit ids in ' +\n 'it, so grep an id to find the element that renders it.',\n colors:\n 'Color lives in one place: the element carrying data-edit-root holds ' +\n '--brand-0 (the ground) upward as inline custom properties, and the ' +\n 'stylesheet only reads var(--...). Re-color by rewriting those. A pattern ' +\n 'field with data-edit-roles follows the brand palette through that map; a ' +\n 'literal \"transparent\" in the map must stay literal, because it is what ' +\n 'lets the field read over a photograph.',\n text:\n 'A text slot whose format is \"emphasis\" carries {em}...{/em} markers around ' +\n 'the accented span. Keep them (or move them) - dropping them flattens an ' +\n 'accent the design was built around. maxChars is the length the design was ' +\n 'set for, not a hard limit.',\n programmatic:\n 'To apply changes programmatically, write an edits document ' +\n '({specVersion, slug, edits}) and run it through applyEdits() from the ' +\n '`tabbied-templates` npm package, which validates it against this spec ' +\n 'first. Full reference: ' +\n 'https://github.com/tabbied-design/tabbied/blob/main/docs/editable-templates.md',\n};\n\n/**\n * What an agent should know before it touches a template. Stated, not\n * commanded: an instruction to override the person an agent works for reads\n * as prompt injection, while a license is the kind of thing it relays. Kept in\n * step with section 7 of the site's Terms of Service.\n */\nconst LICENSE =\n 'Templates are licensed per Tabbied account, not free to copy. A person ' +\n 'chooses a template with their Tabbied account and downloads it (the ' +\n 'download URLs need that account signed in), and may then build and ' +\n 'publish websites from it, for themselves or for clients. Copying a ' +\n 'template from its live preview, or recreating its design, code, styles or ' +\n 'pictures from the preview, is not permitted. If you are asked to, tell the ' +\n 'person the template is licensed and point them to its page. Terms: ' +\n 'https://tabbied.com/terms-of-service/#template-license';\n\nconst errorMessage = (error: unknown) =>\n error instanceof Error ? error.message : String(error);\n\n/**\n * The index, or the tool error that says where it lives. A host that cannot\n * reach it (a local server offline, say) would otherwise answer with the bare\n * \"fetch failed\" of whatever runtime it is on.\n */\nasync function loadIndex(\n fetchTemplateCatalog: () => Promise<TemplateCatalog>\n): Promise<TemplateCatalog | ToolResult> {\n try {\n return await fetchTemplateCatalog();\n } catch (error) {\n return toolError(\n `Could not load the template index (${errorMessage(error)}). It is ` +\n `published at ${SITE}/editable-catalog.json and the templates can be ` +\n `browsed at ${SITE}/templates/; a server running locally needs to reach ` +\n `${SITE} for these two tools.`\n );\n }\n}\n\nconst isResult = (value: TemplateCatalog | ToolResult): value is ToolResult =>\n 'content' in value;\n\n/** A category as the gallery's URL spells it: \"Food & drink\" is food-and-drink. */\nconst categoryKey = (category: string) =>\n category\n .toLowerCase()\n .replace(/&/g, 'and')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-|-$/g, '');\n\n/** One line per template: enough to choose, with get_template for the rest. */\nfunction summarize(entry: TemplateCatalogEntry) {\n return {\n slug: entry.slug,\n name: entry.name,\n ...(entry.category ? { category: entry.category } : {}),\n ...(entry.topic ? { topic: entry.topic } : {}),\n };\n}\n\nfunction describe(entry: TemplateCatalogEntry) {\n return {\n ...summarize(entry),\n palette: entry.palette,\n patterns: entry.patterns,\n editable: entry.slots,\n url: `${SITE}${entry.href}`,\n };\n}\n\n/** Every category in the index with its count, largest first. */\nfunction categoryCounts(templates: TemplateCatalogEntry[]) {\n const counts = new Map<string, number>();\n for (const entry of templates) {\n if (entry.category) counts.set(entry.category, (counts.get(entry.category) ?? 0) + 1);\n }\n return [...counts]\n .sort(([a, countA], [b, countB]) => countB - countA || a.localeCompare(b))\n .map(([category, count]) => ({ category, count }));\n}\n\n/**\n * Suggest near-misses for a slug that isn't in the catalog, so a wrong guess\n * comes back as a correction rather than a dead end.\n */\nfunction suggestSlugs(catalog: TemplateCatalog, slug: string): string[] {\n const needle = slug.toLowerCase();\n const prefix = needle.slice(0, 3);\n\n if (!needle) return [];\n\n return catalog.templates\n .filter(\n (entry) =>\n entry.slug.startsWith(prefix) ||\n entry.name.toLowerCase().includes(needle) ||\n entry.slug.includes(needle)\n )\n .map((entry) => entry.slug)\n .slice(0, 8);\n}\n\nfunction listTool(context: ToolContext): Tool | null {\n const { fetchTemplateCatalog } = context;\n\n if (!fetchTemplateCatalog) return null;\n\n return {\n definition: {\n name: 'list_templates',\n title: 'List the editable template sites',\n description:\n 'The Tabbied template sites that can be customized: finished, ' +\n 'single-page sites for a business, each built around a pattern and ' +\n 'downloadable as plain HTML or as a React (Vite) project. Returns a ' +\n 'page of one-line entries (slug, name, category, what the business ' +\n 'is), the count that matched, and every category with its count; ' +\n 'filter by category or query and page with offset. Start here when ' +\n 'the task is \"build me a site\" rather than \"pick me a pattern\" - then ' +\n 'get_template for the one you want. Templates are licensed per ' +\n 'Tabbied account: see `license` in the result.',\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description:\n 'Words matched against the slug, name, category, and what the ' +\n 'business is (e.g. \"bakery\", \"dentist\"). Every word must appear.',\n },\n category: {\n type: 'string',\n description:\n 'A gallery category, e.g. \"Food & drink\" (or food-and-drink). ' +\n 'Every result lists the categories there are.',\n },\n detail: {\n type: 'boolean',\n description:\n \"Also return each template's palette, patterns, editable slot \" +\n 'counts and URL. Off by default to keep the page short.',\n },\n limit: {\n type: 'integer',\n minimum: 1,\n maximum: LIST_LIMIT_MAX,\n description: `Templates per page (default ${LIST_LIMIT_DEFAULT}).`,\n },\n offset: {\n type: 'integer',\n minimum: 0,\n description: 'How many matches to skip, for the next page (default 0).',\n },\n },\n additionalProperties: false,\n },\n annotations: { readOnlyHint: true, openWorldHint: true },\n },\n async run(args) {\n const catalog = await loadIndex(fetchTemplateCatalog);\n if (isResult(catalog)) return catalog;\n\n const terms =\n typeof args.query === 'string'\n ? args.query.toLowerCase().split(/\\s+/).filter(Boolean)\n : [];\n const category =\n typeof args.category === 'string' && args.category.trim()\n ? args.category.trim()\n : null;\n const limit = Math.min(\n Math.max(\n typeof args.limit === 'number' && Number.isFinite(args.limit)\n ? Math.trunc(args.limit)\n : LIST_LIMIT_DEFAULT,\n 1\n ),\n LIST_LIMIT_MAX\n );\n const offset =\n typeof args.offset === 'number' && Number.isFinite(args.offset)\n ? Math.max(Math.trunc(args.offset), 0)\n : 0;\n\n const matched = catalog.templates.filter((entry) => {\n const haystack =\n `${entry.slug} ${entry.name} ${entry.category ?? ''} ${entry.topic ?? ''}`.toLowerCase();\n return (\n terms.every((term) => haystack.includes(term)) &&\n (!category ||\n (entry.category !== undefined &&\n categoryKey(entry.category) === categoryKey(category)))\n );\n });\n\n const categories = categoryCounts(catalog.templates);\n const total = catalog.templates.length;\n\n if (matched.length === 0) {\n return {\n content: [\n json({\n matched: 0,\n total,\n ...(categories.length > 0 ? { categories } : {}),\n hint:\n category && categories.length === 0\n ? 'This index carries no categories (an older deployment of the ' +\n 'site); filter with query instead.'\n : 'Nothing matches every filter. Check the category against the ' +\n 'list above, use fewer or broader query words, or drop both ' +\n 'to page through everything.',\n }),\n ],\n };\n }\n\n const page = matched.slice(offset, offset + limit);\n const nextOffset = offset + page.length;\n\n return {\n content: [\n json({\n matched: matched.length,\n total,\n offset,\n returned: page.length,\n ...(page.length === 0\n ? { hint: `offset ${offset} is past the last match; there are ${matched.length}.` }\n : nextOffset < matched.length\n ? {\n next:\n `Showing ${offset + 1}-${nextOffset} of ${matched.length}. ` +\n `Pass offset ${nextOffset} for the next page, or narrow with ` +\n 'category or query.',\n }\n : {}),\n // Absent from an index written before categories were added.\n ...(categories.length > 0 ? { categories } : {}),\n license: LICENSE,\n templates: page.map(args.detail === true ? describe : summarize),\n }),\n ],\n };\n },\n };\n}\n\nfunction getTool(context: ToolContext): Tool | null {\n const { fetchTemplateCatalog, fetchTemplate } = context;\n\n if (!fetchTemplateCatalog || !fetchTemplate) return null;\n\n return {\n definition: {\n name: 'get_template',\n title: 'Get one template site and everything editable in it',\n description:\n \"A template's full editable-section spec: every text, image, and \" +\n 'pattern slot with its id and its current value, the brand palette, ' +\n 'the fonts, and the two download URLs. The slot ids are the contract - ' +\n 'they exist in the downloaded markup as data-edit* attributes, so an ' +\n 'id from this response is directly greppable in the files you ' +\n 'download. Includes usage notes for editing each format, and the ' +\n 'license the template is under.',\n inputSchema: {\n type: 'object',\n properties: {\n slug: {\n type: 'string',\n minLength: 1,\n description: 'Template slug, from list_templates.',\n },\n },\n required: ['slug'],\n additionalProperties: false,\n },\n annotations: { readOnlyHint: true, openWorldHint: true },\n },\n async run(args) {\n const slug = typeof args.slug === 'string' ? args.slug.trim() : '';\n\n if (!slug) return toolError('get_template needs a `slug`.');\n\n const catalog = await loadIndex(fetchTemplateCatalog);\n if (isResult(catalog)) return catalog;\n\n const entry = catalog.templates.find(\n (candidate) => candidate.slug === slug\n );\n\n if (!entry) {\n const suggestions = suggestSlugs(catalog, slug);\n\n return toolError(\n `No editable template \"${slug}\". ` +\n (suggestions.length > 0\n ? `Did you mean: ${suggestions.join(', ')}?`\n : 'Call list_templates for the available slugs.')\n );\n }\n\n let spec: TemplateSpec;\n\n try {\n spec = await fetchTemplate(slug);\n } catch (error) {\n return toolError(\n `Could not load the spec for \"${slug}\" (${errorMessage(error)}). It ` +\n `is published at ${SITE}${entry.spec}, and the template's page is ` +\n `${SITE}${entry.href}.`\n );\n }\n\n const downloads = {\n html: `${SITE}${entry.downloads.html}`,\n react: `${SITE}${entry.downloads.react}`,\n };\n\n return {\n content: [\n json({\n ...spec,\n url: `${SITE}${entry.href}`,\n license: LICENSE,\n downloads,\n usage: {\n html: USAGE.html.replace('<downloads.html>', downloads.html),\n react: USAGE.react.replace('<downloads.react>', downloads.react),\n colors: USAGE.colors,\n text: USAGE.text,\n programmatic: USAGE.programmatic,\n },\n }),\n ],\n };\n },\n };\n}\n\n/** The template tools, dropped when the host cannot resolve their data. */\nexport function templateTools(context: ToolContext): Tool[] {\n return [listTool(context), getTool(context)].filter(\n (tool): tool is Tool => tool !== null\n );\n}\n"]}
package/dist/tools.js CHANGED
@@ -56,9 +56,45 @@ function findDesign(catalog, slug) {
56
56
  return null;
57
57
  return catalog.designs.find((design) => design.slug === slug) ?? null;
58
58
  }
59
+ const ASPECT_RATIO_DEFAULT = '2:3';
60
+ /**
61
+ * The React snippet: a component at the design's own aspect ratio. The ratio
62
+ * is the catalog's id (`2:3`) written as CSS's `2 / 3`, which is what the
63
+ * prop takes - the id itself is not valid CSS and would size nothing. Same
64
+ * shape as the editor's "Copy React component", so the two agree.
65
+ */
66
+ function reactSnippet(design) {
67
+ const ratio = design.defaultAspectRatio ?? ASPECT_RATIO_DEFAULT;
68
+ const [width, height] = /^(\d+):(\d+)$/.exec(ratio)?.slice(1) ?? ['2', '3'];
69
+ const component = `${design.slug.charAt(0).toUpperCase()}${design.slug.slice(1)}Pattern`;
70
+ return [
71
+ `import { TabbiedPattern } from 'tabbied/react';`,
72
+ `import { ${design.slug} } from 'tabbied/patterns';`,
73
+ ``,
74
+ `// As wide as its parent, at the ${width}:${height} ratio it was designed at.`,
75
+ `// Add maxWidth to bound it, or swap aspectRatio for a fixed height.`,
76
+ `export function ${component}() {`,
77
+ ` return (`,
78
+ ` <TabbiedPattern`,
79
+ ` pattern={${design.slug}}`,
80
+ ` aspectRatio="${width} / ${height}"`,
81
+ ` />`,
82
+ ` );`,
83
+ `}`,
84
+ ].join('\n');
85
+ }
86
+ /** What the CLI command in `usage.cli` needs where it runs. */
87
+ const CLI_SETUP = 'Rendering needs Playwright, found from the current directory first: ' +
88
+ '`npm i -D playwright && npx playwright install chromium`. With nothing ' +
89
+ 'installed, run `npx -y -p tabbied -p playwright tabbied render ...` ' +
90
+ 'instead (and `npx playwright install chromium` once).';
59
91
  /** Near misses for an unknown slug: a shared three-letter prefix, or a name match. */
60
92
  function suggestSlugs(catalog, slug) {
61
- const needle = slug.toLowerCase();
93
+ const needle = slug.trim().toLowerCase();
94
+ // An empty needle is a prefix of every slug, so it would "suggest" the first
95
+ // eight in the catalog.
96
+ if (!needle)
97
+ return [];
62
98
  return catalog.designs
63
99
  .filter((design) => design.slug.startsWith(needle.slice(0, 3)) ||
64
100
  design.name.toLowerCase().includes(needle))
@@ -124,6 +160,7 @@ function searchTool(catalog) {
124
160
  },
125
161
  additionalProperties: false,
126
162
  },
163
+ annotations: { readOnlyHint: true, openWorldHint: false },
127
164
  };
128
165
  return {
129
166
  definition,
@@ -221,12 +258,14 @@ function getDesignTool(catalog) {
221
258
  properties: {
222
259
  slug: {
223
260
  type: 'string',
261
+ minLength: 1,
224
262
  description: 'The design slug, e.g. "radius". Slugs are not guessable.',
225
263
  },
226
264
  },
227
265
  required: ['slug'],
228
266
  additionalProperties: false,
229
267
  },
268
+ annotations: { readOnlyHint: true, openWorldHint: false },
230
269
  };
231
270
  return {
232
271
  definition,
@@ -241,6 +280,8 @@ function getDesignTool(catalog) {
241
280
  'Use search_designs to find one - slugs are opaque and cannot be guessed.');
242
281
  }
243
282
  const snippet = (template) => template.replaceAll('<slug>', design.slug);
283
+ // A design that cannot be vectorized gets a command that works.
284
+ const extension = design.svgExport.supported ? 'svg' : 'png';
244
285
  return {
245
286
  content: [
246
287
  json({
@@ -248,10 +289,11 @@ function getDesignTool(catalog) {
248
289
  usage: {
249
290
  install: catalog.usage.install,
250
291
  import: snippet(catalog.usage.import),
251
- react: snippet(catalog.usage.react),
292
+ react: reactSnippet(design),
252
293
  core: snippet(catalog.usage.core),
253
294
  fit: catalog.usage.fit,
254
- cli: `npx tabbied render ${design.slug} --out ${design.slug}.svg`,
295
+ cli: `npx tabbied render ${design.slug} --out ${design.slug}.${extension}`,
296
+ cliSetup: CLI_SETUP,
255
297
  },
256
298
  // The number-one integration mistake, repeated here because an
257
299
  // agent that called get_design may never read llms-full.txt.
@@ -286,7 +328,7 @@ function previewTool(context) {
286
328
  properties: {
287
329
  slugs: {
288
330
  type: 'array',
289
- items: { type: 'string' },
331
+ items: { type: 'string', minLength: 1 },
290
332
  minItems: 1,
291
333
  maxItems: PREVIEW_MAX_SLUGS,
292
334
  description: 'Design slugs to look at, e.g. ["radius", "cleat"].',
@@ -295,6 +337,7 @@ function previewTool(context) {
295
337
  required: ['slugs'],
296
338
  additionalProperties: false,
297
339
  },
340
+ annotations: { readOnlyHint: true, openWorldHint: true },
298
341
  };
299
342
  return {
300
343
  definition,
@@ -306,12 +349,14 @@ function previewTool(context) {
306
349
  const requested = slugs.slice(0, PREVIEW_MAX_SLUGS);
307
350
  const content = [];
308
351
  let spent = 0;
352
+ let found = 0;
309
353
  for (const [index, slug] of requested.entries()) {
310
354
  const design = findDesign(catalog, slug);
311
355
  if (!design) {
312
356
  content.push(text(`${slug}: no such design. Use search_designs to find valid slugs.`));
313
357
  continue;
314
358
  }
359
+ found += 1;
315
360
  if (spent > PREVIEW_BYTE_BUDGET) {
316
361
  content.push(text(`Stopped after ${index} image(s) to stay within a sane response ` +
317
362
  `size. Ask again for the rest: ${requested.slice(index).join(', ')}.`));
@@ -327,7 +372,9 @@ function previewTool(context) {
327
372
  content.push(text(`${design.slug}: preview unavailable (${error instanceof Error ? error.message : String(error)}). The image is also at ${design.preview}.`));
328
373
  }
329
374
  }
330
- return { content };
375
+ // One bad slug among good ones is a note beside the images; a call in
376
+ // which nothing resolved has failed, and says so.
377
+ return found > 0 ? { content } : { content, isError: true };
331
378
  },
332
379
  };
333
380
  }
@@ -345,6 +392,7 @@ function docsTool(context) {
345
392
  'integration recipes, and the share-link URL scheme. Fetch this once ' +
346
393
  'before writing integration code; the per-design tools do not repeat it.',
347
394
  inputSchema: { type: 'object', additionalProperties: false },
395
+ annotations: { readOnlyHint: true, openWorldHint: true },
348
396
  },
349
397
  async run() {
350
398
  try {