@zaai-dev/mcp 0.1.0 → 0.3.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.
@@ -0,0 +1,1097 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/http.ts
4
+ import { createServer as createHttpServer } from "http";
5
+ import { randomUUID } from "crypto";
6
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
+
8
+ // src/server.ts
9
+ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+
11
+ // src/tools/health.ts
12
+ import { z } from "zod";
13
+
14
+ // src/version.ts
15
+ var PKG_VERSION = true ? "0.3.0" : "0.0.0-dev";
16
+
17
+ // src/tools/health.ts
18
+ var healthInputSchema = z.object({});
19
+ var healthOutputSchema = z.object({
20
+ ok: z.boolean().describe("Always true \u2014 health is the server admitting it answered."),
21
+ version: z.string().describe("The MCP server's package version."),
22
+ node: z.string().describe("The Node.js runtime version the server is running on."),
23
+ uptimeSeconds: z.number().describe("Seconds since this server process started.")
24
+ });
25
+ function health() {
26
+ return {
27
+ ok: true,
28
+ version: PKG_VERSION,
29
+ node: process.versions.node,
30
+ uptimeSeconds: Math.round(process.uptime())
31
+ };
32
+ }
33
+
34
+ // src/tools/whoami.ts
35
+ import { z as z2 } from "zod";
36
+
37
+ // src/util/auth.ts
38
+ function bearerHeader(token) {
39
+ return { Authorization: `Bearer ${token}` };
40
+ }
41
+
42
+ // src/util/api.ts
43
+ var WorkspaceApiError = class extends Error {
44
+ constructor(status, body, path) {
45
+ super(`${path} \u2192 ${status}`);
46
+ this.status = status;
47
+ this.body = body;
48
+ this.path = path;
49
+ this.name = "WorkspaceApiError";
50
+ }
51
+ status;
52
+ body;
53
+ path;
54
+ };
55
+ var MAX_ATTEMPTS = 3;
56
+ var BASE_BACKOFF_MS = 250;
57
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
58
+ async function mcpApiFetch(config, path, init = {}) {
59
+ const url = `${config.apiUrl}${path}`;
60
+ let lastErr = null;
61
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
62
+ let res;
63
+ try {
64
+ res = await fetch(url, {
65
+ ...init,
66
+ headers: {
67
+ ...bearerHeader(config.apiToken),
68
+ ...init.headers ?? {}
69
+ }
70
+ });
71
+ } catch (err2) {
72
+ lastErr = new WorkspaceApiError(0, { message: String(err2) }, path);
73
+ if (attempt < MAX_ATTEMPTS) {
74
+ await sleep(BASE_BACKOFF_MS * 2 ** (attempt - 1));
75
+ continue;
76
+ }
77
+ throw lastErr;
78
+ }
79
+ if (res.ok) {
80
+ return await res.json();
81
+ }
82
+ const body = await res.json().catch(() => ({ error: `Non-JSON ${res.status} response` }));
83
+ const err = new WorkspaceApiError(res.status, body, path);
84
+ if (RETRYABLE_STATUSES.has(res.status) || res.status >= 502 && res.status <= 504) {
85
+ lastErr = err;
86
+ if (attempt < MAX_ATTEMPTS) {
87
+ await sleep(BASE_BACKOFF_MS * 2 ** (attempt - 1));
88
+ continue;
89
+ }
90
+ }
91
+ throw err;
92
+ }
93
+ throw lastErr ?? new WorkspaceApiError(0, { message: "no attempts" }, path);
94
+ }
95
+ function sleep(ms) {
96
+ return new Promise((resolve) => setTimeout(resolve, ms));
97
+ }
98
+
99
+ // src/tools/whoami.ts
100
+ var whoamiInputSchema = z2.object({});
101
+ var whoamiOutputSchema = z2.object({
102
+ userId: z2.string().describe("Workspace user id this token belongs to."),
103
+ orgId: z2.string().describe("Organisation id the token is scoped to."),
104
+ projectScope: z2.array(z2.string()).nullable().describe(
105
+ "List of project ids the token can access, or null when scoped to every project in the org."
106
+ ),
107
+ balance: z2.object({
108
+ currentBalance: z2.number().describe("Top-up credits (never expire)."),
109
+ monthlyGrantRemaining: z2.number().describe("Monthly grant remaining (resets on the 1st)."),
110
+ totalBalance: z2.number().describe("Sum of the two \u2014 what's available right now.")
111
+ }).describe("Credit balance available for MCP calls."),
112
+ mcpCallCost: z2.number().describe("Credits debited per successful MCP tool call.")
113
+ });
114
+ async function whoami(config) {
115
+ return mcpApiFetch(config, "/api/mcp/me");
116
+ }
117
+
118
+ // src/tools/list-captures.ts
119
+ import { z as z3 } from "zod";
120
+ var listCapturesInputSchema = z3.object({
121
+ q: z3.string().optional().describe(
122
+ "Optional text filter applied to source_title, source_url, and note (case-insensitive substring match). Leave blank to list all."
123
+ ),
124
+ cursor: z3.string().optional().describe(
125
+ "Opaque pagination cursor from a previous response's nextCursor field. Omit on the first page."
126
+ ),
127
+ limit: z3.number().int().min(1).max(50).optional().describe("Max captures to return. Default 20, max 50.")
128
+ });
129
+ var listCapturesOutputSchema = z3.object({
130
+ captures: z3.array(
131
+ z3.object({
132
+ id: z3.string().describe("Capture UUID. Pass to get_capture for the full payload."),
133
+ type: z3.enum(["page", "element", "composite"]).describe(
134
+ "page = full-page screenshot + DOM context; element = a single picked element; composite = multiple elements stacked."
135
+ ),
136
+ sourceTitle: z3.string(),
137
+ sourceUrl: z3.string(),
138
+ capturedAt: z3.string().describe("ISO 8601 timestamp from the extension at capture time."),
139
+ tags: z3.array(z3.string()),
140
+ thumbnailUrl: z3.string().nullable().describe(
141
+ "Signed Supabase Storage URL (1h TTL). Fetch fresh by re-calling list_captures or get_capture."
142
+ )
143
+ })
144
+ ),
145
+ nextCursor: z3.string().optional().describe(
146
+ "When present, pass back as `cursor` to fetch the next page. Absent = no more rows."
147
+ )
148
+ });
149
+ async function listCaptures(store, args) {
150
+ return store.list({
151
+ q: args.q,
152
+ cursor: args.cursor,
153
+ limit: args.limit
154
+ });
155
+ }
156
+
157
+ // src/tools/get-capture.ts
158
+ import { z as z4 } from "zod";
159
+ 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;
160
+ var getCaptureInputSchema = z4.object({
161
+ id: z4.string().regex(UUID_RE, "id must be a UUID").describe("Capture id from list_captures.")
162
+ });
163
+ var getCaptureOutputSchema = z4.object({
164
+ id: z4.string(),
165
+ type: z4.enum(["page", "element", "composite"]),
166
+ projectId: z4.string(),
167
+ sourceTitle: z4.string(),
168
+ sourceUrl: z4.string(),
169
+ capturedAt: z4.string(),
170
+ note: z4.string().nullable(),
171
+ tags: z4.array(z4.string()),
172
+ screenshotUrl: z4.string().nullable().describe("Signed PNG URL, 1h TTL."),
173
+ thumbnailUrl: z4.string().nullable(),
174
+ fullPageScreenshotUrl: z4.string().nullable().describe(
175
+ "Page captures with the M9 stitch get a full-page PNG too. Null for element + composite + page captures without a stitch."
176
+ ),
177
+ payload: z4.unknown().describe(
178
+ "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."
179
+ )
180
+ }).passthrough();
181
+ async function getCapture(store, args) {
182
+ return store.get(args.id);
183
+ }
184
+
185
+ // src/tools/search-captures.ts
186
+ import { z as z5 } from "zod";
187
+ var searchCapturesInputSchema = z5.object({
188
+ q: z5.string().min(1).describe(
189
+ "Case-insensitive substring match against source_title, source_url, and note. Required \u2014 use list_captures if you don't have a query."
190
+ ),
191
+ cursor: z5.string().optional(),
192
+ limit: z5.number().int().min(1).max(50).optional()
193
+ });
194
+ var searchCapturesOutputSchema = z5.object({
195
+ captures: z5.array(
196
+ z5.object({
197
+ id: z5.string(),
198
+ type: z5.enum(["page", "element", "composite"]),
199
+ sourceTitle: z5.string(),
200
+ sourceUrl: z5.string(),
201
+ capturedAt: z5.string(),
202
+ tags: z5.array(z5.string()),
203
+ thumbnailUrl: z5.string().nullable()
204
+ })
205
+ ),
206
+ nextCursor: z5.string().optional()
207
+ });
208
+ async function searchCaptures(store, args) {
209
+ return store.list({ q: args.q, cursor: args.cursor, limit: args.limit });
210
+ }
211
+
212
+ // src/tools/_focused-getter.ts
213
+ import { z as z6 } from "zod";
214
+ 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;
215
+ var focusedGetterInputSchema = z6.object({
216
+ id: z6.string().regex(UUID_RE2, "id must be a UUID").describe(
217
+ "Capture id from list_captures or search_captures. UUID format."
218
+ )
219
+ });
220
+ var focusedGetterOutputSchema = z6.object({
221
+ id: z6.string(),
222
+ type: z6.enum(["page", "element", "composite"]),
223
+ field: z6.string(),
224
+ data: z6.unknown()
225
+ }).passthrough();
226
+
227
+ // src/tools/get-palette.ts
228
+ async function getPalette(store, args) {
229
+ return store.getField(args.id, "palette");
230
+ }
231
+
232
+ // src/tools/get-html.ts
233
+ async function getHtml(store, args) {
234
+ return store.getField(args.id, "html");
235
+ }
236
+
237
+ // src/tools/get-animation.ts
238
+ async function getAnimation(store, args) {
239
+ return store.getField(args.id, "animation");
240
+ }
241
+
242
+ // src/tools/get-media.ts
243
+ async function getMedia(store, args) {
244
+ return store.getField(args.id, "media");
245
+ }
246
+
247
+ // src/tools/get-brand-brief.ts
248
+ import { z as z7 } from "zod";
249
+ var getBrandBriefInputSchema = z7.object({
250
+ project_id: z7.string().uuid().describe(
251
+ "Zaai Dev project UUID. Find via the workspace at zaaistudio.com/dev/projects."
252
+ )
253
+ });
254
+ var getBrandBriefOutputSchema = z7.object({
255
+ project_id: z7.string(),
256
+ project_name: z7.string(),
257
+ version: z7.string().describe("Brief version number, or '0' if no brief has been generated yet."),
258
+ status: z7.enum(["draft", "published", "archived"]),
259
+ positioning: z7.string(),
260
+ values: z7.array(z7.string()),
261
+ voice: z7.object({
262
+ one_liner: z7.string(),
263
+ do_say: z7.array(z7.string()),
264
+ dont_say: z7.array(z7.string())
265
+ }),
266
+ audience: z7.object({
267
+ primary: z7.string(),
268
+ secondary: z7.string().nullable(),
269
+ needs: z7.array(z7.string())
270
+ }),
271
+ design_intent: z7.object({
272
+ descriptors: z7.array(z7.string()),
273
+ anti_descriptors: z7.array(z7.string())
274
+ }),
275
+ decisions: z7.array(
276
+ z7.object({
277
+ id: z7.string(),
278
+ title: z7.string(),
279
+ decided_at: z7.string(),
280
+ rationale: z7.string()
281
+ })
282
+ ),
283
+ updated_at: z7.string()
284
+ }).passthrough();
285
+ async function getBrandBrief(store, args) {
286
+ return store.getBrandBrief(args.project_id);
287
+ }
288
+
289
+ // src/tools/get-voice.ts
290
+ import { z as z8 } from "zod";
291
+ var getVoiceInputSchema = z8.object({
292
+ project_id: z8.string().uuid().describe("Zaai Dev project UUID.")
293
+ });
294
+ var getVoiceOutputSchema = z8.object({
295
+ project_id: z8.string(),
296
+ one_liner: z8.string().describe("One-sentence summary of how the brand sounds."),
297
+ tone_descriptors: z8.array(z8.string()).describe("Adjectives the brand voice aims for."),
298
+ do_say: z8.array(z8.string()).describe("Phrasings / patterns the brand actively uses."),
299
+ dont_say: z8.array(z8.string()).describe("Phrasings / patterns the brand explicitly avoids."),
300
+ example_phrases: z8.array(z8.string()).describe("Concrete phrases that exemplify the voice.")
301
+ }).passthrough();
302
+ async function getVoice(store, args) {
303
+ return store.getVoice(args.project_id);
304
+ }
305
+
306
+ // src/tools/get-audience.ts
307
+ import { z as z9 } from "zod";
308
+ var getAudienceInputSchema = z9.object({
309
+ project_id: z9.string().uuid().describe("Zaai Dev project UUID.")
310
+ });
311
+ var getAudienceOutputSchema = z9.object({
312
+ project_id: z9.string(),
313
+ primary: z9.string().describe("Primary audience segment."),
314
+ secondary: z9.string().nullable().describe("Secondary audience segment, or null."),
315
+ needs: z9.array(z9.string()).describe("Jobs-to-be-done / pain points the audience has."),
316
+ channels: z9.array(z9.string()).describe("Where this audience consumes content.")
317
+ }).passthrough();
318
+ async function getAudience(store, args) {
319
+ return store.getAudience(args.project_id);
320
+ }
321
+
322
+ // src/tools/get-design-intent.ts
323
+ import { z as z10 } from "zod";
324
+ var getDesignIntentInputSchema = z10.object({
325
+ project_id: z10.string().uuid().describe("Zaai Dev project UUID.")
326
+ });
327
+ var getDesignIntentOutputSchema = z10.object({
328
+ project_id: z10.string(),
329
+ descriptors: z10.array(z10.string()).describe(
330
+ "Visual adjectives the brand aims for (e.g. 'editorial', 'spacious', 'monochrome')."
331
+ ),
332
+ anti_descriptors: z10.array(z10.string()).describe("Visual adjectives the brand explicitly avoids."),
333
+ inspiration_summary: z10.string().describe("Free-form summary of the inspiration the brief points at.")
334
+ }).passthrough();
335
+ async function getDesignIntent(store, args) {
336
+ return store.getDesignIntent(args.project_id);
337
+ }
338
+
339
+ // src/tools/get-brand-tokens.ts
340
+ import { z as z11 } from "zod";
341
+ var getBrandTokensInputSchema = z11.object({
342
+ project_id: z11.string().uuid().describe("Zaai Dev project UUID.")
343
+ });
344
+ var getBrandTokensOutputSchema = z11.object({
345
+ project_id: z11.string(),
346
+ colors: z11.array(
347
+ z11.object({
348
+ name: z11.string(),
349
+ value: z11.string().describe("Hex / rgba / hsl value as authored."),
350
+ role: z11.string().optional().describe("Role like 'primary' / 'accent' / 'surface'.")
351
+ })
352
+ ),
353
+ fonts: z11.array(
354
+ z11.object({
355
+ role: z11.string().describe("'display', 'body', 'mono', etc."),
356
+ family: z11.string(),
357
+ weights: z11.array(z11.number().int())
358
+ })
359
+ ),
360
+ radius: z11.array(z11.object({ name: z11.string(), value: z11.string() })),
361
+ shadows: z11.array(z11.object({ name: z11.string(), value: z11.string() }))
362
+ }).passthrough();
363
+ async function getBrandTokens(store, args) {
364
+ return store.getBrandTokens(args.project_id);
365
+ }
366
+
367
+ // src/tools/get-decisions.ts
368
+ import { z as z12 } from "zod";
369
+ var getDecisionsInputSchema = z12.object({
370
+ project_id: z12.string().uuid().describe("Zaai Dev project UUID."),
371
+ limit: z12.number().int().min(1).max(200).optional().describe("Max decisions to return. Default 50, max 200, newest first.")
372
+ });
373
+ var getDecisionsOutputSchema = z12.object({
374
+ project_id: z12.string(),
375
+ items: z12.array(
376
+ z12.object({
377
+ id: z12.string(),
378
+ title: z12.string(),
379
+ rationale: z12.string(),
380
+ attribution: z12.string().describe(
381
+ "Who decided. 'mcp:<token_id>' for AI-logged decisions; user email / id for workspace-authored ones."
382
+ ),
383
+ brief_field: z12.string().nullable().describe("Which brief slice this decision concerns, if any (e.g. 'voice.tone')."),
384
+ reference_capture_id: z12.string().nullable(),
385
+ created_at: z12.string()
386
+ }).passthrough()
387
+ )
388
+ }).passthrough();
389
+ async function getDecisions(store, args) {
390
+ return store.getDecisions(args.project_id, args.limit);
391
+ }
392
+
393
+ // src/tools/get-references.ts
394
+ import { z as z13 } from "zod";
395
+ var getReferencesInputSchema = z13.object({
396
+ project_id: z13.string().uuid().describe("Zaai Dev project UUID."),
397
+ limit: z13.number().int().min(1).max(100).optional().describe("Max references to return. Default 20, max 100, newest first."),
398
+ cursor: z13.string().optional().describe("Opaque pagination cursor from the previous response's next_cursor.")
399
+ });
400
+ var referenceItem = z13.object({
401
+ id: z13.string(),
402
+ type: z13.enum(["page", "element", "composite"]),
403
+ source_url: z13.string(),
404
+ source_title: z13.string(),
405
+ note: z13.string().nullable(),
406
+ captured_at: z13.string(),
407
+ thumbnail_url: z13.string().nullable(),
408
+ screenshot_url: z13.string().nullable()
409
+ }).passthrough();
410
+ var getReferencesOutputSchema = z13.object({
411
+ project_id: z13.string(),
412
+ items: z13.array(referenceItem),
413
+ next_cursor: z13.string().nullable()
414
+ }).passthrough();
415
+ async function getReferences(store, args) {
416
+ return store.getReferences(args.project_id, {
417
+ limit: args.limit,
418
+ cursor: args.cursor
419
+ });
420
+ }
421
+
422
+ // src/tools/search-references.ts
423
+ import { z as z14 } from "zod";
424
+ var searchReferencesInputSchema = z14.object({
425
+ project_id: z14.string().uuid().describe("Zaai Dev project UUID."),
426
+ q: z14.string().min(1).max(200).describe("Keyword query \u2014 matched against title / url / note (case-insensitive substring)."),
427
+ limit: z14.number().int().min(1).max(50).optional().describe("Max results to return. Default 10, max 50.")
428
+ });
429
+ var searchReferenceItem = z14.object({
430
+ id: z14.string(),
431
+ type: z14.enum(["page", "element", "composite"]),
432
+ source_url: z14.string(),
433
+ source_title: z14.string(),
434
+ note: z14.string().nullable(),
435
+ captured_at: z14.string(),
436
+ thumbnail_url: z14.string().nullable(),
437
+ screenshot_url: z14.string().nullable(),
438
+ score: z14.number().describe("Relevance score 0..1. v1 keyword scoring: 1.0 title / 0.7 url / 0.5 note."),
439
+ matched_field: z14.enum(["title", "url", "note", "tag"])
440
+ }).passthrough();
441
+ var searchReferencesOutputSchema = z14.object({
442
+ project_id: z14.string(),
443
+ query: z14.string(),
444
+ items: z14.array(searchReferenceItem)
445
+ }).passthrough();
446
+ async function searchReferences(store, args) {
447
+ return store.searchReferences(args.project_id, {
448
+ q: args.q,
449
+ limit: args.limit
450
+ });
451
+ }
452
+
453
+ // src/store/http-store.ts
454
+ var HttpCaptureStore = class {
455
+ constructor(config) {
456
+ this.config = config;
457
+ }
458
+ config;
459
+ async list(args) {
460
+ const sp = new URLSearchParams();
461
+ if (args.q) sp.set("q", args.q);
462
+ if (args.cursor) sp.set("cursor", args.cursor);
463
+ if (args.limit !== void 0) sp.set("limit", String(args.limit));
464
+ const qs = sp.toString();
465
+ return mcpApiFetch(
466
+ this.config,
467
+ `/api/mcp/captures${qs ? `?${qs}` : ""}`
468
+ );
469
+ }
470
+ async get(id) {
471
+ return mcpApiFetch(
472
+ this.config,
473
+ `/api/mcp/captures/${encodeURIComponent(id)}`
474
+ );
475
+ }
476
+ async getField(id, field) {
477
+ return mcpApiFetch(
478
+ this.config,
479
+ `/api/mcp/captures/${encodeURIComponent(id)}/${field}`
480
+ );
481
+ }
482
+ };
483
+
484
+ // src/store/project-store.ts
485
+ var HttpProjectStore = class {
486
+ constructor(config) {
487
+ this.config = config;
488
+ }
489
+ config;
490
+ async getBrandBrief(projectId) {
491
+ return mcpApiFetch(
492
+ this.config,
493
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/brief`
494
+ );
495
+ }
496
+ async getVoice(projectId) {
497
+ return mcpApiFetch(
498
+ this.config,
499
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/voice`
500
+ );
501
+ }
502
+ async getAudience(projectId) {
503
+ return mcpApiFetch(
504
+ this.config,
505
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/audience`
506
+ );
507
+ }
508
+ async getDesignIntent(projectId) {
509
+ return mcpApiFetch(
510
+ this.config,
511
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/design-intent`
512
+ );
513
+ }
514
+ async getBrandTokens(projectId) {
515
+ return mcpApiFetch(
516
+ this.config,
517
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/brand-tokens`
518
+ );
519
+ }
520
+ async getDecisions(projectId, limit) {
521
+ const sp = new URLSearchParams();
522
+ if (limit !== void 0) sp.set("limit", String(limit));
523
+ const qs = sp.toString();
524
+ return mcpApiFetch(
525
+ this.config,
526
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/decisions${qs ? `?${qs}` : ""}`
527
+ );
528
+ }
529
+ async getReferences(projectId, args) {
530
+ const sp = new URLSearchParams();
531
+ if (args.limit !== void 0) sp.set("limit", String(args.limit));
532
+ if (args.cursor) sp.set("cursor", args.cursor);
533
+ const qs = sp.toString();
534
+ return mcpApiFetch(
535
+ this.config,
536
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/references${qs ? `?${qs}` : ""}`
537
+ );
538
+ }
539
+ async searchReferences(projectId, args) {
540
+ const sp = new URLSearchParams();
541
+ sp.set("q", args.q);
542
+ if (args.limit !== void 0) sp.set("limit", String(args.limit));
543
+ return mcpApiFetch(
544
+ this.config,
545
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/references/search?${sp.toString()}`
546
+ );
547
+ }
548
+ };
549
+
550
+ // src/resources/capture-resource.ts
551
+ import {
552
+ ResourceTemplate
553
+ } from "@modelcontextprotocol/sdk/server/mcp.js";
554
+ function registerCaptureResource(server, store) {
555
+ server.registerResource(
556
+ "capture",
557
+ new ResourceTemplate("zaai-capture://{id}", {
558
+ list: void 0
559
+ }),
560
+ {
561
+ title: "Zaai Dev capture",
562
+ description: "Attach one capture from the user's Zaai Dev library as JSON. URI: zaai-capture://<capture-id>. The id comes from list_captures.",
563
+ mimeType: "application/json"
564
+ },
565
+ async (uri, variables) => {
566
+ const rawId = variables.id;
567
+ const id = Array.isArray(rawId) ? rawId[0] : rawId;
568
+ const capture = await store.get(id);
569
+ return {
570
+ contents: [
571
+ {
572
+ uri: uri.href,
573
+ mimeType: "application/json",
574
+ text: JSON.stringify(capture, null, 2)
575
+ }
576
+ ]
577
+ };
578
+ }
579
+ );
580
+ }
581
+
582
+ // src/log.ts
583
+ var PREFIX = "[zaai-mcp]";
584
+ function write(level, message, meta) {
585
+ let line = `${PREFIX} ${level} ${message}`;
586
+ if (meta !== void 0) {
587
+ line += " " + safeStringify(meta);
588
+ }
589
+ process.stderr.write(line + "\n");
590
+ }
591
+ function safeStringify(value) {
592
+ try {
593
+ if (value instanceof Error) {
594
+ return value.stack ?? `${value.name}: ${value.message}`;
595
+ }
596
+ return JSON.stringify(value);
597
+ } catch {
598
+ return String(value);
599
+ }
600
+ }
601
+ function info(message, meta) {
602
+ write("INFO", message, meta);
603
+ }
604
+ function error(message, meta) {
605
+ write("ERROR", message, meta);
606
+ }
607
+
608
+ // src/server.ts
609
+ function createServer(config) {
610
+ const server = new McpServer2({
611
+ name: "zaai-dev-mcp",
612
+ version: PKG_VERSION
613
+ });
614
+ const captureStore = new HttpCaptureStore(config);
615
+ const projectStore = new HttpProjectStore(config);
616
+ server.registerTool(
617
+ "health",
618
+ {
619
+ title: "Health check",
620
+ 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.",
621
+ inputSchema: healthInputSchema.shape,
622
+ outputSchema: healthOutputSchema.shape
623
+ },
624
+ async () => {
625
+ const result = health();
626
+ return {
627
+ structuredContent: result,
628
+ // Human-readable summary for clients that haven't adopted
629
+ // structuredContent yet. Spec 2025-06-18 still requires both.
630
+ content: [
631
+ {
632
+ type: "text",
633
+ text: `Zaai Dev MCP ${result.version} \xB7 node ${result.node} \xB7 uptime ${result.uptimeSeconds}s`
634
+ }
635
+ ]
636
+ };
637
+ }
638
+ );
639
+ server.registerTool(
640
+ "whoami",
641
+ {
642
+ title: "Who am I",
643
+ 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.",
644
+ inputSchema: whoamiInputSchema.shape,
645
+ outputSchema: whoamiOutputSchema.shape
646
+ },
647
+ async () => {
648
+ try {
649
+ const result = await whoami(config);
650
+ return {
651
+ structuredContent: result,
652
+ content: [
653
+ {
654
+ type: "text",
655
+ text: summariseWhoami(result)
656
+ }
657
+ ]
658
+ };
659
+ } catch (err) {
660
+ return toolErrorResponse(err);
661
+ }
662
+ }
663
+ );
664
+ server.registerTool(
665
+ "list_captures",
666
+ {
667
+ title: "List captures",
668
+ 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.",
669
+ inputSchema: listCapturesInputSchema.shape,
670
+ outputSchema: listCapturesOutputSchema.shape
671
+ },
672
+ async (args) => {
673
+ try {
674
+ const result = await listCaptures(captureStore, args);
675
+ return {
676
+ // Cast: SDK's structuredContent param wants
677
+ // Record<string,unknown> but our typed DTO doesn't carry
678
+ // an index signature. Runtime shape is identical.
679
+ structuredContent: result,
680
+ content: [
681
+ {
682
+ type: "text",
683
+ 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)" : ""}.`
684
+ }
685
+ ]
686
+ };
687
+ } catch (err) {
688
+ return toolErrorResponse(err);
689
+ }
690
+ }
691
+ );
692
+ server.registerTool(
693
+ "get_capture",
694
+ {
695
+ title: "Get capture",
696
+ 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.",
697
+ inputSchema: getCaptureInputSchema.shape,
698
+ outputSchema: getCaptureOutputSchema.shape
699
+ },
700
+ async (args) => {
701
+ try {
702
+ const result = await getCapture(captureStore, args);
703
+ return {
704
+ structuredContent: result,
705
+ content: [
706
+ {
707
+ type: "text",
708
+ text: `${result.type} capture "${result.sourceTitle || result.sourceUrl}" (${result.tags.length} tag${result.tags.length === 1 ? "" : "s"})` + (result.screenshotUrl ? " \u2014 screenshot URL included." : "")
709
+ }
710
+ ]
711
+ };
712
+ } catch (err) {
713
+ return toolErrorResponse(err);
714
+ }
715
+ }
716
+ );
717
+ server.registerTool(
718
+ "search_captures",
719
+ {
720
+ title: "Search captures",
721
+ 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.",
722
+ inputSchema: searchCapturesInputSchema.shape,
723
+ outputSchema: searchCapturesOutputSchema.shape
724
+ },
725
+ async (args) => {
726
+ try {
727
+ const result = await searchCaptures(captureStore, args);
728
+ return {
729
+ structuredContent: result,
730
+ content: [
731
+ {
732
+ type: "text",
733
+ 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)" : ""}.`
734
+ }
735
+ ]
736
+ };
737
+ } catch (err) {
738
+ return toolErrorResponse(err);
739
+ }
740
+ }
741
+ );
742
+ server.registerTool(
743
+ "get_palette",
744
+ {
745
+ title: "Get capture palette",
746
+ 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.",
747
+ inputSchema: focusedGetterInputSchema.shape,
748
+ outputSchema: focusedGetterOutputSchema.shape
749
+ },
750
+ makeFocusedGetterHandler(captureStore, getPalette, "palette")
751
+ );
752
+ server.registerTool(
753
+ "get_html",
754
+ {
755
+ title: "Get capture HTML",
756
+ 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.",
757
+ inputSchema: focusedGetterInputSchema.shape,
758
+ outputSchema: focusedGetterOutputSchema.shape
759
+ },
760
+ makeFocusedGetterHandler(captureStore, getHtml, "html")
761
+ );
762
+ server.registerTool(
763
+ "get_animation",
764
+ {
765
+ title: "Get capture animation data",
766
+ 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.",
767
+ inputSchema: focusedGetterInputSchema.shape,
768
+ outputSchema: focusedGetterOutputSchema.shape
769
+ },
770
+ makeFocusedGetterHandler(captureStore, getAnimation, "animation")
771
+ );
772
+ server.registerTool(
773
+ "get_media",
774
+ {
775
+ title: "Get capture media inventory",
776
+ 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.",
777
+ inputSchema: focusedGetterInputSchema.shape,
778
+ outputSchema: focusedGetterOutputSchema.shape
779
+ },
780
+ makeFocusedGetterHandler(captureStore, getMedia, "media")
781
+ );
782
+ server.registerTool(
783
+ "get_brand_brief",
784
+ {
785
+ title: "Get brand brief",
786
+ description: "Returns the full published brand brief for a project: positioning, values, voice, audience, design intent, and decisions. Use this when the LLM needs the whole picture before writing copy or designing a component. If the project has no brief yet, the response still validates \u2014 most fields are empty strings or empty arrays.",
787
+ inputSchema: getBrandBriefInputSchema.shape,
788
+ outputSchema: getBrandBriefOutputSchema.shape
789
+ },
790
+ makeProjectToolHandler(
791
+ projectStore,
792
+ getBrandBrief,
793
+ (r) => `Brief v${r.version} (${r.status}) for "${r.project_name}". Voice: ${r.voice.one_liner || "\u2014"}.`
794
+ )
795
+ );
796
+ server.registerTool(
797
+ "get_voice",
798
+ {
799
+ title: "Get voice guidelines",
800
+ description: "Returns only the voice slice: one-liner, tone descriptors, do-say / don't-say lists, example phrases. Lighter than the full brief when the LLM is only writing copy. Costs 1 credit.",
801
+ inputSchema: getVoiceInputSchema.shape,
802
+ outputSchema: getVoiceOutputSchema.shape
803
+ },
804
+ makeProjectToolHandler(
805
+ projectStore,
806
+ getVoice,
807
+ (r) => `Voice for project ${r.project_id.slice(0, 8)}\u2026: "${r.one_liner || "\u2014"}". ${r.tone_descriptors.length} tone descriptor${r.tone_descriptors.length === 1 ? "" : "s"}.`
808
+ )
809
+ );
810
+ server.registerTool(
811
+ "get_audience",
812
+ {
813
+ title: "Get audience profile",
814
+ description: "Returns only the audience slice: primary segment, optional secondary, needs / pain points, channels. Use when targeting copy or design at a specific audience. Costs 1 credit.",
815
+ inputSchema: getAudienceInputSchema.shape,
816
+ outputSchema: getAudienceOutputSchema.shape
817
+ },
818
+ makeProjectToolHandler(
819
+ projectStore,
820
+ getAudience,
821
+ (r) => `Audience: ${r.primary || "\u2014"}` + (r.secondary ? ` (secondary: ${r.secondary})` : "") + `. ${r.needs.length} stated need${r.needs.length === 1 ? "" : "s"}.`
822
+ )
823
+ );
824
+ server.registerTool(
825
+ "get_design_intent",
826
+ {
827
+ title: "Get design intent",
828
+ description: "Returns the visual descriptors and anti-descriptors that constrain design exploration, plus the inspiration_summary. Use this before generating layouts, palettes, or component styles \u2014 it's the guardrail set the brief encodes. Costs 1 credit.",
829
+ inputSchema: getDesignIntentInputSchema.shape,
830
+ outputSchema: getDesignIntentOutputSchema.shape
831
+ },
832
+ makeProjectToolHandler(
833
+ projectStore,
834
+ getDesignIntent,
835
+ (r) => `${r.descriptors.length} descriptor${r.descriptors.length === 1 ? "" : "s"}, ${r.anti_descriptors.length} anti-descriptor${r.anti_descriptors.length === 1 ? "" : "s"}.`
836
+ )
837
+ );
838
+ server.registerTool(
839
+ "get_brand_tokens",
840
+ {
841
+ title: "Get brand tokens",
842
+ description: "Returns the brand's design tokens: colors (with optional role), fonts (role / family / weights), radius scale, shadow scale. Use this when generating CSS, Tailwind config, or component styles that should match the brand. Costs 1 credit.",
843
+ inputSchema: getBrandTokensInputSchema.shape,
844
+ outputSchema: getBrandTokensOutputSchema.shape
845
+ },
846
+ makeProjectToolHandler(
847
+ projectStore,
848
+ getBrandTokens,
849
+ (r) => `${r.colors.length} color${r.colors.length === 1 ? "" : "s"}, ${r.fonts.length} font${r.fonts.length === 1 ? "" : "s"}, ${r.radius.length} radius, ${r.shadows.length} shadow${r.shadows.length === 1 ? "" : "s"}.`
850
+ )
851
+ );
852
+ server.registerTool(
853
+ "get_decisions",
854
+ {
855
+ title: "Get decisions log",
856
+ description: "Returns the brand + design decisions log for a project: what was decided, why, who decided, when, and which brief field (if any) it concerns. Use this to avoid re-litigating settled questions. Costs 1 credit.",
857
+ inputSchema: getDecisionsInputSchema.shape,
858
+ outputSchema: getDecisionsOutputSchema.shape
859
+ },
860
+ makeProjectToolHandler(
861
+ projectStore,
862
+ getDecisions,
863
+ (r) => `${r.items.length} decision${r.items.length === 1 ? "" : "s"} returned (newest first).`
864
+ )
865
+ );
866
+ server.registerTool(
867
+ "get_references",
868
+ {
869
+ title: "Get project references",
870
+ description: "Paginated list of references (captures) for one project. Use this when the LLM needs to know what visual / interaction references the user has saved for a specific project \u2014 different from list_captures which is org-wide. Costs 1 credit per call.",
871
+ inputSchema: getReferencesInputSchema.shape,
872
+ outputSchema: getReferencesOutputSchema.shape
873
+ },
874
+ makeProjectToolHandler(
875
+ projectStore,
876
+ getReferences,
877
+ (r) => `${r.items.length} reference${r.items.length === 1 ? "" : "s"} returned` + (r.next_cursor ? " (more available \u2014 pass next_cursor for the next page)." : ".")
878
+ )
879
+ );
880
+ server.registerTool(
881
+ "search_references",
882
+ {
883
+ title: "Search project references",
884
+ description: "Keyword search over a project's references (title / url / note in v1; pgvector ranking in v1.5). Returns scored matches with matched_field signals. Use when the LLM has a specific concept in mind ('hero', 'pricing table'). Costs 1 credit.",
885
+ inputSchema: searchReferencesInputSchema.shape,
886
+ outputSchema: searchReferencesOutputSchema.shape
887
+ },
888
+ makeProjectToolHandler(
889
+ projectStore,
890
+ searchReferences,
891
+ (r) => r.items.length === 0 ? `No references match "${r.query}".` : `${r.items.length} reference${r.items.length === 1 ? "" : "s"} matching "${r.query}".`
892
+ )
893
+ );
894
+ registerCaptureResource(server, captureStore);
895
+ info("server initialized", {
896
+ version: PKG_VERSION,
897
+ tools: [
898
+ "health",
899
+ "whoami",
900
+ "list_captures",
901
+ "search_captures",
902
+ "get_capture",
903
+ "get_palette",
904
+ "get_html",
905
+ "get_animation",
906
+ "get_media",
907
+ "get_brand_brief",
908
+ "get_voice",
909
+ "get_audience",
910
+ "get_design_intent",
911
+ "get_brand_tokens",
912
+ "get_decisions",
913
+ "get_references",
914
+ "search_references"
915
+ ],
916
+ resources: ["zaai-capture://{id}"]
917
+ });
918
+ return server;
919
+ }
920
+ function makeProjectToolHandler(store, fn, summary) {
921
+ return async (args) => {
922
+ try {
923
+ const result = await fn(store, args);
924
+ return {
925
+ structuredContent: result,
926
+ content: [{ type: "text", text: summary(result) }]
927
+ };
928
+ } catch (err) {
929
+ return toolErrorResponse(err);
930
+ }
931
+ };
932
+ }
933
+ function makeFocusedGetterHandler(store, fn, label) {
934
+ return async (args) => {
935
+ try {
936
+ const result = await fn(store, args);
937
+ return {
938
+ structuredContent: result,
939
+ content: [
940
+ {
941
+ type: "text",
942
+ text: `${result.type} capture \xB7 ${label} slice for id ${result.id.slice(0, 8)}\u2026`
943
+ }
944
+ ]
945
+ };
946
+ } catch (err) {
947
+ return toolErrorResponse(err);
948
+ }
949
+ };
950
+ }
951
+ function summariseWhoami(r) {
952
+ const scope = r.projectScope === null ? "all projects in org" : `${r.projectScope.length} project${r.projectScope.length === 1 ? "" : "s"}`;
953
+ 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)`;
954
+ }
955
+ function toolErrorResponse(err) {
956
+ if (err instanceof WorkspaceApiError) {
957
+ switch (err.status) {
958
+ case 401:
959
+ return errorContent(
960
+ "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."
961
+ );
962
+ case 402:
963
+ return errorContent(
964
+ "Out of credits. Top up at https://www.zaaistudio.com/dev/settings/billing."
965
+ );
966
+ case 0:
967
+ return errorContent(
968
+ `Network failure reaching the Zaai Dev workspace: ${describe(err.body)}. Check your connection or ZAAI_API_URL setting.`
969
+ );
970
+ default:
971
+ return errorContent(
972
+ `Workspace returned ${err.status} for ${err.path}: ${describe(err.body)}.`
973
+ );
974
+ }
975
+ }
976
+ return errorContent(`Unexpected MCP tool failure: ${describe(err)}`);
977
+ }
978
+ function errorContent(text) {
979
+ return { isError: true, content: [{ type: "text", text }] };
980
+ }
981
+ function describe(value) {
982
+ if (value instanceof Error) return value.message;
983
+ if (typeof value === "string") return value;
984
+ try {
985
+ return JSON.stringify(value);
986
+ } catch {
987
+ return String(value);
988
+ }
989
+ }
990
+
991
+ // src/config.ts
992
+ var DEFAULT_API_URL = "https://www.zaaistudio.com";
993
+ var ConfigError = class extends Error {
994
+ constructor(message) {
995
+ super(message);
996
+ this.name = "ConfigError";
997
+ }
998
+ };
999
+ function loadConfig() {
1000
+ const apiToken = process.env.ZAAI_API_TOKEN;
1001
+ if (!apiToken) {
1002
+ throw new ConfigError(
1003
+ "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."
1004
+ );
1005
+ }
1006
+ if (!apiToken.startsWith("zaai_mcp_")) {
1007
+ throw new ConfigError(
1008
+ "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."
1009
+ );
1010
+ }
1011
+ const rawUrl = process.env.ZAAI_API_URL ?? DEFAULT_API_URL;
1012
+ const apiUrl = rawUrl.replace(/\/+$/, "");
1013
+ info("config loaded", { apiUrl });
1014
+ return { apiToken, apiUrl };
1015
+ }
1016
+
1017
+ // src/bin/http.ts
1018
+ var DEFAULT_PORT = 3001;
1019
+ async function main() {
1020
+ let config;
1021
+ try {
1022
+ config = loadConfig();
1023
+ } catch (err) {
1024
+ if (err instanceof ConfigError) {
1025
+ error("FATAL: " + err.message);
1026
+ process.exit(1);
1027
+ }
1028
+ throw err;
1029
+ }
1030
+ const port = parseInt(process.env.ZAAI_HTTP_PORT ?? String(DEFAULT_PORT), 10);
1031
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
1032
+ error(`FATAL: ZAAI_HTTP_PORT must be a valid port number, got ${process.env.ZAAI_HTTP_PORT}`);
1033
+ process.exit(1);
1034
+ }
1035
+ const transport = new StreamableHTTPServerTransport({
1036
+ sessionIdGenerator: () => randomUUID()
1037
+ });
1038
+ const server = createServer(config);
1039
+ await server.connect(transport);
1040
+ const httpServer = createHttpServer(async (req, res) => {
1041
+ res.setHeader("Access-Control-Allow-Origin", "*");
1042
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, DELETE");
1043
+ res.setHeader(
1044
+ "Access-Control-Allow-Headers",
1045
+ "Content-Type, Authorization, Mcp-Session-Id, Last-Event-ID"
1046
+ );
1047
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
1048
+ if (req.method === "OPTIONS") {
1049
+ res.writeHead(204).end();
1050
+ return;
1051
+ }
1052
+ if (req.method === "GET" && req.url === "/healthz") {
1053
+ res.writeHead(200, { "Content-Type": "application/json" }).end(
1054
+ JSON.stringify({ ok: true, version: PKG_VERSION, transport: "streamable-http" })
1055
+ );
1056
+ return;
1057
+ }
1058
+ if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
1059
+ try {
1060
+ await transport.handleRequest(req, res);
1061
+ } catch (err) {
1062
+ error("transport.handleRequest threw", err);
1063
+ if (!res.headersSent) {
1064
+ res.writeHead(500, { "Content-Type": "application/json" }).end(
1065
+ JSON.stringify({ error: "internal_error" })
1066
+ );
1067
+ }
1068
+ }
1069
+ return;
1070
+ }
1071
+ res.writeHead(404, { "Content-Type": "application/json" }).end(
1072
+ JSON.stringify({ error: "not_found", expected: ["/mcp", "/healthz"] })
1073
+ );
1074
+ });
1075
+ httpServer.listen(port, () => {
1076
+ info("streamable-http transport listening", {
1077
+ port,
1078
+ version: PKG_VERSION,
1079
+ mode: "single-tenant"
1080
+ });
1081
+ });
1082
+ const shutdown = (signal) => {
1083
+ info(`received ${signal}, closing http server`);
1084
+ httpServer.close(() => {
1085
+ transport.close().catch((err) => error("transport close failed", err));
1086
+ process.exit(0);
1087
+ });
1088
+ setTimeout(() => process.exit(0), 1e4).unref();
1089
+ };
1090
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
1091
+ process.on("SIGINT", () => shutdown("SIGINT"));
1092
+ }
1093
+ main().catch((err) => {
1094
+ error("fatal startup error", err);
1095
+ process.exit(1);
1096
+ });
1097
+ //# sourceMappingURL=http.js.map