@geometra/mcp 1.19.3 → 1.19.4

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
@@ -28,6 +28,7 @@ Geometra proxy: Chromium → DOM geometry → same WebSocket as native →
28
28
  | `geometra_upload_files` | Attach files: auto / hidden input / native chooser / synthetic drop (`@geometra/proxy` only) |
29
29
  | `geometra_pick_listbox_option` | Pick `role=option` (React Select, Headless UI, etc.; `@geometra/proxy` only) |
30
30
  | `geometra_select_option` | Choose an option on a native `<select>` (`@geometra/proxy` only) |
31
+ | `geometra_set_checked` | Set a checkbox or radio by label instead of coordinate clicks (`@geometra/proxy` only) |
31
32
  | `geometra_wheel` | Mouse wheel / scroll (`@geometra/proxy` only) |
32
33
  | `geometra_snapshot` | Default **compact**: flat viewport-visible actionable nodes (minified JSON). `view=full` for nested tree |
33
34
  | `geometra_layout` | Raw computed geometry for every node |
@@ -188,4 +188,38 @@ describe('buildUiDelta', () => {
188
188
  expect(summary).toContain('~ ls:0.1 list "Results" items 2 -> 3');
189
189
  expect(summary).toContain('~ n:0.0 button "Save": disabled unset -> true');
190
190
  });
191
+ it('surfaces checkbox checked-state changes in semantic deltas', () => {
192
+ const before = node('group', undefined, { x: 0, y: 0, width: 640, height: 480 }, {
193
+ children: [
194
+ node('form', 'Application', { x: 20, y: 20, width: 600, height: 220 }, {
195
+ path: [0],
196
+ children: [
197
+ node('checkbox', 'New York, NY', { x: 40, y: 80, width: 24, height: 24 }, {
198
+ path: [0, 0],
199
+ focusable: true,
200
+ state: { checked: false },
201
+ }),
202
+ ],
203
+ }),
204
+ ],
205
+ });
206
+ const after = node('group', undefined, { x: 0, y: 0, width: 640, height: 480 }, {
207
+ children: [
208
+ node('form', 'Application', { x: 20, y: 20, width: 600, height: 220 }, {
209
+ path: [0],
210
+ children: [
211
+ node('checkbox', 'New York, NY', { x: 40, y: 80, width: 24, height: 24 }, {
212
+ path: [0, 0],
213
+ focusable: true,
214
+ state: { checked: true },
215
+ }),
216
+ ],
217
+ }),
218
+ ],
219
+ });
220
+ const delta = buildUiDelta(before, after);
221
+ const summary = summarizeUiDelta(delta);
222
+ expect(delta.updated.some(update => update.after.role === 'checkbox' && update.changes.includes('checked false -> true'))).toBe(true);
223
+ expect(summary).toContain('~ n:0.0 checkbox "New York, NY": checked false -> true');
224
+ });
191
225
  });
package/dist/server.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
3
  import { formatConnectFailureMessage, isHttpUrl, normalizeConnectTarget } from './connect-utils.js';
4
- import { connect, connectThroughProxy, disconnect, getSession, sendClick, sendType, sendKey, sendFileUpload, sendListboxPick, sendSelectOption, sendWheel, buildA11yTree, buildCompactUiIndex, buildPageModel, expandPageSection, buildUiDelta, hasUiDelta, nodeIdForPath, summarizeCompactIndex, summarizePageModel, summarizeUiDelta, } from './session.js';
4
+ import { connect, connectThroughProxy, disconnect, getSession, sendClick, sendType, sendKey, sendFileUpload, sendListboxPick, sendSelectOption, sendSetChecked, sendWheel, buildA11yTree, buildCompactUiIndex, buildPageModel, expandPageSection, buildUiDelta, hasUiDelta, nodeIdForPath, summarizeCompactIndex, summarizePageModel, summarizeUiDelta, } from './session.js';
5
5
  export function createServer() {
6
- const server = new McpServer({ name: 'geometra', version: '1.19.3' }, { capabilities: { tools: {} } });
6
+ const server = new McpServer({ name: 'geometra', version: '1.19.4' }, { capabilities: { tools: {} } });
7
7
  // ── connect ──────────────────────────────────────────────────
8
8
  server.tool('geometra_connect', `Connect to a Geometra WebSocket peer, or start \`geometra-proxy\` automatically for a normal web page.
9
9
 
@@ -271,6 +271,27 @@ Custom React/Vue dropdowns are not supported — open them with geometra_click a
271
271
  return err(e.message);
272
272
  }
273
273
  });
274
+ server.tool('geometra_set_checked', `Set a checkbox or radio by label. Requires \`@geometra/proxy\`.
275
+
276
+ Prefer this over raw coordinate clicks for custom forms that keep the real input visually hidden (common on Ashby, Greenhouse custom widgets, and design-system checkboxes/radios). Uses substring label matching unless exact=true.`, {
277
+ label: z.string().describe('Accessible label or visible option text to match'),
278
+ checked: z.boolean().optional().default(true).describe('Desired checked state (radios only support true)'),
279
+ exact: z.boolean().optional().describe('Exact label match'),
280
+ controlType: z.enum(['checkbox', 'radio']).optional().describe('Limit matching to checkbox or radio'),
281
+ }, async ({ label, checked, exact, controlType }) => {
282
+ const session = getSession();
283
+ if (!session)
284
+ return err('Not connected. Call geometra_connect first.');
285
+ const before = sessionA11y(session);
286
+ try {
287
+ const wait = await sendSetChecked(session, label, { checked, exact, controlType });
288
+ const summary = postActionSummary(session, before, wait);
289
+ return ok(`Set ${controlType ?? 'checkbox/radio'} "${label}" to ${String(checked ?? true)}.\n${summary}`);
290
+ }
291
+ catch (e) {
292
+ return err(e.message);
293
+ }
294
+ });
274
295
  // ── wheel / scroll (proxy) ─────────────────────────────────────
275
296
  server.tool('geometra_wheel', `Scroll the page or an element under the pointer using the mouse wheel. Requires \`@geometra/proxy\` (e.g. virtualized lists, long application forms).`, {
276
297
  deltaY: z.number().describe('Vertical scroll delta (positive scrolls down, typical step ~100)'),
package/dist/session.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface A11yNode {
12
12
  disabled?: boolean;
13
13
  expanded?: boolean;
14
14
  selected?: boolean;
15
+ checked?: boolean | 'mixed';
15
16
  };
16
17
  bounds: {
17
18
  x: number;
@@ -264,6 +265,12 @@ export declare function sendSelectOption(session: Session, x: number, y: number,
264
265
  label?: string;
265
266
  index?: number;
266
267
  }): Promise<UpdateWaitResult>;
268
+ /** Set a checkbox/radio by label instead of relying on coordinate clicks. */
269
+ export declare function sendSetChecked(session: Session, label: string, opts?: {
270
+ checked?: boolean;
271
+ exact?: boolean;
272
+ controlType?: 'checkbox' | 'radio';
273
+ }): Promise<UpdateWaitResult>;
267
274
  /** Mouse wheel / scroll. Optional `x`,`y` move pointer before scrolling. */
268
275
  export declare function sendWheel(session: Session, deltaY: number, opts?: {
269
276
  deltaX?: number;
package/dist/session.js CHANGED
@@ -215,6 +215,17 @@ export function sendSelectOption(session, x, y, option) {
215
215
  ...option,
216
216
  });
217
217
  }
218
+ /** Set a checkbox/radio by label instead of relying on coordinate clicks. */
219
+ export function sendSetChecked(session, label, opts) {
220
+ const payload = { type: 'setChecked', label };
221
+ if (opts?.checked !== undefined)
222
+ payload.checked = opts.checked;
223
+ if (opts?.exact !== undefined)
224
+ payload.exact = opts.exact;
225
+ if (opts?.controlType)
226
+ payload.controlType = opts.controlType;
227
+ return sendAndWaitForUpdate(session, payload);
228
+ }
218
229
  /** Mouse wheel / scroll. Optional `x`,`y` move pointer before scrolling. */
219
230
  export function sendWheel(session, deltaY, opts) {
220
231
  return sendAndWaitForUpdate(session, {
@@ -450,6 +461,8 @@ function cloneState(state) {
450
461
  next.expanded = state.expanded;
451
462
  if (state.selected !== undefined)
452
463
  next.selected = state.selected;
464
+ if (state.checked !== undefined)
465
+ next.checked = state.checked;
453
466
  return Object.keys(next).length > 0 ? next : undefined;
454
467
  }
455
468
  function clonePath(path) {
@@ -835,7 +848,7 @@ function diffCompactNodes(before, after) {
835
848
  }
836
849
  const beforeState = before.state ?? {};
837
850
  const afterState = after.state ?? {};
838
- for (const key of ['disabled', 'expanded', 'selected']) {
851
+ for (const key of ['disabled', 'expanded', 'selected', 'checked']) {
839
852
  if (beforeState[key] !== afterState[key]) {
840
853
  changes.push(`${key} ${formatStateValue(beforeState[key])} -> ${formatStateValue(afterState[key])}`);
841
854
  }
@@ -968,6 +981,40 @@ export function summarizeUiDelta(delta, maxLines = 14) {
968
981
  function truncateUiText(s, max) {
969
982
  return s.length > max ? s.slice(0, max - 1) + '\u2026' : s;
970
983
  }
984
+ const A11Y_ROLE_HINTS = new Set([
985
+ 'button',
986
+ 'checkbox',
987
+ 'radio',
988
+ 'switch',
989
+ 'link',
990
+ 'textbox',
991
+ 'combobox',
992
+ 'heading',
993
+ 'dialog',
994
+ 'alertdialog',
995
+ 'list',
996
+ 'listitem',
997
+ 'tab',
998
+ 'tablist',
999
+ 'tabpanel',
1000
+ ]);
1001
+ function normalizeCheckedState(value) {
1002
+ if (value === 'mixed')
1003
+ return 'mixed';
1004
+ if (value === true || value === false)
1005
+ return value;
1006
+ if (value === 'true')
1007
+ return true;
1008
+ if (value === 'false')
1009
+ return false;
1010
+ return undefined;
1011
+ }
1012
+ function normalizeA11yRoleHint(value) {
1013
+ if (typeof value !== 'string')
1014
+ return undefined;
1015
+ const normalized = value.trim().toLowerCase();
1016
+ return A11Y_ROLE_HINTS.has(normalized) ? normalized : undefined;
1017
+ }
971
1018
  function walkNode(element, layout, path) {
972
1019
  const kind = element.kind;
973
1020
  const semantic = element.semantic;
@@ -990,6 +1037,9 @@ function walkNode(element, layout, path) {
990
1037
  state.expanded = !!semantic.ariaExpanded;
991
1038
  if (semantic?.ariaSelected !== undefined)
992
1039
  state.selected = !!semantic.ariaSelected;
1040
+ const checked = normalizeCheckedState(semantic?.ariaChecked);
1041
+ if (checked !== undefined)
1042
+ state.checked = checked;
993
1043
  const children = [];
994
1044
  const elementChildren = element.children;
995
1045
  const layoutChildren = layout.children;
@@ -1013,6 +1063,9 @@ function walkNode(element, layout, path) {
1013
1063
  function inferRole(kind, semantic, handlers) {
1014
1064
  if (semantic?.role)
1015
1065
  return semantic.role;
1066
+ const hintedRole = normalizeA11yRoleHint(semantic?.a11yRoleHint);
1067
+ if (hintedRole)
1068
+ return hintedRole;
1016
1069
  const tag = semantic?.tag;
1017
1070
  if (kind === 'text') {
1018
1071
  if (tag && /^h[1-6]$/.test(tag))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.19.3",
3
+ "version": "1.19.4",
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",
@@ -30,7 +30,7 @@
30
30
  "ui-testing"
31
31
  ],
32
32
  "dependencies": {
33
- "@geometra/proxy": "1.19.3",
33
+ "@geometra/proxy": "1.19.4",
34
34
  "@modelcontextprotocol/sdk": "^1.12.1",
35
35
  "ws": "^8.18.0",
36
36
  "zod": "^3.23.0"