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