@ikas/code-components-mcp 2.5.1 → 2.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.
package/dist/index.js CHANGED
@@ -2227,7 +2227,7 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
2227
2227
  };
2228
2228
  });
2229
2229
  // Tool: get_type_definition
2230
- server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus'). Shows all properties with types, extends, or enum values.", {
2230
+ server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name. Shows all properties with types, extends, or enum values. Covers both runtime models (e.g. 'IkasProduct', 'IkasOrderStatus') and theme-globals tokens returned by the getters (e.g. 'ThemeSetting', 'DesignToken', 'TypographyToken', 'ColorSchemeToken', 'BreakpointToken', 'KeyframeToken'). If a type reports as not found, it may not be indexed yet — fall back to reading the package's `.d.ts` (e.g. `@ikas/bp-storefront-config`) rather than typing the value as `any`.", {
2231
2231
  name: z
2232
2232
  .string()
2233
2233
  .describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
@@ -3521,6 +3521,15 @@ function parseCliJson(stdout) {
3521
3521
  return null;
3522
3522
  }
3523
3523
  }
3524
+ // Coerce a JSON-ARRAY argument (points / settings / colors) into the single JSON-array string the
3525
+ // CLI expects. Agents sometimes send these PRE-STRINGIFIED (a JSON string instead of a real array);
3526
+ // a blind JSON.stringify would then double-encode it ("[...]" -> "\"[...]\"") and the CLI rejects it
3527
+ // with "must be a JSON array". So pass a string through as-is (the CLI parses + validates it) and
3528
+ // only stringify real arrays/values. NOTE: only for array args — a scalar `value` arg must NOT use
3529
+ // this (a plain string there is a literal value, not stringified JSON).
3530
+ function toJsonArrayArg(value) {
3531
+ return typeof value === "string" ? value : JSON.stringify(value ?? []);
3532
+ }
3524
3533
  async function callEditorAction(projectRoot, args) {
3525
3534
  try {
3526
3535
  const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
@@ -3739,7 +3748,7 @@ server.tool("get_section_values", "Get the CURRENT prop values of one or MANY pl
3739
3748
  return callEditorAction(project_root, args);
3740
3749
  });
3741
3750
  // Tool: update_section_prop
3742
- server.tool("update_section_prop", 'Change a single prop value of a section placed on a page. This is the tool for FILLING/SETTING content (heading, text, image, link, etc.) on an existing section — it is pure data entry: it writes NO component code and needs NO rebuild. Do not author a new prop or component to set a value the section already supports; first check the section\'s existing `props` via `list_page_sections`. Target the specific placement by its `element_id` (from `list_page_sections`) and the prop by `prop_id` or `prop_name` (also from `list_page_sections`). `value` is the prop value object as stored by the editor — for scalar props it is wrapped as `{ "value": <scalar> }` (e.g. `{ "value": "Hello" }` for TEXT, `{ "value": true }` for BOOLEAN, `{ "value": 12 }` for NUMBER, `{ "value": "#FF0000" }` for COLOR, `{ "value": "<enum-key>" }` for ENUM). Richer object prop types use their own object shape and are NOT `{ "value": ... }`-wrapped (full catalog in `get_editor_workflow`): IMAGE = `{ "id": "<asset-id>", "altText"?: "<alt>", "isVideo"?: false }` (the `id` MUST reference an already-uploaded asset — call `upload_image` with a `file_path`/`image_url` to get one, or reuse an existing `id` from another section\'s `propValues`); IMAGE_LIST = `{ "images": [ <image>, ... ] }`; VIDEO = `{ "video": { "id": "<asset-id>" }, "thumbnailImage"?: { "id": "..." }, "autoplay"?: false, "controls"?: true, "loop"?: false, "muted"?: false }` (NOT a bare `{ "id": ... }`); SVG = `{ "value": "<svg markup>" }` (value-wrapped, like a scalar); SVG_LIST = `{ "svgs": [ "<svg>", ... ] }`; entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE = `{ "<entity>Id": "...", "usePageData"?: false }` (e.g. `{ "categoryId": "..." }`) — get real ids from `search_products` (PRODUCT), `list_categories`, `list_brands`, `list_blogs`, `list_blog_categories`; list/collection props are typed objects keyed by a `<entity>ListType` discriminator: PRODUCT_LIST = `{ "id": "<unique>", "productListType": "STATIC", "productIds": [ { "productId": "...", "variantId": "..." } ], "initialLimit"?: 12 }` — note productIds is an array of {productId, variantId} PAIRS (from search_products), not bare ids; productListType ∈ ALL|STATIC|DISCOUNTED|RECOMMENDED|CATEGORY|SEARCH|LAST_VIEWED|RELATED_PRODUCTS|VIEWED_TOGETHER|PURCHASED_TOGETHER, and for a dynamic type use e.g. `{ "id": "<unique>", "productListType": "CATEGORY", "category": "<categoryId>" }` (no productIds) or `"ALL"` (no ids). CATEGORY_LIST = `{ "categoryListType": "STATIC", "categoryIds": ["..."] }` or `{ "categoryListType": "ALL" }`. BRAND_LIST = `{ "brandListType": "STATIC", "brandIds": ["..."] }` or ALL. BLOG_LIST = `{ "blogListType": "STATIC", "blogIds": ["..."] }`, `{ "blogListType": "CATEGORY", "categoryId": "..." }`, or ALL. BLOG_CATEGORY_LIST = `{ "blogCategoryListType": "STATIC", "blogCategoryIds": ["..."] }` or ALL. (Get the entity ids from search_products / list_*; you can also read an existing value with `get_section_values` to copy its exact shape.) LINK = a single link object `{ "id": "<unique-short-id>", "linkType": "PAGE"|"EXTERNAL"|"FILE", "label": "...", "openInNewTab"?: false, "subLinks": [] }` plus the target for its type — for PAGE add `"pageId"` AND `"pageType"` (both from `list_editor_pages`, e.g. pageType "INDEX"; for dynamic page types like PRODUCT/CATEGORY/BLOG also add the target `"itemId"`), for EXTERNAL add `"externalLink": "https://..."`. Examples: PAGE → `{ "id": "k3p9x", "linkType": "PAGE", "label": "Home", "pageId": "<page-id>", "pageType": "INDEX", "openInNewTab": false, "subLinks": [] }`; EXTERNAL → `{ "id": "m7q2z", "linkType": "EXTERNAL", "label": "Docs", "externalLink": "https://example.com", "openInNewTab": true, "subLinks": [] }`. To link to an ENTITY (category/brand/blog/blog-category) do NOT guess a slug — get the entity id from `list_categories`/`list_brands`/`list_blogs`/`list_blog_categories` and the page id from `get_page_by_type("<TYPE>")`, then build `{ "id": "<unique>", "linkType": "PAGE", "label": "...", "pageId": <pageId>, "pageType": "<TYPE>", "itemId": <entityId>, "subLinks": [] }`. LIST_OF_LINK = `{ "links": [ <link>, ... ] }` (each item is a link object as above). PRODUCT = `{ "productId": "...", "variantId"?: "..." }`. This SAME unwrapped shape applies whether the prop sits at section level OR nested inside a COMPONENT_LIST entry\'s `propValues` — there is no section-vs-nested difference. IMPORTANT: pass `value` as the parsed JSON object/array itself, NEVER as a JSON string (a stringified value is double-encoded and stored as a useless string). Values are validated server-side (deeply, including nested COMPONENT_LIST children): wrong shapes AND wrong semantics are REJECTED with an explanatory error instead of being silently stored. This includes: a `{ "value": [...] }` wrapper for a COMPONENT_LIST; a `{ "value": ... }`-wrapped IMAGE; `props` instead of `propValues`; a missing/duplicate entry `id`; a child referenced by the wrong key (a code component MUST use `codeComponentId`, a theme component `componentId`) or by an id that does not exist (child ids are opaque — take them from `get_section_template` / `list_imported_sections`, never invent them); and any `propValues` key that is not a real prop of that child component (the error lists the valid prop names). Auto-import: a referenced child code component that has been BUILT but not yet imported into the theme is imported automatically before the value is set (the response lists any such ids under `autoImported`); only children that are not built at all fail with a "no imported code component" error — build them first (`ikas-component build`/`dev`). For any prop type you are unsure about, inspect the existing value with `get_section_values` and match its exact shape. The change is applied with undo support.\n\n' +
3751
+ server.tool("update_section_prop", 'Change a single prop value of a section placed on a page. This is the tool for FILLING/SETTING content (heading, text, image, link, etc.) on an existing section — it is pure data entry: it writes NO component code and needs NO rebuild. Do not author a new prop or component to set a value the section already supports; first check the section\'s existing `props` via `list_page_sections`. Target the specific placement by its `element_id` (from `list_page_sections`) and the prop by `prop_id` or `prop_name` (also from `list_page_sections`). `value` is the prop value object as stored by the editor — for scalar props it is wrapped as `{ "value": <scalar> }` (e.g. `{ "value": "Hello" }` for TEXT, `{ "value": true }` for BOOLEAN, `{ "value": 12 }` for NUMBER, `{ "value": "#FF0000" }` for COLOR, `{ "value": "<enum-key>" }` for ENUM). Richer object prop types use their own object shape and are NOT `{ "value": ... }`-wrapped (full catalog in `get_editor_workflow`): IMAGE = `{ "id": "<asset-id>", "altText"?: "<alt>", "isVideo"?: false }` (the `id` MUST reference an already-uploaded asset — call `upload_image` with a `file_path`/`image_url` to get one, or reuse an existing `id` from another section\'s `propValues`); IMAGE_LIST = `{ "images": [ <image>, ... ] }`; VIDEO = `{ "video": { "id": "<asset-id>" }, "thumbnailImage"?: { "id": "..." }, "autoplay"?: false, "controls"?: true, "loop"?: false, "muted"?: false }` (NOT a bare `{ "id": ... }`); SVG = `{ "value": "<svg markup>" }` (value-wrapped, like a scalar); SVG_LIST = `{ "svgs": [ "<svg>", ... ] }`; entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE = `{ "<entity>Id": "...", "usePageData"?: false }` (e.g. `{ "categoryId": "..." }`) — get real ids from `search_products` (PRODUCT), `list_categories`, `list_brands`, `list_blogs`, `list_blog_categories`; list/collection props are typed objects keyed by a `<entity>ListType` discriminator: PRODUCT_LIST = `{ "id": "<unique>", "productListType": "STATIC", "productIds": [ { "productId": "...", "variantId": "..." } ], "initialLimit"?: 12 }` — note productIds is an array of {productId, variantId} PAIRS (from search_products), not bare ids; productListType ∈ ALL|STATIC|DISCOUNTED|RECOMMENDED|CATEGORY|SEARCH|LAST_VIEWED|RELATED_PRODUCTS|VIEWED_TOGETHER|PURCHASED_TOGETHER, and for a dynamic type use e.g. `{ "id": "<unique>", "productListType": "CATEGORY", "category": "<id from list_categories>" }` (the field is `category`, NOT `categoryId` — `categoryId` is only the standalone CATEGORY entity prop; no productIds here) or `"ALL"` (no ids). Unknown sub-fields inside a PRODUCT_LIST (e.g. `categoryId`) are REJECTED, not silently stored. CATEGORY_LIST = `{ "categoryListType": "STATIC", "categoryIds": ["..."] }` or `{ "categoryListType": "ALL" }`. BRAND_LIST = `{ "brandListType": "STATIC", "brandIds": ["..."] }` or ALL. BLOG_LIST = `{ "blogListType": "STATIC", "blogIds": ["..."] }`, `{ "blogListType": "CATEGORY", "categoryId": "..." }`, or ALL. BLOG_CATEGORY_LIST = `{ "blogCategoryListType": "STATIC", "blogCategoryIds": ["..."] }` or ALL. (Get the entity ids from search_products / list_*; you can also read an existing value with `get_section_values` to copy its exact shape.) LINK = a single link object `{ "id": "<unique-short-id>", "linkType": "PAGE"|"EXTERNAL"|"FILE", "label": "...", "openInNewTab"?: false, "subLinks": [] }` plus the target for its type — for PAGE add `"pageId"` AND `"pageType"` (both from `list_editor_pages`, e.g. pageType "INDEX"; for dynamic page types like PRODUCT/CATEGORY/BLOG also add the target `"itemId"`), for EXTERNAL add `"externalLink": "https://..."`. Examples: PAGE → `{ "id": "k3p9x", "linkType": "PAGE", "label": "Home", "pageId": "<page-id>", "pageType": "INDEX", "openInNewTab": false, "subLinks": [] }`; EXTERNAL → `{ "id": "m7q2z", "linkType": "EXTERNAL", "label": "Docs", "externalLink": "https://example.com", "openInNewTab": true, "subLinks": [] }`. To link to an ENTITY (category/brand/blog/blog-category) do NOT guess a slug — get the entity id from `list_categories`/`list_brands`/`list_blogs`/`list_blog_categories` and the page id from `get_page_by_type("<TYPE>")`, then build `{ "id": "<unique>", "linkType": "PAGE", "label": "...", "pageId": <pageId>, "pageType": "<TYPE>", "itemId": <entityId>, "subLinks": [] }`. LIST_OF_LINK = `{ "links": [ <link>, ... ] }` (each item is a link object as above). PRODUCT = `{ "productId": "...", "variantId"?: "..." }`. This SAME unwrapped shape applies whether the prop sits at section level OR nested inside a COMPONENT_LIST entry\'s `propValues` — there is no section-vs-nested difference. IMPORTANT: pass `value` as the parsed JSON object/array itself, NEVER as a JSON string (a stringified value is double-encoded and stored as a useless string). Values are validated server-side (deeply, including nested COMPONENT_LIST children): wrong shapes AND wrong semantics are REJECTED with an explanatory error instead of being silently stored. This includes: a `{ "value": [...] }` wrapper for a COMPONENT_LIST; a `{ "value": ... }`-wrapped IMAGE; `props` instead of `propValues`; a missing/duplicate entry `id`; a child referenced by the wrong key (a code component MUST use `codeComponentId`, a theme component `componentId`) or by an id that does not exist (child ids are opaque — take them from `get_section_template` / `list_imported_sections`, never invent them); and any `propValues` key that is not a real prop of that child component (the error lists the valid prop names). Auto-import: a referenced child code component that has been BUILT but not yet imported into the theme is imported automatically before the value is set (the response lists any such ids under `autoImported`); only children that are not built at all fail with a "no imported code component" error — build them first (`ikas-component build`/`dev`). For any prop type you are unsure about, inspect the existing value with `get_section_values` and match its exact shape. The change is applied with undo support.\n\n' +
3743
3752
  'COMPONENT_LIST / COMPONENT props: the value is NOT wrapped in `{ "value": ... }`. A COMPONENT_LIST value is `{ "components": [ <entry>, ... ] }`; a single COMPONENT value is one `<entry>`. Each entry = `{ "id": "<unique-id>", "codeComponentId": "<child-id>" | "componentId": "<child-id>", "propValues": { "<childPropName>": <wrapped-value>, ... } }`. Rules: (1) `id` is a unique short alphanumeric string identifying THIS entry — generate a fresh one per new entry and NEVER reuse an existing entry\'s id. (2) Use `codeComponentId` for code components, `componentId` for built-in theme components. (3) `propValues` is keyed by the CHILD component\'s prop NAMES, each using the same scalar wrappers as above (nesting is recursive — a child may itself hold a COMPONENT_LIST). (4) `update_section_prop` REPLACES the whole prop value — there is NO partial/deep merge. ALWAYS send the COMPLETE, fully-nested value for the prop: every existing entry (with its id), every nested `propValues`, and every nested COMPONENT_LIST at every depth. To change anything (even one deeply-nested scalar, or to add/remove/reorder a child), READ-MODIFY-WRITE: take the current `{ "components": [...] }` from `get_section_values`, edit it in place (preserving all other entries, ids, and nested structures), then send the ENTIRE updated object back. Sending only the changed part — a single entry, or a child without its siblings — wipes everything you omitted. (5) You can only add child component types the prop permits — call `get_component_props(parentComponentId)` to read the COMPONENT_LIST prop\'s `allowedComponentIds`, then `get_component_props(childId)` to learn that child\'s props (names, types, ENUM `options`/valid values) instead of guessing or reading ikas.config.json. The allowed set is wired at build/config time per the `get_section_template` setup recipe (`config update-prop`), not here.\n\n' +
3744
3753
  'Example — a full COMPONENT_LIST `value` with two children, where the second child itself nests another COMPONENT_LIST (note: scalar leaves are `{ "value": ... }`-wrapped, COMPONENT_LIST values are `{ "components": [...] }` and are NOT wrapped, every entry has a unique `id`, and this is the COMPLETE value you would send even to change just one field): ' +
3745
3754
  `{ "components": [ { "id": "a1b2c", "codeComponentId": "7ojrigep-Eml9n5sN3i", "propValues": { "heading": { "value": "Featured" }, "visible": { "value": true } } }, { "id": "d3e4f", "codeComponentId": "x2plk9zq-Qw8rt", "propValues": { "title": { "value": "Sub group" }, "cards": { "components": [ { "id": "g5h6i", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 1" } } }, { "id": "j7k8l", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 2" } } } ] } } } ] }`, {
@@ -4022,12 +4031,13 @@ const EDITOR_WORKFLOW_GUIDE = [
4022
4031
  "## Value shapes by prop type (what to pass as `value`)",
4023
4032
  "- Scalars are WRAPPED: TEXT/RICH_TEXT { \"value\": \"...\" }, BOOLEAN { \"value\": true }, NUMBER { \"value\": 12 }, COLOR { \"value\": \"#FF0000\" }, ENUM { \"value\": \"<key>\" }, DATE { \"value\": ... }.",
4024
4033
  "- SVG is value-wrapped: { \"value\": \"<svg markup>\" }. NUMBER_RANGE: { \"value\": <number>, \"unit\": \"px\"|null }.",
4034
+ "- SVG naming: the editor shows each svg's `class` attribute as its display name in the list; add class=\"my-icon\" to name it. SVG_LIST items with no class render as svg-1, svg-2, ….",
4025
4035
  "- Object props are NOT wrapped (no { \"value\": ... }):",
4026
4036
  " - IMAGE: { \"id\": \"<asset-id>\", \"altText\"?: \"...\", \"isVideo\"?: false } — get the id from upload_image. Same shape at section level AND nested in a component list. IMAGE_LIST: { \"images\": [ <image>, ... ] }.",
4027
4037
  " - VIDEO: { \"video\": { \"id\": \"<asset-id>\" }, \"thumbnailImage\"?: { \"id\": \"...\" }, \"autoplay\"?: false, \"controls\"?: true, \"loop\"?: false, \"muted\"?: false } — NOT a bare { \"id\": ... }. SVG_LIST: { \"svgs\": [ \"<svg>\", ... ] }.",
4028
4038
  " - LINK: { \"id\": \"<unique>\", \"linkType\": \"PAGE\"|\"EXTERNAL\"|\"FILE\", \"label\": \"...\", \"subLinks\": [] } plus target — for a static page: pageId + pageType (from list_editor_pages); for EXTERNAL: externalLink. To link to a CATEGORY/BRAND/BLOG/BLOG_CATEGORY entity, do NOT guess a slug — get the entity id from list_categories/list_brands/list_blogs/list_blog_categories and the page id from get_page_by_type('<TYPE>'), then build { id, linkType:'PAGE', label, pageId:<pageId>, pageType:'<TYPE>', itemId:<entityId>, subLinks:[] }. LIST_OF_LINK = { \"links\": [ <link>, ... ] }.",
4029
4039
  " - Entity props PRODUCT/CATEGORY/BRAND/BLOG/BLOG_CATEGORY/RAFFLE: { \"<entity>Id\": \"...\", \"usePageData\"?: false } — e.g. { \"productId\": \"...\", \"variantId\"?: \"...\" }, { \"categoryId\": \"...\" }. Get real ids (never invent them) from: search_products (PRODUCT), list_categories (CATEGORY), list_brands (BRAND), list_blogs (BLOG), list_blog_categories (BLOG_CATEGORY). (RAFFLE has no lookup tool yet — read an existing value or ask the user.)",
4030
- " - List/collection props (PRODUCT_LIST/CATEGORY_LIST/BRAND_LIST/BLOG_LIST/…): complex typed objects (a <entity>ListType plus initialSort/initialLimit and static ids). Do NOT build from scratchread the current value with get_section_values and edit it.",
4040
+ " - List/collection props (PRODUCT_LIST/CATEGORY_LIST/BRAND_LIST/BLOG_LIST/…): typed objects keyed by a <entity>ListType discriminator plus initialSort/initialLimit. The id field DEPENDS on the type — for PRODUCT_LIST: STATIC → productIds:[{productId,variantId}] (from search_products); CATEGORY category:'<categoryId>' (from list_categories)the field is `category`, NOT `categoryId` (categoryId is ONLY the standalone CATEGORY entity prop above); ALL/DISCOUNTED/RECOMMENDED/etc → no id field. Get the exact field set + writeNote from get_component_props, or read+edit an existing value via get_section_values. Unknown sub-fields (e.g. categoryId inside a PRODUCT_LIST) are REJECTED.",
4031
4041
  " - COMPONENT_LIST: { \"components\": [ { \"id\": \"<unique>\", \"codeComponentId\": \"<child-id>\", \"propValues\": { \"<childProp>\": <value>, ... } }, ... ] }. A single COMPONENT prop is one such entry. childProp values use these same per-type shapes, recursively.",
4032
4042
  "",
4033
4043
  "## COMPONENT_LIST rules (the part most often gotten wrong)",
@@ -4049,6 +4059,365 @@ const EDITOR_WORKFLOW_GUIDE = [
4049
4059
  server.tool("get_editor_workflow", "Read this FIRST when the user wants to place a section on a page or fill/edit a section's content (text, image, link, slides, component lists) in the live editor. Returns the complete step-by-step workflow for the live-editor tools (list_editor_pages, list_imported_sections, import_section, add_section_to_page, list_page_sections, update_section_prop, upload_image), the per-prop-type value shapes, the COMPONENT_LIST read-modify-write rules, and how validation/auto-import behave. Use it to avoid the common mistake of writing component code to set a value a section already supports.", {}, async () => {
4050
4060
  return { content: [{ type: "text", text: EDITOR_WORKFLOW_GUIDE }] };
4051
4061
  });
4062
+ // Tool: list_theme_globals
4063
+ server.tool("list_theme_globals", "List the theme's global settings from the connected editor: global variables (Theme Settings) and design tokens (colors, typography, breakpoints, keyframes, color schemes). Call this BEFORE generating sections/components so you reuse the theme's existing variables and tokens (read them in component code via `getThemeSetting`/`getThemeColors`/... from `@ikas/bp-storefront`). Includes items created manually in the editor UI. Requires `ikas-component dev` running with the editor connected. Two shape gotchas when consuming the result at runtime: (1) color schemes — `colorSchemes.schemes` holds only `{id,name}` slot LABELS; the actual colors live in `colorSchemes.values[].colorsByScheme` keyed by slot id, so iterate `values[].colorsByScheme` (iterating `schemes` renders empty). (2) An IMAGE setting's `value` is an image REFERENCE like `{id:'theme-images/<uuid>'}` with no `.url` — resolve it via `getDefaultSrc`/`getSrc`. (3) Each keyframe now includes its animation `settings` (type-dependent { property, value } entries — duration/iteration/timing/etc. for keyframe, transition for transition) and each point's resolved `styles`, so you can read back the full animation config, not just the `ref`. (4) Each typography entry may include `supportedFontWeights` — the exact numeric weights that text style's font actually ships (e.g. `[400]` for Modak); when present, only set `font_weight` to a value in this list (via `update_text_style`/`create_theme_global`), otherwise the write is rejected. If the field is absent the font's weight set couldn't be determined — fall back to 400 or rely on the validation error. (5) Token NAMES are NOT unique — a theme can contain two tokens with the SAME name (e.g. colors pulled from two different design assets), so NEVER match a token by its human name. ALWAYS identify a token by its `id` (globally unique). IN COMPONENT CODE: prefer embedding the token's id-derived STABLE ref directly — color → cssVar `var(--<id>)`, typography → `className` `_<id>`, keyframe (animation-name) → `ref` `_<id>` — so you don't look anything up at runtime. If you DO need to pick a token out of a `getThemeColors()` / `getThemeTypography()` / `getThemeKeyframes()` / `getThemeColorSchemes()` array, match on `.id` (e.g. `getThemeColors().find(c => c.id === \"<id>\")`), NEVER on `.name`. EXCEPTION — global variables/settings: `getThemeSetting(name)` takes the token's `name`, but here `name` is the unique runtime key (`variableName`, the `_<id>` form returned in this list), which IS safe; do NOT pass a setting's human `displayName`. The update/delete tools also identify a token ONLY by `id` (they do NOT look up by name — on those tools `name` means the NEW name when renaming). Always take the `id` (or, for settings, the `name`/variableName) from THIS list. (6) GROUPING convention: a \"/\" in a token's NAME organizes it into a group in the editor UI — the text before the first \"/\" is the group, the rest is the item label. This applies to COLORS (e.g. \"Brand/Primary\"), TEXT STYLES (e.g. \"Heading/H1\"), and COLOR-SCHEME SLOTS (e.g. \"PrimaryButton/Background\"). When you create globals (via `create_theme_global`/`update_theme_color_scheme`), use \"/\"-prefixed names to keep related tokens grouped and tidy; reuse an existing group's exact prefix so they nest together.", {
4064
+ project_root: z.string().describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
4065
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4066
+ }, async ({ project_root, port }) => {
4067
+ const args = ["list-theme-globals", ...(port ? ["--port", String(port)] : [])];
4068
+ return callEditorAction(project_root, args);
4069
+ });
4070
+ // Tool: create_theme_global
4071
+ server.tool("create_theme_global", "Create a theme global setting in the connected editor. `kind` selects what to create:\n" +
4072
+ "- globalVariable: requires display_name + type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW); value optional. Pass `value` as a REAL JSON value, never a stringified JSON. Value shapes — TEXT/COLOR: string; RICH_TEXT: HTML string; NUMBER: number; BOOLEAN: real JSON boolean true/false (NOT the string \"true\"); IMAGE: an image REFERENCE object { id: \"theme-images/<uuid>\" } (NOT { url }) — get a real id from list_theme_globals; do NOT invent/fabricate an id, omit value instead if you don't have one; BORDER: { width: { value, unit }, style, color }; SHADOW: { x, y, blur, spread, color, position: \"outside\"|\"inside\" }. For object types (IMAGE/BORDER/SHADOW) pass an actual JSON object, not a JSON-encoded string.\n" +
4073
+ "- color: requires name + value — a CONCRETE color (hex e.g. \"#ff0000\", rgb()/hsl() or a CSS color name). A color token cannot alias another color, so a \"var(...)\" reference is REJECTED here (to reuse another theme color, reference its cssVar in component code, or in a colorScheme slot). TIP: a \"/\" in the `name` GROUPS the color in the editor UI (e.g. \"Brand/Primary\", \"Brand/Accent\" appear under a \"Brand\" group); use this to organize related colors. To CHANGE an existing color later, use `update_theme_color` (edits in place, keeps the cssVar stable) — do NOT delete+recreate.\n" +
4074
+ "- typography: requires name + at least one of font_family/font_size/font_weight/line_height/letter_spacing. VALUE FORMATS (validated): font_weight is one of 100,200,300,400,500,600,700,800,900 (append 'i' for italic, e.g. \"600i\") AND must be a weight the chosen font actually ships (many fonts provide only a subset, e.g. \"Modak\" only ships 400) — if unavailable the call fails and lists the supported weights, so when unsure use 400; stored as a numeric weight + a font-style entry; font_size is a number WITH a unit (e.g. \"16px\", \"1.2rem\", \"80%\"); line_height is \"normal\", a unitless number (e.g. \"1.5\") or a length; letter_spacing is \"normal\" or a length (e.g. \"0.02em\", \"0.5px\"); font_family is the font name. TIP: a \"/\" in the `name` GROUPS the text style in the editor UI (e.g. \"Heading/H1\", \"Heading/H2\" appear under a \"Heading\" group); use this to organize related styles. To CHANGE an existing text style later, use `update_text_style` (edits in place, keeps the `className` stable) — do NOT delete+recreate, which changes the id/className.\n" +
4075
+ "- breakpoint: requires name + width (a positive INTEGER, in px — no decimals). To CHANGE an existing breakpoint later, use `update_theme_breakpoint` (edits in place, keeps the id).\n" +
4076
+ "- keyframe: requires name + points (array of { point, styles? }) where each style is { property, value } using a CSS property name (opacity, transform, filter, background, color, …); keyframe_type defaults to \"keyframe\". Optional `settings` = animation-level config as { property, value } entries, TYPE-DEPENDENT: for keyframe — animation-duration, animation-iteration-count, animation-play-state, animation-delay, animation-timing-function, animation-direction, animation-fill-mode, animation-timeline, animation-range, transform-origin, backface-visibility; for transition — transition, backface-visibility. Setting these means the editor drives the animation timing (you don't have to hand-write `animation:` CSS). Use the keyframe's `ref` as the CSS animation-name. For a TRANSITION keyframe, put the target/hover-state styles on the 100% point (points[1].styles) — the 0% point is ignored (the start state is the element's own normal state). To CHANGE an existing keyframe later, use `update_theme_keyframe` (edits in place, keeps the `ref` stable) — do NOT delete+recreate, which changes the ref and breaks references.\n" +
4077
+ "- colorScheme: requires name + colors (array of { slotId? | newSlotName?, value }) — each color targets ONE color SLOT, EITHER by `slotId` (an EXISTING slot's id, from `list_theme_globals` → colorSchemes.schemes[].id or a `colorsByScheme` key — the id-keyed path) OR by `newSlotName` (a name to CREATE-or-reuse a slot; write-only, for the standard vocabulary and brand-new slots). Pass EXACTLY ONE of the two per entry. `value` is EITHER a literal color (hex/rgb/hsl or a CSS color name, e.g. \"#0b1120\") OR a REFERENCE to an existing theme color, passed as that color's cssVar from `list_theme_globals` (e.g. \"var(--<colorId>)\"). A `var(--id)` value is stored as a linked reference (editing the global color updates the scheme too and the dependency is tracked) — do NOT inline a hex you copied from a global color; pass its cssVar. SLOT IDENTITY: `colorSchemes.schemes` lists each slot as `{id, name}`. To set a color on an EXISTING slot, find it there by name and pass its `slotId` (names are NOT unique — two design assets can each have a \"Background\" — so when ambiguous disambiguate via the scheme's existing `colorsByScheme` keys); use `newSlotName` only to add a slot the scheme does not have yet. When creating slots use the theme's STANDARD slot names so components resolve them: \"Background\", \"Heading\", \"Text\", \"Link\", \"HoverLink\", \"Border\", and per button \"PrimaryButton/Background\", \"PrimaryButton/Text\", \"PrimaryButton/Border\", \"PrimaryButton/HoverBackground\", \"PrimaryButton/HoverText\", \"PrimaryButton/HoverBorder\" (and the same six under \"SecondaryButton/\"); a \"/\" groups the slot in the editor UI. A scheme should normally define EVERY slot it uses; omitted slots fall back to the default scheme's color. A mistaken slot can be removed with `delete_theme_global` kind `colorSchemeSlot` (by slot id), or renamed in place (id stays stable) with `rename_theme_color_scheme_slot`. To CHANGE an existing scheme later, use `update_theme_color_scheme` (edits in place, keeps the className stable; merges colors by slot, and can set/clear its default flag via `is_default`).\n" +
4078
+ "Read created items back with `list_theme_globals`. Requires `ikas-component dev` running with the editor connected.", {
4079
+ project_root: z.string().describe("Absolute path to the code-component project."),
4080
+ kind: z
4081
+ .enum(["globalVariable", "color", "typography", "breakpoint", "keyframe", "colorScheme"])
4082
+ .describe("What to create."),
4083
+ name: z.string().optional().describe("Name (design tokens)."),
4084
+ display_name: z.string().optional().describe("Human label (globalVariable)."),
4085
+ type: z.string().optional().describe("globalVariable value type."),
4086
+ value: z.any().optional().describe("globalVariable default value (shape depends on type — see tool description), or color hex string."),
4087
+ font_family: z.string().optional(),
4088
+ font_size: z.string().optional(),
4089
+ font_weight: z.string().optional(),
4090
+ line_height: z.string().optional(),
4091
+ letter_spacing: z.string().optional(),
4092
+ width: z.number().int().optional().describe("breakpoint width in px (positive integer, no decimals)."),
4093
+ keyframe_type: z.enum(["keyframe", "transition"]).optional(),
4094
+ points: z.any().optional().describe("keyframe points array."),
4095
+ settings: z
4096
+ .any()
4097
+ .optional()
4098
+ .describe("keyframe animation settings: array of { property, value } (type-dependent — see tool description)."),
4099
+ colors: z
4100
+ .any()
4101
+ .optional()
4102
+ .describe("colorScheme colors: array of { slotId? | newSlotName?, value } — target an EXISTING slot by `slotId` (its id from list_theme_globals → colorSchemes.schemes / colorsByScheme keys) OR create/reuse one by `newSlotName` (use the standard slot names, \"/\" to group, e.g. \"PrimaryButton/Background\"). Exactly one per entry. value = a literal color hex/rgb/hsl/name, OR an existing theme color's cssVar \"var(--<colorId>)\" to store a linked reference."),
4103
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4104
+ }, async (input) => {
4105
+ const p = input.port ? ["--port", String(input.port)] : [];
4106
+ let args;
4107
+ switch (input.kind) {
4108
+ case "globalVariable":
4109
+ args = [
4110
+ "create-global-variable",
4111
+ "--display-name",
4112
+ String(input.display_name ?? ""),
4113
+ "--type",
4114
+ String(input.type ?? ""),
4115
+ ...(input.value !== undefined ? ["--value", JSON.stringify(input.value)] : []),
4116
+ ...p,
4117
+ ];
4118
+ break;
4119
+ case "color":
4120
+ args = ["create-color", "--name", String(input.name ?? ""), "--value", String(input.value ?? ""), ...p];
4121
+ break;
4122
+ case "typography":
4123
+ args = [
4124
+ "create-text-style",
4125
+ "--name",
4126
+ String(input.name ?? ""),
4127
+ ...(input.font_family ? ["--font-family", input.font_family] : []),
4128
+ ...(input.font_size ? ["--font-size", input.font_size] : []),
4129
+ ...(input.font_weight ? ["--font-weight", input.font_weight] : []),
4130
+ ...(input.line_height ? ["--line-height", input.line_height] : []),
4131
+ ...(input.letter_spacing ? ["--letter-spacing", input.letter_spacing] : []),
4132
+ ...p,
4133
+ ];
4134
+ break;
4135
+ case "breakpoint":
4136
+ args = ["create-breakpoint", "--name", String(input.name ?? ""), "--width", String(input.width ?? ""), ...p];
4137
+ break;
4138
+ case "keyframe":
4139
+ args = [
4140
+ "create-keyframe",
4141
+ "--name",
4142
+ String(input.name ?? ""),
4143
+ ...(input.keyframe_type ? ["--type", input.keyframe_type] : []),
4144
+ "--points",
4145
+ toJsonArrayArg(input.points),
4146
+ ...(input.settings !== undefined ? ["--settings", toJsonArrayArg(input.settings)] : []),
4147
+ ...p,
4148
+ ];
4149
+ break;
4150
+ case "colorScheme":
4151
+ args = [
4152
+ "create-color-scheme",
4153
+ "--name",
4154
+ String(input.name ?? ""),
4155
+ "--colors",
4156
+ toJsonArrayArg(input.colors),
4157
+ ...p,
4158
+ ];
4159
+ break;
4160
+ default:
4161
+ return { content: [{ type: "text", text: `Error: unknown kind "${input.kind}"` }] };
4162
+ }
4163
+ return callEditorAction(input.project_root, args);
4164
+ });
4165
+ // Tool: update_theme_global
4166
+ server.tool("update_theme_global", "Update an existing global variable in the connected editor (e.g. fix its value or change its type). Identify it by `name` (the runtime key from `list_theme_globals`). Only the fields you pass are changed. Value shapes follow `create_theme_global` — pass `value` as REAL JSON (objects/booleans, never stringified); IMAGE is an image REFERENCE object { id: \"theme-images/<uuid>\" } (NOT { url }), BOOLEAN is a real boolean, BORDER/SHADOW are real objects.", {
4167
+ project_root: z.string().describe("Absolute path to the code-component project."),
4168
+ name: z.string().describe("The variable's runtime key (from `list_theme_globals`)."),
4169
+ display_name: z.string().optional().describe("New label."),
4170
+ type: z.string().optional().describe("New value type (TEXT|RICH_TEXT|IMAGE|COLOR|NUMBER|BOOLEAN|BORDER|SHADOW)."),
4171
+ value: z.any().optional().describe('New value as REAL JSON (shape depends on type; e.g. IMAGE: { id: "theme-images/<uuid>" }, BOOLEAN: true/false — see create_theme_global).'),
4172
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4173
+ }, async ({ project_root, name, display_name, type, value, port }) => {
4174
+ const args = [
4175
+ "update-global-variable",
4176
+ "--name",
4177
+ name,
4178
+ ...(display_name !== undefined ? ["--display-name", display_name] : []),
4179
+ ...(type !== undefined ? ["--type", type] : []),
4180
+ ...(value !== undefined ? ["--value", JSON.stringify(value)] : []),
4181
+ ...(port ? ["--port", String(port)] : []),
4182
+ ];
4183
+ return callEditorAction(project_root, args);
4184
+ });
4185
+ // Tool: update_theme_keyframe
4186
+ server.tool("update_theme_keyframe", "Update an existing theme keyframe IN PLACE in the connected editor. Prefer this over delete+recreate: " +
4187
+ "it keeps the keyframe's id, so its runtime `ref` (the CSS animation-name `_<id>`) stays STABLE and every " +
4188
+ "existing reference to it keeps working. Identify the keyframe by `id` (REQUIRED — globally unique, from " +
4189
+ "`list_theme_globals`); name-based lookup is intentionally NOT supported here because names can collide across " +
4190
+ "design assets. To rename, pass `name` (the new name) together with `id`. " +
4191
+ "`points` REPLACES the keyframe's points: an array of { point, styles? } where `point` is a CSS " +
4192
+ "keyframe offset (e.g. \"0%\", \"50%\", \"100%\") and each style is { property, value } using a CSS property " +
4193
+ "name (opacity, transform, filter, background, color, …). Pass `points` as REAL JSON (not a stringified array). " +
4194
+ "For a TRANSITION keyframe, put the target/hover-state styles on the 100% point (points[1].styles) — the 0% " +
4195
+ "point is ignored (the start state is the element's own normal state). " +
4196
+ "Optional `settings` REPLACES the keyframe's animation settings (type-dependent { property, value } entries — " +
4197
+ "see `create_theme_global`); OMIT it to keep the existing settings, or pass [] to clear them. " +
4198
+ "Requires `ikas-component dev` running with the editor connected.", {
4199
+ project_root: z.string().describe("Absolute path to the code-component project."),
4200
+ id: z.string().describe("Keyframe id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4201
+ name: z
4202
+ .string()
4203
+ .optional()
4204
+ .describe("Optional NEW name to rename the keyframe to. NOT used to find the token — lookup is always by id."),
4205
+ keyframe_type: z.enum(["keyframe", "transition"]).optional().describe("Optional new keyframe type."),
4206
+ points: z.any().describe("New keyframe points array of { point, styles? } — REPLACES the existing points."),
4207
+ settings: z
4208
+ .any()
4209
+ .optional()
4210
+ .describe("New animation settings { property, value } array (type-dependent — see create_theme_global). Omit to keep existing; [] to clear."),
4211
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4212
+ }, async ({ project_root, id, name, keyframe_type, points, settings, port }) => {
4213
+ const args = [
4214
+ "update-keyframe",
4215
+ "--id",
4216
+ id,
4217
+ ...(name !== undefined ? ["--name", name] : []),
4218
+ ...(keyframe_type ? ["--type", keyframe_type] : []),
4219
+ "--points",
4220
+ toJsonArrayArg(points),
4221
+ ...(settings !== undefined ? ["--settings", toJsonArrayArg(settings)] : []),
4222
+ ...(port ? ["--port", String(port)] : []),
4223
+ ];
4224
+ return callEditorAction(project_root, args);
4225
+ });
4226
+ // Tool: update_text_style
4227
+ server.tool("update_text_style", "Update an existing theme typography token (text style) IN PLACE in the connected editor. Prefer this over " +
4228
+ "delete+recreate: it keeps the text style's id, so its `className` (`_<id>`) — and every section bound to it — " +
4229
+ "stays STABLE. Identify the text style by `id` (REQUIRED — globally unique, from `list_theme_globals`); " +
4230
+ "name-based lookup is intentionally NOT supported here because names can collide across design assets. " +
4231
+ "To rename, pass `name` (the new name) together with `id`. Pass only the " +
4232
+ "font properties you want to change (font_family, font_size, font_weight, line_height, letter_spacing); omitted " +
4233
+ "ones keep their existing values. VALUE FORMATS (validated): font_weight is one " +
4234
+ "of 100…900 (append 'i' for italic, e.g. \"600i\") AND must be a weight the chosen font actually ships — many fonts " +
4235
+ "only provide a subset (e.g. \"Modak\" only ships 400); if a weight isn't available the call fails and the error " +
4236
+ "lists the supported weights, so when unsure use 400; font_size is a number with a unit (e.g. \"16px\"); line_height " +
4237
+ "is \"normal\", a unitless number or a length; letter_spacing is \"normal\" or a length (e.g. \"0.02em\"). Requires " +
4238
+ "`ikas-component dev` running with the editor connected.", {
4239
+ project_root: z.string().describe("Absolute path to the code-component project."),
4240
+ id: z.string().describe("Text style id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4241
+ name: z
4242
+ .string()
4243
+ .optional()
4244
+ .describe("Optional NEW name to rename the text style to. NOT used to find the token — lookup is always by id."),
4245
+ font_family: z.string().optional(),
4246
+ font_size: z.string().optional(),
4247
+ font_weight: z
4248
+ .string()
4249
+ .optional()
4250
+ .describe("font-weight as a number (e.g. \"600\"); append 'i' for italic (e.g. \"600i\"). Must be a weight the chosen " +
4251
+ "font ships — if it isn't, the error lists the supported weights; when unsure use \"400\"."),
4252
+ line_height: z.string().optional(),
4253
+ letter_spacing: z.string().optional(),
4254
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4255
+ }, async ({ project_root, id, name, font_family, font_size, font_weight, line_height, letter_spacing, port }) => {
4256
+ const args = [
4257
+ "update-text-style",
4258
+ "--id",
4259
+ id,
4260
+ ...(name !== undefined ? ["--name", name] : []),
4261
+ ...(font_family ? ["--font-family", font_family] : []),
4262
+ ...(font_size ? ["--font-size", font_size] : []),
4263
+ ...(font_weight ? ["--font-weight", font_weight] : []),
4264
+ ...(line_height ? ["--line-height", line_height] : []),
4265
+ ...(letter_spacing ? ["--letter-spacing", letter_spacing] : []),
4266
+ ...(port ? ["--port", String(port)] : []),
4267
+ ];
4268
+ return callEditorAction(project_root, args);
4269
+ });
4270
+ // Tool: update_theme_color
4271
+ server.tool("update_theme_color", "Update an existing theme color token IN PLACE in the connected editor. Prefer this over delete+recreate: " +
4272
+ "it keeps the color's id, so its cssVar (`var(--<id>)`) — and every element style bound to it — stays STABLE. " +
4273
+ "Identify it by `id` (REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally " +
4274
+ "NOT supported here because a theme can have two colors with the same name (e.g. from two different design assets). " +
4275
+ "To rename, pass `name` (the new name) together with `id`. Pass `value` to change the color — a CONCRETE " +
4276
+ "color (hex/rgb/hsl or a CSS color name), NOT a \"var(...)\" reference (a color token can't alias another " +
4277
+ "color); omit it to only rename. Requires `ikas-component " +
4278
+ "dev` running with the editor connected.", {
4279
+ project_root: z.string().describe("Absolute path to the code-component project."),
4280
+ id: z.string().describe("Color id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4281
+ name: z
4282
+ .string()
4283
+ .optional()
4284
+ .describe("Optional NEW name to rename the color to. NOT used to find the token — lookup is always by id."),
4285
+ value: z
4286
+ .string()
4287
+ .optional()
4288
+ .describe('New color value — a concrete color (e.g. "#ff0000", rgb/hsl/name), not a "var(...)" reference. Omit to only rename.'),
4289
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4290
+ }, async ({ project_root, id, name, value, port }) => {
4291
+ const args = [
4292
+ "update-color",
4293
+ "--id",
4294
+ id,
4295
+ ...(name !== undefined ? ["--name", name] : []),
4296
+ ...(value !== undefined ? ["--value", value] : []),
4297
+ ...(port ? ["--port", String(port)] : []),
4298
+ ];
4299
+ return callEditorAction(project_root, args);
4300
+ });
4301
+ // Tool: update_theme_breakpoint
4302
+ server.tool("update_theme_breakpoint", "Update an existing theme breakpoint IN PLACE in the connected editor (keeps its id). Identify it by `id` " +
4303
+ "(REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally NOT supported here " +
4304
+ "because names can collide across design assets. To rename, pass `name` (the new name) together with `id`. " +
4305
+ "Pass `width` (px) to change it; omit it to only rename. " +
4306
+ "Requires `ikas-component dev` running with the editor connected.", {
4307
+ project_root: z.string().describe("Absolute path to the code-component project."),
4308
+ id: z.string().describe("Breakpoint id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4309
+ name: z
4310
+ .string()
4311
+ .optional()
4312
+ .describe("Optional NEW name to rename the breakpoint to. NOT used to find the token — lookup is always by id."),
4313
+ width: z.number().int().optional().describe("New breakpoint width in px (positive integer, no decimals). Omit to only rename."),
4314
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4315
+ }, async ({ project_root, id, name, width, port }) => {
4316
+ const args = [
4317
+ "update-breakpoint",
4318
+ "--id",
4319
+ id,
4320
+ ...(name !== undefined ? ["--name", name] : []),
4321
+ ...(width !== undefined ? ["--width", String(width)] : []),
4322
+ ...(port ? ["--port", String(port)] : []),
4323
+ ];
4324
+ return callEditorAction(project_root, args);
4325
+ });
4326
+ // Tool: update_theme_color_scheme
4327
+ server.tool("update_theme_color_scheme", "Update an existing theme color scheme (palette) IN PLACE in the connected editor. Prefer this over " +
4328
+ "delete+recreate: it keeps the scheme's id, so its className (`_<id>`) stays STABLE. Identify it by `id` " +
4329
+ "(REQUIRED — globally unique, from `list_theme_globals`); name-based lookup is intentionally NOT supported here " +
4330
+ "because names can collide across design assets. To rename, pass `name` (the new name) together with `id`. " +
4331
+ "`colors` is an array of { slotId? | newSlotName?, value } — each entry targets ONE slot, EITHER by " +
4332
+ "`slotId` (an EXISTING slot's id, from list_theme_globals colorsByScheme keys / colorSchemes.schemes — the " +
4333
+ "id-keyed path; slot names ARE listed in colorSchemes.schemes but are not unique, so prefer the scheme's colorsByScheme keys to get the right id) OR by `newSlotName` (create/reuse " +
4334
+ "a slot by name; use the standard slot names, \"/\" to group, e.g. \"PrimaryButton/Background\"); value = a " +
4335
+ "literal color hex/rgb/hsl/name, OR an existing theme color's cssVar \"var(--<colorId>)\" from " +
4336
+ "list_theme_globals to store it as a LINKED reference — pass the cssVar, not a hex copied from a global color) " +
4337
+ "and MERGES by slot — only the slots you pass change. Pass `colors` as REAL " +
4338
+ "JSON (not a stringified array). Set `is_default: true` to make this the theme's DEFAULT scheme (the " +
4339
+ "fallback whose colors fill any slot a non-default scheme leaves unset); this is exclusive — the editor " +
4340
+ "automatically clears the default flag from any other scheme in the same group, so there is always exactly " +
4341
+ "one default. Set `is_default: false` to remove the default flag from this scheme. Omit all of name/colors/" +
4342
+ "is_default to no-op. Requires `ikas-component dev` running with the editor connected.", {
4343
+ project_root: z.string().describe("Absolute path to the code-component project."),
4344
+ id: z.string().describe("Color scheme id (REQUIRED) from `list_theme_globals`. The token is always identified by id."),
4345
+ name: z
4346
+ .string()
4347
+ .optional()
4348
+ .describe("Optional NEW name to rename the color scheme to. NOT used to find the token — lookup is always by id."),
4349
+ colors: z
4350
+ .any()
4351
+ .optional()
4352
+ .describe("Array of { slotId? | newSlotName?, value } to set — target an existing slot by `slotId` (from list_theme_globals colorsByScheme keys / colorSchemes.schemes) or create/reuse by `newSlotName`; value = a literal color hex/rgb/hsl/name, OR an existing theme color's cssVar \"var(--<colorId>)\" (linked reference). Merges by slot. Omit to only rename."),
4353
+ is_default: z
4354
+ .boolean()
4355
+ .optional()
4356
+ .describe("true = make this the default scheme (exclusive — the editor clears the default flag from every other scheme in its group); false = remove the default flag. Omit to leave the default unchanged. The result echoes the persisted `isDefault`."),
4357
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4358
+ }, async ({ project_root, id, name, colors, is_default, port }) => {
4359
+ const args = [
4360
+ "update-color-scheme",
4361
+ "--id",
4362
+ id,
4363
+ ...(name !== undefined ? ["--name", name] : []),
4364
+ ...(colors !== undefined ? ["--colors", toJsonArrayArg(colors)] : []),
4365
+ ...(is_default === true ? ["--default"] : is_default === false ? ["--no-default"] : []),
4366
+ ...(port ? ["--port", String(port)] : []),
4367
+ ];
4368
+ return callEditorAction(project_root, args);
4369
+ });
4370
+ // Tool: rename_theme_color_scheme_slot
4371
+ server.tool("rename_theme_color_scheme_slot", "Rename a color scheme SLOT (a scheme key like \"Background\" or \"PrimaryButton/Text\") IN PLACE in the " +
4372
+ "connected editor. A slot's id is STABLE, so renaming keeps every runtime reference intact " +
4373
+ "(`var(--<slotId>)` in CSS, `colorsByScheme[slotId]` in component code) — only the human display name " +
4374
+ "changes; nothing rebinds. Identify the slot by `id` (REQUIRED — the slot id from `list_theme_globals` → " +
4375
+ "colorSchemes.schemes[].id or a `colorsByScheme` key); name-based lookup is intentionally NOT supported " +
4376
+ "because slot names can collide across design assets. `name` is the NEW slot name (a \"/\" groups it in the " +
4377
+ "editor UI, e.g. \"PrimaryButton/Background\"). Only non-design-asset (user/theme) slots can be renamed here. " +
4378
+ "This renames the slot itself across ALL schemes that use it (a slot is shared) — it does NOT rename a single " +
4379
+ "scheme/palette (for that use `update_theme_color_scheme`). Requires `ikas-component dev` running with the " +
4380
+ "editor connected.", {
4381
+ project_root: z.string().describe("Absolute path to the code-component project."),
4382
+ id: z
4383
+ .string()
4384
+ .min(1, "id is REQUIRED — the slot id from list_theme_globals (colorSchemes.schemes[].id / colorsByScheme keys).")
4385
+ .describe("Slot id (REQUIRED) from `list_theme_globals` → colorSchemes.schemes[].id / colorsByScheme keys. The slot is always identified by id."),
4386
+ name: z
4387
+ .string()
4388
+ .min(1, "name is REQUIRED — the new slot name (non-empty).")
4389
+ .describe("The NEW slot name (non-empty). A \"/\" groups it in the editor UI (e.g. \"PrimaryButton/Background\")."),
4390
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4391
+ }, async ({ project_root, id, name, port }) => {
4392
+ const args = [
4393
+ "rename-color-scheme-slot",
4394
+ "--id",
4395
+ id,
4396
+ "--name",
4397
+ name,
4398
+ ...(port ? ["--port", String(port)] : []),
4399
+ ];
4400
+ return callEditorAction(project_root, args);
4401
+ });
4402
+ // Tool: delete_theme_global
4403
+ server.tool("delete_theme_global", "Delete a theme global setting from the connected editor. For `kind: globalVariable` pass `name` (the runtime key); for design-token kinds (color | typography | breakpoint | keyframe | colorScheme) pass the token `id`. For `kind: colorSchemeSlot` pass the color SLOT id (from `list_theme_globals` → colorSchemes.schemes[].id or a `colorsByScheme` key) — this deletes the slot itself across ALL schemes (use it to clean up a junk slot created by a typo); it CASCADES: the slot's color is removed from every scheme and any element/component style bound to that slot is cleared, so only delete a slot you are sure is unused. Only non-design-asset (user/theme) slots can be deleted here. Get names/ids from `list_theme_globals`. Deleting a non-existent id fails with a \"not found\" error (it does NOT report a false success), so a success result means the token really existed and was removed.", {
4404
+ project_root: z.string().describe("Absolute path to the code-component project."),
4405
+ kind: z
4406
+ .enum(["globalVariable", "color", "typography", "breakpoint", "keyframe", "colorScheme", "colorSchemeSlot"])
4407
+ .describe("What to delete."),
4408
+ name: z.string().optional().describe("globalVariable runtime key (from `list_theme_globals`)."),
4409
+ id: z
4410
+ .string()
4411
+ .optional()
4412
+ .describe("design-token id, or for colorSchemeSlot the slot id from `list_theme_globals` → colorSchemes.schemes."),
4413
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
4414
+ }, async ({ project_root, kind, name, id, port }) => {
4415
+ const p = port ? ["--port", String(port)] : [];
4416
+ const args = kind === "globalVariable"
4417
+ ? ["delete-global-variable", "--name", String(name ?? ""), ...p]
4418
+ : ["delete-design-token", "--token-type", kind, "--id", String(id ?? ""), ...p];
4419
+ return callEditorAction(project_root, args);
4420
+ });
4052
4421
  // --- Start server ---
4053
4422
  async function main() {
4054
4423
  const transport = new StdioServerTransport();