tabbied-mcp 0.0.0-stage → 0.2.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sy Hong and Ye Joo Park
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,83 @@
1
- # Temporary Holding Version
1
+ # tabbied-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An [MCP](https://modelcontextprotocol.io) server for
4
+ [Tabbied](https://tabbied.com): search 338 generative pattern designs, look at
5
+ them, and render them to SVG or PNG - from Claude Code, Claude Desktop, Cursor,
6
+ or any other MCP client.
7
+
8
+ ## Use it without installing anything
9
+
10
+ The same server runs at `https://tabbied.com/mcp`:
11
+
12
+ ```bash
13
+ claude mcp add --transport http tabbied https://tabbied.com/mcp
14
+ ```
15
+
16
+ ```jsonc
17
+ {
18
+ "mcpServers": {
19
+ "tabbied": { "url": "https://tabbied.com/mcp" }
20
+ }
21
+ }
22
+ ```
23
+
24
+ ## Or run it locally, and render real files
25
+
26
+ ```bash
27
+ claude mcp add tabbied -- npx -y tabbied-mcp
28
+ ```
29
+
30
+ ```jsonc
31
+ {
32
+ "mcpServers": {
33
+ "tabbied": { "command": "npx", "args": ["-y", "tabbied-mcp"] }
34
+ }
35
+ }
36
+ ```
37
+
38
+ The local server adds `render_design`, which the remote one cannot offer -
39
+ rendering a css-doodle pattern needs a real browser. Install a Playwright
40
+ alongside it (`npm i -D playwright`) or point `TABBIED_CHROMIUM` at a Chromium
41
+ binary.
42
+
43
+ ## Tools
44
+
45
+ | Tool | What it does |
46
+ | --- | --- |
47
+ | `search_designs` | Filter by motif, mood, density, intended use, free text, or SVG-export support. |
48
+ | `get_design` | The full record for one slug, plus ready-to-paste snippets. |
49
+ | `preview_design` | The rendered preview image for up to six designs, so the model can *look*. |
50
+ | `get_docs` | The complete API reference (`llms-full.txt`). |
51
+ | `render_design` | SVG or PNG at any size, seed, palette, and option set. **Local only.** |
52
+
53
+ Slugs are opaque - `cleat`, `karst`, `radius` - so the intended flow is
54
+ `search_designs` to narrow, `preview_design` to look, then `get_design` for the
55
+ options. Choosing off tags alone is the main way this goes wrong.
56
+
57
+ ## Programmatic use
58
+
59
+ The package's main entry point is runtime-agnostic (no node imports), so it can
60
+ be embedded in a Worker or any Web-standard server. It exposes the tools and an
61
+ `McpServer` factory; the transport is the MCP SDK's:
62
+
63
+ ```ts
64
+ import { createMcpHandler } from '@modelcontextprotocol/server';
65
+ import { buildServer, catalogTools } from 'tabbied-mcp';
66
+
67
+ const tools = catalogTools({ catalog, fetchPreview, fetchDocs });
68
+
69
+ export default {
70
+ fetch: createMcpHandler(() => buildServer(tools)).fetch,
71
+ };
72
+ ```
73
+
74
+ Pass the *factory*, not a built server: MCP v2 is stateless and the handler
75
+ constructs one server per request.
76
+
77
+ Built on [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server)
78
+ v2, so it speaks the stateless `2026-07-28` revision and still serves 2025-era
79
+ clients. `docs/mcp-server.md` in the repository has the details.
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,5 @@
1
+ export { buildServer } from './server.js';
2
+ export { catalogTools, createToolset, type Toolset } from './tools.js';
3
+ export { templateTools } from './templates.js';
4
+ export { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';
5
+ export type { Catalog, CatalogDesign, CatalogOption, TemplateCatalog, TemplateCatalogEntry, TemplateSpec, Tool, ToolContent, ToolContext, ToolDefinition, ToolResult, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // The runtime-agnostic half of the server: the tool definitions and the
2
+ // `McpServer` factory. Nothing reachable from here may import node, because
3
+ // this entry point is what the Cloudflare Worker bundles; the local catalog
4
+ // reader and `render_design` live behind the bin. See docs/mcp-server.md.
5
+ export { buildServer } from './server.js';
6
+ export { catalogTools, createToolset } from './tools.js';
7
+ export { templateTools } from './templates.js';
8
+ export { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAgB,MAAM,YAAY,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC","sourcesContent":["// The runtime-agnostic half of the server: the tool definitions and the\n// `McpServer` factory. Nothing reachable from here may import node, because\n// this entry point is what the Cloudflare Worker bundles; the local catalog\n// reader and `render_design` live behind the bin. See docs/mcp-server.md.\nexport { buildServer } from './server.js';\nexport { catalogTools, createToolset, type Toolset } from './tools.js';\nexport { templateTools } from './templates.js';\nexport { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';\nexport type {\n Catalog,\n CatalogDesign,\n CatalogOption,\n TemplateCatalog,\n TemplateCatalogEntry,\n TemplateSpec,\n Tool,\n ToolContent,\n ToolContext,\n ToolDefinition,\n ToolResult,\n} from './types.js';\n"]}
package/dist/info.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export declare const SERVER_NAME = "tabbied";
2
+ export declare const VERSION = "0.2.3";
3
+ /**
4
+ * Shown to the model as a preamble. It carries what no tool description can,
5
+ * because they are properties of the *set* rather than of one call: slugs
6
+ * cannot be guessed, the previews are what a choice should rest on, and the
7
+ * sizing rule is the most common way a correct-looking integration renders as
8
+ * nothing.
9
+ */
10
+ export declare const INSTRUCTIONS = "Tabbied is a catalog of generative patterns (css-doodle) usable as backgrounds, textures, posters, and exported SVG/PNG assets.\n\nDesigns are addressed by slug, and slugs are opaque - \"cleat\", \"karst\", \"radius\" say nothing about what they look like. Never guess one: call search_designs, which filters on a closed vocabulary of motifs, moods, density, and intended use.\n\nMetadata narrows the field; it does not settle it. These are pictures, so call preview_design on your shortlist and look before you commit. Choosing off tags alone is the main way this goes wrong.\n\nWhen you write integration code, remember a pattern has no intrinsic size: it fills its parent and collapses to nothing in a parent that sizes to content. Pass height or aspectRatio unless the parent is definitely sized. get_docs has the full API contract and recipes.";
package/dist/info.js ADDED
@@ -0,0 +1,23 @@
1
+ // Server identity and the instructions clients hand to the model.
2
+ //
3
+ // VERSION is a literal rather than a package.json read because this module is
4
+ // bundled into a Cloudflare Worker, which has no filesystem. The release's
5
+ // `version-packages` step rewrites it from package.json
6
+ // (scripts/sync-version.mjs), and test/info.test.mjs pins the two together.
7
+ export const SERVER_NAME = 'tabbied';
8
+ export const VERSION = '0.2.3';
9
+ /**
10
+ * Shown to the model as a preamble. It carries what no tool description can,
11
+ * because they are properties of the *set* rather than of one call: slugs
12
+ * cannot be guessed, the previews are what a choice should rest on, and the
13
+ * sizing rule is the most common way a correct-looking integration renders as
14
+ * nothing.
15
+ */
16
+ export const INSTRUCTIONS = `Tabbied is a catalog of generative patterns (css-doodle) usable as backgrounds, textures, posters, and exported SVG/PNG assets.
17
+
18
+ Designs are addressed by slug, and slugs are opaque - "cleat", "karst", "radius" say nothing about what they look like. Never guess one: call search_designs, which filters on a closed vocabulary of motifs, moods, density, and intended use.
19
+
20
+ Metadata narrows the field; it does not settle it. These are pictures, so call preview_design on your shortlist and look before you commit. Choosing off tags alone is the main way this goes wrong.
21
+
22
+ When you write integration code, remember a pattern has no intrinsic size: it fills its parent and collapses to nothing in a parent that sizes to content. Pass height or aspectRatio unless the parent is definitely sized. get_docs has the full API contract and recipes.`;
23
+ //# sourceMappingURL=info.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"info.js","sourceRoot":"","sources":["../src/info.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,8EAA8E;AAC9E,2EAA2E;AAC3E,wDAAwD;AACxD,4EAA4E;AAC5E,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AACrC,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;6QAMiP,CAAC","sourcesContent":["// Server identity and the instructions clients hand to the model.\n//\n// VERSION is a literal rather than a package.json read because this module is\n// bundled into a Cloudflare Worker, which has no filesystem. The release's\n// `version-packages` step rewrites it from package.json\n// (scripts/sync-version.mjs), and test/info.test.mjs pins the two together.\nexport const SERVER_NAME = 'tabbied';\nexport const VERSION = '0.2.3';\n\n/**\n * Shown to the model as a preamble. It carries what no tool description can,\n * because they are properties of the *set* rather than of one call: slugs\n * cannot be guessed, the previews are what a choice should rest on, and the\n * sizing rule is the most common way a correct-looking integration renders as\n * nothing.\n */\nexport const INSTRUCTIONS = `Tabbied is a catalog of generative patterns (css-doodle) usable as backgrounds, textures, posters, and exported SVG/PNG assets.\n\nDesigns are addressed by slug, and slugs are opaque - \"cleat\", \"karst\", \"radius\" say nothing about what they look like. Never guess one: call search_designs, which filters on a closed vocabulary of motifs, moods, density, and intended use.\n\nMetadata narrows the field; it does not settle it. These are pictures, so call preview_design on your shortlist and look before you commit. Choosing off tags alone is the main way this goes wrong.\n\nWhen you write integration code, remember a pattern has no intrinsic size: it fills its parent and collapses to nothing in a parent that sizes to content. Pass height or aspectRatio unless the parent is definitely sized. get_docs has the full API contract and recipes.`;\n"]}
@@ -0,0 +1,2 @@
1
+ import type { Catalog, Tool } from '../types.js';
2
+ export declare function renderTool(catalog: Catalog): Tool;
@@ -0,0 +1,208 @@
1
+ // `render_design`, the one tool that only exists over stdio.
2
+ //
3
+ // It shells out to the `tabbied` CLI: the only faithful renderer for a
4
+ // css-doodle pattern is css-doodle in a real browser (see docs/svg-export.md),
5
+ // and the CLI already owns the headless browser, the SVG converter, and the
6
+ // option parsing. A Worker has no browser, so it doesn't offer the tool.
7
+ import { execFile } from 'node:child_process';
8
+ import { mkdtemp, readFile, rm, stat } from 'node:fs/promises';
9
+ import { tmpdir } from 'node:os';
10
+ import path from 'node:path';
11
+ import { promisify } from 'node:util';
12
+ import { tabbiedRoot } from './resources.js';
13
+ const run = promisify(execFile);
14
+ // Rendering launches a browser and waits for the first arrangement to settle.
15
+ const RENDER_TIMEOUT_MS = 180_000;
16
+ // A PNG returned inline is spent context. Past this the file stays on disk and
17
+ // the agent gets the path - better a usable pointer than a truncated image.
18
+ const INLINE_BYTE_BUDGET = 1_500_000;
19
+ const text = (value) => ({ type: 'text', text: value });
20
+ const toolError = (message) => ({
21
+ content: [text(message)],
22
+ isError: true,
23
+ });
24
+ export function renderTool(catalog) {
25
+ return {
26
+ definition: {
27
+ name: 'render_design',
28
+ title: 'Render a design to a file',
29
+ description: 'Render a design to SVG or PNG with a real browser, at any size, seed, ' +
30
+ 'palette, and option set. Use this to produce an actual asset - or to ' +
31
+ 'preview a *customized* configuration, which preview_design cannot show ' +
32
+ '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.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ slug: { type: 'string', description: 'The design to render.' },
39
+ format: {
40
+ type: 'string',
41
+ enum: ['svg', 'png'],
42
+ description: 'Vector or raster. A few designs cannot be vectorized - check ' +
43
+ 'svgExport on the design first.',
44
+ },
45
+ out: {
46
+ type: 'string',
47
+ description: 'Absolute path to write to. Omit to get the image back inline ' +
48
+ 'instead of on disk.',
49
+ },
50
+ seed: {
51
+ type: 'string',
52
+ description: 'Fixed seed - the same seed always gives the same image. Random ' +
53
+ 'when omitted.',
54
+ },
55
+ palette: {
56
+ type: 'array',
57
+ items: { type: 'string' },
58
+ description: "CSS colors, background first. Omit for the design's authored palette.",
59
+ },
60
+ options: {
61
+ type: 'object',
62
+ description: 'Option values keyed by option id, e.g. {"scale": 3}. Read the ' +
63
+ 'ids and ranges off get_design.',
64
+ },
65
+ width: { type: 'integer', description: 'Pixels wide (default 960).' },
66
+ height: { type: 'integer', description: 'Pixels tall (default 960).' },
67
+ fit: {
68
+ type: 'string',
69
+ enum: ['grid', 'cover', 'fixed'],
70
+ description: 'Sizing mode (default grid).',
71
+ },
72
+ scale: {
73
+ type: 'integer',
74
+ description: 'PNG device-scale factor. Defaults to 2 when writing a file and ' +
75
+ '1 when returning inline, to keep the response small.',
76
+ },
77
+ },
78
+ required: ['slug', 'format'],
79
+ additionalProperties: false,
80
+ },
81
+ },
82
+ async run(args) {
83
+ const slug = typeof args.slug === 'string' ? args.slug : '';
84
+ const design = catalog.designs.find((entry) => entry.slug === slug);
85
+ if (!design) {
86
+ return toolError(`No design with slug "${slug}". Use search_designs to find one.`);
87
+ }
88
+ const format = args.format === 'png' ? 'png' : 'svg';
89
+ if (format === 'svg' && !design.svgExport.supported) {
90
+ return toolError(`"${slug}" paints effects SVG cannot represent, so it has no vector ` +
91
+ 'export. Render it as PNG instead.');
92
+ }
93
+ const root = tabbiedRoot();
94
+ if (!root) {
95
+ return toolError('The `tabbied` package is not installed next to this server, so the ' +
96
+ 'renderer is unavailable. Run `npm install tabbied`.');
97
+ }
98
+ const inline = typeof args.out !== 'string' || args.out.length === 0;
99
+ // An inline render gets a scratch directory, removed once its bytes
100
+ // have been read back.
101
+ 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;
109
+ const argv = [
110
+ path.join(root, 'dist', 'cli.js'),
111
+ 'render',
112
+ slug,
113
+ '--out',
114
+ outPath,
115
+ '--format',
116
+ format,
117
+ '--scale',
118
+ String(scale),
119
+ ];
120
+ const push = (flag, value) => argv.push(flag, value);
121
+ if (typeof args.seed === 'string' && args.seed)
122
+ push('--seed', args.seed);
123
+ 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);
137
+ }
138
+ let stderr = '';
139
+ 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();
178
+ return {
179
+ content: [
180
+ text(`Rendered ${slug} as SVG (${width}x${height}).` +
181
+ (notes ? `\n${notes}` : '')),
182
+ text(svg),
183
+ ],
184
+ };
185
+ }
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
+ };
196
+ }
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
+ },
206
+ };
207
+ }
208
+ //# sourceMappingURL=render.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,24 @@
1
+ import type { Catalog, CatalogDesign, TemplateCatalog, TemplateSpec } from '../types.js';
2
+ /** Root of the installed `tabbied` package, or null when it isn't resolvable. */
3
+ export declare function tabbiedRoot(): string | null;
4
+ export declare function loadCatalog(): Promise<Catalog>;
5
+ /**
6
+ * Preview bytes for one design: the committed @2x renders the site serves, so
7
+ * the agent sees the same image a human browsing the gallery does.
8
+ */
9
+ export declare function fetchPreview(design: CatalogDesign): Promise<{
10
+ data: string;
11
+ mimeType: string;
12
+ }>;
13
+ /**
14
+ * The full reference. The package's own llms.txt is the *full* text (in
15
+ * node_modules depth beats brevity), so this works offline too.
16
+ */
17
+ export declare function fetchDocs(): Promise<string>;
18
+ /**
19
+ * The editable-template index and one site's spec. Network only, deliberately:
20
+ * they are *site* artifacts generated from the static export
21
+ * (docs/editable-templates.md), and the `tabbied` package does not contain them.
22
+ */
23
+ export declare function fetchTemplateCatalog(): Promise<TemplateCatalog>;
24
+ export declare function fetchTemplate(slug: string): Promise<TemplateSpec>;
@@ -0,0 +1,97 @@
1
+ // Where the stdio server gets its catalog, previews, and docs.
2
+ //
3
+ // The catalog is read from the installed `tabbied` package rather than fetched,
4
+ // so the tools describe exactly the version the caller will `npm install`: a
5
+ // design that only exists on the site would give an import that doesn't
6
+ // resolve. The network is the fallback, not the default.
7
+ import { createRequire } from 'node:module';
8
+ import { readFile } from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ const require = createRequire(import.meta.url);
11
+ const SITE = 'https://tabbied.com';
12
+ /** Root of the installed `tabbied` package, or null when it isn't resolvable. */
13
+ export function tabbiedRoot() {
14
+ try {
15
+ return path.dirname(require.resolve('tabbied/package.json'));
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ export async function loadCatalog() {
22
+ const root = tabbiedRoot();
23
+ if (root) {
24
+ try {
25
+ return JSON.parse(await readFile(path.join(root, 'catalog.json'), 'utf-8'));
26
+ }
27
+ catch (error) {
28
+ // A resolvable package with no catalog.json means a source checkout that
29
+ // hasn't been built. Say so - the network copy would silently disagree
30
+ // with the local patterns the user is about to edit.
31
+ const reason = error instanceof Error ? error.message : String(error);
32
+ process.stderr.write(`tabbied-mcp: could not read the local catalog (${reason}); ` +
33
+ `falling back to ${SITE}/catalog.json\n`);
34
+ }
35
+ }
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}. ` +
39
+ 'Install the `tabbied` package (npm i tabbied) or restore network access.');
40
+ }
41
+ return (await response.json());
42
+ }
43
+ /**
44
+ * Preview bytes for one design: the committed @2x renders the site serves, so
45
+ * the agent sees the same image a human browsing the gallery does.
46
+ */
47
+ 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
+ }
52
+ const buffer = Buffer.from(await response.arrayBuffer());
53
+ return {
54
+ data: buffer.toString('base64'),
55
+ mimeType: response.headers.get('Content-Type') ?? 'image/webp',
56
+ };
57
+ }
58
+ /**
59
+ * The full reference. The package's own llms.txt is the *full* text (in
60
+ * node_modules depth beats brevity), so this works offline too.
61
+ */
62
+ export async function fetchDocs() {
63
+ const root = tabbiedRoot();
64
+ if (root) {
65
+ try {
66
+ return await readFile(path.join(root, 'llms.txt'), 'utf-8');
67
+ }
68
+ catch {
69
+ // fall through to the network copy
70
+ }
71
+ }
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();
77
+ }
78
+ /**
79
+ * The editable-template index and one site's spec. Network only, deliberately:
80
+ * they are *site* artifacts generated from the static export
81
+ * (docs/editable-templates.md), and the `tabbied` package does not contain them.
82
+ */
83
+ 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());
89
+ }
90
+ 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());
96
+ }
97
+ //# sourceMappingURL=resources.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/node/resources.ts"],"names":[],"mappings":"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","sourcesContent":["// Where the stdio server gets its catalog, previews, and docs.\n//\n// The catalog is read from the installed `tabbied` package rather than fetched,\n// so the tools describe exactly the version the caller will `npm install`: a\n// design that only exists on the site would give an import that doesn't\n// resolve. The network is the fallback, not the default.\nimport { createRequire } from 'node:module';\nimport { readFile } from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type {\n Catalog,\n CatalogDesign,\n TemplateCatalog,\n TemplateSpec,\n} from '../types.js';\n\nconst require = createRequire(import.meta.url);\n\nconst SITE = 'https://tabbied.com';\n\n/** Root of the installed `tabbied` package, or null when it isn't resolvable. */\nexport function tabbiedRoot(): string | null {\n try {\n return path.dirname(require.resolve('tabbied/package.json'));\n } catch {\n return null;\n }\n}\n\nexport async function loadCatalog(): Promise<Catalog> {\n const root = tabbiedRoot();\n\n if (root) {\n try {\n return JSON.parse(\n await readFile(path.join(root, 'catalog.json'), 'utf-8')\n ) as Catalog;\n } catch (error) {\n // A resolvable package with no catalog.json means a source checkout that\n // hasn't been built. Say so - the network copy would silently disagree\n // with the local patterns the user is about to edit.\n const reason = error instanceof Error ? error.message : String(error);\n process.stderr.write(\n `tabbied-mcp: could not read the local catalog (${reason}); ` +\n `falling back to ${SITE}/catalog.json\\n`\n );\n }\n }\n\n const response = await fetch(`${SITE}/catalog.json`);\n if (!response.ok) {\n throw new Error(\n `no local catalog and ${SITE}/catalog.json returned ${response.status}. ` +\n 'Install the `tabbied` package (npm i tabbied) or restore network access.'\n );\n }\n return (await response.json()) as Catalog;\n}\n\n/**\n * Preview bytes for one design: the committed @2x renders the site serves, so\n * the agent sees the same image a human browsing the gallery does.\n */\nexport async function fetchPreview(\n design: CatalogDesign\n): Promise<{ data: string; mimeType: string }> {\n const response = await fetch(design.preview);\n if (!response.ok) {\n throw new Error(`${design.preview} returned ${response.status}`);\n }\n const buffer = Buffer.from(await response.arrayBuffer());\n return {\n data: buffer.toString('base64'),\n mimeType: response.headers.get('Content-Type') ?? 'image/webp',\n };\n}\n\n/**\n * The full reference. The package's own llms.txt is the *full* text (in\n * node_modules depth beats brevity), so this works offline too.\n */\nexport async function fetchDocs(): Promise<string> {\n const root = tabbiedRoot();\n if (root) {\n try {\n return await readFile(path.join(root, 'llms.txt'), 'utf-8');\n } catch {\n // fall through to the network copy\n }\n }\n\n const response = await fetch(`${SITE}/llms-full.txt`);\n if (!response.ok) {\n throw new Error(`${SITE}/llms-full.txt returned ${response.status}`);\n }\n return await response.text();\n}\n\n/**\n * The editable-template index and one site's spec. Network only, deliberately:\n * they are *site* artifacts generated from the static export\n * (docs/editable-templates.md), and the `tabbied` package does not contain them.\n */\nexport async function fetchTemplateCatalog(): Promise<TemplateCatalog> {\n const response = await fetch(`${SITE}/editable-catalog.json`);\n\n if (!response.ok) {\n throw new Error(`${SITE}/editable-catalog.json returned ${response.status}`);\n }\n\n return (await response.json()) as TemplateCatalog;\n}\n\nexport async function fetchTemplate(slug: string): Promise<TemplateSpec> {\n const response = await fetch(`${SITE}/editable/${slug}.json`);\n\n if (!response.ok) {\n throw new Error(`${SITE}/editable/${slug}.json returned ${response.status}`);\n }\n\n return (await response.json()) as TemplateSpec;\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import type { Tool } from './types.js';
3
+ /**
4
+ * Register a toolset onto a fresh `McpServer`.
5
+ *
6
+ * Tool schemas stay plain JSON Schema, adapted by `fromJsonSchema`, because
7
+ * `search_designs`'s enums are derived from the catalog being served and so
8
+ * cannot be static Zod. Registering the schema also makes the SDK validate
9
+ * arguments: a bad enum value comes back as a tool error naming the allowed
10
+ * values instead of silently matching nothing.
11
+ */
12
+ export declare function buildServer(tools: Tool[]): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,30 @@
1
+ // Turns the runtime-agnostic toolset into an SDK `McpServer`. The Worker hands
2
+ // the factory to `createMcpHandler` and the bin hands it to `serveStdio`, so
3
+ // both transports get the same tools, instructions, and era handling.
4
+ //
5
+ // A *factory*, not an instance: MCP v2 is stateless and the SDK builds one
6
+ // server per request (per connection on stdio). Nothing here may capture
7
+ // per-request state.
8
+ import { McpServer, fromJsonSchema } from '@modelcontextprotocol/server';
9
+ import { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';
10
+ /**
11
+ * Register a toolset onto a fresh `McpServer`.
12
+ *
13
+ * Tool schemas stay plain JSON Schema, adapted by `fromJsonSchema`, because
14
+ * `search_designs`'s enums are derived from the catalog being served and so
15
+ * cannot be static Zod. Registering the schema also makes the SDK validate
16
+ * arguments: a bad enum value comes back as a tool error naming the allowed
17
+ * values instead of silently matching nothing.
18
+ */
19
+ export function buildServer(tools) {
20
+ const server = new McpServer({ name: SERVER_NAME, version: VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
21
+ for (const tool of tools) {
22
+ server.registerTool(tool.definition.name, {
23
+ title: tool.definition.title,
24
+ description: tool.definition.description,
25
+ inputSchema: fromJsonSchema(tool.definition.inputSchema),
26
+ }, async (args) => (await tool.run(args ?? {})));
27
+ }
28
+ return server;
29
+ }
30
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,6EAA6E;AAC7E,sEAAsE;AACtE,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,qBAAqB;AACrB,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAEzE,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG/D;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,EACvC,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,CAC5D,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,YAAY,CACjB,IAAI,CAAC,UAAU,CAAC,IAAI,EACpB;YACE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,KAAK;YAC5B,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,WAAW;YACxC,WAAW,EAAE,cAAc,CACzB,IAAI,CAAC,UAAU,CAAC,WAAW,CAC5B;SACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAU,CACtD,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// Turns the runtime-agnostic toolset into an SDK `McpServer`. The Worker hands\n// the factory to `createMcpHandler` and the bin hands it to `serveStdio`, so\n// both transports get the same tools, instructions, and era handling.\n//\n// A *factory*, not an instance: MCP v2 is stateless and the SDK builds one\n// server per request (per connection on stdio). Nothing here may capture\n// per-request state.\nimport { McpServer, fromJsonSchema } from '@modelcontextprotocol/server';\n\nimport { INSTRUCTIONS, SERVER_NAME, VERSION } from './info.js';\nimport type { Tool } from './types.js';\n\n/**\n * Register a toolset onto a fresh `McpServer`.\n *\n * Tool schemas stay plain JSON Schema, adapted by `fromJsonSchema`, because\n * `search_designs`'s enums are derived from the catalog being served and so\n * cannot be static Zod. Registering the schema also makes the SDK validate\n * arguments: a bad enum value comes back as a tool error naming the allowed\n * values instead of silently matching nothing.\n */\nexport function buildServer(tools: Tool[]): McpServer {\n const server = new McpServer(\n { name: SERVER_NAME, version: VERSION },\n { capabilities: { tools: {} }, instructions: INSTRUCTIONS }\n );\n\n for (const tool of tools) {\n server.registerTool(\n tool.definition.name,\n {\n title: tool.definition.title,\n description: tool.definition.description,\n inputSchema: fromJsonSchema<Record<string, unknown>>(\n tool.definition.inputSchema\n ),\n },\n async (args) => (await tool.run(args ?? {})) as never\n );\n }\n\n return server;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};