@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.
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.1.0" : "0.0.0-dev";
13
+ var PKG_VERSION = true ? "0.3.0" : "0.0.0-dev";
14
14
 
15
15
  // src/tools/health.ts
16
16
  var healthInputSchema = z.object({});
@@ -50,25 +50,48 @@ var WorkspaceApiError = class extends Error {
50
50
  body;
51
51
  path;
52
52
  };
53
+ var MAX_ATTEMPTS = 3;
54
+ var BASE_BACKOFF_MS = 250;
55
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
53
56
  async function mcpApiFetch(config, path, init = {}) {
54
57
  const url = `${config.apiUrl}${path}`;
55
- let res;
56
- try {
57
- res = await fetch(url, {
58
- ...init,
59
- headers: {
60
- ...bearerHeader(config.apiToken),
61
- ...init.headers ?? {}
58
+ let lastErr = null;
59
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
60
+ let res;
61
+ try {
62
+ res = await fetch(url, {
63
+ ...init,
64
+ headers: {
65
+ ...bearerHeader(config.apiToken),
66
+ ...init.headers ?? {}
67
+ }
68
+ });
69
+ } catch (err2) {
70
+ lastErr = new WorkspaceApiError(0, { message: String(err2) }, path);
71
+ if (attempt < MAX_ATTEMPTS) {
72
+ await sleep(BASE_BACKOFF_MS * 2 ** (attempt - 1));
73
+ continue;
62
74
  }
63
- });
64
- } catch (err) {
65
- throw new WorkspaceApiError(0, { message: String(err) }, path);
66
- }
67
- if (!res.ok) {
75
+ throw lastErr;
76
+ }
77
+ if (res.ok) {
78
+ return await res.json();
79
+ }
68
80
  const body = await res.json().catch(() => ({ error: `Non-JSON ${res.status} response` }));
69
- throw new WorkspaceApiError(res.status, body, path);
81
+ const err = new WorkspaceApiError(res.status, body, path);
82
+ if (RETRYABLE_STATUSES.has(res.status) || res.status >= 502 && res.status <= 504) {
83
+ lastErr = err;
84
+ if (attempt < MAX_ATTEMPTS) {
85
+ await sleep(BASE_BACKOFF_MS * 2 ** (attempt - 1));
86
+ continue;
87
+ }
88
+ }
89
+ throw err;
70
90
  }
71
- return await res.json();
91
+ throw lastErr ?? new WorkspaceApiError(0, { message: "no attempts" }, path);
92
+ }
93
+ function sleep(ms) {
94
+ return new Promise((resolve) => setTimeout(resolve, ms));
72
95
  }
73
96
 
74
97
  // src/tools/whoami.ts
@@ -219,6 +242,212 @@ async function getMedia(store, args) {
219
242
  return store.getField(args.id, "media");
220
243
  }
221
244
 
245
+ // src/tools/get-brand-brief.ts
246
+ import { z as z7 } from "zod";
247
+ var getBrandBriefInputSchema = z7.object({
248
+ project_id: z7.string().uuid().describe(
249
+ "Zaai Dev project UUID. Find via the workspace at zaaistudio.com/dev/projects."
250
+ )
251
+ });
252
+ var getBrandBriefOutputSchema = z7.object({
253
+ project_id: z7.string(),
254
+ project_name: z7.string(),
255
+ version: z7.string().describe("Brief version number, or '0' if no brief has been generated yet."),
256
+ status: z7.enum(["draft", "published", "archived"]),
257
+ positioning: z7.string(),
258
+ values: z7.array(z7.string()),
259
+ voice: z7.object({
260
+ one_liner: z7.string(),
261
+ do_say: z7.array(z7.string()),
262
+ dont_say: z7.array(z7.string())
263
+ }),
264
+ audience: z7.object({
265
+ primary: z7.string(),
266
+ secondary: z7.string().nullable(),
267
+ needs: z7.array(z7.string())
268
+ }),
269
+ design_intent: z7.object({
270
+ descriptors: z7.array(z7.string()),
271
+ anti_descriptors: z7.array(z7.string())
272
+ }),
273
+ decisions: z7.array(
274
+ z7.object({
275
+ id: z7.string(),
276
+ title: z7.string(),
277
+ decided_at: z7.string(),
278
+ rationale: z7.string()
279
+ })
280
+ ),
281
+ updated_at: z7.string()
282
+ }).passthrough();
283
+ async function getBrandBrief(store, args) {
284
+ return store.getBrandBrief(args.project_id);
285
+ }
286
+
287
+ // src/tools/get-voice.ts
288
+ import { z as z8 } from "zod";
289
+ var getVoiceInputSchema = z8.object({
290
+ project_id: z8.string().uuid().describe("Zaai Dev project UUID.")
291
+ });
292
+ var getVoiceOutputSchema = z8.object({
293
+ project_id: z8.string(),
294
+ one_liner: z8.string().describe("One-sentence summary of how the brand sounds."),
295
+ tone_descriptors: z8.array(z8.string()).describe("Adjectives the brand voice aims for."),
296
+ do_say: z8.array(z8.string()).describe("Phrasings / patterns the brand actively uses."),
297
+ dont_say: z8.array(z8.string()).describe("Phrasings / patterns the brand explicitly avoids."),
298
+ example_phrases: z8.array(z8.string()).describe("Concrete phrases that exemplify the voice.")
299
+ }).passthrough();
300
+ async function getVoice(store, args) {
301
+ return store.getVoice(args.project_id);
302
+ }
303
+
304
+ // src/tools/get-audience.ts
305
+ import { z as z9 } from "zod";
306
+ var getAudienceInputSchema = z9.object({
307
+ project_id: z9.string().uuid().describe("Zaai Dev project UUID.")
308
+ });
309
+ var getAudienceOutputSchema = z9.object({
310
+ project_id: z9.string(),
311
+ primary: z9.string().describe("Primary audience segment."),
312
+ secondary: z9.string().nullable().describe("Secondary audience segment, or null."),
313
+ needs: z9.array(z9.string()).describe("Jobs-to-be-done / pain points the audience has."),
314
+ channels: z9.array(z9.string()).describe("Where this audience consumes content.")
315
+ }).passthrough();
316
+ async function getAudience(store, args) {
317
+ return store.getAudience(args.project_id);
318
+ }
319
+
320
+ // src/tools/get-design-intent.ts
321
+ import { z as z10 } from "zod";
322
+ var getDesignIntentInputSchema = z10.object({
323
+ project_id: z10.string().uuid().describe("Zaai Dev project UUID.")
324
+ });
325
+ var getDesignIntentOutputSchema = z10.object({
326
+ project_id: z10.string(),
327
+ descriptors: z10.array(z10.string()).describe(
328
+ "Visual adjectives the brand aims for (e.g. 'editorial', 'spacious', 'monochrome')."
329
+ ),
330
+ anti_descriptors: z10.array(z10.string()).describe("Visual adjectives the brand explicitly avoids."),
331
+ inspiration_summary: z10.string().describe("Free-form summary of the inspiration the brief points at.")
332
+ }).passthrough();
333
+ async function getDesignIntent(store, args) {
334
+ return store.getDesignIntent(args.project_id);
335
+ }
336
+
337
+ // src/tools/get-brand-tokens.ts
338
+ import { z as z11 } from "zod";
339
+ var getBrandTokensInputSchema = z11.object({
340
+ project_id: z11.string().uuid().describe("Zaai Dev project UUID.")
341
+ });
342
+ var getBrandTokensOutputSchema = z11.object({
343
+ project_id: z11.string(),
344
+ colors: z11.array(
345
+ z11.object({
346
+ name: z11.string(),
347
+ value: z11.string().describe("Hex / rgba / hsl value as authored."),
348
+ role: z11.string().optional().describe("Role like 'primary' / 'accent' / 'surface'.")
349
+ })
350
+ ),
351
+ fonts: z11.array(
352
+ z11.object({
353
+ role: z11.string().describe("'display', 'body', 'mono', etc."),
354
+ family: z11.string(),
355
+ weights: z11.array(z11.number().int())
356
+ })
357
+ ),
358
+ radius: z11.array(z11.object({ name: z11.string(), value: z11.string() })),
359
+ shadows: z11.array(z11.object({ name: z11.string(), value: z11.string() }))
360
+ }).passthrough();
361
+ async function getBrandTokens(store, args) {
362
+ return store.getBrandTokens(args.project_id);
363
+ }
364
+
365
+ // src/tools/get-decisions.ts
366
+ import { z as z12 } from "zod";
367
+ var getDecisionsInputSchema = z12.object({
368
+ project_id: z12.string().uuid().describe("Zaai Dev project UUID."),
369
+ limit: z12.number().int().min(1).max(200).optional().describe("Max decisions to return. Default 50, max 200, newest first.")
370
+ });
371
+ var getDecisionsOutputSchema = z12.object({
372
+ project_id: z12.string(),
373
+ items: z12.array(
374
+ z12.object({
375
+ id: z12.string(),
376
+ title: z12.string(),
377
+ rationale: z12.string(),
378
+ attribution: z12.string().describe(
379
+ "Who decided. 'mcp:<token_id>' for AI-logged decisions; user email / id for workspace-authored ones."
380
+ ),
381
+ brief_field: z12.string().nullable().describe("Which brief slice this decision concerns, if any (e.g. 'voice.tone')."),
382
+ reference_capture_id: z12.string().nullable(),
383
+ created_at: z12.string()
384
+ }).passthrough()
385
+ )
386
+ }).passthrough();
387
+ async function getDecisions(store, args) {
388
+ return store.getDecisions(args.project_id, args.limit);
389
+ }
390
+
391
+ // src/tools/get-references.ts
392
+ import { z as z13 } from "zod";
393
+ var getReferencesInputSchema = z13.object({
394
+ project_id: z13.string().uuid().describe("Zaai Dev project UUID."),
395
+ limit: z13.number().int().min(1).max(100).optional().describe("Max references to return. Default 20, max 100, newest first."),
396
+ cursor: z13.string().optional().describe("Opaque pagination cursor from the previous response's next_cursor.")
397
+ });
398
+ var referenceItem = z13.object({
399
+ id: z13.string(),
400
+ type: z13.enum(["page", "element", "composite"]),
401
+ source_url: z13.string(),
402
+ source_title: z13.string(),
403
+ note: z13.string().nullable(),
404
+ captured_at: z13.string(),
405
+ thumbnail_url: z13.string().nullable(),
406
+ screenshot_url: z13.string().nullable()
407
+ }).passthrough();
408
+ var getReferencesOutputSchema = z13.object({
409
+ project_id: z13.string(),
410
+ items: z13.array(referenceItem),
411
+ next_cursor: z13.string().nullable()
412
+ }).passthrough();
413
+ async function getReferences(store, args) {
414
+ return store.getReferences(args.project_id, {
415
+ limit: args.limit,
416
+ cursor: args.cursor
417
+ });
418
+ }
419
+
420
+ // src/tools/search-references.ts
421
+ import { z as z14 } from "zod";
422
+ var searchReferencesInputSchema = z14.object({
423
+ project_id: z14.string().uuid().describe("Zaai Dev project UUID."),
424
+ q: z14.string().min(1).max(200).describe("Keyword query \u2014 matched against title / url / note (case-insensitive substring)."),
425
+ limit: z14.number().int().min(1).max(50).optional().describe("Max results to return. Default 10, max 50.")
426
+ });
427
+ var searchReferenceItem = z14.object({
428
+ id: z14.string(),
429
+ type: z14.enum(["page", "element", "composite"]),
430
+ source_url: z14.string(),
431
+ source_title: z14.string(),
432
+ note: z14.string().nullable(),
433
+ captured_at: z14.string(),
434
+ thumbnail_url: z14.string().nullable(),
435
+ screenshot_url: z14.string().nullable(),
436
+ score: z14.number().describe("Relevance score 0..1. v1 keyword scoring: 1.0 title / 0.7 url / 0.5 note."),
437
+ matched_field: z14.enum(["title", "url", "note", "tag"])
438
+ }).passthrough();
439
+ var searchReferencesOutputSchema = z14.object({
440
+ project_id: z14.string(),
441
+ query: z14.string(),
442
+ items: z14.array(searchReferenceItem)
443
+ }).passthrough();
444
+ async function searchReferences(store, args) {
445
+ return store.searchReferences(args.project_id, {
446
+ q: args.q,
447
+ limit: args.limit
448
+ });
449
+ }
450
+
222
451
  // src/store/http-store.ts
223
452
  var HttpCaptureStore = class {
224
453
  constructor(config) {
@@ -250,6 +479,72 @@ var HttpCaptureStore = class {
250
479
  }
251
480
  };
252
481
 
482
+ // src/store/project-store.ts
483
+ var HttpProjectStore = class {
484
+ constructor(config) {
485
+ this.config = config;
486
+ }
487
+ config;
488
+ async getBrandBrief(projectId) {
489
+ return mcpApiFetch(
490
+ this.config,
491
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/brief`
492
+ );
493
+ }
494
+ async getVoice(projectId) {
495
+ return mcpApiFetch(
496
+ this.config,
497
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/voice`
498
+ );
499
+ }
500
+ async getAudience(projectId) {
501
+ return mcpApiFetch(
502
+ this.config,
503
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/audience`
504
+ );
505
+ }
506
+ async getDesignIntent(projectId) {
507
+ return mcpApiFetch(
508
+ this.config,
509
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/design-intent`
510
+ );
511
+ }
512
+ async getBrandTokens(projectId) {
513
+ return mcpApiFetch(
514
+ this.config,
515
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/brand-tokens`
516
+ );
517
+ }
518
+ async getDecisions(projectId, limit) {
519
+ const sp = new URLSearchParams();
520
+ if (limit !== void 0) sp.set("limit", String(limit));
521
+ const qs = sp.toString();
522
+ return mcpApiFetch(
523
+ this.config,
524
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/decisions${qs ? `?${qs}` : ""}`
525
+ );
526
+ }
527
+ async getReferences(projectId, args) {
528
+ const sp = new URLSearchParams();
529
+ if (args.limit !== void 0) sp.set("limit", String(args.limit));
530
+ if (args.cursor) sp.set("cursor", args.cursor);
531
+ const qs = sp.toString();
532
+ return mcpApiFetch(
533
+ this.config,
534
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/references${qs ? `?${qs}` : ""}`
535
+ );
536
+ }
537
+ async searchReferences(projectId, args) {
538
+ const sp = new URLSearchParams();
539
+ sp.set("q", args.q);
540
+ if (args.limit !== void 0) sp.set("limit", String(args.limit));
541
+ return mcpApiFetch(
542
+ this.config,
543
+ `/api/mcp/projects/${encodeURIComponent(projectId)}/references/search?${sp.toString()}`
544
+ );
545
+ }
546
+ };
547
+
253
548
  // src/resources/capture-resource.ts
254
549
  import {
255
550
  ResourceTemplate
@@ -315,6 +610,7 @@ function createServer(config) {
315
610
  version: PKG_VERSION
316
611
  });
317
612
  const captureStore = new HttpCaptureStore(config);
613
+ const projectStore = new HttpProjectStore(config);
318
614
  server.registerTool(
319
615
  "health",
320
616
  {
@@ -481,6 +777,118 @@ function createServer(config) {
481
777
  },
482
778
  makeFocusedGetterHandler(captureStore, getMedia, "media")
483
779
  );
780
+ server.registerTool(
781
+ "get_brand_brief",
782
+ {
783
+ title: "Get brand brief",
784
+ 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.",
785
+ inputSchema: getBrandBriefInputSchema.shape,
786
+ outputSchema: getBrandBriefOutputSchema.shape
787
+ },
788
+ makeProjectToolHandler(
789
+ projectStore,
790
+ getBrandBrief,
791
+ (r) => `Brief v${r.version} (${r.status}) for "${r.project_name}". Voice: ${r.voice.one_liner || "\u2014"}.`
792
+ )
793
+ );
794
+ server.registerTool(
795
+ "get_voice",
796
+ {
797
+ title: "Get voice guidelines",
798
+ 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.",
799
+ inputSchema: getVoiceInputSchema.shape,
800
+ outputSchema: getVoiceOutputSchema.shape
801
+ },
802
+ makeProjectToolHandler(
803
+ projectStore,
804
+ getVoice,
805
+ (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"}.`
806
+ )
807
+ );
808
+ server.registerTool(
809
+ "get_audience",
810
+ {
811
+ title: "Get audience profile",
812
+ 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.",
813
+ inputSchema: getAudienceInputSchema.shape,
814
+ outputSchema: getAudienceOutputSchema.shape
815
+ },
816
+ makeProjectToolHandler(
817
+ projectStore,
818
+ getAudience,
819
+ (r) => `Audience: ${r.primary || "\u2014"}` + (r.secondary ? ` (secondary: ${r.secondary})` : "") + `. ${r.needs.length} stated need${r.needs.length === 1 ? "" : "s"}.`
820
+ )
821
+ );
822
+ server.registerTool(
823
+ "get_design_intent",
824
+ {
825
+ title: "Get design intent",
826
+ 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.",
827
+ inputSchema: getDesignIntentInputSchema.shape,
828
+ outputSchema: getDesignIntentOutputSchema.shape
829
+ },
830
+ makeProjectToolHandler(
831
+ projectStore,
832
+ getDesignIntent,
833
+ (r) => `${r.descriptors.length} descriptor${r.descriptors.length === 1 ? "" : "s"}, ${r.anti_descriptors.length} anti-descriptor${r.anti_descriptors.length === 1 ? "" : "s"}.`
834
+ )
835
+ );
836
+ server.registerTool(
837
+ "get_brand_tokens",
838
+ {
839
+ title: "Get brand tokens",
840
+ 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.",
841
+ inputSchema: getBrandTokensInputSchema.shape,
842
+ outputSchema: getBrandTokensOutputSchema.shape
843
+ },
844
+ makeProjectToolHandler(
845
+ projectStore,
846
+ getBrandTokens,
847
+ (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"}.`
848
+ )
849
+ );
850
+ server.registerTool(
851
+ "get_decisions",
852
+ {
853
+ title: "Get decisions log",
854
+ 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.",
855
+ inputSchema: getDecisionsInputSchema.shape,
856
+ outputSchema: getDecisionsOutputSchema.shape
857
+ },
858
+ makeProjectToolHandler(
859
+ projectStore,
860
+ getDecisions,
861
+ (r) => `${r.items.length} decision${r.items.length === 1 ? "" : "s"} returned (newest first).`
862
+ )
863
+ );
864
+ server.registerTool(
865
+ "get_references",
866
+ {
867
+ title: "Get project references",
868
+ 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.",
869
+ inputSchema: getReferencesInputSchema.shape,
870
+ outputSchema: getReferencesOutputSchema.shape
871
+ },
872
+ makeProjectToolHandler(
873
+ projectStore,
874
+ getReferences,
875
+ (r) => `${r.items.length} reference${r.items.length === 1 ? "" : "s"} returned` + (r.next_cursor ? " (more available \u2014 pass next_cursor for the next page)." : ".")
876
+ )
877
+ );
878
+ server.registerTool(
879
+ "search_references",
880
+ {
881
+ title: "Search project references",
882
+ 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.",
883
+ inputSchema: searchReferencesInputSchema.shape,
884
+ outputSchema: searchReferencesOutputSchema.shape
885
+ },
886
+ makeProjectToolHandler(
887
+ projectStore,
888
+ searchReferences,
889
+ (r) => r.items.length === 0 ? `No references match "${r.query}".` : `${r.items.length} reference${r.items.length === 1 ? "" : "s"} matching "${r.query}".`
890
+ )
891
+ );
484
892
  registerCaptureResource(server, captureStore);
485
893
  info("server initialized", {
486
894
  version: PKG_VERSION,
@@ -493,12 +901,33 @@ function createServer(config) {
493
901
  "get_palette",
494
902
  "get_html",
495
903
  "get_animation",
496
- "get_media"
904
+ "get_media",
905
+ "get_brand_brief",
906
+ "get_voice",
907
+ "get_audience",
908
+ "get_design_intent",
909
+ "get_brand_tokens",
910
+ "get_decisions",
911
+ "get_references",
912
+ "search_references"
497
913
  ],
498
914
  resources: ["zaai-capture://{id}"]
499
915
  });
500
916
  return server;
501
917
  }
918
+ function makeProjectToolHandler(store, fn, summary) {
919
+ return async (args) => {
920
+ try {
921
+ const result = await fn(store, args);
922
+ return {
923
+ structuredContent: result,
924
+ content: [{ type: "text", text: summary(result) }]
925
+ };
926
+ } catch (err) {
927
+ return toolErrorResponse(err);
928
+ }
929
+ };
930
+ }
502
931
  function makeFocusedGetterHandler(store, fn, label) {
503
932
  return async (args) => {
504
933
  try {