@geometra/mcp 1.19.20 → 1.19.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,25 +24,28 @@ 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; can inline `formSchema` and/or `pageModel` for lower-turn starts |
28
- | `geometra_query` | Find elements by stable id, role, name, text content, ancestor/prompt context, current value, or semantic state such as `invalid`, `required`, or `busy` |
27
+ | `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; can inline `formSchema` and/or `pageModel`, or defer the page model for a faster first connect response |
28
+ | `geometra_prepare_browser` | Pre-launch and pre-navigate a reusable proxy/browser for `pageUrl` without creating an active session; best when the agent can prepare before the user-facing task starts |
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` |
29
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
+ | `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` |
30
32
  | `geometra_form_schema` | Compact, fill-oriented form schema with stable field ids and collapsed radio/button groups; can auto-connect from `pageUrl` / `url` |
31
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 |
32
- | `geometra_fill_fields` | Fill labeled text/choice/toggle/file fields in one MCP call; can return final-only status for the smallest responses |
33
- | `geometra_run_actions` | Execute a batch of high-level actions in one MCP round trip and get one consolidated result, with optional final-only output |
34
- | `geometra_page_model` | Summary-first webpage model: archetypes, stable section ids, counts, top-level sections, primary actions |
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
+ | `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, and primary actions with nearby section/item context when available |
37
+ | `geometra_find_action` | Resolve a repeated button/link by action label plus optional `sectionText`, `promptText`, or `itemText` before clicking |
35
38
  | `geometra_expand_section` | Expand one form/dialog/list/landmark from `geometra_page_model` on demand, with paging/filtering for long sections |
36
- | `geometra_reveal` | Scroll until a matching node is visible instead of guessing wheel deltas |
37
- | `geometra_click` | Click by coordinates or semantic target, optionally waiting for a post-click semantic condition |
39
+ | `geometra_reveal` | Scroll until a matching node is visible instead of guessing wheel deltas; auto-scales reveal steps for tall forms when omitted |
40
+ | `geometra_click` | Click by coordinates or semantic target, including repeated-action disambiguation with `promptText`, `sectionText`, or `itemText`, optionally waiting for a post-click semantic condition |
38
41
  | `geometra_type` | Type text into the focused element |
39
42
  | `geometra_key` | Send special keys (Enter, Tab, Escape, arrows) |
40
43
  | `geometra_upload_files` | Attach files: labeled field / auto / hidden input / native chooser / synthetic drop (`@geometra/proxy` only) |
41
- | `geometra_pick_listbox_option` | Pick an option from a custom dropdown/searchable combobox; can open by field label (`@geometra/proxy` only) |
42
- | `geometra_select_option` | Choose an option on a native `<select>` (`@geometra/proxy` only) |
44
+ | `geometra_pick_listbox_option` | Pick an option from a custom dropdown/searchable combobox; can open by field label, falls back to keyboard selection, and returns visible option hints on failure (`@geometra/proxy` only) |
45
+ | `geometra_select_option` | Choose an option on a native `<select>` only; use `geometra_pick_listbox_option` for custom dropdowns (`@geometra/proxy` only) |
43
46
  | `geometra_set_checked` | Set a checkbox or radio by label instead of coordinate clicks (`@geometra/proxy` only) |
44
47
  | `geometra_wheel` | Mouse wheel / scroll (`@geometra/proxy` only) |
45
- | `geometra_snapshot` | Default **compact**: flat viewport-visible actionable nodes (minified JSON). `view=full` for nested tree |
48
+ | `geometra_snapshot` | Default **compact**: flat viewport-visible actionable nodes (minified JSON). `view=full` for nested tree, `view=form-required` for required fields including offscreen ones |
46
49
  | `geometra_layout` | Raw computed geometry for every node |
47
50
  | `geometra_disconnect` | Close the connection |
48
51
 
@@ -50,6 +53,20 @@ Proxy-backed sessions stay warm by default on disconnect, and MCP now keeps a sm
50
53
 
51
54
  The tool input is **strict**: unknown keys are rejected (so mistaken names like `textGone` fail fast instead of being ignored). To wait until copy such as “Parsing…” or “Parsing your resume” is **gone**, pass a substring that matches the banner in **`text`** and set **`present`** to **`false`**. Example: `{ "text": "Parsing", "present": false }` (adjust the substring per site).
52
55
 
56
+ For the common resume-upload case, prefer **`geometra_wait_for_resume_parse`** (optional `text`, default `"Parsing"`; optional `timeoutMs`, default `60000`) so agents do not have to remember `present: false`.
57
+
58
+ ### Custom dropdowns and searchable comboboxes
59
+
60
+ Use **`geometra_pick_listbox_option`** for React Select / Radix / Headless UI / custom ATS dropdowns. Prefer **`fieldLabel`** so MCP opens the right combobox semantically, and pass **`query`** when the dropdown is searchable. If click-selection cannot be confirmed, the proxy now falls back to keyboard navigation for editable comboboxes.
61
+
62
+ When selection fails, the error payload includes a capped **`visibleOptions`** list so an agent can retry with a real label instead of blindly guessing. Use **`geometra_select_option`** only for native HTML `<select>` controls.
63
+
64
+ ### Long forms and offscreen required fields
65
+
66
+ For tall application forms, **`geometra_snapshot({ view: "form-required" })`** returns required fields even when they are offscreen, with **bounds**, **visibility**, and **scrollHint** data. This is useful for “what required fields are left?” without paging through a full tree.
67
+
68
+ When a target is below the fold, **`geometra_reveal`** and semantic click actions auto-scale reveal steps when `maxSteps` / `maxRevealSteps` are omitted, so long forms do not fail after a hard-coded short scroll budget. If you already have schema ids, **`geometra_fill_fields`** can now use `fieldId` directly for text / choice / toggle entries without repeating `fieldLabel` / `label`.
69
+
53
70
  ## Setup
54
71
 
55
72
  <details>
@@ -304,13 +321,15 @@ Agent: geometra_query({ role: "button", name: "Save" })
304
321
  1. The MCP server connects to a WebSocket peer that speaks GEOM v1 (`frame` with `layout` + `tree`, optional `patch` updates).
305
322
  2. It receives the computed layout (`{ x, y, width, height }` for every node) and the UI tree (`kind`, `semantic`, `props`, `handlers`, `children`).
306
323
  3. It builds an accessibility tree from that data — roles, names, focusable state, bounds.
307
- 4. **`geometra_snapshot`** defaults to a **compact** flat list of viewport-visible actionable nodes (minified JSON) to reduce LLM tokens; use `view: "full"` for the complete nested tree.
324
+ 4. **`geometra_snapshot`** defaults to a **compact** flat list of viewport-visible actionable nodes (minified JSON) to reduce LLM tokens; use `view: "full"` for the complete nested tree or `view: "form-required"` for offscreen required fields plus scroll hints.
308
325
  5. **`geometra_form_schema`** is the compact form-specific path: stable field ids, required/invalid state, current values, and collapsed choice groups without layout-heavy section detail. It can auto-connect when you pass `pageUrl` / `url`.
309
326
  6. **`geometra_fill_form`** turns a compact values object into semantic field operations server-side, so the model does not need to emit one tool call per field. It can also auto-connect, which collapses “open page + fill known values” into a single tool call.
310
- 7. **`geometra_page_model`** is still the right summary-first path for non-form exploration: page archetypes, stable section ids, counts, top-level landmarks/forms/dialogs/lists, and a few primary actions.
311
- 8. **`geometra_expand_section`** fetches richer details only for the section you care about (fields, actions, headings, nested lists, list items, text preview).
312
- 9. After interactions, action tools return a **semantic delta** when possible (dialogs opened/closed, forms appeared/removed, list counts changed, named/focusable nodes added/removed/updated). If nothing meaningful changed, they fall back to a short current-UI overview.
313
- 10. After each interaction, the peer sends updated geometry (full `frame` or `patch`) the MCP tools interpret that into compact summaries.
327
+ 7. **`geometra_fill_fields`** is the lower-level field-intent path when you need direct control; text / choice / toggle entries can now resolve `fieldId` from `geometra_form_schema` without repeating labels.
328
+ 8. **`geometra_page_model`** is still the right summary-first path for non-form exploration: page archetypes, stable section ids, counts, top-level landmarks/forms/dialogs/lists, and a few primary actions with nearby context.
329
+ 9. **`geometra_find_action`** is the narrow repeated-action resolver when the page has many identical buttons/links and you want one exact target by `itemText`, `sectionText`, or `promptText`.
330
+ 10. **`geometra_expand_section`** fetches richer details only for the section you care about (fields, actions, headings, nested lists, list items, text preview).
331
+ 11. After interactions, action tools return a **semantic delta** when possible (dialogs opened/closed, forms appeared/removed, list counts changed, named/focusable nodes added/removed/updated). If nothing meaningful changed, they fall back to a short current-UI overview.
332
+ 12. After each interaction, the peer sends updated geometry (full `frame` or `patch`) — the MCP tools interpret that into compact summaries.
314
333
 
315
334
  ## Long Forms
316
335
 
@@ -319,11 +338,15 @@ For long application flows, prefer one of these patterns:
319
338
  1. `geometra_fill_form({ pageUrl, valuesByLabel })` when you already know the fields you want to set
320
339
  2. otherwise `geometra_form_schema`
321
340
  3. then `geometra_fill_form`
322
- 3. `geometra_reveal` for far-below-fold targets such as submit buttons
323
- 4. `geometra_click({ ..., waitFor: ... })` when one action should also wait for the next semantic state
324
- 5. `geometra_run_actions` when you need mixed navigation + waits + field entry
325
- 6. `geometra_connect({ pageUrl, returnPageModel: true })` when you want connect + summary-first exploration in one turn
326
- 7. `geometra_page_model` + `geometra_expand_section` when you are still exploring the page rather than filling it
341
+ 4. `geometra_snapshot({ view: "form-required" })` when you need the remaining required fields including offscreen ones
342
+ 5. `geometra_reveal` for far-below-fold targets such as submit buttons (auto-scales reveal steps when you omit `maxSteps`)
343
+ 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
+ 8. `geometra_run_actions` when you need mixed navigation + waits + field entry, especially with `pageUrl` / `url` for a one-call flow
346
+ 9. `geometra_connect({ pageUrl, returnPageModel: true })` when you want connect + summary-first exploration in one turn
347
+ 10. `geometra_connect({ pageUrl, returnPageModel: true, pageModelMode: "deferred" })` when first-response latency matters more than inlining the page model; follow with `geometra_page_model`
348
+ 11. `geometra_find_action` or `geometra_click({ name, itemText / sectionText / promptText, ... })` when the target action repeats across cards/rows
349
+ 12. `geometra_page_model` + `geometra_expand_section` when you are still exploring the page rather than filling it
327
350
 
328
351
  Typical batch:
329
352
 
@@ -338,12 +361,14 @@ Typical batch:
338
361
  }
339
362
  ```
340
363
 
341
- Single action tools now default to terse summaries. Pass `detail: "verbose"` when you need a fuller current-UI fallback for debugging.
364
+ Single action tools default to short human-readable summaries (`detail: "minimal"`). Use `detail: "terse"` for the smallest machine-friendly JSON responses, or `detail: "verbose"` when you need a fuller current-UI fallback for debugging.
342
365
 
343
366
  For the smallest long-form responses, prefer:
344
367
 
345
- 1. `detail: "minimal"` for structured step metadata instead of narrated deltas
368
+ 1. `detail: "terse"` for compact machine-friendly action responses
346
369
  2. `includeSteps: false` when you only need aggregate success/error counts plus the final validation/state payload
370
+ 3. `output: "final"` on `geometra_run_actions` when you only need completion state plus the final semantic signals
371
+ 4. `geometra_prepare_browser` before the flow when you can shift browser launch out of the user-visible task window
347
372
 
348
373
  Typical low-token form fill:
349
374
 
@@ -8,7 +8,7 @@ vi.mock('../proxy-spawn.js', () => ({
8
8
  startEmbeddedGeometraProxy: mockState.startEmbeddedGeometraProxy,
9
9
  spawnGeometraProxy: mockState.spawnGeometraProxy,
10
10
  }));
11
- const { connectThroughProxy, disconnect } = await import('../session.js');
11
+ const { connectThroughProxy, disconnect, prewarmProxy } = await import('../session.js');
12
12
  function frame(pageUrl) {
13
13
  return {
14
14
  type: 'frame',
@@ -28,12 +28,17 @@ function frame(pageUrl) {
28
28
  async function createProxyPeer(options) {
29
29
  const wss = new WebSocketServer({ port: 0 });
30
30
  wss.on('connection', ws => {
31
- ws.send(JSON.stringify(frame(options?.pageUrl ?? 'https://jobs.example.com/original')));
31
+ if (options?.sendInitialFrame !== false) {
32
+ ws.send(JSON.stringify(frame(options?.pageUrl ?? 'https://jobs.example.com/original')));
33
+ }
32
34
  ws.on('message', raw => {
33
35
  const msg = JSON.parse(String(raw));
34
36
  if (msg.type === 'navigate') {
35
37
  options?.onNavigate?.(ws, msg);
36
38
  }
39
+ else if (msg.type === 'resize') {
40
+ options?.onResize?.(ws, msg);
41
+ }
37
42
  });
38
43
  });
39
44
  const port = await new Promise((resolve, reject) => {
@@ -78,6 +83,7 @@ describe('connectThroughProxy recovery', () => {
78
83
  });
79
84
  const staleRuntime = {
80
85
  wsUrl: stalePeer.wsUrl,
86
+ ready: Promise.resolve(),
81
87
  closed: false,
82
88
  close: vi.fn(async () => {
83
89
  staleRuntime.closed = true;
@@ -85,6 +91,7 @@ describe('connectThroughProxy recovery', () => {
85
91
  };
86
92
  const freshRuntime = {
87
93
  wsUrl: freshPeer.wsUrl,
94
+ ready: Promise.resolve(),
88
95
  closed: false,
89
96
  close: vi.fn(async () => {
90
97
  freshRuntime.closed = true;
@@ -124,6 +131,7 @@ describe('connectThroughProxy recovery', () => {
124
131
  });
125
132
  const headedRuntime = {
126
133
  wsUrl: headedPeer.wsUrl,
134
+ ready: Promise.resolve(),
127
135
  closed: false,
128
136
  close: vi.fn(async () => {
129
137
  headedRuntime.closed = true;
@@ -131,6 +139,7 @@ describe('connectThroughProxy recovery', () => {
131
139
  };
132
140
  const headlessRuntime = {
133
141
  wsUrl: headlessPeer.wsUrl,
142
+ ready: Promise.resolve(),
134
143
  closed: false,
135
144
  close: vi.fn(async () => {
136
145
  headlessRuntime.closed = true;
@@ -170,4 +179,84 @@ describe('connectThroughProxy recovery', () => {
170
179
  await closePeer(headlessPeer.wss);
171
180
  }
172
181
  });
182
+ it('can prewarm a reusable proxy before the first measured task', async () => {
183
+ const preparedPeer = await createProxyPeer({
184
+ pageUrl: 'https://jobs.example.com/prepared',
185
+ });
186
+ const preparedRuntime = {
187
+ wsUrl: preparedPeer.wsUrl,
188
+ ready: Promise.resolve(),
189
+ closed: false,
190
+ close: vi.fn(async () => {
191
+ preparedRuntime.closed = true;
192
+ }),
193
+ };
194
+ mockState.startEmbeddedGeometraProxy.mockResolvedValue({
195
+ runtime: preparedRuntime,
196
+ wsUrl: preparedPeer.wsUrl,
197
+ });
198
+ mockState.spawnGeometraProxy.mockRejectedValue(new Error('spawn fallback should not be used'));
199
+ try {
200
+ const prepared = await prewarmProxy({
201
+ pageUrl: 'https://jobs.example.com/prepared',
202
+ headless: true,
203
+ });
204
+ expect(prepared).toMatchObject({
205
+ prepared: true,
206
+ reused: false,
207
+ transport: 'embedded',
208
+ pageUrl: 'https://jobs.example.com/prepared',
209
+ });
210
+ const session = await connectThroughProxy({
211
+ pageUrl: 'https://jobs.example.com/prepared',
212
+ headless: true,
213
+ });
214
+ expect(session.proxyRuntime).toBe(preparedRuntime);
215
+ expect(mockState.startEmbeddedGeometraProxy).toHaveBeenCalledTimes(1);
216
+ expect(mockState.spawnGeometraProxy).not.toHaveBeenCalled();
217
+ }
218
+ finally {
219
+ disconnect({ closeProxy: true });
220
+ expect(preparedRuntime.close).toHaveBeenCalledTimes(1);
221
+ await closePeer(preparedPeer.wss);
222
+ }
223
+ });
224
+ it('starts without an eager initial extract when the caller defers the first frame', async () => {
225
+ const lazyPeer = await createProxyPeer({
226
+ pageUrl: 'https://jobs.example.com/lazy',
227
+ sendInitialFrame: false,
228
+ });
229
+ const lazyRuntime = {
230
+ wsUrl: lazyPeer.wsUrl,
231
+ ready: Promise.resolve(),
232
+ closed: false,
233
+ close: vi.fn(async () => {
234
+ lazyRuntime.closed = true;
235
+ }),
236
+ };
237
+ mockState.startEmbeddedGeometraProxy.mockResolvedValueOnce({
238
+ runtime: lazyRuntime,
239
+ wsUrl: lazyPeer.wsUrl,
240
+ });
241
+ mockState.spawnGeometraProxy.mockRejectedValue(new Error('spawn fallback should not be used'));
242
+ try {
243
+ const session = await connectThroughProxy({
244
+ pageUrl: 'https://jobs.example.com/lazy',
245
+ headless: true,
246
+ awaitInitialFrame: false,
247
+ });
248
+ expect(session.proxyRuntime).toBe(lazyRuntime);
249
+ expect(mockState.startEmbeddedGeometraProxy).toHaveBeenCalledWith(expect.objectContaining({
250
+ pageUrl: 'https://jobs.example.com/lazy',
251
+ headless: true,
252
+ eagerInitialExtract: false,
253
+ }));
254
+ expect(session.tree).toBeNull();
255
+ }
256
+ finally {
257
+ disconnect({ closeProxy: true });
258
+ expect(lazyRuntime.close).toHaveBeenCalledTimes(1);
259
+ await closePeer(lazyPeer.wss);
260
+ }
261
+ });
173
262
  });