@linktr.ee/arbor-mcp 0.5.0 → 0.6.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.
Files changed (2) hide show
  1. package/dist/index.js +193 -27
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e) {
12
+ throw mod = 0, e;
13
+ }
10
14
  };
11
15
  var __export = (target, all) => {
12
16
  for (var name in all)
@@ -5532,7 +5536,7 @@ ZodNaN.create = (params) => {
5532
5536
  ...processCreateParams(params)
5533
5537
  });
5534
5538
  };
5535
- var BRAND = Symbol("zod_brand");
5539
+ var BRAND = /* @__PURE__ */ Symbol("zod_brand");
5536
5540
  var ZodBranded = class extends ZodType {
5537
5541
  _parse(input) {
5538
5542
  const { ctx } = this._processInputParams(input);
@@ -6130,6 +6134,68 @@ async function resolveVersionId(version, deps = {}) {
6130
6134
  return { ok: true, versionId, versionName };
6131
6135
  }
6132
6136
 
6137
+ // src/lib/intentions.ts
6138
+ var INTENTIONS_ARTIFACT = "component-intentions.json";
6139
+ var INTENTION_STATUSES = [
6140
+ "shipped",
6141
+ "in-progress",
6142
+ "design-only",
6143
+ "deprecated",
6144
+ "researched-not-shipped"
6145
+ ];
6146
+ var STATUS_SET = new Set(INTENTION_STATUSES);
6147
+ function normalizeComponentKey(name) {
6148
+ return name.toLowerCase().replace(/[^a-z0-9]/g, "");
6149
+ }
6150
+ function asStringArray(v) {
6151
+ return Array.isArray(v) ? v.filter((s) => typeof s === "string") : [];
6152
+ }
6153
+ function asDisambiguations(v) {
6154
+ if (!Array.isArray(v)) return [];
6155
+ const out = [];
6156
+ for (const d of v) {
6157
+ if (d && typeof d === "object") {
6158
+ const component = d.component;
6159
+ const distinction = d.distinction;
6160
+ if (typeof component === "string" && typeof distinction === "string") {
6161
+ out.push({ component, distinction });
6162
+ }
6163
+ }
6164
+ }
6165
+ return out;
6166
+ }
6167
+ function coerceEntry(raw) {
6168
+ if (!raw || typeof raw !== "object") return null;
6169
+ const r = raw;
6170
+ const name = typeof r.name === "string" ? r.name.trim() : "";
6171
+ const status = typeof r.status === "string" ? r.status : "";
6172
+ if (!name || !STATUS_SET.has(status)) return null;
6173
+ return {
6174
+ name,
6175
+ status,
6176
+ shippedVersion: typeof r.shippedVersion === "string" ? r.shippedVersion : null,
6177
+ useWhen: asStringArray(r.useWhen),
6178
+ notWhen: asStringArray(r.notWhen),
6179
+ disambiguateFrom: asDisambiguations(r.disambiguateFrom),
6180
+ guideline: typeof r.guideline === "string" ? r.guideline : null,
6181
+ decisions: asStringArray(r.decisions)
6182
+ };
6183
+ }
6184
+ async function loadIntentions(load = loadPublishedJson) {
6185
+ const res = await load(INTENTIONS_ARTIFACT);
6186
+ if (!res.ok) return { ok: false, reason: res.reason };
6187
+ const data = res.data;
6188
+ if (data.degraded === true || !Array.isArray(data.components)) {
6189
+ return { ok: false, reason: "degraded" };
6190
+ }
6191
+ const byKey = /* @__PURE__ */ new Map();
6192
+ for (const raw of data.components) {
6193
+ const entry = coerceEntry(raw);
6194
+ if (entry) byKey.set(normalizeComponentKey(entry.name), entry);
6195
+ }
6196
+ return { ok: true, byKey, generatedAt: typeof data.generatedAt === "string" ? data.generatedAt : null };
6197
+ }
6198
+
6133
6199
  // src/tools/list-components.ts
6134
6200
  var TOOL_NAME3 = "arbor_list_components";
6135
6201
  var TOOL_TITLE3 = "List Arbor components";
@@ -6146,7 +6212,16 @@ var inputSchema3 = external_exports.object({
6146
6212
  var outputSchema3 = external_exports.object({
6147
6213
  source: external_exports.enum(["remote", "unavailable"]),
6148
6214
  total: external_exports.number(),
6149
- components: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), description: external_exports.string().nullable() })),
6215
+ components: external_exports.array(
6216
+ external_exports.object({
6217
+ id: external_exports.string(),
6218
+ name: external_exports.string(),
6219
+ description: external_exports.string().nullable(),
6220
+ // Lifecycle status merged from the component-intentions manifest (DNG-726).
6221
+ // null when the manifest has no entry for this component or could not be loaded.
6222
+ status: external_exports.enum(INTENTION_STATUSES).nullable()
6223
+ })
6224
+ ),
6150
6225
  detail: external_exports.string().nullable()
6151
6226
  });
6152
6227
  var annotations3 = { readOnlyHint: true, idempotentHint: true, openWorldHint: true, destructiveHint: false };
@@ -6196,7 +6271,13 @@ async function handleListComponents(args, deps = {}) {
6196
6271
  (c) => c.name.toLowerCase().includes(q) || (c.description ? c.description.toLowerCase().includes(q) : false)
6197
6272
  );
6198
6273
  }
6199
- out = { source: "remote", total: components.length, components, detail: null };
6274
+ const intentionsRes = await (deps.loadIntentions ?? (() => loadIntentions()))();
6275
+ const byKey = intentionsRes.ok ? intentionsRes.byKey : null;
6276
+ const withStatus = components.map((c) => ({
6277
+ ...c,
6278
+ status: byKey ? byKey.get(normalizeComponentKey(c.name))?.status ?? null : null
6279
+ }));
6280
+ out = { source: "remote", total: withStatus.length, components: withStatus, detail: null };
6200
6281
  }
6201
6282
  const summary = out.source === "unavailable" ? `Could not load components (${out.detail}). Treat as unavailable \u2014 do not assume there are none.` : `${out.total} component${out.total === 1 ? "" : "s"}${args.query ? ` matching "${args.query}"` : ""}.`;
6202
6283
  return { content: [{ type: "text", text: summary }], structuredContent: out };
@@ -6210,7 +6291,7 @@ var listComponentsTool = {
6210
6291
  // src/tools/get-component.ts
6211
6292
  var TOOL_NAME4 = "arbor_get_component";
6212
6293
  var TOOL_TITLE4 = "Get one Arbor component";
6213
- var TOOL_DESCRIPTION4 = "Get a single Arbor component's metadata by name (case-insensitive) or id from Supernova via the Arbor federation proxy (name, id, description). Read-only; matches top-level components, not Figma variants. Does NOT return token values or rendered source \u2014 use the shadcn registry / Storybook for implementation and arbor_open_in_playroom for a runnable snippet. Does NOT change anything.";
6294
+ var TOOL_DESCRIPTION4 = "Get a single Arbor component's metadata by name (case-insensitive) or id (name, id, description) PLUS its intention overlay \u2014 lifecycle status (shipped | in-progress | design-only | deprecated | researched-not-shipped), when to use it, when NOT to (and what to use instead), and what it is commonly confused with. Intention comes from Arbor's source of truth, so a design-only component absent from the live design data still resolves. Read-only; matches top-level components, not Figma variants. Does NOT return token values or rendered source \u2014 use the shadcn registry / Storybook for implementation and arbor_open_in_playroom for a runnable snippet. Does NOT change anything.";
6214
6295
  var inputSchema4 = external_exports.object({
6215
6296
  name: external_exports.string().trim().min(1).max(100).optional().describe('Component name (case-insensitive), e.g. "Button"'),
6216
6297
  id: external_exports.string().trim().min(1).max(100).optional().describe("Supernova component id"),
@@ -6221,53 +6302,114 @@ var inputSchema4 = external_exports.object({
6221
6302
  'Read a specific released version, e.g. "15.1.0" \u2014 resolves to that frozen Supernova version and reads the component there ("what shipped in 15.1.0"). Takes precedence over versionSelector. See arbor_list_versions for the versions that have been cut.'
6222
6303
  )
6223
6304
  }).strict();
6305
+ var disambiguationSchema = external_exports.object({
6306
+ component: external_exports.string().describe("The thing to disambiguate from \u2014 an Arbor component or an external concept (chip, snackbar, \u2026)"),
6307
+ distinction: external_exports.string().describe("How they differ and which to pick when")
6308
+ });
6309
+ var intentionSchema = external_exports.object({
6310
+ status: external_exports.enum(["shipped", "in-progress", "design-only", "deprecated", "researched-not-shipped"]).describe("Lifecycle/availability in the @linktr.ee/arbor package"),
6311
+ shippedVersion: external_exports.string().nullable().describe("The arbor version where this became available, when known"),
6312
+ useWhen: external_exports.array(external_exports.string()).describe("Situations this is the right choice for"),
6313
+ notWhen: external_exports.array(external_exports.string()).describe("When NOT to reach for it, ideally naming the better alternative"),
6314
+ disambiguateFrom: external_exports.array(disambiguationSchema).describe("Things this is commonly confused with"),
6315
+ guideline: external_exports.string().nullable().describe("Repo-relative path to the long-form guideline, when one exists"),
6316
+ decisions: external_exports.array(external_exports.string()).describe("Decision-log ids (DL-NN) that explain this intention")
6317
+ });
6224
6318
  var outputSchema4 = external_exports.object({
6225
- source: external_exports.enum(["remote", "unavailable"]),
6226
- found: external_exports.boolean(),
6319
+ source: external_exports.enum(["remote", "unavailable"]).describe("Live design-data (Supernova) availability"),
6320
+ found: external_exports.boolean().describe("True when a component OR an intention entry matched"),
6227
6321
  component: external_exports.object({ id: external_exports.string(), name: external_exports.string(), description: external_exports.string().nullable() }).nullable(),
6322
+ intention: intentionSchema.nullable().describe("Intention overlay from the source-of-truth manifest; null when no entry matched or it could not be loaded"),
6323
+ intentionSource: external_exports.enum(["remote", "absent", "unavailable"]).describe("'remote' = matched, 'absent' = manifest loaded but no entry, 'unavailable' = manifest could not be loaded"),
6228
6324
  detail: external_exports.string().nullable()
6229
6325
  });
6230
6326
  var annotations4 = { readOnlyHint: true, idempotentHint: true, openWorldHint: true, destructiveHint: false };
6327
+ async function resolveIntention(lookupName, load) {
6328
+ const res = await load();
6329
+ if (!res.ok) return { intention: null, intentionSource: "unavailable" };
6330
+ if (!lookupName) return { intention: null, intentionSource: "absent" };
6331
+ const match = res.byKey.get(normalizeComponentKey(lookupName));
6332
+ if (!match) return { intention: null, intentionSource: "absent" };
6333
+ const intention = {
6334
+ status: match.status,
6335
+ shippedVersion: match.shippedVersion,
6336
+ useWhen: match.useWhen,
6337
+ notWhen: match.notWhen,
6338
+ disambiguateFrom: match.disambiguateFrom.map((d) => ({ component: d.component, distinction: d.distinction })),
6339
+ guideline: match.guideline,
6340
+ decisions: match.decisions
6341
+ };
6342
+ return { intention, intentionSource: "remote" };
6343
+ }
6231
6344
  async function handleGetComponent(args, deps = {}) {
6232
6345
  const name = args.name?.trim();
6233
6346
  const id = args.id?.trim();
6347
+ const loadIntentionsFn = deps.loadIntentions ?? (() => loadIntentions());
6234
6348
  if (!name && !id) {
6235
- const out2 = { source: "remote", found: false, component: null, detail: "provide name or id" };
6349
+ const out2 = {
6350
+ source: "remote",
6351
+ found: false,
6352
+ component: null,
6353
+ intention: null,
6354
+ intentionSource: "absent",
6355
+ detail: "provide name or id"
6356
+ };
6236
6357
  return {
6237
6358
  content: [{ type: "text", text: 'Provide a component name or id \u2014 e.g. {"name":"Button"} or {"id":"35988997"}.' }],
6238
6359
  structuredContent: out2
6239
6360
  };
6240
6361
  }
6241
6362
  const query = deps.query ?? ((r, opts) => querySupernova(r, opts));
6242
- let queryOpts;
6363
+ let supernovaSource = "remote";
6364
+ let component = null;
6365
+ let supernovaDetail = null;
6366
+ let queryOpts = null;
6243
6367
  if (args.version) {
6244
6368
  const resolveVersion = deps.resolveVersion ?? ((v) => resolveVersionId(v));
6245
6369
  const resolved = await resolveVersion(args.version);
6246
- if (!resolved.ok) {
6247
- const out2 = { source: "unavailable", found: false, component: null, detail: resolved.detail };
6248
- return {
6249
- content: [{ type: "text", text: `Could not read version ${args.version}: ${resolved.detail}. Treat as unavailable.` }],
6250
- structuredContent: out2
6251
- };
6370
+ if (resolved.ok) {
6371
+ queryOpts = { versionId: resolved.versionId };
6372
+ } else {
6373
+ supernovaSource = "unavailable";
6374
+ supernovaDetail = resolved.detail;
6252
6375
  }
6253
- queryOpts = { versionId: resolved.versionId };
6254
6376
  } else {
6255
6377
  queryOpts = { versionSelector: args.versionSelector };
6256
6378
  }
6257
- const result = await query("components", queryOpts);
6258
- let out;
6259
- if (!result.ok) {
6260
- out = { source: "unavailable", found: false, component: null, detail: `could not load components (${result.reason})` };
6261
- } else {
6262
- const components = extractComponents(result.data);
6263
- if (components === null) {
6264
- out = { source: "unavailable", found: false, component: null, detail: "proxy returned an unrecognized components payload" };
6379
+ if (queryOpts) {
6380
+ const result = await query("components", queryOpts);
6381
+ if (!result.ok) {
6382
+ supernovaSource = "unavailable";
6383
+ supernovaDetail = `could not load components (${result.reason})`;
6265
6384
  } else {
6266
- const match = id ? components.find((c) => c.id === id) : components.find((c) => c.name.toLowerCase() === name.toLowerCase());
6267
- out = match ? { source: "remote", found: true, component: match, detail: null } : { source: "remote", found: false, component: null, detail: `no component ${id ? `with id ${id}` : `named "${name}"`}` };
6385
+ const components = extractComponents(result.data);
6386
+ if (components === null) {
6387
+ supernovaSource = "unavailable";
6388
+ supernovaDetail = "proxy returned an unrecognized components payload";
6389
+ } else {
6390
+ const target = name ? normalizeComponentKey(name) : null;
6391
+ const match = id ? components.find((c) => c.id === id) : components.find((c) => normalizeComponentKey(c.name) === target);
6392
+ if (match) component = match;
6393
+ else supernovaDetail = `no component ${id ? `with id ${id}` : `named "${name}"`}`;
6394
+ }
6268
6395
  }
6269
6396
  }
6270
- const summary = out.source === "unavailable" ? `Could not load components (${out.detail}). Treat as unavailable.` : out.found ? `${out.component.name}${out.component.description ? ` \u2014 ${out.component.description}` : " (no description)"}.` : `No Arbor component ${args.id ? `with id ${args.id.trim()}` : `named "${args.name?.trim()}"`} exists.`;
6397
+ const lookupName = component?.name ?? name ?? null;
6398
+ const { intention, intentionSource } = await resolveIntention(lookupName, loadIntentionsFn);
6399
+ const found = component !== null || intention !== null;
6400
+ let detail = supernovaDetail;
6401
+ if (!component && intention) {
6402
+ detail = supernovaSource === "unavailable" ? `live design data unavailable (${supernovaDetail}); intention found \u2014 status "${intention.status}"` : `not in the live design data, but tracked as "${intention.status}" in Arbor's component intentions`;
6403
+ }
6404
+ const out = { source: supernovaSource, found, component, intention, intentionSource, detail };
6405
+ let summary;
6406
+ if (!found) {
6407
+ summary = supernovaSource === "unavailable" ? `Could not load components (${supernovaDetail}). Treat as unavailable.` : `No Arbor component ${id ? `with id ${id}` : `named "${name}"`} exists.`;
6408
+ } else {
6409
+ const head = component ? `${component.name}${component.description ? ` \u2014 ${component.description}` : " (no description)"}.` : `${lookupName} \u2014 ${detail}.`;
6410
+ const intentionLine = intention ? ` Status: ${intention.status}.${intention.useWhen.length ? ` Use when: ${intention.useWhen[0]}` : ""}` : intentionSource === "unavailable" ? " (intention overlay unavailable)" : "";
6411
+ summary = `${head}${intentionLine}`;
6412
+ }
6271
6413
  return { content: [{ type: "text", text: summary }], structuredContent: out };
6272
6414
  }
6273
6415
  var getComponentTool = {
@@ -7177,6 +7319,30 @@ var compositions_default = [
7177
7319
  code: `<RadioGroup defaultValue="option-1">
7178
7320
  <RadioButton value="option-1" variant="pill">Option 1</RadioButton>
7179
7321
  <RadioButton value="option-2" variant="pill">Option 2</RadioButton>
7322
+ </RadioGroup>`
7323
+ },
7324
+ // ── Radio ──
7325
+ // Maps to figma-mappings `Radio` (canonical control, node 9232-27291). The bare
7326
+ // 20px control must render inside a RadioGroup; pair each with an accessible
7327
+ // name (aria-label here, or a sibling <label htmlFor> in real usage).
7328
+ {
7329
+ group: "Radio",
7330
+ name: "Radio",
7331
+ code: `<RadioGroup defaultValue="email" aria-label="Notifications">
7332
+ <Radio value="email" aria-label="Email" />
7333
+ <Radio value="sms" aria-label="SMS" />
7334
+ <Radio value="push" aria-label="Push notification" />
7335
+ </RadioGroup>`
7336
+ },
7337
+ // ── RadioItem ──
7338
+ // Maps to figma-mappings `RadioItem` (canonical control + label, node 12360-58265).
7339
+ {
7340
+ group: "RadioItem",
7341
+ name: "RadioItem",
7342
+ code: `<RadioGroup defaultValue="email" aria-label="Notifications">
7343
+ <RadioItem value="email" description="We'll send a confirmation">Email</RadioItem>
7344
+ <RadioItem value="sms">SMS</RadioItem>
7345
+ <RadioItem value="push" disabled>Push notification</RadioItem>
7180
7346
  </RadioGroup>`
7181
7347
  },
7182
7348
  // ── SearchInput ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/arbor-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Model Context Protocol server exposing Arbor design system tools: Playroom snippets, Figma→code sync health, UX-writing checks, decision-log lookup, and federated component/version metadata.",
5
5
  "keywords": [
6
6
  "arbor",
@@ -45,7 +45,7 @@
45
45
  "@modelcontextprotocol/sdk": "^1.29.0"
46
46
  },
47
47
  "devDependencies": {
48
- "esbuild": "^0.25.0",
48
+ "esbuild": "^0.28.1",
49
49
  "tsx": "^4.7.0",
50
50
  "typescript": "^5.7.0",
51
51
  "zod": "3.25.76"