@geometra/mcp 1.61.2 → 1.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/proxy-spawn.d.ts +5 -1
- package/dist/proxy-spawn.js +21 -7
- package/dist/server.js +34 -9
- package/dist/session.d.ts +3 -0
- package/dist/session.js +40 -14
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -18,14 +18,14 @@ Geometra proxy: Chromium → DOM geometry → same WebSocket as native →
|
|
|
18
18
|
|
|
19
19
|
Use Geometra MCP when an LLM needs to explore, interpret, and operate a real UI with compact semantic state instead of repeatedly consuming large browser snapshots. Keep Playwright-style tooling for deterministic scripts, DOM-oriented test automation, and compatibility fallback paths while Geometra closes remaining live-site gaps.
|
|
20
20
|
|
|
21
|
-
Proxy-backed sessions stay warm by default on disconnect, and MCP now keeps a small warm pool so compatible headed and
|
|
21
|
+
Proxy-backed sessions stay warm by default on disconnect, and MCP now keeps a small warm pool so compatible headed, headless, and stealth workflows do not immediately evict each other.
|
|
22
22
|
|
|
23
23
|
## Tools
|
|
24
24
|
|
|
25
25
|
| Tool | Description |
|
|
26
26
|
|---|---|
|
|
27
|
-
| `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; can inline `formSchema` and/or `pageModel`, or defer the page model for a faster first connect response |
|
|
28
|
-
| `geometra_prepare_browser` | Pre-launch and pre-navigate a reusable proxy/browser for `pageUrl` without creating an active session;
|
|
27
|
+
| `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; pass `stealth: true` for CloakBrowser's patched Chromium; can inline `formSchema` and/or `pageModel`, or defer the page model for a faster first connect response |
|
|
28
|
+
| `geometra_prepare_browser` | Pre-launch and pre-navigate a reusable proxy/browser for `pageUrl` without creating an active session; supports the same headed/headless/stealth browser settings as `geometra_connect` |
|
|
29
29
|
| `geometra_query` | Find elements by stable id, role, name, text content, prompt/section/item context, current value, or semantic state such as `invalid`, `required`, or `busy` |
|
|
30
30
|
| `geometra_wait_for` | Wait for a semantic condition instead of guessing sleeps (`busy`, `disabled`, alerts, values, etc.). **Strict parameters** — use `text` plus `present: false` to wait until a substring disappears (e.g. “Parsing your resume”); there is no `textGone` field |
|
|
31
31
|
| `geometra_wait_for_resume_parse` | Convenience wait until a parsing banner is gone (defaults: `text`: `"Parsing"`, `present` implied false). Same engine as `geometra_wait_for` |
|
|
@@ -252,9 +252,10 @@ In another terminal (from repo root after `npm install` / `bun install` and `bun
|
|
|
252
252
|
```bash
|
|
253
253
|
npx geometra-proxy http://localhost:8080 --port 3200
|
|
254
254
|
# Requires Chromium: npx playwright install chromium
|
|
255
|
+
# Optional stealth prefetch: npx cloakbrowser install
|
|
255
256
|
```
|
|
256
257
|
|
|
257
|
-
`geometra-proxy` opens a **visible Chromium window by default**. For servers or CI, pass **`--headless`** or set **`GEOMETRA_HEADLESS=1`**. Optional **`--slow-mo <ms>`** slows Playwright actions so they are easier to watch. Headed vs headless usually does **not** materially change token usage, since token usage is driven by MCP tool output rather than whether Chromium is visible.
|
|
258
|
+
`geometra-proxy` opens a **visible Chromium window by default**. For servers or CI, pass **`--headless`** or set **`GEOMETRA_HEADLESS=1`**. Pass **`--stealth`**, **`stealth: true`** on MCP tools, or **`GEOMETRA_STEALTH=1`** to use CloakBrowser's patched Chromium through the same proxy protocol. Optional **`--slow-mo <ms>`** slows Playwright actions so they are easier to watch. Headed vs headless usually does **not** materially change token usage, since token usage is driven by MCP tool output rather than whether Chromium is visible.
|
|
258
259
|
|
|
259
260
|
Point MCP at `ws://127.0.0.1:3200` instead of a native Geometra server. The proxy translates clicks and keyboard messages into Playwright actions and streams updated geometry.
|
|
260
261
|
|
|
@@ -341,7 +342,7 @@ For long application flows, prefer one of these patterns:
|
|
|
341
342
|
4. `geometra_snapshot({ view: "form-required" })` when you need the remaining required fields including offscreen ones
|
|
342
343
|
5. `geometra_reveal` for far-below-fold targets such as submit buttons (auto-scales reveal steps when you omit `maxSteps`)
|
|
343
344
|
6. `geometra_click({ ..., waitFor: ... })` when one action should also wait for the next semantic state
|
|
344
|
-
7. `geometra_prepare_browser({ pageUrl, headless, width, height })` when you can hide browser startup before the real task and want the next proxy-backed flow to attach warm
|
|
345
|
+
7. `geometra_prepare_browser({ pageUrl, headless, stealth, width, height })` when you can hide browser startup before the real task and want the next proxy-backed flow to attach warm
|
|
345
346
|
8. `geometra_run_actions` when you need mixed navigation + waits + field entry, especially with `pageUrl` / `url` for a one-call flow
|
|
346
347
|
9. `geometra_connect({ pageUrl, returnPageModel: true })` when you want connect + summary-first exploration in one turn
|
|
347
348
|
10. `geometra_connect({ pageUrl, returnPageModel: true, pageModelMode: "deferred" })` when first-response latency matters more than inlining the page model; follow with `geometra_page_model`
|
package/dist/proxy-spawn.d.ts
CHANGED
|
@@ -31,14 +31,18 @@ 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;
|
|
40
42
|
}>;
|
|
41
|
-
export declare function parseProxyReadySignalLine(line: string
|
|
43
|
+
export declare function parseProxyReadySignalLine(line: string, options?: {
|
|
44
|
+
allowLegacy?: boolean;
|
|
45
|
+
}): string | undefined;
|
|
42
46
|
export declare function formatProxyStartupFailure(message: string, opts: SpawnProxyParams): string;
|
|
43
47
|
/**
|
|
44
48
|
* Spawn geometra-proxy as a child process and resolve when it emits a structured ready signal.
|
package/dist/proxy-spawn.js
CHANGED
|
@@ -7,6 +7,7 @@ const require = createRequire(import.meta.url);
|
|
|
7
7
|
const READY_SIGNAL_TYPE = 'geometra-proxy-ready';
|
|
8
8
|
const READY_TIMEOUT_MS = 45_000;
|
|
9
9
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const PLAYWRIGHT_INSTALL_HINT = 'Install Chromium with the Playwright version bundled in this package: npm run browsers:install -w @geometra/proxy (repo checkout) or npx --no-install playwright install chromium.';
|
|
10
11
|
/** Resolve bundled @geometra/proxy CLI entry (dist/index.js). */
|
|
11
12
|
export function resolveProxyScriptPath() {
|
|
12
13
|
return resolveProxyScriptPathWith(require);
|
|
@@ -125,6 +126,9 @@ function buildLocalProxyDistIfPossible(packageDir, entryFile, errors) {
|
|
|
125
126
|
}
|
|
126
127
|
return undefined;
|
|
127
128
|
}
|
|
129
|
+
export function resolveStealthMode(stealth) {
|
|
130
|
+
return stealth ?? true;
|
|
131
|
+
}
|
|
128
132
|
export async function startEmbeddedGeometraProxy(opts) {
|
|
129
133
|
const runtimePath = resolveProxyRuntimePath();
|
|
130
134
|
const runtimeModule = await import(pathToFileURL(runtimePath).href);
|
|
@@ -136,14 +140,15 @@ export async function startEmbeddedGeometraProxy(opts) {
|
|
|
136
140
|
port: opts.port,
|
|
137
141
|
width: opts.width,
|
|
138
142
|
height: opts.height,
|
|
139
|
-
headed: opts.headless
|
|
143
|
+
headed: opts.headless === false,
|
|
140
144
|
slowMo: opts.slowMo,
|
|
145
|
+
...(opts.stealth !== undefined && { stealth: opts.stealth }),
|
|
141
146
|
eagerInitialExtract: opts.eagerInitialExtract,
|
|
142
147
|
...(opts.proxy && { proxy: opts.proxy }),
|
|
143
148
|
});
|
|
144
149
|
return { runtime, wsUrl: runtime.wsUrl };
|
|
145
150
|
}
|
|
146
|
-
export function parseProxyReadySignalLine(line) {
|
|
151
|
+
export function parseProxyReadySignalLine(line, options) {
|
|
147
152
|
const trimmed = line.trim();
|
|
148
153
|
if (!trimmed)
|
|
149
154
|
return undefined;
|
|
@@ -160,13 +165,18 @@ export function parseProxyReadySignalLine(line) {
|
|
|
160
165
|
/* ignore non-JSON lines */
|
|
161
166
|
}
|
|
162
167
|
}
|
|
168
|
+
if (options?.allowLegacy === false)
|
|
169
|
+
return undefined;
|
|
163
170
|
const fallback = trimmed.match(/WebSocket listening on (ws:\/\/127\.0\.0\.1:\d+)/);
|
|
164
171
|
return fallback?.[1];
|
|
165
172
|
}
|
|
166
173
|
export function formatProxyStartupFailure(message, opts) {
|
|
167
174
|
const hints = [];
|
|
168
175
|
if (/Executable doesn't exist|playwright install chromium|browserType\.launch/i.test(message)) {
|
|
169
|
-
hints.push(
|
|
176
|
+
hints.push(PLAYWRIGHT_INSTALL_HINT);
|
|
177
|
+
}
|
|
178
|
+
if (/cloakbrowser|CLOAKBROWSER|ERR_MODULE_NOT_FOUND|Cannot find package/i.test(message)) {
|
|
179
|
+
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');
|
|
170
180
|
}
|
|
171
181
|
if (opts.port > 0 && /EADDRINUSE|address already in use/i.test(message)) {
|
|
172
182
|
hints.push(`Requested port ${opts.port} is unavailable. Omit the port to use an ephemeral OS-assigned port, or choose another local port.`);
|
|
@@ -187,10 +197,14 @@ export function spawnGeometraProxy(opts) {
|
|
|
187
197
|
args.push('--height', String(opts.height));
|
|
188
198
|
if (opts.slowMo != null && opts.slowMo > 0)
|
|
189
199
|
args.push('--slow-mo', String(opts.slowMo));
|
|
190
|
-
if (opts.headless ===
|
|
191
|
-
args.push('--headless');
|
|
192
|
-
else if (opts.headless === false)
|
|
200
|
+
if (opts.headless === false)
|
|
193
201
|
args.push('--headed');
|
|
202
|
+
else
|
|
203
|
+
args.push('--headless');
|
|
204
|
+
if (opts.stealth === true)
|
|
205
|
+
args.push('--stealth');
|
|
206
|
+
else if (opts.stealth === false)
|
|
207
|
+
args.push('--no-stealth');
|
|
194
208
|
if (opts.eagerInitialExtract === false)
|
|
195
209
|
args.push('--lazy-initial-extract');
|
|
196
210
|
if (opts.proxy?.server) {
|
|
@@ -216,7 +230,7 @@ export function spawnGeometraProxy(opts) {
|
|
|
216
230
|
child.stderr?.removeAllListeners('data');
|
|
217
231
|
};
|
|
218
232
|
const tryResolveReady = (line) => {
|
|
219
|
-
const wsUrl = parseProxyReadySignalLine(line);
|
|
233
|
+
const wsUrl = parseProxyReadySignalLine(line, { allowLegacy: false });
|
|
220
234
|
if (!wsUrl || settled)
|
|
221
235
|
return false;
|
|
222
236
|
settled = true;
|
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({
|
|
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
|
-
existing.headless = opts.headless
|
|
150
|
+
existing.headless = opts.headless !== false;
|
|
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;
|
|
@@ -161,7 +163,8 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
161
163
|
const entry = {
|
|
162
164
|
child,
|
|
163
165
|
wsUrl,
|
|
164
|
-
headless: opts.headless
|
|
166
|
+
headless: opts.headless !== false,
|
|
167
|
+
stealth,
|
|
165
168
|
slowMo: opts.slowMo ?? 0,
|
|
166
169
|
width: opts.width ?? 1280,
|
|
167
170
|
height: opts.height ?? 720,
|
|
@@ -187,7 +190,8 @@ function setReusableProxy(proxy, wsUrl, opts) {
|
|
|
187
190
|
reusableProxies.push({
|
|
188
191
|
runtime: proxy.runtime,
|
|
189
192
|
wsUrl,
|
|
190
|
-
headless: opts.headless
|
|
193
|
+
headless: opts.headless !== false,
|
|
194
|
+
stealth,
|
|
191
195
|
slowMo: opts.slowMo ?? 0,
|
|
192
196
|
width: opts.width ?? 1280,
|
|
193
197
|
height: opts.height ?? 720,
|
|
@@ -320,6 +324,11 @@ function evictOldestSession() {
|
|
|
320
324
|
function formatUnknownError(err) {
|
|
321
325
|
return err instanceof Error ? err.message : String(err);
|
|
322
326
|
}
|
|
327
|
+
function rejectOnRuntimeReadyFailure(runtime) {
|
|
328
|
+
return new Promise((_, reject) => {
|
|
329
|
+
runtime.ready.catch(reject);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
323
332
|
function warnSessionLifecycleError(action, session, err) {
|
|
324
333
|
console.warn(`geometra-mcp: failed to ${action} for session ${session.id}: ${formatUnknownError(err)}`);
|
|
325
334
|
}
|
|
@@ -404,7 +413,8 @@ function stopSessionHeartbeat(session) {
|
|
|
404
413
|
}
|
|
405
414
|
function reusableProxyMatchesOptions(entry, options) {
|
|
406
415
|
return (entry.pageUrl === options.pageUrl &&
|
|
407
|
-
entry.headless === (options.headless
|
|
416
|
+
entry.headless === (options.headless !== false) &&
|
|
417
|
+
entry.stealth === resolveStealthMode(options.stealth) &&
|
|
408
418
|
entry.slowMo === (options.slowMo ?? 0) &&
|
|
409
419
|
entry.width === (options.width ?? 1280) &&
|
|
410
420
|
entry.height === (options.height ?? 720) &&
|
|
@@ -421,13 +431,15 @@ function findExactReusableProxy(options) {
|
|
|
421
431
|
}
|
|
422
432
|
function findReusableProxy(options) {
|
|
423
433
|
clearReusableProxiesIfExited();
|
|
424
|
-
const desiredHeadless = options.headless
|
|
434
|
+
const desiredHeadless = options.headless !== false;
|
|
435
|
+
const desiredStealth = resolveStealthMode(options.stealth);
|
|
425
436
|
const desiredSlowMo = options.slowMo ?? 0;
|
|
426
437
|
const desiredWidth = options.width ?? 1280;
|
|
427
438
|
const desiredHeight = options.height ?? 720;
|
|
428
439
|
const desiredProxyKey = proxyKeyFor(options.proxy);
|
|
429
440
|
return reusableProxies
|
|
430
441
|
.filter(entry => entry.headless === desiredHeadless
|
|
442
|
+
&& entry.stealth === desiredStealth
|
|
431
443
|
&& entry.slowMo === desiredSlowMo
|
|
432
444
|
// Proxy partition is hard — a session with residential proxy MUST NOT
|
|
433
445
|
// attach to a pooled direct-connection Chromium (and vice versa).
|
|
@@ -458,7 +470,8 @@ export async function prewarmProxy(options) {
|
|
|
458
470
|
transport: existing.runtime ? 'embedded' : 'child',
|
|
459
471
|
pageUrl: options.pageUrl,
|
|
460
472
|
wsUrl: existing.wsUrl,
|
|
461
|
-
headless: options.headless
|
|
473
|
+
headless: options.headless !== false,
|
|
474
|
+
stealth: resolveStealthMode(options.stealth),
|
|
462
475
|
width: options.width ?? 1280,
|
|
463
476
|
height: options.height ?? 720,
|
|
464
477
|
};
|
|
@@ -472,6 +485,7 @@ export async function prewarmProxy(options) {
|
|
|
472
485
|
width: options.width,
|
|
473
486
|
height: options.height,
|
|
474
487
|
slowMo: options.slowMo,
|
|
488
|
+
stealth: options.stealth,
|
|
475
489
|
proxy: options.proxy,
|
|
476
490
|
});
|
|
477
491
|
try {
|
|
@@ -484,6 +498,7 @@ export async function prewarmProxy(options) {
|
|
|
484
498
|
setReusableProxy({ runtime }, wsUrl, {
|
|
485
499
|
headless: options.headless,
|
|
486
500
|
slowMo: options.slowMo,
|
|
501
|
+
stealth: options.stealth,
|
|
487
502
|
width: options.width,
|
|
488
503
|
height: options.height,
|
|
489
504
|
pageUrl: options.pageUrl,
|
|
@@ -496,7 +511,8 @@ export async function prewarmProxy(options) {
|
|
|
496
511
|
transport: 'embedded',
|
|
497
512
|
pageUrl: options.pageUrl,
|
|
498
513
|
wsUrl,
|
|
499
|
-
headless: options.headless
|
|
514
|
+
headless: options.headless !== false,
|
|
515
|
+
stealth: resolveStealthMode(options.stealth),
|
|
500
516
|
width: options.width ?? 1280,
|
|
501
517
|
height: options.height ?? 720,
|
|
502
518
|
};
|
|
@@ -512,11 +528,13 @@ export async function prewarmProxy(options) {
|
|
|
512
528
|
width: options.width,
|
|
513
529
|
height: options.height,
|
|
514
530
|
slowMo: options.slowMo,
|
|
531
|
+
stealth: options.stealth,
|
|
515
532
|
proxy: options.proxy,
|
|
516
533
|
});
|
|
517
534
|
setReusableProxy({ child }, wsUrl, {
|
|
518
535
|
headless: options.headless,
|
|
519
536
|
slowMo: options.slowMo,
|
|
537
|
+
stealth: options.stealth,
|
|
520
538
|
width: options.width,
|
|
521
539
|
height: options.height,
|
|
522
540
|
pageUrl: options.pageUrl,
|
|
@@ -528,7 +546,8 @@ export async function prewarmProxy(options) {
|
|
|
528
546
|
transport: 'child',
|
|
529
547
|
pageUrl: options.pageUrl,
|
|
530
548
|
wsUrl,
|
|
531
|
-
headless: options.headless
|
|
549
|
+
headless: options.headless !== false,
|
|
550
|
+
stealth: resolveStealthMode(options.stealth),
|
|
532
551
|
width: options.width ?? 1280,
|
|
533
552
|
height: options.height ?? 720,
|
|
534
553
|
};
|
|
@@ -621,16 +640,20 @@ async function startFreshProxySession(options) {
|
|
|
621
640
|
width: options.width,
|
|
622
641
|
height: options.height,
|
|
623
642
|
slowMo: options.slowMo,
|
|
643
|
+
stealth: options.stealth,
|
|
624
644
|
eagerInitialExtract,
|
|
625
645
|
proxy: options.proxy,
|
|
626
646
|
});
|
|
627
647
|
pendingEmbeddedRuntime = runtime;
|
|
628
648
|
const proxyStartMs = performance.now() - proxyStartStartedAt;
|
|
629
|
-
const session = await
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
649
|
+
const session = await Promise.race([
|
|
650
|
+
connect(wsUrl, {
|
|
651
|
+
skipInitialResize: true,
|
|
652
|
+
closePreviousProxy: false,
|
|
653
|
+
awaitInitialFrame: options.awaitInitialFrame,
|
|
654
|
+
}),
|
|
655
|
+
rejectOnRuntimeReadyFailure(runtime),
|
|
656
|
+
]);
|
|
634
657
|
// Connect succeeded — the session now owns the runtime, so the
|
|
635
658
|
// catch-block cleanup below must not also close it.
|
|
636
659
|
pendingEmbeddedRuntime = undefined;
|
|
@@ -649,6 +672,7 @@ async function startFreshProxySession(options) {
|
|
|
649
672
|
setReusableProxy({ runtime }, wsUrl, {
|
|
650
673
|
headless: options.headless,
|
|
651
674
|
slowMo: options.slowMo,
|
|
675
|
+
stealth: options.stealth,
|
|
652
676
|
width: options.width,
|
|
653
677
|
height: options.height,
|
|
654
678
|
pageUrl: options.pageUrl,
|
|
@@ -695,6 +719,7 @@ async function startFreshProxySession(options) {
|
|
|
695
719
|
width: options.width,
|
|
696
720
|
height: options.height,
|
|
697
721
|
slowMo: options.slowMo,
|
|
722
|
+
stealth: options.stealth,
|
|
698
723
|
eagerInitialExtract,
|
|
699
724
|
proxy: options.proxy,
|
|
700
725
|
});
|
|
@@ -717,6 +742,7 @@ async function startFreshProxySession(options) {
|
|
|
717
742
|
setReusableProxy({ child }, wsUrl, {
|
|
718
743
|
headless: options.headless,
|
|
719
744
|
slowMo: options.slowMo,
|
|
745
|
+
stealth: options.stealth,
|
|
720
746
|
width: options.width,
|
|
721
747
|
height: options.height,
|
|
722
748
|
pageUrl: options.pageUrl,
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geometra/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.62.1",
|
|
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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
9
|
+
"url": "https://github.com/Agent-Pattern-Labs/geometra",
|
|
10
10
|
"directory": "mcp"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"ui-testing"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@geometra/proxy": "^1.
|
|
35
|
+
"@geometra/proxy": "^1.62.1",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
37
37
|
"@razroo/parallel-mcp": "^0.1.0",
|
|
38
38
|
"ws": "^8.18.0",
|