@geometra/mcp 1.61.2 → 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'])
@@ -401,7 +407,7 @@ export function createServer() {
401
407
 
402
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.
403
409
 
404
- 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.
405
411
 
406
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.`, {
407
413
  url: z
@@ -433,6 +439,7 @@ Chromium opens **visible** by default unless \`headless: true\`. File upload / w
433
439
  .nonnegative()
434
440
  .optional()
435
441
  .describe('Playwright slowMo (ms) on spawned proxy for easier visual following.'),
442
+ stealth: stealthInput(),
436
443
  isolated: z
437
444
  .boolean()
438
445
  .optional()
@@ -506,6 +513,7 @@ Chromium opens **visible** by default unless \`headless: true\`. File upload / w
506
513
  width: input.width,
507
514
  height: input.height,
508
515
  slowMo: input.slowMo,
516
+ ...(input.stealth !== undefined && { stealth: input.stealth }),
509
517
  isolated: input.isolated,
510
518
  proxy: input.proxy,
511
519
  awaitInitialFrame: deferInlinePageModel ? false : undefined,
@@ -578,6 +586,7 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
578
586
  .nonnegative()
579
587
  .optional()
580
588
  .describe('Playwright slowMo (ms) for the warmed browser.'),
589
+ stealth: stealthInput(),
581
590
  proxy: z
582
591
  .object({
583
592
  server: z
@@ -592,9 +601,18 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
592
601
  })
593
602
  .optional()
594
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.'),
595
- }, async ({ pageUrl, port, headless, width, height, slowMo, proxy }) => {
604
+ }, async ({ pageUrl, port, headless, width, height, slowMo, stealth, proxy }) => {
596
605
  try {
597
- 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
+ });
598
616
  return ok(JSON.stringify(prepared));
599
617
  }
600
618
  catch (e) {
@@ -961,6 +979,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
961
979
  width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
962
980
  height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
963
981
  slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
982
+ stealth: stealthInput(),
964
983
  formId: z.string().optional().describe('Optional form id from geometra_form_schema or geometra_page_model'),
965
984
  valuesById: formValuesRecordSchema.optional().describe('Form values keyed by stable field id from geometra_form_schema'),
966
985
  valuesByLabel: formValuesRecordSchema.optional().describe('Form values keyed by schema field label'),
@@ -998,7 +1017,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
998
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.'),
999
1018
  detail: detailInput(),
1000
1019
  sessionId: sessionIdInput,
1001
- }, 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 }) => {
1002
1021
  const directFields = !includeSteps && !formId && Object.keys(valuesById ?? {}).length === 0
1003
1022
  ? directLabelBatchFields(valuesByLabel)
1004
1023
  : null;
@@ -1011,6 +1030,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
1011
1030
  width,
1012
1031
  height,
1013
1032
  slowMo,
1033
+ stealth,
1014
1034
  isolated,
1015
1035
  awaitInitialFrame: directFields ? false : undefined,
1016
1036
  }, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_fill_form.');
@@ -1270,6 +1290,7 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
1270
1290
  width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
1271
1291
  height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
1272
1292
  slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
1293
+ stealth: stealthInput(),
1273
1294
  isolated: z.boolean().optional().default(false).describe('When auto-connecting via pageUrl/url, request an isolated proxy. Required for safe parallel form submission.'),
1274
1295
  formId: z.string().optional().describe('Optional form id from geometra_form_schema or geometra_page_model'),
1275
1296
  valuesById: formValuesRecordSchema.optional().describe('Form values keyed by stable field id from geometra_form_schema'),
@@ -1283,11 +1304,11 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
1283
1304
  failOnInvalid: z.boolean().optional().default(false).describe('Return an error if invalid fields remain after the submit wait resolves.'),
1284
1305
  detail: detailInput(),
1285
1306
  sessionId: sessionIdInput,
1286
- }, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, formId, valuesById, valuesByLabel, submit, submitIndex, submitTimeoutMs, waitFor, skipFill, softTimeoutMs, failOnInvalid, detail, sessionId }) => {
1307
+ }, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, isolated, formId, valuesById, valuesByLabel, submit, submitIndex, submitTimeoutMs, waitFor, skipFill, softTimeoutMs, failOnInvalid, detail, sessionId }) => {
1287
1308
  const toolStartedAt = performance.now();
1288
1309
  const effectiveSoftTimeoutMs = softTimeoutMs ?? HOST_SAFE_TOOL_TIMEOUT_MS;
1289
1310
  const deadlineAt = toolStartedAt + effectiveSoftTimeoutMs;
1290
- const resolved = await ensureToolSession({ sessionId, url, pageUrl, port, headless, width, height, slowMo, isolated }, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_submit_form.');
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.');
1291
1312
  if (!resolved.ok)
1292
1313
  return err(resolved.error);
1293
1314
  const session = resolved.session;
@@ -1572,6 +1593,7 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
1572
1593
  width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
1573
1594
  height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
1574
1595
  slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
1596
+ stealth: stealthInput(),
1575
1597
  isolated: z
1576
1598
  .boolean()
1577
1599
  .optional()
@@ -1595,7 +1617,7 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
1595
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.'),
1596
1618
  detail: detailInput(),
1597
1619
  sessionId: sessionIdInput,
1598
- }, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, actions, resumeFromIndex, softTimeoutMs, stopOnError, includeSteps, output, detail, sessionId }) => {
1620
+ }, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, isolated, actions, resumeFromIndex, softTimeoutMs, stopOnError, includeSteps, output, detail, sessionId }) => {
1599
1621
  const toolStartedAt = performance.now();
1600
1622
  const effectiveSoftTimeoutMs = softTimeoutMs ?? HOST_SAFE_TOOL_TIMEOUT_MS;
1601
1623
  const deadlineAt = toolStartedAt + effectiveSoftTimeoutMs;
@@ -1608,6 +1630,7 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
1608
1630
  width,
1609
1631
  height,
1610
1632
  slowMo,
1633
+ stealth,
1611
1634
  isolated,
1612
1635
  awaitInitialFrame: canDeferInitialFrameForRunActions(actions) ? false : undefined,
1613
1636
  }, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_run_actions.');
@@ -1800,6 +1823,7 @@ Unlike geometra_expand_section, this collapses repeated radio/button groups into
1800
1823
  width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
1801
1824
  height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
1802
1825
  slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
1826
+ stealth: stealthInput(),
1803
1827
  isolated: z
1804
1828
  .boolean()
1805
1829
  .optional()
@@ -1814,8 +1838,8 @@ Unlike geometra_expand_section, this collapses repeated radio/button groups into
1814
1838
  sinceSchemaId: z.string().optional().describe('If the current schema matches this id, return changed=false without resending forms'),
1815
1839
  format: formSchemaFormatInput(),
1816
1840
  sessionId: sessionIdInput,
1817
- }, async ({ url, pageUrl, port, headless, width, height, slowMo, isolated, formId, maxFields, onlyRequiredFields, onlyInvalidFields, includeOptions, includeContext, sinceSchemaId, format, sessionId }) => {
1818
- 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.');
1819
1843
  if (!resolved.ok)
1820
1844
  return err(resolved.error);
1821
1845
  const session = resolved.session;
@@ -2852,6 +2876,7 @@ async function ensureToolSession(target, missingConnectionMessage = 'Not connect
2852
2876
  width: target.width,
2853
2877
  height: target.height,
2854
2878
  slowMo: target.slowMo,
2879
+ ...(target.stealth !== undefined && { stealth: target.stealth }),
2855
2880
  awaitInitialFrame: target.awaitInitialFrame,
2856
2881
  isolated: target.isolated,
2857
2882
  });
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.2",
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.2",
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",