@geometra/mcp 1.58.0 → 1.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,19 @@ export declare function resolveProxyScriptPath(): string;
11
11
  export declare function resolveProxyScriptPathWith(customRequire: NodeRequire, moduleDir?: string): string;
12
12
  export declare function resolveProxyRuntimePath(): string;
13
13
  export declare function resolveProxyRuntimePathWith(customRequire: NodeRequire, moduleDir?: string): string;
14
+ /**
15
+ * BYO outbound proxy for the spawned Chromium. JobForge sets this when the
16
+ * user configures a residential / mobile / SOCKS proxy in `profile.yml` to
17
+ * bypass datacenter-IP fingerprinting on apply portals (Ashby class B,
18
+ * Lever Mapbox geocoder, Cloudflare Bot Management, etc.). Geometra is the
19
+ * wire — the user supplies the proxy.
20
+ */
21
+ export interface SpawnProxyConfig {
22
+ server: string;
23
+ username?: string;
24
+ password?: string;
25
+ bypass?: string;
26
+ }
14
27
  export interface SpawnProxyParams {
15
28
  pageUrl: string;
16
29
  port: number;
@@ -19,6 +32,7 @@ export interface SpawnProxyParams {
19
32
  height?: number;
20
33
  slowMo?: number;
21
34
  eagerInitialExtract?: boolean;
35
+ proxy?: SpawnProxyConfig;
22
36
  }
23
37
  export declare function startEmbeddedGeometraProxy(opts: SpawnProxyParams): Promise<{
24
38
  runtime: EmbeddedProxyRuntime;
@@ -139,6 +139,7 @@ export async function startEmbeddedGeometraProxy(opts) {
139
139
  headed: opts.headless !== true,
140
140
  slowMo: opts.slowMo,
141
141
  eagerInitialExtract: opts.eagerInitialExtract,
142
+ ...(opts.proxy && { proxy: opts.proxy }),
142
143
  });
143
144
  return { runtime, wsUrl: runtime.wsUrl };
144
145
  }
@@ -192,6 +193,15 @@ export function spawnGeometraProxy(opts) {
192
193
  args.push('--headed');
193
194
  if (opts.eagerInitialExtract === false)
194
195
  args.push('--lazy-initial-extract');
196
+ if (opts.proxy?.server) {
197
+ args.push('--proxy-server', opts.proxy.server);
198
+ if (opts.proxy.username !== undefined)
199
+ args.push('--proxy-username', opts.proxy.username);
200
+ if (opts.proxy.password !== undefined)
201
+ args.push('--proxy-password', opts.proxy.password);
202
+ if (opts.proxy.bypass !== undefined)
203
+ args.push('--proxy-bypass', opts.proxy.bypass);
204
+ }
195
205
  return new Promise((resolve, reject) => {
196
206
  const child = spawn(process.execPath, args, {
197
207
  stdio: ['ignore', 'pipe', 'pipe'],
package/dist/server.js CHANGED
@@ -376,6 +376,20 @@ Chromium opens **visible** by default unless \`headless: true\`. File upload / w
376
376
  .optional()
377
377
  .default(false)
378
378
  .describe('When true, bypass the reusable proxy pool and spawn a brand-new Chromium for this session that is destroyed on disconnect. Required for safe parallel form submission — without this, two parallel sessions can land on the same pooled proxy and contaminate each other. Default false (use the pool for speed).'),
379
+ proxy: z
380
+ .object({
381
+ server: z
382
+ .string()
383
+ .describe('Proxy URL (http://host:port, https://host:port, or socks5://host:port).'),
384
+ username: z.string().optional().describe('Proxy auth username.'),
385
+ password: z.string().optional().describe('Proxy auth password.'),
386
+ bypass: z
387
+ .string()
388
+ .optional()
389
+ .describe('Comma-separated host patterns to bypass (e.g. "*.internal,localhost").'),
390
+ })
391
+ .optional()
392
+ .describe('BYO outbound proxy for the spawned Chromium. Routes all browser traffic through the supplied residential / mobile / SOCKS proxy — useful for apply portals (Ashby, Lever, Cloudflare-fronted ATSes) that fingerprint datacenter IPs and flag headless sessions as bots. The reusable proxy pool is partitioned by proxy identity so callers with different proxy configs never share a Chromium instance.'),
379
393
  returnForms: z
380
394
  .boolean()
381
395
  .optional()
@@ -431,6 +445,7 @@ Chromium opens **visible** by default unless \`headless: true\`. File upload / w
431
445
  height: input.height,
432
446
  slowMo: input.slowMo,
433
447
  isolated: input.isolated,
448
+ proxy: input.proxy,
434
449
  awaitInitialFrame: deferInlinePageModel ? false : undefined,
435
450
  eagerInitialExtract: deferInlinePageModel ? true : undefined,
436
451
  });
@@ -501,9 +516,23 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
501
516
  .nonnegative()
502
517
  .optional()
503
518
  .describe('Playwright slowMo (ms) for the warmed browser.'),
504
- }, async ({ pageUrl, port, headless, width, height, slowMo }) => {
519
+ proxy: z
520
+ .object({
521
+ server: z
522
+ .string()
523
+ .describe('Proxy URL (http://host:port, https://host:port, or socks5://host:port).'),
524
+ username: z.string().optional().describe('Proxy auth username.'),
525
+ password: z.string().optional().describe('Proxy auth password.'),
526
+ bypass: z
527
+ .string()
528
+ .optional()
529
+ .describe('Comma-separated host patterns to bypass.'),
530
+ })
531
+ .optional()
532
+ .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 }) => {
505
534
  try {
506
- const prepared = await prewarmProxy({ pageUrl, port, headless, width, height, slowMo });
535
+ const prepared = await prewarmProxy({ pageUrl, port, headless, width, height, slowMo, proxy });
507
536
  return ok(JSON.stringify(prepared));
508
537
  }
509
538
  catch (e) {
package/dist/session.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
2
  import WebSocket from 'ws';
3
- import { type EmbeddedProxyRuntime } from './proxy-spawn.js';
3
+ import { type EmbeddedProxyRuntime, type SpawnProxyConfig } from './proxy-spawn.js';
4
4
  /**
5
5
  * Parsed accessibility node from the UI tree + computed layout.
6
6
  * Mirrors the shape of @geometra/core's AccessibilityNode without importing it
@@ -492,6 +492,7 @@ export declare function prewarmProxy(options: {
492
492
  width?: number;
493
493
  height?: number;
494
494
  slowMo?: number;
495
+ proxy?: SpawnProxyConfig;
495
496
  }): Promise<{
496
497
  prepared: true;
497
498
  reused: boolean;
@@ -535,6 +536,13 @@ export declare function connectThroughProxy(options: {
535
536
  * leak into another. Default false preserves the existing pool behavior.
536
537
  */
537
538
  isolated?: boolean;
539
+ /**
540
+ * BYO outbound proxy for the Chromium. Routes all browser traffic through
541
+ * the supplied residential / mobile / SOCKS proxy. The reusable pool is
542
+ * partitioned by proxy identity so two callers with different proxy
543
+ * configs never share a Chromium instance.
544
+ */
545
+ proxy?: SpawnProxyConfig;
538
546
  }): Promise<Session>;
539
547
  export declare function getSession(id?: string): Session | null;
540
548
  export declare function pruneDisconnectedSessions(): string[];
package/dist/session.js CHANGED
@@ -3,6 +3,19 @@ import { performance } from 'node:perf_hooks';
3
3
  import WebSocket from 'ws';
4
4
  import { spawnGeometraProxy, startEmbeddedGeometraProxy } from './proxy-spawn.js';
5
5
  import { completeSessionLifecycle, failSessionLifecycle, heartbeatSessionLifecycle, initializeSessionLifecycle, recordSessionSnapshot, } from './session-state.js';
6
+ /**
7
+ * Stable identity for an outbound proxy config, used as the reusable-pool
8
+ * partition key. Two sessions with different proxy configs MUST NOT share a
9
+ * pooled Chromium — otherwise the first apply's IP leaks into subsequent
10
+ * applies even when the caller opted into a fresh proxy. Password is
11
+ * excluded from the key to keep logs safe; `server + username + bypass` is
12
+ * enough to distinguish every realistic multi-tenant config.
13
+ */
14
+ function proxyKeyFor(proxy) {
15
+ if (!proxy?.server)
16
+ return '';
17
+ return `${proxy.server}|${proxy.username ?? ''}|${proxy.bypass ?? ''}`;
18
+ }
6
19
  const activeSessions = new Map();
7
20
  let defaultSessionId = null;
8
21
  const MAX_ACTIVE_SESSIONS = 5;
@@ -129,6 +142,7 @@ function enforceReusableProxyPoolLimit() {
129
142
  function setReusableProxy(proxy, wsUrl, opts) {
130
143
  clearReusableProxiesIfExited();
131
144
  const now = Date.now();
145
+ const proxyKey = proxyKeyFor(opts.proxy);
132
146
  const existing = reusableProxies.find(entry => sameReusableProxyEntry(entry, proxy));
133
147
  if (existing) {
134
148
  existing.wsUrl = wsUrl;
@@ -137,6 +151,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
137
151
  existing.width = opts.width ?? 1280;
138
152
  existing.height = opts.height ?? 720;
139
153
  existing.pageUrl = opts.pageUrl;
154
+ existing.proxyKey = proxyKey;
140
155
  existing.snapshotReady = opts.snapshotReady ?? existing.snapshotReady;
141
156
  existing.lastUsedAt = now;
142
157
  return;
@@ -151,6 +166,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
151
166
  width: opts.width ?? 1280,
152
167
  height: opts.height ?? 720,
153
168
  pageUrl: opts.pageUrl,
169
+ proxyKey,
154
170
  snapshotReady: opts.snapshotReady === true,
155
171
  lastUsedAt: now,
156
172
  };
@@ -176,6 +192,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
176
192
  width: opts.width ?? 1280,
177
193
  height: opts.height ?? 720,
178
194
  pageUrl: opts.pageUrl,
195
+ proxyKey,
179
196
  snapshotReady: opts.snapshotReady === true,
180
197
  lastUsedAt: now,
181
198
  });
@@ -390,7 +407,8 @@ function reusableProxyMatchesOptions(entry, options) {
390
407
  entry.headless === (options.headless === true) &&
391
408
  entry.slowMo === (options.slowMo ?? 0) &&
392
409
  entry.width === (options.width ?? 1280) &&
393
- entry.height === (options.height ?? 720));
410
+ entry.height === (options.height ?? 720) &&
411
+ entry.proxyKey === proxyKeyFor(options.proxy));
394
412
  }
395
413
  function findExactReusableProxy(options) {
396
414
  clearReusableProxiesIfExited();
@@ -407,8 +425,14 @@ function findReusableProxy(options) {
407
425
  const desiredSlowMo = options.slowMo ?? 0;
408
426
  const desiredWidth = options.width ?? 1280;
409
427
  const desiredHeight = options.height ?? 720;
428
+ const desiredProxyKey = proxyKeyFor(options.proxy);
410
429
  return reusableProxies
411
- .filter(entry => entry.headless === desiredHeadless && entry.slowMo === desiredSlowMo)
430
+ .filter(entry => entry.headless === desiredHeadless
431
+ && entry.slowMo === desiredSlowMo
432
+ // Proxy partition is hard — a session with residential proxy MUST NOT
433
+ // attach to a pooled direct-connection Chromium (and vice versa).
434
+ // Different proxy credentials also get separate pool entries.
435
+ && entry.proxyKey === desiredProxyKey)
412
436
  .sort((a, b) => {
413
437
  const score = (entry) => {
414
438
  let value = 0;
@@ -448,6 +472,7 @@ export async function prewarmProxy(options) {
448
472
  width: options.width,
449
473
  height: options.height,
450
474
  slowMo: options.slowMo,
475
+ proxy: options.proxy,
451
476
  });
452
477
  try {
453
478
  await runtime.ready;
@@ -463,6 +488,7 @@ export async function prewarmProxy(options) {
463
488
  height: options.height,
464
489
  pageUrl: options.pageUrl,
465
490
  snapshotReady: true,
491
+ proxy: options.proxy,
466
492
  });
467
493
  return {
468
494
  prepared: true,
@@ -486,6 +512,7 @@ export async function prewarmProxy(options) {
486
512
  width: options.width,
487
513
  height: options.height,
488
514
  slowMo: options.slowMo,
515
+ proxy: options.proxy,
489
516
  });
490
517
  setReusableProxy({ child }, wsUrl, {
491
518
  headless: options.headless,
@@ -493,6 +520,7 @@ export async function prewarmProxy(options) {
493
520
  width: options.width,
494
521
  height: options.height,
495
522
  pageUrl: options.pageUrl,
523
+ proxy: options.proxy,
496
524
  });
497
525
  return {
498
526
  prepared: true,
@@ -594,6 +622,7 @@ async function startFreshProxySession(options) {
594
622
  height: options.height,
595
623
  slowMo: options.slowMo,
596
624
  eagerInitialExtract,
625
+ proxy: options.proxy,
597
626
  });
598
627
  pendingEmbeddedRuntime = runtime;
599
628
  const proxyStartMs = performance.now() - proxyStartStartedAt;
@@ -624,6 +653,7 @@ async function startFreshProxySession(options) {
624
653
  height: options.height,
625
654
  pageUrl: options.pageUrl,
626
655
  snapshotReady: Boolean(session.tree && session.layout),
656
+ proxy: options.proxy,
627
657
  });
628
658
  }
629
659
  const baseConnectTrace = session.connectTrace;
@@ -667,6 +697,7 @@ async function startFreshProxySession(options) {
667
697
  height: options.height,
668
698
  slowMo: options.slowMo,
669
699
  eagerInitialExtract,
700
+ proxy: options.proxy,
670
701
  });
671
702
  const proxyStartMs = performance.now() - proxyStartStartedAt;
672
703
  try {
@@ -691,6 +722,7 @@ async function startFreshProxySession(options) {
691
722
  height: options.height,
692
723
  pageUrl: options.pageUrl,
693
724
  snapshotReady: Boolean(session.tree && session.layout),
725
+ proxy: options.proxy,
694
726
  });
695
727
  }
696
728
  const baseConnectTrace = session.connectTrace;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.58.0",
3
+ "version": "1.59.0",
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",