@geometra/mcp 1.61.1 → 1.61.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/server.js +222 -17
  2. package/package.json +2 -2
package/dist/server.js CHANGED
@@ -117,6 +117,68 @@ const geometraWaitForResumeParseInputSchema = z
117
117
  })
118
118
  .strict();
119
119
  const timeoutMsInput = z.number().int().min(50).max(60_000).optional();
120
+ const HOST_SAFE_TOOL_TIMEOUT_MS = 20_000;
121
+ const HOST_SAFE_TIMEOUT_RESERVE_MS = 750;
122
+ const HOST_SAFE_REVEAL_TIMEOUT_MS = 8_000;
123
+ const MIN_ACTION_TIMEOUT_MS = 50;
124
+ function softTimeoutMsInput() {
125
+ return z
126
+ .number()
127
+ .int()
128
+ .min(1_000)
129
+ .max(55_000)
130
+ .optional()
131
+ .default(HOST_SAFE_TOOL_TIMEOUT_MS)
132
+ .describe('Server-side soft deadline for this MCP call. When reached, the tool returns partial progress plus a resume hint before the MCP host request deadline can kill the call.');
133
+ }
134
+ function remainingUntil(deadlineAt) {
135
+ return Math.max(0, Math.floor(deadlineAt - performance.now()));
136
+ }
137
+ function hasSoftBudget(deadlineAt, reserveMs = HOST_SAFE_TIMEOUT_RESERVE_MS) {
138
+ return remainingUntil(deadlineAt) > reserveMs;
139
+ }
140
+ function timeoutCapFromDeadline(deadlineAt, reserveMs = HOST_SAFE_TIMEOUT_RESERVE_MS) {
141
+ return Math.max(MIN_ACTION_TIMEOUT_MS, remainingUntil(deadlineAt) - reserveMs);
142
+ }
143
+ function capTimeoutMs(timeoutMs, capMs, fallbackMs) {
144
+ return Math.max(MIN_ACTION_TIMEOUT_MS, Math.min(timeoutMs ?? fallbackMs, Math.max(MIN_ACTION_TIMEOUT_MS, Math.floor(capMs))));
145
+ }
146
+ function capFillFieldTimeout(field, capMs, fallbackMs = 5_000) {
147
+ return {
148
+ ...field,
149
+ timeoutMs: capTimeoutMs(field.timeoutMs, capMs, fallbackMs),
150
+ };
151
+ }
152
+ function capBatchActionTimeouts(action, capMs) {
153
+ switch (action.type) {
154
+ case 'click':
155
+ return {
156
+ ...action,
157
+ timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 5_000),
158
+ revealTimeoutMs: capTimeoutMs(action.revealTimeoutMs, capMs, 2_500),
159
+ ...(action.waitFor ? { waitFor: { ...action.waitFor, timeoutMs: capTimeoutMs(action.waitFor.timeoutMs, capMs, 10_000) } } : {}),
160
+ };
161
+ case 'type':
162
+ case 'key':
163
+ case 'select_option':
164
+ case 'set_checked':
165
+ case 'wheel':
166
+ return { ...action, timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 5_000) };
167
+ case 'upload_files':
168
+ return { ...action, timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 8_000) };
169
+ case 'pick_listbox_option':
170
+ return { ...action, timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 4_500) };
171
+ case 'wait_for':
172
+ return { ...action, timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 10_000) };
173
+ case 'fill_fields':
174
+ return {
175
+ ...action,
176
+ fields: action.fields.map(field => capFillFieldTimeout(field, capMs)),
177
+ };
178
+ case 'expand_section':
179
+ return action;
180
+ }
181
+ }
120
182
  const fillFieldSchema = z.union([
121
183
  z.object({
122
184
  kind: z.literal('text'),
@@ -1217,24 +1279,44 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
1217
1279
  submitTimeoutMs: z.number().int().min(50).max(60_000).optional().default(15_000).describe('Action wait timeout for the submit click (default 15000ms). Increase for slow backends.'),
1218
1280
  waitFor: z.object(waitConditionShape()).optional().describe('Optional semantic condition to wait for after the submit click (success banner, navigation, submit gone, etc.)'),
1219
1281
  skipFill: z.boolean().optional().default(false).describe('Skip the fill phase and go straight to submit+wait. Use when values have already been filled by a previous call.'),
1282
+ softTimeoutMs: softTimeoutMsInput(),
1220
1283
  failOnInvalid: z.boolean().optional().default(false).describe('Return an error if invalid fields remain after the submit wait resolves.'),
1221
1284
  detail: detailInput(),
1222
1285
  sessionId: sessionIdInput,
1223
- }, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, formId, valuesById, valuesByLabel, submit, submitIndex, submitTimeoutMs, waitFor, skipFill, failOnInvalid, detail, sessionId }) => {
1286
+ }, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, formId, valuesById, valuesByLabel, submit, submitIndex, submitTimeoutMs, waitFor, skipFill, softTimeoutMs, failOnInvalid, detail, sessionId }) => {
1287
+ const toolStartedAt = performance.now();
1288
+ const effectiveSoftTimeoutMs = softTimeoutMs ?? HOST_SAFE_TOOL_TIMEOUT_MS;
1289
+ const deadlineAt = toolStartedAt + effectiveSoftTimeoutMs;
1224
1290
  const resolved = await ensureToolSession({ sessionId, url, pageUrl, port, headless, width, height, slowMo, isolated }, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_submit_form.');
1225
1291
  if (!resolved.ok)
1226
1292
  return err(resolved.error);
1227
1293
  const session = resolved.session;
1228
1294
  const connection = autoConnectionPayload(resolved);
1295
+ let fillSummary;
1296
+ let fillFallback;
1297
+ const pausedPayload = (phase, resumeHint, extra) => ({
1298
+ ...connection,
1299
+ completed: false,
1300
+ paused: true,
1301
+ phase,
1302
+ pauseReason: 'soft-timeout',
1303
+ softTimeoutMs: effectiveSoftTimeoutMs,
1304
+ elapsedMs: Number((performance.now() - toolStartedAt).toFixed(1)),
1305
+ ...(resumeHint ? { resumeHint } : {}),
1306
+ ...(fillSummary ? { fill: fillSummary } : {}),
1307
+ ...(fillFallback ? { fill_fallback: fillFallback } : {}),
1308
+ ...(extra ?? {}),
1309
+ });
1229
1310
  if (!session.tree || !session.layout) {
1230
- await waitForUiCondition(session, () => Boolean(session.tree && session.layout), 2_000);
1311
+ await waitForUiCondition(session, () => Boolean(session.tree && session.layout), capTimeoutMs(2_000, timeoutCapFromDeadline(deadlineAt), 2_000));
1231
1312
  }
1232
1313
  const entryA11y = sessionA11y(session);
1233
1314
  if (!entryA11y)
1234
1315
  return err('No UI tree available for form submission');
1235
1316
  const entryUrl = entryA11y.meta?.pageUrl;
1236
- let fillSummary;
1237
- let fillFallback;
1317
+ if (!hasSoftBudget(deadlineAt)) {
1318
+ return ok(JSON.stringify(pausedPayload('before-fill', { retrySameCall: true }), null, detail === 'verbose' ? 2 : undefined));
1319
+ }
1238
1320
  if (!skipFill) {
1239
1321
  const entryCount = Object.keys(valuesById ?? {}).length + Object.keys(valuesByLabel ?? {}).length;
1240
1322
  if (entryCount === 0) {
@@ -1253,16 +1335,26 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
1253
1335
  let usedBatch = true;
1254
1336
  try {
1255
1337
  const startRevision = session.updateRevision;
1256
- const wait = await sendFillFields(session, planned.fields);
1338
+ const wait = await sendFillFields(session, planned.fields.map(field => capFillFieldTimeout(field, timeoutCapFromDeadline(deadlineAt))), capTimeoutMs(undefined, timeoutCapFromDeadline(deadlineAt), HOST_SAFE_TOOL_TIMEOUT_MS));
1257
1339
  const ack = parseProxyFillAckResult(wait.result);
1258
1340
  await waitForDeferredBatchUpdate(session, startRevision, wait);
1259
1341
  fillSummary = {
1260
1342
  formId: schema.formId,
1261
1343
  execution: 'batched',
1262
1344
  fieldCount: planned.fields.length,
1345
+ ...waitStatusPayload(wait),
1263
1346
  ...(ack ? { invalidCount: ack.invalidCount, alertCount: ack.alertCount } : {}),
1264
1347
  ...(entryCount !== planned.fields.length ? { requestedValueCount: entryCount } : {}),
1265
1348
  };
1349
+ if (wait.status === 'timed_out') {
1350
+ return ok(JSON.stringify(pausedPayload('fill', {
1351
+ tool: 'geometra_submit_form',
1352
+ skipFill: true,
1353
+ submit: submit ?? { role: 'button', name: 'Submit' },
1354
+ submitIndex,
1355
+ ...(waitFor ? { waitFor } : {}),
1356
+ }), null, detail === 'verbose' ? 2 : undefined));
1357
+ }
1266
1358
  }
1267
1359
  catch (e) {
1268
1360
  if (!canFallbackToSequentialFill(e)) {
@@ -1276,8 +1368,24 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
1276
1368
  let successCount = 0;
1277
1369
  let firstErr;
1278
1370
  for (const field of planned.fields) {
1371
+ if (!hasSoftBudget(deadlineAt)) {
1372
+ fillSummary = {
1373
+ formId: schema.formId,
1374
+ execution: 'sequential',
1375
+ fieldCount: planned.fields.length,
1376
+ successCount,
1377
+ ...(entryCount !== planned.fields.length ? { requestedValueCount: entryCount } : {}),
1378
+ };
1379
+ return ok(JSON.stringify(pausedPayload('fill', {
1380
+ tool: 'geometra_submit_form',
1381
+ skipFill: true,
1382
+ submit: submit ?? { role: 'button', name: 'Submit' },
1383
+ submitIndex,
1384
+ ...(waitFor ? { waitFor } : {}),
1385
+ }), null, detail === 'verbose' ? 2 : undefined));
1386
+ }
1279
1387
  try {
1280
- await executeFillField(session, field, detail);
1388
+ await executeFillField(session, capFillFieldTimeout(field, timeoutCapFromDeadline(deadlineAt)), detail);
1281
1389
  successCount += 1;
1282
1390
  }
1283
1391
  catch (e) {
@@ -1297,12 +1405,22 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
1297
1405
  };
1298
1406
  }
1299
1407
  }
1408
+ const submitResumeHint = {
1409
+ tool: 'geometra_submit_form',
1410
+ skipFill: true,
1411
+ submit: submit ?? { role: 'button', name: 'Submit' },
1412
+ submitIndex,
1413
+ ...(waitFor ? { waitFor } : {}),
1414
+ };
1415
+ if (!hasSoftBudget(deadlineAt)) {
1416
+ return ok(JSON.stringify(pausedPayload('submit', submitResumeHint), null, detail === 'verbose' ? 2 : undefined));
1417
+ }
1300
1418
  const submitFilter = submit ?? { role: 'button', name: 'Submit' };
1301
1419
  const resolvedClick = await resolveClickLocationWithFallback(session, {
1302
1420
  filter: submitFilter,
1303
1421
  index: submitIndex,
1304
1422
  fullyVisible: true,
1305
- revealTimeoutMs: 2_500,
1423
+ revealTimeoutMs: capTimeoutMs(2_500, timeoutCapFromDeadline(deadlineAt), 2_500),
1306
1424
  });
1307
1425
  if (!resolvedClick.ok) {
1308
1426
  const payload = {
@@ -1315,10 +1433,44 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
1315
1433
  };
1316
1434
  return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
1317
1435
  }
1436
+ if (!hasSoftBudget(deadlineAt)) {
1437
+ return ok(JSON.stringify(pausedPayload('submit', submitResumeHint), null, detail === 'verbose' ? 2 : undefined));
1438
+ }
1318
1439
  const beforeSubmit = sessionA11y(session);
1319
- const clickWait = await sendClick(session, resolvedClick.value.x, resolvedClick.value.y, submitTimeoutMs);
1440
+ const clickWait = await sendClick(session, resolvedClick.value.x, resolvedClick.value.y, capTimeoutMs(submitTimeoutMs, timeoutCapFromDeadline(deadlineAt), 15_000));
1320
1441
  let waitResult;
1321
1442
  if (waitFor) {
1443
+ if (!hasSoftBudget(deadlineAt)) {
1444
+ return ok(JSON.stringify(pausedPayload('wait_for', {
1445
+ tool: 'geometra_wait_for',
1446
+ filter: compactFilterPayload({
1447
+ id: waitFor.id,
1448
+ role: waitFor.role,
1449
+ name: waitFor.name,
1450
+ text: waitFor.text,
1451
+ contextText: waitFor.contextText,
1452
+ promptText: waitFor.promptText,
1453
+ sectionText: waitFor.sectionText,
1454
+ itemText: waitFor.itemText,
1455
+ value: waitFor.value,
1456
+ checked: waitFor.checked,
1457
+ disabled: waitFor.disabled,
1458
+ focused: waitFor.focused,
1459
+ selected: waitFor.selected,
1460
+ expanded: waitFor.expanded,
1461
+ invalid: waitFor.invalid,
1462
+ required: waitFor.required,
1463
+ busy: waitFor.busy,
1464
+ }),
1465
+ present: waitFor.present ?? true,
1466
+ }, {
1467
+ submit: {
1468
+ at: { x: resolvedClick.value.x, y: resolvedClick.value.y },
1469
+ ...(resolvedClick.value.target ? { target: compactNodeReference(resolvedClick.value.target) } : {}),
1470
+ ...waitStatusPayload(clickWait),
1471
+ },
1472
+ }), null, detail === 'verbose' ? 2 : undefined));
1473
+ }
1322
1474
  const postWait = await waitForSemanticCondition(session, {
1323
1475
  filter: {
1324
1476
  id: waitFor.id,
@@ -1340,7 +1492,7 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
1340
1492
  busy: waitFor.busy,
1341
1493
  },
1342
1494
  present: waitFor.present ?? true,
1343
- timeoutMs: waitFor.timeoutMs ?? 15_000,
1495
+ timeoutMs: capTimeoutMs(waitFor.timeoutMs, timeoutCapFromDeadline(deadlineAt), 15_000),
1344
1496
  });
1345
1497
  if (!postWait.ok) {
1346
1498
  const preErrFallbacks = [];
@@ -1426,6 +1578,14 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
1426
1578
  .default(false)
1427
1579
  .describe('When auto-connecting via pageUrl/url, request an isolated proxy. See geometra_connect for details.'),
1428
1580
  actions: z.array(batchActionSchema).min(1).max(80).describe('Ordered high-level action steps to run sequentially'),
1581
+ resumeFromIndex: z
1582
+ .number()
1583
+ .int()
1584
+ .min(0)
1585
+ .optional()
1586
+ .default(0)
1587
+ .describe('Resume a previous partial geometra_run_actions result from this action index. Use the returned resumeFromIndex when a call pauses.'),
1588
+ softTimeoutMs: softTimeoutMsInput(),
1429
1589
  stopOnError: z.boolean().optional().default(true).describe('Stop at the first failing step (default true)'),
1430
1590
  includeSteps: z
1431
1591
  .boolean()
@@ -1435,7 +1595,10 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
1435
1595
  output: z.enum(['full', 'final']).optional().default('full').describe('`full` (default) returns counts and optional step listings. `final` keeps only completion state plus final semantic signals.'),
1436
1596
  detail: detailInput(),
1437
1597
  sessionId: sessionIdInput,
1438
- }, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, actions, stopOnError, includeSteps, output, detail, sessionId }) => {
1598
+ }, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, actions, resumeFromIndex, softTimeoutMs, stopOnError, includeSteps, output, detail, sessionId }) => {
1599
+ const toolStartedAt = performance.now();
1600
+ const effectiveSoftTimeoutMs = softTimeoutMs ?? HOST_SAFE_TOOL_TIMEOUT_MS;
1601
+ const deadlineAt = toolStartedAt + effectiveSoftTimeoutMs;
1439
1602
  const resolved = await ensureToolSession({
1440
1603
  sessionId,
1441
1604
  url,
@@ -1452,23 +1615,36 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
1452
1615
  return err(resolved.error);
1453
1616
  const session = resolved.session;
1454
1617
  const connection = autoConnectionPayload(resolved);
1618
+ const startIndex = resumeFromIndex ?? 0;
1619
+ if (startIndex > actions.length) {
1620
+ return err(`resumeFromIndex ${startIndex} exceeds actions length ${actions.length}`);
1621
+ }
1455
1622
  const steps = [];
1456
1623
  let stoppedAt;
1624
+ let pausedAt;
1457
1625
  const batchStartedAt = performance.now();
1458
1626
  // Collect transparent-fallback signals from each step so run_actions
1459
1627
  // surfaces them at top level regardless of `includeSteps` — otherwise
1460
1628
  // the telemetry is dead code when callers opt out of the steps listing.
1461
1629
  const fallbackRecords = [];
1462
- for (let index = 0; index < actions.length; index++) {
1463
- const action = actions[index];
1630
+ for (let index = startIndex; index < actions.length; index++) {
1631
+ if (!hasSoftBudget(deadlineAt)) {
1632
+ pausedAt = index;
1633
+ break;
1634
+ }
1635
+ const action = capBatchActionTimeouts(actions[index], timeoutCapFromDeadline(deadlineAt));
1464
1636
  const startedAt = performance.now();
1465
1637
  let uiTreeWaitMs = 0;
1466
1638
  try {
1467
1639
  if (actionNeedsUiTree(action) && (!session.tree || !session.layout)) {
1468
1640
  const uiTreeWaitStartedAt = performance.now();
1469
- await waitForUiCondition(session, () => Boolean(session.tree && session.layout), 2_000);
1641
+ await waitForUiCondition(session, () => Boolean(session.tree && session.layout), capTimeoutMs(2_000, timeoutCapFromDeadline(deadlineAt), 2_000));
1470
1642
  uiTreeWaitMs = performance.now() - uiTreeWaitStartedAt;
1471
1643
  }
1644
+ if (!hasSoftBudget(deadlineAt)) {
1645
+ pausedAt = index;
1646
+ break;
1647
+ }
1472
1648
  const result = await executeBatchAction(session, action, detail, includeSteps);
1473
1649
  const stepFallback = result.compact.fallback;
1474
1650
  if (stepFallback?.used) {
@@ -1511,6 +1687,10 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
1511
1687
  ...result.compact,
1512
1688
  ...(stepSignals ? { signals: stepSignals } : {}),
1513
1689
  });
1690
+ if (index + 1 < actions.length && !hasSoftBudget(deadlineAt)) {
1691
+ pausedAt = index + 1;
1692
+ break;
1693
+ }
1514
1694
  }
1515
1695
  catch (e) {
1516
1696
  const message = e instanceof Error ? e.message : String(e);
@@ -1534,20 +1714,37 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
1534
1714
  const after = sessionA11y(session);
1535
1715
  const successCount = steps.filter(step => step.ok === true).length;
1536
1716
  const errorCount = steps.length - successCount;
1717
+ const elapsedMs = Number((performance.now() - toolStartedAt).toFixed(1));
1718
+ const completed = stoppedAt === undefined && pausedAt === undefined && startIndex + steps.length >= actions.length;
1719
+ const resumePayload = {
1720
+ ...(startIndex > 0 ? { resumedFromIndex: startIndex } : {}),
1721
+ ...(pausedAt !== undefined
1722
+ ? {
1723
+ paused: true,
1724
+ pausedAt,
1725
+ resumeFromIndex: pausedAt,
1726
+ pauseReason: 'soft-timeout',
1727
+ softTimeoutMs: effectiveSoftTimeoutMs,
1728
+ elapsedMs,
1729
+ }
1730
+ : {}),
1731
+ };
1537
1732
  const payload = output === 'final'
1538
1733
  ? {
1539
1734
  ...connection,
1540
- completed: stoppedAt === undefined && steps.length === actions.length,
1735
+ completed,
1736
+ ...resumePayload,
1541
1737
  ...(stoppedAt !== undefined ? { stoppedAt } : {}),
1542
1738
  ...(fallbackRecords.length > 0 ? { fallbacks: fallbackRecords } : {}),
1543
1739
  ...(after ? { final: sessionSignalsPayload(collectSessionSignals(after), detail) } : {}),
1544
1740
  }
1545
1741
  : {
1546
1742
  ...connection,
1547
- completed: stoppedAt === undefined && steps.length === actions.length,
1743
+ completed,
1548
1744
  stepCount: actions.length,
1549
1745
  successCount,
1550
1746
  errorCount,
1747
+ ...resumePayload,
1551
1748
  ...(includeSteps ? { steps } : {}),
1552
1749
  ...(stoppedAt !== undefined ? { stoppedAt } : {}),
1553
1750
  ...(fallbackRecords.length > 0 ? { fallbacks: fallbackRecords } : {}),
@@ -2994,13 +3191,21 @@ function inferRevealStepBudget(target, viewport) {
2994
3191
  return clamp(Math.max(6, Math.max(verticalSteps, horizontalSteps) + 1), 6, 48);
2995
3192
  }
2996
3193
  async function revealSemanticTarget(session, options) {
2997
- const initialTreeReady = await ensureSessionUiTree(session, Math.max(4_000, options.timeoutMs));
3194
+ const revealStartedAt = performance.now();
3195
+ const revealDeadlineAt = revealStartedAt + HOST_SAFE_REVEAL_TIMEOUT_MS;
3196
+ const initialTreeReady = await ensureSessionUiTree(session, capTimeoutMs(Math.max(4_000, options.timeoutMs), timeoutCapFromDeadline(revealDeadlineAt, 100), HOST_SAFE_REVEAL_TIMEOUT_MS));
2998
3197
  if (!initialTreeReady) {
2999
3198
  return { ok: false, error: 'Timed out waiting for the initial UI tree after connect.' };
3000
3199
  }
3001
3200
  let attempts = 0;
3002
3201
  let stepBudget = options.maxSteps;
3003
3202
  while (attempts <= (stepBudget ?? 48)) {
3203
+ if (!hasSoftBudget(revealDeadlineAt, 100)) {
3204
+ return {
3205
+ ok: false,
3206
+ error: `Reveal exceeded the host-safe ${HOST_SAFE_REVEAL_TIMEOUT_MS}ms budget after ${attempts} step(s). Retry with a more specific filter/id or reveal the target in smaller geometra_scroll_to calls.`,
3207
+ };
3208
+ }
3004
3209
  const a11y = sessionA11y(session);
3005
3210
  if (!a11y)
3006
3211
  return { ok: false, error: 'No UI tree available to reveal from' };
@@ -3046,7 +3251,7 @@ async function revealSemanticTarget(session, options) {
3046
3251
  deltaX,
3047
3252
  x: formatted.center.x,
3048
3253
  y: formatted.center.y,
3049
- }, options.timeoutMs);
3254
+ }, capTimeoutMs(options.timeoutMs, timeoutCapFromDeadline(revealDeadlineAt, 100), 2_500));
3050
3255
  attempts++;
3051
3256
  }
3052
3257
  return { ok: false, error: `Failed to reveal ${JSON.stringify(options.filter)}` };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.61.1",
3
+ "version": "1.61.2",
4
4
  "description": "MCP server for Geometra — interact with running Geometra apps via the geometry protocol, no browser needed",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,7 +32,7 @@
32
32
  "ui-testing"
33
33
  ],
34
34
  "dependencies": {
35
- "@geometra/proxy": "^1.61.1",
35
+ "@geometra/proxy": "^1.61.2",
36
36
  "@modelcontextprotocol/sdk": "^1.12.1",
37
37
  "@razroo/parallel-mcp": "^0.1.0",
38
38
  "ws": "^8.18.0",