@geometra/mcp 1.27.0 → 1.28.0
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/__tests__/server-batch-results.test.js +2 -2
- package/dist/server.js +32 -4
- package/dist/session.d.ts +6 -0
- package/dist/session.js +54 -0
- package/package.json +1 -1
|
@@ -967,7 +967,7 @@ describe('query and reveal tools', () => {
|
|
|
967
967
|
});
|
|
968
968
|
});
|
|
969
969
|
it('reveals an offscreen target with semantic scrolling instead of requiring manual wheels', async () => {
|
|
970
|
-
const handler = getToolHandler('
|
|
970
|
+
const handler = getToolHandler('geometra_scroll_to');
|
|
971
971
|
mockState.currentA11yRoot = node('group', undefined, {
|
|
972
972
|
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
973
973
|
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
@@ -1024,7 +1024,7 @@ describe('query and reveal tools', () => {
|
|
|
1024
1024
|
});
|
|
1025
1025
|
});
|
|
1026
1026
|
it('auto-scales reveal steps for tall forms when maxSteps is omitted', async () => {
|
|
1027
|
-
const handler = getToolHandler('
|
|
1027
|
+
const handler = getToolHandler('geometra_scroll_to');
|
|
1028
1028
|
let scrollY = 0;
|
|
1029
1029
|
const setTree = () => {
|
|
1030
1030
|
mockState.currentA11yRoot = node('group', undefined, {
|
package/dist/server.js
CHANGED
|
@@ -329,8 +329,8 @@ Chromium opens **visible** by default unless \`headless: true\`. File upload / w
|
|
|
329
329
|
returnForms: z
|
|
330
330
|
.boolean()
|
|
331
331
|
.optional()
|
|
332
|
-
.default(
|
|
333
|
-
.describe('Include compact form schema discovery in the connect response
|
|
332
|
+
.default(true)
|
|
333
|
+
.describe('Include compact form schema discovery in the connect response (default true). Set false to skip form discovery for non-form workflows.'),
|
|
334
334
|
returnPageModel: z
|
|
335
335
|
.boolean()
|
|
336
336
|
.optional()
|
|
@@ -753,6 +753,17 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
753
753
|
}
|
|
754
754
|
catch (e) {
|
|
755
755
|
const message = e instanceof Error ? e.message : String(e);
|
|
756
|
+
// Retry once for transient selection failures
|
|
757
|
+
if (message.includes('selection_not_confirmed')) {
|
|
758
|
+
try {
|
|
759
|
+
const retryResult = await executeFillField(session, field, detail);
|
|
760
|
+
steps.push(detail === 'verbose'
|
|
761
|
+
? { index, kind: field.kind, ok: true, summary: retryResult.summary, retried: true }
|
|
762
|
+
: { index, kind: field.kind, ok: true, ...retryResult.compact, retried: true });
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
catch { /* fall through to error handling */ }
|
|
766
|
+
}
|
|
756
767
|
const suggestion = isResolvedFillFieldInput(field) ? suggestRecovery(field, message) : undefined;
|
|
757
768
|
steps.push({ index, kind: field.kind, ok: false, error: message, ...(suggestion ? { suggestion } : {}) });
|
|
758
769
|
if (stopOnError) {
|
|
@@ -901,6 +912,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
901
912
|
});
|
|
902
913
|
if (schemas.length === 0)
|
|
903
914
|
return err('No forms found in the current UI');
|
|
915
|
+
const entryUrl = afterConnect?.meta?.pageUrl;
|
|
904
916
|
const resolution = resolveTargetFormSchema(schemas, { formId, valuesById, valuesByLabel });
|
|
905
917
|
if (!resolution.ok)
|
|
906
918
|
return err(resolution.error);
|
|
@@ -1053,6 +1065,22 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
1053
1065
|
...(verification ? { verification } : {}),
|
|
1054
1066
|
...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
|
|
1055
1067
|
};
|
|
1068
|
+
// Detect page navigation after fill (e.g. multi-page form submission)
|
|
1069
|
+
const afterUrl = after?.meta?.pageUrl;
|
|
1070
|
+
if (afterUrl && entryUrl && afterUrl !== entryUrl) {
|
|
1071
|
+
;
|
|
1072
|
+
payload.navigated = true;
|
|
1073
|
+
payload.afterUrl = afterUrl;
|
|
1074
|
+
const model = after ? buildPageModel(after) : undefined;
|
|
1075
|
+
if (model) {
|
|
1076
|
+
;
|
|
1077
|
+
payload.pageModel = summarizePageModel(model);
|
|
1078
|
+
if (model.captcha)
|
|
1079
|
+
payload.captcha = model.captcha;
|
|
1080
|
+
if (model.verification)
|
|
1081
|
+
payload.verification = model.verification;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1056
1084
|
recordWorkflowFill(session, schema.formId, schema.name, valuesById, valuesByLabel, invalidRemaining, planned.fields.length);
|
|
1057
1085
|
if (failOnInvalid && invalidRemaining > 0) {
|
|
1058
1086
|
return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
@@ -1307,9 +1335,9 @@ Use this after geometra_page_model when you know which form/dialog/list/landmark
|
|
|
1307
1335
|
return err(`No expandable section found for id ${id}`);
|
|
1308
1336
|
return ok(JSON.stringify(detail));
|
|
1309
1337
|
});
|
|
1310
|
-
server.tool('
|
|
1338
|
+
server.tool('geometra_scroll_to', `Scroll the page until a matching element is visible. Use this to reach off-screen elements like Submit buttons at the bottom of long forms, or fields below the fold.
|
|
1311
1339
|
|
|
1312
|
-
|
|
1340
|
+
This is the preferred approach for scrolling — no need to guess pixel offsets or wheel deltas. Accepts the same filters as geometra_query, plus an optional match index when repeated controls share the same visible label. Auto-scales scroll steps based on distance.`, {
|
|
1313
1341
|
...nodeFilterShape(),
|
|
1314
1342
|
index: z.number().int().min(0).optional().default(0).describe('Which matching node to reveal after sorting top-to-bottom'),
|
|
1315
1343
|
fullyVisible: z.boolean().optional().default(true).describe('Require the target to become fully visible (default true)'),
|
package/dist/session.d.ts
CHANGED
|
@@ -130,6 +130,11 @@ export interface CaptchaDetection {
|
|
|
130
130
|
type?: 'recaptcha' | 'hcaptcha' | 'turnstile' | 'cloudflare-challenge' | 'unknown';
|
|
131
131
|
hint?: string;
|
|
132
132
|
}
|
|
133
|
+
export interface VerificationDetection {
|
|
134
|
+
detected: boolean;
|
|
135
|
+
type?: 'email_code' | 'sms_code' | 'security_question' | 'unknown';
|
|
136
|
+
hint?: string;
|
|
137
|
+
}
|
|
133
138
|
export interface PageModel {
|
|
134
139
|
viewport: {
|
|
135
140
|
width: number;
|
|
@@ -144,6 +149,7 @@ export interface PageModel {
|
|
|
144
149
|
focusableCount: number;
|
|
145
150
|
};
|
|
146
151
|
captcha?: CaptchaDetection;
|
|
152
|
+
verification?: VerificationDetection;
|
|
147
153
|
primaryActions: PagePrimaryAction[];
|
|
148
154
|
landmarks: PageLandmark[];
|
|
149
155
|
forms: PageFormModel[];
|
package/dist/session.js
CHANGED
|
@@ -578,6 +578,21 @@ export function connect(url, opts) {
|
|
|
578
578
|
cachedFormSchemas: new Map(),
|
|
579
579
|
};
|
|
580
580
|
let resolved = false;
|
|
581
|
+
let lastMessageAt = Date.now();
|
|
582
|
+
let heartbeatInterval = null;
|
|
583
|
+
function startHeartbeat() {
|
|
584
|
+
if (heartbeatInterval)
|
|
585
|
+
return;
|
|
586
|
+
heartbeatInterval = setInterval(() => {
|
|
587
|
+
if (Date.now() - lastMessageAt > 30_000) {
|
|
588
|
+
try {
|
|
589
|
+
ws.close();
|
|
590
|
+
}
|
|
591
|
+
catch { /* ignore */ }
|
|
592
|
+
}
|
|
593
|
+
}, 30_000);
|
|
594
|
+
heartbeatInterval.unref();
|
|
595
|
+
}
|
|
581
596
|
const timeout = setTimeout(() => {
|
|
582
597
|
if (!resolved) {
|
|
583
598
|
resolved = true;
|
|
@@ -603,10 +618,12 @@ export function connect(url, opts) {
|
|
|
603
618
|
}
|
|
604
619
|
activeSessions.set(session.id, session);
|
|
605
620
|
defaultSessionId = session.id;
|
|
621
|
+
startHeartbeat();
|
|
606
622
|
resolve(session);
|
|
607
623
|
}
|
|
608
624
|
});
|
|
609
625
|
ws.on('message', (data) => {
|
|
626
|
+
lastMessageAt = Date.now();
|
|
610
627
|
try {
|
|
611
628
|
const msg = JSON.parse(String(data));
|
|
612
629
|
if (msg.type === 'frame') {
|
|
@@ -626,6 +643,7 @@ export function connect(url, opts) {
|
|
|
626
643
|
}
|
|
627
644
|
activeSessions.set(session.id, session);
|
|
628
645
|
defaultSessionId = session.id;
|
|
646
|
+
startHeartbeat();
|
|
629
647
|
resolve(session);
|
|
630
648
|
}
|
|
631
649
|
}
|
|
@@ -645,6 +663,10 @@ export function connect(url, opts) {
|
|
|
645
663
|
}
|
|
646
664
|
});
|
|
647
665
|
ws.on('close', () => {
|
|
666
|
+
if (heartbeatInterval) {
|
|
667
|
+
clearInterval(heartbeatInterval);
|
|
668
|
+
heartbeatInterval = null;
|
|
669
|
+
}
|
|
648
670
|
if (activeSessions.get(session.id) === session) {
|
|
649
671
|
activeSessions.delete(session.id);
|
|
650
672
|
if (defaultSessionId === session.id)
|
|
@@ -1989,6 +2011,36 @@ function detectCaptcha(root) {
|
|
|
1989
2011
|
}
|
|
1990
2012
|
return found ?? { detected: false };
|
|
1991
2013
|
}
|
|
2014
|
+
const VERIFICATION_FIELD_PATTERN = /verif|security.?code|confirm.*(code|email)|one.?time|otp|2fa|mfa|passcode/i;
|
|
2015
|
+
const VERIFICATION_CONTEXT_PATTERN = /sent.*(code|email|sms|text)|enter.*code|check.your.(email|phone|inbox)|we.sent|verification/i;
|
|
2016
|
+
function detectVerification(root) {
|
|
2017
|
+
let found;
|
|
2018
|
+
function walk(node) {
|
|
2019
|
+
if (found)
|
|
2020
|
+
return;
|
|
2021
|
+
const name = node.name ?? '';
|
|
2022
|
+
if (node.role === 'textbox' && VERIFICATION_FIELD_PATTERN.test(name)) {
|
|
2023
|
+
const type = /email|inbox/i.test(name) ? 'email_code'
|
|
2024
|
+
: /sms|phone|text/i.test(name) ? 'sms_code'
|
|
2025
|
+
: /security.?question/i.test(name) ? 'security_question'
|
|
2026
|
+
: 'unknown';
|
|
2027
|
+
found = { detected: true, type, hint: `Verification field: "${name}"` };
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
const text = [name, node.value].filter(Boolean).join(' ');
|
|
2031
|
+
if (VERIFICATION_CONTEXT_PATTERN.test(text)) {
|
|
2032
|
+
const type = /email|inbox/i.test(text) ? 'email_code'
|
|
2033
|
+
: /sms|phone|text.message/i.test(text) ? 'sms_code'
|
|
2034
|
+
: 'unknown';
|
|
2035
|
+
found = { detected: true, type, hint: text.slice(0, 120) };
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
for (const child of node.children)
|
|
2039
|
+
walk(child);
|
|
2040
|
+
}
|
|
2041
|
+
walk(root);
|
|
2042
|
+
return found ?? { detected: false };
|
|
2043
|
+
}
|
|
1992
2044
|
export function buildPageModel(root, options) {
|
|
1993
2045
|
const maxPrimaryActions = options?.maxPrimaryActions ?? 6;
|
|
1994
2046
|
const maxSectionsPerKind = options?.maxSectionsPerKind ?? 8;
|
|
@@ -2073,9 +2125,11 @@ export function buildPageModel(root, options) {
|
|
|
2073
2125
|
lists: sortByBounds(lists).slice(0, maxSectionsPerKind),
|
|
2074
2126
|
};
|
|
2075
2127
|
const captcha = detectCaptcha(root);
|
|
2128
|
+
const verification = detectVerification(root);
|
|
2076
2129
|
return {
|
|
2077
2130
|
...baseModel,
|
|
2078
2131
|
...(captcha.detected ? { captcha } : {}),
|
|
2132
|
+
...(verification.detected ? { verification } : {}),
|
|
2079
2133
|
archetypes: inferPageArchetypes(baseModel),
|
|
2080
2134
|
};
|
|
2081
2135
|
}
|
package/package.json
CHANGED