@geometra/mcp 1.61.0 → 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.
- package/dist/server.js +222 -17
- 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
|
-
|
|
1237
|
-
|
|
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
|
|
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 =
|
|
1463
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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",
|