@feimacode/open-design-agent-kit-mcp 0.1.5 → 0.1.7

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 (3) hide show
  1. package/README.md +10 -3
  2. package/out/index.js +455 -83
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,10 +1,17 @@
1
1
  # @feimacode/open-design-agent-kit-mcp
2
2
 
3
- The [OpenDesign](https://github.com/nexu-io/open-design) skill/design-system/artifact workflow as a standalone MCP server — for Claude Code, Codex, Cursor, or any other MCP-capable coding agent. Same nine tools and the same content library (163 skills, 114 design templates, 152 brand design systems, 167 remixable examples) as the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=feima.open-design-agent-kit), over stdio instead of native `languageModelTools`.
3
+ **Pro-grade design for any MCP-capable coding agent. One command, nothing else to run.**
4
4
 
5
- No daemon, no account, no editor required. Your agent's own selected model does the actual generation with its normal file-editing tools; this server only composes instructions and does the bookkeeping (manifests, active design system) around them.
5
+ This is [Open Design](https://github.com/nexu-io/open-design)'s design library and workflow, packaged as a plain stdio MCP server for Claude Code, Codex, Cursor, and anything else that speaks MCP. It includes 163 skills, 114 design templates, 152 brand design systems, and 167 remixable examples. Ask your agent for a deck, a landing page, a dashboard, or a prototype, and it builds one that looks designed rather than generated, as real files in your project.
6
6
 
7
- ## What you get
7
+ - **No desktop app, no daemon, no account.** Your agent launches the server on demand over stdio, and it exits when the agent does.
8
+ - **No API keys or model settings.** The server never calls a model. Your agent's own selected model does the generation with its normal file-editing tools. The server supplies the library and handles the bookkeeping (manifests, active design system).
9
+ - **Zero config.** It works in the current project out of the box. The active design system is remembered in `.open-design/config.json`.
10
+ - **Plain files.** Everything it produces is ordinary HTML plus small JSON sidecars in your repo, so you can diff, review, and commit it.
11
+
12
+ It has the same core tools and the same content as the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=feima.open-design-agent-kit), over MCP instead of native `languageModelTools`.
13
+
14
+ ## See what it builds
8
15
 
9
16
  Four of the ~280 vendored skills and templates, rendered exactly as-is:
10
17
 
package/out/index.js CHANGED
@@ -3341,11 +3341,11 @@ var require_excerpt = __commonJS({
3341
3341
  if (typeof opts.excerpt === "function") {
3342
3342
  return opts.excerpt(file, opts);
3343
3343
  }
3344
- const sep2 = file.data.excerpt_separator || opts.excerpt_separator;
3345
- if (sep2 == null && (opts.excerpt === false || opts.excerpt == null)) {
3344
+ const sep3 = file.data.excerpt_separator || opts.excerpt_separator;
3345
+ if (sep3 == null && (opts.excerpt === false || opts.excerpt == null)) {
3346
3346
  return file;
3347
3347
  }
3348
- const delimiter = typeof opts.excerpt === "string" ? opts.excerpt : sep2 || opts.delimiters[0];
3348
+ const delimiter = typeof opts.excerpt === "string" ? opts.excerpt : sep3 || opts.delimiters[0];
3349
3349
  const idx = file.content.indexOf(delimiter);
3350
3350
  if (idx !== -1) {
3351
3351
  file.excerpt = file.content.slice(0, idx);
@@ -3410,7 +3410,7 @@ var require_parse = __commonJS({
3410
3410
  var require_gray_matter = __commonJS({
3411
3411
  "../../node_modules/gray-matter/index.js"(exports2, module2) {
3412
3412
  "use strict";
3413
- var fs8 = require("fs");
3413
+ var fs9 = require("fs");
3414
3414
  var sections = require_section_matter();
3415
3415
  var defaults = require_defaults();
3416
3416
  var stringify = require_stringify();
@@ -3494,7 +3494,7 @@ var require_gray_matter = __commonJS({
3494
3494
  return stringify(file, data, options2);
3495
3495
  };
3496
3496
  matter2.read = function(filepath, options2) {
3497
- const str2 = fs8.readFileSync(filepath, "utf8");
3497
+ const str2 = fs9.readFileSync(filepath, "utf8");
3498
3498
  const file = matter2(str2, options2);
3499
3499
  file.path = filepath;
3500
3500
  return file;
@@ -3531,7 +3531,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
3531
3531
  var import_node_fs = require("node:fs");
3532
3532
  var path = __toESM(require("node:path"));
3533
3533
  var import_gray_matter = __toESM(require_gray_matter());
3534
- var SKILL_SOURCES = ["skill", "design-template", "example"];
3534
+ var SKILL_SOURCES = ["skill", "design-template", "example", "community"];
3535
3535
  var SKILL_MODES = [
3536
3536
  "prototype",
3537
3537
  "deck",
@@ -3611,7 +3611,7 @@ async function loadSkillLikeDir(assetsRoot, subdir, source) {
3611
3611
  }
3612
3612
  return result;
3613
3613
  }
3614
- async function loadExamples(assetsRoot) {
3614
+ async function loadExamples(assetsRoot, source = "example") {
3615
3615
  const dir = path.join(assetsRoot, "examples");
3616
3616
  const result = /* @__PURE__ */ new Map();
3617
3617
  if (!await pathExists(dir)) return result;
@@ -3642,7 +3642,7 @@ async function loadExamples(assetsRoot) {
3642
3642
  triggers: [...normalizeStringArray(data.triggers), ...normalizeStringArray(data.tags)],
3643
3643
  category: typeof od?.category === "string" ? od.category : typeof data.category === "string" ? data.category : void 0,
3644
3644
  mode: normalizeMode(od?.mode),
3645
- source: "example",
3645
+ source,
3646
3646
  examplePrompt,
3647
3647
  featured: false,
3648
3648
  exampleArtifactPath: path.posix.join("examples", entry.name, "example.html"),
@@ -3771,9 +3771,10 @@ function mergeSkillPools(...pools) {
3771
3771
  return result;
3772
3772
  }
3773
3773
  var ContentIndex = class {
3774
- constructor(assetsRoot, getUserDesignSystemsDir) {
3774
+ constructor(assetsRoot, getUserDesignSystemsDir, getCommunityContentDir) {
3775
3775
  this.assetsRoot = assetsRoot;
3776
3776
  this.getUserDesignSystemsDir = getUserDesignSystemsDir;
3777
+ this.getCommunityContentDir = getCommunityContentDir;
3777
3778
  }
3778
3779
  ensureLoaded() {
3779
3780
  if (!this.loaded) {
@@ -3791,8 +3792,24 @@ var ContentIndex = class {
3791
3792
  }
3792
3793
  return this.loaded;
3793
3794
  }
3794
- async listSkills(query, mode, source, remixableOnly) {
3795
+ // Merges the cached built-in skill/template/example pool with a
3796
+ // freshly-rescanned community pool on every call — same reasoning as
3797
+ // loadedDesignSystems()'s user-pool rescan: a sync command can replace the
3798
+ // on-disk community cache at any moment in the session, with no reload.
3799
+ // mergeSkillPools regroups by each entry's own `.id` (dirId) regardless of
3800
+ // whether its input map is raw or already merged, so re-running it here
3801
+ // against the cached (already-merged) pool plus the fresh community pool
3802
+ // correctly re-applies collision-priority ordering (community always
3803
+ // loses) without re-deriving anything from the cached pool.
3804
+ async loadedSkills() {
3795
3805
  const { skills } = await this.ensureLoaded();
3806
+ const communityDir = this.getCommunityContentDir?.();
3807
+ const communitySkills = communityDir ? await loadExamples(communityDir, "community") : /* @__PURE__ */ new Map();
3808
+ if (communitySkills.size === 0) return skills;
3809
+ return mergeSkillPools(skills, communitySkills);
3810
+ }
3811
+ async listSkills(query, mode, source, remixableOnly) {
3812
+ const skills = await this.loadedSkills();
3796
3813
  const wantedMode = mode?.trim().toLowerCase();
3797
3814
  const wantedSource = source?.trim().toLowerCase();
3798
3815
  const results = [];
@@ -3818,7 +3835,7 @@ var ContentIndex = class {
3818
3835
  return results;
3819
3836
  }
3820
3837
  async getSkill(id) {
3821
- const { skills } = await this.ensureLoaded();
3838
+ const skills = await this.loadedSkills();
3822
3839
  const trimmed = id.trim();
3823
3840
  const exact = skills.get(trimmed);
3824
3841
  if (exact) return exact;
@@ -3829,7 +3846,7 @@ var ContentIndex = class {
3829
3846
  return candidates[0];
3830
3847
  }
3831
3848
  async listSkillModes() {
3832
- const { skills } = await this.ensureLoaded();
3849
+ const skills = await this.loadedSkills();
3833
3850
  const modes = /* @__PURE__ */ new Set();
3834
3851
  for (const skill of skills.values()) modes.add(skill.mode);
3835
3852
  return [...modes].sort((a, b) => a.localeCompare(b));
@@ -3882,11 +3899,11 @@ function selectCraftSections(all, suggestedIds) {
3882
3899
  function composeInstructions(input) {
3883
3900
  const parts = [];
3884
3901
  parts.push(
3885
- `# OpenDesign generation brief
3902
+ `# Open Design generation brief
3886
3903
 
3887
- You are authoring a new design artifact using the OpenDesign skill "${input.skillName}". Follow the skill's workflow below, treat the active design system's tokens (if any) as authoritative, and author the files yourself with your own file-editing tools \u2014 do not look for any tool that writes design content on your behalf.
3904
+ You are authoring a new design artifact using the Open Design skill "${input.skillName}". Follow the skill's workflow below, treat the active design system's tokens (if any) as authoritative, and author the files yourself with your own file-editing tools \u2014 do not look for any tool that writes design content on your behalf.
3888
3905
 
3889
- **Everything below is delivered to you as plain text; none of it is a file path you can open.** The skill text, craft rules, and design system notes below are copied verbatim from OpenDesign's own upstream catalog and may mention file names or source paths (e.g. "example.html", "apps/daemon/src/...", a "Resource map" of sibling files) that describe OpenDesign's own separate codebase or runtime \u2014 none of those exist in this workspace or are readable by you. Never search for, open, or navigate to any file path mentioned anywhere below; if you want reference examples, call remix_open_design_example or list_open_design_skills instead of searching the filesystem. Treat every path-looking string below as descriptive prose only.`
3906
+ **Everything below is delivered to you as plain text; none of it is a file path you can open.** The skill text, craft rules, and design system notes below are copied verbatim from Open Design's own upstream catalog and may mention file names or source paths (e.g. "example.html", "apps/daemon/src/...", a "Resource map" of sibling files) that describe Open Design's own separate codebase or runtime \u2014 none of those exist in this workspace or are readable by you. Never search for, open, or navigate to any file path mentioned anywhere below; if you want reference examples, call remix_open_design_example or list_open_design_skills instead of searching the filesystem. Treat every path-looking string below as descriptive prose only.`
3890
3907
  );
3891
3908
  parts.push(`
3892
3909
 
@@ -3933,6 +3950,21 @@ ${s.body.trim()}`).join("\n\n")}`
3933
3950
  Detected: ${input.existingAppFrameworks.join(", ")}. Before generating, consider looking at a few of its real existing components/pages for actual conventions (styling approach, layout patterns, component structure) with your own file-reading tools, so the artifact you produce is closer to how this app already looks. This does NOT change where or how you write the artifact \u2014 it's still a standalone prototype at the output path below, not a real app file; it's purely a nudge toward visual consistency, not a requirement to integrate with the app's code.`
3934
3951
  );
3935
3952
  }
3953
+ if (input.collectionContext) {
3954
+ const { collectionName, index, total, role, siblingScreens } = input.collectionContext;
3955
+ const siblingsText = siblingScreens.length > 0 ? siblingScreens.map((s) => `- **${s.role}** \u2014 "${s.title}"`).join("\n") : "(none yet \u2014 this is the first screen)";
3956
+ parts.push(
3957
+ `
3958
+
3959
+ ## Part of a design collection
3960
+
3961
+ This artifact is screen ${index} of ${total} in the design collection "${collectionName}" \u2014 this screen's role: **${role}**. The other screens in this collection so far:
3962
+
3963
+ ${siblingsText}
3964
+
3965
+ Keep this screen visually and stylistically consistent with the others (same design system, same header/nav treatment, same component style) without re-reading their HTML \u2014 you already have their role and title above as context. Do not duplicate content that belongs on a different screen.`
3966
+ );
3967
+ }
3936
3968
  parts.push(
3937
3969
  `
3938
3970
 
@@ -3945,7 +3977,7 @@ Choose a short semantic filename derived from the brief, product, or artifact ty
3945
3977
 
3946
3978
  ## Output
3947
3979
 
3948
- Author the artifact's entry file at \`${input.suggestedEntryPath}\` (adjust the filename to follow the semantic-naming guidance above, or the skill's own fixed-name convention, but keep it under the same directory). Write any supporting files (stylesheets, scripts, imported assets) as siblings under that directory. After writing all files, call register_open_design_artifact with the entry path, a kind, a title, and the supporting file paths (relative to the entry file's own directory) so it is recognized as an OpenDesign artifact.`
3980
+ Author the artifact's entry file at \`${input.suggestedEntryPath}\` (adjust the filename to follow the semantic-naming guidance above, or the skill's own fixed-name convention, but keep it under the same directory). Write any supporting files (stylesheets, scripts, imported assets) as siblings under that directory. After writing all files, call register_open_design_artifact with the entry path, a kind, a title, and the supporting file paths (relative to the entry file's own directory) so it is recognized as an Open Design artifact.`
3949
3981
  );
3950
3982
  return parts.join("");
3951
3983
  }
@@ -4043,11 +4075,17 @@ async function extractBrandEvidence(sourceUrl) {
4043
4075
  return synthesizeBrandEvidence(base, html, stylesheetTexts, warnings);
4044
4076
  }
4045
4077
 
4078
+ // ../core/src/generation/collectionEntryPath.ts
4079
+ var path2 = __toESM(require("node:path"));
4080
+ function suggestCollectionScreenEntryPath(outputDir, collectionId, screenSlug) {
4081
+ return path2.posix.join(outputDir, collectionId, `${screenSlug}.html`);
4082
+ }
4083
+
4046
4084
  // ../core/src/generation/customDesignSystemInstructions.ts
4047
4085
  function composeCustomDesignSystemInstructions(input) {
4048
4086
  const parts = [];
4049
4087
  parts.push(
4050
- `# Create a custom OpenDesign design system
4088
+ `# Create a custom Open Design design system
4051
4089
 
4052
4090
  You are authoring a new, custom design system as a single \`DESIGN.md\` file. Once written, it becomes a first-class, selectable design system \u2014 the exact same mechanism as the ~150 bundled ones, just workspace-local instead of built into the extension.`
4053
4091
  );
@@ -4115,9 +4153,155 @@ Call \`set_active_design_system\` with designSystemId "${input.id}" so it's used
4115
4153
  return parts.join("");
4116
4154
  }
4117
4155
 
4156
+ // ../core/src/generation/figmaPull.ts
4157
+ var FIGMA_URL_RE = /^https?:\/\/(?:www\.)?figma\.com\/(?:file|design)\/([A-Za-z0-9]+)/i;
4158
+ function parseFigmaUrl(url) {
4159
+ const match = FIGMA_URL_RE.exec(url.trim());
4160
+ if (!match) return void 0;
4161
+ const fileKey = match[1];
4162
+ let nodeId;
4163
+ try {
4164
+ const parsed = new URL(url);
4165
+ const raw = parsed.searchParams.get("node-id");
4166
+ if (raw) nodeId = raw.replace("-", ":");
4167
+ } catch {
4168
+ }
4169
+ return { fileKey, nodeId };
4170
+ }
4171
+ var FigmaApiError = class extends Error {
4172
+ };
4173
+ async function fetchFigmaNode(token, ref, fetchImpl = fetch) {
4174
+ if (!ref.nodeId) {
4175
+ throw new FigmaApiError(
4176
+ `This Figma URL has no node id. Use Figma's "Copy link to selection" on the specific frame (not just the file link) and try again.`
4177
+ );
4178
+ }
4179
+ const url = `https://api.figma.com/v1/files/${ref.fileKey}/nodes?ids=${encodeURIComponent(ref.nodeId)}`;
4180
+ const response = await fetchImpl(url, { headers: { "X-Figma-Token": token } });
4181
+ if (!response.ok) {
4182
+ throw new FigmaApiError(`Figma API responded with HTTP ${response.status} while fetching the frame.`);
4183
+ }
4184
+ const body = await response.json();
4185
+ const document = body.nodes?.[ref.nodeId]?.document;
4186
+ if (!document) {
4187
+ throw new FigmaApiError(`Figma API response did not include a document for node "${ref.nodeId}" \u2014 it may not exist, or the token lacks access.`);
4188
+ }
4189
+ return document;
4190
+ }
4191
+ async function fetchFigmaFrameImage(token, fileKey, nodeId, fetchImpl = fetch) {
4192
+ try {
4193
+ const url = `https://api.figma.com/v1/images/${fileKey}?ids=${encodeURIComponent(nodeId)}&format=png&scale=2`;
4194
+ const response = await fetchImpl(url, { headers: { "X-Figma-Token": token } });
4195
+ if (!response.ok) return void 0;
4196
+ const body = await response.json();
4197
+ if (body.err) return void 0;
4198
+ return body.images?.[nodeId] ?? void 0;
4199
+ } catch {
4200
+ return void 0;
4201
+ }
4202
+ }
4203
+ function toHexByte(value) {
4204
+ return Math.round(Math.max(0, Math.min(1, value)) * 255).toString(16).padStart(2, "0");
4205
+ }
4206
+ function toHexColor(color) {
4207
+ const hex = `#${toHexByte(color.r)}${toHexByte(color.g)}${toHexByte(color.b)}`;
4208
+ return color.a !== void 0 && color.a < 1 ? `${hex}${toHexByte(color.a)}` : hex;
4209
+ }
4210
+ function firstSolidColor(paints) {
4211
+ const solid = paints?.find((p) => p.type === "SOLID" && p.visible !== false && p.color);
4212
+ if (!solid?.color) return void 0;
4213
+ return toHexColor({ ...solid.color, a: solid.opacity ?? solid.color.a });
4214
+ }
4215
+ var DEFAULT_MAX_NODES = 500;
4216
+ function summarizeFigmaNode(node, opts = {}) {
4217
+ const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
4218
+ let count = 0;
4219
+ let truncated = false;
4220
+ function walk(n) {
4221
+ if (count >= maxNodes) {
4222
+ truncated = true;
4223
+ return void 0;
4224
+ }
4225
+ count++;
4226
+ const summary = { id: n.id, name: n.name, type: n.type };
4227
+ if (n.absoluteBoundingBox) summary.box = { ...n.absoluteBoundingBox };
4228
+ const fillColor = firstSolidColor(n.fills);
4229
+ if (fillColor) summary.fillColor = fillColor;
4230
+ const strokeColor = firstSolidColor(n.strokes);
4231
+ if (strokeColor) summary.strokeColor = strokeColor;
4232
+ if (typeof n.cornerRadius === "number") summary.cornerRadius = n.cornerRadius;
4233
+ if (typeof n.opacity === "number" && n.opacity < 1) summary.opacity = n.opacity;
4234
+ if (n.type === "TEXT") {
4235
+ summary.characters = n.characters;
4236
+ summary.fontFamily = n.style?.fontFamily;
4237
+ summary.fontSize = n.style?.fontSize;
4238
+ summary.fontWeight = n.style?.fontWeight;
4239
+ summary.textAlign = n.style?.textAlignHorizontal;
4240
+ }
4241
+ if (n.children && n.children.length > 0) {
4242
+ const children = n.children.map(walk).filter((c) => !!c);
4243
+ if (children.length > 0) summary.children = children;
4244
+ }
4245
+ return summary;
4246
+ }
4247
+ const root = walk(node) ?? { id: node.id, name: node.name, type: node.type };
4248
+ return { root, nodeCount: count, truncated };
4249
+ }
4250
+ function composePullFigmaInstructions(input) {
4251
+ const { frameSummary, frameName, imageUrl, designSystemId, suggestedEntryPath } = input;
4252
+ const parts = [];
4253
+ parts.push(
4254
+ `# Translate a Figma frame into a code artifact
4255
+
4256
+ You are translating the Figma frame "${frameName}" into a real, production-quality HTML/CSS (or React, if more appropriate) artifact with 1:1 visual fidelity \u2014 not a loose interpretation.`
4257
+ );
4258
+ parts.push(
4259
+ `
4260
+
4261
+ ## Ground truth
4262
+
4263
+ The JSON below is a deterministic, exact structural summary of the frame \u2014 positions, sizes, fill/stroke colors (hex), corner radii, and text runs (with their exact characters, font, size). Treat it as authoritative: match exact colors, relative positions/sizes, and text content precisely. Do not invent content, copy, or elements that aren't present in this data.${frameSummary.truncated ? ` NOTE: this summary was truncated at ${frameSummary.nodeCount} nodes \u2014 the frame has more content than shown; ask the user if the missing portion matters before proceeding.` : ""}
4264
+
4265
+ \`\`\`json
4266
+ ${JSON.stringify(frameSummary.root, null, 2)}
4267
+ \`\`\``
4268
+ );
4269
+ parts.push(
4270
+ imageUrl ? `
4271
+
4272
+ ## Visual reference
4273
+
4274
+ A rendered PNG export of this exact frame is available at: ${imageUrl}
4275
+ If your environment can view images from a URL, fetch and look at it to cross-check visual fidelity (layering, gradients, and other details the structural JSON above simplifies away). If it cannot, rely on the structural JSON alone.` : `
4276
+
4277
+ ## Visual reference
4278
+
4279
+ No rendered image export was available \u2014 rely on the structural JSON alone.`
4280
+ );
4281
+ parts.push(
4282
+ designSystemId ? `
4283
+
4284
+ ## Design system
4285
+
4286
+ This frame is associated with design system "${designSystemId}" \u2014 reuse its component conventions where the frame's structure clearly matches them, but the frame's own exact colors/type from the JSON above still take precedence over the design system's generic tokens.` : `
4287
+
4288
+ ## Design system
4289
+
4290
+ No design system was specified \u2014 implement the frame's own exact styling from the JSON above directly (inline styles or plain CSS), without inventing a design-system abstraction that wasn't requested.`
4291
+ );
4292
+ parts.push(
4293
+ `
4294
+
4295
+ ## Output
4296
+
4297
+ Write the artifact's entry file at \`${suggestedEntryPath}\` (adjust the filename if there's a clearly better one, e.g. if it collides with something already there) using your own file-editing tools, then call register_open_design_artifact.`
4298
+ );
4299
+ return parts.join("");
4300
+ }
4301
+
4118
4302
  // ../core/src/generation/portToAppInstructions.ts
4119
4303
  var import_node_fs2 = require("node:fs");
4120
- var path2 = __toESM(require("node:path"));
4304
+ var path3 = __toESM(require("node:path"));
4121
4305
  var CANDIDATE_COMPONENT_DIRS = ["src/components", "app/components", "components"];
4122
4306
  var KNOWN_COMPONENT_EXTENSIONS = [".tsx", ".jsx", ".vue", ".svelte", ".ts", ".js"];
4123
4307
  async function pathExists2(p) {
@@ -4134,7 +4318,7 @@ function toPascalCase(name) {
4134
4318
  }
4135
4319
  async function suggestTargetComponentPath(workspaceRoot, artifactName) {
4136
4320
  for (const candidateDir of CANDIDATE_COMPONENT_DIRS) {
4137
- const absDir = path2.join(workspaceRoot, candidateDir);
4321
+ const absDir = path3.join(workspaceRoot, candidateDir);
4138
4322
  if (!await pathExists2(absDir)) continue;
4139
4323
  let entries = [];
4140
4324
  try {
@@ -4148,7 +4332,7 @@ async function suggestTargetComponentPath(workspaceRoot, artifactName) {
4148
4332
  if (ext) extensionCounts.set(ext, (extensionCounts.get(ext) ?? 0) + 1);
4149
4333
  }
4150
4334
  const guessedExt = [...extensionCounts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ".tsx";
4151
- return path2.posix.join(candidateDir, `${toPascalCase(artifactName)}${guessedExt}`);
4335
+ return path3.posix.join(candidateDir, `${toPascalCase(artifactName)}${guessedExt}`);
4152
4336
  }
4153
4337
  return void 0;
4154
4338
  }
@@ -4156,9 +4340,9 @@ function composePortToAppInstructions(input) {
4156
4340
  const { artifactEntryPath, artifactContent, targetComponentPath, referenceComponentPath } = input;
4157
4341
  const parts = [];
4158
4342
  parts.push(
4159
- `# Promote an OpenDesign prototype into this app's real code
4343
+ `# Promote an Open Design prototype into this app's real code
4160
4344
 
4161
- You are porting the finished OpenDesign artifact below into this workspace's actual, existing application \u2014 reproducing its visual design as idiomatic, real production code, not copying it into the app verbatim.`
4345
+ You are porting the finished Open Design artifact below into this workspace's actual, existing application \u2014 reproducing its visual design as idiomatic, real production code, not copying it into the app verbatim.`
4162
4346
  );
4163
4347
  parts.push(`
4164
4348
 
@@ -4209,7 +4393,7 @@ Do NOT wire the new component into routing, navigation, or any other part of the
4209
4393
  }
4210
4394
 
4211
4395
  // ../core/src/vendored/artifactManifest.ts
4212
- var path3 = __toESM(require("node:path"));
4396
+ var path4 = __toESM(require("node:path"));
4213
4397
  var MANIFEST_VERSION = 1;
4214
4398
  var MAX_TITLE_LENGTH = 200;
4215
4399
  var MAX_SOURCE_SKILL_ID_LENGTH = 128;
@@ -4217,6 +4401,10 @@ var MAX_DESIGN_SYSTEM_ID_LENGTH = 128;
4217
4401
  var MAX_SUPPORTING_FILE_LENGTH = 260;
4218
4402
  var MAX_SUPPORTING_FILES = 128;
4219
4403
  var MAX_METADATA_BYTES = 16 * 1024;
4404
+ var MAX_COLLECTION_ID_LENGTH = 100;
4405
+ var MAX_COLLECTION_NAME_LENGTH = 200;
4406
+ var MAX_SCREEN_ROLE_LENGTH = 100;
4407
+ var MAX_SCREEN_INDEX = 999;
4220
4408
  var ALLOWED_KINDS = /* @__PURE__ */ new Set([
4221
4409
  "html",
4222
4410
  "deck",
@@ -4337,6 +4525,29 @@ function validateArtifactManifestInput(manifest, entry, options2 = {}) {
4337
4525
  );
4338
4526
  if (dsErr) return { ok: false, error: dsErr };
4339
4527
  }
4528
+ if (manifest.collectionId !== void 0 && manifest.collectionId !== null) {
4529
+ const collectionIdErr = validateBoundedString(manifest.collectionId, "artifactManifest.collectionId", MAX_COLLECTION_ID_LENGTH, {
4530
+ allowEmpty: true
4531
+ });
4532
+ if (collectionIdErr) return { ok: false, error: collectionIdErr };
4533
+ }
4534
+ if (manifest.collectionName !== void 0 && manifest.collectionName !== null) {
4535
+ const collectionNameErr = validateBoundedString(manifest.collectionName, "artifactManifest.collectionName", MAX_COLLECTION_NAME_LENGTH, {
4536
+ allowEmpty: true
4537
+ });
4538
+ if (collectionNameErr) return { ok: false, error: collectionNameErr };
4539
+ }
4540
+ if (manifest.screenRole !== void 0 && manifest.screenRole !== null) {
4541
+ const screenRoleErr = validateBoundedString(manifest.screenRole, "artifactManifest.screenRole", MAX_SCREEN_ROLE_LENGTH, {
4542
+ allowEmpty: true
4543
+ });
4544
+ if (screenRoleErr) return { ok: false, error: screenRoleErr };
4545
+ }
4546
+ if (manifest.screenIndex !== void 0 && manifest.screenIndex !== null) {
4547
+ if (typeof manifest.screenIndex !== "number" || !Number.isInteger(manifest.screenIndex) || manifest.screenIndex < 0 || manifest.screenIndex > MAX_SCREEN_INDEX) {
4548
+ return { ok: false, error: `artifactManifest.screenIndex must be an integer between 0 and ${MAX_SCREEN_INDEX}` };
4549
+ }
4550
+ }
4340
4551
  if (manifest.metadata !== void 0) {
4341
4552
  if (!isPlainObject(manifest.metadata)) {
4342
4553
  return { ok: false, error: "artifactManifest.metadata must be a plain object" };
@@ -4368,12 +4579,27 @@ function sanitizeManifest(manifest, entry, options2 = {}) {
4368
4579
  updatedAt: options2.preserveUpdatedAt && typeof manifest.updatedAt === "string" ? manifest.updatedAt : now,
4369
4580
  sourceSkillId: manifest.sourceSkillId,
4370
4581
  designSystemId: manifest.designSystemId ?? void 0,
4582
+ collectionId: manifest.collectionId ?? void 0,
4583
+ collectionName: manifest.collectionName ?? void 0,
4584
+ screenRole: manifest.screenRole ?? void 0,
4585
+ screenIndex: typeof manifest.screenIndex === "number" ? manifest.screenIndex : void 0,
4371
4586
  metadata: manifest.metadata
4372
4587
  };
4373
4588
  }
4589
+ function parsePersistedManifest(raw, fallbackEntry) {
4590
+ try {
4591
+ const parsed = JSON.parse(raw);
4592
+ if (!parsed || parsed.version !== MANIFEST_VERSION) return null;
4593
+ const entry = typeof parsed.entry === "string" && parsed.entry ? parsed.entry : fallbackEntry;
4594
+ const result = validateArtifactManifestInput(parsed, entry, { preserveUpdatedAt: true });
4595
+ return result.ok ? result.value : null;
4596
+ } catch {
4597
+ return null;
4598
+ }
4599
+ }
4374
4600
  function inferLegacyManifest(entry) {
4375
4601
  const lower = entry.toLowerCase();
4376
- const ext = path3.extname(lower);
4602
+ const ext = path4.extname(lower);
4377
4603
  const isDeck = ext === ".html" && (lower.includes("deck") || lower.includes("slides") || lower.includes("pitch"));
4378
4604
  if (ext === ".html" || ext === ".htm") {
4379
4605
  return {
@@ -4416,7 +4642,7 @@ function inferLegacyManifest(entry) {
4416
4642
 
4417
4643
  // ../core/src/vendored/artifactCreate.ts
4418
4644
  var import_node_fs3 = require("node:fs");
4419
- var path4 = __toESM(require("node:path"));
4645
+ var path5 = __toESM(require("node:path"));
4420
4646
  var ArtifactManifestRequiredError = class extends Error {
4421
4647
  constructor(entry) {
4422
4648
  super(`artifactManifest is required for ${entry}; no safe default manifest can be inferred`);
@@ -4450,9 +4676,9 @@ function artifactManifestSidecarPath(entryPath) {
4450
4676
  return `${entryPath}.artifact.json`;
4451
4677
  }
4452
4678
  function assertWorkspaceRelative(workspaceRoot, entryPath) {
4453
- const abs = path4.join(workspaceRoot, entryPath);
4454
- const rel = path4.relative(workspaceRoot, abs);
4455
- if (rel.startsWith("..") || path4.isAbsolute(rel)) {
4679
+ const abs = path5.isAbsolute(entryPath) ? entryPath : path5.join(workspaceRoot, entryPath);
4680
+ const rel = path5.relative(workspaceRoot, abs);
4681
+ if (rel.startsWith("..") || path5.isAbsolute(rel)) {
4456
4682
  throw new Error(`entryPath escapes the workspace: ${entryPath}`);
4457
4683
  }
4458
4684
  return abs;
@@ -4518,7 +4744,7 @@ async function setActiveDesignSystem(designSystemId, store, getDesignSystem) {
4518
4744
 
4519
4745
  // ../core/src/workspace/appDetection.ts
4520
4746
  var import_node_fs4 = require("node:fs");
4521
- var path5 = __toESM(require("node:path"));
4747
+ var path6 = __toESM(require("node:path"));
4522
4748
  var FRAMEWORK_PACKAGES = {
4523
4749
  react: "React",
4524
4750
  next: "Next.js",
@@ -4533,7 +4759,7 @@ async function detectExistingApp(workspaceRoot) {
4533
4759
  if (!workspaceRoot) return [];
4534
4760
  let raw;
4535
4761
  try {
4536
- raw = await import_node_fs4.promises.readFile(path5.join(workspaceRoot, "package.json"), "utf8");
4762
+ raw = await import_node_fs4.promises.readFile(path6.join(workspaceRoot, "package.json"), "utf8");
4537
4763
  } catch {
4538
4764
  return [];
4539
4765
  }
@@ -4553,14 +4779,14 @@ async function detectExistingApp(workspaceRoot) {
4553
4779
 
4554
4780
  // ../core/src/workspace/artifactComments.ts
4555
4781
  var import_node_fs5 = require("node:fs");
4556
- var path6 = __toESM(require("node:path"));
4782
+ var path7 = __toESM(require("node:path"));
4557
4783
  function commentsSidecarPath(entryPath) {
4558
4784
  return `${entryPath}.comments.json`;
4559
4785
  }
4560
4786
  function assertWorkspaceRelative2(workspaceRoot, entryPath) {
4561
- const abs = path6.join(workspaceRoot, entryPath);
4562
- const rel = path6.relative(workspaceRoot, abs);
4563
- if (rel.startsWith("..") || path6.isAbsolute(rel)) {
4787
+ const abs = path7.isAbsolute(entryPath) ? entryPath : path7.join(workspaceRoot, entryPath);
4788
+ const rel = path7.relative(workspaceRoot, abs);
4789
+ if (rel.startsWith("..") || path7.isAbsolute(rel)) {
4564
4790
  throw new Error(`entryPath escapes the workspace: ${entryPath}`);
4565
4791
  }
4566
4792
  return abs;
@@ -4576,39 +4802,95 @@ async function readArtifactComments(workspaceRoot, entryPath) {
4576
4802
  }
4577
4803
  }
4578
4804
 
4579
- // ../core/src/workspace/remixExample.ts
4805
+ // ../core/src/workspace/collectionScan.ts
4580
4806
  var import_node_fs6 = require("node:fs");
4581
- var path7 = __toESM(require("node:path"));
4807
+ var path8 = __toESM(require("node:path"));
4808
+ var ARTIFACT_SIDECAR_SUFFIX = ".artifact.json";
4809
+ async function pathExists3(p) {
4810
+ try {
4811
+ await import_node_fs6.promises.access(p);
4812
+ return true;
4813
+ } catch {
4814
+ return false;
4815
+ }
4816
+ }
4817
+ async function walkArtifactSidecars(absDir, workspaceRoot, out) {
4818
+ let entries;
4819
+ try {
4820
+ entries = await import_node_fs6.promises.readdir(absDir, { withFileTypes: true });
4821
+ } catch {
4822
+ return;
4823
+ }
4824
+ for (const entry of entries) {
4825
+ const absChild = path8.join(absDir, entry.name);
4826
+ if (entry.isDirectory()) {
4827
+ await walkArtifactSidecars(absChild, workspaceRoot, out);
4828
+ continue;
4829
+ }
4830
+ if (!entry.isFile() || !entry.name.endsWith(ARTIFACT_SIDECAR_SUFFIX)) continue;
4831
+ const entryPath = path8.relative(workspaceRoot, absChild).slice(0, -ARTIFACT_SIDECAR_SUFFIX.length).split(path8.sep).join("/");
4832
+ try {
4833
+ const raw = await import_node_fs6.promises.readFile(absChild, "utf8");
4834
+ const manifest = parsePersistedManifest(raw, entryPath);
4835
+ if (manifest) out.push({ entryPath, manifest });
4836
+ } catch {
4837
+ }
4838
+ }
4839
+ }
4840
+ function toScreen(entryPath, manifest) {
4841
+ return {
4842
+ entryPath,
4843
+ title: typeof manifest.title === "string" && manifest.title ? manifest.title : entryPath,
4844
+ screenIndex: typeof manifest.screenIndex === "number" ? manifest.screenIndex : void 0,
4845
+ screenRole: typeof manifest.screenRole === "string" ? manifest.screenRole : void 0
4846
+ };
4847
+ }
4848
+ function sortScreens(screens) {
4849
+ return [...screens].sort((a, b) => (a.screenIndex ?? Number.MAX_SAFE_INTEGER) - (b.screenIndex ?? Number.MAX_SAFE_INTEGER));
4850
+ }
4851
+ async function findCollectionArtifacts(workspaceRoot, outputDir, collectionId) {
4852
+ const absOutputDir = path8.join(workspaceRoot, outputDir);
4853
+ if (!await pathExists3(absOutputDir)) return [];
4854
+ const found = [];
4855
+ await walkArtifactSidecars(absOutputDir, workspaceRoot, found);
4856
+ return sortScreens(
4857
+ found.filter(({ manifest }) => manifest.collectionId === collectionId).map(({ entryPath, manifest }) => toScreen(entryPath, manifest))
4858
+ );
4859
+ }
4860
+
4861
+ // ../core/src/workspace/remixExample.ts
4862
+ var import_node_fs7 = require("node:fs");
4863
+ var path9 = __toESM(require("node:path"));
4582
4864
  async function listFilesRecursive(dir, baseDir) {
4583
4865
  const out = [];
4584
- const entries = await import_node_fs6.promises.readdir(dir, { withFileTypes: true });
4866
+ const entries = await import_node_fs7.promises.readdir(dir, { withFileTypes: true });
4585
4867
  for (const entry of entries) {
4586
- const full = path7.join(dir, entry.name);
4868
+ const full = path9.join(dir, entry.name);
4587
4869
  if (entry.isDirectory()) {
4588
4870
  out.push(...await listFilesRecursive(full, baseDir));
4589
4871
  } else if (entry.isFile()) {
4590
- out.push(path7.relative(baseDir, full).split(path7.sep).join("/"));
4872
+ out.push(path9.relative(baseDir, full).split(path9.sep).join("/"));
4591
4873
  }
4592
4874
  }
4593
4875
  return out;
4594
4876
  }
4595
4877
  async function copyExampleArtifact(options2) {
4596
- const sourceHtmlAbs = path7.join(options2.assetsRoot, options2.exampleArtifactPath);
4597
- const sourceDir = path7.dirname(sourceHtmlAbs);
4598
- const destHtmlAbs = path7.join(options2.workspaceRoot, options2.entryPath);
4599
- const destRel = path7.relative(options2.workspaceRoot, destHtmlAbs);
4600
- if (destRel.startsWith("..") || path7.isAbsolute(destRel)) {
4878
+ const sourceHtmlAbs = path9.join(options2.assetsRoot, options2.exampleArtifactPath);
4879
+ const sourceDir = path9.dirname(sourceHtmlAbs);
4880
+ const destHtmlAbs = path9.isAbsolute(options2.entryPath) ? options2.entryPath : path9.join(options2.workspaceRoot, options2.entryPath);
4881
+ const destRel = path9.relative(options2.workspaceRoot, destHtmlAbs);
4882
+ if (destRel.startsWith("..") || path9.isAbsolute(destRel)) {
4601
4883
  throw new Error(`entryPath escapes the workspace: ${options2.entryPath}`);
4602
4884
  }
4603
- const destDir = path7.dirname(destHtmlAbs);
4604
- await import_node_fs6.promises.mkdir(destDir, { recursive: true });
4605
- await import_node_fs6.promises.copyFile(sourceHtmlAbs, destHtmlAbs);
4606
- const sourceAssetsDir = path7.join(sourceDir, "assets");
4885
+ const destDir = path9.dirname(destHtmlAbs);
4886
+ await import_node_fs7.promises.mkdir(destDir, { recursive: true });
4887
+ await import_node_fs7.promises.copyFile(sourceHtmlAbs, destHtmlAbs);
4888
+ const sourceAssetsDir = path9.join(sourceDir, "assets");
4607
4889
  let supportingFiles = [];
4608
4890
  try {
4609
- await import_node_fs6.promises.access(sourceAssetsDir);
4610
- const destAssetsDir = path7.join(destDir, "assets");
4611
- await import_node_fs6.promises.cp(sourceAssetsDir, destAssetsDir, { recursive: true });
4891
+ await import_node_fs7.promises.access(sourceAssetsDir);
4892
+ const destAssetsDir = path9.join(destDir, "assets");
4893
+ await import_node_fs7.promises.cp(sourceAssetsDir, destAssetsDir, { recursive: true });
4612
4894
  supportingFiles = await listFilesRecursive(destAssetsDir, destDir);
4613
4895
  } catch {
4614
4896
  }
@@ -4616,7 +4898,7 @@ async function copyExampleArtifact(options2) {
4616
4898
  }
4617
4899
 
4618
4900
  // src/env.ts
4619
- var path8 = __toESM(require("node:path"));
4901
+ var path10 = __toESM(require("node:path"));
4620
4902
  function getWorkspaceRoot() {
4621
4903
  return process.env.OPEN_DESIGN_WORKSPACE_ROOT?.trim() || process.cwd();
4622
4904
  }
@@ -4624,19 +4906,22 @@ var DEFAULT_OUTPUT_DIR = ".open-design";
4624
4906
  function getOutputDirectory() {
4625
4907
  return process.env.OPEN_DESIGN_OUTPUT_DIR?.trim() || DEFAULT_OUTPUT_DIR;
4626
4908
  }
4909
+ function getFigmaToken() {
4910
+ return process.env.OPEN_DESIGN_FIGMA_TOKEN?.trim() || void 0;
4911
+ }
4627
4912
  function getAssetsRoot() {
4628
4913
  const contentPackageJson = require.resolve("@feimacode/open-design-agent-kit-content/package.json");
4629
- return path8.join(path8.dirname(contentPackageJson), "assets", "open-design");
4914
+ return path10.join(path10.dirname(contentPackageJson), "assets", "open-design");
4630
4915
  }
4631
4916
 
4632
4917
  // src/store.ts
4633
- var import_node_fs7 = require("node:fs");
4634
- var path9 = __toESM(require("node:path"));
4918
+ var import_node_fs8 = require("node:fs");
4919
+ var path11 = __toESM(require("node:path"));
4635
4920
  function createFileActiveDesignSystemStore(workspaceRoot) {
4636
- const configPath = path9.join(workspaceRoot, ".open-design", "config.json");
4921
+ const configPath = path11.join(workspaceRoot, ".open-design", "config.json");
4637
4922
  async function readConfig() {
4638
4923
  try {
4639
- const raw = await import_node_fs7.promises.readFile(configPath, "utf8");
4924
+ const raw = await import_node_fs8.promises.readFile(configPath, "utf8");
4640
4925
  const parsed = JSON.parse(raw);
4641
4926
  return typeof parsed === "object" && parsed !== null ? parsed : {};
4642
4927
  } catch {
@@ -4656,14 +4941,14 @@ function createFileActiveDesignSystemStore(workspaceRoot) {
4656
4941
  } else {
4657
4942
  delete config.activeDesignSystemId;
4658
4943
  }
4659
- await import_node_fs7.promises.mkdir(path9.dirname(configPath), { recursive: true });
4660
- await import_node_fs7.promises.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
4944
+ await import_node_fs8.promises.mkdir(path11.dirname(configPath), { recursive: true });
4945
+ await import_node_fs8.promises.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
4661
4946
  }
4662
4947
  };
4663
4948
  }
4664
4949
 
4665
4950
  // src/tools.ts
4666
- var path10 = __toESM(require("node:path"));
4951
+ var path12 = __toESM(require("node:path"));
4667
4952
  function slugify(input) {
4668
4953
  const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
4669
4954
  return slug || "artifact";
@@ -4671,7 +4956,7 @@ function slugify(input) {
4671
4956
  function suggestEntryPath(ctx, brief, skillName) {
4672
4957
  const slugSource = brief.trim().length > 0 ? brief : skillName;
4673
4958
  const slug = slugify(slugSource);
4674
- return path10.posix.join(ctx.outputDir, slug, `${slug}.html`);
4959
+ return path12.posix.join(ctx.outputDir, slug, `${slug}.html`);
4675
4960
  }
4676
4961
  var KIND_TO_RENDERER = {
4677
4962
  html: "html",
@@ -4720,7 +5005,7 @@ async function listRemixablePrompts(ctx) {
4720
5005
  }
4721
5006
  function buildRemixPromptMessage(prompt) {
4722
5007
  const brief = prompt.examplePrompt ? ` ${prompt.examplePrompt}` : "";
4723
- return `Use the OpenDesign skill "${prompt.publicId}" (${prompt.displayName}).${brief}`;
5008
+ return `Remix the Open Design example "${prompt.displayName}" \u2014 call the open-design MCP server's remix_open_design_example tool with skillId "${prompt.publicId}".${brief}`;
4724
5009
  }
4725
5010
  async function listDesignSystems(ctx, input) {
4726
5011
  const designSystems = await ctx.contentIndex.listDesignSystems(input.query, input.category);
@@ -4748,8 +5033,25 @@ async function prepareBrief(ctx, input) {
4748
5033
  const { designSystemId, designSystem } = resolution;
4749
5034
  const allCraftSections = await ctx.contentIndex.craftSections();
4750
5035
  const craftSections = selectCraftSections(allCraftSections, designSystem?.craftSuggested);
4751
- const suggestedEntryPath = suggestEntryPath(ctx, input.brief, skill.name);
4752
5036
  const existingAppFrameworks = await detectExistingApp(ctx.workspaceRoot);
5037
+ let suggestedEntryPath;
5038
+ let collectionContext;
5039
+ if (input.collectionId) {
5040
+ if (!input.screenRole) {
5041
+ return `screenRole is required when collectionId is given \u2014 a short label for this screen's role in the flow, e.g. "splash", "value-prop", "checkout".`;
5042
+ }
5043
+ const siblings = await findCollectionArtifacts(ctx.workspaceRoot, ctx.outputDir, input.collectionId);
5044
+ suggestedEntryPath = suggestCollectionScreenEntryPath(ctx.outputDir, input.collectionId, slugify(input.screenRole));
5045
+ collectionContext = {
5046
+ collectionName: input.collectionName ?? input.collectionId,
5047
+ index: siblings.length + 1,
5048
+ total: input.screenTotal ?? siblings.length + 1,
5049
+ role: input.screenRole,
5050
+ siblingScreens: siblings.map((s) => ({ role: s.screenRole ?? "?", title: s.title }))
5051
+ };
5052
+ } else {
5053
+ suggestedEntryPath = suggestEntryPath(ctx, input.brief, skill.name);
5054
+ }
4753
5055
  const instructions = composeInstructions({
4754
5056
  skillName: skill.name,
4755
5057
  skillBody: skill.body,
@@ -4758,7 +5060,8 @@ async function prepareBrief(ctx, input) {
4758
5060
  craftSections,
4759
5061
  brief: input.brief,
4760
5062
  suggestedEntryPath,
4761
- existingAppFrameworks
5063
+ existingAppFrameworks,
5064
+ collectionContext
4762
5065
  });
4763
5066
  const payload = {
4764
5067
  instructions,
@@ -4786,14 +5089,18 @@ async function registerArtifact(ctx, input) {
4786
5089
  title: input.title,
4787
5090
  supportingFiles: input.supportingFiles,
4788
5091
  sourceSkillId: input.sourceSkillId,
4789
- designSystemId: input.designSystemId
5092
+ designSystemId: input.designSystemId,
5093
+ collectionId: input.collectionId,
5094
+ collectionName: input.collectionName,
5095
+ screenIndex: input.screenIndex,
5096
+ screenRole: input.screenRole
4790
5097
  }
4791
5098
  });
4792
5099
  return `Artifact registered at ${input.entryPath}.artifact.json
4793
5100
 
4794
5101
  ${JSON.stringify(manifest, null, 2)}
4795
5102
 
4796
- Written to ${path10.join(ctx.workspaceRoot, input.entryPath)}. This host has no live preview editor \u2014 there is nothing further to open.`;
5103
+ Written to ${path12.join(ctx.workspaceRoot, input.entryPath)}. This host has no live preview editor \u2014 there is nothing further to open.`;
4797
5104
  } catch (err) {
4798
5105
  const message = err instanceof Error ? err.message : String(err);
4799
5106
  return `Failed to register artifact: ${message}`;
@@ -4816,17 +5123,17 @@ async function getArtifact(ctx, input) {
4816
5123
  }
4817
5124
  async function setActiveDesignSystemTool(ctx, input) {
4818
5125
  const result = await setActiveDesignSystem(input.designSystemId, ctx.store, (id) => ctx.contentIndex.getDesignSystem(id));
4819
- if (result.outcome === "cleared") return "Cleared the active OpenDesign design system.";
5126
+ if (result.outcome === "cleared") return "Cleared the active Open Design design system.";
4820
5127
  if (result.outcome === "unknown") {
4821
5128
  const available = (await ctx.contentIndex.listDesignSystems()).map((d) => d.id).slice(0, 20);
4822
5129
  return `Unknown designSystemId "${result.unknownId}". Call list_open_design_design_systems to see available ids. A few available ids: ${available.join(", ")}`;
4823
5130
  }
4824
- return `Active OpenDesign design system set to "${input.designSystemId}" (${result.designSystem.name}).`;
5131
+ return `Active Open Design design system set to "${input.designSystemId}" (${result.designSystem.name}).`;
4825
5132
  }
4826
5133
  async function createCustomDesignSystem(ctx, input) {
4827
5134
  const slug = slugify(input.name);
4828
5135
  const id = `user:${slug}`;
4829
- const suggestedEntryPath = path10.posix.join(ctx.outputDir, "design-systems", slug, "DESIGN.md");
5136
+ const suggestedEntryPath = path12.posix.join(ctx.outputDir, "design-systems", slug, "DESIGN.md");
4830
5137
  const evidence = input.sourceUrl ? await extractBrandEvidence(input.sourceUrl) : void 0;
4831
5138
  const instructions = composeCustomDesignSystemInstructions({ name: input.name, brief: input.brief, suggestedEntryPath, id, evidence });
4832
5139
  return JSON.stringify({ instructions, suggestedEntryPath, id }, null, 2);
@@ -4836,7 +5143,7 @@ async function portToAppCode(ctx, input) {
4836
5143
  if (!artifact) {
4837
5144
  return `No artifact found at ${input.entryPath}. It may not have been written yet.`;
4838
5145
  }
4839
- const artifactName = typeof artifact.manifest?.title === "string" ? artifact.manifest.title : path10.basename(input.entryPath, path10.extname(input.entryPath));
5146
+ const artifactName = typeof artifact.manifest?.title === "string" ? artifact.manifest.title : path12.basename(input.entryPath, path12.extname(input.entryPath));
4840
5147
  const targetComponentPath = input.targetComponentPath ?? await suggestTargetComponentPath(ctx.workspaceRoot, artifactName);
4841
5148
  const instructions = composePortToAppInstructions({
4842
5149
  artifactEntryPath: input.entryPath,
@@ -4846,6 +5153,33 @@ async function portToAppCode(ctx, input) {
4846
5153
  });
4847
5154
  return JSON.stringify({ instructions, suggestedTargetComponentPath: targetComponentPath }, null, 2);
4848
5155
  }
5156
+ async function pullFigmaFrame(ctx, input) {
5157
+ if (!ctx.figmaToken) {
5158
+ return "No Figma access token configured. Set the OPEN_DESIGN_FIGMA_TOKEN environment variable for this MCP server to a Figma personal access token (Figma \u2192 Settings \u2192 Personal access tokens) and try again.";
5159
+ }
5160
+ const ref = parseFigmaUrl(input.figmaUrl);
5161
+ if (!ref) {
5162
+ return `"${input.figmaUrl}" does not look like a Figma file/design URL.`;
5163
+ }
5164
+ let node;
5165
+ try {
5166
+ node = await fetchFigmaNode(ctx.figmaToken, ref);
5167
+ } catch (err) {
5168
+ return err instanceof FigmaApiError ? err.message : `Failed to fetch the Figma frame: ${err instanceof Error ? err.message : String(err)}`;
5169
+ }
5170
+ const frameSummary = summarizeFigmaNode(node);
5171
+ const imageUrl = await fetchFigmaFrameImage(ctx.figmaToken, ref.fileKey, ref.nodeId);
5172
+ const slug = slugify(node.name);
5173
+ const suggestedEntryPath = path12.posix.join(ctx.outputDir, "figma", `${slug}.html`);
5174
+ const instructions = composePullFigmaInstructions({
5175
+ frameSummary,
5176
+ frameName: node.name,
5177
+ imageUrl,
5178
+ designSystemId: input.designSystemId,
5179
+ suggestedEntryPath
5180
+ });
5181
+ return JSON.stringify({ instructions, suggestedEntryPath }, null, 2);
5182
+ }
4849
5183
  async function remixExample(ctx, input) {
4850
5184
  const skill = await ctx.contentIndex.getSkill(input.skillId);
4851
5185
  if (!skill) {
@@ -4890,7 +5224,7 @@ var TOOL_DEFS = [
4890
5224
  {
4891
5225
  tool: {
4892
5226
  name: "list_open_design_skills",
4893
- description: "Lists available OpenDesign skills, design templates, and remixable examples \u2014 reusable design-task recipes, rendering styles, and (for 'example' entries) actual starting artifacts bundled with this server. Each result's id is namespaced as 'od:<mode>:<name>' (e.g. 'od:deck:guizang-ppt') \u2014 pass this full id as skillId to prepare_open_design_brief or remix_open_design_example. Each result's 'source' field is 'skill', 'design-template', or 'example'. Some results include an 'examplePrompt' \u2014 prefer it (or lightly adapt it) over inventing your own brief when it closely fits. A result with a non-empty 'exampleArtifactPath' has an actual rendered starting artifact \u2014 prefer remix_open_design_example over prepare_open_design_brief for those. Optionally filter by a free-text query, an exact mode, an exact source, and/or remixableOnly to see only entries with a rendered starting artifact.",
5227
+ description: "Lists available Open Design skills, design templates, and remixable examples \u2014 reusable design-task recipes, rendering styles, and (for 'example' entries) actual starting artifacts bundled with this server. Each result's id is namespaced as 'od:<mode>:<name>' (e.g. 'od:deck:guizang-ppt') \u2014 pass this full id as skillId to prepare_open_design_brief or remix_open_design_example. Each result's 'source' field is 'skill', 'design-template', or 'example'. Some results include an 'examplePrompt' \u2014 prefer it (or lightly adapt it) over inventing your own brief when it closely fits. A result with a non-empty 'exampleArtifactPath' has an actual rendered starting artifact \u2014 prefer remix_open_design_example over prepare_open_design_brief for those. Optionally filter by a free-text query, an exact mode, an exact source, and/or remixableOnly to see only entries with a rendered starting artifact. If a query returns few or no results, call this tool again with a broader query or no arguments at all to browse the full catalog \u2014 do NOT search the filesystem, grep, or read any file (this catalog is not stored anywhere as a single readable file such as a content.json, index, or schema file; it exists only inside this server's own runtime and is reachable exclusively through this tool).",
4894
5228
  inputSchema: {
4895
5229
  type: "object",
4896
5230
  properties: {
@@ -4917,7 +5251,7 @@ var TOOL_DEFS = [
4917
5251
  {
4918
5252
  tool: {
4919
5253
  name: "list_open_design_design_systems",
4920
- description: "Lists available OpenDesign design systems \u2014 brand-inspired visual token sets (palette, typography, spacing, component rules), each with a 'category' and an 'active' flag marking the current active design system, if any. Optionally filter by a free-text query and/or an exact category.",
5254
+ description: "Lists available Open Design design systems \u2014 brand-inspired visual token sets (palette, typography, spacing, component rules), each with a 'category' and an 'active' flag marking the current active design system, if any. Optionally filter by a free-text query and/or an exact category. If a query returns few or no results, call this tool again with a broader query or no arguments at all to browse the full catalog \u2014 do NOT search the filesystem, grep, or read any file (this catalog is not stored anywhere as a single readable file such as a content.json, index, or schema file; it exists only inside this server's own runtime and is reachable exclusively through this tool).",
4921
5255
  inputSchema: {
4922
5256
  type: "object",
4923
5257
  properties: {
@@ -4931,7 +5265,7 @@ var TOOL_DEFS = [
4931
5265
  {
4932
5266
  tool: {
4933
5267
  name: "prepare_open_design_brief",
4934
- description: "Composes generation instructions for an OpenDesign artifact by combining the chosen skill's workflow, an optional design system's visual tokens, universal craft rules, and the user's brief. Returns an 'instructions' string you must follow, plus a 'suggestedEntryPath'. Does NOT write any files \u2014 after calling it, author the entry file (and any supporting files) yourself with your normal file-editing tools, then call register_open_design_artifact.",
5268
+ description: "Composes generation instructions for an Open Design artifact by combining the chosen skill's workflow, an optional design system's visual tokens, universal craft rules, and the user's brief. Returns an 'instructions' string you must follow, plus a 'suggestedEntryPath'. Does NOT write any files \u2014 after calling it, author the entry file (and any supporting files) yourself with your normal file-editing tools, then call register_open_design_artifact.",
4935
5269
  inputSchema: {
4936
5270
  type: "object",
4937
5271
  properties: {
@@ -4940,12 +5274,31 @@ var TOOL_DEFS = [
4940
5274
  type: "string",
4941
5275
  description: "Optional design system id from list_open_design_design_systems. If omitted, falls back to the active design system. If given explicitly, it also becomes the new active one."
4942
5276
  },
4943
- brief: { type: "string", description: "The user's design brief / request, in their own words." }
5277
+ brief: { type: "string", description: "The user's design brief / request, in their own words." },
5278
+ collectionId: {
5279
+ type: "string",
5280
+ description: 'Set this (a slug you derive once from the collection name, reused verbatim on every call) when this artifact is one screen of a multi-screen design collection \u2014 see the "design collections" guidance for the full workflow.'
5281
+ },
5282
+ collectionName: {
5283
+ type: "string",
5284
+ description: 'A short display name for the collection, e.g. "Fintech Onboarding Flow". Required together with collectionId.'
5285
+ },
5286
+ screenRole: {
5287
+ type: "string",
5288
+ description: `Required together with collectionId \u2014 this screen's short role label, e.g. "splash", "value-prop", "checkout".`
5289
+ },
5290
+ screenTotal: {
5291
+ type: "number",
5292
+ description: `Optional: the total number of screens you've planned for this collection, for an accurate "screen N of M" framing.`
5293
+ }
4944
5294
  },
4945
5295
  required: ["skillId", "brief"]
4946
5296
  }
4947
5297
  },
4948
- handler: (ctx, args) => prepareBrief(ctx, args)
5298
+ handler: (ctx, args) => prepareBrief(
5299
+ ctx,
5300
+ args
5301
+ )
4949
5302
  },
4950
5303
  {
4951
5304
  tool: {
@@ -4967,7 +5320,11 @@ var TOOL_DEFS = [
4967
5320
  description: "Workspace-relative paths to sibling files the entry depends on, relative to the entry file's directory."
4968
5321
  },
4969
5322
  sourceSkillId: { type: "string", description: "The skillId used to generate this artifact, if any." },
4970
- designSystemId: { type: "string", description: "The designSystemId used to generate this artifact, if any." }
5323
+ designSystemId: { type: "string", description: "The designSystemId used to generate this artifact, if any." },
5324
+ collectionId: { type: "string", description: "The same collectionId passed to prepare_open_design_brief, if this screen is part of a design collection." },
5325
+ collectionName: { type: "string", description: "The same collectionName passed to prepare_open_design_brief, if this screen is part of a design collection." },
5326
+ screenIndex: { type: "number", description: "0-based position of this screen within its collection (0 for the first screen, 1 for the second, ...)." },
5327
+ screenRole: { type: "string", description: "The same screenRole passed to prepare_open_design_brief, if this screen is part of a design collection." }
4971
5328
  },
4972
5329
  required: ["entryPath", "kind", "title"]
4973
5330
  }
@@ -4980,7 +5337,7 @@ var TOOL_DEFS = [
4980
5337
  {
4981
5338
  tool: {
4982
5339
  name: "get_open_design_artifact",
4983
- description: "Reads back a registered OpenDesign artifact: its manifest, entry file content, supporting file list, and any open comments. This host has no comment-authoring UI, so 'openComments' will normally be empty.",
5340
+ description: "Reads back a registered Open Design artifact: its manifest, entry file content, supporting file list, and any open comments. This host has no comment-authoring UI, so 'openComments' will normally be empty.",
4984
5341
  inputSchema: {
4985
5342
  type: "object",
4986
5343
  properties: { entryPath: { type: "string", description: "Workspace-relative path to the artifact's entry file." } },
@@ -4992,7 +5349,7 @@ var TOOL_DEFS = [
4992
5349
  {
4993
5350
  tool: {
4994
5351
  name: "set_active_design_system",
4995
- description: "Sets (or clears, if designSystemId is omitted/empty) the active OpenDesign design system, persisted for this workspace. Used automatically by prepare_open_design_brief whenever called without an explicit designSystemId.",
5352
+ description: "Sets (or clears, if designSystemId is omitted/empty) the active Open Design design system, persisted for this workspace. Used automatically by prepare_open_design_brief whenever called without an explicit designSystemId.",
4996
5353
  inputSchema: {
4997
5354
  type: "object",
4998
5355
  properties: {
@@ -5024,7 +5381,7 @@ var TOOL_DEFS = [
5024
5381
  {
5025
5382
  tool: {
5026
5383
  name: "port_open_design_artifact_to_app",
5027
- description: "Composes instructions for porting a finished OpenDesign artifact's design into this workspace's actual, existing application as real, idiomatic production code. Does NOT write any files. Directs the model to ground the port in a real existing component and explicitly excludes routing/navigation wiring.",
5384
+ description: "Composes instructions for porting a finished Open Design artifact's design into this workspace's actual, existing application as real, idiomatic production code. Does NOT write any files. Directs the model to ground the port in a real existing component and explicitly excludes routing/navigation wiring.",
5028
5385
  inputSchema: {
5029
5386
  type: "object",
5030
5387
  properties: {
@@ -5037,10 +5394,25 @@ var TOOL_DEFS = [
5037
5394
  },
5038
5395
  handler: (ctx, args) => portToAppCode(ctx, args)
5039
5396
  },
5397
+ {
5398
+ tool: {
5399
+ name: "pull_open_design_figma_frame",
5400
+ description: `Composes instructions for translating a Figma frame into a real code artifact with 1:1 visual fidelity. Fetches the frame's structure via the Figma REST API (requires the OPEN_DESIGN_FIGMA_TOKEN environment variable \u2014 a Figma personal access token) and a best-effort rendered image export, then returns 'instructions' embedding a deterministic structural summary as ground truth. Does NOT write any files \u2014 after calling it, author the entry file yourself with your normal file-editing tools, then call register_open_design_artifact. figmaUrl must be a frame-scoped link (Figma's "Copy link to selection"), not a bare file link.`,
5401
+ inputSchema: {
5402
+ type: "object",
5403
+ properties: {
5404
+ figmaUrl: { type: "string", description: 'A Figma frame URL, e.g. from "Copy link to selection" \u2014 must include a node-id.' },
5405
+ designSystemId: { type: "string", description: "Optional design system id from list_open_design_design_systems to align the translated code with." }
5406
+ },
5407
+ required: ["figmaUrl"]
5408
+ }
5409
+ },
5410
+ handler: (ctx, args) => pullFigmaFrame(ctx, args)
5411
+ },
5040
5412
  {
5041
5413
  tool: {
5042
5414
  name: "remix_open_design_example",
5043
- description: "Copies a curated OpenDesign example artifact (identified by its skillId, which must have a non-empty 'exampleArtifactPath') into the workspace as a starting point, registers it, and returns instructions to MODIFY the copied file rather than regenerate it from scratch.",
5415
+ description: "Copies a curated Open Design example artifact (identified by its skillId, which must have a non-empty 'exampleArtifactPath') into the workspace as a starting point, registers it, and returns instructions to MODIFY the copied file rather than regenerate it from scratch.",
5044
5416
  inputSchema: {
5045
5417
  type: "object",
5046
5418
  properties: { skillId: { type: "string", description: "A skillId whose 'exampleArtifactPath' is non-empty." } },
@@ -5056,7 +5428,7 @@ function buildContext() {
5056
5428
  const assetsRoot = getAssetsRoot();
5057
5429
  const contentIndex = new ContentIndex(assetsRoot, () => `${workspaceRoot}/${outputDir}/design-systems`);
5058
5430
  const store = createFileActiveDesignSystemStore(workspaceRoot);
5059
- return { contentIndex, store, workspaceRoot, outputDir, assetsRoot };
5431
+ return { contentIndex, store, workspaceRoot, outputDir, assetsRoot, figmaToken: getFigmaToken() };
5060
5432
  }
5061
5433
  async function main() {
5062
5434
  const ctx = buildContext();
@@ -5077,7 +5449,7 @@ async function main() {
5077
5449
  });
5078
5450
  server.setRequestHandler(import_types.ListPromptsRequestSchema, async () => {
5079
5451
  const prompts = await listRemixablePrompts(ctx);
5080
- return { prompts: prompts.map((p) => ({ name: p.name, description: `${p.displayName} (OpenDesign remixable example)` })) };
5452
+ return { prompts: prompts.map((p) => ({ name: p.name, description: `${p.displayName} (Open Design remixable example)` })) };
5081
5453
  });
5082
5454
  server.setRequestHandler(import_types.GetPromptRequestSchema, async (request) => {
5083
5455
  const prompts = await listRemixablePrompts(ctx);
@@ -5086,7 +5458,7 @@ async function main() {
5086
5458
  throw new Error(`Unknown prompt: ${request.params.name}`);
5087
5459
  }
5088
5460
  return {
5089
- description: `${prompt.displayName} (OpenDesign remixable example)`,
5461
+ description: `${prompt.displayName} (Open Design remixable example)`,
5090
5462
  messages: [{ role: "user", content: { type: "text", text: buildRemixPromptMessage(prompt) } }]
5091
5463
  };
5092
5464
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@feimacode/open-design-agent-kit-mcp",
3
- "version": "0.1.5",
4
- "description": "OpenDesign as an MCP server — the same skill/design-system/artifact tools the VS Code extension exposes, usable from any MCP-capable coding agent (Claude Code, Codex, and others).",
3
+ "version": "0.1.7",
4
+ "description": "Open Design as an MCP server — the same skill/design-system/artifact tools the VS Code extension exposes, usable from any MCP-capable coding agent (Claude Code, Codex, and others).",
5
5
  "license": "MIT",
6
6
  "engines": {
7
7
  "node": ">=18"
@@ -42,7 +42,7 @@
42
42
  "start": "node ./out/index.js"
43
43
  },
44
44
  "dependencies": {
45
- "@feimacode/open-design-agent-kit-content": "^0.1.5",
45
+ "@feimacode/open-design-agent-kit-content": "^0.1.7",
46
46
  "@modelcontextprotocol/sdk": "^1.30.0"
47
47
  },
48
48
  "devDependencies": {