@ompo-design/mcp-server 0.1.12 → 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.
@@ -1,13 +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
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.`,
10
- 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.`
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.`
11
17
  };
12
18
  function describeAnchor(anchor) {
13
19
  const parts = [anchor.tagName];
@@ -29,6 +35,8 @@ function buildDomInsertPlan(operation) {
29
35
  const inserted = operation.insertedElement;
30
36
  const isIcon = operation.insertKind === 'icon';
31
37
  const isText = operation.insertKind === 'text';
38
+ const isSection = operation.insertKind === 'section';
39
+ const isFrame = operation.insertKind === 'frame';
32
40
  const steps = [];
33
41
  if (operation.parent) {
34
42
  steps.push(`Locate destination parent: ${describeAnchor(operation.parent)}.`);
@@ -45,11 +53,20 @@ function buildDomInsertPlan(operation) {
45
53
  if (isText) {
46
54
  steps.push(OMPO_GLOSSARY.insertText);
47
55
  }
56
+ else if (isSection) {
57
+ steps.push(OMPO_GLOSSARY.insertSection);
58
+ }
59
+ else if (isFrame) {
60
+ steps.push(OMPO_GLOSSARY.insertFrame);
61
+ }
48
62
  else if (isIcon) {
49
63
  steps.push(OMPO_GLOSSARY.insertIcon);
50
64
  if (operation.iconId) {
51
65
  steps.push(`Iconify id: ${operation.iconId}`);
52
66
  }
67
+ if (operation.iconSvg) {
68
+ steps.push('iconSvg is included in the plan payload for inline SVG or reference.');
69
+ }
53
70
  }
54
71
  steps.push(`Insert at child index ${operation.index}.`);
55
72
  steps.push('Sanitize and adapt the HTML to the project’s component conventions before writing.');
@@ -57,7 +74,11 @@ function buildDomInsertPlan(operation) {
57
74
  ? `Insert Iconify icon${operation.iconId ? ` (${operation.iconId})` : ''}`
58
75
  : isText
59
76
  ? 'Insert text block'
60
- : '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';
61
82
  return {
62
83
  kind: 'dom.insert',
63
84
  summary,
@@ -69,6 +90,7 @@ function buildDomInsertPlan(operation) {
69
90
  selector: operation.selector,
70
91
  insertKind: operation.insertKind,
71
92
  iconId: operation.iconId,
93
+ iconSvg: operation.iconSvg,
72
94
  insertedElement: operation.insertedElement,
73
95
  parent: operation.parent
74
96
  }
@@ -84,6 +106,9 @@ function buildIconReplacePlan(operation) {
84
106
  }
85
107
  steps.push(OMPO_GLOSSARY.replaceIcon);
86
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
+ }
87
112
  if (operation.previousIconId) {
88
113
  steps.push(`Previous icon id: ${operation.previousIconId}`);
89
114
  }
@@ -94,6 +119,7 @@ function buildIconReplacePlan(operation) {
94
119
  payload: {
95
120
  selector: operation.selector,
96
121
  iconId: operation.iconId,
122
+ iconSvg: operation.iconSvg,
97
123
  previousIconId: operation.previousIconId,
98
124
  element: operation.element
99
125
  }
@@ -188,10 +214,120 @@ function formatChangedValue(property, value) {
188
214
  }
189
215
  return '(empty / remove)';
190
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
+ }
191
239
  function styleSuggestionForProperty(property, value, changed) {
192
240
  const parentFlex = changed.ensureParentFlex;
193
241
  const parentFlexLabel = parentFlex === 'row' || parentFlex === 'column' ? parentFlex : 'row or column';
242
+ const layoutMode = layoutModeFromChanged(changed);
194
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
+ };
195
331
  case 'gap':
196
332
  return {
197
333
  property,
@@ -281,7 +417,7 @@ function styleSuggestionForProperty(property, value, changed) {
281
417
  property,
282
418
  value: formatChangedValue(property, value),
283
419
  strategy: 'component-markup',
284
- 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))
285
421
  };
286
422
  case 'backgroundImage':
287
423
  if (changed.backgroundImageSource) {
@@ -289,7 +425,7 @@ function styleSuggestionForProperty(property, value, changed) {
289
425
  property,
290
426
  value: formatChangedValue(property, value),
291
427
  strategy: 'css-rule',
292
- notes: `${OMPO_GLOSSARY.imageFill} Apply after copying the file from backgroundImageSource.`
428
+ notes: imageFillSourceNotes(String(changed.backgroundImageSource))
293
429
  };
294
430
  }
295
431
  break;
@@ -313,6 +449,13 @@ function styleSuggestionForProperty(property, value, changed) {
313
449
  strategy: 'inline-style',
314
450
  notes: OMPO_GLOSSARY.iconStroke
315
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
+ };
316
459
  }
317
460
  return {
318
461
  property,
@@ -327,6 +470,30 @@ function styleSuggestions(operation) {
327
470
  }
328
471
  function summarizeLayoutIntent(changed) {
329
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
+ }
330
497
  if (changed.gap !== undefined) {
331
498
  parts.push(`Set container gap to ${changed.gap}`);
332
499
  }
@@ -339,6 +506,9 @@ function summarizeLayoutIntent(changed) {
339
506
  if (changed.ensureParentFlex) {
340
507
  parts.push(`Parent needs display:flex with flex-direction:${changed.ensureParentFlex}`);
341
508
  }
509
+ if (changed.zIndex !== undefined) {
510
+ parts.push(`Set z-index to ${changed.zIndex}`);
511
+ }
342
512
  return parts.length > 0 ? parts.join('. ') : null;
343
513
  }
344
514
  function buildDomStructurePlan(operation) {
@@ -351,7 +521,8 @@ function buildDomStructurePlan(operation) {
351
521
  `Locate the parent element matching "${operation.parentSelector}" in source.`,
352
522
  `Insert a new <div> wrapper at child index ${operation.index} inside that parent.`,
353
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.`,
354
- `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.',
355
526
  'Do not change unrelated siblings or parent styling.'
356
527
  ],
357
528
  payload: {
@@ -466,15 +637,19 @@ export function buildApplyPlan(bundle) {
466
637
  instructions: [
467
638
  'Only apply properties and operations listed in this plan.',
468
639
  'Leave all untouched styling and markup unchanged.',
469
- 'Read ompoGlossary.fill and ompoGlossary.gap before applying layout changes — Ompo terms are not literal CSS properties.',
470
- '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.',
471
645
  'Gap belongs on the flex/grid parent. Fill belongs on the child inside a flex parent.',
472
646
  'DOM moves are in domStructurePlans and domChanges — reorder or reparent elements in source markup/components, not just CSS.',
473
647
  'Read ompoGlossary.domMove before applying dom.move operations.',
474
648
  'For dom.move: destinationChildren is the structural snapshot of the final sibling order — match it in JSX/HTML or reorder mapped arrays.',
475
649
  'For dom.flexWrap: create a new wrapper, move matched children, then apply wrapperStyles.',
476
650
  'For dom.insert with insertKind text or icon: read ompoGlossary.insertText / insertIcon; use insertedElement anchors and iconId when applying.',
477
- 'For dom.iconReplace: read ompoGlossary.replaceIcon; update the project icon to the new iconId.',
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.',
478
653
  'Use child anchors (tag, id, class, textSnippet, insertKind, iconId) to find elements in source when selectors are unstable.',
479
654
  'Prefer the smallest possible diff for each file.'
480
655
  ],
@@ -509,10 +684,10 @@ export function explainEdit(bundle) {
509
684
  lines.push(`${textInsertCount} text insert${textInsertCount === 1 ? '' : 's'} — apply via domStructurePlans (markup at parent/index)`);
510
685
  }
511
686
  if (iconInsertCount > 0) {
512
- 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`);
513
688
  }
514
689
  if (iconReplaceCount > 0) {
515
- lines.push(`${iconReplaceCount} icon replace${iconReplaceCount === 1 ? '' : 's'} — update iconId via domStructurePlans`);
690
+ lines.push(`${iconReplaceCount} icon replace${iconReplaceCount === 1 ? '' : 's'} — update iconId/iconSvg via domStructurePlans`);
516
691
  }
517
692
  lines.push(`Source preview: ${bundle.source.url}`);
518
693
  return lines.join('\n');
@@ -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.12'
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
  };
@@ -42,6 +43,7 @@ export type DomIconReplaceOperation = {
42
43
  kind: 'dom.iconReplace';
43
44
  selector: string;
44
45
  iconId: string;
46
+ iconSvg?: string;
45
47
  previousIconId?: string;
46
48
  element?: ElementAnchor;
47
49
  };
@@ -51,7 +53,7 @@ export type ElementAnchor = {
51
53
  id?: string;
52
54
  className?: string;
53
55
  textSnippet?: string;
54
- insertKind?: 'text' | 'icon';
56
+ insertKind?: 'text' | 'icon' | 'frame' | 'section';
55
57
  iconId?: string;
56
58
  };
57
59
  export type DomFlexWrapOperation = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ompo-design/mcp-server",
3
- "version": "0.1.12",
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",