@ompo-design/mcp-server 0.1.11 → 0.1.13

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.
@@ -11,7 +11,7 @@ export type ApplyPlanFile = {
11
11
  layoutIntent?: string;
12
12
  };
13
13
  export type DomStructurePlan = {
14
- kind: 'dom.insert' | 'dom.move' | 'dom.delete' | 'dom.flexWrap';
14
+ kind: 'dom.insert' | 'dom.move' | 'dom.delete' | 'dom.flexWrap' | 'dom.iconReplace';
15
15
  summary: string;
16
16
  steps: string[];
17
17
  payload: Record<string, unknown>;
@@ -1,12 +1,19 @@
1
1
  const OMPO_GLOSSARY = {
2
+ layoutNone: `Ompo layout "None" means the element is a normal block container (\`display: block\`), not a flex or grid layout parent. Do not add flex-direction, grid-template-columns, or gap unless a separate edit sets \`display: flex\` or \`display: grid\` on this selector.`,
3
+ layoutFlex: `Ompo layout "Flex" maps to \`display: flex\` (or inline-flex) on this selector. Apply flexDirection, justifyContent, alignItems, and gap together on the same container. Row/column toggles in Ompo map to flex-direction.`,
4
+ layoutGrid: `Ompo layout "Grid" maps to \`display: grid\` on this selector. Apply gridTemplateColumns (often \`repeat(n, 1fr)\`), optional gridTemplateRows / gridAutoRows, gridAutoFlow (row vs column), justifyContent, alignItems, and gap together on the grid parent. Prefer grid over flex for multi-column card/bento layouts.`,
2
5
  gap: `Ompo "Gap" is the space between children inside a flex or grid container. Apply \`gap\` on the parent layout element (not on children). The parent needs \`display: flex\` or \`display: grid\`. Example: gap "16px" → CSS \`gap: 16px\` or Tailwind \`gap-4\`.`,
3
- fill: `Ompo "Fill" means an element should expand to consume free space inside its parent. Fill is NOT a CSS property — read \`widthMode: fill\` / \`heightMode: fill\` together with \`ensureParentFlex\`, \`flexGrow\`, \`flexShrink\`, \`flexBasis\`, and \`alignSelf\` on the same selector. Parent must be \`display: flex\`. Main axis (direction of flex): use \`flex: 1 1 0\` (grow/shrink/basis). Cross axis: use \`align-self: stretch\` and remove fixed width/height on that axis. If \`ensureParentFlex\` is present, make the parent a flex container with that direction first.`,
6
+ fill: `Ompo "Fill" means an element should expand to consume free space inside its parent. Fill is NOT a CSS property — read \`widthMode: fill\` / \`heightMode: fill\` together with \`ensureParentFlex\`, \`flexGrow\`, \`flexShrink\`, \`flexBasis\`, and \`alignSelf\` on the same selector. Parent must be \`display: flex\` (Fill does not apply inside grid parents). Main axis (direction of flex): use \`flex: 1 1 0\` (grow/shrink/basis). Cross axis: use \`align-self: stretch\` and remove fixed width/height on that axis. If \`ensureParentFlex\` is present, make the parent a flex container with that direction first.`,
4
7
  fit: `Ompo "Fit" means size to content. Map \`widthMode: fit\` → \`width: fit-content\`, \`heightMode: fit\` → \`height: fit-content\` (or the project's equivalent).`,
5
- imageFill: `Ompo image background fill: copy the source file from \`backgroundImageSource\` into the project (e.g. public/images/), then set \`background-image\` to a project-relative url(), with \`background-size: cover\`, \`background-position: center\`, and \`background-repeat: no-repeat\`. Do not commit file:// URLs to source.`,
8
+ imageFill: `Ompo image background fill: when \`backgroundImageSource\` is an https URL, download that asset into the project (e.g. public/images/) and set \`background-image\` to a project-relative url(). For legacy local paths, copy from the user's machine instead. Always use \`background-size: cover\`, \`background-position: center\`, and \`background-repeat: no-repeat\`. Do not commit file:// URLs to source.`,
6
9
  domMove: `Ompo DOM move: reorder or reparent elements in source markup/components. Use movedElement anchors (tag, id, class, textSnippet) to find nodes. destinationChildren is the authoritative sibling order after the move — match this order in JSX/HTML or reorder mapped arrays. CSS alone cannot satisfy a dom.move.`,
7
10
  insertText: `Ompo text insert: the user added a new text block (usually <p>). insertedElement carries insertKind "text", a stable data-ompo-insert-id selector, and textSnippet for the current copy. Apply as markup or a text component at parent/index; check separate text operations for edits after insert.`,
11
+ insertSection: `Ompo section insert: the user added a full-width page section (<div>, insertKind "section"). Default size is width 100% and height 300px with fill #AEC8F8. Append at the page root / bottom of main content unless parent/index says otherwise.`,
12
+ insertFrame: `Ompo frame insert: the user added a small div (insertKind "frame"). Default size is 150×80px with fill #AEC8F8. Use for nested containers, cards, or tiles inside a section.`,
8
13
  insertIcon: `Ompo icon insert: the user added an Iconify icon (span.ompo-insert-icon) or editable SVG. iconId is the Iconify name (e.g. lucide:home) when known. Prefer the project's icon library or @iconify/react with that id instead of inlining raw SVG when possible. Size, fill color, and stroke changes appear as style operations on the same stable data-ompo-insert-id or data-ompo-icon-id selector.`,
9
- iconStroke: `Ompo icon/SVG stroke: iconStrokeWidth sets stroke-width on the inner SVG; iconStrokeColor sets stroke (often currentColor). Applies to inserted icons and native SVG elements selected in Ompo.`
14
+ replaceIcon: `Ompo icon replace: the user swapped an existing icon/SVG to a different Iconify icon. Update the project's icon component or @iconify/react usage to the new iconId do not only patch inline SVG unless the project already inlines icons.`,
15
+ iconStroke: `Ompo icon/SVG stroke: iconStrokeWidth sets stroke-width on the inner SVG; iconStrokeColor sets stroke (often currentColor). Applies to inserted icons and native SVG elements selected in Ompo.`,
16
+ zIndex: `Ompo "Z Index" maps to CSS \`z-index\` on the selected element. Use the integer from the edit, or \`auto\` to clear an authored value. Higher values stack above lower values within the same stacking context. If stacking does not change in the browser, the element may also need a non-static \`position\` (e.g. relative) in source — Ompo may set inline z-index without changing position.`
10
17
  };
11
18
  function describeAnchor(anchor) {
12
19
  const parts = [anchor.tagName];
@@ -28,6 +35,8 @@ function buildDomInsertPlan(operation) {
28
35
  const inserted = operation.insertedElement;
29
36
  const isIcon = operation.insertKind === 'icon';
30
37
  const isText = operation.insertKind === 'text';
38
+ const isSection = operation.insertKind === 'section';
39
+ const isFrame = operation.insertKind === 'frame';
31
40
  const steps = [];
32
41
  if (operation.parent) {
33
42
  steps.push(`Locate destination parent: ${describeAnchor(operation.parent)}.`);
@@ -44,11 +53,20 @@ function buildDomInsertPlan(operation) {
44
53
  if (isText) {
45
54
  steps.push(OMPO_GLOSSARY.insertText);
46
55
  }
56
+ else if (isSection) {
57
+ steps.push(OMPO_GLOSSARY.insertSection);
58
+ }
59
+ else if (isFrame) {
60
+ steps.push(OMPO_GLOSSARY.insertFrame);
61
+ }
47
62
  else if (isIcon) {
48
63
  steps.push(OMPO_GLOSSARY.insertIcon);
49
64
  if (operation.iconId) {
50
65
  steps.push(`Iconify id: ${operation.iconId}`);
51
66
  }
67
+ if (operation.iconSvg) {
68
+ steps.push('iconSvg is included in the plan payload for inline SVG or reference.');
69
+ }
52
70
  }
53
71
  steps.push(`Insert at child index ${operation.index}.`);
54
72
  steps.push('Sanitize and adapt the HTML to the project’s component conventions before writing.');
@@ -56,7 +74,11 @@ function buildDomInsertPlan(operation) {
56
74
  ? `Insert Iconify icon${operation.iconId ? ` (${operation.iconId})` : ''}`
57
75
  : isText
58
76
  ? 'Insert text block'
59
- : 'Insert new element into the page';
77
+ : isSection
78
+ ? 'Insert page section'
79
+ : isFrame
80
+ ? 'Insert frame div'
81
+ : 'Insert new element into the page';
60
82
  return {
61
83
  kind: 'dom.insert',
62
84
  summary,
@@ -68,11 +90,41 @@ function buildDomInsertPlan(operation) {
68
90
  selector: operation.selector,
69
91
  insertKind: operation.insertKind,
70
92
  iconId: operation.iconId,
93
+ iconSvg: operation.iconSvg,
71
94
  insertedElement: operation.insertedElement,
72
95
  parent: operation.parent
73
96
  }
74
97
  };
75
98
  }
99
+ function buildIconReplacePlan(operation) {
100
+ const steps = [];
101
+ if (operation.element) {
102
+ steps.push(`Locate icon element: ${describeAnchor(operation.element)} (selector: "${operation.selector}").`);
103
+ }
104
+ else {
105
+ steps.push(`Locate icon element "${operation.selector}" in source.`);
106
+ }
107
+ steps.push(OMPO_GLOSSARY.replaceIcon);
108
+ steps.push(`Set Iconify id to ${operation.iconId}.`);
109
+ if (operation.iconSvg) {
110
+ steps.push('Use iconSvg from the payload when the project inlines SVG markup.');
111
+ }
112
+ if (operation.previousIconId) {
113
+ steps.push(`Previous icon id: ${operation.previousIconId}`);
114
+ }
115
+ return {
116
+ kind: 'dom.iconReplace',
117
+ summary: `Replace icon with ${operation.iconId}`,
118
+ steps,
119
+ payload: {
120
+ selector: operation.selector,
121
+ iconId: operation.iconId,
122
+ iconSvg: operation.iconSvg,
123
+ previousIconId: operation.previousIconId,
124
+ element: operation.element
125
+ }
126
+ };
127
+ }
76
128
  function formatChildOrder(children, movedSelector) {
77
129
  return children.map((child, index) => {
78
130
  const marker = child.selector === movedSelector ? ' ← moved element' : '';
@@ -162,10 +214,120 @@ function formatChangedValue(property, value) {
162
214
  }
163
215
  return '(empty / remove)';
164
216
  }
217
+ function layoutModeFromChanged(changed) {
218
+ const display = typeof changed.display === 'string' ? changed.display : null;
219
+ if (display === 'flex' || display === 'inline-flex')
220
+ return 'flex';
221
+ if (display === 'grid' || display === 'inline-grid')
222
+ return 'grid';
223
+ if (display === 'block' || display === 'none')
224
+ return 'block';
225
+ if (changed.gridTemplateColumns !== undefined || changed.gridAutoFlow !== undefined) {
226
+ return 'grid';
227
+ }
228
+ if (changed.flexDirection !== undefined)
229
+ return 'flex';
230
+ return null;
231
+ }
232
+ function imageFillSourceNotes(source) {
233
+ const trimmed = source.trim();
234
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
235
+ return `${OMPO_GLOSSARY.imageFill} Download ${trimmed}, save into the project (e.g. public/images/), and reference the copied asset in CSS.`;
236
+ }
237
+ return `${OMPO_GLOSSARY.imageFill} Read this file from the user's machine (${trimmed}), copy it into the repo, and reference the copied asset in CSS.`;
238
+ }
165
239
  function styleSuggestionForProperty(property, value, changed) {
166
240
  const parentFlex = changed.ensureParentFlex;
167
241
  const parentFlexLabel = parentFlex === 'row' || parentFlex === 'column' ? parentFlex : 'row or column';
242
+ const layoutMode = layoutModeFromChanged(changed);
168
243
  switch (property) {
244
+ case 'display': {
245
+ const displayValue = String(value);
246
+ if (displayValue === 'grid' || displayValue === 'inline-grid') {
247
+ return {
248
+ property,
249
+ value: displayValue,
250
+ strategy: 'css-rule',
251
+ notes: OMPO_GLOSSARY.layoutGrid
252
+ };
253
+ }
254
+ if (displayValue === 'flex' || displayValue === 'inline-flex') {
255
+ return {
256
+ property,
257
+ value: displayValue,
258
+ strategy: 'css-rule',
259
+ notes: OMPO_GLOSSARY.layoutFlex
260
+ };
261
+ }
262
+ return {
263
+ property,
264
+ value: displayValue,
265
+ strategy: 'css-rule',
266
+ notes: OMPO_GLOSSARY.layoutNone
267
+ };
268
+ }
269
+ case 'gridTemplateColumns':
270
+ return {
271
+ property,
272
+ value: formatChangedValue(property, value),
273
+ strategy: 'css-rule',
274
+ notes: `${OMPO_GLOSSARY.layoutGrid} Column template for the grid parent — e.g. repeat(3, 1fr) for three equal columns. Tailwind: grid-cols-3 or arbitrary grid-template-columns.`
275
+ };
276
+ case 'gridTemplateRows':
277
+ return {
278
+ property,
279
+ value: formatChangedValue(property, value),
280
+ strategy: 'css-rule',
281
+ notes: `${OMPO_GLOSSARY.layoutGrid} Explicit row template. Use when Ompo authored fixed row tracks; otherwise grid-auto-rows may define implicit row sizing.`
282
+ };
283
+ case 'gridAutoRows':
284
+ return {
285
+ property,
286
+ value: formatChangedValue(property, value),
287
+ strategy: 'css-rule',
288
+ notes: `${OMPO_GLOSSARY.layoutGrid} Size for implicit grid rows (auto-generated tracks). Common: auto, minmax(180px, auto).`
289
+ };
290
+ case 'gridAutoColumns':
291
+ return {
292
+ property,
293
+ value: formatChangedValue(property, value),
294
+ strategy: 'css-rule',
295
+ notes: `${OMPO_GLOSSARY.layoutGrid} Size for implicit grid columns when grid-auto-flow creates new columns.`
296
+ };
297
+ case 'gridAutoFlow':
298
+ return {
299
+ property,
300
+ value: formatChangedValue(property, value),
301
+ strategy: 'css-rule',
302
+ notes: `${OMPO_GLOSSARY.layoutGrid} Ompo row/column direction toggle on grid maps to grid-auto-flow (row | column). Apply on the grid container.`
303
+ };
304
+ case 'gridColumn':
305
+ case 'gridRow':
306
+ return {
307
+ property,
308
+ value: formatChangedValue(property, value),
309
+ strategy: 'css-rule',
310
+ notes: `${OMPO_GLOSSARY.layoutGrid} Grid item placement on this child selector (span / line placement).`
311
+ };
312
+ case 'flexDirection':
313
+ return {
314
+ property,
315
+ value: formatChangedValue(property, value),
316
+ strategy: 'css-rule',
317
+ notes: layoutMode === 'grid'
318
+ ? `${OMPO_GLOSSARY.layoutGrid} flex-direction does not apply when display is grid — use gridAutoFlow instead if present in the same edit.`
319
+ : `${OMPO_GLOSSARY.layoutFlex} Main axis direction for the flex container.`
320
+ };
321
+ case 'justifyContent':
322
+ case 'alignItems':
323
+ return {
324
+ property,
325
+ value: formatChangedValue(property, value),
326
+ strategy: 'css-rule',
327
+ notes: layoutMode === 'grid'
328
+ ? `${OMPO_GLOSSARY.layoutGrid} Alignment on the grid container (${property}). Maps from Ompo’s alignment pinner / space-between toggle.`
329
+ : `${OMPO_GLOSSARY.layoutFlex} Alignment on the flex container (${property}). Maps from Ompo’s alignment pinner / space-between toggle.`
330
+ };
169
331
  case 'gap':
170
332
  return {
171
333
  property,
@@ -255,7 +417,7 @@ function styleSuggestionForProperty(property, value, changed) {
255
417
  property,
256
418
  value: formatChangedValue(property, value),
257
419
  strategy: 'component-markup',
258
- notes: `${OMPO_GLOSSARY.imageFill} Read this file from the user's machine (${String(value)}), copy it into the repo, and reference the copied asset in CSS.`
420
+ notes: imageFillSourceNotes(String(value))
259
421
  };
260
422
  case 'backgroundImage':
261
423
  if (changed.backgroundImageSource) {
@@ -263,7 +425,7 @@ function styleSuggestionForProperty(property, value, changed) {
263
425
  property,
264
426
  value: formatChangedValue(property, value),
265
427
  strategy: 'css-rule',
266
- notes: `${OMPO_GLOSSARY.imageFill} Apply after copying the file from backgroundImageSource.`
428
+ notes: imageFillSourceNotes(String(changed.backgroundImageSource))
267
429
  };
268
430
  }
269
431
  break;
@@ -287,6 +449,13 @@ function styleSuggestionForProperty(property, value, changed) {
287
449
  strategy: 'inline-style',
288
450
  notes: OMPO_GLOSSARY.iconStroke
289
451
  };
452
+ case 'zIndex':
453
+ return {
454
+ property,
455
+ value: formatChangedValue(property, value),
456
+ strategy: 'css-rule',
457
+ notes: `${OMPO_GLOSSARY.zIndex} Apply as CSS \`z-index\` on this selector.`
458
+ };
290
459
  }
291
460
  return {
292
461
  property,
@@ -301,6 +470,30 @@ function styleSuggestions(operation) {
301
470
  }
302
471
  function summarizeLayoutIntent(changed) {
303
472
  const parts = [];
473
+ const layoutMode = layoutModeFromChanged(changed);
474
+ if (changed.display !== undefined) {
475
+ if (layoutMode === 'grid') {
476
+ parts.push('Layout: Grid container');
477
+ }
478
+ else if (layoutMode === 'flex') {
479
+ parts.push('Layout: Flex container');
480
+ }
481
+ else if (layoutMode === 'block') {
482
+ parts.push('Layout: None (block flow, not flex/grid)');
483
+ }
484
+ }
485
+ if (changed.gridTemplateColumns !== undefined) {
486
+ parts.push(`Grid columns: ${changed.gridTemplateColumns}`);
487
+ }
488
+ if (changed.gridTemplateRows !== undefined) {
489
+ parts.push(`Grid rows: ${changed.gridTemplateRows}`);
490
+ }
491
+ if (changed.gridAutoFlow !== undefined) {
492
+ parts.push(`Grid auto-flow: ${changed.gridAutoFlow}`);
493
+ }
494
+ if (changed.flexDirection !== undefined && layoutMode !== 'grid') {
495
+ parts.push(`Flex direction: ${changed.flexDirection}`);
496
+ }
304
497
  if (changed.gap !== undefined) {
305
498
  parts.push(`Set container gap to ${changed.gap}`);
306
499
  }
@@ -313,6 +506,9 @@ function summarizeLayoutIntent(changed) {
313
506
  if (changed.ensureParentFlex) {
314
507
  parts.push(`Parent needs display:flex with flex-direction:${changed.ensureParentFlex}`);
315
508
  }
509
+ if (changed.zIndex !== undefined) {
510
+ parts.push(`Set z-index to ${changed.zIndex}`);
511
+ }
316
512
  return parts.length > 0 ? parts.join('. ') : null;
317
513
  }
318
514
  function buildDomStructurePlan(operation) {
@@ -325,7 +521,8 @@ function buildDomStructurePlan(operation) {
325
521
  `Locate the parent element matching "${operation.parentSelector}" in source.`,
326
522
  `Insert a new <div> wrapper at child index ${operation.index} inside that parent.`,
327
523
  `Move the matched child elements into the new wrapper. Use the child anchors (tag, id, class, textSnippet) to find the correct nodes in source.`,
328
- `Apply only the wrapper styles listed in wrapperStyles to the new container.`,
524
+ `Apply only the wrapper styles listed in wrapperStyles to the new container (typically display:flex and flex-direction).`,
525
+ 'For multi-column card layouts, prefer display:grid with grid-template-columns if wrapperStyles do not already specify layout.',
329
526
  'Do not change unrelated siblings or parent styling.'
330
527
  ],
331
528
  payload: {
@@ -341,6 +538,8 @@ function buildDomStructurePlan(operation) {
341
538
  return buildDomInsertPlan(operation);
342
539
  case 'dom.move':
343
540
  return buildDomMovePlan(operation);
541
+ case 'dom.iconReplace':
542
+ return buildIconReplacePlan(operation);
344
543
  case 'dom.delete':
345
544
  return {
346
545
  kind: 'dom.delete',
@@ -438,14 +637,19 @@ export function buildApplyPlan(bundle) {
438
637
  instructions: [
439
638
  'Only apply properties and operations listed in this plan.',
440
639
  'Leave all untouched styling and markup unchanged.',
441
- 'Read ompoGlossary.fill and ompoGlossary.gap before applying layout changes — Ompo terms are not literal CSS properties.',
442
- 'When widthMode or heightMode is "fill", apply the grouped flex properties together (flexGrow, flexBasis, alignSelf, removed width/height) do not set a fixed px width/height on the filled axis.',
640
+ 'Read ompoGlossary.layoutNone, layoutFlex, layoutGrid, fill, and gap before applying layout changes.',
641
+ 'Ompo layout types: None display:block; Flex display:flex + flexDirection; Grid display:grid + gridTemplateColumns (+ gridAutoFlow for row/column).',
642
+ 'When display is grid, apply gridTemplateColumns / gridAutoFlow / gap / justifyContent / alignItems on the grid parent — do not convert grid containers to flex.',
643
+ 'When display is block (layout None), do not add flex or grid properties unless this edit explicitly sets display:flex or display:grid.',
644
+ 'When widthMode or heightMode is "fill", apply the grouped flex properties together (flexGrow, flexBasis, alignSelf, removed width/height) — Fill requires a flex parent, not grid.',
443
645
  'Gap belongs on the flex/grid parent. Fill belongs on the child inside a flex parent.',
444
646
  'DOM moves are in domStructurePlans and domChanges — reorder or reparent elements in source markup/components, not just CSS.',
445
647
  'Read ompoGlossary.domMove before applying dom.move operations.',
446
648
  'For dom.move: destinationChildren is the structural snapshot of the final sibling order — match it in JSX/HTML or reorder mapped arrays.',
447
649
  'For dom.flexWrap: create a new wrapper, move matched children, then apply wrapperStyles.',
448
650
  'For dom.insert with insertKind text or icon: read ompoGlossary.insertText / insertIcon; use insertedElement anchors and iconId when applying.',
651
+ 'For dom.iconReplace: read ompoGlossary.replaceIcon; update the project icon to the new iconId. Use iconSvg from domStructurePlans when inlining SVG.',
652
+ 'For zIndex: read ompoGlossary.zIndex; map to CSS z-index on the matching selector.',
449
653
  'Use child anchors (tag, id, class, textSnippet, insertKind, iconId) to find elements in source when selectors are unstable.',
450
654
  'Prefer the smallest possible diff for each file.'
451
655
  ],
@@ -459,6 +663,7 @@ export function explainEdit(bundle) {
459
663
  const moveCount = bundle.operations.filter((operation) => operation.kind === 'dom.move').length;
460
664
  const textInsertCount = bundle.operations.filter((operation) => operation.kind === 'dom.insert' && operation.insertKind === 'text').length;
461
665
  const iconInsertCount = bundle.operations.filter((operation) => operation.kind === 'dom.insert' && operation.insertKind === 'icon').length;
666
+ const iconReplaceCount = bundle.operations.filter((operation) => operation.kind === 'dom.iconReplace').length;
462
667
  const domCount = bundle.operations.length - styleCount - textCount;
463
668
  const scopeLabel = bundle.scope.mode === 'subtree' && bundle.scope.rootLabel
464
669
  ? `${bundle.scope.rootLabel} and its children`
@@ -479,7 +684,10 @@ export function explainEdit(bundle) {
479
684
  lines.push(`${textInsertCount} text insert${textInsertCount === 1 ? '' : 's'} — apply via domStructurePlans (markup at parent/index)`);
480
685
  }
481
686
  if (iconInsertCount > 0) {
482
- lines.push(`${iconInsertCount} icon insert${iconInsertCount === 1 ? '' : 's'} — map iconId to project icons when possible`);
687
+ lines.push(`${iconInsertCount} icon insert${iconInsertCount === 1 ? '' : 's'} — apply iconId (and iconSvg when present) via domStructurePlans`);
688
+ }
689
+ if (iconReplaceCount > 0) {
690
+ lines.push(`${iconReplaceCount} icon replace${iconReplaceCount === 1 ? '' : 's'} — update iconId/iconSvg via domStructurePlans`);
483
691
  }
484
692
  lines.push(`Source preview: ${bundle.source.url}`);
485
693
  return lines.join('\n');
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { getOmpoEditsStorePath } from './edits-path.js';
7
7
  import { readOmpoMcpSession } from './session.js';
8
8
  import { readMcpTokenBalance } from './tokens.js';
9
9
  const PACKAGE_NAME = '@ompo-design/mcp-server';
10
- const PACKAGE_VERSION = '0.1.11';
10
+ const PACKAGE_VERSION = '0.1.12';
11
11
  const SERVER_NAME = 'ompo';
12
12
  function resolveExecutable(name) {
13
13
  try {
@@ -187,7 +187,7 @@ async function runDoctor() {
187
187
  console.log('Edits: none yet (click Send in Ompo first)');
188
188
  }
189
189
  console.log('');
190
- console.log('If tools skip token usage, quit and reopen Claude Code to reload MCP v0.1.11+');
190
+ console.log('If tools skip token usage, quit and reopen Claude Code to reload MCP v0.1.12+');
191
191
  console.log('Global install: npx @ompo-design/mcp-server setup-global');
192
192
  }
193
193
  function printProjectNextSteps(projectRoot) {
@@ -64,7 +64,7 @@ export async function consumeMcpTokenForEdit(toolName, editId) {
64
64
  const balance = await readMcpTokenBalance();
65
65
  return balance ?? 0;
66
66
  }
67
- const tokensRemaining = await consumeMcpToken(toolName);
67
+ const tokensRemaining = await consumeMcpToken(toolName, editId);
68
68
  markEditBilled(editId);
69
69
  return tokensRemaining;
70
70
  }
@@ -0,0 +1,11 @@
1
+ export type IconifySearchResult = {
2
+ icons: string[];
3
+ total: number;
4
+ limit: number;
5
+ start: number;
6
+ };
7
+ export declare function searchIconifyIcons(query: string, options?: {
8
+ limit?: number;
9
+ start?: number;
10
+ }): Promise<IconifySearchResult>;
11
+ export declare function fetchIconifySvg(iconId: string): Promise<string>;
@@ -0,0 +1,42 @@
1
+ const ICONIFY_API_BASE = 'https://api.iconify.design';
2
+ function splitIconifyId(iconId) {
3
+ const separator = iconId.indexOf(':');
4
+ if (separator <= 0)
5
+ return ['', ''];
6
+ return [iconId.slice(0, separator), iconId.slice(separator + 1)];
7
+ }
8
+ export async function searchIconifyIcons(query, options) {
9
+ const trimmed = query.trim();
10
+ const limit = Math.min(Math.max(options?.limit ?? 32, 1), 999);
11
+ const start = Math.max(options?.start ?? 0, 0);
12
+ if (!trimmed) {
13
+ return { icons: [], total: 0, limit, start };
14
+ }
15
+ const url = new URL(`${ICONIFY_API_BASE}/search`);
16
+ url.searchParams.set('query', trimmed);
17
+ url.searchParams.set('limit', String(limit));
18
+ url.searchParams.set('start', String(start));
19
+ const response = await fetch(url);
20
+ if (!response.ok) {
21
+ throw new Error(`Iconify search failed with ${response.status}`);
22
+ }
23
+ const payload = (await response.json());
24
+ return {
25
+ icons: payload.icons ?? [],
26
+ total: payload.total ?? payload.icons?.length ?? 0,
27
+ limit: payload.limit ?? limit,
28
+ start: payload.start ?? start
29
+ };
30
+ }
31
+ export async function fetchIconifySvg(iconId) {
32
+ const [prefix, name] = splitIconifyId(iconId);
33
+ if (!prefix || !name) {
34
+ throw new Error(`Invalid Iconify icon id: ${iconId}`);
35
+ }
36
+ const url = `${ICONIFY_API_BASE}/${prefix}/${name}.svg`;
37
+ const response = await fetch(url);
38
+ if (!response.ok) {
39
+ throw new Error(`Iconify SVG fetch failed with ${response.status}`);
40
+ }
41
+ return response.text();
42
+ }
package/dist/index.js CHANGED
@@ -7,10 +7,11 @@ import { runCli } from './cli.js';
7
7
  import { consumeMcpTokenForEdit, requireMcpSessionBalance } from './edit-billing.js';
8
8
  import { editsStoreReady, listEdits, readEditBundle, recordEditApplied, recordEditPull } from './edit-store.js';
9
9
  import { getOmpoEditsStorePath } from './edits-path.js';
10
+ import { fetchIconifySvg, searchIconifyIcons } from './iconify.js';
10
11
  import { McpTokenError } from './tokens.js';
11
12
  const server = new McpServer({
12
13
  name: 'ompo-mcp-server',
13
- version: '0.1.11'
14
+ version: '0.1.13'
14
15
  });
15
16
  function requireEditsStore() {
16
17
  const storePath = getOmpoEditsStorePath();
@@ -129,6 +130,44 @@ server.tool('apply_edit', 'Build an apply plan for an Ompo edit. The agent shoul
129
130
  return tokenErrorResult(error);
130
131
  }
131
132
  });
133
+ server.tool('search_icons', 'Search Iconify icons by keyword (same catalog Ompo uses in the editor)', {
134
+ query: z.string().describe('Search terms, e.g. "home", "menu", "arrow right"'),
135
+ limit: z.number().int().min(1).max(64).optional().describe('Max results (default 32)'),
136
+ start: z.number().int().min(0).optional().describe('Pagination offset')
137
+ }, async ({ query, limit, start }) => {
138
+ try {
139
+ const result = await searchIconifyIcons(query, { limit, start });
140
+ return {
141
+ content: [
142
+ {
143
+ type: 'text',
144
+ text: JSON.stringify(result, null, 2)
145
+ }
146
+ ]
147
+ };
148
+ }
149
+ catch (error) {
150
+ return tokenErrorResult(error);
151
+ }
152
+ });
153
+ server.tool('fetch_icon', 'Fetch Iconify SVG markup for an icon id (e.g. lucide:home)', {
154
+ iconId: z.string().describe('Iconify id in prefix:name format, e.g. lucide:home')
155
+ }, async ({ iconId }) => {
156
+ try {
157
+ const iconSvg = await fetchIconifySvg(iconId);
158
+ return {
159
+ content: [
160
+ {
161
+ type: 'text',
162
+ text: JSON.stringify({ iconId, iconSvg }, null, 2)
163
+ }
164
+ ]
165
+ };
166
+ }
167
+ catch (error) {
168
+ return tokenErrorResult(error);
169
+ }
170
+ });
132
171
  async function main() {
133
172
  if (runCli(process.argv.slice(2)))
134
173
  return;
package/dist/tokens.d.ts CHANGED
@@ -3,4 +3,4 @@ export declare class McpTokenError extends Error {
3
3
  constructor(code: McpTokenError['code'], message: string);
4
4
  }
5
5
  export declare function readMcpTokenBalance(): Promise<number | null>;
6
- export declare function consumeMcpToken(toolName: string): Promise<number>;
6
+ export declare function consumeMcpToken(toolName: string, editId?: string): Promise<number>;
package/dist/tokens.js CHANGED
@@ -16,6 +16,9 @@ function mapRpcError(message) {
16
16
  if (normalized.includes('INSUFFICIENT_TOKENS')) {
17
17
  return new McpTokenError('INSUFFICIENT_TOKENS', 'You are out of MCP tokens. Add more tokens to your account before using Ompo MCP tools.');
18
18
  }
19
+ if (normalized.includes('COULD NOT CHOOSE THE BEST CANDIDATE FUNCTION')) {
20
+ return new McpTokenError('TOKEN_SERVICE_UNAVAILABLE', 'MCP token service is misconfigured on the server. Update Ompo or contact support.');
21
+ }
19
22
  return new McpTokenError('TOKEN_SERVICE_UNAVAILABLE', 'Could not verify MCP token balance. Check your connection and try again.');
20
23
  }
21
24
  async function createAuthedSupabase(session) {
@@ -70,14 +73,15 @@ export async function readMcpTokenBalance() {
70
73
  throw mapRpcError(error.message);
71
74
  return data?.tokens_available ?? null;
72
75
  }
73
- export async function consumeMcpToken(toolName) {
76
+ export async function consumeMcpToken(toolName, editId) {
74
77
  const session = readOmpoMcpSession();
75
78
  if (!session) {
76
79
  throw new McpTokenError('NOT_SIGNED_IN', 'Sign in to Ompo and keep the app open so your MCP session stays active.');
77
80
  }
78
81
  const supabase = await createAuthedSupabase(session);
79
82
  const { data, error } = await supabase.rpc('consume_mcp_token', {
80
- p_tool_name: toolName
83
+ p_tool_name: toolName,
84
+ p_edit_id: editId ?? null
81
85
  });
82
86
  if (error) {
83
87
  throw mapRpcError(error.message);
package/dist/types.d.ts CHANGED
@@ -15,8 +15,9 @@ export type DomInsertOperation = {
15
15
  index: number;
16
16
  html: string;
17
17
  selector?: string;
18
- insertKind?: 'text' | 'icon';
18
+ insertKind?: 'text' | 'icon' | 'frame' | 'section';
19
19
  iconId?: string;
20
+ iconSvg?: string;
20
21
  insertedElement?: ElementAnchor;
21
22
  parent?: ElementAnchor;
22
23
  };
@@ -38,13 +39,21 @@ export type DomDeleteOperation = {
38
39
  kind: 'dom.delete';
39
40
  selector: string;
40
41
  };
42
+ export type DomIconReplaceOperation = {
43
+ kind: 'dom.iconReplace';
44
+ selector: string;
45
+ iconId: string;
46
+ iconSvg?: string;
47
+ previousIconId?: string;
48
+ element?: ElementAnchor;
49
+ };
41
50
  export type ElementAnchor = {
42
51
  selector: string;
43
52
  tagName: string;
44
53
  id?: string;
45
54
  className?: string;
46
55
  textSnippet?: string;
47
- insertKind?: 'text' | 'icon';
56
+ insertKind?: 'text' | 'icon' | 'frame' | 'section';
48
57
  iconId?: string;
49
58
  };
50
59
  export type DomFlexWrapOperation = {
@@ -62,7 +71,7 @@ export type TextOperation = {
62
71
  textContent?: string;
63
72
  innerHTML?: string;
64
73
  };
65
- export type OmpoOperation = StyleOperation | DomInsertOperation | DomMoveOperation | DomDeleteOperation | DomFlexWrapOperation | TextOperation;
74
+ export type OmpoOperation = StyleOperation | DomInsertOperation | DomMoveOperation | DomDeleteOperation | DomIconReplaceOperation | DomFlexWrapOperation | TextOperation;
66
75
  export type OmpoEditBundle = {
67
76
  schemaVersion: number;
68
77
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ompo-design/mcp-server",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "MCP server for applying Ompo visual edits to a codebase",
5
5
  "type": "module",
6
6
  "license": "MIT",