@zaai-dev/mcp 0.1.0

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 Zaai Studio
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 ADDED
@@ -0,0 +1,117 @@
1
+ # Zaai Dev MCP
2
+
3
+ Model Context Protocol server for the [Zaai Dev](https://www.zaaistudio.com) platform. Exposes your captured design references to MCP-compatible AI tools — Claude Code, Claude Desktop, Cursor, Continue, Cline, anything else that speaks MCP.
4
+
5
+ After a one-time token paste, prompts like *"list my last 5 captures tagged hero and show me their palettes"* call the workspace directly and pull back real data.
6
+
7
+ ## Install
8
+
9
+ You need:
10
+
11
+ - **Node 20 or newer** (`node --version`)
12
+ - A **Zaai Dev workspace account** at https://www.zaaistudio.com
13
+ - An **MCP token** — mint one at https://www.zaaistudio.com/dev/settings/tokens (pick "mcp" as the kind). The token-mint screen also shows the config snippets below pre-filled with your secret.
14
+
15
+ ### Claude Desktop
16
+
17
+ Add to `claude_desktop_config.json` — merge into the existing `mcpServers` block, don't replace the whole file.
18
+
19
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
20
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
21
+ - **Linux:** `~/.config/Claude/claude_desktop_config.json`
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "zaai-dev": {
27
+ "command": "npx",
28
+ "args": ["-y", "@zaai-dev/mcp"],
29
+ "env": {
30
+ "ZAAI_API_TOKEN": "zaai_mcp_YOUR_SECRET_HERE"
31
+ }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ Restart Claude Desktop. The 9 tools below appear in the slash-command picker.
38
+
39
+ ### Cursor
40
+
41
+ Add to `~/.cursor/mcp.json` — same JSON shape as Claude Desktop.
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "zaai-dev": {
47
+ "command": "npx",
48
+ "args": ["-y", "@zaai-dev/mcp"],
49
+ "env": {
50
+ "ZAAI_API_TOKEN": "zaai_mcp_YOUR_SECRET_HERE"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ Restart Cursor.
58
+
59
+ ### Claude Code (CLI)
60
+
61
+ One-liner — updates `~/.claude/mcp_servers.json` automatically:
62
+
63
+ ```sh
64
+ claude mcp add zaai-dev -e ZAAI_API_TOKEN=zaai_mcp_YOUR_SECRET_HERE -- npx -y @zaai-dev/mcp
65
+ ```
66
+
67
+ ## Tools
68
+
69
+ All tools except `health` need a valid token. All tools except `health` and `whoami` charge **1 credit** per successful call (your workspace plan determines the monthly credit grant — see [pricing](https://www.zaaistudio.com/dev/pricing)).
70
+
71
+ | Tool | What it does |
72
+ |---|---|
73
+ | `health` | Server status + version + uptime. No auth, no charge. |
74
+ | `whoami` | Returns your userId, orgId, project scope, and credit balance. No charge. |
75
+ | `list_captures` | Paginated list of your captures (newest first). Args: `q`, `cursor`, `limit`. |
76
+ | `search_captures` | Same as list but with `q` required. Tuned description for targeted retrieval. |
77
+ | `get_capture` | Full payload + signed screenshot URLs for one capture id. |
78
+ | `get_palette` | Just the palette slice (page) or eyedropper picks (element/composite). |
79
+ | `get_html` | Just the HTML. Page → full HTML; element → outerHTML; composite → concat with markers. |
80
+ | `get_animation` | Just the animation data — CSS transitions, keyframes, library hints. |
81
+ | `get_media` | Just the media inventory — videos, images, backgrounds, carousels. |
82
+
83
+ Plus the `zaai-capture://{id}` resource template — attach individual captures to a conversation via the resource picker.
84
+
85
+ ## Errors
86
+
87
+ | Error class | Meaning | Action |
88
+ |---|---|---|
89
+ | `Unauthorized` | Token invalid, revoked, or wrong kind | Mint a fresh `mcp` token, update your config, restart |
90
+ | `InsufficientCredits` | Out of credits for the month | [Top up](https://www.zaaistudio.com/dev/settings/billing) or wait for the monthly grant |
91
+ | `CaptureNotFound` | id doesn't exist OR is in a project the token can't see | `list_captures` to find valid ids |
92
+ | `InvalidCaptureId` | id isn't a UUID | Pass an id from `list_captures` or the workspace URL |
93
+ | `UnknownCaptureField` | Hit a focused-getter route with an unknown field | Use one of: `palette`, `html`, `animation`, `media` |
94
+
95
+ Errors return as `isError: true` in the tool result so the LLM can read and act on them.
96
+
97
+ ## Privacy + scoping
98
+
99
+ The server holds **only your MCP token** — it never sees your password, your Supabase service-role key, or other users' data. The workspace's `verifyToken` derives `userId` + `orgId` from your token on every request; queries are scoped to that user + the token's project allowlist (which you set at mint time). A revoked token causes the next tool call to fail with `Unauthorized`.
100
+
101
+ The server itself runs entirely on your machine — `npx -y @zaai-dev/mcp` downloads the package once, then your AI tool launches it as a subprocess. No telemetry, no analytics SDK.
102
+
103
+ ## Building locally
104
+
105
+ ```sh
106
+ git clone https://github.com/POLONIBOI/ZAAI_dev_mcp.git
107
+ cd ZAAI_dev_mcp
108
+ pnpm install
109
+ pnpm build
110
+ pnpm inspector # MCP Inspector for interactive testing
111
+ ```
112
+
113
+ You'll need `ZAAI_API_TOKEN` set as an env var (or pasted into the Inspector's Environment Variables panel before clicking Connect).
114
+
115
+ ## License
116
+
117
+ [MIT](LICENSE).
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/stdio.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+
9
+ // src/tools/health.ts
10
+ import { z } from "zod";
11
+
12
+ // src/version.ts
13
+ var PKG_VERSION = true ? "0.1.0" : "0.0.0-dev";
14
+
15
+ // src/tools/health.ts
16
+ var healthInputSchema = z.object({});
17
+ var healthOutputSchema = z.object({
18
+ ok: z.boolean().describe("Always true \u2014 health is the server admitting it answered."),
19
+ version: z.string().describe("The MCP server's package version."),
20
+ node: z.string().describe("The Node.js runtime version the server is running on."),
21
+ uptimeSeconds: z.number().describe("Seconds since this server process started.")
22
+ });
23
+ function health() {
24
+ return {
25
+ ok: true,
26
+ version: PKG_VERSION,
27
+ node: process.versions.node,
28
+ uptimeSeconds: Math.round(process.uptime())
29
+ };
30
+ }
31
+
32
+ // src/tools/whoami.ts
33
+ import { z as z2 } from "zod";
34
+
35
+ // src/util/auth.ts
36
+ function bearerHeader(token) {
37
+ return { Authorization: `Bearer ${token}` };
38
+ }
39
+
40
+ // src/util/api.ts
41
+ var WorkspaceApiError = class extends Error {
42
+ constructor(status, body, path) {
43
+ super(`${path} \u2192 ${status}`);
44
+ this.status = status;
45
+ this.body = body;
46
+ this.path = path;
47
+ this.name = "WorkspaceApiError";
48
+ }
49
+ status;
50
+ body;
51
+ path;
52
+ };
53
+ async function mcpApiFetch(config, path, init = {}) {
54
+ const url = `${config.apiUrl}${path}`;
55
+ let res;
56
+ try {
57
+ res = await fetch(url, {
58
+ ...init,
59
+ headers: {
60
+ ...bearerHeader(config.apiToken),
61
+ ...init.headers ?? {}
62
+ }
63
+ });
64
+ } catch (err) {
65
+ throw new WorkspaceApiError(0, { message: String(err) }, path);
66
+ }
67
+ if (!res.ok) {
68
+ const body = await res.json().catch(() => ({ error: `Non-JSON ${res.status} response` }));
69
+ throw new WorkspaceApiError(res.status, body, path);
70
+ }
71
+ return await res.json();
72
+ }
73
+
74
+ // src/tools/whoami.ts
75
+ var whoamiInputSchema = z2.object({});
76
+ var whoamiOutputSchema = z2.object({
77
+ userId: z2.string().describe("Workspace user id this token belongs to."),
78
+ orgId: z2.string().describe("Organisation id the token is scoped to."),
79
+ projectScope: z2.array(z2.string()).nullable().describe(
80
+ "List of project ids the token can access, or null when scoped to every project in the org."
81
+ ),
82
+ balance: z2.object({
83
+ currentBalance: z2.number().describe("Top-up credits (never expire)."),
84
+ monthlyGrantRemaining: z2.number().describe("Monthly grant remaining (resets on the 1st)."),
85
+ totalBalance: z2.number().describe("Sum of the two \u2014 what's available right now.")
86
+ }).describe("Credit balance available for MCP calls."),
87
+ mcpCallCost: z2.number().describe("Credits debited per successful MCP tool call.")
88
+ });
89
+ async function whoami(config) {
90
+ return mcpApiFetch(config, "/api/mcp/me");
91
+ }
92
+
93
+ // src/tools/list-captures.ts
94
+ import { z as z3 } from "zod";
95
+ var listCapturesInputSchema = z3.object({
96
+ q: z3.string().optional().describe(
97
+ "Optional text filter applied to source_title, source_url, and note (case-insensitive substring match). Leave blank to list all."
98
+ ),
99
+ cursor: z3.string().optional().describe(
100
+ "Opaque pagination cursor from a previous response's nextCursor field. Omit on the first page."
101
+ ),
102
+ limit: z3.number().int().min(1).max(50).optional().describe("Max captures to return. Default 20, max 50.")
103
+ });
104
+ var listCapturesOutputSchema = z3.object({
105
+ captures: z3.array(
106
+ z3.object({
107
+ id: z3.string().describe("Capture UUID. Pass to get_capture for the full payload."),
108
+ type: z3.enum(["page", "element", "composite"]).describe(
109
+ "page = full-page screenshot + DOM context; element = a single picked element; composite = multiple elements stacked."
110
+ ),
111
+ sourceTitle: z3.string(),
112
+ sourceUrl: z3.string(),
113
+ capturedAt: z3.string().describe("ISO 8601 timestamp from the extension at capture time."),
114
+ tags: z3.array(z3.string()),
115
+ thumbnailUrl: z3.string().nullable().describe(
116
+ "Signed Supabase Storage URL (1h TTL). Fetch fresh by re-calling list_captures or get_capture."
117
+ )
118
+ })
119
+ ),
120
+ nextCursor: z3.string().optional().describe(
121
+ "When present, pass back as `cursor` to fetch the next page. Absent = no more rows."
122
+ )
123
+ });
124
+ async function listCaptures(store, args) {
125
+ return store.list({
126
+ q: args.q,
127
+ cursor: args.cursor,
128
+ limit: args.limit
129
+ });
130
+ }
131
+
132
+ // src/tools/get-capture.ts
133
+ import { z as z4 } from "zod";
134
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
135
+ var getCaptureInputSchema = z4.object({
136
+ id: z4.string().regex(UUID_RE, "id must be a UUID").describe("Capture id from list_captures.")
137
+ });
138
+ var getCaptureOutputSchema = z4.object({
139
+ id: z4.string(),
140
+ type: z4.enum(["page", "element", "composite"]),
141
+ projectId: z4.string(),
142
+ sourceTitle: z4.string(),
143
+ sourceUrl: z4.string(),
144
+ capturedAt: z4.string(),
145
+ note: z4.string().nullable(),
146
+ tags: z4.array(z4.string()),
147
+ screenshotUrl: z4.string().nullable().describe("Signed PNG URL, 1h TTL."),
148
+ thumbnailUrl: z4.string().nullable(),
149
+ fullPageScreenshotUrl: z4.string().nullable().describe(
150
+ "Page captures with the M9 stitch get a full-page PNG too. Null for element + composite + page captures without a stitch."
151
+ ),
152
+ payload: z4.unknown().describe(
153
+ "Full extension-side Capture JSON. Shape depends on `type` \u2014 see https://github.com/POLONIBOI/ZAAI_STUDIO-ext/blob/main/src/shared/types.ts for the union."
154
+ )
155
+ }).passthrough();
156
+ async function getCapture(store, args) {
157
+ return store.get(args.id);
158
+ }
159
+
160
+ // src/tools/search-captures.ts
161
+ import { z as z5 } from "zod";
162
+ var searchCapturesInputSchema = z5.object({
163
+ q: z5.string().min(1).describe(
164
+ "Case-insensitive substring match against source_title, source_url, and note. Required \u2014 use list_captures if you don't have a query."
165
+ ),
166
+ cursor: z5.string().optional(),
167
+ limit: z5.number().int().min(1).max(50).optional()
168
+ });
169
+ var searchCapturesOutputSchema = z5.object({
170
+ captures: z5.array(
171
+ z5.object({
172
+ id: z5.string(),
173
+ type: z5.enum(["page", "element", "composite"]),
174
+ sourceTitle: z5.string(),
175
+ sourceUrl: z5.string(),
176
+ capturedAt: z5.string(),
177
+ tags: z5.array(z5.string()),
178
+ thumbnailUrl: z5.string().nullable()
179
+ })
180
+ ),
181
+ nextCursor: z5.string().optional()
182
+ });
183
+ async function searchCaptures(store, args) {
184
+ return store.list({ q: args.q, cursor: args.cursor, limit: args.limit });
185
+ }
186
+
187
+ // src/tools/_focused-getter.ts
188
+ import { z as z6 } from "zod";
189
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
190
+ var focusedGetterInputSchema = z6.object({
191
+ id: z6.string().regex(UUID_RE2, "id must be a UUID").describe(
192
+ "Capture id from list_captures or search_captures. UUID format."
193
+ )
194
+ });
195
+ var focusedGetterOutputSchema = z6.object({
196
+ id: z6.string(),
197
+ type: z6.enum(["page", "element", "composite"]),
198
+ field: z6.string(),
199
+ data: z6.unknown()
200
+ }).passthrough();
201
+
202
+ // src/tools/get-palette.ts
203
+ async function getPalette(store, args) {
204
+ return store.getField(args.id, "palette");
205
+ }
206
+
207
+ // src/tools/get-html.ts
208
+ async function getHtml(store, args) {
209
+ return store.getField(args.id, "html");
210
+ }
211
+
212
+ // src/tools/get-animation.ts
213
+ async function getAnimation(store, args) {
214
+ return store.getField(args.id, "animation");
215
+ }
216
+
217
+ // src/tools/get-media.ts
218
+ async function getMedia(store, args) {
219
+ return store.getField(args.id, "media");
220
+ }
221
+
222
+ // src/store/http-store.ts
223
+ var HttpCaptureStore = class {
224
+ constructor(config) {
225
+ this.config = config;
226
+ }
227
+ config;
228
+ async list(args) {
229
+ const sp = new URLSearchParams();
230
+ if (args.q) sp.set("q", args.q);
231
+ if (args.cursor) sp.set("cursor", args.cursor);
232
+ if (args.limit !== void 0) sp.set("limit", String(args.limit));
233
+ const qs = sp.toString();
234
+ return mcpApiFetch(
235
+ this.config,
236
+ `/api/mcp/captures${qs ? `?${qs}` : ""}`
237
+ );
238
+ }
239
+ async get(id) {
240
+ return mcpApiFetch(
241
+ this.config,
242
+ `/api/mcp/captures/${encodeURIComponent(id)}`
243
+ );
244
+ }
245
+ async getField(id, field) {
246
+ return mcpApiFetch(
247
+ this.config,
248
+ `/api/mcp/captures/${encodeURIComponent(id)}/${field}`
249
+ );
250
+ }
251
+ };
252
+
253
+ // src/resources/capture-resource.ts
254
+ import {
255
+ ResourceTemplate
256
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
257
+ function registerCaptureResource(server, store) {
258
+ server.registerResource(
259
+ "capture",
260
+ new ResourceTemplate("zaai-capture://{id}", {
261
+ list: void 0
262
+ }),
263
+ {
264
+ title: "Zaai Dev capture",
265
+ description: "Attach one capture from the user's Zaai Dev library as JSON. URI: zaai-capture://<capture-id>. The id comes from list_captures.",
266
+ mimeType: "application/json"
267
+ },
268
+ async (uri, variables) => {
269
+ const rawId = variables.id;
270
+ const id = Array.isArray(rawId) ? rawId[0] : rawId;
271
+ const capture = await store.get(id);
272
+ return {
273
+ contents: [
274
+ {
275
+ uri: uri.href,
276
+ mimeType: "application/json",
277
+ text: JSON.stringify(capture, null, 2)
278
+ }
279
+ ]
280
+ };
281
+ }
282
+ );
283
+ }
284
+
285
+ // src/log.ts
286
+ var PREFIX = "[zaai-mcp]";
287
+ function write(level, message, meta) {
288
+ let line = `${PREFIX} ${level} ${message}`;
289
+ if (meta !== void 0) {
290
+ line += " " + safeStringify(meta);
291
+ }
292
+ process.stderr.write(line + "\n");
293
+ }
294
+ function safeStringify(value) {
295
+ try {
296
+ if (value instanceof Error) {
297
+ return value.stack ?? `${value.name}: ${value.message}`;
298
+ }
299
+ return JSON.stringify(value);
300
+ } catch {
301
+ return String(value);
302
+ }
303
+ }
304
+ function info(message, meta) {
305
+ write("INFO", message, meta);
306
+ }
307
+ function error(message, meta) {
308
+ write("ERROR", message, meta);
309
+ }
310
+
311
+ // src/server.ts
312
+ function createServer(config) {
313
+ const server = new McpServer2({
314
+ name: "zaai-dev-mcp",
315
+ version: PKG_VERSION
316
+ });
317
+ const captureStore = new HttpCaptureStore(config);
318
+ server.registerTool(
319
+ "health",
320
+ {
321
+ title: "Health check",
322
+ description: "Returns the Zaai Dev MCP server's status, version, and uptime. Use this to confirm the server is running before relying on auth-gated tools. No workspace access required. Useful as a smoke test from the MCP Inspector before configuring credentials.",
323
+ inputSchema: healthInputSchema.shape,
324
+ outputSchema: healthOutputSchema.shape
325
+ },
326
+ async () => {
327
+ const result = health();
328
+ return {
329
+ structuredContent: result,
330
+ // Human-readable summary for clients that haven't adopted
331
+ // structuredContent yet. Spec 2025-06-18 still requires both.
332
+ content: [
333
+ {
334
+ type: "text",
335
+ text: `Zaai Dev MCP ${result.version} \xB7 node ${result.node} \xB7 uptime ${result.uptimeSeconds}s`
336
+ }
337
+ ]
338
+ };
339
+ }
340
+ );
341
+ server.registerTool(
342
+ "whoami",
343
+ {
344
+ title: "Who am I",
345
+ description: "Returns the Zaai Dev workspace account this server is bound to: user id, organisation id, project scope, and current credit balance. Use this to confirm the configured ZAAI_API_TOKEN is valid before calling any other workspace tool. Does NOT debit credits \u2014 the workspace treats /me as a heartbeat, not user-facing work.",
346
+ inputSchema: whoamiInputSchema.shape,
347
+ outputSchema: whoamiOutputSchema.shape
348
+ },
349
+ async () => {
350
+ try {
351
+ const result = await whoami(config);
352
+ return {
353
+ structuredContent: result,
354
+ content: [
355
+ {
356
+ type: "text",
357
+ text: summariseWhoami(result)
358
+ }
359
+ ]
360
+ };
361
+ } catch (err) {
362
+ return toolErrorResponse(err);
363
+ }
364
+ }
365
+ );
366
+ server.registerTool(
367
+ "list_captures",
368
+ {
369
+ title: "List captures",
370
+ description: "List the user's Zaai Dev captures (newest first, paginated). Use this to discover what's in the library before calling get_capture for specifics. Each row carries id, type, title, URL, capturedAt, tags, and a thumbnail URL \u2014 but NOT the full HTML, computed style, or animation data. Use get_capture for those. Costs 1 credit per call.",
371
+ inputSchema: listCapturesInputSchema.shape,
372
+ outputSchema: listCapturesOutputSchema.shape
373
+ },
374
+ async (args) => {
375
+ try {
376
+ const result = await listCaptures(captureStore, args);
377
+ return {
378
+ // Cast: SDK's structuredContent param wants
379
+ // Record<string,unknown> but our typed DTO doesn't carry
380
+ // an index signature. Runtime shape is identical.
381
+ structuredContent: result,
382
+ content: [
383
+ {
384
+ type: "text",
385
+ text: result.captures.length === 0 ? "No captures yet. The user can push captures from the Zaai Dev Chrome extension." : `${result.captures.length} capture${result.captures.length === 1 ? "" : "s"}${result.nextCursor ? " (more available \u2014 pass nextCursor for the next page)" : ""}.`
386
+ }
387
+ ]
388
+ };
389
+ } catch (err) {
390
+ return toolErrorResponse(err);
391
+ }
392
+ }
393
+ );
394
+ server.registerTool(
395
+ "get_capture",
396
+ {
397
+ title: "Get capture",
398
+ description: "Fetch the full payload for one capture by id. Includes everything the extension extracted (HTML, computed CSS, palette, fonts, animation data, media inventory, ancestry, page sections, manual eyedropper picks) plus fresh-signed Supabase URLs for the screenshot and thumbnail. Use list_captures or search_captures first to discover ids. Costs 1 credit per call; the 404 path (unknown id) doesn't bill.",
399
+ inputSchema: getCaptureInputSchema.shape,
400
+ outputSchema: getCaptureOutputSchema.shape
401
+ },
402
+ async (args) => {
403
+ try {
404
+ const result = await getCapture(captureStore, args);
405
+ return {
406
+ structuredContent: result,
407
+ content: [
408
+ {
409
+ type: "text",
410
+ text: `${result.type} capture "${result.sourceTitle || result.sourceUrl}" (${result.tags.length} tag${result.tags.length === 1 ? "" : "s"})` + (result.screenshotUrl ? " \u2014 screenshot URL included." : "")
411
+ }
412
+ ]
413
+ };
414
+ } catch (err) {
415
+ return toolErrorResponse(err);
416
+ }
417
+ }
418
+ );
419
+ server.registerTool(
420
+ "search_captures",
421
+ {
422
+ title: "Search captures",
423
+ description: "Find captures matching a text query (case-insensitive substring match against source_title, source_url, and note). Returns the same summary shape as list_captures. Use this when you have a specific concept in mind ('hero', 'pricing table', 'stripe'); use list_captures to browse instead. Costs 1 credit per call.",
424
+ inputSchema: searchCapturesInputSchema.shape,
425
+ outputSchema: searchCapturesOutputSchema.shape
426
+ },
427
+ async (args) => {
428
+ try {
429
+ const result = await searchCaptures(captureStore, args);
430
+ return {
431
+ structuredContent: result,
432
+ content: [
433
+ {
434
+ type: "text",
435
+ text: result.captures.length === 0 ? `No captures match "${args.q}".` : `${result.captures.length} capture${result.captures.length === 1 ? "" : "s"} matching "${args.q}"${result.nextCursor ? " (more available \u2014 pass nextCursor for the next page)" : ""}.`
436
+ }
437
+ ]
438
+ };
439
+ } catch (err) {
440
+ return toolErrorResponse(err);
441
+ }
442
+ }
443
+ );
444
+ server.registerTool(
445
+ "get_palette",
446
+ {
447
+ title: "Get capture palette",
448
+ description: "Returns just the colour palette for a capture. Page captures carry a 6-colour extracted palette (`palette`); element + composite captures carry the user's eyedropper picks (`manualPalette`). Use this when the LLM needs colour context without loading the full payload. Costs 1 credit.",
449
+ inputSchema: focusedGetterInputSchema.shape,
450
+ outputSchema: focusedGetterOutputSchema.shape
451
+ },
452
+ makeFocusedGetterHandler(captureStore, getPalette, "palette")
453
+ );
454
+ server.registerTool(
455
+ "get_html",
456
+ {
457
+ title: "Get capture HTML",
458
+ description: "Returns just the HTML for a capture. Page \u2192 full page HTML; element \u2192 outerHTML of the picked element; composite \u2192 concatenated outerHTMLs with `<!-- Element N: selector -->` markers. Use this for component-extraction prompts ('rebuild this in Tailwind'). Costs 1 credit. Page captures can be large \u2014 budget your context accordingly.",
459
+ inputSchema: focusedGetterInputSchema.shape,
460
+ outputSchema: focusedGetterOutputSchema.shape
461
+ },
462
+ makeFocusedGetterHandler(captureStore, getHtml, "html")
463
+ );
464
+ server.registerTool(
465
+ "get_animation",
466
+ {
467
+ title: "Get capture animation data",
468
+ description: "Returns just the animation data. Page \u2192 pageAnimations (keyframes vocabulary + detected motion libraries like Framer Motion, GSAP, AOS, anime.js, Lenis, Locomotive). Element \u2192 the element's CSS transitions, keyframes, hover/focus/active state diffs, and per-element library hints. Composite \u2192 per-element variants. Use this to give the LLM motion context without loading HTML and computed style. Costs 1 credit.",
469
+ inputSchema: focusedGetterInputSchema.shape,
470
+ outputSchema: focusedGetterOutputSchema.shape
471
+ },
472
+ makeFocusedGetterHandler(captureStore, getAnimation, "animation")
473
+ );
474
+ server.registerTool(
475
+ "get_media",
476
+ {
477
+ title: "Get capture media inventory",
478
+ description: "Returns just the rich-media inventory for element + composite captures: videos (src/poster/autoplay/loop), images (src/srcset/alt), background-image URLs across the subtree, and carousel containers (Swiper, Glide, Splide, Slick, Flickity, Embla, `[data-carousel]`) with slide counts. Page captures return media:null (page-level palette/fonts only). Costs 1 credit.",
479
+ inputSchema: focusedGetterInputSchema.shape,
480
+ outputSchema: focusedGetterOutputSchema.shape
481
+ },
482
+ makeFocusedGetterHandler(captureStore, getMedia, "media")
483
+ );
484
+ registerCaptureResource(server, captureStore);
485
+ info("server initialized", {
486
+ version: PKG_VERSION,
487
+ tools: [
488
+ "health",
489
+ "whoami",
490
+ "list_captures",
491
+ "search_captures",
492
+ "get_capture",
493
+ "get_palette",
494
+ "get_html",
495
+ "get_animation",
496
+ "get_media"
497
+ ],
498
+ resources: ["zaai-capture://{id}"]
499
+ });
500
+ return server;
501
+ }
502
+ function makeFocusedGetterHandler(store, fn, label) {
503
+ return async (args) => {
504
+ try {
505
+ const result = await fn(store, args);
506
+ return {
507
+ structuredContent: result,
508
+ content: [
509
+ {
510
+ type: "text",
511
+ text: `${result.type} capture \xB7 ${label} slice for id ${result.id.slice(0, 8)}\u2026`
512
+ }
513
+ ]
514
+ };
515
+ } catch (err) {
516
+ return toolErrorResponse(err);
517
+ }
518
+ };
519
+ }
520
+ function summariseWhoami(r) {
521
+ const scope = r.projectScope === null ? "all projects in org" : `${r.projectScope.length} project${r.projectScope.length === 1 ? "" : "s"}`;
522
+ return `Zaai Dev workspace \xB7 user ${r.userId.slice(0, 8)}\u2026 \xB7 org ${r.orgId.slice(0, 8)}\u2026 \xB7 ${scope} \xB7 ${r.balance.totalBalance} credits (${r.mcpCallCost}/call)`;
523
+ }
524
+ function toolErrorResponse(err) {
525
+ if (err instanceof WorkspaceApiError) {
526
+ switch (err.status) {
527
+ case 401:
528
+ return errorContent(
529
+ "Authentication failed. Your ZAAI_API_TOKEN is invalid, expired, or revoked. Mint a new MCP token at https://www.zaaistudio.com/dev/settings/tokens and update your MCP client config."
530
+ );
531
+ case 402:
532
+ return errorContent(
533
+ "Out of credits. Top up at https://www.zaaistudio.com/dev/settings/billing."
534
+ );
535
+ case 0:
536
+ return errorContent(
537
+ `Network failure reaching the Zaai Dev workspace: ${describe(err.body)}. Check your connection or ZAAI_API_URL setting.`
538
+ );
539
+ default:
540
+ return errorContent(
541
+ `Workspace returned ${err.status} for ${err.path}: ${describe(err.body)}.`
542
+ );
543
+ }
544
+ }
545
+ return errorContent(`Unexpected MCP tool failure: ${describe(err)}`);
546
+ }
547
+ function errorContent(text) {
548
+ return { isError: true, content: [{ type: "text", text }] };
549
+ }
550
+ function describe(value) {
551
+ if (value instanceof Error) return value.message;
552
+ if (typeof value === "string") return value;
553
+ try {
554
+ return JSON.stringify(value);
555
+ } catch {
556
+ return String(value);
557
+ }
558
+ }
559
+
560
+ // src/config.ts
561
+ var DEFAULT_API_URL = "https://www.zaaistudio.com";
562
+ var ConfigError = class extends Error {
563
+ constructor(message) {
564
+ super(message);
565
+ this.name = "ConfigError";
566
+ }
567
+ };
568
+ function loadConfig() {
569
+ const apiToken = process.env.ZAAI_API_TOKEN;
570
+ if (!apiToken) {
571
+ throw new ConfigError(
572
+ "ZAAI_API_TOKEN is not set. Mint a token at https://www.zaaistudio.com/dev/settings/tokens (kind = mcp) and pass it via the env in your MCP client config."
573
+ );
574
+ }
575
+ if (!apiToken.startsWith("zaai_mcp_")) {
576
+ throw new ConfigError(
577
+ "ZAAI_API_TOKEN must start with 'zaai_mcp_'. Did you paste an extension token (zaai_ext_*) by accident? Issue an AI tool token at https://www.zaaistudio.com/dev/settings/tokens."
578
+ );
579
+ }
580
+ const rawUrl = process.env.ZAAI_API_URL ?? DEFAULT_API_URL;
581
+ const apiUrl = rawUrl.replace(/\/+$/, "");
582
+ info("config loaded", { apiUrl });
583
+ return { apiToken, apiUrl };
584
+ }
585
+
586
+ // src/bin/stdio.ts
587
+ async function main() {
588
+ let config;
589
+ try {
590
+ config = loadConfig();
591
+ } catch (err) {
592
+ if (err instanceof ConfigError) {
593
+ error("FATAL: " + err.message);
594
+ process.exit(1);
595
+ }
596
+ throw err;
597
+ }
598
+ const server = createServer(config);
599
+ const transport = new StdioServerTransport();
600
+ await server.connect(transport);
601
+ info("stdio transport connected, awaiting JSON-RPC");
602
+ }
603
+ main().catch((err) => {
604
+ error("fatal startup error", err);
605
+ process.exit(1);
606
+ });
607
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bin/stdio.ts","../../src/server.ts","../../src/tools/health.ts","../../src/version.ts","../../src/tools/whoami.ts","../../src/util/auth.ts","../../src/util/api.ts","../../src/tools/list-captures.ts","../../src/tools/get-capture.ts","../../src/tools/search-captures.ts","../../src/tools/_focused-getter.ts","../../src/tools/get-palette.ts","../../src/tools/get-html.ts","../../src/tools/get-animation.ts","../../src/tools/get-media.ts","../../src/store/http-store.ts","../../src/resources/capture-resource.ts","../../src/log.ts","../../src/config.ts"],"sourcesContent":["#!/usr/bin/env node\r\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\r\nimport { createServer } from \"../server.js\";\r\nimport { loadConfig, ConfigError } from \"../config.js\";\r\nimport * as log from \"../log.js\";\r\n\r\n// stdio entry point. Run via `npx -y @zaai-dev/mcp` or directly via\r\n// `node dist/bin/stdio.js`. The MCP client (Claude Code / Desktop /\r\n// Cursor) launches this as a subprocess and talks JSON-RPC over\r\n// stdin/stdout. Nothing else may write to stdout — see src/log.ts.\r\n\r\nasync function main(): Promise<void> {\r\n // Fail fast on missing/malformed token. Without this, the first\r\n // auth-gated tool call surfaces an opaque 401 and the user has no\r\n // signal which env var is wrong.\r\n let config;\r\n try {\r\n config = loadConfig();\r\n } catch (err) {\r\n if (err instanceof ConfigError) {\r\n log.error(\"FATAL: \" + err.message);\r\n process.exit(1);\r\n }\r\n throw err;\r\n }\r\n\r\n const server = createServer(config);\r\n const transport = new StdioServerTransport();\r\n await server.connect(transport);\r\n log.info(\"stdio transport connected, awaiting JSON-RPC\");\r\n}\r\n\r\nmain().catch((err) => {\r\n log.error(\"fatal startup error\", err);\r\n process.exit(1);\r\n});\r\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\r\nimport {\r\n health,\r\n healthInputSchema,\r\n healthOutputSchema,\r\n} from \"./tools/health.js\";\r\nimport {\r\n whoami,\r\n whoamiInputSchema,\r\n whoamiOutputSchema,\r\n} from \"./tools/whoami.js\";\r\nimport {\r\n listCaptures,\r\n listCapturesInputSchema,\r\n listCapturesOutputSchema,\r\n} from \"./tools/list-captures.js\";\r\nimport {\r\n getCapture,\r\n getCaptureInputSchema,\r\n getCaptureOutputSchema,\r\n} from \"./tools/get-capture.js\";\r\nimport {\r\n searchCaptures,\r\n searchCapturesInputSchema,\r\n searchCapturesOutputSchema,\r\n} from \"./tools/search-captures.js\";\r\nimport {\r\n getPalette,\r\n getPaletteInputSchema,\r\n getPaletteOutputSchema,\r\n} from \"./tools/get-palette.js\";\r\nimport {\r\n getHtml,\r\n getHtmlInputSchema,\r\n getHtmlOutputSchema,\r\n} from \"./tools/get-html.js\";\r\nimport {\r\n getAnimation,\r\n getAnimationInputSchema,\r\n getAnimationOutputSchema,\r\n} from \"./tools/get-animation.js\";\r\nimport {\r\n getMedia,\r\n getMediaInputSchema,\r\n getMediaOutputSchema,\r\n} from \"./tools/get-media.js\";\r\nimport { HttpCaptureStore } from \"./store/http-store.js\";\r\nimport { registerCaptureResource } from \"./resources/capture-resource.js\";\r\nimport { WorkspaceApiError } from \"./util/api.js\";\r\nimport type { Config } from \"./config.js\";\r\nimport { PKG_VERSION } from \"./version.js\";\r\nimport * as log from \"./log.js\";\r\n\r\n// Factory: produces a fully-configured MCP server instance. The\r\n// stdio entry today calls this; the HTTP transport added in v1.5\r\n// will call the same factory. Keep the function pure so tests can\r\n// drive it with an in-process transport.\r\nexport function createServer(config: Config): McpServer {\r\n const server = new McpServer({\r\n name: \"zaai-dev-mcp\",\r\n version: PKG_VERSION,\r\n });\r\n\r\n const captureStore = new HttpCaptureStore(config);\r\n\r\n server.registerTool(\r\n \"health\",\r\n {\r\n title: \"Health check\",\r\n description:\r\n \"Returns the Zaai Dev MCP server's status, version, and uptime. \" +\r\n \"Use this to confirm the server is running before relying on auth-\" +\r\n \"gated tools. No workspace access required. Useful as a smoke test \" +\r\n \"from the MCP Inspector before configuring credentials.\",\r\n inputSchema: healthInputSchema.shape,\r\n outputSchema: healthOutputSchema.shape,\r\n },\r\n async () => {\r\n const result = health();\r\n return {\r\n structuredContent: result,\r\n // Human-readable summary for clients that haven't adopted\r\n // structuredContent yet. Spec 2025-06-18 still requires both.\r\n content: [\r\n {\r\n type: \"text\",\r\n text:\r\n `Zaai Dev MCP ${result.version} · node ${result.node}` +\r\n ` · uptime ${result.uptimeSeconds}s`,\r\n },\r\n ],\r\n };\r\n },\r\n );\r\n\r\n server.registerTool(\r\n \"whoami\",\r\n {\r\n title: \"Who am I\",\r\n description:\r\n \"Returns the Zaai Dev workspace account this server is bound to: \" +\r\n \"user id, organisation id, project scope, and current credit balance. \" +\r\n \"Use this to confirm the configured ZAAI_API_TOKEN is valid before \" +\r\n \"calling any other workspace tool. Does NOT debit credits — the \" +\r\n \"workspace treats /me as a heartbeat, not user-facing work.\",\r\n inputSchema: whoamiInputSchema.shape,\r\n outputSchema: whoamiOutputSchema.shape,\r\n },\r\n async () => {\r\n try {\r\n const result = await whoami(config);\r\n return {\r\n structuredContent: result,\r\n content: [\r\n {\r\n type: \"text\",\r\n text: summariseWhoami(result),\r\n },\r\n ],\r\n };\r\n } catch (err) {\r\n return toolErrorResponse(err);\r\n }\r\n },\r\n );\r\n\r\n server.registerTool(\r\n \"list_captures\",\r\n {\r\n title: \"List captures\",\r\n description:\r\n \"List the user's Zaai Dev captures (newest first, paginated). \" +\r\n \"Use this to discover what's in the library before calling \" +\r\n \"get_capture for specifics. Each row carries id, type, title, \" +\r\n \"URL, capturedAt, tags, and a thumbnail URL — but NOT the full \" +\r\n \"HTML, computed style, or animation data. Use get_capture for \" +\r\n \"those. Costs 1 credit per call.\",\r\n inputSchema: listCapturesInputSchema.shape,\r\n outputSchema: listCapturesOutputSchema.shape,\r\n },\r\n async (args) => {\r\n try {\r\n const result = await listCaptures(captureStore, args);\r\n return {\r\n // Cast: SDK's structuredContent param wants\r\n // Record<string,unknown> but our typed DTO doesn't carry\r\n // an index signature. Runtime shape is identical.\r\n structuredContent: result as unknown as Record<string, unknown>,\r\n content: [\r\n {\r\n type: \"text\",\r\n text:\r\n result.captures.length === 0\r\n ? \"No captures yet. The user can push captures from the Zaai Dev Chrome extension.\"\r\n : `${result.captures.length} capture${result.captures.length === 1 ? \"\" : \"s\"}${result.nextCursor ? \" (more available — pass nextCursor for the next page)\" : \"\"}.`,\r\n },\r\n ],\r\n };\r\n } catch (err) {\r\n return toolErrorResponse(err);\r\n }\r\n },\r\n );\r\n\r\n server.registerTool(\r\n \"get_capture\",\r\n {\r\n title: \"Get capture\",\r\n description:\r\n \"Fetch the full payload for one capture by id. Includes everything \" +\r\n \"the extension extracted (HTML, computed CSS, palette, fonts, \" +\r\n \"animation data, media inventory, ancestry, page sections, manual \" +\r\n \"eyedropper picks) plus fresh-signed Supabase URLs for the \" +\r\n \"screenshot and thumbnail. Use list_captures or search_captures \" +\r\n \"first to discover ids. Costs 1 credit per call; the 404 path \" +\r\n \"(unknown id) doesn't bill.\",\r\n inputSchema: getCaptureInputSchema.shape,\r\n outputSchema: getCaptureOutputSchema.shape,\r\n },\r\n async (args) => {\r\n try {\r\n const result = await getCapture(captureStore, args);\r\n return {\r\n structuredContent: result as unknown as Record<string, unknown>,\r\n content: [\r\n {\r\n type: \"text\",\r\n text:\r\n `${result.type} capture \"${result.sourceTitle || result.sourceUrl}\"` +\r\n ` (${result.tags.length} tag${result.tags.length === 1 ? \"\" : \"s\"})` +\r\n (result.screenshotUrl ? \" — screenshot URL included.\" : \"\"),\r\n },\r\n ],\r\n };\r\n } catch (err) {\r\n return toolErrorResponse(err);\r\n }\r\n },\r\n );\r\n\r\n server.registerTool(\r\n \"search_captures\",\r\n {\r\n title: \"Search captures\",\r\n description:\r\n \"Find captures matching a text query (case-insensitive substring \" +\r\n \"match against source_title, source_url, and note). Returns the \" +\r\n \"same summary shape as list_captures. Use this when you have a \" +\r\n \"specific concept in mind ('hero', 'pricing table', 'stripe'); \" +\r\n \"use list_captures to browse instead. Costs 1 credit per call.\",\r\n inputSchema: searchCapturesInputSchema.shape,\r\n outputSchema: searchCapturesOutputSchema.shape,\r\n },\r\n async (args) => {\r\n try {\r\n const result = await searchCaptures(captureStore, args);\r\n return {\r\n structuredContent: result as unknown as Record<string, unknown>,\r\n content: [\r\n {\r\n type: \"text\",\r\n text:\r\n result.captures.length === 0\r\n ? `No captures match \"${args.q}\".`\r\n : `${result.captures.length} capture${result.captures.length === 1 ? \"\" : \"s\"} matching \"${args.q}\"${result.nextCursor ? \" (more available — pass nextCursor for the next page)\" : \"\"}.`,\r\n },\r\n ],\r\n };\r\n } catch (err) {\r\n return toolErrorResponse(err);\r\n }\r\n },\r\n );\r\n\r\n server.registerTool(\r\n \"get_palette\",\r\n {\r\n title: \"Get capture palette\",\r\n description:\r\n \"Returns just the colour palette for a capture. Page captures \" +\r\n \"carry a 6-colour extracted palette (`palette`); element + \" +\r\n \"composite captures carry the user's eyedropper picks \" +\r\n \"(`manualPalette`). Use this when the LLM needs colour context \" +\r\n \"without loading the full payload. Costs 1 credit.\",\r\n inputSchema: getPaletteInputSchema.shape,\r\n outputSchema: getPaletteOutputSchema.shape,\r\n },\r\n makeFocusedGetterHandler(captureStore, getPalette, \"palette\"),\r\n );\r\n\r\n server.registerTool(\r\n \"get_html\",\r\n {\r\n title: \"Get capture HTML\",\r\n description:\r\n \"Returns just the HTML for a capture. Page → full page HTML; \" +\r\n \"element → outerHTML of the picked element; composite → \" +\r\n \"concatenated outerHTMLs with `<!-- Element N: selector -->` \" +\r\n \"markers. Use this for component-extraction prompts ('rebuild \" +\r\n \"this in Tailwind'). Costs 1 credit. Page captures can be large \" +\r\n \"— budget your context accordingly.\",\r\n inputSchema: getHtmlInputSchema.shape,\r\n outputSchema: getHtmlOutputSchema.shape,\r\n },\r\n makeFocusedGetterHandler(captureStore, getHtml, \"html\"),\r\n );\r\n\r\n server.registerTool(\r\n \"get_animation\",\r\n {\r\n title: \"Get capture animation data\",\r\n description:\r\n \"Returns just the animation data. Page → pageAnimations \" +\r\n \"(keyframes vocabulary + detected motion libraries like Framer \" +\r\n \"Motion, GSAP, AOS, anime.js, Lenis, Locomotive). Element → \" +\r\n \"the element's CSS transitions, keyframes, hover/focus/active \" +\r\n \"state diffs, and per-element library hints. Composite → \" +\r\n \"per-element variants. Use this to give the LLM motion context \" +\r\n \"without loading HTML and computed style. Costs 1 credit.\",\r\n inputSchema: getAnimationInputSchema.shape,\r\n outputSchema: getAnimationOutputSchema.shape,\r\n },\r\n makeFocusedGetterHandler(captureStore, getAnimation, \"animation\"),\r\n );\r\n\r\n server.registerTool(\r\n \"get_media\",\r\n {\r\n title: \"Get capture media inventory\",\r\n description:\r\n \"Returns just the rich-media inventory for element + composite \" +\r\n \"captures: videos (src/poster/autoplay/loop), images (src/\" +\r\n \"srcset/alt), background-image URLs across the subtree, and \" +\r\n \"carousel containers (Swiper, Glide, Splide, Slick, Flickity, \" +\r\n \"Embla, `[data-carousel]`) with slide counts. Page captures \" +\r\n \"return media:null (page-level palette/fonts only). Costs 1 credit.\",\r\n inputSchema: getMediaInputSchema.shape,\r\n outputSchema: getMediaOutputSchema.shape,\r\n },\r\n makeFocusedGetterHandler(captureStore, getMedia, \"media\"),\r\n );\r\n\r\n registerCaptureResource(server, captureStore);\r\n\r\n log.info(\"server initialized\", {\r\n version: PKG_VERSION,\r\n tools: [\r\n \"health\",\r\n \"whoami\",\r\n \"list_captures\",\r\n \"search_captures\",\r\n \"get_capture\",\r\n \"get_palette\",\r\n \"get_html\",\r\n \"get_animation\",\r\n \"get_media\",\r\n ],\r\n resources: [\"zaai-capture://{id}\"],\r\n });\r\n return server;\r\n}\r\n\r\n// Shared handler factory for the four focused-getter tools — they\r\n// differ only in the `field` they pass to the store + the tool's\r\n// LLM-facing description, which is set at registerTool time.\r\nfunction makeFocusedGetterHandler(\r\n store: HttpCaptureStore,\r\n fn: (\r\n s: HttpCaptureStore,\r\n args: { id: string },\r\n ) => Promise<{ id: string; type: string; field: string; data: unknown }>,\r\n label: string,\r\n) {\r\n return async (args: { id: string }) => {\r\n try {\r\n const result = await fn(store, args);\r\n return {\r\n structuredContent: result as unknown as Record<string, unknown>,\r\n content: [\r\n {\r\n type: \"text\" as const,\r\n text: `${result.type} capture · ${label} slice for id ${result.id.slice(0, 8)}…`,\r\n },\r\n ],\r\n };\r\n } catch (err) {\r\n return toolErrorResponse(err);\r\n }\r\n };\r\n}\r\n\r\nfunction summariseWhoami(r: {\r\n userId: string;\r\n orgId: string;\r\n projectScope: string[] | null;\r\n balance: { totalBalance: number };\r\n mcpCallCost: number;\r\n}): string {\r\n const scope =\r\n r.projectScope === null\r\n ? \"all projects in org\"\r\n : `${r.projectScope.length} project${r.projectScope.length === 1 ? \"\" : \"s\"}`;\r\n return (\r\n `Zaai Dev workspace · user ${r.userId.slice(0, 8)}… · ` +\r\n `org ${r.orgId.slice(0, 8)}… · ${scope} · ` +\r\n `${r.balance.totalBalance} credits (${r.mcpCallCost}/call)`\r\n );\r\n}\r\n\r\n// Convert a workspace API failure into an MCP tool result the LLM\r\n// can read and act on. Returning isError:true is preferred over\r\n// throwing because the LLM sees the message and can fix the\r\n// underlying issue (e.g. \"ask the user to reconnect\").\r\nfunction toolErrorResponse(err: unknown): {\r\n isError: true;\r\n content: Array<{ type: \"text\"; text: string }>;\r\n} {\r\n if (err instanceof WorkspaceApiError) {\r\n switch (err.status) {\r\n case 401:\r\n return errorContent(\r\n \"Authentication failed. Your ZAAI_API_TOKEN is invalid, expired, \" +\r\n \"or revoked. Mint a new MCP token at \" +\r\n \"https://www.zaaistudio.com/dev/settings/tokens and update \" +\r\n \"your MCP client config.\",\r\n );\r\n case 402:\r\n return errorContent(\r\n \"Out of credits. Top up at \" +\r\n \"https://www.zaaistudio.com/dev/settings/billing.\",\r\n );\r\n case 0:\r\n return errorContent(\r\n `Network failure reaching the Zaai Dev workspace: ${describe(err.body)}. ` +\r\n \"Check your connection or ZAAI_API_URL setting.\",\r\n );\r\n default:\r\n return errorContent(\r\n `Workspace returned ${err.status} for ${err.path}: ${describe(err.body)}.`,\r\n );\r\n }\r\n }\r\n return errorContent(`Unexpected MCP tool failure: ${describe(err)}`);\r\n}\r\n\r\nfunction errorContent(text: string): {\r\n isError: true;\r\n content: Array<{ type: \"text\"; text: string }>;\r\n} {\r\n return { isError: true, content: [{ type: \"text\", text }] };\r\n}\r\n\r\nfunction describe(value: unknown): string {\r\n if (value instanceof Error) return value.message;\r\n if (typeof value === \"string\") return value;\r\n try {\r\n return JSON.stringify(value);\r\n } catch {\r\n return String(value);\r\n }\r\n}\r\n","import { z } from \"zod\";\r\nimport { PKG_VERSION } from \"../version.js\";\r\n\r\n// Health-check tool. No auth, no workspace access, no Supabase round\r\n// trip — useful for two cases:\r\n// 1. The MCP Inspector smoke test before configuring credentials\r\n// (M-MCP1 acceptance criteria)\r\n// 2. A future MCP client's own up/down probe — clients can call\r\n// health() to confirm the server is reachable without needing\r\n// to call something user-scoped first.\r\n//\r\n// Returns { ok, version, node, uptimeSeconds } — small, fixed shape.\r\n// Updates here should be backward-compatible (additive only) because\r\n// clients depending on the probe shouldn't break across versions.\r\n\r\nexport const healthInputSchema = z.object({});\r\n\r\nexport const healthOutputSchema = z.object({\r\n ok: z.boolean().describe(\"Always true — health is the server admitting it answered.\"),\r\n version: z.string().describe(\"The MCP server's package version.\"),\r\n node: z.string().describe(\"The Node.js runtime version the server is running on.\"),\r\n uptimeSeconds: z\r\n .number()\r\n .describe(\"Seconds since this server process started.\"),\r\n});\r\n\r\nexport type HealthOutput = z.infer<typeof healthOutputSchema>;\r\n\r\nexport function health(): HealthOutput {\r\n return {\r\n ok: true,\r\n version: PKG_VERSION,\r\n node: process.versions.node,\r\n uptimeSeconds: Math.round(process.uptime()),\r\n };\r\n}\r\n","// Build-time-substituted package version. tsup's `define` config in\r\n// tsup.config.ts replaces `__PKG_VERSION__` at bundle time with the\r\n// version from package.json. The fallback is for `tsc --noEmit` and\r\n// the dev-time editor — those don't run through tsup, so they see\r\n// the raw token. Runtime always sees the real version.\r\ndeclare const __PKG_VERSION__: string;\r\n\r\nexport const PKG_VERSION: string =\r\n typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0-dev\";\r\n","import { z } from \"zod\";\r\nimport type { Config } from \"../config.js\";\r\nimport { mcpApiFetch } from \"../util/api.js\";\r\n\r\n// First auth-gated tool. Calls GET /api/mcp/me — verifies the\r\n// configured ZAAI_API_TOKEN works against the workspace and\r\n// surfaces the user/org/balance so the LLM (and the human in the\r\n// MCP Inspector) can confirm what account it's bound to.\r\n//\r\n// Cheap, doesn't bill — `/me` is the protocol heartbeat per\r\n// docs/zaai-dev-mcp-plan.md § M-MCP2 in the extension repo.\r\n\r\nexport const whoamiInputSchema = z.object({});\r\n\r\nexport const whoamiOutputSchema = z.object({\r\n userId: z.string().describe(\"Workspace user id this token belongs to.\"),\r\n orgId: z.string().describe(\"Organisation id the token is scoped to.\"),\r\n projectScope: z\r\n .array(z.string())\r\n .nullable()\r\n .describe(\r\n \"List of project ids the token can access, or null when scoped to every project in the org.\",\r\n ),\r\n balance: z\r\n .object({\r\n currentBalance: z\r\n .number()\r\n .describe(\"Top-up credits (never expire).\"),\r\n monthlyGrantRemaining: z\r\n .number()\r\n .describe(\"Monthly grant remaining (resets on the 1st).\"),\r\n totalBalance: z\r\n .number()\r\n .describe(\"Sum of the two — what's available right now.\"),\r\n })\r\n .describe(\"Credit balance available for MCP calls.\"),\r\n mcpCallCost: z\r\n .number()\r\n .describe(\"Credits debited per successful MCP tool call.\"),\r\n});\r\n\r\nexport type WhoamiOutput = z.infer<typeof whoamiOutputSchema>;\r\n\r\nexport async function whoami(config: Config): Promise<WhoamiOutput> {\r\n return mcpApiFetch<WhoamiOutput>(config, \"/api/mcp/me\");\r\n}\r\n","// Bearer-header builder. Single place to format `Authorization`\r\n// so every HTTP call has identical shape — useful when grepping\r\n// for \"who sets the bearer\" later.\r\n\r\nexport function bearerHeader(token: string): Record<string, string> {\r\n return { Authorization: `Bearer ${token}` };\r\n}\r\n","import type { Config } from \"../config.js\";\r\nimport { bearerHeader } from \"./auth.js\";\r\n\r\n// Structured failure from a workspace API call. Tools branch on\r\n// `.status` to render LLM-friendly recovery hints (401 → \"reauth\",\r\n// 402 → \"out of credits\", 404 → \"not found\", 5xx → \"try again\").\r\n\r\nexport class WorkspaceApiError extends Error {\r\n constructor(\r\n public readonly status: number,\r\n public readonly body: unknown,\r\n public readonly path: string,\r\n ) {\r\n super(`${path} → ${status}`);\r\n this.name = \"WorkspaceApiError\";\r\n }\r\n}\r\n\r\n// Typed HTTPS wrapper. Prepends the configured workspace base URL,\r\n// adds the bearer header, parses JSON, throws WorkspaceApiError on\r\n// non-2xx so callers don't have to inspect Response objects.\r\n//\r\n// Default method is GET (set `init.method` to override). Caller is\r\n// responsible for JSON-encoding any body — pass via init.body as a\r\n// string and set init.headers['Content-Type'].\r\n\r\nexport async function mcpApiFetch<T>(\r\n config: Config,\r\n path: string,\r\n init: RequestInit = {},\r\n): Promise<T> {\r\n const url = `${config.apiUrl}${path}`;\r\n\r\n let res: Response;\r\n try {\r\n res = await fetch(url, {\r\n ...init,\r\n headers: {\r\n ...bearerHeader(config.apiToken),\r\n ...(init.headers ?? {}),\r\n },\r\n });\r\n } catch (err) {\r\n // Network failure (DNS, offline, TLS). Convert to the same\r\n // structured error so tool handlers have one error type to\r\n // branch on rather than two.\r\n throw new WorkspaceApiError(0, { message: String(err) }, path);\r\n }\r\n\r\n if (!res.ok) {\r\n const body = await res\r\n .json()\r\n .catch(() => ({ error: `Non-JSON ${res.status} response` }));\r\n throw new WorkspaceApiError(res.status, body, path);\r\n }\r\n\r\n return (await res.json()) as T;\r\n}\r\n","import { z } from \"zod\";\r\nimport type { CaptureStore, ListCapturesResult } from \"../store/types.js\";\r\n\r\n// Discovery tool. The LLM uses this to enumerate the user's library\r\n// before fetching specifics with get_capture. Paginated; returns\r\n// summaries only (no HTML / computed style / full payload — those\r\n// land via get_capture or the focused getters in M-MCP4).\r\n//\r\n// Charges 1 credit per successful call (workspace-side enforcement).\r\n\r\nexport const listCapturesInputSchema = z.object({\r\n q: z\r\n .string()\r\n .optional()\r\n .describe(\r\n \"Optional text filter applied to source_title, source_url, and note (case-insensitive substring match). Leave blank to list all.\",\r\n ),\r\n cursor: z\r\n .string()\r\n .optional()\r\n .describe(\r\n \"Opaque pagination cursor from a previous response's nextCursor field. Omit on the first page.\",\r\n ),\r\n limit: z\r\n .number()\r\n .int()\r\n .min(1)\r\n .max(50)\r\n .optional()\r\n .describe(\"Max captures to return. Default 20, max 50.\"),\r\n});\r\n\r\nexport const listCapturesOutputSchema = z.object({\r\n captures: z.array(\r\n z.object({\r\n id: z.string().describe(\"Capture UUID. Pass to get_capture for the full payload.\"),\r\n type: z\r\n .enum([\"page\", \"element\", \"composite\"])\r\n .describe(\r\n \"page = full-page screenshot + DOM context; element = a single picked element; composite = multiple elements stacked.\",\r\n ),\r\n sourceTitle: z.string(),\r\n sourceUrl: z.string(),\r\n capturedAt: z\r\n .string()\r\n .describe(\"ISO 8601 timestamp from the extension at capture time.\"),\r\n tags: z.array(z.string()),\r\n thumbnailUrl: z\r\n .string()\r\n .nullable()\r\n .describe(\r\n \"Signed Supabase Storage URL (1h TTL). Fetch fresh by re-calling list_captures or get_capture.\",\r\n ),\r\n }),\r\n ),\r\n nextCursor: z\r\n .string()\r\n .optional()\r\n .describe(\r\n \"When present, pass back as `cursor` to fetch the next page. Absent = no more rows.\",\r\n ),\r\n});\r\n\r\nexport type ListCapturesArgs = z.infer<typeof listCapturesInputSchema>;\r\n\r\nexport async function listCaptures(\r\n store: CaptureStore,\r\n args: ListCapturesArgs,\r\n): Promise<ListCapturesResult> {\r\n return store.list({\r\n q: args.q,\r\n cursor: args.cursor,\r\n limit: args.limit,\r\n });\r\n}\r\n","import { z } from \"zod\";\r\nimport type { CaptureFull, CaptureStore } from \"../store/types.js\";\r\n\r\n// Returns the full payload for one capture id — everything the\r\n// extension extracted (HTML, computed style, palette, fonts,\r\n// animation, media, ancestry, page sections, eyedropper picks) plus\r\n// fresh-signed Supabase URLs for the screenshot, thumbnail, and\r\n// (page captures only) the full-page stitch.\r\n//\r\n// Charges 1 credit per successful call. 404 (CaptureNotFound)\r\n// doesn't bill but is audited.\r\n\r\nconst UUID_RE =\r\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\r\n\r\nexport const getCaptureInputSchema = z.object({\r\n id: z\r\n .string()\r\n .regex(UUID_RE, \"id must be a UUID\")\r\n .describe(\"Capture id from list_captures.\"),\r\n});\r\n\r\n// Output schema is intentionally loose. The `payload` field's shape\r\n// is the extension's Capture union (page / element / composite),\r\n// which is too rich to declare fully here without dragging the\r\n// whole type tree from @zaai/types. Clients that need typed access\r\n// can navigate via z.unknown() + their own narrowing.\r\nexport const getCaptureOutputSchema = z\r\n .object({\r\n id: z.string(),\r\n type: z.enum([\"page\", \"element\", \"composite\"]),\r\n projectId: z.string(),\r\n sourceTitle: z.string(),\r\n sourceUrl: z.string(),\r\n capturedAt: z.string(),\r\n note: z.string().nullable(),\r\n tags: z.array(z.string()),\r\n screenshotUrl: z\r\n .string()\r\n .nullable()\r\n .describe(\"Signed PNG URL, 1h TTL.\"),\r\n thumbnailUrl: z.string().nullable(),\r\n fullPageScreenshotUrl: z\r\n .string()\r\n .nullable()\r\n .describe(\r\n \"Page captures with the M9 stitch get a full-page PNG too. Null for element + composite + page captures without a stitch.\",\r\n ),\r\n payload: z\r\n .unknown()\r\n .describe(\r\n \"Full extension-side Capture JSON. Shape depends on `type` — see https://github.com/POLONIBOI/ZAAI_STUDIO-ext/blob/main/src/shared/types.ts for the union.\",\r\n ),\r\n })\r\n .passthrough();\r\n\r\nexport type GetCaptureArgs = z.infer<typeof getCaptureInputSchema>;\r\n\r\nexport async function getCapture(\r\n store: CaptureStore,\r\n args: GetCaptureArgs,\r\n): Promise<CaptureFull> {\r\n return store.get(args.id);\r\n}\r\n","import { z } from \"zod\";\r\nimport type { CaptureStore, ListCapturesResult } from \"../store/types.js\";\r\n\r\n// Search variant of list_captures with `q` REQUIRED. Same workspace\r\n// endpoint, same output shape — the win is a tool description tuned\r\n// for \"I have a specific concept to find\" so the LLM picks it over\r\n// list_captures when the user's intent is targeted retrieval.\r\n\r\nexport const searchCapturesInputSchema = z.object({\r\n q: z\r\n .string()\r\n .min(1)\r\n .describe(\r\n \"Case-insensitive substring match against source_title, source_url, and note. Required — use list_captures if you don't have a query.\",\r\n ),\r\n cursor: z.string().optional(),\r\n limit: z.number().int().min(1).max(50).optional(),\r\n});\r\n\r\nexport const searchCapturesOutputSchema = z.object({\r\n captures: z.array(\r\n z.object({\r\n id: z.string(),\r\n type: z.enum([\"page\", \"element\", \"composite\"]),\r\n sourceTitle: z.string(),\r\n sourceUrl: z.string(),\r\n capturedAt: z.string(),\r\n tags: z.array(z.string()),\r\n thumbnailUrl: z.string().nullable(),\r\n }),\r\n ),\r\n nextCursor: z.string().optional(),\r\n});\r\n\r\nexport type SearchCapturesArgs = z.infer<typeof searchCapturesInputSchema>;\r\n\r\nexport async function searchCaptures(\r\n store: CaptureStore,\r\n args: SearchCapturesArgs,\r\n): Promise<ListCapturesResult> {\r\n return store.list({ q: args.q, cursor: args.cursor, limit: args.limit });\r\n}\r\n","// Shared Zod shapes for the four focused-getter tools (get_palette,\r\n// get_html, get_animation, get_media). The tools differ only in\r\n// which `field` they pass through — schemas are identical otherwise,\r\n// so they live here to avoid copy-paste drift.\r\n\r\nimport { z } from \"zod\";\r\n\r\nconst UUID_RE =\r\n /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\r\n\r\nexport const focusedGetterInputSchema = z.object({\r\n id: z\r\n .string()\r\n .regex(UUID_RE, \"id must be a UUID\")\r\n .describe(\r\n \"Capture id from list_captures or search_captures. UUID format.\",\r\n ),\r\n});\r\n\r\n// Shape of `data` varies per (capture.type, field) — see workspace\r\n// route lib/mcp/captures.ts → extractField() for the per-combination\r\n// shape. Using z.unknown() keeps the schema flexible; the LLM reads\r\n// structuredContent and the workspace docs are the source of truth.\r\nexport const focusedGetterOutputSchema = z\r\n .object({\r\n id: z.string(),\r\n type: z.enum([\"page\", \"element\", \"composite\"]),\r\n field: z.string(),\r\n data: z.unknown(),\r\n })\r\n .passthrough();\r\n\r\nexport type FocusedGetterArgs = z.infer<typeof focusedGetterInputSchema>;\r\n","import type { CaptureFieldResult, CaptureStore } from \"../store/types.js\";\r\nimport {\r\n focusedGetterInputSchema,\r\n focusedGetterOutputSchema,\r\n type FocusedGetterArgs,\r\n} from \"./_focused-getter.js\";\r\n\r\nexport {\r\n focusedGetterInputSchema as getPaletteInputSchema,\r\n focusedGetterOutputSchema as getPaletteOutputSchema,\r\n};\r\n\r\n// Returns the capture's colour palette.\r\n// page → 6 dominant hex colours extracted at capture time\r\n// element → manualPalette: eyedropper picks (Alt-held in inspect)\r\n// composite → primary slot's manualPalette\r\nexport async function getPalette(\r\n store: CaptureStore,\r\n args: FocusedGetterArgs,\r\n): Promise<CaptureFieldResult> {\r\n return store.getField(args.id, \"palette\");\r\n}\r\n","import type { CaptureFieldResult, CaptureStore } from \"../store/types.js\";\r\nimport {\r\n focusedGetterInputSchema,\r\n focusedGetterOutputSchema,\r\n type FocusedGetterArgs,\r\n} from \"./_focused-getter.js\";\r\n\r\nexport {\r\n focusedGetterInputSchema as getHtmlInputSchema,\r\n focusedGetterOutputSchema as getHtmlOutputSchema,\r\n};\r\n\r\n// Returns the capture's HTML.\r\n// page → full page HTML (large — wrap consumption appropriately)\r\n// element → outerHTML of the picked element\r\n// composite → outerHTML of each element joined with\r\n// `<!-- Element N: selector -->` markers\r\nexport async function getHtml(\r\n store: CaptureStore,\r\n args: FocusedGetterArgs,\r\n): Promise<CaptureFieldResult> {\r\n return store.getField(args.id, \"html\");\r\n}\r\n","import type { CaptureFieldResult, CaptureStore } from \"../store/types.js\";\r\nimport {\r\n focusedGetterInputSchema,\r\n focusedGetterOutputSchema,\r\n type FocusedGetterArgs,\r\n} from \"./_focused-getter.js\";\r\n\r\nexport {\r\n focusedGetterInputSchema as getAnimationInputSchema,\r\n focusedGetterOutputSchema as getAnimationOutputSchema,\r\n};\r\n\r\n// Returns the capture's animation data.\r\n// page → pageAnimations: keyframes vocabulary + detected\r\n// motion libraries (Framer Motion, GSAP, AOS, etc.)\r\n// element → element.animation (CSS transitions, keyframes,\r\n// hover/focus/active state diffs, live-observed flag)\r\n// + libraryHints (per-element data-attribute scan)\r\n// composite → per-element animation + libraryHints array\r\nexport async function getAnimation(\r\n store: CaptureStore,\r\n args: FocusedGetterArgs,\r\n): Promise<CaptureFieldResult> {\r\n return store.getField(args.id, \"animation\");\r\n}\r\n","import type { CaptureFieldResult, CaptureStore } from \"../store/types.js\";\r\nimport {\r\n focusedGetterInputSchema,\r\n focusedGetterOutputSchema,\r\n type FocusedGetterArgs,\r\n} from \"./_focused-getter.js\";\r\n\r\nexport {\r\n focusedGetterInputSchema as getMediaInputSchema,\r\n focusedGetterOutputSchema as getMediaOutputSchema,\r\n};\r\n\r\n// Returns the capture's rich-media inventory.\r\n// page → null (page captures don't carry per-element media)\r\n// element → videos (src/poster/autoplay/loop), images (src/srcset/\r\n// alt), backgrounds (computed background-image URLs),\r\n// carousels (Swiper/Glide/Splide/Slick/Flickity/Embla\r\n// with slide counts)\r\n// composite → per-element media object\r\nexport async function getMedia(\r\n store: CaptureStore,\r\n args: FocusedGetterArgs,\r\n): Promise<CaptureFieldResult> {\r\n return store.getField(args.id, \"media\");\r\n}\r\n","import type { Config } from \"../config.js\";\r\nimport { mcpApiFetch } from \"../util/api.js\";\r\nimport type {\r\n CaptureField,\r\n CaptureFieldResult,\r\n CaptureFull,\r\n CaptureStore,\r\n ListCapturesArgs,\r\n ListCapturesResult,\r\n} from \"./types.js\";\r\n\r\n// HTTP shim against the workspace's /api/mcp/captures endpoints.\r\n// Each method is one mcpApiFetch call — bearer header + WorkspaceApiError\r\n// wrapping handled there. The thinness is the point: every store\r\n// method maps 1:1 to a workspace endpoint, so the surface is grep-able\r\n// and easy to mock for tests.\r\n\r\nexport class HttpCaptureStore implements CaptureStore {\r\n constructor(private readonly config: Config) {}\r\n\r\n async list(args: ListCapturesArgs): Promise<ListCapturesResult> {\r\n const sp = new URLSearchParams();\r\n if (args.q) sp.set(\"q\", args.q);\r\n if (args.cursor) sp.set(\"cursor\", args.cursor);\r\n if (args.limit !== undefined) sp.set(\"limit\", String(args.limit));\r\n const qs = sp.toString();\r\n return mcpApiFetch<ListCapturesResult>(\r\n this.config,\r\n `/api/mcp/captures${qs ? `?${qs}` : \"\"}`,\r\n );\r\n }\r\n\r\n async get(id: string): Promise<CaptureFull> {\r\n return mcpApiFetch<CaptureFull>(\r\n this.config,\r\n `/api/mcp/captures/${encodeURIComponent(id)}`,\r\n );\r\n }\r\n\r\n async getField(\r\n id: string,\r\n field: CaptureField,\r\n ): Promise<CaptureFieldResult> {\r\n return mcpApiFetch<CaptureFieldResult>(\r\n this.config,\r\n `/api/mcp/captures/${encodeURIComponent(id)}/${field}`,\r\n );\r\n }\r\n}\r\n","import {\r\n McpServer,\r\n ResourceTemplate,\r\n} from \"@modelcontextprotocol/sdk/server/mcp.js\";\r\nimport type { CaptureStore } from \"../store/types.js\";\r\n\r\n// Resource: zaai-capture://{id}\r\n//\r\n// Lets the user attach a specific capture to a Claude Desktop /\r\n// Cursor conversation via the resource picker without invoking a\r\n// tool first. The body is the same JSON the get_capture tool\r\n// returns, served as application/json.\r\n//\r\n// Resource reads bypass the credit charge — they're user-driven\r\n// attachments, not LLM-driven tool calls. Workspace currently\r\n// routes both through GET /api/mcp/captures/[id] which DOES debit.\r\n// For v1 we accept the charge: it's still bounded by the user's own\r\n// attachment behaviour and folds into the same audit trail. If\r\n// resource reads ever outpace tool calls we can split into a\r\n// non-billing endpoint.\r\n\r\nexport function registerCaptureResource(\r\n server: McpServer,\r\n store: CaptureStore,\r\n): void {\r\n server.registerResource(\r\n \"capture\",\r\n new ResourceTemplate(\"zaai-capture://{id}\", {\r\n list: undefined,\r\n }),\r\n {\r\n title: \"Zaai Dev capture\",\r\n description:\r\n \"Attach one capture from the user's Zaai Dev library as JSON. \" +\r\n \"URI: zaai-capture://<capture-id>. The id comes from list_captures.\",\r\n mimeType: \"application/json\",\r\n },\r\n async (uri, variables) => {\r\n const rawId = variables.id;\r\n const id = Array.isArray(rawId) ? rawId[0] : rawId;\r\n const capture = await store.get(id);\r\n return {\r\n contents: [\r\n {\r\n uri: uri.href,\r\n mimeType: \"application/json\",\r\n text: JSON.stringify(capture, null, 2),\r\n },\r\n ],\r\n };\r\n },\r\n );\r\n}\r\n","// stderr-only logger.\r\n//\r\n// MCP servers using the stdio transport MUST NOT write anything to\r\n// stdout other than the JSON-RPC framing produced by the SDK. A\r\n// stray byte — a banner, console.log, unhandled rejection, even a\r\n// trailing newline from a misconfigured shebang — closes the\r\n// transport with `-32000 connection closed`. This is the #1\r\n// production failure mode for MCP servers (research memory\r\n// `project_mcp_build_plan` § Don't-make-these-mistakes #1).\r\n//\r\n// Use this module instead of console.* anywhere in src/.\r\n\r\nconst PREFIX = \"[zaai-mcp]\";\r\n\r\nfunction write(level: string, message: string, meta?: unknown): void {\r\n let line = `${PREFIX} ${level} ${message}`;\r\n if (meta !== undefined) {\r\n line += \" \" + safeStringify(meta);\r\n }\r\n process.stderr.write(line + \"\\n\");\r\n}\r\n\r\nfunction safeStringify(value: unknown): string {\r\n try {\r\n if (value instanceof Error) {\r\n return value.stack ?? `${value.name}: ${value.message}`;\r\n }\r\n return JSON.stringify(value);\r\n } catch {\r\n return String(value);\r\n }\r\n}\r\n\r\nexport function info(message: string, meta?: unknown): void {\r\n write(\"INFO\", message, meta);\r\n}\r\n\r\nexport function warn(message: string, meta?: unknown): void {\r\n write(\"WARN\", message, meta);\r\n}\r\n\r\nexport function error(message: string, meta?: unknown): void {\r\n write(\"ERROR\", message, meta);\r\n}\r\n","import * as log from \"./log.js\";\r\n\r\n// Runtime config — parsed from env vars at startup. The MCP server\r\n// fails fast in src/bin/stdio.ts if required values are missing,\r\n// because launching without them produces opaque downstream errors\r\n// (the first tool call 401s, and the user has no signal why).\r\n//\r\n// Env vars are the only config primitive every MCP client supports\r\n// uniformly. Claude Desktop / Cursor / Claude Code all let users\r\n// set them in their per-server config.\r\n\r\nexport interface Config {\r\n /** Bearer token. Must start with `zaai_mcp_`. */\r\n apiToken: string;\r\n /** Base URL of the workspace API (no trailing slash). */\r\n apiUrl: string;\r\n}\r\n\r\nconst DEFAULT_API_URL = \"https://www.zaaistudio.com\";\r\n\r\nexport class ConfigError extends Error {\r\n constructor(message: string) {\r\n super(message);\r\n this.name = \"ConfigError\";\r\n }\r\n}\r\n\r\nexport function loadConfig(): Config {\r\n const apiToken = process.env.ZAAI_API_TOKEN;\r\n if (!apiToken) {\r\n throw new ConfigError(\r\n \"ZAAI_API_TOKEN is not set. Mint a token at \" +\r\n \"https://www.zaaistudio.com/dev/settings/tokens (kind = mcp) \" +\r\n \"and pass it via the env in your MCP client config.\",\r\n );\r\n }\r\n if (!apiToken.startsWith(\"zaai_mcp_\")) {\r\n throw new ConfigError(\r\n \"ZAAI_API_TOKEN must start with 'zaai_mcp_'. Did you paste an \" +\r\n \"extension token (zaai_ext_*) by accident? Issue an AI tool \" +\r\n \"token at https://www.zaaistudio.com/dev/settings/tokens.\",\r\n );\r\n }\r\n\r\n const rawUrl = process.env.ZAAI_API_URL ?? DEFAULT_API_URL;\r\n const apiUrl = rawUrl.replace(/\\/+$/, \"\");\r\n\r\n log.info(\"config loaded\", { apiUrl });\r\n return { apiToken, apiUrl };\r\n}\r\n"],"mappings":"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","names":["McpServer","z","z","z","z","z","z","UUID_RE","McpServer"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@zaai-dev/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Zaai Dev MCP server — exposes your captures (and, in v1.5, your brand brief) to MCP-compatible AI tools.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Zaai Studio",
8
+ "homepage": "https://github.com/POLONIBOI/ZAAI_dev_mcp",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/POLONIBOI/ZAAI_dev_mcp.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/POLONIBOI/ZAAI_dev_mcp/issues"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "claude",
20
+ "cursor",
21
+ "zaai",
22
+ "design",
23
+ "captures"
24
+ ],
25
+ "bin": {
26
+ "zaai-dev-mcp": "./dist/bin/stdio.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.0.0",
38
+ "zod": "^4.4.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.0.0",
42
+ "tsup": "^8.3.5",
43
+ "typescript": "^5.6.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsup --watch",
51
+ "typecheck": "tsc --noEmit",
52
+ "inspector": "npx -y @modelcontextprotocol/inspector node dist/bin/stdio.js"
53
+ }
54
+ }