@zaai-dev/mcp 0.3.1 → 0.6.1

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/dist/bin/http.js CHANGED
@@ -12,7 +12,7 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js
12
12
  import { z } from "zod";
13
13
 
14
14
  // src/version.ts
15
- var PKG_VERSION = true ? "0.3.1" : "0.0.0-dev";
15
+ var PKG_VERSION = true ? "0.6.1" : "0.0.0-dev";
16
16
 
17
17
  // src/tools/health.ts
18
18
  var healthInputSchema = z.object({});
@@ -84,7 +84,8 @@ async function mcpApiFetch(config, path, init = {}) {
84
84
  if (RETRYABLE_STATUSES.has(res.status) || res.status >= 502 && res.status <= 504) {
85
85
  lastErr = err;
86
86
  if (attempt < MAX_ATTEMPTS) {
87
- await sleep(BASE_BACKOFF_MS * 2 ** (attempt - 1));
87
+ const serverWait = res.status === 429 ? rateLimitWaitMs(res.headers) : null;
88
+ await sleep(serverWait ?? BASE_BACKOFF_MS * 2 ** (attempt - 1));
88
89
  continue;
89
90
  }
90
91
  }
@@ -92,6 +93,25 @@ async function mcpApiFetch(config, path, init = {}) {
92
93
  }
93
94
  throw lastErr ?? new WorkspaceApiError(0, { message: "no attempts" }, path);
94
95
  }
96
+ var MAX_RATE_LIMIT_WAIT_MS = 1e4;
97
+ function rateLimitWaitMs(headers) {
98
+ const retryAfter = headers.get("retry-after");
99
+ if (retryAfter) {
100
+ const secs = Number(retryAfter);
101
+ if (Number.isFinite(secs) && secs >= 0) {
102
+ return Math.min(secs * 1e3, MAX_RATE_LIMIT_WAIT_MS);
103
+ }
104
+ }
105
+ const reset = headers.get("x-ratelimit-reset");
106
+ if (reset) {
107
+ const resetEpoch = Number(reset);
108
+ if (Number.isFinite(resetEpoch)) {
109
+ const ms = resetEpoch * 1e3 - Date.now();
110
+ if (ms > 0) return Math.min(ms, MAX_RATE_LIMIT_WAIT_MS);
111
+ }
112
+ }
113
+ return null;
114
+ }
95
115
  function sleep(ms) {
96
116
  return new Promise((resolve) => setTimeout(resolve, ms));
97
117
  }
@@ -130,7 +150,7 @@ var listCapturesOutputSchema = z3.object({
130
150
  captures: z3.array(
131
151
  z3.object({
132
152
  id: z3.string().describe("Capture UUID. Pass to get_capture for the full payload."),
133
- type: z3.enum(["page", "element", "composite"]).describe(
153
+ type: z3.enum(["page", "element", "composite", "recording"]).describe(
134
154
  "page = full-page screenshot + DOM context; element = a single picked element; composite = multiple elements stacked."
135
155
  ),
136
156
  sourceTitle: z3.string(),
@@ -162,7 +182,7 @@ var getCaptureInputSchema = z4.object({
162
182
  });
163
183
  var getCaptureOutputSchema = z4.object({
164
184
  id: z4.string(),
165
- type: z4.enum(["page", "element", "composite"]),
185
+ type: z4.enum(["page", "element", "composite", "recording"]),
166
186
  projectId: z4.string(),
167
187
  sourceTitle: z4.string(),
168
188
  sourceUrl: z4.string(),
@@ -195,7 +215,7 @@ var searchCapturesOutputSchema = z5.object({
195
215
  captures: z5.array(
196
216
  z5.object({
197
217
  id: z5.string(),
198
- type: z5.enum(["page", "element", "composite"]),
218
+ type: z5.enum(["page", "element", "composite", "recording"]),
199
219
  sourceTitle: z5.string(),
200
220
  sourceUrl: z5.string(),
201
221
  capturedAt: z5.string(),
@@ -219,7 +239,7 @@ var focusedGetterInputSchema = z6.object({
219
239
  });
220
240
  var focusedGetterOutputSchema = z6.object({
221
241
  id: z6.string(),
222
- type: z6.enum(["page", "element", "composite"]),
242
+ type: z6.enum(["page", "element", "composite", "recording"]),
223
243
  field: z6.string(),
224
244
  data: z6.unknown()
225
245
  }).passthrough();
@@ -244,11 +264,19 @@ async function getMedia(store, args) {
244
264
  return store.getField(args.id, "media");
245
265
  }
246
266
 
267
+ // src/tools/get-structure.ts
268
+ async function getStructure(store, args) {
269
+ return store.getField(args.id, "structure");
270
+ }
271
+
247
272
  // src/tools/get-brand-brief.ts
248
273
  import { z as z7 } from "zod";
249
274
  var getBrandBriefInputSchema = z7.object({
250
275
  project_id: z7.string().uuid().describe(
251
- "Zaai Dev project UUID. Find via the workspace at zaaistudio.com/dev/projects."
276
+ "Zaai Dev project UUID. Find via the workspace at zaaidev.com/dev/projects."
277
+ ),
278
+ brief_id: z7.string().uuid().optional().describe(
279
+ "Target a specific brief by id (from list_briefs). Omit to read the project's brand-identity brief."
252
280
  )
253
281
  });
254
282
  var getBrandBriefOutputSchema = z7.object({
@@ -283,13 +311,16 @@ var getBrandBriefOutputSchema = z7.object({
283
311
  updated_at: z7.string()
284
312
  }).passthrough();
285
313
  async function getBrandBrief(store, args) {
286
- return store.getBrandBrief(args.project_id);
314
+ return store.getBrandBrief(args.project_id, args.brief_id);
287
315
  }
288
316
 
289
317
  // src/tools/get-voice.ts
290
318
  import { z as z8 } from "zod";
291
319
  var getVoiceInputSchema = z8.object({
292
- project_id: z8.string().uuid().describe("Zaai Dev project UUID.")
320
+ project_id: z8.string().uuid().describe("Zaai Dev project UUID."),
321
+ brief_id: z8.string().uuid().optional().describe(
322
+ "Target a specific brief by id (from list_briefs). Omit to read the project's brand-identity brief."
323
+ )
293
324
  });
294
325
  var getVoiceOutputSchema = z8.object({
295
326
  project_id: z8.string(),
@@ -300,13 +331,16 @@ var getVoiceOutputSchema = z8.object({
300
331
  example_phrases: z8.array(z8.string()).describe("Concrete phrases that exemplify the voice.")
301
332
  }).passthrough();
302
333
  async function getVoice(store, args) {
303
- return store.getVoice(args.project_id);
334
+ return store.getVoice(args.project_id, args.brief_id);
304
335
  }
305
336
 
306
337
  // src/tools/get-audience.ts
307
338
  import { z as z9 } from "zod";
308
339
  var getAudienceInputSchema = z9.object({
309
- project_id: z9.string().uuid().describe("Zaai Dev project UUID.")
340
+ project_id: z9.string().uuid().describe("Zaai Dev project UUID."),
341
+ brief_id: z9.string().uuid().optional().describe(
342
+ "Target a specific brief by id (from list_briefs). Omit to read the project's brand-identity brief."
343
+ )
310
344
  });
311
345
  var getAudienceOutputSchema = z9.object({
312
346
  project_id: z9.string(),
@@ -316,13 +350,16 @@ var getAudienceOutputSchema = z9.object({
316
350
  channels: z9.array(z9.string()).describe("Where this audience consumes content.")
317
351
  }).passthrough();
318
352
  async function getAudience(store, args) {
319
- return store.getAudience(args.project_id);
353
+ return store.getAudience(args.project_id, args.brief_id);
320
354
  }
321
355
 
322
356
  // src/tools/get-design-intent.ts
323
357
  import { z as z10 } from "zod";
324
358
  var getDesignIntentInputSchema = z10.object({
325
- project_id: z10.string().uuid().describe("Zaai Dev project UUID.")
359
+ project_id: z10.string().uuid().describe("Zaai Dev project UUID."),
360
+ brief_id: z10.string().uuid().optional().describe(
361
+ "Target a specific brief by id (from list_briefs). Omit to read the project's brand-identity brief."
362
+ )
326
363
  });
327
364
  var getDesignIntentOutputSchema = z10.object({
328
365
  project_id: z10.string(),
@@ -333,13 +370,16 @@ var getDesignIntentOutputSchema = z10.object({
333
370
  inspiration_summary: z10.string().describe("Free-form summary of the inspiration the brief points at.")
334
371
  }).passthrough();
335
372
  async function getDesignIntent(store, args) {
336
- return store.getDesignIntent(args.project_id);
373
+ return store.getDesignIntent(args.project_id, args.brief_id);
337
374
  }
338
375
 
339
376
  // src/tools/get-brand-tokens.ts
340
377
  import { z as z11 } from "zod";
341
378
  var getBrandTokensInputSchema = z11.object({
342
- project_id: z11.string().uuid().describe("Zaai Dev project UUID.")
379
+ project_id: z11.string().uuid().describe("Zaai Dev project UUID."),
380
+ brief_id: z11.string().uuid().optional().describe(
381
+ "Target a specific brief by id (from list_briefs). Omit to read the project's brand-identity brief."
382
+ )
343
383
  });
344
384
  var getBrandTokensOutputSchema = z11.object({
345
385
  project_id: z11.string(),
@@ -361,7 +401,7 @@ var getBrandTokensOutputSchema = z11.object({
361
401
  shadows: z11.array(z11.object({ name: z11.string(), value: z11.string() }))
362
402
  }).passthrough();
363
403
  async function getBrandTokens(store, args) {
364
- return store.getBrandTokens(args.project_id);
404
+ return store.getBrandTokens(args.project_id, args.brief_id);
365
405
  }
366
406
 
367
407
  // src/tools/get-decisions.ts
@@ -399,7 +439,7 @@ var getReferencesInputSchema = z13.object({
399
439
  });
400
440
  var referenceItem = z13.object({
401
441
  id: z13.string(),
402
- type: z13.enum(["page", "element", "composite"]),
442
+ type: z13.enum(["page", "element", "composite", "recording"]),
403
443
  source_url: z13.string(),
404
444
  source_title: z13.string(),
405
445
  note: z13.string().nullable(),
@@ -428,7 +468,7 @@ var searchReferencesInputSchema = z14.object({
428
468
  });
429
469
  var searchReferenceItem = z14.object({
430
470
  id: z14.string(),
431
- type: z14.enum(["page", "element", "composite"]),
471
+ type: z14.enum(["page", "element", "composite", "recording"]),
432
472
  source_url: z14.string(),
433
473
  source_title: z14.string(),
434
474
  note: z14.string().nullable(),
@@ -450,6 +490,164 @@ async function searchReferences(store, args) {
450
490
  });
451
491
  }
452
492
 
493
+ // src/tools/list-briefs.ts
494
+ import { z as z15 } from "zod";
495
+ var listBriefsInputSchema = z15.object({
496
+ project_id: z15.string().uuid().describe("Zaai Dev project UUID.")
497
+ });
498
+ var briefTypeEnum = z15.enum([
499
+ "brand_identity",
500
+ "web_site",
501
+ "layout_page",
502
+ "component",
503
+ "campaign",
504
+ "naming"
505
+ ]);
506
+ var briefStatusEnum = z15.enum([
507
+ "to_draft",
508
+ "drafting",
509
+ "synthesised",
510
+ "in_review",
511
+ "changes_requested",
512
+ "approved"
513
+ ]);
514
+ var listBriefsItem = z15.object({
515
+ id: z15.string(),
516
+ brief_type: briefTypeEnum,
517
+ name: z15.string().nullable(),
518
+ display_name: z15.string(),
519
+ status: briefStatusEnum,
520
+ current_version: z15.object({ version: z15.number().int(), created_at: z15.string() }).nullable(),
521
+ created_at: z15.string(),
522
+ updated_at: z15.string()
523
+ }).passthrough();
524
+ var listBriefsOutputSchema = z15.object({
525
+ project_id: z15.string(),
526
+ items: z15.array(listBriefsItem)
527
+ }).passthrough();
528
+ async function listBriefs(store, args) {
529
+ return store.listBriefs(args.project_id);
530
+ }
531
+
532
+ // src/tools/list-docs.ts
533
+ var listDocsInputSchema = listBriefsInputSchema;
534
+ var listDocsOutputSchema = listBriefsOutputSchema;
535
+ async function listDocs(store, args) {
536
+ return store.listDocs(args.project_id);
537
+ }
538
+
539
+ // src/tools/get-doc.ts
540
+ import { z as z16 } from "zod";
541
+ var getDocInputSchema = z16.object({
542
+ project_id: z16.string().uuid().describe("Zaai Dev project UUID."),
543
+ doc_id: z16.string().uuid().optional().describe(
544
+ "Target a specific doc by id (from list_docs). Omit to read the project's brand-identity doc."
545
+ )
546
+ });
547
+ var briefStatusEnum2 = z16.enum([
548
+ "to_draft",
549
+ "drafting",
550
+ "synthesised",
551
+ "in_review",
552
+ "changes_requested",
553
+ "approved"
554
+ ]);
555
+ var docBlock = z16.object({
556
+ type: z16.enum(["text", "capture", "image", "divider"]),
557
+ markdown: z16.string().optional(),
558
+ capture_id: z16.string().nullable().optional(),
559
+ role: z16.string().optional(),
560
+ treatment: z16.string().optional(),
561
+ note: z16.string().nullable().optional(),
562
+ src: z16.string().optional()
563
+ }).passthrough();
564
+ var getDocOutputSchema = z16.object({
565
+ project_id: z16.string(),
566
+ doc_id: z16.string(),
567
+ kind: z16.literal("block-doc"),
568
+ name: z16.string(),
569
+ version: z16.number().int(),
570
+ status: briefStatusEnum2,
571
+ approval: z16.object({ state: z16.string(), version: z16.number().int().nullable() }).nullable().describe("Sign-off signal: whether the doc is an approved spec or a draft."),
572
+ blocks: z16.array(docBlock),
573
+ references: z16.array(z16.object({}).passthrough())
574
+ }).passthrough();
575
+ async function getDoc(store, args) {
576
+ return store.getDoc(args.project_id, args.doc_id);
577
+ }
578
+
579
+ // src/tools/get-project-bundle.ts
580
+ import { z as z17 } from "zod";
581
+ var getProjectBundleInputSchema = z17.object({
582
+ project_id: z17.string().uuid().describe("Zaai Dev project UUID.")
583
+ });
584
+ var getProjectBundleOutputSchema = z17.object({
585
+ project_id: z17.string(),
586
+ project_name: z17.string(),
587
+ docs: z17.array(getDocOutputSchema),
588
+ captures: z17.array(
589
+ z17.object({
590
+ id: z17.string(),
591
+ title: z17.string(),
592
+ type: z17.string(),
593
+ palette: z17.array(z17.string()),
594
+ vibe: z17.array(z17.string())
595
+ }).passthrough()
596
+ )
597
+ }).passthrough();
598
+ async function getProjectBundle(store, args) {
599
+ return store.getProjectBundle(args.project_id);
600
+ }
601
+
602
+ // src/tools/add-reference.ts
603
+ import { z as z18 } from "zod";
604
+ var addReferenceInputSchema = z18.object({
605
+ project_id: z18.string().uuid().describe("Zaai Dev project UUID."),
606
+ source_url: z18.string().url().describe("URL of the page/element being referenced."),
607
+ source_title: z18.string().min(1).max(500).describe("Human-readable title for the reference."),
608
+ note: z18.string().max(5e3).nullable().optional().describe("Optional note explaining why this reference matters."),
609
+ tags: z18.array(z18.string().min(1).max(60)).max(50).optional().describe("Optional tags to organise the reference.")
610
+ });
611
+ var addReferenceOutputSchema = z18.object({
612
+ id: z18.string(),
613
+ project_id: z18.string(),
614
+ source_url: z18.string(),
615
+ source_title: z18.string(),
616
+ source_kind: z18.enum(["extension", "mcp"]).describe("Origin of the reference. MCP-added references are 'mcp'."),
617
+ captured_at: z18.string()
618
+ }).passthrough();
619
+ async function addReference(store, args) {
620
+ return store.addReference(args.project_id, {
621
+ source_url: args.source_url,
622
+ source_title: args.source_title,
623
+ note: args.note,
624
+ tags: args.tags
625
+ });
626
+ }
627
+
628
+ // src/tools/log-decision.ts
629
+ import { z as z19 } from "zod";
630
+ var logDecisionInputSchema = z19.object({
631
+ project_id: z19.string().uuid().describe("Zaai Dev project UUID."),
632
+ title: z19.string().min(1).max(200).describe("What was decided (one line)."),
633
+ rationale: z19.string().max(5e3).nullable().optional().describe("Optional reasoning behind the decision."),
634
+ brief_field: z19.string().max(100).nullable().optional().describe("Optional brief slice this decision concerns (e.g. 'voice.tone').")
635
+ });
636
+ var logDecisionOutputSchema = z19.object({
637
+ id: z19.string(),
638
+ project_id: z19.string(),
639
+ title: z19.string(),
640
+ brief_field: z19.string().nullable(),
641
+ created_at: z19.string()
642
+ }).passthrough();
643
+ async function logDecision(store, args) {
644
+ return store.logDecision(args.project_id, {
645
+ title: args.title,
646
+ rationale: args.rationale,
647
+ brief_field: args.brief_field
648
+ });
649
+ }
650
+
453
651
  // src/store/http-store.ts
454
652
  var HttpCaptureStore = class {
455
653
  constructor(config) {
@@ -482,39 +680,45 @@ var HttpCaptureStore = class {
482
680
  };
483
681
 
484
682
  // src/store/project-store.ts
683
+ function briefIdQuery(briefId) {
684
+ if (!briefId) return "";
685
+ const sp = new URLSearchParams();
686
+ sp.set("brief_id", briefId);
687
+ return `?${sp.toString()}`;
688
+ }
485
689
  var HttpProjectStore = class {
486
690
  constructor(config) {
487
691
  this.config = config;
488
692
  }
489
693
  config;
490
- async getBrandBrief(projectId) {
694
+ async getBrandBrief(projectId, briefId) {
491
695
  return mcpApiFetch(
492
696
  this.config,
493
- `/api/mcp/projects/${encodeURIComponent(projectId)}/brief`
697
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/brief${briefIdQuery(briefId)}`
494
698
  );
495
699
  }
496
- async getVoice(projectId) {
700
+ async getVoice(projectId, briefId) {
497
701
  return mcpApiFetch(
498
702
  this.config,
499
- `/api/mcp/projects/${encodeURIComponent(projectId)}/voice`
703
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/voice${briefIdQuery(briefId)}`
500
704
  );
501
705
  }
502
- async getAudience(projectId) {
706
+ async getAudience(projectId, briefId) {
503
707
  return mcpApiFetch(
504
708
  this.config,
505
- `/api/mcp/projects/${encodeURIComponent(projectId)}/audience`
709
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/audience${briefIdQuery(briefId)}`
506
710
  );
507
711
  }
508
- async getDesignIntent(projectId) {
712
+ async getDesignIntent(projectId, briefId) {
509
713
  return mcpApiFetch(
510
714
  this.config,
511
- `/api/mcp/projects/${encodeURIComponent(projectId)}/design-intent`
715
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/design-intent${briefIdQuery(briefId)}`
512
716
  );
513
717
  }
514
- async getBrandTokens(projectId) {
718
+ async getBrandTokens(projectId, briefId) {
515
719
  return mcpApiFetch(
516
720
  this.config,
517
- `/api/mcp/projects/${encodeURIComponent(projectId)}/brand-tokens`
721
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/brand-tokens${briefIdQuery(briefId)}`
518
722
  );
519
723
  }
520
724
  async getDecisions(projectId, limit) {
@@ -545,6 +749,54 @@ var HttpProjectStore = class {
545
749
  `/api/mcp/projects/${encodeURIComponent(projectId)}/references/search?${sp.toString()}`
546
750
  );
547
751
  }
752
+ // ── v1.2 / v1.3 / v1.4 ──────────────────────────────────────
753
+ async listBriefs(projectId) {
754
+ return mcpApiFetch(
755
+ this.config,
756
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/briefs`
757
+ );
758
+ }
759
+ async listDocs(projectId) {
760
+ return mcpApiFetch(
761
+ this.config,
762
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/docs`
763
+ );
764
+ }
765
+ async getDoc(projectId, docId) {
766
+ const seg = docId ? encodeURIComponent(docId) : "default";
767
+ return mcpApiFetch(
768
+ this.config,
769
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/docs/${seg}`
770
+ );
771
+ }
772
+ async getProjectBundle(projectId) {
773
+ return mcpApiFetch(
774
+ this.config,
775
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/bundle`
776
+ );
777
+ }
778
+ async addReference(projectId, args) {
779
+ return mcpApiFetch(
780
+ this.config,
781
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/references`,
782
+ {
783
+ method: "POST",
784
+ headers: { "Content-Type": "application/json" },
785
+ body: JSON.stringify(args)
786
+ }
787
+ );
788
+ }
789
+ async logDecision(projectId, args) {
790
+ return mcpApiFetch(
791
+ this.config,
792
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/decisions`,
793
+ {
794
+ method: "POST",
795
+ headers: { "Content-Type": "application/json" },
796
+ body: JSON.stringify(args)
797
+ }
798
+ );
799
+ }
548
800
  };
549
801
 
550
802
  // src/resources/capture-resource.ts
@@ -779,6 +1031,16 @@ function createServer(config) {
779
1031
  },
780
1032
  makeFocusedGetterHandler(captureStore, getMedia, "media")
781
1033
  );
1034
+ server.registerTool(
1035
+ "get_structure",
1036
+ {
1037
+ title: "Get capture structure map",
1038
+ description: "Returns just the Layer-Explorer teardown \u2014 the 'structure map' (BUILD-PLAN \xA76.6): a flat list of the captured subtree's nodes, each with depth, a root-relative rect, short selector, inferred component, headline computed style (display/background/color/radius/font), and flags (flex/grid, animated, brandToken, a11yIssue, hidden). Page \u2192 main-content subtree; element \u2192 the captured element's subtree; composite \u2192 one map per picked element. Use this to understand HOW a reference is built before an on-brand rebuild, without loading full HTML + computed style. Returns structure:null on pre-\xA76.6 captures. Costs 1 credit.",
1039
+ inputSchema: focusedGetterInputSchema.shape,
1040
+ outputSchema: focusedGetterOutputSchema.shape
1041
+ },
1042
+ makeFocusedGetterHandler(captureStore, getStructure, "structure")
1043
+ );
782
1044
  server.registerTool(
783
1045
  "get_brand_brief",
784
1046
  {
@@ -891,6 +1153,90 @@ function createServer(config) {
891
1153
  (r) => r.items.length === 0 ? `No references match "${r.query}".` : `${r.items.length} reference${r.items.length === 1 ? "" : "s"} matching "${r.query}".`
892
1154
  )
893
1155
  );
1156
+ server.registerTool(
1157
+ "list_briefs",
1158
+ {
1159
+ title: "List briefs",
1160
+ description: "List every brief on a project \u2014 id, type, name, status, and current-version metadata. Call this first when working with a project so you know what briefs exist; then pass `brief_id` into the targeted read tools (get_brand_brief / get_voice / get_audience / get_design_intent / get_brand_tokens) to target a non-default brief. Returns oldest-first by created_at. Costs 1 credit.",
1161
+ inputSchema: listBriefsInputSchema.shape,
1162
+ outputSchema: listBriefsOutputSchema.shape
1163
+ },
1164
+ makeProjectToolHandler(
1165
+ projectStore,
1166
+ listBriefs,
1167
+ (r) => `${r.items.length} brief${r.items.length === 1 ? "" : "s"} on project ${r.project_id.slice(0, 8)}\u2026 (oldest first).`
1168
+ )
1169
+ );
1170
+ server.registerTool(
1171
+ "list_docs",
1172
+ {
1173
+ title: "List docs",
1174
+ description: "List every doc on a project \u2014 id, type, name, status, and current-version metadata. Friendly alias of list_briefs. Call first, then pass `doc_id` into get_doc. Returns oldest-first. Costs 1 credit.",
1175
+ inputSchema: listDocsInputSchema.shape,
1176
+ outputSchema: listDocsOutputSchema.shape
1177
+ },
1178
+ makeProjectToolHandler(
1179
+ projectStore,
1180
+ listDocs,
1181
+ (r) => `${r.items.length} doc${r.items.length === 1 ? "" : "s"} on project ${r.project_id.slice(0, 8)}\u2026 (oldest first).`
1182
+ )
1183
+ );
1184
+ server.registerTool(
1185
+ "get_doc",
1186
+ {
1187
+ title: "Get doc",
1188
+ description: "Read a doc as an agent-ready block list: text blocks as markdown, capture blocks as structured references with role / treatment / note and full capture metadata (palette, typography, vibe, inferred component) plus the captured `code` (element outerHTML + computed CSS + build tokens like bg/color/radius/padding/font/shadow/border) so you build from real values, not a screenshot guess. Includes the sign-off signal (approval state + version) so you know whether you're building from an approved spec or a draft. Omit doc_id for the project's brand-identity doc. Costs 1 credit.",
1189
+ inputSchema: getDocInputSchema.shape,
1190
+ outputSchema: getDocOutputSchema.shape
1191
+ },
1192
+ makeProjectToolHandler(
1193
+ projectStore,
1194
+ getDoc,
1195
+ (r) => `Doc "${r.name}" v${r.version} (${r.status})` + (r.approval ? `, approval: ${r.approval.state}` : "") + `, ${r.blocks.length} block${r.blocks.length === 1 ? "" : "s"}.`
1196
+ )
1197
+ );
1198
+ server.registerTool(
1199
+ "get_project_bundle",
1200
+ {
1201
+ title: "Get project bundle",
1202
+ description: "Fetch the whole project as one agent-ready bundle: every doc's block list (text + roled capture references + approval state) plus the project's capture library. Use this to load full project context up front in a single call. Can be large. Costs 1 credit.",
1203
+ inputSchema: getProjectBundleInputSchema.shape,
1204
+ outputSchema: getProjectBundleOutputSchema.shape
1205
+ },
1206
+ makeProjectToolHandler(
1207
+ projectStore,
1208
+ getProjectBundle,
1209
+ (r) => `Bundle for "${r.project_name}": ${r.docs.length} doc${r.docs.length === 1 ? "" : "s"}, ${r.captures.length} capture${r.captures.length === 1 ? "" : "s"}.`
1210
+ )
1211
+ );
1212
+ server.registerTool(
1213
+ "add_reference",
1214
+ {
1215
+ title: "Add reference",
1216
+ description: "Add a URL reference to a project's library \u2014 for when you've found a relevant page and want to save it. No screenshot required; stored with source_kind='mcp' so the UI distinguishes it from extension-pushed captures. Use when discovering inspiration, never to bulk-import URLs the user already has. Costs 5 credits.",
1217
+ inputSchema: addReferenceInputSchema.shape,
1218
+ outputSchema: addReferenceOutputSchema.shape
1219
+ },
1220
+ makeProjectToolHandler(
1221
+ projectStore,
1222
+ addReference,
1223
+ (r) => `Added reference "${r.source_title}" (${r.source_kind}) to project ${r.project_id.slice(0, 8)}\u2026.`
1224
+ )
1225
+ );
1226
+ server.registerTool(
1227
+ "log_decision",
1228
+ {
1229
+ title: "Log decision",
1230
+ description: "Append a decision to a project's decisions log: what was decided, optionally why, optionally which brief slice it concerns (e.g. 'voice.tone'). Use this when you make or recommend a brand- or design-relevant choice that future you should trace back. Costs 1 credit.",
1231
+ inputSchema: logDecisionInputSchema.shape,
1232
+ outputSchema: logDecisionOutputSchema.shape
1233
+ },
1234
+ makeProjectToolHandler(
1235
+ projectStore,
1236
+ logDecision,
1237
+ (r) => `Logged decision "${r.title}"` + (r.brief_field ? ` (${r.brief_field})` : "") + `.`
1238
+ )
1239
+ );
894
1240
  registerCaptureResource(server, captureStore);
895
1241
  info("server initialized", {
896
1242
  version: PKG_VERSION,
@@ -904,6 +1250,7 @@ function createServer(config) {
904
1250
  "get_html",
905
1251
  "get_animation",
906
1252
  "get_media",
1253
+ "get_structure",
907
1254
  "get_brand_brief",
908
1255
  "get_voice",
909
1256
  "get_audience",
@@ -911,7 +1258,13 @@ function createServer(config) {
911
1258
  "get_brand_tokens",
912
1259
  "get_decisions",
913
1260
  "get_references",
914
- "search_references"
1261
+ "search_references",
1262
+ "list_briefs",
1263
+ "list_docs",
1264
+ "get_doc",
1265
+ "get_project_bundle",
1266
+ "add_reference",
1267
+ "log_decision"
915
1268
  ],
916
1269
  resources: ["zaai-capture://{id}"]
917
1270
  });
@@ -954,27 +1307,61 @@ function summariseWhoami(r) {
954
1307
  }
955
1308
  function toolErrorResponse(err) {
956
1309
  if (err instanceof WorkspaceApiError) {
1310
+ const cls = errorClassOf(err.body);
1311
+ const detail = describe(err.body);
957
1312
  switch (err.status) {
958
1313
  case 401:
959
1314
  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."
1315
+ "Authentication failed. Your ZAAI_API_TOKEN is invalid, expired, or revoked. Mint a new MCP token at https://zaaidev.com/dev/settings/tokens and update your MCP client config."
961
1316
  );
962
1317
  case 402:
1318
+ if (cls === "SubscriptionRequired") {
1319
+ return errorContent(
1320
+ "Zaai Dev needs an active subscription or trial to use the MCP. Start one (14-day free trial, no card required) at https://zaaidev.com/dev/settings/billing."
1321
+ );
1322
+ }
963
1323
  return errorContent(
964
- "Out of credits. Top up at https://www.zaaistudio.com/dev/settings/billing."
1324
+ "Out of credits for MCP calls. Top up at https://zaaidev.com/dev/settings/billing."
1325
+ );
1326
+ case 403:
1327
+ return errorContent(
1328
+ "This token is out of scope for that project (OutOfScope). The MCP token is restricted to specific projects; pick a project the token can access, or mint a token with broader scope at https://zaaidev.com/dev/settings/tokens."
1329
+ );
1330
+ case 404:
1331
+ return errorContent(
1332
+ `Not found (${cls ?? "not_found"}): ${detail}. The project, brief, or doc id doesn't exist or isn't visible to this token. List first (list_briefs / list_docs / list_captures) to get valid ids.`
1333
+ );
1334
+ case 410:
1335
+ return errorContent(
1336
+ "That project is archived (ProjectArchived) and can no longer be read over MCP. Unarchive it in the workspace to access it again."
1337
+ );
1338
+ case 422:
1339
+ return errorContent(
1340
+ `Unprocessable (${cls ?? "invalid"}): ${detail}. Likely a wrong brief type for this tool or an invalid doc id \u2014 check the tool's expected input against list_briefs / list_docs.`
1341
+ );
1342
+ case 429:
1343
+ return errorContent(
1344
+ "Rate limited (RateLimited). The MCP server retried with backoff and still hit the limit \u2014 wait a moment before the next call."
965
1345
  );
966
1346
  case 0:
967
1347
  return errorContent(
968
- `Network failure reaching the Zaai Dev workspace: ${describe(err.body)}. Check your connection or ZAAI_API_URL setting.`
1348
+ `Network failure reaching the Zaai Dev workspace: ${detail}. Check your connection or ZAAI_API_URL setting.`
969
1349
  );
970
1350
  default:
971
1351
  return errorContent(
972
- `Workspace returned ${err.status} for ${err.path}: ${describe(err.body)}.`
1352
+ `Workspace returned ${err.status}${cls ? ` (${cls})` : ""} for ${err.path}: ${detail}.`
973
1353
  );
974
1354
  }
975
1355
  }
976
1356
  return errorContent(`Unexpected MCP tool failure: ${describe(err)}`);
977
1357
  }
1358
+ function errorClassOf(body) {
1359
+ if (body && typeof body === "object" && "errorClass" in body) {
1360
+ const v = body.errorClass;
1361
+ return typeof v === "string" ? v : null;
1362
+ }
1363
+ return null;
1364
+ }
978
1365
  function errorContent(text) {
979
1366
  return { isError: true, content: [{ type: "text", text }] };
980
1367
  }
@@ -989,7 +1376,7 @@ function describe(value) {
989
1376
  }
990
1377
 
991
1378
  // src/config.ts
992
- var DEFAULT_API_URL = "https://www.zaaistudio.com";
1379
+ var DEFAULT_API_URL = "https://zaaidev.com";
993
1380
  var ConfigError = class extends Error {
994
1381
  constructor(message) {
995
1382
  super(message);
@@ -1000,19 +1387,44 @@ function loadConfig() {
1000
1387
  const apiToken = process.env.ZAAI_API_TOKEN;
1001
1388
  if (!apiToken) {
1002
1389
  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."
1390
+ "ZAAI_API_TOKEN is not set. Mint a token at https://zaaidev.com/dev/settings/tokens (kind = mcp) and pass it via the env in your MCP client config."
1004
1391
  );
1005
1392
  }
1006
1393
  if (!apiToken.startsWith("zaai_mcp_")) {
1007
1394
  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."
1395
+ "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://zaaidev.com/dev/settings/tokens."
1009
1396
  );
1010
1397
  }
1011
1398
  const rawUrl = process.env.ZAAI_API_URL ?? DEFAULT_API_URL;
1012
- const apiUrl = rawUrl.replace(/\/+$/, "");
1399
+ const apiUrl = validateApiUrl(rawUrl.replace(/\/+$/, ""));
1013
1400
  info("config loaded", { apiUrl });
1014
1401
  return { apiToken, apiUrl };
1015
1402
  }
1403
+ function validateApiUrl(value) {
1404
+ let url;
1405
+ try {
1406
+ url = new URL(value);
1407
+ } catch {
1408
+ throw new ConfigError(
1409
+ `ZAAI_API_URL is not a valid URL: ${JSON.stringify(value)}.`
1410
+ );
1411
+ }
1412
+ const host = url.hostname.toLowerCase();
1413
+ const isLocal = host === "localhost" || host === "127.0.0.1";
1414
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocal)) {
1415
+ throw new ConfigError(
1416
+ `ZAAI_API_URL must use https:// (got ${url.protocol}). The MCP token is sent on every request and must never travel in cleartext.`
1417
+ );
1418
+ }
1419
+ const allowAny = process.env.ZAAI_API_URL_ALLOW_ANY === "1";
1420
+ const isZaai = host === "zaaidev.com" || host.endsWith(".zaaidev.com") || host === "zaaistudio.com" || host.endsWith(".zaaistudio.com");
1421
+ if (!isZaai && !isLocal && !allowAny) {
1422
+ throw new ConfigError(
1423
+ `ZAAI_API_URL host ${JSON.stringify(host)} is not a zaaidev.com domain. The MCP token would be sent to it. If this is intentional (self-hosted/staging workspace), set ZAAI_API_URL_ALLOW_ANY=1.`
1424
+ );
1425
+ }
1426
+ return value;
1427
+ }
1016
1428
 
1017
1429
  // src/bin/http.ts
1018
1430
  var DEFAULT_PORT = 3001;