@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 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 headless workflows do not immediately evict each other.
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; best when the agent can prepare before the user-facing task starts |
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`
@@ -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;
@@ -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({ pageUrl, port, headless, width, height, slowMo, proxy });
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 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.');
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
- let fillSummary;
1237
- let fillFallback;
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 ?? 15_000,
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 = 0; index < actions.length; index++) {
1463
- const action = actions[index];
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: stoppedAt === undefined && steps.length === actions.length,
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: stoppedAt === undefined && steps.length === actions.length,
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 initialTreeReady = await ensureSessionUiTree(session, Math.max(4_000, options.timeoutMs));
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.1",
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.1",
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",