@geometra/mcp 1.62.1 → 1.62.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/proxy-spawn.d.ts +3 -5
- package/dist/proxy-spawn.js +21 -2
- package/dist/server.js +196 -34
- package/dist/session.d.ts +12 -4
- package/dist/session.js +91 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Proxy-backed sessions stay warm by default on disconnect, and MCP now keeps a sm
|
|
|
24
24
|
|
|
25
25
|
| Tool | Description |
|
|
26
26
|
|---|---|
|
|
27
|
-
| `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; pass `
|
|
27
|
+
| `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; pass `browserMode: "stock" | "cloakbrowser"` for explicit authorized testing modes; can inline `formSchema` and/or `pageModel`, or defer the page model for a faster first connect response |
|
|
28
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 |
|
|
@@ -33,7 +33,7 @@ Proxy-backed sessions stay warm by default on disconnect, and MCP now keeps a sm
|
|
|
33
33
|
| `geometra_fill_form` | Fill a form from `valuesById` / `valuesByLabel` in one MCP call; can auto-connect from `pageUrl` / `url` for the lowest-token known-form path |
|
|
34
34
|
| `geometra_fill_fields` | Fill text/choice/toggle/file fields in one MCP call; text-only batches now use the proxy fast path when step output is omitted, and text/choice/toggle entries can use `fieldId` from `geometra_form_schema` without repeating the label |
|
|
35
35
|
| `geometra_run_actions` | Execute a batch of high-level actions in one MCP round trip; can auto-connect from `pageUrl` / `url` and return a final-only payload for the smallest multi-step responses |
|
|
36
|
-
| `geometra_page_model` | Summary-first webpage model: archetypes, stable section ids, counts, top-level sections,
|
|
36
|
+
| `geometra_page_model` | Summary-first webpage model: archetypes, stable section ids, counts, top-level sections, primary actions, and `blockedSite` metadata for CAPTCHA/challenge/access-denied states |
|
|
37
37
|
| `geometra_find_action` | Resolve a repeated button/link by action label plus optional `sectionText`, `promptText`, or `itemText` before clicking |
|
|
38
38
|
| `geometra_expand_section` | Expand one form/dialog/list/landmark from `geometra_page_model` on demand, with paging/filtering for long sections |
|
|
39
39
|
| `geometra_reveal` | Scroll until a matching node is visible instead of guessing wheel deltas; auto-scales reveal steps for tall forms when omitted |
|
|
@@ -252,10 +252,12 @@ 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
|
+
# Optional authorized stealth-testing prefetch: npx cloakbrowser install
|
|
256
256
|
```
|
|
257
257
|
|
|
258
|
-
`geometra-proxy`
|
|
258
|
+
`geometra-proxy` runs **headless by default**. Pass **`--headed`** or **`headless: false`** on MCP tools when you need a visible Chromium window. Pass **`--stealth`**, **`stealth: true`** on MCP tools, or **`GEOMETRA_STEALTH=1`** to use CloakBrowser's Chromium through the same proxy protocol for authorized testing. 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.
|
|
259
|
+
|
|
260
|
+
For blocked/challenge pages, keep `blockDetection: true` (default). Set `blockedSitePolicy` to `"continue"` (default), `"manual-handoff"` to return retry guidance for a visible user handoff, or `"error"` to stop immediately with structured `blockedSite` details.
|
|
259
261
|
|
|
260
262
|
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.
|
|
261
263
|
|
package/dist/proxy-spawn.d.ts
CHANGED
|
@@ -12,11 +12,9 @@ export declare function resolveProxyScriptPathWith(customRequire: NodeRequire, m
|
|
|
12
12
|
export declare function resolveProxyRuntimePath(): string;
|
|
13
13
|
export declare function resolveProxyRuntimePathWith(customRequire: NodeRequire, moduleDir?: string): string;
|
|
14
14
|
/**
|
|
15
|
-
* BYO outbound proxy for the spawned Chromium.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Lever Mapbox geocoder, Cloudflare Bot Management, etc.). Geometra is the
|
|
19
|
-
* wire — the user supplies the proxy.
|
|
15
|
+
* BYO outbound proxy for the spawned Chromium. Geometra only passes the
|
|
16
|
+
* caller-provided network route through to Playwright; callers are responsible
|
|
17
|
+
* for using proxies with authorization and in line with the target site's rules.
|
|
20
18
|
*/
|
|
21
19
|
export interface SpawnProxyConfig {
|
|
22
20
|
server: string;
|
package/dist/proxy-spawn.js
CHANGED
|
@@ -127,7 +127,26 @@ function buildLocalProxyDistIfPossible(packageDir, entryFile, errors) {
|
|
|
127
127
|
return undefined;
|
|
128
128
|
}
|
|
129
129
|
export function resolveStealthMode(stealth) {
|
|
130
|
-
|
|
130
|
+
if (stealth !== undefined)
|
|
131
|
+
return stealth;
|
|
132
|
+
const explicit = process.env.GEOMETRA_STEALTH;
|
|
133
|
+
if (truthyEnv(explicit))
|
|
134
|
+
return true;
|
|
135
|
+
if (falseyEnv(explicit))
|
|
136
|
+
return false;
|
|
137
|
+
const browser = process.env.GEOMETRA_BROWSER?.trim().toLowerCase();
|
|
138
|
+
if (browser === 'stealth' || browser === 'cloakbrowser' || browser === 'cloak')
|
|
139
|
+
return true;
|
|
140
|
+
if (browser === 'chromium' || browser === 'chrome' || browser === 'stock' || browser === 'playwright') {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
function truthyEnv(value) {
|
|
146
|
+
return value != null && /^(1|true|yes|on)$/i.test(value);
|
|
147
|
+
}
|
|
148
|
+
function falseyEnv(value) {
|
|
149
|
+
return value != null && /^(0|false|no|off)$/i.test(value);
|
|
131
150
|
}
|
|
132
151
|
export async function startEmbeddedGeometraProxy(opts) {
|
|
133
152
|
const runtimePath = resolveProxyRuntimePath();
|
|
@@ -176,7 +195,7 @@ export function formatProxyStartupFailure(message, opts) {
|
|
|
176
195
|
hints.push(PLAYWRIGHT_INSTALL_HINT);
|
|
177
196
|
}
|
|
178
197
|
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
|
|
198
|
+
hints.push('Stealth mode uses CloakBrowser. Install dependencies with npm install, or disable stealth with stealth=false / GEOMETRA_STEALTH=0. To prefetch the browser binary for authorized testing, run: npx cloakbrowser install');
|
|
180
199
|
}
|
|
181
200
|
if (opts.port > 0 && /EADDRINUSE|address already in use/i.test(message)) {
|
|
182
201
|
hints.push(`Requested port ${opts.port} is unavailable. Omit the port to use an ephemeral OS-assigned port, or choose another local port.`);
|
package/dist/server.js
CHANGED
|
@@ -21,7 +21,34 @@ function stealthInput() {
|
|
|
21
21
|
return z
|
|
22
22
|
.boolean()
|
|
23
23
|
.optional()
|
|
24
|
-
.describe('Launch CloakBrowser
|
|
24
|
+
.describe('Launch CloakBrowser Chromium for authorized proxy-backed pageUrl testing. Default false unless GEOMETRA_STEALTH=1 or GEOMETRA_BROWSER=stealth is set.');
|
|
25
|
+
}
|
|
26
|
+
function browserModeInput() {
|
|
27
|
+
return z
|
|
28
|
+
.enum(['stock', 'cloakbrowser'])
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Explicit browser engine for proxy-backed sessions. `stock` forces Playwright Chromium; `cloakbrowser` opts into CloakBrowser for authorized testing. Conflicts with a contradictory stealth value.');
|
|
31
|
+
}
|
|
32
|
+
function blockDetectionInput() {
|
|
33
|
+
return z
|
|
34
|
+
.boolean()
|
|
35
|
+
.optional()
|
|
36
|
+
.default(true)
|
|
37
|
+
.describe('Detect CAPTCHA/challenge/blocked/unsupported-browser pages and surface `blockedSite` metadata (default true).');
|
|
38
|
+
}
|
|
39
|
+
function blockedSitePolicyInput() {
|
|
40
|
+
return z
|
|
41
|
+
.enum(['continue', 'manual-handoff', 'error'])
|
|
42
|
+
.optional()
|
|
43
|
+
.default('continue')
|
|
44
|
+
.describe('How to respond when blockDetection finds a blocked/challenge page: continue (default), manual-handoff (return handoff metadata), or error.');
|
|
45
|
+
}
|
|
46
|
+
function manualHandoffInput() {
|
|
47
|
+
return z
|
|
48
|
+
.boolean()
|
|
49
|
+
.optional()
|
|
50
|
+
.default(false)
|
|
51
|
+
.describe('Shortcut for blockedSitePolicy="manual-handoff" when a blocked/challenge page is detected.');
|
|
25
52
|
}
|
|
26
53
|
function formSchemaFormatInput() {
|
|
27
54
|
return z
|
|
@@ -37,6 +64,76 @@ function pageModelModeInput() {
|
|
|
37
64
|
.default('inline')
|
|
38
65
|
.describe('When returnPageModel=true, `inline` includes the full page model in the connect response. `deferred` returns connect as soon as the transport is ready and lets the caller fetch geometra_page_model separately.');
|
|
39
66
|
}
|
|
67
|
+
function resolveBrowserStealth(input) {
|
|
68
|
+
const modeStealth = input.browserMode === 'cloakbrowser'
|
|
69
|
+
? true
|
|
70
|
+
: input.browserMode === 'stock'
|
|
71
|
+
? false
|
|
72
|
+
: undefined;
|
|
73
|
+
if (modeStealth !== undefined && input.stealth !== undefined && input.stealth !== modeStealth) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: `Conflicting browser settings: browserMode="${input.browserMode}" implies stealth=${modeStealth}, but stealth=${input.stealth} was also provided.`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const stealth = input.stealth ?? modeStealth;
|
|
80
|
+
return stealth === undefined ? { ok: true } : { ok: true, stealth };
|
|
81
|
+
}
|
|
82
|
+
function effectiveBlockedSitePolicy(policy, manualHandoff) {
|
|
83
|
+
return manualHandoff ? 'manual-handoff' : policy;
|
|
84
|
+
}
|
|
85
|
+
function blockedSiteManualHandoffPayload(blockedSite, options) {
|
|
86
|
+
return {
|
|
87
|
+
required: true,
|
|
88
|
+
reason: blockedSite.hint ?? 'Blocked or challenge page detected',
|
|
89
|
+
recommendedAction: blockedSite.recommendedAction ?? 'manual-handoff',
|
|
90
|
+
message: 'Pause automation and let a user complete the challenge or review access in a normal visible browser session.',
|
|
91
|
+
retry: {
|
|
92
|
+
...(options?.pageUrl ? { pageUrl: options.pageUrl } : {}),
|
|
93
|
+
headless: false,
|
|
94
|
+
blockDetection: true,
|
|
95
|
+
blockedSitePolicy: 'continue',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function blockedSitePolicyErrorPayload(blockedSite, policy) {
|
|
100
|
+
return {
|
|
101
|
+
blocked: true,
|
|
102
|
+
blockedSite,
|
|
103
|
+
blockedSitePolicy: policy,
|
|
104
|
+
message: blockedSite.hint ?? 'Blocked or challenge page detected',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function applyBlockedSitePolicyPayload(payload, blockedSite, options) {
|
|
108
|
+
if (!blockedSite?.detected)
|
|
109
|
+
return payload;
|
|
110
|
+
const policy = effectiveBlockedSitePolicy(options.policy, options.manualHandoff);
|
|
111
|
+
const next = {
|
|
112
|
+
...payload,
|
|
113
|
+
blockedSite,
|
|
114
|
+
blockedSitePolicy: policy,
|
|
115
|
+
};
|
|
116
|
+
if (policy === 'manual-handoff') {
|
|
117
|
+
next.manualHandoff = blockedSiteManualHandoffPayload(blockedSite, options);
|
|
118
|
+
}
|
|
119
|
+
return next;
|
|
120
|
+
}
|
|
121
|
+
function blockedSiteFromPayload(payload) {
|
|
122
|
+
const value = payload.blockedSite;
|
|
123
|
+
if (!value || typeof value !== 'object')
|
|
124
|
+
return undefined;
|
|
125
|
+
const candidate = value;
|
|
126
|
+
return candidate.detected ? candidate : undefined;
|
|
127
|
+
}
|
|
128
|
+
function blockedSiteErrorIfNeeded(payload, policy, manualHandoff) {
|
|
129
|
+
const effectivePolicy = effectiveBlockedSitePolicy(policy, manualHandoff);
|
|
130
|
+
if (effectivePolicy !== 'error')
|
|
131
|
+
return undefined;
|
|
132
|
+
const blockedSite = blockedSiteFromPayload(payload);
|
|
133
|
+
if (!blockedSite)
|
|
134
|
+
return undefined;
|
|
135
|
+
return err(JSON.stringify(blockedSitePolicyErrorPayload(blockedSite, effectivePolicy)));
|
|
136
|
+
}
|
|
40
137
|
function formSchemaContextInput() {
|
|
41
138
|
return z
|
|
42
139
|
.enum(['auto', 'always', 'none'])
|
|
@@ -407,7 +504,7 @@ export function createServer() {
|
|
|
407
504
|
|
|
408
505
|
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.
|
|
409
506
|
|
|
410
|
-
Chromium
|
|
507
|
+
Chromium runs **headless** by default unless \`headless: false\`. Pass \`stealth: true\` (or set \`GEOMETRA_STEALTH=1\`) to opt into CloakBrowser's Chromium for authorized testing 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.
|
|
411
508
|
|
|
412
509
|
**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.`, {
|
|
413
510
|
url: z
|
|
@@ -430,7 +527,7 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
430
527
|
headless: z
|
|
431
528
|
.boolean()
|
|
432
529
|
.optional()
|
|
433
|
-
.describe('Run Chromium headless (default false
|
|
530
|
+
.describe('Run Chromium headless (default true). Set false for a visible window.'),
|
|
434
531
|
width: z.number().int().positive().optional().describe('Viewport width for spawned proxy.'),
|
|
435
532
|
height: z.number().int().positive().optional().describe('Viewport height for spawned proxy.'),
|
|
436
533
|
slowMo: z
|
|
@@ -440,6 +537,7 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
440
537
|
.optional()
|
|
441
538
|
.describe('Playwright slowMo (ms) on spawned proxy for easier visual following.'),
|
|
442
539
|
stealth: stealthInput(),
|
|
540
|
+
browserMode: browserModeInput(),
|
|
443
541
|
isolated: z
|
|
444
542
|
.boolean()
|
|
445
543
|
.optional()
|
|
@@ -458,7 +556,7 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
458
556
|
.describe('Comma-separated host patterns to bypass (e.g. "*.internal,localhost").'),
|
|
459
557
|
})
|
|
460
558
|
.optional()
|
|
461
|
-
.describe('BYO outbound proxy for the spawned Chromium. Routes
|
|
559
|
+
.describe('BYO outbound proxy for the spawned Chromium. Routes browser traffic through the supplied HTTP/SOCKS proxy. Use only proxies you are authorized to use and in line with the target site rules. The reusable proxy pool is partitioned by proxy identity so callers with different proxy configs never share a Chromium instance.'),
|
|
462
560
|
returnForms: z
|
|
463
561
|
.boolean()
|
|
464
562
|
.optional()
|
|
@@ -470,6 +568,9 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
470
568
|
.default(false)
|
|
471
569
|
.describe('Include geometra_page_model output in the connect response so exploration can start in one turn.'),
|
|
472
570
|
pageModelMode: pageModelModeInput(),
|
|
571
|
+
blockDetection: blockDetectionInput(),
|
|
572
|
+
blockedSitePolicy: blockedSitePolicyInput(),
|
|
573
|
+
manualHandoff: manualHandoffInput(),
|
|
473
574
|
formId: z.string().optional().describe('Optional form id filter when returnForms=true'),
|
|
474
575
|
maxFields: z.number().int().min(1).max(120).optional().default(80).describe('Cap returned fields per form when returnForms=true'),
|
|
475
576
|
onlyRequiredFields: z.boolean().optional().default(false).describe('Only include required fields when returnForms=true'),
|
|
@@ -485,6 +586,9 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
485
586
|
const normalized = normalizeConnectTarget({ url: input.url, pageUrl: input.pageUrl });
|
|
486
587
|
if (!normalized.ok)
|
|
487
588
|
return err(normalized.error);
|
|
589
|
+
const browser = resolveBrowserStealth({ stealth: input.stealth, browserMode: input.browserMode });
|
|
590
|
+
if (!browser.ok)
|
|
591
|
+
return err(browser.error);
|
|
488
592
|
const target = normalized.value;
|
|
489
593
|
const formSchema = {
|
|
490
594
|
formId: input.formId,
|
|
@@ -513,7 +617,7 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
513
617
|
width: input.width,
|
|
514
618
|
height: input.height,
|
|
515
619
|
slowMo: input.slowMo,
|
|
516
|
-
...(
|
|
620
|
+
...(browser.stealth !== undefined && { stealth: browser.stealth }),
|
|
517
621
|
isolated: input.isolated,
|
|
518
622
|
proxy: input.proxy,
|
|
519
623
|
awaitInitialFrame: deferInlinePageModel ? false : undefined,
|
|
@@ -522,7 +626,7 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
522
626
|
if (input.returnForms) {
|
|
523
627
|
await stabilizeInlineFormSchemas(session, formSchema);
|
|
524
628
|
}
|
|
525
|
-
|
|
629
|
+
const payload = connectResponsePayload(session, {
|
|
526
630
|
transport: 'proxy',
|
|
527
631
|
requestedPageUrl: target.pageUrl,
|
|
528
632
|
autoCoercedFromUrl: target.autoCoercedFromUrl,
|
|
@@ -530,9 +634,17 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
530
634
|
returnForms: input.returnForms,
|
|
531
635
|
returnPageModel: input.returnPageModel,
|
|
532
636
|
pageModelMode: input.pageModelMode,
|
|
637
|
+
blockDetection: input.blockDetection,
|
|
638
|
+
blockedSitePolicy: input.blockedSitePolicy,
|
|
639
|
+
manualHandoff: input.manualHandoff,
|
|
533
640
|
formSchema,
|
|
534
641
|
pageModelOptions,
|
|
535
|
-
|
|
642
|
+
headless: input.headless,
|
|
643
|
+
});
|
|
644
|
+
const blockedError = blockedSiteErrorIfNeeded(payload, input.blockedSitePolicy, input.manualHandoff);
|
|
645
|
+
if (blockedError)
|
|
646
|
+
return blockedError;
|
|
647
|
+
return ok(JSON.stringify(payload, null, input.detail === 'verbose' ? 2 : undefined));
|
|
536
648
|
}
|
|
537
649
|
const session = await connect(target.wsUrl, {
|
|
538
650
|
width: input.width,
|
|
@@ -542,7 +654,7 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
542
654
|
if (input.returnForms) {
|
|
543
655
|
await stabilizeInlineFormSchemas(session, formSchema);
|
|
544
656
|
}
|
|
545
|
-
|
|
657
|
+
const payload = connectResponsePayload(session, {
|
|
546
658
|
transport: 'ws',
|
|
547
659
|
requestedWsUrl: target.wsUrl,
|
|
548
660
|
autoCoercedFromUrl: false,
|
|
@@ -550,9 +662,17 @@ Chromium opens **visible** by default unless \`headless: true\`. Pass \`stealth:
|
|
|
550
662
|
returnForms: input.returnForms,
|
|
551
663
|
returnPageModel: input.returnPageModel,
|
|
552
664
|
pageModelMode: input.pageModelMode,
|
|
665
|
+
blockDetection: input.blockDetection,
|
|
666
|
+
blockedSitePolicy: input.blockedSitePolicy,
|
|
667
|
+
manualHandoff: input.manualHandoff,
|
|
553
668
|
formSchema,
|
|
554
669
|
pageModelOptions,
|
|
555
|
-
|
|
670
|
+
headless: input.headless,
|
|
671
|
+
});
|
|
672
|
+
const blockedError = blockedSiteErrorIfNeeded(payload, input.blockedSitePolicy, input.manualHandoff);
|
|
673
|
+
if (blockedError)
|
|
674
|
+
return blockedError;
|
|
675
|
+
return ok(JSON.stringify(payload, null, input.detail === 'verbose' ? 2 : undefined));
|
|
556
676
|
}
|
|
557
677
|
catch (e) {
|
|
558
678
|
return err(`Failed to connect: ${formatConnectFailureMessage(e, target)}`);
|
|
@@ -577,7 +697,7 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
|
|
|
577
697
|
headless: z
|
|
578
698
|
.boolean()
|
|
579
699
|
.optional()
|
|
580
|
-
.describe('Run Chromium headless (default false
|
|
700
|
+
.describe('Run Chromium headless (default true). Set false for a visible window.'),
|
|
581
701
|
width: z.number().int().positive().optional().describe('Viewport width for the warmed browser.'),
|
|
582
702
|
height: z.number().int().positive().optional().describe('Viewport height for the warmed browser.'),
|
|
583
703
|
slowMo: z
|
|
@@ -587,6 +707,7 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
|
|
|
587
707
|
.optional()
|
|
588
708
|
.describe('Playwright slowMo (ms) for the warmed browser.'),
|
|
589
709
|
stealth: stealthInput(),
|
|
710
|
+
browserMode: browserModeInput(),
|
|
590
711
|
proxy: z
|
|
591
712
|
.object({
|
|
592
713
|
server: z
|
|
@@ -601,8 +722,11 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
|
|
|
601
722
|
})
|
|
602
723
|
.optional()
|
|
603
724
|
.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.'),
|
|
604
|
-
}, async ({ pageUrl, port, headless, width, height, slowMo, stealth, proxy }) => {
|
|
725
|
+
}, async ({ pageUrl, port, headless, width, height, slowMo, stealth, browserMode, proxy }) => {
|
|
605
726
|
try {
|
|
727
|
+
const browser = resolveBrowserStealth({ stealth, browserMode });
|
|
728
|
+
if (!browser.ok)
|
|
729
|
+
return err(browser.error);
|
|
606
730
|
const prepared = await prewarmProxy({
|
|
607
731
|
pageUrl,
|
|
608
732
|
port,
|
|
@@ -610,7 +734,7 @@ Use this when you can prepare ahead of the user-facing task so the next \`geomet
|
|
|
610
734
|
width,
|
|
611
735
|
height,
|
|
612
736
|
slowMo,
|
|
613
|
-
...(stealth !== undefined && { stealth }),
|
|
737
|
+
...(browser.stealth !== undefined && { stealth: browser.stealth }),
|
|
614
738
|
proxy,
|
|
615
739
|
});
|
|
616
740
|
return ok(JSON.stringify(prepared));
|
|
@@ -975,11 +1099,12 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
975
1099
|
url: z.string().optional().describe('Optional target URL. Use a ws:// Geometra server URL or an http(s) page URL to auto-connect before filling.'),
|
|
976
1100
|
pageUrl: z.string().optional().describe('Optional http(s) page URL to auto-connect before filling. Prefer this over url for browser pages.'),
|
|
977
1101
|
port: z.number().int().min(0).max(65535).optional().describe('Preferred local port for an auto-spawned proxy (default: ephemeral OS-assigned port).'),
|
|
978
|
-
headless: z.boolean().optional().describe('Run Chromium headless when auto-spawning a proxy (default false
|
|
1102
|
+
headless: z.boolean().optional().describe('Run Chromium headless when auto-spawning a proxy (default true). Set false for a visible window.'),
|
|
979
1103
|
width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
|
|
980
1104
|
height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
|
|
981
1105
|
slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
|
|
982
1106
|
stealth: stealthInput(),
|
|
1107
|
+
browserMode: browserModeInput(),
|
|
983
1108
|
formId: z.string().optional().describe('Optional form id from geometra_form_schema or geometra_page_model'),
|
|
984
1109
|
valuesById: formValuesRecordSchema.optional().describe('Form values keyed by stable field id from geometra_form_schema'),
|
|
985
1110
|
valuesByLabel: formValuesRecordSchema.optional().describe('Form values keyed by schema field label'),
|
|
@@ -1017,7 +1142,10 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
1017
1142
|
.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.'),
|
|
1018
1143
|
detail: detailInput(),
|
|
1019
1144
|
sessionId: sessionIdInput,
|
|
1020
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, formId, valuesById, valuesByLabel, stopOnError, failOnInvalid, includeSteps, resumeFromIndex, verifyFills, skipPreFilled, isolated, detail, sessionId }) => {
|
|
1145
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, browserMode, formId, valuesById, valuesByLabel, stopOnError, failOnInvalid, includeSteps, resumeFromIndex, verifyFills, skipPreFilled, isolated, detail, sessionId }) => {
|
|
1146
|
+
const browser = resolveBrowserStealth({ stealth, browserMode });
|
|
1147
|
+
if (!browser.ok)
|
|
1148
|
+
return err(browser.error);
|
|
1021
1149
|
const directFields = !includeSteps && !formId && Object.keys(valuesById ?? {}).length === 0
|
|
1022
1150
|
? directLabelBatchFields(valuesByLabel)
|
|
1023
1151
|
: null;
|
|
@@ -1030,7 +1158,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
1030
1158
|
width,
|
|
1031
1159
|
height,
|
|
1032
1160
|
slowMo,
|
|
1033
|
-
stealth,
|
|
1161
|
+
stealth: browser.stealth,
|
|
1034
1162
|
isolated,
|
|
1035
1163
|
awaitInitialFrame: directFields ? false : undefined,
|
|
1036
1164
|
}, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_fill_form.');
|
|
@@ -1286,11 +1414,12 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
|
|
|
1286
1414
|
url: z.string().optional().describe('Optional target URL. Use a ws:// Geometra server URL or an http(s) page URL to auto-connect before submitting.'),
|
|
1287
1415
|
pageUrl: z.string().optional().describe('Optional http(s) page URL to auto-connect before submitting. Prefer this over url for browser pages.'),
|
|
1288
1416
|
port: z.number().int().min(0).max(65535).optional().describe('Preferred local port for an auto-spawned proxy (default: ephemeral OS-assigned port).'),
|
|
1289
|
-
headless: z.boolean().optional().describe('Run Chromium headless when auto-spawning a proxy (default false
|
|
1417
|
+
headless: z.boolean().optional().describe('Run Chromium headless when auto-spawning a proxy (default true). Set false for a visible window.'),
|
|
1290
1418
|
width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
|
|
1291
1419
|
height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
|
|
1292
1420
|
slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
|
|
1293
1421
|
stealth: stealthInput(),
|
|
1422
|
+
browserMode: browserModeInput(),
|
|
1294
1423
|
isolated: z.boolean().optional().default(false).describe('When auto-connecting via pageUrl/url, request an isolated proxy. Required for safe parallel form submission.'),
|
|
1295
1424
|
formId: z.string().optional().describe('Optional form id from geometra_form_schema or geometra_page_model'),
|
|
1296
1425
|
valuesById: formValuesRecordSchema.optional().describe('Form values keyed by stable field id from geometra_form_schema'),
|
|
@@ -1304,11 +1433,14 @@ Pass \`pageUrl\`/\`url\` to auto-connect in the same call — use \`isolated: tr
|
|
|
1304
1433
|
failOnInvalid: z.boolean().optional().default(false).describe('Return an error if invalid fields remain after the submit wait resolves.'),
|
|
1305
1434
|
detail: detailInput(),
|
|
1306
1435
|
sessionId: sessionIdInput,
|
|
1307
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, isolated, formId, valuesById, valuesByLabel, submit, submitIndex, submitTimeoutMs, waitFor, skipFill, softTimeoutMs, failOnInvalid, detail, sessionId }) => {
|
|
1436
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, browserMode, isolated, formId, valuesById, valuesByLabel, submit, submitIndex, submitTimeoutMs, waitFor, skipFill, softTimeoutMs, failOnInvalid, detail, sessionId }) => {
|
|
1437
|
+
const browser = resolveBrowserStealth({ stealth, browserMode });
|
|
1438
|
+
if (!browser.ok)
|
|
1439
|
+
return err(browser.error);
|
|
1308
1440
|
const toolStartedAt = performance.now();
|
|
1309
1441
|
const effectiveSoftTimeoutMs = softTimeoutMs ?? HOST_SAFE_TOOL_TIMEOUT_MS;
|
|
1310
1442
|
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.');
|
|
1443
|
+
const resolved = await ensureToolSession({ sessionId, url, pageUrl, port, headless, width, height, slowMo, stealth: browser.stealth, isolated }, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_submit_form.');
|
|
1312
1444
|
if (!resolved.ok)
|
|
1313
1445
|
return err(resolved.error);
|
|
1314
1446
|
const session = resolved.session;
|
|
@@ -1589,11 +1721,12 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1589
1721
|
url: z.string().optional().describe('Optional target URL. Use a ws:// Geometra server URL or an http(s) page URL to auto-connect before running actions.'),
|
|
1590
1722
|
pageUrl: z.string().optional().describe('Optional http(s) page URL to auto-connect before running actions. Prefer this over url for browser pages.'),
|
|
1591
1723
|
port: z.number().int().min(0).max(65535).optional().describe('Preferred local port for an auto-spawned proxy (default: ephemeral OS-assigned port).'),
|
|
1592
|
-
headless: z.boolean().optional().describe('Run Chromium headless when auto-spawning a proxy (default false
|
|
1724
|
+
headless: z.boolean().optional().describe('Run Chromium headless when auto-spawning a proxy (default true). Set false for a visible window.'),
|
|
1593
1725
|
width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
|
|
1594
1726
|
height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
|
|
1595
1727
|
slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
|
|
1596
1728
|
stealth: stealthInput(),
|
|
1729
|
+
browserMode: browserModeInput(),
|
|
1597
1730
|
isolated: z
|
|
1598
1731
|
.boolean()
|
|
1599
1732
|
.optional()
|
|
@@ -1617,7 +1750,10 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1617
1750
|
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.'),
|
|
1618
1751
|
detail: detailInput(),
|
|
1619
1752
|
sessionId: sessionIdInput,
|
|
1620
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, isolated, actions, resumeFromIndex, softTimeoutMs, stopOnError, includeSteps, output, detail, sessionId }) => {
|
|
1753
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, browserMode, isolated, actions, resumeFromIndex, softTimeoutMs, stopOnError, includeSteps, output, detail, sessionId }) => {
|
|
1754
|
+
const browser = resolveBrowserStealth({ stealth, browserMode });
|
|
1755
|
+
if (!browser.ok)
|
|
1756
|
+
return err(browser.error);
|
|
1621
1757
|
const toolStartedAt = performance.now();
|
|
1622
1758
|
const effectiveSoftTimeoutMs = softTimeoutMs ?? HOST_SAFE_TOOL_TIMEOUT_MS;
|
|
1623
1759
|
const deadlineAt = toolStartedAt + effectiveSoftTimeoutMs;
|
|
@@ -1630,7 +1766,7 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1630
1766
|
width,
|
|
1631
1767
|
height,
|
|
1632
1768
|
slowMo,
|
|
1633
|
-
stealth,
|
|
1769
|
+
stealth: browser.stealth,
|
|
1634
1770
|
isolated,
|
|
1635
1771
|
awaitInitialFrame: canDeferInitialFrameForRunActions(actions) ? false : undefined,
|
|
1636
1772
|
}, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_run_actions.');
|
|
@@ -1800,8 +1936,11 @@ Use this first on normal HTML pages when you want to understand the page shape w
|
|
|
1800
1936
|
.optional()
|
|
1801
1937
|
.default(false)
|
|
1802
1938
|
.describe('Attach a base64 PNG viewport screenshot. Requires @geometra/proxy. Use when geometry alone is ambiguous (icon-only buttons, visual styling cues).'),
|
|
1939
|
+
blockDetection: blockDetectionInput(),
|
|
1940
|
+
blockedSitePolicy: blockedSitePolicyInput(),
|
|
1941
|
+
manualHandoff: manualHandoffInput(),
|
|
1803
1942
|
sessionId: sessionIdInput,
|
|
1804
|
-
}, async ({ maxPrimaryActions, maxSectionsPerKind, includeScreenshot, sessionId }) => {
|
|
1943
|
+
}, async ({ maxPrimaryActions, maxSectionsPerKind, includeScreenshot, blockDetection, blockedSitePolicy, manualHandoff, sessionId }) => {
|
|
1805
1944
|
const sessionResult = resolveToolSession(sessionId);
|
|
1806
1945
|
if ('error' in sessionResult)
|
|
1807
1946
|
return sessionResult.error;
|
|
@@ -1809,9 +1948,14 @@ Use this first on normal HTML pages when you want to understand the page shape w
|
|
|
1809
1948
|
const a11y = await sessionA11yWhenReady(session);
|
|
1810
1949
|
if (!a11y)
|
|
1811
1950
|
return err('No UI tree available');
|
|
1812
|
-
const model = buildPageModel(a11y, { maxPrimaryActions, maxSectionsPerKind });
|
|
1951
|
+
const model = buildPageModel(a11y, { maxPrimaryActions, maxSectionsPerKind, blockDetection });
|
|
1952
|
+
const policy = effectiveBlockedSitePolicy(blockedSitePolicy, manualHandoff);
|
|
1953
|
+
if (policy === 'error' && model.blockedSite?.detected) {
|
|
1954
|
+
return err(JSON.stringify(blockedSitePolicyErrorPayload(model.blockedSite, policy)));
|
|
1955
|
+
}
|
|
1956
|
+
const payload = applyBlockedSitePolicyPayload(model, model.blockedSite, { policy: blockedSitePolicy, manualHandoff, pageUrl: a11y.meta?.pageUrl });
|
|
1813
1957
|
const screenshot = includeScreenshot ? await captureScreenshotBase64(session) : undefined;
|
|
1814
|
-
return ok(JSON.stringify(
|
|
1958
|
+
return ok(JSON.stringify(payload), screenshot);
|
|
1815
1959
|
});
|
|
1816
1960
|
server.tool('geometra_form_schema', `Get a compact, fill-oriented schema for forms on the page. This is the preferred discovery step before geometra_fill_form.
|
|
1817
1961
|
|
|
@@ -1819,11 +1963,12 @@ Unlike geometra_expand_section, this collapses repeated radio/button groups into
|
|
|
1819
1963
|
url: z.string().optional().describe('Optional target URL. Use a ws:// Geometra server URL or an http(s) page URL to auto-connect before discovery.'),
|
|
1820
1964
|
pageUrl: z.string().optional().describe('Optional http(s) page URL to auto-connect before discovery. Prefer this over url for browser pages.'),
|
|
1821
1965
|
port: z.number().int().min(0).max(65535).optional().describe('Preferred local port for an auto-spawned proxy (default: ephemeral OS-assigned port).'),
|
|
1822
|
-
headless: z.boolean().optional().describe('Run Chromium headless when auto-spawning a proxy (default false
|
|
1966
|
+
headless: z.boolean().optional().describe('Run Chromium headless when auto-spawning a proxy (default true). Set false for a visible window.'),
|
|
1823
1967
|
width: z.number().int().positive().optional().describe('Viewport width for auto-connected sessions.'),
|
|
1824
1968
|
height: z.number().int().positive().optional().describe('Viewport height for auto-connected sessions.'),
|
|
1825
1969
|
slowMo: z.number().int().nonnegative().optional().describe('Playwright slowMo (ms) when auto-spawning a proxy.'),
|
|
1826
1970
|
stealth: stealthInput(),
|
|
1971
|
+
browserMode: browserModeInput(),
|
|
1827
1972
|
isolated: z
|
|
1828
1973
|
.boolean()
|
|
1829
1974
|
.optional()
|
|
@@ -1838,8 +1983,11 @@ Unlike geometra_expand_section, this collapses repeated radio/button groups into
|
|
|
1838
1983
|
sinceSchemaId: z.string().optional().describe('If the current schema matches this id, return changed=false without resending forms'),
|
|
1839
1984
|
format: formSchemaFormatInput(),
|
|
1840
1985
|
sessionId: sessionIdInput,
|
|
1841
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, isolated, formId, maxFields, onlyRequiredFields, onlyInvalidFields, includeOptions, includeContext, sinceSchemaId, format, sessionId }) => {
|
|
1842
|
-
const
|
|
1986
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, stealth, browserMode, isolated, formId, maxFields, onlyRequiredFields, onlyInvalidFields, includeOptions, includeContext, sinceSchemaId, format, sessionId }) => {
|
|
1987
|
+
const browser = resolveBrowserStealth({ stealth, browserMode });
|
|
1988
|
+
if (!browser.ok)
|
|
1989
|
+
return err(browser.error);
|
|
1990
|
+
const resolved = await ensureToolSession({ sessionId, url, pageUrl, port, headless, width, height, slowMo, stealth: browser.stealth, isolated }, 'Not connected. Call geometra_connect first, or pass pageUrl/url to geometra_form_schema.');
|
|
1843
1991
|
if (!resolved.ok)
|
|
1844
1992
|
return err(resolved.error);
|
|
1845
1993
|
const session = resolved.session;
|
|
@@ -2811,16 +2959,27 @@ async function stabilizeInlineFormSchemas(session, options, opts) {
|
|
|
2811
2959
|
}
|
|
2812
2960
|
function connectResponsePayload(session, opts) {
|
|
2813
2961
|
const payload = connectPayload(session, opts);
|
|
2962
|
+
const policy = opts.blockedSitePolicy ?? 'continue';
|
|
2963
|
+
const pageModelOptions = {
|
|
2964
|
+
...opts.pageModelOptions,
|
|
2965
|
+
blockDetection: opts.blockDetection,
|
|
2966
|
+
};
|
|
2967
|
+
const model = opts.blockDetection === false ? undefined : pageModelFromSession(session, pageModelOptions);
|
|
2968
|
+
const nextPayload = applyBlockedSitePolicyPayload({ ...payload }, model?.blockedSite, {
|
|
2969
|
+
policy,
|
|
2970
|
+
manualHandoff: opts.manualHandoff,
|
|
2971
|
+
pageUrl: opts.requestedPageUrl,
|
|
2972
|
+
headless: opts.headless,
|
|
2973
|
+
});
|
|
2814
2974
|
if (!opts.returnForms && !opts.returnPageModel)
|
|
2815
|
-
return
|
|
2816
|
-
const nextPayload = { ...payload };
|
|
2975
|
+
return nextPayload;
|
|
2817
2976
|
if (opts.returnForms) {
|
|
2818
2977
|
nextPayload.formSchema = formSchemaResponsePayload(session, opts.formSchema ?? {});
|
|
2819
2978
|
}
|
|
2820
2979
|
if (opts.returnPageModel) {
|
|
2821
2980
|
nextPayload.pageModel = opts.pageModelMode === 'deferred'
|
|
2822
|
-
? deferredPageModelConnectPayload(session,
|
|
2823
|
-
: pageModelResponsePayload(session,
|
|
2981
|
+
? deferredPageModelConnectPayload(session, pageModelOptions)
|
|
2982
|
+
: (model ?? pageModelResponsePayload(session, pageModelOptions));
|
|
2824
2983
|
}
|
|
2825
2984
|
return nextPayload;
|
|
2826
2985
|
}
|
|
@@ -2832,14 +2991,17 @@ function deferredPageModelConnectPayload(session, options) {
|
|
|
2832
2991
|
options: {
|
|
2833
2992
|
maxPrimaryActions: options?.maxPrimaryActions ?? 6,
|
|
2834
2993
|
maxSectionsPerKind: options?.maxSectionsPerKind ?? 8,
|
|
2994
|
+
blockDetection: options?.blockDetection !== false,
|
|
2835
2995
|
},
|
|
2836
2996
|
};
|
|
2837
2997
|
}
|
|
2838
2998
|
function pageModelResponsePayload(session, options) {
|
|
2999
|
+
return pageModelFromSession(session, options) ?? { available: false };
|
|
3000
|
+
}
|
|
3001
|
+
function pageModelFromSession(session, options) {
|
|
2839
3002
|
const a11y = sessionA11y(session);
|
|
2840
|
-
if (!a11y)
|
|
2841
|
-
return
|
|
2842
|
-
}
|
|
3003
|
+
if (!a11y)
|
|
3004
|
+
return undefined;
|
|
2843
3005
|
return buildPageModel(a11y, options);
|
|
2844
3006
|
}
|
|
2845
3007
|
async function ensureToolSession(target, missingConnectionMessage = 'Not connected. Call geometra_connect first.') {
|
package/dist/session.d.ts
CHANGED
|
@@ -142,6 +142,13 @@ export interface CaptchaDetection {
|
|
|
142
142
|
type?: 'recaptcha' | 'hcaptcha' | 'turnstile' | 'cloudflare-challenge' | 'unknown';
|
|
143
143
|
hint?: string;
|
|
144
144
|
}
|
|
145
|
+
export interface BlockedSiteDetection {
|
|
146
|
+
detected: boolean;
|
|
147
|
+
type?: 'captcha' | 'cloudflare-challenge' | 'automation-detected' | 'access-denied' | 'unsupported-browser' | 'rate-limited' | 'unknown';
|
|
148
|
+
hint?: string;
|
|
149
|
+
evidence?: string[];
|
|
150
|
+
recommendedAction?: 'manual-handoff' | 'retry-later' | 'review-site-rules';
|
|
151
|
+
}
|
|
145
152
|
export interface VerificationDetection {
|
|
146
153
|
detected: boolean;
|
|
147
154
|
type?: 'email_code' | 'sms_code' | 'security_question' | 'unknown';
|
|
@@ -160,6 +167,7 @@ export interface PageModel {
|
|
|
160
167
|
listCount: number;
|
|
161
168
|
focusableCount: number;
|
|
162
169
|
};
|
|
170
|
+
blockedSite?: BlockedSiteDetection;
|
|
163
171
|
captcha?: CaptchaDetection;
|
|
164
172
|
verification?: VerificationDetection;
|
|
165
173
|
primaryActions: PagePrimaryAction[];
|
|
@@ -539,10 +547,9 @@ export declare function connectThroughProxy(options: {
|
|
|
539
547
|
*/
|
|
540
548
|
isolated?: boolean;
|
|
541
549
|
/**
|
|
542
|
-
* BYO outbound proxy for
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
* configs never share a Chromium instance.
|
|
550
|
+
* BYO outbound proxy for Chromium. The reusable pool is partitioned by proxy
|
|
551
|
+
* identity so two callers with different proxy configs never share a
|
|
552
|
+
* Chromium instance.
|
|
546
553
|
*/
|
|
547
554
|
proxy?: SpawnProxyConfig;
|
|
548
555
|
}): Promise<Session>;
|
|
@@ -722,6 +729,7 @@ export declare function nodeContextForNode(root: A11yNode, node: A11yNode): Node
|
|
|
722
729
|
export declare function buildPageModel(root: A11yNode, options?: {
|
|
723
730
|
maxPrimaryActions?: number;
|
|
724
731
|
maxSectionsPerKind?: number;
|
|
732
|
+
blockDetection?: boolean;
|
|
725
733
|
}): PageModel;
|
|
726
734
|
export declare function buildFormSchemas(root: A11yNode, options?: FormSchemaBuildOptions): FormSchemaModel[];
|
|
727
735
|
/**
|
package/dist/session.js
CHANGED
|
@@ -441,8 +441,8 @@ function findReusableProxy(options) {
|
|
|
441
441
|
.filter(entry => entry.headless === desiredHeadless
|
|
442
442
|
&& entry.stealth === desiredStealth
|
|
443
443
|
&& entry.slowMo === desiredSlowMo
|
|
444
|
-
// Proxy partition is hard
|
|
445
|
-
// attach to a pooled direct-connection Chromium (and vice versa).
|
|
444
|
+
// Proxy partition is hard: a session with a caller-provided proxy MUST
|
|
445
|
+
// NOT attach to a pooled direct-connection Chromium (and vice versa).
|
|
446
446
|
// Different proxy credentials also get separate pool entries.
|
|
447
447
|
&& entry.proxyKey === desiredProxyKey)
|
|
448
448
|
.sort((a, b) => {
|
|
@@ -2529,6 +2529,90 @@ function detectCaptcha(root) {
|
|
|
2529
2529
|
}
|
|
2530
2530
|
return found ?? { detected: false };
|
|
2531
2531
|
}
|
|
2532
|
+
const BLOCKED_SITE_PATTERNS = [
|
|
2533
|
+
{
|
|
2534
|
+
pattern: /cloudflare.*challenge|challenge-platform|just a moment|checking your browser|cdn-cgi\/challenge/i,
|
|
2535
|
+
type: 'cloudflare-challenge',
|
|
2536
|
+
hint: 'Cloudflare challenge page detected',
|
|
2537
|
+
recommendedAction: 'manual-handoff',
|
|
2538
|
+
},
|
|
2539
|
+
{
|
|
2540
|
+
pattern: /verify (you are|that you are|you're|you.re) human|are you human|human verification|i.m not a robot|not a robot/i,
|
|
2541
|
+
type: 'captcha',
|
|
2542
|
+
hint: 'Human verification challenge detected',
|
|
2543
|
+
recommendedAction: 'manual-handoff',
|
|
2544
|
+
},
|
|
2545
|
+
{
|
|
2546
|
+
pattern: /automated access|automation detected|bot detected|bot activity|unusual traffic|suspicious traffic|browser automation/i,
|
|
2547
|
+
type: 'automation-detected',
|
|
2548
|
+
hint: 'Automation block detected',
|
|
2549
|
+
recommendedAction: 'manual-handoff',
|
|
2550
|
+
},
|
|
2551
|
+
{
|
|
2552
|
+
pattern: /access denied|forbidden|blocked from accessing|temporarily blocked|request blocked|not authorized/i,
|
|
2553
|
+
type: 'access-denied',
|
|
2554
|
+
hint: 'Access denied or request blocked page detected',
|
|
2555
|
+
recommendedAction: 'review-site-rules',
|
|
2556
|
+
},
|
|
2557
|
+
{
|
|
2558
|
+
pattern: /unsupported browser|browser is not supported|please update your browser|enable javascript/i,
|
|
2559
|
+
type: 'unsupported-browser',
|
|
2560
|
+
hint: 'Unsupported browser or JavaScript requirement detected',
|
|
2561
|
+
recommendedAction: 'manual-handoff',
|
|
2562
|
+
},
|
|
2563
|
+
{
|
|
2564
|
+
pattern: /too many requests|rate limit|temporarily unavailable|try again later/i,
|
|
2565
|
+
type: 'rate-limited',
|
|
2566
|
+
hint: 'Rate limit or temporary block detected',
|
|
2567
|
+
recommendedAction: 'retry-later',
|
|
2568
|
+
},
|
|
2569
|
+
];
|
|
2570
|
+
function detectBlockedSite(root, captcha) {
|
|
2571
|
+
if (captcha.detected) {
|
|
2572
|
+
return {
|
|
2573
|
+
detected: true,
|
|
2574
|
+
type: captcha.type === 'cloudflare-challenge' ? 'cloudflare-challenge' : 'captcha',
|
|
2575
|
+
...(captcha.hint ? { hint: captcha.hint } : {}),
|
|
2576
|
+
recommendedAction: 'manual-handoff',
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
let found;
|
|
2580
|
+
const evidence = [];
|
|
2581
|
+
const checkText = (raw) => {
|
|
2582
|
+
if (!raw || found)
|
|
2583
|
+
return;
|
|
2584
|
+
const text = raw.replace(/\s+/g, ' ').trim();
|
|
2585
|
+
if (!text)
|
|
2586
|
+
return;
|
|
2587
|
+
for (const candidate of BLOCKED_SITE_PATTERNS) {
|
|
2588
|
+
if (!candidate.pattern.test(text))
|
|
2589
|
+
continue;
|
|
2590
|
+
found = {
|
|
2591
|
+
detected: true,
|
|
2592
|
+
type: candidate.type,
|
|
2593
|
+
hint: candidate.hint,
|
|
2594
|
+
recommendedAction: candidate.recommendedAction,
|
|
2595
|
+
};
|
|
2596
|
+
evidence.push(truncateUiText(text, 140));
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
};
|
|
2600
|
+
checkText(root.meta?.pageUrl);
|
|
2601
|
+
function walk(node) {
|
|
2602
|
+
if (found)
|
|
2603
|
+
return;
|
|
2604
|
+
checkText([node.name, node.value, node.validation?.error, node.validation?.description].filter(Boolean).join(' '));
|
|
2605
|
+
for (const child of node.children)
|
|
2606
|
+
walk(child);
|
|
2607
|
+
}
|
|
2608
|
+
walk(root);
|
|
2609
|
+
if (!found)
|
|
2610
|
+
return { detected: false };
|
|
2611
|
+
return {
|
|
2612
|
+
...found,
|
|
2613
|
+
...(evidence.length > 0 ? { evidence: evidence.slice(0, 3) } : {}),
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2532
2616
|
const VERIFICATION_FIELD_PATTERN = /verif|security.?code|confirm.*(code|email)|one.?time|otp|2fa|mfa|passcode/i;
|
|
2533
2617
|
const VERIFICATION_CONTEXT_PATTERN = /sent.*(code|email|sms|text)|enter.*code|check.your.(email|phone|inbox)|we.sent|verification/i;
|
|
2534
2618
|
function detectVerification(root) {
|
|
@@ -2643,9 +2727,11 @@ export function buildPageModel(root, options) {
|
|
|
2643
2727
|
lists: sortByBounds(lists).slice(0, maxSectionsPerKind),
|
|
2644
2728
|
};
|
|
2645
2729
|
const captcha = detectCaptcha(root);
|
|
2730
|
+
const blockedSite = options?.blockDetection === false ? { detected: false } : detectBlockedSite(root, captcha);
|
|
2646
2731
|
const verification = detectVerification(root);
|
|
2647
2732
|
return {
|
|
2648
2733
|
...baseModel,
|
|
2734
|
+
...(blockedSite.detected ? { blockedSite } : {}),
|
|
2649
2735
|
...(captcha.detected ? { captcha } : {}),
|
|
2650
2736
|
...(verification.detected ? { verification } : {}),
|
|
2651
2737
|
archetypes: inferPageArchetypes(baseModel),
|
|
@@ -2844,6 +2930,9 @@ export function summarizePageModel(model, maxLines = 10) {
|
|
|
2844
2930
|
if (model.archetypes.length > 0) {
|
|
2845
2931
|
lines.push(`archetypes: ${model.archetypes.join(', ')}`);
|
|
2846
2932
|
}
|
|
2933
|
+
if (model.blockedSite?.detected) {
|
|
2934
|
+
lines.push(`blocked: ${model.blockedSite.type ?? 'unknown'}${model.blockedSite.hint ? ` - ${model.blockedSite.hint}` : ''}`);
|
|
2935
|
+
}
|
|
2847
2936
|
lines.push(`summary: ${model.summary.landmarkCount} landmarks, ${model.summary.formCount} forms, ${model.summary.dialogCount} dialogs, ${model.summary.listCount} lists, ${model.summary.focusableCount} focusable`);
|
|
2848
2937
|
for (const landmark of model.landmarks.slice(0, 3)) {
|
|
2849
2938
|
const name = landmark.name ? ` "${truncateUiText(landmark.name, 32)}"` : '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geometra/mcp",
|
|
3
|
-
"version": "1.62.
|
|
3
|
+
"version": "1.62.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.62.
|
|
35
|
+
"@geometra/proxy": "^1.62.3",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
37
37
|
"@razroo/parallel-mcp": "^0.1.0",
|
|
38
38
|
"ws": "^8.18.0",
|