@geometra/mcp 1.61.1 → 1.61.3
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/README.md +6 -5
- package/dist/proxy-spawn.d.ts +2 -0
- package/dist/proxy-spawn.js +20 -0
- package/dist/server.js +254 -24
- package/dist/session.d.ts +3 -0
- package/dist/session.js +19 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,14 +18,14 @@ Geometra proxy: Chromium → DOM geometry → same WebSocket as native →
|
|
|
18
18
|
|
|
19
19
|
Use Geometra MCP when an LLM needs to explore, interpret, and operate a real UI with compact semantic state instead of repeatedly consuming large browser snapshots. Keep Playwright-style tooling for deterministic scripts, DOM-oriented test automation, and compatibility fallback paths while Geometra closes remaining live-site gaps.
|
|
20
20
|
|
|
21
|
-
Proxy-backed sessions stay warm by default on disconnect, and MCP now keeps a small warm pool so compatible headed and
|
|
21
|
+
Proxy-backed sessions stay warm by default on disconnect, and MCP now keeps a small warm pool so compatible headed, headless, and stealth workflows do not immediately evict each other.
|
|
22
22
|
|
|
23
23
|
## Tools
|
|
24
24
|
|
|
25
25
|
| Tool | Description |
|
|
26
26
|
|---|---|
|
|
27
|
-
| `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; can inline `formSchema` and/or `pageModel`, or defer the page model for a faster first connect response |
|
|
28
|
-
| `geometra_prepare_browser` | Pre-launch and pre-navigate a reusable proxy/browser for `pageUrl` without creating an active session;
|
|
27
|
+
| `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; pass `stealth: true` for CloakBrowser's patched Chromium; can inline `formSchema` and/or `pageModel`, or defer the page model for a faster first connect response |
|
|
28
|
+
| `geometra_prepare_browser` | Pre-launch and pre-navigate a reusable proxy/browser for `pageUrl` without creating an active session; supports the same headed/headless/stealth browser settings as `geometra_connect` |
|
|
29
29
|
| `geometra_query` | Find elements by stable id, role, name, text content, prompt/section/item context, current value, or semantic state such as `invalid`, `required`, or `busy` |
|
|
30
30
|
| `geometra_wait_for` | Wait for a semantic condition instead of guessing sleeps (`busy`, `disabled`, alerts, values, etc.). **Strict parameters** — use `text` plus `present: false` to wait until a substring disappears (e.g. “Parsing your resume”); there is no `textGone` field |
|
|
31
31
|
| `geometra_wait_for_resume_parse` | Convenience wait until a parsing banner is gone (defaults: `text`: `"Parsing"`, `present` implied false). Same engine as `geometra_wait_for` |
|
|
@@ -252,9 +252,10 @@ In another terminal (from repo root after `npm install` / `bun install` and `bun
|
|
|
252
252
|
```bash
|
|
253
253
|
npx geometra-proxy http://localhost:8080 --port 3200
|
|
254
254
|
# Requires Chromium: npx playwright install chromium
|
|
255
|
+
# Optional stealth prefetch: npx cloakbrowser install
|
|
255
256
|
```
|
|
256
257
|
|
|
257
|
-
`geometra-proxy` opens a **visible Chromium window by default**. For servers or CI, pass **`--headless`** or set **`GEOMETRA_HEADLESS=1`**. Optional **`--slow-mo <ms>`** slows Playwright actions so they are easier to watch. Headed vs headless usually does **not** materially change token usage, since token usage is driven by MCP tool output rather than whether Chromium is visible.
|
|
258
|
+
`geometra-proxy` opens a **visible Chromium window by default**. For servers or CI, pass **`--headless`** or set **`GEOMETRA_HEADLESS=1`**. Pass **`--stealth`**, **`stealth: true`** on MCP tools, or **`GEOMETRA_STEALTH=1`** to use CloakBrowser's patched Chromium through the same proxy protocol. Optional **`--slow-mo <ms>`** slows Playwright actions so they are easier to watch. Headed vs headless usually does **not** materially change token usage, since token usage is driven by MCP tool output rather than whether Chromium is visible.
|
|
258
259
|
|
|
259
260
|
Point MCP at `ws://127.0.0.1:3200` instead of a native Geometra server. The proxy translates clicks and keyboard messages into Playwright actions and streams updated geometry.
|
|
260
261
|
|
|
@@ -341,7 +342,7 @@ For long application flows, prefer one of these patterns:
|
|
|
341
342
|
4. `geometra_snapshot({ view: "form-required" })` when you need the remaining required fields including offscreen ones
|
|
342
343
|
5. `geometra_reveal` for far-below-fold targets such as submit buttons (auto-scales reveal steps when you omit `maxSteps`)
|
|
343
344
|
6. `geometra_click({ ..., waitFor: ... })` when one action should also wait for the next semantic state
|
|
344
|
-
7. `geometra_prepare_browser({ pageUrl, headless, width, height })` when you can hide browser startup before the real task and want the next proxy-backed flow to attach warm
|
|
345
|
+
7. `geometra_prepare_browser({ pageUrl, headless, stealth, width, height })` when you can hide browser startup before the real task and want the next proxy-backed flow to attach warm
|
|
345
346
|
8. `geometra_run_actions` when you need mixed navigation + waits + field entry, especially with `pageUrl` / `url` for a one-call flow
|
|
346
347
|
9. `geometra_connect({ pageUrl, returnPageModel: true })` when you want connect + summary-first exploration in one turn
|
|
347
348
|
10. `geometra_connect({ pageUrl, returnPageModel: true, pageModelMode: "deferred" })` when first-response latency matters more than inlining the page model; follow with `geometra_page_model`
|
package/dist/proxy-spawn.d.ts
CHANGED
|
@@ -31,9 +31,11 @@ export interface SpawnProxyParams {
|
|
|
31
31
|
width?: number;
|
|
32
32
|
height?: number;
|
|
33
33
|
slowMo?: number;
|
|
34
|
+
stealth?: boolean;
|
|
34
35
|
eagerInitialExtract?: boolean;
|
|
35
36
|
proxy?: SpawnProxyConfig;
|
|
36
37
|
}
|
|
38
|
+
export declare function resolveStealthMode(stealth?: boolean): boolean;
|
|
37
39
|
export declare function startEmbeddedGeometraProxy(opts: SpawnProxyParams): Promise<{
|
|
38
40
|
runtime: EmbeddedProxyRuntime;
|
|
39
41
|
wsUrl: string;
|
package/dist/proxy-spawn.js
CHANGED
|
@@ -125,6 +125,18 @@ function buildLocalProxyDistIfPossible(packageDir, entryFile, errors) {
|
|
|
125
125
|
}
|
|
126
126
|
return undefined;
|
|
127
127
|
}
|
|
128
|
+
function envRequestsStealth() {
|
|
129
|
+
const explicit = process.env.GEOMETRA_STEALTH;
|
|
130
|
+
if (explicit !== undefined) {
|
|
131
|
+
const v = explicit.toLowerCase();
|
|
132
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'stealth' || v === 'cloak';
|
|
133
|
+
}
|
|
134
|
+
const browser = (process.env.GEOMETRA_BROWSER ?? '').toLowerCase();
|
|
135
|
+
return browser === 'stealth' || browser === 'cloak' || browser === 'cloakbrowser';
|
|
136
|
+
}
|
|
137
|
+
export function resolveStealthMode(stealth) {
|
|
138
|
+
return stealth ?? envRequestsStealth();
|
|
139
|
+
}
|
|
128
140
|
export async function startEmbeddedGeometraProxy(opts) {
|
|
129
141
|
const runtimePath = resolveProxyRuntimePath();
|
|
130
142
|
const runtimeModule = await import(pathToFileURL(runtimePath).href);
|
|
@@ -138,6 +150,7 @@ export async function startEmbeddedGeometraProxy(opts) {
|
|
|
138
150
|
height: opts.height,
|
|
139
151
|
headed: opts.headless !== true,
|
|
140
152
|
slowMo: opts.slowMo,
|
|
153
|
+
...(opts.stealth !== undefined && { stealth: opts.stealth }),
|
|
141
154
|
eagerInitialExtract: opts.eagerInitialExtract,
|
|
142
155
|
...(opts.proxy && { proxy: opts.proxy }),
|
|
143
156
|
});
|
|
@@ -168,6 +181,9 @@ export function formatProxyStartupFailure(message, opts) {
|
|
|
168
181
|
if (/Executable doesn't exist|playwright install chromium|browserType\.launch/i.test(message)) {
|
|
169
182
|
hints.push('Install Chromium with: npx playwright install chromium');
|
|
170
183
|
}
|
|
184
|
+
if (/cloakbrowser|CLOAKBROWSER|ERR_MODULE_NOT_FOUND|Cannot find package/i.test(message)) {
|
|
185
|
+
hints.push('Stealth mode uses CloakBrowser. Install dependencies with npm install, or disable stealth with stealth=false / GEOMETRA_STEALTH=0. To prefetch the patched Chromium binary, run: npx cloakbrowser install');
|
|
186
|
+
}
|
|
171
187
|
if (opts.port > 0 && /EADDRINUSE|address already in use/i.test(message)) {
|
|
172
188
|
hints.push(`Requested port ${opts.port} is unavailable. Omit the port to use an ephemeral OS-assigned port, or choose another local port.`);
|
|
173
189
|
}
|
|
@@ -191,6 +207,10 @@ export function spawnGeometraProxy(opts) {
|
|
|
191
207
|
args.push('--headless');
|
|
192
208
|
else if (opts.headless === false)
|
|
193
209
|
args.push('--headed');
|
|
210
|
+
if (opts.stealth === true)
|
|
211
|
+
args.push('--stealth');
|
|
212
|
+
else if (opts.stealth === false)
|
|
213
|
+
args.push('--no-stealth');
|
|
194
214
|
if (opts.eagerInitialExtract === false)
|
|
195
215
|
args.push('--lazy-initial-extract');
|
|
196
216
|
if (opts.proxy?.server) {
|
package/dist/server.js
CHANGED
|
@@ -17,6 +17,12 @@ function detailInput() {
|
|
|
17
17
|
.default('minimal')
|
|
18
18
|
.describe('`terse` returns compact machine-friendly JSON. `minimal` (default) returns short human-readable summaries. `verbose` adds fuller fallback context.');
|
|
19
19
|
}
|
|
20
|
+
function stealthInput() {
|
|
21
|
+
return z
|
|
22
|
+
.boolean()
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Launch CloakBrowser stealth Chromium for proxy-backed pageUrl sessions. Default false unless GEOMETRA_STEALTH=1 or GEOMETRA_BROWSER=stealth is set.');
|
|
25
|
+
}
|
|
20
26
|
function formSchemaFormatInput() {
|
|
21
27
|
return z
|
|
22
28
|
.enum(['compact', 'packed'])
|
|
@@ -117,6 +123,68 @@ const geometraWaitForResumeParseInputSchema = z
|
|
|
117
123
|
})
|
|
118
124
|
.strict();
|
|
119
125
|
const timeoutMsInput = z.number().int().min(50).max(60_000).optional();
|
|
126
|
+
const HOST_SAFE_TOOL_TIMEOUT_MS = 20_000;
|
|
127
|
+
const HOST_SAFE_TIMEOUT_RESERVE_MS = 750;
|
|
128
|
+
const HOST_SAFE_REVEAL_TIMEOUT_MS = 8_000;
|
|
129
|
+
const MIN_ACTION_TIMEOUT_MS = 50;
|
|
130
|
+
function softTimeoutMsInput() {
|
|
131
|
+
return z
|
|
132
|
+
.number()
|
|
133
|
+
.int()
|
|
134
|
+
.min(1_000)
|
|
135
|
+
.max(55_000)
|
|
136
|
+
.optional()
|
|
137
|
+
.default(HOST_SAFE_TOOL_TIMEOUT_MS)
|
|
138
|
+
.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.');
|
|
139
|
+
}
|
|
140
|
+
function remainingUntil(deadlineAt) {
|
|
141
|
+
return Math.max(0, Math.floor(deadlineAt - performance.now()));
|
|
142
|
+
}
|
|
143
|
+
function hasSoftBudget(deadlineAt, reserveMs = HOST_SAFE_TIMEOUT_RESERVE_MS) {
|
|
144
|
+
return remainingUntil(deadlineAt) > reserveMs;
|
|
145
|
+
}
|
|
146
|
+
function timeoutCapFromDeadline(deadlineAt, reserveMs = HOST_SAFE_TIMEOUT_RESERVE_MS) {
|
|
147
|
+
return Math.max(MIN_ACTION_TIMEOUT_MS, remainingUntil(deadlineAt) - reserveMs);
|
|
148
|
+
}
|
|
149
|
+
function capTimeoutMs(timeoutMs, capMs, fallbackMs) {
|
|
150
|
+
return Math.max(MIN_ACTION_TIMEOUT_MS, Math.min(timeoutMs ?? fallbackMs, Math.max(MIN_ACTION_TIMEOUT_MS, Math.floor(capMs))));
|
|
151
|
+
}
|
|
152
|
+
function capFillFieldTimeout(field, capMs, fallbackMs = 5_000) {
|
|
153
|
+
return {
|
|
154
|
+
...field,
|
|
155
|
+
timeoutMs: capTimeoutMs(field.timeoutMs, capMs, fallbackMs),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function capBatchActionTimeouts(action, capMs) {
|
|
159
|
+
switch (action.type) {
|
|
160
|
+
case 'click':
|
|
161
|
+
return {
|
|
162
|
+
...action,
|
|
163
|
+
timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 5_000),
|
|
164
|
+
revealTimeoutMs: capTimeoutMs(action.revealTimeoutMs, capMs, 2_500),
|
|
165
|
+
...(action.waitFor ? { waitFor: { ...action.waitFor, timeoutMs: capTimeoutMs(action.waitFor.timeoutMs, capMs, 10_000) } } : {}),
|
|
166
|
+
};
|
|
167
|
+
case 'type':
|
|
168
|
+
case 'key':
|
|
169
|
+
case 'select_option':
|
|
170
|
+
case 'set_checked':
|
|
171
|
+
case 'wheel':
|
|
172
|
+
return { ...action, timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 5_000) };
|
|
173
|
+
case 'upload_files':
|
|
174
|
+
return { ...action, timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 8_000) };
|
|
175
|
+
case 'pick_listbox_option':
|
|
176
|
+
return { ...action, timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 4_500) };
|
|
177
|
+
case 'wait_for':
|
|
178
|
+
return { ...action, timeoutMs: capTimeoutMs(action.timeoutMs, capMs, 10_000) };
|
|
179
|
+
case 'fill_fields':
|
|
180
|
+
return {
|
|
181
|
+
...action,
|
|
182
|
+
fields: action.fields.map(field => capFillFieldTimeout(field, capMs)),
|
|
183
|
+
};
|
|
184
|
+
case 'expand_section':
|
|
185
|
+
return action;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
120
188
|
const fillFieldSchema = z.union([
|
|
121
189
|
z.object({
|
|
122
190
|
kind: z.literal('text'),
|
|
@@ -339,7 +407,7 @@ export function createServer() {
|
|
|
339
407
|
|
|
340
408
|
Use \`url\` (ws://…) only when a Geometra/native server or an already-running proxy is listening. If you accidentally pass \`https://…\` in \`url\`, MCP treats it like \`pageUrl\` and starts the proxy for you.
|
|
341
409
|
|
|
342
|
-
Chromium opens **visible** by default unless \`headless: true\`. File upload / wheel / native \`<select>\` need the proxy path (\`pageUrl\` or ws to proxy). Set \`returnForms: true\` and/or \`returnPageModel: true\` when you want a lower-turn startup response. When connect first-response latency matters more than inlining the page model, pair \`returnPageModel: true\` with \`pageModelMode: "deferred"\` and call \`geometra_page_model\` next.
|
|
410
|
+
Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth: true\` (or set \`GEOMETRA_STEALTH=1\`) to launch CloakBrowser's patched Chromium instead of stock Playwright Chromium. File upload / wheel / native \`<select>\` need the proxy path (\`pageUrl\` or ws to proxy). Set \`returnForms: true\` and/or \`returnPageModel: true\` when you want a lower-turn startup response. When connect first-response latency matters more than inlining the page model, pair \`returnPageModel: true\` with \`pageModelMode: "deferred"\` and call \`geometra_page_model\` next.
|
|
343
411
|
|
|
344
412
|
**Parallelism:** by default, geometra MCP pools and reuses Chromium instances across sessions for speed. That pooling is safe for read-only exploration, but it shares localStorage / cookies / page state across whichever sessions land on the same proxy — which means **two parallel form-submission flows can contaminate each other** (one job's email/autocomplete state leaks into another, or worse, two agents end up driving the same browser tab). For parallel apply / form submission, pass \`isolated: true\`. Each isolated session gets its own brand-new Chromium that is destroyed on disconnect, never enters the pool, and is guaranteed independent of every other session. The cost is ~1–2s of extra startup vs the ~50ms reusable-proxy attach.`, {
|
|
345
413
|
url: z
|
|
@@ -371,6 +439,7 @@ Chromium opens **visible** by default unless \`headless: true\`. File upload / w
|
|
|
371
439
|
.nonnegative()
|
|
372
440
|
.optional()
|
|
373
441
|
.describe('Playwright slowMo (ms) on spawned proxy for easier visual following.'),
|
|
442
|
+
stealth: stealthInput(),
|
|
374
443
|
isolated: z
|
|
375
444
|
.boolean()
|
|
376
445
|
.optional()
|
|
@@ -444,6 +513,7 @@ Chromium opens **visible** by default unless \`headless: true\`. File upload / w
|
|
|
444
513
|
width: input.width,
|
|
445
514
|
height: input.height,
|
|
446
515
|
slowMo: input.slowMo,
|
|
516
|
+
...(input.stealth !== undefined && { stealth: input.stealth }),
|
|
447
517
|
isolated: input.isolated,
|
|
448
518
|
proxy: input.proxy,
|
|
449
519
|
awaitInitialFrame: deferInlinePageModel ? false : undefined,
|
|
@@ -516,6 +586,7 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
|
|
|
516
586
|
.nonnegative()
|
|
517
587
|
.optional()
|
|
518
588
|
.describe('Playwright slowMo (ms) for the warmed browser.'),
|
|
589
|
+
stealth: stealthInput(),
|
|
519
590
|
proxy: z
|
|
520
591
|
.object({
|
|
521
592
|
server: z
|
|
@@ -530,9 +601,18 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
|
|
|
530
601
|
})
|
|
531
602
|
.optional()
|
|
532
603
|
.describe('BYO outbound proxy for the warmed Chromium. The pool entry is partitioned by proxy identity, so a later geometra_connect with the same proxy config will reuse this warmed browser; a different proxy config (or no proxy) will not.'),
|
|
533
|
-
}, async ({ pageUrl, port, headless, width, height, slowMo, proxy }) => {
|
|
604
|
+
}, async ({ pageUrl, port, headless, width, height, slowMo, stealth, proxy }) => {
|
|
534
605
|
try {
|
|
535
|
-
const prepared = await prewarmProxy({
|
|
606
|
+
const prepared = await prewarmProxy({
|
|
607
|
+
pageUrl,
|
|
608
|
+
port,
|
|
609
|
+
headless,
|
|
610
|
+
width,
|
|
611
|
+
height,
|
|
612
|
+
slowMo,
|
|
613
|
+
...(stealth !== undefined && { stealth }),
|
|
614
|
+
proxy,
|
|
615
|
+
});
|
|
536
616
|
return ok(JSON.stringify(prepared));
|
|
537
617
|
}
|
|
538
618
|
catch (e) {
|
|
@@ -899,6 +979,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
899
979
|
width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
|
|
900
980
|
height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
|
|
901
981
|
slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
|
|
982
|
+
stealth: stealthInput(),
|
|
902
983
|
formId: z.string().optional().describe('Optional form id from geometra_form_schema or geometra_page_model'),
|
|
903
984
|
valuesById: formValuesRecordSchema.optional().describe('Form values keyed by stable field id from geometra_form_schema'),
|
|
904
985
|
valuesByLabel: formValuesRecordSchema.optional().describe('Form values keyed by schema field label'),
|
|
@@ -936,7 +1017,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
936
1017
|
.describe('When auto-connecting via pageUrl/url, request an isolated proxy (own brand-new Chromium, destroyed on disconnect). Required for safe parallel form submission. See geometra_connect for details. Ignored when reusing an existing sessionId — set isolated on the original geometra_connect for that case.'),
|
|
937
1018
|
detail: detailInput(),
|
|
938
1019
|
sessionId: sessionIdInput,
|
|
939
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, formId, valuesById, valuesByLabel, stopOnError, failOnInvalid, includeSteps, resumeFromIndex, verifyFills, skipPreFilled, isolated, detail, sessionId }) => {
|
|
1020
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, formId, valuesById, valuesByLabel, stopOnError, failOnInvalid, includeSteps, resumeFromIndex, verifyFills, skipPreFilled, isolated, detail, sessionId }) => {
|
|
940
1021
|
const directFields = !includeSteps && !formId && Object.keys(valuesById ?? {}).length === 0
|
|
941
1022
|
? directLabelBatchFields(valuesByLabel)
|
|
942
1023
|
: null;
|
|
@@ -949,6 +1030,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
949
1030
|
width,
|
|
950
1031
|
height,
|
|
951
1032
|
slowMo,
|
|
1033
|
+
stealth,
|
|
952
1034
|
isolated,
|
|
953
1035
|
awaitInitialFrame: directFields ? false : undefined,
|
|
954
1036
|
}, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_fill_form.');
|
|
@@ -1208,6 +1290,7 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
|
|
|
1208
1290
|
width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
|
|
1209
1291
|
height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
|
|
1210
1292
|
slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
|
|
1293
|
+
stealth: stealthInput(),
|
|
1211
1294
|
isolated: z.boolean().optional().default(false).describe('When auto-connecting via pageUrl/url, request an isolated proxy. Required for safe parallel form submission.'),
|
|
1212
1295
|
formId: z.string().optional().describe('Optional form id from geometra_form_schema or geometra_page_model'),
|
|
1213
1296
|
valuesById: formValuesRecordSchema.optional().describe('Form values keyed by stable field id from geometra_form_schema'),
|
|
@@ -1217,24 +1300,44 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
|
|
|
1217
1300
|
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
1301
|
waitFor: z.object(waitConditionShape()).optional().describe('Optional semantic condition to wait for after the submit click (success banner, navigation, submit gone, etc.)'),
|
|
1219
1302
|
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.'),
|
|
1303
|
+
softTimeoutMs: softTimeoutMsInput(),
|
|
1220
1304
|
failOnInvalid: z.boolean().optional().default(false).describe('Return an error if invalid fields remain after the submit wait resolves.'),
|
|
1221
1305
|
detail: detailInput(),
|
|
1222
1306
|
sessionId: sessionIdInput,
|
|
1223
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, formId, valuesById, valuesByLabel, submit, submitIndex, submitTimeoutMs, waitFor, skipFill, failOnInvalid, detail, sessionId }) => {
|
|
1224
|
-
const
|
|
1307
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, isolated, formId, valuesById, valuesByLabel, submit, submitIndex, submitTimeoutMs, waitFor, skipFill, softTimeoutMs, failOnInvalid, detail, sessionId }) => {
|
|
1308
|
+
const toolStartedAt = performance.now();
|
|
1309
|
+
const effectiveSoftTimeoutMs = softTimeoutMs ?? HOST_SAFE_TOOL_TIMEOUT_MS;
|
|
1310
|
+
const deadlineAt = toolStartedAt + effectiveSoftTimeoutMs;
|
|
1311
|
+
const resolved = await ensureToolSession({ sessionId, url, pageUrl, port, headless, width, height, slowMo, stealth, isolated }, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_submit_form.');
|
|
1225
1312
|
if (!resolved.ok)
|
|
1226
1313
|
return err(resolved.error);
|
|
1227
1314
|
const session = resolved.session;
|
|
1228
1315
|
const connection = autoConnectionPayload(resolved);
|
|
1316
|
+
let fillSummary;
|
|
1317
|
+
let fillFallback;
|
|
1318
|
+
const pausedPayload = (phase, resumeHint, extra) => ({
|
|
1319
|
+
...connection,
|
|
1320
|
+
completed: false,
|
|
1321
|
+
paused: true,
|
|
1322
|
+
phase,
|
|
1323
|
+
pauseReason: 'soft-timeout',
|
|
1324
|
+
softTimeoutMs: effectiveSoftTimeoutMs,
|
|
1325
|
+
elapsedMs: Number((performance.now() - toolStartedAt).toFixed(1)),
|
|
1326
|
+
...(resumeHint ? { resumeHint } : {}),
|
|
1327
|
+
...(fillSummary ? { fill: fillSummary } : {}),
|
|
1328
|
+
...(fillFallback ? { fill_fallback: fillFallback } : {}),
|
|
1329
|
+
...(extra ?? {}),
|
|
1330
|
+
});
|
|
1229
1331
|
if (!session.tree || !session.layout) {
|
|
1230
|
-
await waitForUiCondition(session, () => Boolean(session.tree && session.layout), 2_000);
|
|
1332
|
+
await waitForUiCondition(session, () => Boolean(session.tree && session.layout), capTimeoutMs(2_000, timeoutCapFromDeadline(deadlineAt), 2_000));
|
|
1231
1333
|
}
|
|
1232
1334
|
const entryA11y = sessionA11y(session);
|
|
1233
1335
|
if (!entryA11y)
|
|
1234
1336
|
return err('No UI tree available for form submission');
|
|
1235
1337
|
const entryUrl = entryA11y.meta?.pageUrl;
|
|
1236
|
-
|
|
1237
|
-
|
|
1338
|
+
if (!hasSoftBudget(deadlineAt)) {
|
|
1339
|
+
return ok(JSON.stringify(pausedPayload('before-fill', { retrySameCall: true }), null, detail === 'verbose' ? 2 : undefined));
|
|
1340
|
+
}
|
|
1238
1341
|
if (!skipFill) {
|
|
1239
1342
|
const entryCount = Object.keys(valuesById ?? {}).length + Object.keys(valuesByLabel ?? {}).length;
|
|
1240
1343
|
if (entryCount === 0) {
|
|
@@ -1253,16 +1356,26 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
|
|
|
1253
1356
|
let usedBatch = true;
|
|
1254
1357
|
try {
|
|
1255
1358
|
const startRevision = session.updateRevision;
|
|
1256
|
-
const wait = await sendFillFields(session, planned.fields);
|
|
1359
|
+
const wait = await sendFillFields(session, planned.fields.map(field => capFillFieldTimeout(field, timeoutCapFromDeadline(deadlineAt))), capTimeoutMs(undefined, timeoutCapFromDeadline(deadlineAt), HOST_SAFE_TOOL_TIMEOUT_MS));
|
|
1257
1360
|
const ack = parseProxyFillAckResult(wait.result);
|
|
1258
1361
|
await waitForDeferredBatchUpdate(session, startRevision, wait);
|
|
1259
1362
|
fillSummary = {
|
|
1260
1363
|
formId: schema.formId,
|
|
1261
1364
|
execution: 'batched',
|
|
1262
1365
|
fieldCount: planned.fields.length,
|
|
1366
|
+
...waitStatusPayload(wait),
|
|
1263
1367
|
...(ack ? { invalidCount: ack.invalidCount, alertCount: ack.alertCount } : {}),
|
|
1264
1368
|
...(entryCount !== planned.fields.length ? { requestedValueCount: entryCount } : {}),
|
|
1265
1369
|
};
|
|
1370
|
+
if (wait.status === 'timed_out') {
|
|
1371
|
+
return ok(JSON.stringify(pausedPayload('fill', {
|
|
1372
|
+
tool: 'geometra_submit_form',
|
|
1373
|
+
skipFill: true,
|
|
1374
|
+
submit: submit ?? { role: 'button', name: 'Submit' },
|
|
1375
|
+
submitIndex,
|
|
1376
|
+
...(waitFor ? { waitFor } : {}),
|
|
1377
|
+
}), null, detail === 'verbose' ? 2 : undefined));
|
|
1378
|
+
}
|
|
1266
1379
|
}
|
|
1267
1380
|
catch (e) {
|
|
1268
1381
|
if (!canFallbackToSequentialFill(e)) {
|
|
@@ -1276,8 +1389,24 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
|
|
|
1276
1389
|
let successCount = 0;
|
|
1277
1390
|
let firstErr;
|
|
1278
1391
|
for (const field of planned.fields) {
|
|
1392
|
+
if (!hasSoftBudget(deadlineAt)) {
|
|
1393
|
+
fillSummary = {
|
|
1394
|
+
formId: schema.formId,
|
|
1395
|
+
execution: 'sequential',
|
|
1396
|
+
fieldCount: planned.fields.length,
|
|
1397
|
+
successCount,
|
|
1398
|
+
...(entryCount !== planned.fields.length ? { requestedValueCount: entryCount } : {}),
|
|
1399
|
+
};
|
|
1400
|
+
return ok(JSON.stringify(pausedPayload('fill', {
|
|
1401
|
+
tool: 'geometra_submit_form',
|
|
1402
|
+
skipFill: true,
|
|
1403
|
+
submit: submit ?? { role: 'button', name: 'Submit' },
|
|
1404
|
+
submitIndex,
|
|
1405
|
+
...(waitFor ? { waitFor } : {}),
|
|
1406
|
+
}), null, detail === 'verbose' ? 2 : undefined));
|
|
1407
|
+
}
|
|
1279
1408
|
try {
|
|
1280
|
-
await executeFillField(session, field, detail);
|
|
1409
|
+
await executeFillField(session, capFillFieldTimeout(field, timeoutCapFromDeadline(deadlineAt)), detail);
|
|
1281
1410
|
successCount += 1;
|
|
1282
1411
|
}
|
|
1283
1412
|
catch (e) {
|
|
@@ -1297,12 +1426,22 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
|
|
|
1297
1426
|
};
|
|
1298
1427
|
}
|
|
1299
1428
|
}
|
|
1429
|
+
const submitResumeHint = {
|
|
1430
|
+
tool: 'geometra_submit_form',
|
|
1431
|
+
skipFill: true,
|
|
1432
|
+
submit: submit ?? { role: 'button', name: 'Submit' },
|
|
1433
|
+
submitIndex,
|
|
1434
|
+
...(waitFor ? { waitFor } : {}),
|
|
1435
|
+
};
|
|
1436
|
+
if (!hasSoftBudget(deadlineAt)) {
|
|
1437
|
+
return ok(JSON.stringify(pausedPayload('submit', submitResumeHint), null, detail === 'verbose' ? 2 : undefined));
|
|
1438
|
+
}
|
|
1300
1439
|
const submitFilter = submit ?? { role: 'button', name: 'Submit' };
|
|
1301
1440
|
const resolvedClick = await resolveClickLocationWithFallback(session, {
|
|
1302
1441
|
filter: submitFilter,
|
|
1303
1442
|
index: submitIndex,
|
|
1304
1443
|
fullyVisible: true,
|
|
1305
|
-
revealTimeoutMs: 2_500,
|
|
1444
|
+
revealTimeoutMs: capTimeoutMs(2_500, timeoutCapFromDeadline(deadlineAt), 2_500),
|
|
1306
1445
|
});
|
|
1307
1446
|
if (!resolvedClick.ok) {
|
|
1308
1447
|
const payload = {
|
|
@@ -1315,10 +1454,44 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
|
|
|
1315
1454
|
};
|
|
1316
1455
|
return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
1317
1456
|
}
|
|
1457
|
+
if (!hasSoftBudget(deadlineAt)) {
|
|
1458
|
+
return ok(JSON.stringify(pausedPayload('submit', submitResumeHint), null, detail === 'verbose' ? 2 : undefined));
|
|
1459
|
+
}
|
|
1318
1460
|
const beforeSubmit = sessionA11y(session);
|
|
1319
|
-
const clickWait = await sendClick(session, resolvedClick.value.x, resolvedClick.value.y, submitTimeoutMs);
|
|
1461
|
+
const clickWait = await sendClick(session, resolvedClick.value.x, resolvedClick.value.y, capTimeoutMs(submitTimeoutMs, timeoutCapFromDeadline(deadlineAt), 15_000));
|
|
1320
1462
|
let waitResult;
|
|
1321
1463
|
if (waitFor) {
|
|
1464
|
+
if (!hasSoftBudget(deadlineAt)) {
|
|
1465
|
+
return ok(JSON.stringify(pausedPayload('wait_for', {
|
|
1466
|
+
tool: 'geometra_wait_for',
|
|
1467
|
+
filter: compactFilterPayload({
|
|
1468
|
+
id: waitFor.id,
|
|
1469
|
+
role: waitFor.role,
|
|
1470
|
+
name: waitFor.name,
|
|
1471
|
+
text: waitFor.text,
|
|
1472
|
+
contextText: waitFor.contextText,
|
|
1473
|
+
promptText: waitFor.promptText,
|
|
1474
|
+
sectionText: waitFor.sectionText,
|
|
1475
|
+
itemText: waitFor.itemText,
|
|
1476
|
+
value: waitFor.value,
|
|
1477
|
+
checked: waitFor.checked,
|
|
1478
|
+
disabled: waitFor.disabled,
|
|
1479
|
+
focused: waitFor.focused,
|
|
1480
|
+
selected: waitFor.selected,
|
|
1481
|
+
expanded: waitFor.expanded,
|
|
1482
|
+
invalid: waitFor.invalid,
|
|
1483
|
+
required: waitFor.required,
|
|
1484
|
+
busy: waitFor.busy,
|
|
1485
|
+
}),
|
|
1486
|
+
present: waitFor.present ?? true,
|
|
1487
|
+
}, {
|
|
1488
|
+
submit: {
|
|
1489
|
+
at: { x: resolvedClick.value.x, y: resolvedClick.value.y },
|
|
1490
|
+
...(resolvedClick.value.target ? { target: compactNodeReference(resolvedClick.value.target) } : {}),
|
|
1491
|
+
...waitStatusPayload(clickWait),
|
|
1492
|
+
},
|
|
1493
|
+
}), null, detail === 'verbose' ? 2 : undefined));
|
|
1494
|
+
}
|
|
1322
1495
|
const postWait = await waitForSemanticCondition(session, {
|
|
1323
1496
|
filter: {
|
|
1324
1497
|
id: waitFor.id,
|
|
@@ -1340,7 +1513,7 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
|
|
|
1340
1513
|
busy: waitFor.busy,
|
|
1341
1514
|
},
|
|
1342
1515
|
present: waitFor.present ?? true,
|
|
1343
|
-
timeoutMs: waitFor.timeoutMs
|
|
1516
|
+
timeoutMs: capTimeoutMs(waitFor.timeoutMs, timeoutCapFromDeadline(deadlineAt), 15_000),
|
|
1344
1517
|
});
|
|
1345
1518
|
if (!postWait.ok) {
|
|
1346
1519
|
const preErrFallbacks = [];
|
|
@@ -1420,12 +1593,21 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1420
1593
|
width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
|
|
1421
1594
|
height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
|
|
1422
1595
|
slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
|
|
1596
|
+
stealth: stealthInput(),
|
|
1423
1597
|
isolated: z
|
|
1424
1598
|
.boolean()
|
|
1425
1599
|
.optional()
|
|
1426
1600
|
.default(false)
|
|
1427
1601
|
.describe('When auto-connecting via pageUrl/url, request an isolated proxy. See geometra_connect for details.'),
|
|
1428
1602
|
actions: z.array(batchActionSchema).min(1).max(80).describe('Ordered high-level action steps to run sequentially'),
|
|
1603
|
+
resumeFromIndex: z
|
|
1604
|
+
.number()
|
|
1605
|
+
.int()
|
|
1606
|
+
.min(0)
|
|
1607
|
+
.optional()
|
|
1608
|
+
.default(0)
|
|
1609
|
+
.describe('Resume a previous partial geometra_run_actions result from this action index. Use the returned resumeFromIndex when a call pauses.'),
|
|
1610
|
+
softTimeoutMs: softTimeoutMsInput(),
|
|
1429
1611
|
stopOnError: z.boolean().optional().default(true).describe('Stop at the first failing step (default true)'),
|
|
1430
1612
|
includeSteps: z
|
|
1431
1613
|
.boolean()
|
|
@@ -1435,7 +1617,10 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1435
1617
|
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
1618
|
detail: detailInput(),
|
|
1437
1619
|
sessionId: sessionIdInput,
|
|
1438
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, actions, stopOnError, includeSteps, output, detail, sessionId }) => {
|
|
1620
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, isolated, actions, resumeFromIndex, softTimeoutMs, stopOnError, includeSteps, output, detail, sessionId }) => {
|
|
1621
|
+
const toolStartedAt = performance.now();
|
|
1622
|
+
const effectiveSoftTimeoutMs = softTimeoutMs ?? HOST_SAFE_TOOL_TIMEOUT_MS;
|
|
1623
|
+
const deadlineAt = toolStartedAt + effectiveSoftTimeoutMs;
|
|
1439
1624
|
const resolved = await ensureToolSession({
|
|
1440
1625
|
sessionId,
|
|
1441
1626
|
url,
|
|
@@ -1445,6 +1630,7 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1445
1630
|
width,
|
|
1446
1631
|
height,
|
|
1447
1632
|
slowMo,
|
|
1633
|
+
stealth,
|
|
1448
1634
|
isolated,
|
|
1449
1635
|
awaitInitialFrame: canDeferInitialFrameForRunActions(actions) ? false : undefined,
|
|
1450
1636
|
}, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_run_actions.');
|
|
@@ -1452,23 +1638,36 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1452
1638
|
return err(resolved.error);
|
|
1453
1639
|
const session = resolved.session;
|
|
1454
1640
|
const connection = autoConnectionPayload(resolved);
|
|
1641
|
+
const startIndex = resumeFromIndex ?? 0;
|
|
1642
|
+
if (startIndex > actions.length) {
|
|
1643
|
+
return err(`resumeFromIndex ${startIndex} exceeds actions length ${actions.length}`);
|
|
1644
|
+
}
|
|
1455
1645
|
const steps = [];
|
|
1456
1646
|
let stoppedAt;
|
|
1647
|
+
let pausedAt;
|
|
1457
1648
|
const batchStartedAt = performance.now();
|
|
1458
1649
|
// Collect transparent-fallback signals from each step so run_actions
|
|
1459
1650
|
// surfaces them at top level regardless of `includeSteps` — otherwise
|
|
1460
1651
|
// the telemetry is dead code when callers opt out of the steps listing.
|
|
1461
1652
|
const fallbackRecords = [];
|
|
1462
|
-
for (let index =
|
|
1463
|
-
|
|
1653
|
+
for (let index = startIndex; index < actions.length; index++) {
|
|
1654
|
+
if (!hasSoftBudget(deadlineAt)) {
|
|
1655
|
+
pausedAt = index;
|
|
1656
|
+
break;
|
|
1657
|
+
}
|
|
1658
|
+
const action = capBatchActionTimeouts(actions[index], timeoutCapFromDeadline(deadlineAt));
|
|
1464
1659
|
const startedAt = performance.now();
|
|
1465
1660
|
let uiTreeWaitMs = 0;
|
|
1466
1661
|
try {
|
|
1467
1662
|
if (actionNeedsUiTree(action) && (!session.tree || !session.layout)) {
|
|
1468
1663
|
const uiTreeWaitStartedAt = performance.now();
|
|
1469
|
-
await waitForUiCondition(session, () => Boolean(session.tree && session.layout), 2_000);
|
|
1664
|
+
await waitForUiCondition(session, () => Boolean(session.tree && session.layout), capTimeoutMs(2_000, timeoutCapFromDeadline(deadlineAt), 2_000));
|
|
1470
1665
|
uiTreeWaitMs = performance.now() - uiTreeWaitStartedAt;
|
|
1471
1666
|
}
|
|
1667
|
+
if (!hasSoftBudget(deadlineAt)) {
|
|
1668
|
+
pausedAt = index;
|
|
1669
|
+
break;
|
|
1670
|
+
}
|
|
1472
1671
|
const result = await executeBatchAction(session, action, detail, includeSteps);
|
|
1473
1672
|
const stepFallback = result.compact.fallback;
|
|
1474
1673
|
if (stepFallback?.used) {
|
|
@@ -1511,6 +1710,10 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1511
1710
|
...result.compact,
|
|
1512
1711
|
...(stepSignals ? { signals: stepSignals } : {}),
|
|
1513
1712
|
});
|
|
1713
|
+
if (index + 1 < actions.length && !hasSoftBudget(deadlineAt)) {
|
|
1714
|
+
pausedAt = index + 1;
|
|
1715
|
+
break;
|
|
1716
|
+
}
|
|
1514
1717
|
}
|
|
1515
1718
|
catch (e) {
|
|
1516
1719
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -1534,20 +1737,37 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1534
1737
|
const after = sessionA11y(session);
|
|
1535
1738
|
const successCount = steps.filter(step => step.ok === true).length;
|
|
1536
1739
|
const errorCount = steps.length - successCount;
|
|
1740
|
+
const elapsedMs = Number((performance.now() - toolStartedAt).toFixed(1));
|
|
1741
|
+
const completed = stoppedAt === undefined && pausedAt === undefined && startIndex + steps.length >= actions.length;
|
|
1742
|
+
const resumePayload = {
|
|
1743
|
+
...(startIndex > 0 ? { resumedFromIndex: startIndex } : {}),
|
|
1744
|
+
...(pausedAt !== undefined
|
|
1745
|
+
? {
|
|
1746
|
+
paused: true,
|
|
1747
|
+
pausedAt,
|
|
1748
|
+
resumeFromIndex: pausedAt,
|
|
1749
|
+
pauseReason: 'soft-timeout',
|
|
1750
|
+
softTimeoutMs: effectiveSoftTimeoutMs,
|
|
1751
|
+
elapsedMs,
|
|
1752
|
+
}
|
|
1753
|
+
: {}),
|
|
1754
|
+
};
|
|
1537
1755
|
const payload = output === 'final'
|
|
1538
1756
|
? {
|
|
1539
1757
|
...connection,
|
|
1540
|
-
completed
|
|
1758
|
+
completed,
|
|
1759
|
+
...resumePayload,
|
|
1541
1760
|
...(stoppedAt !== undefined ? { stoppedAt } : {}),
|
|
1542
1761
|
...(fallbackRecords.length > 0 ? { fallbacks: fallbackRecords } : {}),
|
|
1543
1762
|
...(after ? { final: sessionSignalsPayload(collectSessionSignals(after), detail) } : {}),
|
|
1544
1763
|
}
|
|
1545
1764
|
: {
|
|
1546
1765
|
...connection,
|
|
1547
|
-
completed
|
|
1766
|
+
completed,
|
|
1548
1767
|
stepCount: actions.length,
|
|
1549
1768
|
successCount,
|
|
1550
1769
|
errorCount,
|
|
1770
|
+
...resumePayload,
|
|
1551
1771
|
...(includeSteps ? { steps } : {}),
|
|
1552
1772
|
...(stoppedAt !== undefined ? { stoppedAt } : {}),
|
|
1553
1773
|
...(fallbackRecords.length > 0 ? { fallbacks: fallbackRecords } : {}),
|
|
@@ -1603,6 +1823,7 @@ Unlike geometra_expand_section, this collapses repeated radio/button groups into
|
|
|
1603
1823
|
width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
|
|
1604
1824
|
height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
|
|
1605
1825
|
slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
|
|
1826
|
+
stealth: stealthInput(),
|
|
1606
1827
|
isolated: z
|
|
1607
1828
|
.boolean()
|
|
1608
1829
|
.optional()
|
|
@@ -1617,8 +1838,8 @@ Unlike geometra_expand_section, this collapses repeated radio/button groups into
|
|
|
1617
1838
|
sinceSchemaId: z.string().optional().describe('If the current schema matches this id, return changed=false without resending forms'),
|
|
1618
1839
|
format: formSchemaFormatInput(),
|
|
1619
1840
|
sessionId: sessionIdInput,
|
|
1620
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, formId, maxFields, onlyRequiredFields, onlyInvalidFields, includeOptions, includeContext, sinceSchemaId, format, sessionId }) => {
|
|
1621
|
-
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_form_schema.');
|
|
1841
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, isolated, formId, maxFields, onlyRequiredFields, onlyInvalidFields, includeOptions, includeContext, sinceSchemaId, format, sessionId }) => {
|
|
1842
|
+
const resolved = await ensureToolSession({ sessionId, url, pageUrl, port, headless, width, height, slowMo, stealth, isolated }, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_form_schema.');
|
|
1622
1843
|
if (!resolved.ok)
|
|
1623
1844
|
return err(resolved.error);
|
|
1624
1845
|
const session = resolved.session;
|
|
@@ -2655,6 +2876,7 @@ async function ensureToolSession(target, missingConnectionMessage = 'Not connect
|
|
|
2655
2876
|
width: target.width,
|
|
2656
2877
|
height: target.height,
|
|
2657
2878
|
slowMo: target.slowMo,
|
|
2879
|
+
...(target.stealth !== undefined && { stealth: target.stealth }),
|
|
2658
2880
|
awaitInitialFrame: target.awaitInitialFrame,
|
|
2659
2881
|
isolated: target.isolated,
|
|
2660
2882
|
});
|
|
@@ -2994,13 +3216,21 @@ function inferRevealStepBudget(target, viewport) {
|
|
|
2994
3216
|
return clamp(Math.max(6, Math.max(verticalSteps, horizontalSteps) + 1), 6, 48);
|
|
2995
3217
|
}
|
|
2996
3218
|
async function revealSemanticTarget(session, options) {
|
|
2997
|
-
const
|
|
3219
|
+
const revealStartedAt = performance.now();
|
|
3220
|
+
const revealDeadlineAt = revealStartedAt + HOST_SAFE_REVEAL_TIMEOUT_MS;
|
|
3221
|
+
const initialTreeReady = await ensureSessionUiTree(session, capTimeoutMs(Math.max(4_000, options.timeoutMs), timeoutCapFromDeadline(revealDeadlineAt, 100), HOST_SAFE_REVEAL_TIMEOUT_MS));
|
|
2998
3222
|
if (!initialTreeReady) {
|
|
2999
3223
|
return { ok: false, error: 'Timed out waiting for the initial UI tree after connect.' };
|
|
3000
3224
|
}
|
|
3001
3225
|
let attempts = 0;
|
|
3002
3226
|
let stepBudget = options.maxSteps;
|
|
3003
3227
|
while (attempts <= (stepBudget ?? 48)) {
|
|
3228
|
+
if (!hasSoftBudget(revealDeadlineAt, 100)) {
|
|
3229
|
+
return {
|
|
3230
|
+
ok: false,
|
|
3231
|
+
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.`,
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3004
3234
|
const a11y = sessionA11y(session);
|
|
3005
3235
|
if (!a11y)
|
|
3006
3236
|
return { ok: false, error: 'No UI tree available to reveal from' };
|
|
@@ -3046,7 +3276,7 @@ async function revealSemanticTarget(session, options) {
|
|
|
3046
3276
|
deltaX,
|
|
3047
3277
|
x: formatted.center.x,
|
|
3048
3278
|
y: formatted.center.y,
|
|
3049
|
-
}, options.timeoutMs);
|
|
3279
|
+
}, capTimeoutMs(options.timeoutMs, timeoutCapFromDeadline(revealDeadlineAt, 100), 2_500));
|
|
3050
3280
|
attempts++;
|
|
3051
3281
|
}
|
|
3052
3282
|
return { ok: false, error: `Failed to reveal ${JSON.stringify(options.filter)}` };
|
package/dist/session.d.ts
CHANGED
|
@@ -488,6 +488,7 @@ export declare function prewarmProxy(options: {
|
|
|
488
488
|
pageUrl: string;
|
|
489
489
|
port?: number;
|
|
490
490
|
headless?: boolean;
|
|
491
|
+
stealth?: boolean;
|
|
491
492
|
width?: number;
|
|
492
493
|
height?: number;
|
|
493
494
|
slowMo?: number;
|
|
@@ -499,6 +500,7 @@ export declare function prewarmProxy(options: {
|
|
|
499
500
|
pageUrl: string;
|
|
500
501
|
wsUrl: string;
|
|
501
502
|
headless: boolean;
|
|
503
|
+
stealth: boolean;
|
|
502
504
|
width: number;
|
|
503
505
|
height: number;
|
|
504
506
|
}>;
|
|
@@ -524,6 +526,7 @@ export declare function connectThroughProxy(options: {
|
|
|
524
526
|
width?: number;
|
|
525
527
|
height?: number;
|
|
526
528
|
slowMo?: number;
|
|
529
|
+
stealth?: boolean;
|
|
527
530
|
awaitInitialFrame?: boolean;
|
|
528
531
|
eagerInitialExtract?: boolean;
|
|
529
532
|
/**
|
package/dist/session.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { performance } from 'node:perf_hooks';
|
|
3
3
|
import WebSocket from 'ws';
|
|
4
|
-
import { spawnGeometraProxy, startEmbeddedGeometraProxy } from './proxy-spawn.js';
|
|
4
|
+
import { resolveStealthMode, spawnGeometraProxy, startEmbeddedGeometraProxy, } from './proxy-spawn.js';
|
|
5
5
|
import { completeSessionLifecycle, failSessionLifecycle, heartbeatSessionLifecycle, initializeSessionLifecycle, recordSessionSnapshot, } from './session-state.js';
|
|
6
6
|
/**
|
|
7
7
|
* Stable identity for an outbound proxy config, used as the reusable-pool
|
|
@@ -143,10 +143,12 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
143
143
|
clearReusableProxiesIfExited();
|
|
144
144
|
const now = Date.now();
|
|
145
145
|
const proxyKey = proxyKeyFor(opts.proxy);
|
|
146
|
+
const stealth = resolveStealthMode(opts.stealth);
|
|
146
147
|
const existing = reusableProxies.find(entry => sameReusableProxyEntry(entry, proxy));
|
|
147
148
|
if (existing) {
|
|
148
149
|
existing.wsUrl = wsUrl;
|
|
149
150
|
existing.headless = opts.headless === true;
|
|
151
|
+
existing.stealth = stealth;
|
|
150
152
|
existing.slowMo = opts.slowMo ?? 0;
|
|
151
153
|
existing.width = opts.width ?? 1280;
|
|
152
154
|
existing.height = opts.height ?? 720;
|
|
@@ -162,6 +164,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
162
164
|
child,
|
|
163
165
|
wsUrl,
|
|
164
166
|
headless: opts.headless === true,
|
|
167
|
+
stealth,
|
|
165
168
|
slowMo: opts.slowMo ?? 0,
|
|
166
169
|
width: opts.width ?? 1280,
|
|
167
170
|
height: opts.height ?? 720,
|
|
@@ -188,6 +191,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
188
191
|
runtime: proxy.runtime,
|
|
189
192
|
wsUrl,
|
|
190
193
|
headless: opts.headless === true,
|
|
194
|
+
stealth,
|
|
191
195
|
slowMo: opts.slowMo ?? 0,
|
|
192
196
|
width: opts.width ?? 1280,
|
|
193
197
|
height: opts.height ?? 720,
|
|
@@ -405,6 +409,7 @@ function stopSessionHeartbeat(session) {
|
|
|
405
409
|
function reusableProxyMatchesOptions(entry, options) {
|
|
406
410
|
return (entry.pageUrl === options.pageUrl &&
|
|
407
411
|
entry.headless === (options.headless === true) &&
|
|
412
|
+
entry.stealth === resolveStealthMode(options.stealth) &&
|
|
408
413
|
entry.slowMo === (options.slowMo ?? 0) &&
|
|
409
414
|
entry.width === (options.width ?? 1280) &&
|
|
410
415
|
entry.height === (options.height ?? 720) &&
|
|
@@ -422,12 +427,14 @@ function findExactReusableProxy(options) {
|
|
|
422
427
|
function findReusableProxy(options) {
|
|
423
428
|
clearReusableProxiesIfExited();
|
|
424
429
|
const desiredHeadless = options.headless === true;
|
|
430
|
+
const desiredStealth = resolveStealthMode(options.stealth);
|
|
425
431
|
const desiredSlowMo = options.slowMo ?? 0;
|
|
426
432
|
const desiredWidth = options.width ?? 1280;
|
|
427
433
|
const desiredHeight = options.height ?? 720;
|
|
428
434
|
const desiredProxyKey = proxyKeyFor(options.proxy);
|
|
429
435
|
return reusableProxies
|
|
430
436
|
.filter(entry => entry.headless === desiredHeadless
|
|
437
|
+
&& entry.stealth === desiredStealth
|
|
431
438
|
&& entry.slowMo === desiredSlowMo
|
|
432
439
|
// Proxy partition is hard — a session with residential proxy MUST NOT
|
|
433
440
|
// attach to a pooled direct-connection Chromium (and vice versa).
|
|
@@ -459,6 +466,7 @@ export async function prewarmProxy(options) {
|
|
|
459
466
|
pageUrl: options.pageUrl,
|
|
460
467
|
wsUrl: existing.wsUrl,
|
|
461
468
|
headless: options.headless === true,
|
|
469
|
+
stealth: resolveStealthMode(options.stealth),
|
|
462
470
|
width: options.width ?? 1280,
|
|
463
471
|
height: options.height ?? 720,
|
|
464
472
|
};
|
|
@@ -472,6 +480,7 @@ export async function prewarmProxy(options) {
|
|
|
472
480
|
width: options.width,
|
|
473
481
|
height: options.height,
|
|
474
482
|
slowMo: options.slowMo,
|
|
483
|
+
stealth: options.stealth,
|
|
475
484
|
proxy: options.proxy,
|
|
476
485
|
});
|
|
477
486
|
try {
|
|
@@ -484,6 +493,7 @@ export async function prewarmProxy(options) {
|
|
|
484
493
|
setReusableProxy({ runtime }, wsUrl, {
|
|
485
494
|
headless: options.headless,
|
|
486
495
|
slowMo: options.slowMo,
|
|
496
|
+
stealth: options.stealth,
|
|
487
497
|
width: options.width,
|
|
488
498
|
height: options.height,
|
|
489
499
|
pageUrl: options.pageUrl,
|
|
@@ -497,6 +507,7 @@ export async function prewarmProxy(options) {
|
|
|
497
507
|
pageUrl: options.pageUrl,
|
|
498
508
|
wsUrl,
|
|
499
509
|
headless: options.headless === true,
|
|
510
|
+
stealth: resolveStealthMode(options.stealth),
|
|
500
511
|
width: options.width ?? 1280,
|
|
501
512
|
height: options.height ?? 720,
|
|
502
513
|
};
|
|
@@ -512,11 +523,13 @@ export async function prewarmProxy(options) {
|
|
|
512
523
|
width: options.width,
|
|
513
524
|
height: options.height,
|
|
514
525
|
slowMo: options.slowMo,
|
|
526
|
+
stealth: options.stealth,
|
|
515
527
|
proxy: options.proxy,
|
|
516
528
|
});
|
|
517
529
|
setReusableProxy({ child }, wsUrl, {
|
|
518
530
|
headless: options.headless,
|
|
519
531
|
slowMo: options.slowMo,
|
|
532
|
+
stealth: options.stealth,
|
|
520
533
|
width: options.width,
|
|
521
534
|
height: options.height,
|
|
522
535
|
pageUrl: options.pageUrl,
|
|
@@ -529,6 +542,7 @@ export async function prewarmProxy(options) {
|
|
|
529
542
|
pageUrl: options.pageUrl,
|
|
530
543
|
wsUrl,
|
|
531
544
|
headless: options.headless === true,
|
|
545
|
+
stealth: resolveStealthMode(options.stealth),
|
|
532
546
|
width: options.width ?? 1280,
|
|
533
547
|
height: options.height ?? 720,
|
|
534
548
|
};
|
|
@@ -621,6 +635,7 @@ async function startFreshProxySession(options) {
|
|
|
621
635
|
width: options.width,
|
|
622
636
|
height: options.height,
|
|
623
637
|
slowMo: options.slowMo,
|
|
638
|
+
stealth: options.stealth,
|
|
624
639
|
eagerInitialExtract,
|
|
625
640
|
proxy: options.proxy,
|
|
626
641
|
});
|
|
@@ -649,6 +664,7 @@ async function startFreshProxySession(options) {
|
|
|
649
664
|
setReusableProxy({ runtime }, wsUrl, {
|
|
650
665
|
headless: options.headless,
|
|
651
666
|
slowMo: options.slowMo,
|
|
667
|
+
stealth: options.stealth,
|
|
652
668
|
width: options.width,
|
|
653
669
|
height: options.height,
|
|
654
670
|
pageUrl: options.pageUrl,
|
|
@@ -695,6 +711,7 @@ async function startFreshProxySession(options) {
|
|
|
695
711
|
width: options.width,
|
|
696
712
|
height: options.height,
|
|
697
713
|
slowMo: options.slowMo,
|
|
714
|
+
stealth: options.stealth,
|
|
698
715
|
eagerInitialExtract,
|
|
699
716
|
proxy: options.proxy,
|
|
700
717
|
});
|
|
@@ -717,6 +734,7 @@ async function startFreshProxySession(options) {
|
|
|
717
734
|
setReusableProxy({ child }, wsUrl, {
|
|
718
735
|
headless: options.headless,
|
|
719
736
|
slowMo: options.slowMo,
|
|
737
|
+
stealth: options.stealth,
|
|
720
738
|
width: options.width,
|
|
721
739
|
height: options.height,
|
|
722
740
|
pageUrl: options.pageUrl,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geometra/mcp",
|
|
3
|
-
"version": "1.61.
|
|
3
|
+
"version": "1.61.3",
|
|
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.3",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
37
37
|
"@razroo/parallel-mcp": "^0.1.0",
|
|
38
38
|
"ws": "^8.18.0",
|