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.
@@ -16,11 +16,49 @@ const RENDER_TIMEOUT_MS = 180_000;
16
16
  // A PNG returned inline is spent context. Past this the file stays on disk and
17
17
  // the agent gets the path - better a usable pointer than a truncated image.
18
18
  const INLINE_BYTE_BUDGET = 1_500_000;
19
+ // Past these a PNG is tens of megapixels (or a browser canvas limit), which
20
+ // no asset this tool exists for needs.
21
+ const SIZE_MAX = 4096;
22
+ const SCALE_MAX = 4;
19
23
  const text = (value) => ({ type: 'text', text: value });
20
24
  const toolError = (message) => ({
21
25
  content: [text(message)],
22
26
  isError: true,
23
27
  });
28
+ /** A whole number in [min, max], the default when absent, or null when invalid. */
29
+ function integerArg(value, fallback, min, max) {
30
+ if (value === undefined)
31
+ return fallback;
32
+ return typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max
33
+ ? value
34
+ : null;
35
+ }
36
+ /**
37
+ * The CLI's own message out of its stderr: each `tabbied:` line and the
38
+ * indented lines that continue it. Anything else there (Node's warnings, a
39
+ * deprecation notice from a dependency) is noise to the agent. When there is
40
+ * no such line, the CLI died some other way, and the tail is the best guess.
41
+ */
42
+ function cliMessage(stderr) {
43
+ const lines = stderr.split('\n');
44
+ const kept = [];
45
+ let continuing = false;
46
+ for (const line of lines) {
47
+ if (line.startsWith('tabbied:')) {
48
+ kept.push(line);
49
+ continuing = true;
50
+ }
51
+ else if (continuing && /^\s+\S/.test(line)) {
52
+ kept.push(line);
53
+ }
54
+ else {
55
+ continuing = false;
56
+ }
57
+ }
58
+ return kept.length > 0
59
+ ? kept.join('\n')
60
+ : lines.filter((line) => line.trim()).slice(-5).join('\n');
61
+ }
24
62
  export function renderTool(catalog) {
25
63
  return {
26
64
  definition: {
@@ -30,12 +68,14 @@ export function renderTool(catalog) {
30
68
  'palette, and option set. Use this to produce an actual asset - or to ' +
31
69
  'preview a *customized* configuration, which preview_design cannot show ' +
32
70
  'you (it only has the stock palette and defaults). Needs Playwright ' +
33
- 'installed alongside this server. Without "out" the image comes back ' +
34
- 'inline; with it, the file is written where you ask.',
71
+ 'where this server runs (start it as `npx -y -p tabbied-mcp -p ' +
72
+ 'playwright tabbied-mcp`, with `npx playwright install chromium` once). ' +
73
+ 'Without "out" the image comes back inline; with it, the file is ' +
74
+ 'written where you ask, replacing any file already there.',
35
75
  inputSchema: {
36
76
  type: 'object',
37
77
  properties: {
38
- slug: { type: 'string', description: 'The design to render.' },
78
+ slug: { type: 'string', minLength: 1, description: 'The design to render.' },
39
79
  format: {
40
80
  type: 'string',
41
81
  enum: ['svg', 'png'],
@@ -44,26 +84,40 @@ export function renderTool(catalog) {
44
84
  },
45
85
  out: {
46
86
  type: 'string',
47
- description: 'Absolute path to write to. Omit to get the image back inline ' +
48
- 'instead of on disk.',
87
+ minLength: 1,
88
+ description: 'Absolute path to write to, ending in .svg or .png to match ' +
89
+ '"format". Omit to get the image back inline instead of on disk.',
49
90
  },
50
91
  seed: {
51
92
  type: 'string',
93
+ minLength: 1,
52
94
  description: 'Fixed seed - the same seed always gives the same image. Random ' +
53
95
  'when omitted.',
54
96
  },
55
97
  palette: {
56
98
  type: 'array',
57
- items: { type: 'string' },
99
+ items: { type: 'string', minLength: 1 },
100
+ minItems: 1,
58
101
  description: "CSS colors, background first. Omit for the design's authored palette.",
59
102
  },
60
103
  options: {
61
104
  type: 'object',
62
- description: 'Option values keyed by option id, e.g. {"scale": 3}. Read the ' +
63
- 'ids and ranges off get_design.',
105
+ additionalProperties: { type: ['string', 'number', 'boolean'] },
106
+ description: 'Option values keyed by option id, e.g. {"frequency": 0.6}. Read ' +
107
+ 'the ids, ranges, and choices off get_design.',
108
+ },
109
+ width: {
110
+ type: 'integer',
111
+ minimum: 1,
112
+ maximum: SIZE_MAX,
113
+ description: 'Pixels wide (default 960).',
114
+ },
115
+ height: {
116
+ type: 'integer',
117
+ minimum: 1,
118
+ maximum: SIZE_MAX,
119
+ description: 'Pixels tall (default 960).',
64
120
  },
65
- width: { type: 'integer', description: 'Pixels wide (default 960).' },
66
- height: { type: 'integer', description: 'Pixels tall (default 960).' },
67
121
  fit: {
68
122
  type: 'string',
69
123
  enum: ['grid', 'cover', 'fixed'],
@@ -71,6 +125,8 @@ export function renderTool(catalog) {
71
125
  },
72
126
  scale: {
73
127
  type: 'integer',
128
+ minimum: 1,
129
+ maximum: SCALE_MAX,
74
130
  description: 'PNG device-scale factor. Defaults to 2 when writing a file and ' +
75
131
  '1 when returning inline, to keep the response small.',
76
132
  },
@@ -78,6 +134,9 @@ export function renderTool(catalog) {
78
134
  required: ['slug', 'format'],
79
135
  additionalProperties: false,
80
136
  },
137
+ // It writes a file, so it is not read-only; it only ever adds or
138
+ // replaces the one file it was asked for.
139
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },
81
140
  },
82
141
  async run(args) {
83
142
  const slug = typeof args.slug === 'string' ? args.slug : '';
@@ -90,118 +149,157 @@ export function renderTool(catalog) {
90
149
  return toolError(`"${slug}" paints effects SVG cannot represent, so it has no vector ` +
91
150
  'export. Render it as PNG instead.');
92
151
  }
152
+ // The schema states these, but a host that skips validation (or calls
153
+ // run() directly) must not hand the CLI a path it would resolve against
154
+ // whatever directory the server happened to start in.
155
+ const out = typeof args.out === 'string' && args.out.length > 0 ? args.out : null;
156
+ if (out !== null && !path.isAbsolute(out)) {
157
+ return toolError(`"out" must be an absolute path, got "${out}". A relative one would ` +
158
+ "land in the server's working directory, not yours.");
159
+ }
160
+ if (out !== null && path.extname(out).toLowerCase() !== `.${format}`) {
161
+ return toolError(`"out" must end in .${format} to match format "${format}", got "${out}".`);
162
+ }
163
+ const inline = out === null;
164
+ const width = integerArg(args.width, 960, 1, SIZE_MAX);
165
+ const height = integerArg(args.height, 960, 1, SIZE_MAX);
166
+ const scale = integerArg(args.scale, inline ? 1 : 2, 1, SCALE_MAX);
167
+ if (width === null || height === null) {
168
+ return toolError(`"width" and "height" must be whole pixels from 1 to ${SIZE_MAX}.`);
169
+ }
170
+ if (scale === null) {
171
+ return toolError(`"scale" must be a whole number from 1 to ${SCALE_MAX}.`);
172
+ }
173
+ // Options reach the CLI as one "id: value; id: value" string, so a `;`
174
+ // inside a value would end it and start an option of its own, past
175
+ // every check on the first. Ranges and choices are the CLI's to check.
176
+ let options = null;
177
+ if (args.options !== undefined) {
178
+ if (!args.options || typeof args.options !== 'object' || Array.isArray(args.options)) {
179
+ return toolError('"options" must be an object of option values keyed by id.');
180
+ }
181
+ const ids = design.options.map((option) => option.id);
182
+ const pairs = [];
183
+ for (const [id, value] of Object.entries(args.options)) {
184
+ if (!ids.includes(id)) {
185
+ return toolError(`"${slug}" has no option "${id}" (has: ${ids.join(', ') || 'none'}).`);
186
+ }
187
+ if (!['string', 'number', 'boolean'].includes(typeof value) || /[;\n]/.test(String(value))) {
188
+ return toolError(`Option "${id}" must be a single string, number, or boolean value ` +
189
+ '(no ";" or line breaks).');
190
+ }
191
+ pairs.push(`${id}: ${String(value)}`);
192
+ }
193
+ if (pairs.length > 0)
194
+ options = pairs.join('; ');
195
+ }
93
196
  const root = tabbiedRoot();
94
197
  if (!root) {
95
198
  return toolError('The `tabbied` package is not installed next to this server, so the ' +
96
199
  'renderer is unavailable. Run `npm install tabbied`.');
97
200
  }
98
- const inline = typeof args.out !== 'string' || args.out.length === 0;
99
201
  // An inline render gets a scratch directory, removed once its bytes
100
- // have been read back.
202
+ // have been read back, or once the render has failed.
101
203
  const scratch = inline ? await mkdtemp(path.join(tmpdir(), 'tabbied-')) : null;
102
- const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : args.out;
103
- const discard = () => (scratch ? rm(scratch, { recursive: true, force: true }) : Promise.resolve());
104
- const scale = typeof args.scale === 'number' && Number.isFinite(args.scale)
105
- ? Math.trunc(args.scale)
106
- : inline
107
- ? 1
108
- : 2;
204
+ const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : out;
205
+ let keepScratch = false;
206
+ // A value that starts with "-" (a seed of "--browser", say) would read
207
+ // as the next flag, so it goes as --flag=value, which the CLI takes
208
+ // from tabbied 0.7.1. Everything else stays two arguments, which every
209
+ // version of the CLI reads.
210
+ const flag = (name, value) => value.startsWith('-') ? [`--${name}=${value}`] : [`--${name}`, value];
109
211
  const argv = [
110
212
  path.join(root, 'dist', 'cli.js'),
111
213
  'render',
112
214
  slug,
113
- '--out',
114
- outPath,
115
- '--format',
116
- format,
117
- '--scale',
118
- String(scale),
215
+ ...flag('out', outPath),
216
+ ...flag('format', format),
217
+ ...flag('scale', String(scale)),
218
+ ...flag('size', `${width}x${height}`),
119
219
  ];
120
- const push = (flag, value) => argv.push(flag, value);
121
220
  if (typeof args.seed === 'string' && args.seed)
122
- push('--seed', args.seed);
221
+ argv.push(...flag('seed', args.seed));
123
222
  if (typeof args.fit === 'string')
124
- push('--fit', args.fit);
125
- const width = typeof args.width === 'number' ? Math.trunc(args.width) : 960;
126
- const height = typeof args.height === 'number' ? Math.trunc(args.height) : 960;
127
- push('--size', `${width}x${height}`);
128
- if (Array.isArray(args.palette) && args.palette.length) {
129
- push('--palette', args.palette.filter(Boolean).join(','));
130
- }
131
- if (args.options && typeof args.options === 'object') {
132
- const pairs = Object.entries(args.options)
133
- .map(([id, value]) => `${id}: ${String(value)}`)
134
- .join('; ');
135
- if (pairs)
136
- push('--options', pairs);
223
+ argv.push(...flag('fit', args.fit));
224
+ if (Array.isArray(args.palette)) {
225
+ const colors = args.palette.filter((color) => typeof color === 'string' && color.trim().length > 0);
226
+ if (colors.length > 0)
227
+ argv.push(...flag('palette', colors.join(',')));
137
228
  }
138
- let stderr = '';
229
+ if (options)
230
+ argv.push(...flag('options', options));
139
231
  try {
140
- // No shell: argv goes straight to node, so a palette or option value
141
- // can't be read as shell syntax.
142
- ({ stderr } = await run(process.execPath, argv, {
143
- timeout: RENDER_TIMEOUT_MS,
144
- maxBuffer: 8 * 1024 * 1024,
145
- }));
146
- }
147
- catch (error) {
148
- const detail = error && typeof error === 'object' && 'stderr' in error
149
- ? String(error.stderr).trim()
150
- : error instanceof Error
151
- ? error.message
152
- : String(error);
153
- return toolError(`Rendering failed: ${detail || 'the CLI exited non-zero.'}\n\n` +
154
- 'If this says a browser is missing, install one alongside the ' +
155
- 'server (`npm i -D playwright`) or point TABBIED_CHROMIUM at a ' +
156
- 'Chromium binary.');
157
- }
158
- // The CLI reports SVG-fidelity caveats on stderr; they matter enough to
159
- // pass through rather than swallow.
160
- const notes = stderr
161
- .split('\n')
162
- .filter((line) => line.trim().startsWith('note:'))
163
- .join('\n');
164
- if (!inline) {
165
- const { size } = await stat(outPath);
166
- return {
167
- content: [
168
- text(`Rendered ${slug} to ${outPath} (${width}x${height}` +
169
- (format === 'png' ? ` @${scale}x` : '') +
170
- `, ${Math.round(size / 1024)} KB).` +
171
- (notes ? `\n${notes}` : '')),
172
- ],
173
- };
174
- }
175
- if (format === 'svg') {
176
- const svg = await readFile(outPath, 'utf-8');
177
- await discard();
232
+ let stderr = '';
233
+ try {
234
+ // No shell: argv goes straight to node, so a palette or option value
235
+ // can't be read as shell syntax.
236
+ ({ stderr } = await run(process.execPath, argv, {
237
+ timeout: RENDER_TIMEOUT_MS,
238
+ maxBuffer: 8 * 1024 * 1024,
239
+ }));
240
+ }
241
+ catch (error) {
242
+ const failure = error;
243
+ if (failure.killed) {
244
+ return toolError(`Rendering timed out after ${RENDER_TIMEOUT_MS / 1000}s. Try a smaller ` +
245
+ 'width/height or scale.');
246
+ }
247
+ const detail = typeof failure.stderr === 'string' && failure.stderr.trim()
248
+ ? cliMessage(failure.stderr)
249
+ : (failure.message ?? String(error));
250
+ return toolError(`Rendering failed:\n${detail}`);
251
+ }
252
+ // The CLI reports SVG-fidelity caveats on stderr; they matter enough to
253
+ // pass through rather than swallow.
254
+ const notes = stderr
255
+ .split('\n')
256
+ .filter((line) => line.trim().startsWith('note:'))
257
+ .join('\n');
258
+ if (!inline) {
259
+ const { size } = await stat(outPath);
260
+ return {
261
+ content: [
262
+ text(`Rendered ${slug} to ${outPath} (${width}x${height}` +
263
+ (format === 'png' ? ` @${scale}x` : '') +
264
+ `, ${Math.round(size / 1024)} KB).` +
265
+ (notes ? `\n${notes}` : '')),
266
+ ],
267
+ };
268
+ }
269
+ if (format === 'svg') {
270
+ const svg = await readFile(outPath, 'utf-8');
271
+ return {
272
+ content: [
273
+ text(`Rendered ${slug} as SVG (${width}x${height}).` +
274
+ (notes ? `\n${notes}` : '')),
275
+ text(svg),
276
+ ],
277
+ };
278
+ }
279
+ const png = await readFile(outPath);
280
+ const data = png.toString('base64');
281
+ if (data.length > INLINE_BYTE_BUDGET) {
282
+ // Kept: the answer tells the agent to read it from that path.
283
+ keepScratch = true;
284
+ return {
285
+ content: [
286
+ text(`Rendered ${slug} to ${outPath} - ${Math.round(png.byteLength / 1024)} KB, too large to return inline. Read it from that path, or ` +
287
+ 're-render smaller (lower "scale", or a smaller width/height).'),
288
+ ],
289
+ };
290
+ }
178
291
  return {
179
292
  content: [
180
- text(`Rendered ${slug} as SVG (${width}x${height}).` +
293
+ text(`Rendered ${slug} (${width}x${height} @${scale}x).` +
181
294
  (notes ? `\n${notes}` : '')),
182
- text(svg),
295
+ { type: 'image', data, mimeType: 'image/png' },
183
296
  ],
184
297
  };
185
298
  }
186
- const png = await readFile(outPath);
187
- const data = png.toString('base64');
188
- if (data.length > INLINE_BYTE_BUDGET) {
189
- // Kept: the answer tells the agent to read it from that path.
190
- return {
191
- content: [
192
- text(`Rendered ${slug} to ${outPath} - ${Math.round(png.byteLength / 1024)} KB, too large to return inline. Read it from that path, or ` +
193
- 're-render smaller (lower "scale", or a smaller width/height).'),
194
- ],
195
- };
299
+ finally {
300
+ if (scratch && !keepScratch)
301
+ await rm(scratch, { recursive: true, force: true });
196
302
  }
197
- await discard();
198
- return {
199
- content: [
200
- text(`Rendered ${slug} (${width}x${height} @${scale}x).` +
201
- (notes ? `\n${notes}` : '')),
202
- { type: 'image', data, mimeType: 'image/png' },
203
- ],
204
- };
205
303
  },
206
304
  };
207
305
  }
@@ -1 +1 @@
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.\r\n//\r\n// It shells out to the `tabbied` CLI: the only faithful renderer for a\r\n// css-doodle pattern is css-doodle in a real browser (see docs/svg-export.md),\r\n// and the CLI already owns the headless browser, the SVG converter, and the\r\n// option parsing. A Worker has no browser, so it doesn't offer the tool.\r\nimport { execFile } from 'node:child_process';\r\nimport { mkdtemp, readFile, rm, stat } from 'node:fs/promises';\r\nimport { tmpdir } from 'node:os';\r\nimport path from 'node:path';\r\nimport { promisify } from 'node:util';\r\n\r\nimport type { Catalog, Tool, ToolResult } from '../types.js';\r\nimport { tabbiedRoot } from './resources.js';\r\n\r\nconst run = promisify(execFile);\r\n\r\n// Rendering launches a browser and waits for the first arrangement to settle.\r\nconst RENDER_TIMEOUT_MS = 180_000;\r\n\r\n// A PNG returned inline is spent context. Past this the file stays on disk and\r\n// the agent gets the path - better a usable pointer than a truncated image.\r\nconst INLINE_BYTE_BUDGET = 1_500_000;\r\n\r\nconst text = (value: string) => ({ type: 'text' as const, text: value });\r\n\r\nconst toolError = (message: string): ToolResult => ({\r\n content: [text(message)],\r\n isError: true,\r\n});\r\n\r\nexport function renderTool(catalog: Catalog): Tool {\r\n return {\r\n definition: {\r\n name: 'render_design',\r\n title: 'Render a design to a file',\r\n description:\r\n 'Render a design to SVG or PNG with a real browser, at any size, seed, ' +\r\n 'palette, and option set. Use this to produce an actual asset - or to ' +\r\n 'preview a *customized* configuration, which preview_design cannot show ' +\r\n 'you (it only has the stock palette and defaults). Needs Playwright ' +\r\n 'installed alongside this server. Without \"out\" the image comes back ' +\r\n 'inline; with it, the file is written where you ask.',\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n slug: { type: 'string', description: 'The design to render.' },\r\n format: {\r\n type: 'string',\r\n enum: ['svg', 'png'],\r\n description:\r\n 'Vector or raster. A few designs cannot be vectorized - check ' +\r\n 'svgExport on the design first.',\r\n },\r\n out: {\r\n type: 'string',\r\n description:\r\n 'Absolute path to write to. Omit to get the image back inline ' +\r\n 'instead of on disk.',\r\n },\r\n seed: {\r\n type: 'string',\r\n description:\r\n 'Fixed seed - the same seed always gives the same image. Random ' +\r\n 'when omitted.',\r\n },\r\n palette: {\r\n type: 'array',\r\n items: { type: 'string' },\r\n description:\r\n \"CSS colors, background first. Omit for the design's authored palette.\",\r\n },\r\n options: {\r\n type: 'object',\r\n description:\r\n 'Option values keyed by option id, e.g. {\"scale\": 3}. Read the ' +\r\n 'ids and ranges off get_design.',\r\n },\r\n width: { type: 'integer', description: 'Pixels wide (default 960).' },\r\n height: { type: 'integer', description: 'Pixels tall (default 960).' },\r\n fit: {\r\n type: 'string',\r\n enum: ['grid', 'cover', 'fixed'],\r\n description: 'Sizing mode (default grid).',\r\n },\r\n scale: {\r\n type: 'integer',\r\n description:\r\n 'PNG device-scale factor. Defaults to 2 when writing a file and ' +\r\n '1 when returning inline, to keep the response small.',\r\n },\r\n },\r\n required: ['slug', 'format'],\r\n additionalProperties: false,\r\n },\r\n },\r\n\r\n async run(args): Promise<ToolResult> {\r\n const slug = typeof args.slug === 'string' ? args.slug : '';\r\n const design = catalog.designs.find((entry) => entry.slug === slug);\r\n if (!design) {\r\n return toolError(\r\n `No design with slug \"${slug}\". Use search_designs to find one.`\r\n );\r\n }\r\n\r\n const format = args.format === 'png' ? 'png' : 'svg';\r\n if (format === 'svg' && !design.svgExport.supported) {\r\n return toolError(\r\n `\"${slug}\" paints effects SVG cannot represent, so it has no vector ` +\r\n 'export. Render it as PNG instead.'\r\n );\r\n }\r\n\r\n const root = tabbiedRoot();\r\n if (!root) {\r\n return toolError(\r\n 'The `tabbied` package is not installed next to this server, so the ' +\r\n 'renderer is unavailable. Run `npm install tabbied`.'\r\n );\r\n }\r\n\r\n const inline = typeof args.out !== 'string' || args.out.length === 0;\r\n // An inline render gets a scratch directory, removed once its bytes\r\n // have been read back.\r\n const scratch = inline ? await mkdtemp(path.join(tmpdir(), 'tabbied-')) : null;\r\n const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : (args.out as string);\r\n const discard = () => (scratch ? rm(scratch, { recursive: true, force: true }) : Promise.resolve());\r\n\r\n const scale =\r\n typeof args.scale === 'number' && Number.isFinite(args.scale)\r\n ? Math.trunc(args.scale)\r\n : inline\r\n ? 1\r\n : 2;\r\n\r\n const argv = [\r\n path.join(root, 'dist', 'cli.js'),\r\n 'render',\r\n slug,\r\n '--out',\r\n outPath,\r\n '--format',\r\n format,\r\n '--scale',\r\n String(scale),\r\n ];\r\n\r\n const push = (flag: string, value: string) => argv.push(flag, value);\r\n\r\n if (typeof args.seed === 'string' && args.seed) push('--seed', args.seed);\r\n if (typeof args.fit === 'string') push('--fit', args.fit);\r\n\r\n const width =\r\n typeof args.width === 'number' ? Math.trunc(args.width) : 960;\r\n const height =\r\n typeof args.height === 'number' ? Math.trunc(args.height) : 960;\r\n push('--size', `${width}x${height}`);\r\n\r\n if (Array.isArray(args.palette) && args.palette.length) {\r\n push('--palette', args.palette.filter(Boolean).join(','));\r\n }\r\n\r\n if (args.options && typeof args.options === 'object') {\r\n const pairs = Object.entries(args.options as Record<string, unknown>)\r\n .map(([id, value]) => `${id}: ${String(value)}`)\r\n .join('; ');\r\n if (pairs) push('--options', pairs);\r\n }\r\n\r\n let stderr = '';\r\n try {\r\n // No shell: argv goes straight to node, so a palette or option value\r\n // can't be read as shell syntax.\r\n ({ stderr } = await run(process.execPath, argv, {\r\n timeout: RENDER_TIMEOUT_MS,\r\n maxBuffer: 8 * 1024 * 1024,\r\n }));\r\n } catch (error) {\r\n const detail =\r\n error && typeof error === 'object' && 'stderr' in error\r\n ? String((error as { stderr: unknown }).stderr).trim()\r\n : error instanceof Error\r\n ? error.message\r\n : String(error);\r\n return toolError(\r\n `Rendering failed: ${detail || 'the CLI exited non-zero.'}\\n\\n` +\r\n 'If this says a browser is missing, install one alongside the ' +\r\n 'server (`npm i -D playwright`) or point TABBIED_CHROMIUM at a ' +\r\n 'Chromium binary.'\r\n );\r\n }\r\n\r\n // The CLI reports SVG-fidelity caveats on stderr; they matter enough to\r\n // pass through rather than swallow.\r\n const notes = stderr\r\n .split('\\n')\r\n .filter((line) => line.trim().startsWith('note:'))\r\n .join('\\n');\r\n\r\n if (!inline) {\r\n const { size } = await stat(outPath);\r\n return {\r\n content: [\r\n text(\r\n `Rendered ${slug} to ${outPath} (${width}x${height}` +\r\n (format === 'png' ? ` @${scale}x` : '') +\r\n `, ${Math.round(size / 1024)} KB).` +\r\n (notes ? `\\n${notes}` : '')\r\n ),\r\n ],\r\n };\r\n }\r\n\r\n if (format === 'svg') {\r\n const svg = await readFile(outPath, 'utf-8');\r\n await discard();\r\n return {\r\n content: [\r\n text(\r\n `Rendered ${slug} as SVG (${width}x${height}).` +\r\n (notes ? `\\n${notes}` : '')\r\n ),\r\n text(svg),\r\n ],\r\n };\r\n }\r\n\r\n const png = await readFile(outPath);\r\n const data = png.toString('base64');\r\n if (data.length > INLINE_BYTE_BUDGET) {\r\n // Kept: the answer tells the agent to read it from that path.\r\n return {\r\n content: [\r\n text(\r\n `Rendered ${slug} to ${outPath} - ${Math.round(\r\n png.byteLength / 1024\r\n )} KB, too large to return inline. Read it from that path, or ` +\r\n 're-render smaller (lower \"scale\", or a smaller width/height).'\r\n ),\r\n ],\r\n };\r\n }\r\n\r\n await discard();\r\n return {\r\n content: [\r\n text(\r\n `Rendered ${slug} (${width}x${height} @${scale}x).` +\r\n (notes ? `\\n${notes}` : '')\r\n ),\r\n { type: 'image', data, mimeType: 'image/png' },\r\n ],\r\n };\r\n },\r\n };\r\n}\r\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.\r\n//\r\n// The catalog is read from the installed `tabbied` package rather than fetched,\r\n// so the tools describe exactly the version the caller will `npm install`: a\r\n// design that only exists on the site would give an import that doesn't\r\n// resolve. The network is the fallback, not the default.\r\nimport { createRequire } from 'node:module';\r\nimport { readFile } from 'node:fs/promises';\r\nimport path from 'node:path';\r\n\r\nimport type {\r\n Catalog,\r\n CatalogDesign,\r\n TemplateCatalog,\r\n TemplateSpec,\r\n} from '../types.js';\r\n\r\nconst require = createRequire(import.meta.url);\r\n\r\nconst SITE = 'https://tabbied.com';\r\n\r\n/** Root of the installed `tabbied` package, or null when it isn't resolvable. */\r\nexport function tabbiedRoot(): string | null {\r\n try {\r\n return path.dirname(require.resolve('tabbied/package.json'));\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nexport async function loadCatalog(): Promise<Catalog> {\r\n const root = tabbiedRoot();\r\n\r\n if (root) {\r\n try {\r\n return JSON.parse(\r\n await readFile(path.join(root, 'catalog.json'), 'utf-8')\r\n ) as Catalog;\r\n } catch (error) {\r\n // A resolvable package with no catalog.json means a source checkout that\r\n // hasn't been built. Say so - the network copy would silently disagree\r\n // with the local patterns the user is about to edit.\r\n const reason = error instanceof Error ? error.message : String(error);\r\n process.stderr.write(\r\n `tabbied-mcp: could not read the local catalog (${reason}); ` +\r\n `falling back to ${SITE}/catalog.json\\n`\r\n );\r\n }\r\n }\r\n\r\n const response = await fetch(`${SITE}/catalog.json`);\r\n if (!response.ok) {\r\n throw new Error(\r\n `no local catalog and ${SITE}/catalog.json returned ${response.status}. ` +\r\n 'Install the `tabbied` package (npm i tabbied) or restore network access.'\r\n );\r\n }\r\n return (await response.json()) as Catalog;\r\n}\r\n\r\n/**\r\n * Preview bytes for one design: the committed @2x renders the site serves, so\r\n * the agent sees the same image a human browsing the gallery does.\r\n */\r\nexport async function fetchPreview(\r\n design: CatalogDesign\r\n): Promise<{ data: string; mimeType: string }> {\r\n const response = await fetch(design.preview);\r\n if (!response.ok) {\r\n throw new Error(`${design.preview} returned ${response.status}`);\r\n }\r\n const buffer = Buffer.from(await response.arrayBuffer());\r\n return {\r\n data: buffer.toString('base64'),\r\n mimeType: response.headers.get('Content-Type') ?? 'image/webp',\r\n };\r\n}\r\n\r\n/**\r\n * The full reference. The package's own llms.txt is the *full* text (in\r\n * node_modules depth beats brevity), so this works offline too.\r\n */\r\nexport async function fetchDocs(): Promise<string> {\r\n const root = tabbiedRoot();\r\n if (root) {\r\n try {\r\n return await readFile(path.join(root, 'llms.txt'), 'utf-8');\r\n } catch {\r\n // fall through to the network copy\r\n }\r\n }\r\n\r\n const response = await fetch(`${SITE}/llms-full.txt`);\r\n if (!response.ok) {\r\n throw new Error(`${SITE}/llms-full.txt returned ${response.status}`);\r\n }\r\n return await response.text();\r\n}\r\n\r\n/**\r\n * The editable-template index and one site's spec. Network only, deliberately:\r\n * they are *site* artifacts generated from the static export\r\n * (docs/editable-templates.md), and the `tabbied` package does not contain them.\r\n */\r\nexport async function fetchTemplateCatalog(): Promise<TemplateCatalog> {\r\n const response = await fetch(`${SITE}/editable-catalog.json`);\r\n\r\n if (!response.ok) {\r\n throw new Error(`${SITE}/editable-catalog.json returned ${response.status}`);\r\n }\r\n\r\n return (await response.json()) as TemplateCatalog;\r\n}\r\n\r\nexport async function fetchTemplate(slug: string): Promise<TemplateSpec> {\r\n const response = await fetch(`${SITE}/editable/${slug}.json`);\r\n\r\n if (!response.ok) {\r\n throw new Error(`${SITE}/editable/${slug}.json returned ${response.status}`);\r\n }\r\n\r\n return (await response.json()) as TemplateSpec;\r\n}\r\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"]}