tabbied-mcp 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -14
- package/dist/info.d.ts +1 -1
- package/dist/info.js +1 -1
- package/dist/info.js.map +1 -1
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +7 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/render.js +197 -99
- package/dist/node/render.js.map +1 -1
- package/dist/node/resources.js +37 -23
- package/dist/node/resources.js.map +1 -1
- package/dist/server.js +9 -1
- package/dist/server.js.map +1 -1
- package/dist/stdio.js +2 -0
- package/dist/stdio.js.map +1 -1
- package/dist/templates.js +143 -27
- package/dist/templates.js.map +1 -1
- package/dist/tools.js +53 -5
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +11 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -4
package/dist/node/render.js.map
CHANGED
|
@@ -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.\n//\n// It shells out to the `tabbied` CLI: the only faithful renderer for a\n// css-doodle pattern is css-doodle in a real browser (see docs/svg-export.md),\n// and the CLI already owns the headless browser, the SVG converter, and the\n// option parsing. A Worker has no browser, so it doesn't offer the tool.\nimport { execFile } from 'node:child_process';\nimport { mkdtemp, readFile, rm, stat } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport type { Catalog, Tool, ToolResult } from '../types.js';\nimport { tabbiedRoot } from './resources.js';\n\nconst run = promisify(execFile);\n\n// Rendering launches a browser and waits for the first arrangement to settle.\nconst RENDER_TIMEOUT_MS = 180_000;\n\n// A PNG returned inline is spent context. Past this the file stays on disk and\n// the agent gets the path - better a usable pointer than a truncated image.\nconst INLINE_BYTE_BUDGET = 1_500_000;\n\nconst text = (value: string) => ({ type: 'text' as const, text: value });\n\nconst toolError = (message: string): ToolResult => ({\n content: [text(message)],\n isError: true,\n});\n\nexport function renderTool(catalog: Catalog): Tool {\n return {\n definition: {\n name: 'render_design',\n title: 'Render a design to a file',\n description:\n 'Render a design to SVG or PNG with a real browser, at any size, seed, ' +\n 'palette, and option set. Use this to produce an actual asset - or to ' +\n 'preview a *customized* configuration, which preview_design cannot show ' +\n 'you (it only has the stock palette and defaults). Needs Playwright ' +\n 'installed alongside this server. Without \"out\" the image comes back ' +\n 'inline; with it, the file is written where you ask.',\n inputSchema: {\n type: 'object',\n properties: {\n slug: { type: 'string', description: 'The design to render.' },\n format: {\n type: 'string',\n enum: ['svg', 'png'],\n description:\n 'Vector or raster. A few designs cannot be vectorized - check ' +\n 'svgExport on the design first.',\n },\n out: {\n type: 'string',\n description:\n 'Absolute path to write to. Omit to get the image back inline ' +\n 'instead of on disk.',\n },\n seed: {\n type: 'string',\n description:\n 'Fixed seed - the same seed always gives the same image. Random ' +\n 'when omitted.',\n },\n palette: {\n type: 'array',\n items: { type: 'string' },\n description:\n \"CSS colors, background first. Omit for the design's authored palette.\",\n },\n options: {\n type: 'object',\n description:\n 'Option values keyed by option id, e.g. {\"scale\": 3}. Read the ' +\n 'ids and ranges off get_design.',\n },\n width: { type: 'integer', description: 'Pixels wide (default 960).' },\n height: { type: 'integer', description: 'Pixels tall (default 960).' },\n fit: {\n type: 'string',\n enum: ['grid', 'cover', 'fixed'],\n description: 'Sizing mode (default grid).',\n },\n scale: {\n type: 'integer',\n description:\n 'PNG device-scale factor. Defaults to 2 when writing a file and ' +\n '1 when returning inline, to keep the response small.',\n },\n },\n required: ['slug', 'format'],\n additionalProperties: false,\n },\n },\n\n async run(args): Promise<ToolResult> {\n const slug = typeof args.slug === 'string' ? args.slug : '';\n const design = catalog.designs.find((entry) => entry.slug === slug);\n if (!design) {\n return toolError(\n `No design with slug \"${slug}\". Use search_designs to find one.`\n );\n }\n\n const format = args.format === 'png' ? 'png' : 'svg';\n if (format === 'svg' && !design.svgExport.supported) {\n return toolError(\n `\"${slug}\" paints effects SVG cannot represent, so it has no vector ` +\n 'export. Render it as PNG instead.'\n );\n }\n\n const root = tabbiedRoot();\n if (!root) {\n return toolError(\n 'The `tabbied` package is not installed next to this server, so the ' +\n 'renderer is unavailable. Run `npm install tabbied`.'\n );\n }\n\n const inline = typeof args.out !== 'string' || args.out.length === 0;\n // An inline render gets a scratch directory, removed once its bytes\n // have been read back.\n const scratch = inline ? await mkdtemp(path.join(tmpdir(), 'tabbied-')) : null;\n const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : (args.out as string);\n const discard = () => (scratch ? rm(scratch, { recursive: true, force: true }) : Promise.resolve());\n\n const scale =\n typeof args.scale === 'number' && Number.isFinite(args.scale)\n ? Math.trunc(args.scale)\n : inline\n ? 1\n : 2;\n\n const argv = [\n path.join(root, 'dist', 'cli.js'),\n 'render',\n slug,\n '--out',\n outPath,\n '--format',\n format,\n '--scale',\n String(scale),\n ];\n\n const push = (flag: string, value: string) => argv.push(flag, value);\n\n if (typeof args.seed === 'string' && args.seed) push('--seed', args.seed);\n if (typeof args.fit === 'string') push('--fit', args.fit);\n\n const width =\n typeof args.width === 'number' ? Math.trunc(args.width) : 960;\n const height =\n typeof args.height === 'number' ? Math.trunc(args.height) : 960;\n push('--size', `${width}x${height}`);\n\n if (Array.isArray(args.palette) && args.palette.length) {\n push('--palette', args.palette.filter(Boolean).join(','));\n }\n\n if (args.options && typeof args.options === 'object') {\n const pairs = Object.entries(args.options as Record<string, unknown>)\n .map(([id, value]) => `${id}: ${String(value)}`)\n .join('; ');\n if (pairs) push('--options', pairs);\n }\n\n let stderr = '';\n try {\n // No shell: argv goes straight to node, so a palette or option value\n // can't be read as shell syntax.\n ({ stderr } = await run(process.execPath, argv, {\n timeout: RENDER_TIMEOUT_MS,\n maxBuffer: 8 * 1024 * 1024,\n }));\n } catch (error) {\n const detail =\n error && typeof error === 'object' && 'stderr' in error\n ? String((error as { stderr: unknown }).stderr).trim()\n : error instanceof Error\n ? error.message\n : String(error);\n return toolError(\n `Rendering failed: ${detail || 'the CLI exited non-zero.'}\\n\\n` +\n 'If this says a browser is missing, install one alongside the ' +\n 'server (`npm i -D playwright`) or point TABBIED_CHROMIUM at a ' +\n 'Chromium binary.'\n );\n }\n\n // The CLI reports SVG-fidelity caveats on stderr; they matter enough to\n // pass through rather than swallow.\n const notes = stderr\n .split('\\n')\n .filter((line) => line.trim().startsWith('note:'))\n .join('\\n');\n\n if (!inline) {\n const { size } = await stat(outPath);\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} (${width}x${height}` +\n (format === 'png' ? ` @${scale}x` : '') +\n `, ${Math.round(size / 1024)} KB).` +\n (notes ? `\\n${notes}` : '')\n ),\n ],\n };\n }\n\n if (format === 'svg') {\n const svg = await readFile(outPath, 'utf-8');\n await discard();\n return {\n content: [\n text(\n `Rendered ${slug} as SVG (${width}x${height}).` +\n (notes ? `\\n${notes}` : '')\n ),\n text(svg),\n ],\n };\n }\n\n const png = await readFile(outPath);\n const data = png.toString('base64');\n if (data.length > INLINE_BYTE_BUDGET) {\n // Kept: the answer tells the agent to read it from that path.\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} - ${Math.round(\n png.byteLength / 1024\n )} KB, too large to return inline. Read it from that path, or ` +\n 're-render smaller (lower \"scale\", or a smaller width/height).'\n ),\n ],\n };\n }\n\n await discard();\n return {\n content: [\n text(\n `Rendered ${slug} (${width}x${height} @${scale}x).` +\n (notes ? `\\n${notes}` : '')\n ),\n { type: 'image', data, mimeType: 'image/png' },\n ],\n };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/node/render.ts"],"names":[],"mappings":"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","sourcesContent":["// `render_design`, the one tool that only exists over stdio.\n//\n// It shells out to the `tabbied` CLI: the only faithful renderer for a\n// css-doodle pattern is css-doodle in a real browser (see docs/svg-export.md),\n// and the CLI already owns the headless browser, the SVG converter, and the\n// option parsing. A Worker has no browser, so it doesn't offer the tool.\nimport { execFile } from 'node:child_process';\nimport { mkdtemp, readFile, rm, stat } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport path from 'node:path';\nimport { promisify } from 'node:util';\n\nimport type { Catalog, Tool, ToolResult } from '../types.js';\nimport { tabbiedRoot } from './resources.js';\n\nconst run = promisify(execFile);\n\n// Rendering launches a browser and waits for the first arrangement to settle.\nconst RENDER_TIMEOUT_MS = 180_000;\n\n// A PNG returned inline is spent context. Past this the file stays on disk and\n// the agent gets the path - better a usable pointer than a truncated image.\nconst INLINE_BYTE_BUDGET = 1_500_000;\n\n// Past these a PNG is tens of megapixels (or a browser canvas limit), which\n// no asset this tool exists for needs.\nconst SIZE_MAX = 4096;\nconst SCALE_MAX = 4;\n\nconst text = (value: string) => ({ type: 'text' as const, text: value });\n\nconst toolError = (message: string): ToolResult => ({\n content: [text(message)],\n isError: true,\n});\n\n/** A whole number in [min, max], the default when absent, or null when invalid. */\nfunction integerArg(value: unknown, fallback: number, min: number, max: number): number | null {\n if (value === undefined) return fallback;\n return typeof value === 'number' && Number.isInteger(value) && value >= min && value <= max\n ? value\n : null;\n}\n\n/**\n * The CLI's own message out of its stderr: each `tabbied:` line and the\n * indented lines that continue it. Anything else there (Node's warnings, a\n * deprecation notice from a dependency) is noise to the agent. When there is\n * no such line, the CLI died some other way, and the tail is the best guess.\n */\nfunction cliMessage(stderr: string): string {\n const lines = stderr.split('\\n');\n const kept: string[] = [];\n let continuing = false;\n for (const line of lines) {\n if (line.startsWith('tabbied:')) {\n kept.push(line);\n continuing = true;\n } else if (continuing && /^\\s+\\S/.test(line)) {\n kept.push(line);\n } else {\n continuing = false;\n }\n }\n return kept.length > 0\n ? kept.join('\\n')\n : lines.filter((line) => line.trim()).slice(-5).join('\\n');\n}\n\nexport function renderTool(catalog: Catalog): Tool {\n return {\n definition: {\n name: 'render_design',\n title: 'Render a design to a file',\n description:\n 'Render a design to SVG or PNG with a real browser, at any size, seed, ' +\n 'palette, and option set. Use this to produce an actual asset - or to ' +\n 'preview a *customized* configuration, which preview_design cannot show ' +\n 'you (it only has the stock palette and defaults). Needs Playwright ' +\n 'where this server runs (start it as `npx -y -p tabbied-mcp -p ' +\n 'playwright tabbied-mcp`, with `npx playwright install chromium` once). ' +\n 'Without \"out\" the image comes back inline; with it, the file is ' +\n 'written where you ask, replacing any file already there.',\n inputSchema: {\n type: 'object',\n properties: {\n slug: { type: 'string', minLength: 1, description: 'The design to render.' },\n format: {\n type: 'string',\n enum: ['svg', 'png'],\n description:\n 'Vector or raster. A few designs cannot be vectorized - check ' +\n 'svgExport on the design first.',\n },\n out: {\n type: 'string',\n minLength: 1,\n description:\n 'Absolute path to write to, ending in .svg or .png to match ' +\n '\"format\". Omit to get the image back inline instead of on disk.',\n },\n seed: {\n type: 'string',\n minLength: 1,\n description:\n 'Fixed seed - the same seed always gives the same image. Random ' +\n 'when omitted.',\n },\n palette: {\n type: 'array',\n items: { type: 'string', minLength: 1 },\n minItems: 1,\n description:\n \"CSS colors, background first. Omit for the design's authored palette.\",\n },\n options: {\n type: 'object',\n additionalProperties: { type: ['string', 'number', 'boolean'] },\n description:\n 'Option values keyed by option id, e.g. {\"frequency\": 0.6}. Read ' +\n 'the ids, ranges, and choices off get_design.',\n },\n width: {\n type: 'integer',\n minimum: 1,\n maximum: SIZE_MAX,\n description: 'Pixels wide (default 960).',\n },\n height: {\n type: 'integer',\n minimum: 1,\n maximum: SIZE_MAX,\n description: 'Pixels tall (default 960).',\n },\n fit: {\n type: 'string',\n enum: ['grid', 'cover', 'fixed'],\n description: 'Sizing mode (default grid).',\n },\n scale: {\n type: 'integer',\n minimum: 1,\n maximum: SCALE_MAX,\n description:\n 'PNG device-scale factor. Defaults to 2 when writing a file and ' +\n '1 when returning inline, to keep the response small.',\n },\n },\n required: ['slug', 'format'],\n additionalProperties: false,\n },\n // It writes a file, so it is not read-only; it only ever adds or\n // replaces the one file it was asked for.\n annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false },\n },\n\n async run(args): Promise<ToolResult> {\n const slug = typeof args.slug === 'string' ? args.slug : '';\n const design = catalog.designs.find((entry) => entry.slug === slug);\n if (!design) {\n return toolError(\n `No design with slug \"${slug}\". Use search_designs to find one.`\n );\n }\n\n const format = args.format === 'png' ? 'png' : 'svg';\n if (format === 'svg' && !design.svgExport.supported) {\n return toolError(\n `\"${slug}\" paints effects SVG cannot represent, so it has no vector ` +\n 'export. Render it as PNG instead.'\n );\n }\n\n // The schema states these, but a host that skips validation (or calls\n // run() directly) must not hand the CLI a path it would resolve against\n // whatever directory the server happened to start in.\n const out = typeof args.out === 'string' && args.out.length > 0 ? args.out : null;\n if (out !== null && !path.isAbsolute(out)) {\n return toolError(\n `\"out\" must be an absolute path, got \"${out}\". A relative one would ` +\n \"land in the server's working directory, not yours.\"\n );\n }\n if (out !== null && path.extname(out).toLowerCase() !== `.${format}`) {\n return toolError(\n `\"out\" must end in .${format} to match format \"${format}\", got \"${out}\".`\n );\n }\n\n const inline = out === null;\n const width = integerArg(args.width, 960, 1, SIZE_MAX);\n const height = integerArg(args.height, 960, 1, SIZE_MAX);\n const scale = integerArg(args.scale, inline ? 1 : 2, 1, SCALE_MAX);\n if (width === null || height === null) {\n return toolError(`\"width\" and \"height\" must be whole pixels from 1 to ${SIZE_MAX}.`);\n }\n if (scale === null) {\n return toolError(`\"scale\" must be a whole number from 1 to ${SCALE_MAX}.`);\n }\n\n // Options reach the CLI as one \"id: value; id: value\" string, so a `;`\n // inside a value would end it and start an option of its own, past\n // every check on the first. Ranges and choices are the CLI's to check.\n let options: string | null = null;\n if (args.options !== undefined) {\n if (!args.options || typeof args.options !== 'object' || Array.isArray(args.options)) {\n return toolError('\"options\" must be an object of option values keyed by id.');\n }\n const ids = design.options.map((option) => option.id);\n const pairs: string[] = [];\n for (const [id, value] of Object.entries(args.options as Record<string, unknown>)) {\n if (!ids.includes(id)) {\n return toolError(\n `\"${slug}\" has no option \"${id}\" (has: ${ids.join(', ') || 'none'}).`\n );\n }\n if (!['string', 'number', 'boolean'].includes(typeof value) || /[;\\n]/.test(String(value))) {\n return toolError(\n `Option \"${id}\" must be a single string, number, or boolean value ` +\n '(no \";\" or line breaks).'\n );\n }\n pairs.push(`${id}: ${String(value)}`);\n }\n if (pairs.length > 0) options = pairs.join('; ');\n }\n\n const root = tabbiedRoot();\n if (!root) {\n return toolError(\n 'The `tabbied` package is not installed next to this server, so the ' +\n 'renderer is unavailable. Run `npm install tabbied`.'\n );\n }\n\n // An inline render gets a scratch directory, removed once its bytes\n // have been read back, or once the render has failed.\n const scratch = inline ? await mkdtemp(path.join(tmpdir(), 'tabbied-')) : null;\n const outPath = scratch ? path.join(scratch, `${slug}.${format}`) : (out as string);\n let keepScratch = false;\n\n // A value that starts with \"-\" (a seed of \"--browser\", say) would read\n // as the next flag, so it goes as --flag=value, which the CLI takes\n // from tabbied 0.7.1. Everything else stays two arguments, which every\n // version of the CLI reads.\n const flag = (name: string, value: string) =>\n value.startsWith('-') ? [`--${name}=${value}`] : [`--${name}`, value];\n const argv = [\n path.join(root, 'dist', 'cli.js'),\n 'render',\n slug,\n ...flag('out', outPath),\n ...flag('format', format),\n ...flag('scale', String(scale)),\n ...flag('size', `${width}x${height}`),\n ];\n if (typeof args.seed === 'string' && args.seed) argv.push(...flag('seed', args.seed));\n if (typeof args.fit === 'string') argv.push(...flag('fit', args.fit));\n if (Array.isArray(args.palette)) {\n const colors = args.palette.filter(\n (color): color is string => typeof color === 'string' && color.trim().length > 0\n );\n if (colors.length > 0) argv.push(...flag('palette', colors.join(',')));\n }\n if (options) argv.push(...flag('options', options));\n\n try {\n let stderr = '';\n try {\n // No shell: argv goes straight to node, so a palette or option value\n // can't be read as shell syntax.\n ({ stderr } = await run(process.execPath, argv, {\n timeout: RENDER_TIMEOUT_MS,\n maxBuffer: 8 * 1024 * 1024,\n }));\n } catch (error) {\n const failure = error as { killed?: boolean; stderr?: unknown; message?: string };\n if (failure.killed) {\n return toolError(\n `Rendering timed out after ${RENDER_TIMEOUT_MS / 1000}s. Try a smaller ` +\n 'width/height or scale.'\n );\n }\n const detail =\n typeof failure.stderr === 'string' && failure.stderr.trim()\n ? cliMessage(failure.stderr)\n : (failure.message ?? String(error));\n return toolError(`Rendering failed:\\n${detail}`);\n }\n\n // The CLI reports SVG-fidelity caveats on stderr; they matter enough to\n // pass through rather than swallow.\n const notes = stderr\n .split('\\n')\n .filter((line) => line.trim().startsWith('note:'))\n .join('\\n');\n\n if (!inline) {\n const { size } = await stat(outPath);\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} (${width}x${height}` +\n (format === 'png' ? ` @${scale}x` : '') +\n `, ${Math.round(size / 1024)} KB).` +\n (notes ? `\\n${notes}` : '')\n ),\n ],\n };\n }\n\n if (format === 'svg') {\n const svg = await readFile(outPath, 'utf-8');\n return {\n content: [\n text(\n `Rendered ${slug} as SVG (${width}x${height}).` +\n (notes ? `\\n${notes}` : '')\n ),\n text(svg),\n ],\n };\n }\n\n const png = await readFile(outPath);\n const data = png.toString('base64');\n if (data.length > INLINE_BYTE_BUDGET) {\n // Kept: the answer tells the agent to read it from that path.\n keepScratch = true;\n return {\n content: [\n text(\n `Rendered ${slug} to ${outPath} - ${Math.round(\n png.byteLength / 1024\n )} KB, too large to return inline. Read it from that path, or ` +\n 're-render smaller (lower \"scale\", or a smaller width/height).'\n ),\n ],\n };\n }\n\n return {\n content: [\n text(\n `Rendered ${slug} (${width}x${height} @${scale}x).` +\n (notes ? `\\n${notes}` : '')\n ),\n { type: 'image', data, mimeType: 'image/png' },\n ],\n };\n } finally {\n if (scratch && !keepScratch) await rm(scratch, { recursive: true, force: true });\n }\n },\n };\n}\n"]}
|
package/dist/node/resources.js
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"AAAA,+DAA+D;AAC/D,EAAE;AACF,gFAAgF;AAChF,6EAA6E;AAC7E,wEAAwE;AACxE,yDAAyD;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAS7B,MAAM,OAAO,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C,MAAM,IAAI,GAAG,qBAAqB,CAAC;AAEnC,iFAAiF;AACjF,MAAM,UAAU,WAAW;IACzB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,IAAI,GAAG,WAAW,EAAE,CAAC;IAE3B,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CACf,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAC9C,CAAC;QACf,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,yEAAyE;YACzE,uEAAuE;YACvE,qDAAqD;YACrD,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,kDAAkD,MAAM,KAAK;gBAC3D,mBAAmB,IAAI,iBAAiB,CAC3C,CAAC;QACJ,CAAC;IACH,CAAC;IAED,
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/node/resources.ts"],"names":[],"mappings":"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","sourcesContent":["// Where the stdio server gets its catalog, previews, and docs.\n//\n// The catalog is read from the installed `tabbied` package rather than fetched,\n// so the tools describe exactly the version the caller will `npm install`: a\n// design that only exists on the site would give an import that doesn't\n// resolve. The network is the fallback, not the default.\nimport { createRequire } from 'node:module';\nimport { readFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type {\n Catalog,\n CatalogDesign,\n TemplateCatalog,\n TemplateSpec,\n} from '../types.js';\n\nconst require = createRequire(import.meta.url);\n\nconst SITE = 'https://tabbied.com';\n\n// Every read here is a small static file. Without a limit, a network that\n// swallows packets would hold a tool call (or the server's startup) open for\n// as long as the client is willing to wait.\nconst FETCH_TIMEOUT_MS = 10_000;\n\n/**\n * GET a URL, failing with a message that names it. Node's own failure is a\n * bare \"fetch failed\" (the reason is in `cause`), which tells an agent nothing\n * about what was unreachable.\n */\nasync function get(url: string): Promise<Response> {\n let response: Response;\n try {\n response = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });\n } catch (error) {\n const reason =\n error instanceof Error && error.name === 'TimeoutError'\n ? `no answer within ${FETCH_TIMEOUT_MS / 1000}s`\n : error instanceof Error\n ? [error.message, (error.cause as Error | undefined)?.message]\n .filter(Boolean)\n .join(': ')\n : String(error);\n throw new Error(`${url} could not be reached (${reason})`);\n }\n if (!response.ok) throw new Error(`${url} returned ${response.status}`);\n return response;\n}\n\n/** Root of the installed `tabbied` package, or null when it isn't resolvable. */\nexport function tabbiedRoot(): string | null {\n try {\n return path.dirname(require.resolve('tabbied/package.json'));\n } catch {\n return null;\n }\n}\n\nexport async function loadCatalog(): Promise<Catalog> {\n const root = tabbiedRoot();\n\n if (root) {\n try {\n return JSON.parse(\n await readFile(path.join(root, 'catalog.json'), 'utf-8')\n ) as Catalog;\n } catch (error) {\n // A resolvable package with no catalog.json means a source checkout that\n // hasn't been built. Say so - the network copy would silently disagree\n // with the local patterns the user is about to edit.\n const reason = error instanceof Error ? error.message : String(error);\n process.stderr.write(\n `tabbied-mcp: could not read the local catalog (${reason}); ` +\n `falling back to ${SITE}/catalog.json\\n`\n );\n }\n }\n\n try {\n return (await (await get(`${SITE}/catalog.json`)).json()) as Catalog;\n } catch (error) {\n throw new Error(\n `no local catalog, and ${error instanceof Error ? error.message : String(error)}. ` +\n 'Install the `tabbied` package (npm i tabbied) or restore network access.'\n );\n }\n}\n\n/**\n * Preview bytes for one design: the committed @2x renders the site serves, so\n * the agent sees the same image a human browsing the gallery does.\n */\nexport async function fetchPreview(\n design: CatalogDesign\n): Promise<{ data: string; mimeType: string }> {\n const response = await get(design.preview);\n const buffer = Buffer.from(await response.arrayBuffer());\n return {\n data: buffer.toString('base64'),\n mimeType: response.headers.get('Content-Type') ?? 'image/webp',\n };\n}\n\n/**\n * The full reference. The package's own llms.txt is the *full* text (in\n * node_modules depth beats brevity), so this works offline too.\n */\nexport async function fetchDocs(): Promise<string> {\n const root = tabbiedRoot();\n if (root) {\n try {\n return await readFile(path.join(root, 'llms.txt'), 'utf-8');\n } catch {\n // fall through to the network copy\n }\n }\n\n return await (await get(`${SITE}/llms-full.txt`)).text();\n}\n\n/**\n * The editable-template index and one site's spec. Network only, deliberately:\n * they are *site* artifacts generated from the static export\n * (docs/editable-templates.md), and the `tabbied` package does not contain them.\n */\nexport async function fetchTemplateCatalog(): Promise<TemplateCatalog> {\n return (await (await get(`${SITE}/editable-catalog.json`)).json()) as TemplateCatalog;\n}\n\nexport async function fetchTemplate(slug: string): Promise<TemplateSpec> {\n return (await (await get(`${SITE}/editable/${encodeURIComponent(slug)}.json`)).json()) as TemplateSpec;\n}\n"]}
|
package/dist/server.js
CHANGED
|
@@ -17,12 +17,20 @@ import { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';
|
|
|
17
17
|
* values instead of silently matching nothing.
|
|
18
18
|
*/
|
|
19
19
|
export function buildServer(tools) {
|
|
20
|
-
|
|
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;
|
package/dist/server.js.map
CHANGED
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,6EAA6E;AAC7E,sEAAsE;AACtE,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,qBAAqB;AACrB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG/D;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,yEAAyE;IACzE,0EAA0E;IAC1E,6EAA6E;IAC7E,0EAA0E;IAC1E,6BAA6B;IAC7B,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,EACvC,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,CAChF,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,YAAY,CACjB,IAAI,CAAC,UAAU,CAAC,IAAI,EACpB;YACE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,KAAK;YAC5B,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;YACxC,WAAW,EAAE,cAAc,CACzB,IAAI,CAAC,UAAU,CAAC,WAAW,CAC5B;YACD,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW;gBAC7B,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE;gBAC9C,CAAC,CAAC,EAAE,CAAC;SACR,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAU,CACtD,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// Turns the runtime-agnostic toolset into an SDK `McpServer`. The Worker hands\n// the factory to `createMcpHandler` and the bin hands it to `serveStdio`, so\n// both transports get the same tools, instructions, and era handling.\n//\n// A *factory*, not an instance: MCP v2 is stateless and the SDK builds one\n// server per request (per connection on stdio). Nothing here may capture\n// per-request state.\nimport { McpServer, fromJsonSchema } from '@modelcontextprotocol/server';\n\nimport { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';\nimport type { Tool } from './types.js';\n\n/**\n * Register a toolset onto a fresh `McpServer`.\n *\n * Tool schemas stay plain JSON Schema, adapted by `fromJsonSchema`, because\n * `search_designs`'s enums are derived from the catalog being served and so\n * cannot be static Zod. Registering the schema also makes the SDK validate\n * arguments: a bad enum value comes back as a tool error naming the allowed\n * values instead of silently matching nothing.\n */\nexport function buildServer(tools: Tool[]): McpServer {\n // `listChanged: false` because the list never changes: a server is built\n // per request (per connection on stdio) with its tools fixed, and nothing\n // ever sends the notification. Left unset, the SDK advertises it as true the\n // moment the first tool registers, inviting a modern client to listen for\n // a change that cannot come.\n const server = new McpServer(\n { name: SERVER_NAME, version: VERSION },\n { capabilities: { tools: { listChanged: false } }, instructions: INSTRUCTIONS }\n );\n\n for (const tool of tools) {\n server.registerTool(\n tool.definition.name,\n {\n title: tool.definition.title,\n description: tool.definition.description,\n inputSchema: fromJsonSchema<Record<string, unknown>>(\n tool.definition.inputSchema\n ),\n ...(tool.definition.annotations\n ? { annotations: tool.definition.annotations }\n : {}),\n },\n async (args) => (await tool.run(args ?? {})) as never\n );\n }\n\n return server;\n}\n"]}
|
package/dist/stdio.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Configure it in an MCP client as:
|
|
8
8
|
// { "command": "npx", "args": ["-y", "tabbied-mcp"] }
|
|
9
|
+
// or, for render_design, with Playwright installed beside it:
|
|
10
|
+
// { "command": "npx", "args": ["-y", "-p", "tabbied-mcp", "-p", "playwright", "tabbied-mcp"] }
|
|
9
11
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
10
12
|
import { buildServer } from './server.js';
|
|
11
13
|
import { catalogTools } from './tools.js';
|
package/dist/stdio.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":";AACA,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;AAC3E,uCAAuC;AACvC,EAAE;AACF,oCAAoC;AACpC,wDAAwD;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,SAAS,EACT,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;IAEpC,MAAM,KAAK,GAAG;QACZ,GAAG,YAAY,CAAC;YACd,OAAO;YACP,YAAY;YACZ,SAAS;YACT,oBAAoB;YACpB,aAAa;SACd,CAAC;QACF,qEAAqE;QACrE,UAAU,CAAC,OAAO,CAAC;KACpB,CAAC;IAEF,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAEpD,4EAA4E;IAC5E,uBAAuB;IACvB,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAC7B,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,gEAAgE;IAChE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gBAAgB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC3E,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n// The `tabbied-mcp` bin: the same server the site exposes at /mcp, over stdio.\n// `serveStdio` owns the transport (framing, the era decision, one instance per\n// connection). The catalog is loaded once here rather than in the factory,\n// because it is immutable per process.\n//\n// Configure it in an MCP client as:\n// { \"command\": \"npx\", \"args\": [\"-y\", \"tabbied-mcp\"] }\nimport { serveStdio } from '@modelcontextprotocol/server/stdio';\n\nimport { buildServer } from './server.js';\nimport { catalogTools } from './tools.js';\nimport {\n fetchDocs,\n fetchPreview,\n fetchTemplate,\n fetchTemplateCatalog,\n loadCatalog,\n} from './node/resources.js';\nimport { renderTool } from './node/render.js';\n\nasync function main(): Promise<void> {\n const catalog = await loadCatalog();\n\n const tools = [\n ...catalogTools({\n catalog,\n fetchPreview,\n fetchDocs,\n fetchTemplateCatalog,\n fetchTemplate,\n }),\n // Only the local server can render: it has a browser to render with.\n renderTool(catalog),\n ];\n\n const handle = serveStdio(() => buildServer(tools));\n\n // The client closing stdin is the shutdown signal; without this the process\n // outlives the client.\n process.stdin.on('close', () => {\n void handle.close();\n });\n}\n\nmain().catch((error) => {\n // stdout is the protocol channel and nothing else may touch it.\n process.stderr.write(\n `tabbied-mcp: ${error instanceof Error ? error.message : String(error)}\\n`\n );\n process.exit(1);\n});\n"]}
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":";AACA,+EAA+E;AAC/E,+EAA+E;AAC/E,2EAA2E;AAC3E,uCAAuC;AACvC,EAAE;AACF,oCAAoC;AACpC,wDAAwD;AACxD,8DAA8D;AAC9D,iGAAiG;AACjG,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,SAAS,EACT,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,WAAW,GACZ,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;IAEpC,MAAM,KAAK,GAAG;QACZ,GAAG,YAAY,CAAC;YACd,OAAO;YACP,YAAY;YACZ,SAAS;YACT,oBAAoB;YACpB,aAAa;SACd,CAAC;QACF,qEAAqE;QACrE,UAAU,CAAC,OAAO,CAAC;KACpB,CAAC;IAEF,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;IAEpD,4EAA4E;IAC5E,uBAAuB;IACvB,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAC7B,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,gEAAgE;IAChE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,gBAAgB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC3E,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n// The `tabbied-mcp` bin: the same server the site exposes at /mcp, over stdio.\n// `serveStdio` owns the transport (framing, the era decision, one instance per\n// connection). The catalog is loaded once here rather than in the factory,\n// because it is immutable per process.\n//\n// Configure it in an MCP client as:\n// { \"command\": \"npx\", \"args\": [\"-y\", \"tabbied-mcp\"] }\n// or, for render_design, with Playwright installed beside it:\n// { \"command\": \"npx\", \"args\": [\"-y\", \"-p\", \"tabbied-mcp\", \"-p\", \"playwright\", \"tabbied-mcp\"] }\nimport { serveStdio } from '@modelcontextprotocol/server/stdio';\n\nimport { buildServer } from './server.js';\nimport { catalogTools } from './tools.js';\nimport {\n fetchDocs,\n fetchPreview,\n fetchTemplate,\n fetchTemplateCatalog,\n loadCatalog,\n} from './node/resources.js';\nimport { renderTool } from './node/render.js';\n\nasync function main(): Promise<void> {\n const catalog = await loadCatalog();\n\n const tools = [\n ...catalogTools({\n catalog,\n fetchPreview,\n fetchDocs,\n fetchTemplateCatalog,\n fetchTemplate,\n }),\n // Only the local server can render: it has a browser to render with.\n renderTool(catalog),\n ];\n\n const handle = serveStdio(() => buildServer(tools));\n\n // The client closing stdin is the shutdown signal; without this the process\n // outlives the client.\n process.stdin.on('close', () => {\n void handle.close();\n });\n}\n\nmain().catch((error) => {\n // stdout is the protocol channel and nothing else may touch it.\n process.stderr.write(\n `tabbied-mcp: ${error instanceof Error ? error.message : String(error)}\\n`\n );\n process.exit(1);\n});\n"]}
|
package/dist/templates.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const SITE = 'https://tabbied.com';
|
|
2
|
+
const LIST_LIMIT_DEFAULT = 20;
|
|
3
|
+
const LIST_LIMIT_MAX = 100;
|
|
1
4
|
const text = (value) => ({ type: 'text', text: value });
|
|
2
5
|
const json = (value) => text(JSON.stringify(value, null, 2));
|
|
3
6
|
const toolError = (message) => ({
|
|
@@ -30,7 +33,8 @@ const USAGE = {
|
|
|
30
33
|
programmatic: 'To apply changes programmatically, write an edits document ' +
|
|
31
34
|
'({specVersion, slug, edits}) and run it through applyEdits() from the ' +
|
|
32
35
|
'`tabbied-templates` npm package, which validates it against this spec ' +
|
|
33
|
-
'first. Full reference:
|
|
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:
|
|
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
|
|
83
|
-
'plain HTML or as a React (Vite) project. Returns
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
'
|
|
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: '
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
:
|
|
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
|
|
114
|
-
|
|
115
|
-
hint:
|
|
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
|
|
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:
|
|
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}" (${
|
|
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:
|
|
182
|
-
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:
|
|
304
|
+
url: `${SITE}${entry.href}`,
|
|
189
305
|
license: LICENSE,
|
|
190
306
|
downloads,
|
|
191
307
|
usage: {
|