@ompo-design/mcp-server 0.1.7 → 0.1.10

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.
@@ -2,8 +2,99 @@ const OMPO_GLOSSARY = {
2
2
  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
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.`,
4
4
  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.`
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.`,
6
+ 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.`
6
7
  };
8
+ function describeAnchor(anchor) {
9
+ const parts = [anchor.tagName];
10
+ if (anchor.id)
11
+ parts.push(`#${anchor.id}`);
12
+ if (anchor.className) {
13
+ const classes = anchor.className.split(/\s+/).filter(Boolean).slice(0, 3).join('.');
14
+ if (classes)
15
+ parts.push(`.${classes}`);
16
+ }
17
+ if (anchor.textSnippet)
18
+ parts.push(`"${anchor.textSnippet}"`);
19
+ return parts.join(' ');
20
+ }
21
+ function formatChildOrder(children, movedSelector) {
22
+ return children.map((child, index) => {
23
+ const marker = child.selector === movedSelector ? ' ← moved element' : '';
24
+ return `${index + 1}. ${describeAnchor(child)}${marker}`;
25
+ });
26
+ }
27
+ function buildDomMovePlan(operation) {
28
+ const reordered = operation.fromParentSelector === operation.toParentSelector;
29
+ const moved = operation.movedElement;
30
+ const steps = [];
31
+ if (moved) {
32
+ steps.push(`Find the moved element in source: ${describeAnchor(moved)}.`);
33
+ }
34
+ else {
35
+ steps.push(`Locate element "${operation.selector}" in source using tag, id, class, or visible text.`);
36
+ }
37
+ if (operation.toParent) {
38
+ steps.push(`Destination parent: ${describeAnchor(operation.toParent)}.`);
39
+ }
40
+ else {
41
+ steps.push(`Destination parent selector: "${operation.toParentSelector}".`);
42
+ }
43
+ if (operation.insertBefore) {
44
+ steps.push(`Place the moved element immediately before: ${describeAnchor(operation.insertBefore)}.`);
45
+ }
46
+ else if (operation.destinationChildren && operation.destinationChildren.length > 0) {
47
+ steps.push('Place the moved element as the last child of the destination parent.');
48
+ }
49
+ if (operation.destinationChildren && operation.destinationChildren.length > 0) {
50
+ steps.push('Match this sibling order in source (structural snapshot after the move):');
51
+ steps.push(...formatChildOrder(operation.destinationChildren, operation.selector));
52
+ }
53
+ else {
54
+ steps.push(reordered
55
+ ? `Reorder inside the parent from index ${operation.fromIndex} to index ${operation.index}.`
56
+ : `Move from "${operation.fromParentSelector}" (index ${operation.fromIndex}) to "${operation.toParentSelector}" at index ${operation.index}.`);
57
+ }
58
+ if (operation.fromChildrenBefore && operation.fromChildrenBefore.length > 0) {
59
+ steps.push('Previous sibling order before the move:');
60
+ steps.push(...formatChildOrder(operation.fromChildrenBefore, operation.selector));
61
+ }
62
+ steps.push('Update JSX/HTML/component structure — this is a markup or children-order change, not CSS.', 'If children come from a mapped array (React .map, v-for, etc.), reorder that array or its data source.', 'Preserve element content and styling that Ompo did not change.');
63
+ const payload = {
64
+ selector: operation.selector,
65
+ fromParentSelector: operation.fromParentSelector,
66
+ fromIndex: operation.fromIndex,
67
+ toParentSelector: operation.toParentSelector,
68
+ insertIndex: operation.index,
69
+ reordered
70
+ };
71
+ if (moved)
72
+ payload.movedElement = moved;
73
+ if (operation.fromParent)
74
+ payload.fromParent = operation.fromParent;
75
+ if (operation.toParent)
76
+ payload.toParent = operation.toParent;
77
+ if (operation.insertBefore)
78
+ payload.insertBefore = operation.insertBefore;
79
+ if (operation.destinationChildren) {
80
+ payload.destinationChildren = operation.destinationChildren;
81
+ }
82
+ if (operation.fromChildrenBefore) {
83
+ payload.fromChildrenBefore = operation.fromChildrenBefore;
84
+ }
85
+ return {
86
+ kind: 'dom.move',
87
+ summary: moved
88
+ ? reordered
89
+ ? `Reorder ${describeAnchor(moved)} within its parent`
90
+ : `Move ${describeAnchor(moved)} to ${operation.toParent ? describeAnchor(operation.toParent) : operation.toParentSelector}`
91
+ : reordered
92
+ ? `Reorder element within parent "${operation.toParentSelector}"`
93
+ : `Move element to parent "${operation.toParentSelector}"`,
94
+ steps,
95
+ payload
96
+ };
97
+ }
7
98
  function formatChangedValue(property, value) {
8
99
  const text = String(value);
9
100
  if (text !== '')
@@ -199,32 +290,8 @@ function buildDomStructurePlan(operation) {
199
290
  selector: operation.selector
200
291
  }
201
292
  };
202
- case 'dom.move': {
203
- const reordered = operation.fromParentSelector === operation.toParentSelector;
204
- return {
205
- kind: 'dom.move',
206
- summary: reordered
207
- ? `Reorder element within parent "${operation.toParentSelector}"`
208
- : `Move element to a new parent "${operation.toParentSelector}"`,
209
- steps: [
210
- `Locate element "${operation.selector}" in source using selector, tag, id, class, or nearby text.`,
211
- reordered
212
- ? `Reorder it inside "${operation.toParentSelector}" from child index ${operation.fromIndex} to index ${operation.index}.`
213
- : `Move it from "${operation.fromParentSelector}" (index ${operation.fromIndex}) to "${operation.toParentSelector}" at index ${operation.index}.`,
214
- 'Update JSX/HTML/component structure in source — this is a markup change, not a CSS tweak.',
215
- 'Preserve the element’s content and non-Ompo styling.',
216
- 'If the project uses mapped lists or arrays, update item order in data when that drives rendering.'
217
- ],
218
- payload: {
219
- selector: operation.selector,
220
- fromParentSelector: operation.fromParentSelector,
221
- fromIndex: operation.fromIndex,
222
- toParentSelector: operation.toParentSelector,
223
- insertIndex: operation.index,
224
- reordered
225
- }
226
- };
227
- }
293
+ case 'dom.move':
294
+ return buildDomMovePlan(operation);
228
295
  case 'dom.delete':
229
296
  return {
230
297
  kind: 'dom.delete',
@@ -295,6 +362,14 @@ export function buildApplyPlan(bundle) {
295
362
  if (domPlan)
296
363
  domStructurePlans.push(domPlan);
297
364
  domChanges.push(operation);
365
+ if (operation.kind === 'dom.move') {
366
+ if (!operation.movedElement && operation.selector.includes('nth-of-type')) {
367
+ warnings.push(`DOM move "${operation.selector}" uses nth-of-type selectors. Re-export from Ompo after rearranging to get richer anchors, or match by visible text and parent structure.`);
368
+ }
369
+ else if (operation.movedElement && !operation.movedElement.id && !operation.movedElement.textSnippet) {
370
+ warnings.push(`DOM move target "${describeAnchor(operation.movedElement)}" has no id or text. Use destinationChildren order and parent context to locate it in source.`);
371
+ }
372
+ }
298
373
  if (operation.kind === 'dom.flexWrap') {
299
374
  for (const child of operation.children) {
300
375
  if (!child.id && !child.className && child.selector.includes('nth-of-type')) {
@@ -318,6 +393,8 @@ export function buildApplyPlan(bundle) {
318
393
  '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.',
319
394
  'Gap belongs on the flex/grid parent. Fill belongs on the child inside a flex parent.',
320
395
  'DOM moves are in domStructurePlans and domChanges — reorder or reparent elements in source markup/components, not just CSS.',
396
+ 'Read ompoGlossary.domMove before applying dom.move operations.',
397
+ 'For dom.move: destinationChildren is the structural snapshot of the final sibling order — match it in JSX/HTML or reorder mapped arrays.',
321
398
  'For dom.flexWrap: create a new wrapper, move matched children, then apply wrapperStyles.',
322
399
  'Use child anchors (tag, id, class, textSnippet) to find elements in source when selectors are unstable.',
323
400
  'Prefer the smallest possible diff for each file.'
@@ -329,6 +406,7 @@ export function explainEdit(bundle) {
329
406
  const styleCount = bundle.operations.filter((operation) => operation.kind === 'style').length;
330
407
  const textCount = bundle.operations.filter((operation) => operation.kind === 'text').length;
331
408
  const flexWrapCount = bundle.operations.filter((operation) => operation.kind === 'dom.flexWrap').length;
409
+ const moveCount = bundle.operations.filter((operation) => operation.kind === 'dom.move').length;
332
410
  const domCount = bundle.operations.length - styleCount - textCount;
333
411
  const scopeLabel = bundle.scope.mode === 'subtree' && bundle.scope.rootLabel
334
412
  ? `${bundle.scope.rootLabel} and its children`
@@ -342,6 +420,9 @@ export function explainEdit(bundle) {
342
420
  if (flexWrapCount > 0) {
343
421
  lines.push(`${flexWrapCount} flex-wrap operation${flexWrapCount === 1 ? '' : 's'}`);
344
422
  }
423
+ if (moveCount > 0) {
424
+ lines.push(`${moveCount} DOM move${moveCount === 1 ? '' : 's'} — apply via domStructurePlans (markup/children order, not CSS)`);
425
+ }
345
426
  lines.push(`Source preview: ${bundle.source.url}`);
346
427
  return lines.join('\n');
347
428
  }
package/dist/cli.js CHANGED
@@ -4,7 +4,10 @@ import { homedir } from 'os';
4
4
  import { join, resolve } from 'path';
5
5
  import { editsStoreReady } from './edit-store.js';
6
6
  import { getOmpoEditsStorePath } from './edits-path.js';
7
+ import { readOmpoMcpSession } from './session.js';
8
+ import { readMcpTokenBalance } from './tokens.js';
7
9
  const PACKAGE_NAME = '@ompo-design/mcp-server';
10
+ const PACKAGE_VERSION = '0.1.10';
8
11
  const SERVER_NAME = 'ompo';
9
12
  function resolveExecutable(name) {
10
13
  try {
@@ -85,7 +88,7 @@ export function runCli(argv) {
85
88
  return true;
86
89
  }
87
90
  if (command === 'doctor') {
88
- runDoctor();
91
+ void runDoctor();
89
92
  return true;
90
93
  }
91
94
  if (command === 'help' || command === '--help' || command === '-h') {
@@ -123,7 +126,7 @@ function runGlobalSetup() {
123
126
  console.log(' 5. In Ompo, click Send on an edit before using the tools');
124
127
  console.log(` Edits are stored in ${getOmpoEditsStorePath()}`);
125
128
  }
126
- function runDoctor() {
129
+ async function runDoctor() {
127
130
  const projectRoot = resolve(process.cwd());
128
131
  const configPath = join(projectRoot, '.mcp.json');
129
132
  const desktopPath = claudeDesktopConfigPath();
@@ -131,6 +134,7 @@ function runDoctor() {
131
134
  const storePath = getOmpoEditsStorePath();
132
135
  console.log('Ompo MCP doctor');
133
136
  console.log('');
137
+ console.log(`Package version: ${PACKAGE_VERSION}`);
134
138
  console.log(`Current folder: ${projectRoot}`);
135
139
  console.log(`Node: ${process.version}`);
136
140
  console.log(`npx: ${resolveExecutable('npx')}`);
@@ -156,6 +160,26 @@ function runDoctor() {
156
160
  }
157
161
  }
158
162
  console.log('');
163
+ const session = readOmpoMcpSession();
164
+ if (!session) {
165
+ console.log('Session: missing (open Ompo, sign in, and keep the app running)');
166
+ }
167
+ else {
168
+ console.log(`Session: ready for user ${session.userId}`);
169
+ console.log(`Session updated: ${session.updatedAt}`);
170
+ if (!session.refreshToken) {
171
+ console.log('Session: missing refresh token (restart Ompo after updating)');
172
+ }
173
+ try {
174
+ const balance = await readMcpTokenBalance();
175
+ console.log(`Tokens: ${balance ?? 'unknown'} available`);
176
+ }
177
+ catch (error) {
178
+ const message = error instanceof Error ? error.message : 'unknown error';
179
+ console.log(`Tokens: could not read balance (${message})`);
180
+ }
181
+ }
182
+ console.log('');
159
183
  if (editsStoreReady()) {
160
184
  console.log('Edits: ready (Send from Ompo to add more)');
161
185
  }
@@ -163,6 +187,7 @@ function runDoctor() {
163
187
  console.log('Edits: none yet (click Send in Ompo first)');
164
188
  }
165
189
  console.log('');
190
+ console.log('If tools skip token usage, quit and reopen Claude Code to reload MCP v0.1.10+');
166
191
  console.log('Global install: npx @ompo-design/mcp-server setup-global');
167
192
  }
168
193
  function printProjectNextSteps(projectRoot) {
@@ -0,0 +1,2 @@
1
+ export declare function consumeMcpTokenForEdit(toolName: string, editId: string): Promise<number>;
2
+ export declare function requireMcpSessionBalance(): Promise<number>;
@@ -0,0 +1,75 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { dirname, join } from 'path';
4
+ import { consumeMcpToken, McpTokenError, readMcpTokenBalance } from './tokens.js';
5
+ import { readOmpoMcpSession } from './session.js';
6
+ function requireSignedIn() {
7
+ if (!readOmpoMcpSession()) {
8
+ throw new McpTokenError('NOT_SIGNED_IN', 'Sign in to Ompo and keep the app open so your MCP session stays active.');
9
+ }
10
+ }
11
+ const BILLING_TTL_MS = 24 * 60 * 60 * 1000;
12
+ function getEditBillingPath() {
13
+ return join(homedir(), '.ompo', 'edit-billing.json');
14
+ }
15
+ function readBillingState() {
16
+ const billingPath = getEditBillingPath();
17
+ if (!existsSync(billingPath)) {
18
+ return { billedEdits: {} };
19
+ }
20
+ try {
21
+ const parsed = JSON.parse(readFileSync(billingPath, 'utf8'));
22
+ return {
23
+ billedEdits: typeof parsed.billedEdits === 'object' && parsed.billedEdits !== null
24
+ ? parsed.billedEdits
25
+ : {}
26
+ };
27
+ }
28
+ catch {
29
+ return { billedEdits: {} };
30
+ }
31
+ }
32
+ function writeBillingState(state) {
33
+ const billingPath = getEditBillingPath();
34
+ mkdirSync(dirname(billingPath), { recursive: true });
35
+ writeFileSync(billingPath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
36
+ }
37
+ function pruneExpiredBilledEdits(billedEdits) {
38
+ const now = Date.now();
39
+ const pruned = {};
40
+ for (const [editId, billedAt] of Object.entries(billedEdits)) {
41
+ const billedTime = Date.parse(billedAt);
42
+ if (Number.isNaN(billedTime))
43
+ continue;
44
+ if (now - billedTime <= BILLING_TTL_MS) {
45
+ pruned[editId] = billedAt;
46
+ }
47
+ }
48
+ return pruned;
49
+ }
50
+ function isEditAlreadyBilled(editId) {
51
+ const state = readBillingState();
52
+ const billedEdits = pruneExpiredBilledEdits(state.billedEdits);
53
+ return editId in billedEdits;
54
+ }
55
+ function markEditBilled(editId) {
56
+ const state = readBillingState();
57
+ const billedEdits = pruneExpiredBilledEdits(state.billedEdits);
58
+ billedEdits[editId] = new Date().toISOString();
59
+ writeBillingState({ billedEdits });
60
+ }
61
+ export async function consumeMcpTokenForEdit(toolName, editId) {
62
+ requireSignedIn();
63
+ if (isEditAlreadyBilled(editId)) {
64
+ const balance = await readMcpTokenBalance();
65
+ return balance ?? 0;
66
+ }
67
+ const tokensRemaining = await consumeMcpToken(toolName);
68
+ markEditBilled(editId);
69
+ return tokensRemaining;
70
+ }
71
+ export async function requireMcpSessionBalance() {
72
+ requireSignedIn();
73
+ const balance = await readMcpTokenBalance();
74
+ return balance ?? 0;
75
+ }
package/dist/index.js CHANGED
@@ -4,12 +4,13 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
4
  import { z } from 'zod';
5
5
  import { buildApplyPlan, explainEdit } from './apply-plan.js';
6
6
  import { runCli } from './cli.js';
7
+ import { consumeMcpTokenForEdit, requireMcpSessionBalance } from './edit-billing.js';
7
8
  import { editsStoreReady, listEdits, readEditBundle, recordEditApplied, recordEditPull } from './edit-store.js';
8
9
  import { getOmpoEditsStorePath } from './edits-path.js';
9
- import { consumeMcpToken, McpTokenError } from './tokens.js';
10
+ import { McpTokenError } from './tokens.js';
10
11
  const server = new McpServer({
11
12
  name: 'ompo-mcp-server',
12
- version: '0.1.7'
13
+ version: '0.1.10'
13
14
  });
14
15
  function requireEditsStore() {
15
16
  const storePath = getOmpoEditsStorePath();
@@ -18,8 +19,8 @@ function requireEditsStore() {
18
19
  }
19
20
  return storePath;
20
21
  }
21
- async function withMcpTokenUse(toolName, handler) {
22
- const tokensRemaining = await consumeMcpToken(toolName);
22
+ async function withMcpTokenUseForEdit(toolName, editId, handler) {
23
+ const tokensRemaining = await consumeMcpTokenForEdit(toolName, editId);
23
24
  return handler(tokensRemaining);
24
25
  }
25
26
  function tokenErrorResult(error) {
@@ -38,18 +39,17 @@ function tokenErrorResult(error) {
38
39
  }
39
40
  server.tool('list_edits', 'List Ompo edit bundles saved on this machine', {}, async () => {
40
41
  try {
41
- return await withMcpTokenUse('list_edits', async (tokensRemaining) => {
42
- const storePath = requireEditsStore();
43
- const edits = listEdits();
44
- return {
45
- content: [
46
- {
47
- type: 'text',
48
- text: JSON.stringify({ storePath, edits, tokensRemaining }, null, 2)
49
- }
50
- ]
51
- };
52
- });
42
+ const storePath = requireEditsStore();
43
+ const edits = listEdits();
44
+ const tokensRemaining = await requireMcpSessionBalance();
45
+ return {
46
+ content: [
47
+ {
48
+ type: 'text',
49
+ text: JSON.stringify({ storePath, edits, tokensRemaining }, null, 2)
50
+ }
51
+ ]
52
+ };
53
53
  }
54
54
  catch (error) {
55
55
  return tokenErrorResult(error);
@@ -59,7 +59,7 @@ server.tool('get_edit', 'Load a specific Ompo edit bundle by id', {
59
59
  id: z.string().describe('Edit id, e.g. ed_8K42P')
60
60
  }, async ({ id }) => {
61
61
  try {
62
- return await withMcpTokenUse('get_edit', async (tokensRemaining) => {
62
+ return await withMcpTokenUseForEdit('get_edit', id, async (tokensRemaining) => {
63
63
  requireEditsStore();
64
64
  const bundle = readEditBundle(id);
65
65
  recordEditPull(id);
@@ -81,7 +81,7 @@ server.tool('explain_edit', 'Summarize what an Ompo edit changes', {
81
81
  id: z.string().describe('Edit id, e.g. ed_8K42P')
82
82
  }, async ({ id }) => {
83
83
  try {
84
- return await withMcpTokenUse('explain_edit', async (tokensRemaining) => {
84
+ return await withMcpTokenUseForEdit('explain_edit', id, async (tokensRemaining) => {
85
85
  requireEditsStore();
86
86
  const bundle = readEditBundle(id);
87
87
  recordEditPull(id);
@@ -107,7 +107,7 @@ server.tool('apply_edit', 'Build an apply plan for an Ompo edit. The agent shoul
107
107
  .describe('Set true after the agent successfully applies the edit')
108
108
  }, async ({ id, markApplied }) => {
109
109
  try {
110
- return await withMcpTokenUse('apply_edit', async (tokensRemaining) => {
110
+ return await withMcpTokenUseForEdit('apply_edit', id, async (tokensRemaining) => {
111
111
  requireEditsStore();
112
112
  const bundle = readEditBundle(id);
113
113
  recordEditPull(id);
package/dist/session.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export type OmpoMcpSession = {
2
2
  userId: string;
3
3
  accessToken: string;
4
+ refreshToken?: string;
4
5
  supabaseUrl: string;
5
6
  supabaseAnonKey: string;
6
7
  updatedAt: string;
7
8
  };
8
9
  export declare function getOmpoMcpSessionPath(): string;
9
10
  export declare function readOmpoMcpSession(): OmpoMcpSession | null;
11
+ export declare function writeOmpoMcpSession(session: OmpoMcpSession): void;
package/dist/session.js CHANGED
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync } from 'fs';
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
- import { join } from 'path';
3
+ import { dirname, join } from 'path';
4
4
  export function getOmpoMcpSessionPath() {
5
5
  return join(homedir(), '.ompo', 'session.json');
6
6
  }
@@ -19,6 +19,7 @@ export function readOmpoMcpSession() {
19
19
  return {
20
20
  userId: parsed.userId,
21
21
  accessToken: parsed.accessToken,
22
+ refreshToken: typeof parsed.refreshToken === 'string' ? parsed.refreshToken : undefined,
22
23
  supabaseUrl: parsed.supabaseUrl,
23
24
  supabaseAnonKey: parsed.supabaseAnonKey,
24
25
  updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date(0).toISOString()
@@ -28,3 +29,8 @@ export function readOmpoMcpSession() {
28
29
  return null;
29
30
  }
30
31
  }
32
+ export function writeOmpoMcpSession(session) {
33
+ const sessionPath = getOmpoMcpSessionPath();
34
+ mkdirSync(dirname(sessionPath), { recursive: true });
35
+ writeFileSync(sessionPath, `${JSON.stringify({ ...session, updatedAt: new Date().toISOString() }, null, 2)}\n`, 'utf8');
36
+ }
package/dist/tokens.d.ts CHANGED
@@ -2,4 +2,5 @@ export declare class McpTokenError extends Error {
2
2
  code: 'NOT_SIGNED_IN' | 'INSUFFICIENT_TOKENS' | 'TOKEN_SERVICE_UNAVAILABLE';
3
3
  constructor(code: McpTokenError['code'], message: string);
4
4
  }
5
+ export declare function readMcpTokenBalance(): Promise<number | null>;
5
6
  export declare function consumeMcpToken(toolName: string): Promise<number>;
package/dist/tokens.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createClient } from '@supabase/supabase-js';
2
- import { readOmpoMcpSession } from './session.js';
2
+ import { readOmpoMcpSession, writeOmpoMcpSession } from './session.js';
3
3
  export class McpTokenError extends Error {
4
4
  code;
5
5
  constructor(code, message) {
@@ -18,12 +18,32 @@ function mapRpcError(message) {
18
18
  }
19
19
  return new McpTokenError('TOKEN_SERVICE_UNAVAILABLE', 'Could not verify MCP token balance. Check your connection and try again.');
20
20
  }
21
- export async function consumeMcpToken(toolName) {
22
- const session = readOmpoMcpSession();
23
- if (!session) {
24
- throw new McpTokenError('NOT_SIGNED_IN', 'Sign in to Ompo and keep the app open so your MCP session stays active.');
25
- }
21
+ async function createAuthedSupabase(session) {
26
22
  const supabase = createClient(session.supabaseUrl, session.supabaseAnonKey, {
23
+ auth: {
24
+ persistSession: false,
25
+ autoRefreshToken: false,
26
+ detectSessionInUrl: false
27
+ }
28
+ });
29
+ if (session.refreshToken) {
30
+ const { data, error } = await supabase.auth.setSession({
31
+ access_token: session.accessToken,
32
+ refresh_token: session.refreshToken
33
+ });
34
+ if (!error && data.session) {
35
+ if (data.session.access_token !== session.accessToken ||
36
+ data.session.refresh_token !== session.refreshToken) {
37
+ writeOmpoMcpSession({
38
+ ...session,
39
+ accessToken: data.session.access_token,
40
+ refreshToken: data.session.refresh_token
41
+ });
42
+ }
43
+ return supabase;
44
+ }
45
+ }
46
+ return createClient(session.supabaseUrl, session.supabaseAnonKey, {
27
47
  auth: {
28
48
  persistSession: false,
29
49
  autoRefreshToken: false,
@@ -35,6 +55,27 @@ export async function consumeMcpToken(toolName) {
35
55
  }
36
56
  }
37
57
  });
58
+ }
59
+ export async function readMcpTokenBalance() {
60
+ const session = readOmpoMcpSession();
61
+ if (!session)
62
+ return null;
63
+ const supabase = await createAuthedSupabase(session);
64
+ const { data, error } = await supabase
65
+ .from('user_token_balances')
66
+ .select('tokens_available')
67
+ .eq('user_id', session.userId)
68
+ .maybeSingle();
69
+ if (error)
70
+ throw mapRpcError(error.message);
71
+ return data?.tokens_available ?? null;
72
+ }
73
+ export async function consumeMcpToken(toolName) {
74
+ const session = readOmpoMcpSession();
75
+ if (!session) {
76
+ throw new McpTokenError('NOT_SIGNED_IN', 'Sign in to Ompo and keep the app open so your MCP session stays active.');
77
+ }
78
+ const supabase = await createAuthedSupabase(session);
38
79
  const { data, error } = await supabase.rpc('consume_mcp_token', {
39
80
  p_tool_name: toolName
40
81
  });
package/dist/types.d.ts CHANGED
@@ -23,6 +23,12 @@ export type DomMoveOperation = {
23
23
  fromIndex: number;
24
24
  toParentSelector: string;
25
25
  index: number;
26
+ movedElement?: ElementAnchor;
27
+ fromParent?: ElementAnchor;
28
+ toParent?: ElementAnchor;
29
+ insertBefore?: ElementAnchor;
30
+ destinationChildren?: ElementAnchor[];
31
+ fromChildrenBefore?: ElementAnchor[];
26
32
  };
27
33
  export type DomDeleteOperation = {
28
34
  kind: 'dom.delete';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ompo-design/mcp-server",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "MCP server for applying Ompo visual edits to a codebase",
5
5
  "type": "module",
6
6
  "license": "MIT",