@geometra/mcp 1.19.2 → 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 +1 -0
- package/dist/__tests__/connect-utils.test.js +28 -0
- package/dist/__tests__/session-model.test.js +34 -0
- package/dist/proxy-spawn.d.ts +1 -1
- package/dist/proxy-spawn.js +7 -2
- package/dist/server.js +23 -2
- package/dist/session.d.ts +7 -0
- package/dist/session.js +54 -1
- package/package.json +2 -2
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 |
|
|
@@ -89,6 +89,34 @@ describe('proxy ready helpers', () => {
|
|
|
89
89
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
|
+
it('falls back to the packaged sibling proxy dist when package exports are stale', () => {
|
|
93
|
+
const tempRoot = mkdtempSync(path.join(tmpdir(), 'geometra-proxy-stale-exports-'));
|
|
94
|
+
try {
|
|
95
|
+
const proxyDir = path.join(tempRoot, 'node_modules', '@geometra', 'proxy');
|
|
96
|
+
const mcpDistDir = path.join(tempRoot, 'node_modules', '@geometra', 'mcp', 'dist');
|
|
97
|
+
const proxyDistDir = path.join(proxyDir, 'dist');
|
|
98
|
+
const probePath = path.join(mcpDistDir, 'proxy-spawn.cjs');
|
|
99
|
+
mkdirSync(proxyDistDir, { recursive: true });
|
|
100
|
+
mkdirSync(mcpDistDir, { recursive: true });
|
|
101
|
+
writeFileSync(path.join(proxyDir, 'package.json'), JSON.stringify({
|
|
102
|
+
name: '@geometra/proxy',
|
|
103
|
+
type: 'module',
|
|
104
|
+
exports: {
|
|
105
|
+
'.': {
|
|
106
|
+
import: './dist/index.js',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}, null, 2));
|
|
110
|
+
writeFileSync(path.join(proxyDistDir, 'index.js'), 'export {};\n');
|
|
111
|
+
writeFileSync(probePath, 'module.exports = {};\n');
|
|
112
|
+
const customRequire = createRequire(probePath);
|
|
113
|
+
const scriptPath = resolveProxyScriptPathWith(customRequire, mcpDistDir);
|
|
114
|
+
expect(scriptPath).toBe(path.join(proxyDistDir, 'index.js'));
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
92
120
|
it('parses structured proxy ready JSON', () => {
|
|
93
121
|
const wsUrl = parseProxyReadySignalLine('{"type":"geometra-proxy-ready","wsUrl":"ws://127.0.0.1:41237","pageUrl":"https://example.com"}');
|
|
94
122
|
expect(wsUrl).toBe('ws://127.0.0.1:41237');
|
|
@@ -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/proxy-spawn.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ChildProcess } from 'node:child_process';
|
|
2
2
|
/** Resolve bundled @geometra/proxy CLI entry (dist/index.js). */
|
|
3
3
|
export declare function resolveProxyScriptPath(): string;
|
|
4
|
-
export declare function resolveProxyScriptPathWith(customRequire: NodeRequire): string;
|
|
4
|
+
export declare function resolveProxyScriptPathWith(customRequire: NodeRequire, moduleDir?: string): string;
|
|
5
5
|
export interface SpawnProxyParams {
|
|
6
6
|
pageUrl: string;
|
|
7
7
|
port: number;
|
package/dist/proxy-spawn.js
CHANGED
|
@@ -11,7 +11,7 @@ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
11
11
|
export function resolveProxyScriptPath() {
|
|
12
12
|
return resolveProxyScriptPathWith(require);
|
|
13
13
|
}
|
|
14
|
-
export function resolveProxyScriptPathWith(customRequire) {
|
|
14
|
+
export function resolveProxyScriptPathWith(customRequire, moduleDir = MODULE_DIR) {
|
|
15
15
|
const errors = [];
|
|
16
16
|
try {
|
|
17
17
|
const pkgJson = customRequire.resolve('@geometra/proxy/package.json');
|
|
@@ -26,7 +26,12 @@ export function resolveProxyScriptPathWith(customRequire) {
|
|
|
26
26
|
catch (err) {
|
|
27
27
|
errors.push(err instanceof Error ? err.message : String(err));
|
|
28
28
|
}
|
|
29
|
-
const
|
|
29
|
+
const packagedSiblingDist = path.resolve(moduleDir, '../../proxy/dist/index.js');
|
|
30
|
+
if (existsSync(packagedSiblingDist)) {
|
|
31
|
+
return packagedSiblingDist;
|
|
32
|
+
}
|
|
33
|
+
errors.push(`Packaged sibling fallback not found at ${packagedSiblingDist}`);
|
|
34
|
+
const workspaceDist = path.resolve(moduleDir, '../../packages/proxy/dist/index.js');
|
|
30
35
|
if (existsSync(workspaceDist)) {
|
|
31
36
|
return workspaceDist;
|
|
32
37
|
}
|
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.
|
|
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
|
+
"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.
|
|
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"
|