@geometra/mcp 1.19.10 → 1.19.11
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 +143 -8
- package/dist/__tests__/server-batch-results.test.d.ts +1 -0
- package/dist/__tests__/server-batch-results.test.js +206 -0
- package/dist/server.js +254 -68
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,8 +21,8 @@ Geometra proxy: Chromium → DOM geometry → same WebSocket as native →
|
|
|
21
21
|
| `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; `url: "https://…"` is auto-coerced onto the proxy path |
|
|
22
22
|
| `geometra_query` | Find elements by stable id, role, name, text content, current value, or semantic state such as `invalid`, `required`, or `busy` |
|
|
23
23
|
| `geometra_wait_for` | Wait for a semantic condition instead of guessing sleeps (`busy`, `disabled`, alerts, values, etc.) |
|
|
24
|
-
| `geometra_fill_fields` | Fill labeled text/choice/toggle/file fields in one MCP call |
|
|
25
|
-
| `geometra_run_actions` | Execute a batch of high-level actions in one MCP round trip and get one consolidated result |
|
|
24
|
+
| `geometra_fill_fields` | Fill labeled text/choice/toggle/file fields in one MCP call; can return final-only status for the smallest responses |
|
|
25
|
+
| `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 |
|
|
26
26
|
| `geometra_page_model` | Summary-first webpage model: archetypes, stable section ids, counts, top-level sections, primary actions |
|
|
27
27
|
| `geometra_expand_section` | Expand one form/dialog/list/landmark from `geometra_page_model` on demand |
|
|
28
28
|
| `geometra_click` | Click an element by coordinates |
|
|
@@ -39,27 +39,155 @@ Geometra proxy: Chromium → DOM geometry → same WebSocket as native →
|
|
|
39
39
|
|
|
40
40
|
## Setup
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
<details>
|
|
43
|
+
<summary>Claude Code</summary>
|
|
43
44
|
|
|
45
|
+
**One-line install:**
|
|
44
46
|
```bash
|
|
45
|
-
claude mcp add geometra -- npx @geometra/mcp
|
|
47
|
+
claude mcp add geometra -- npx -y @geometra/mcp
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
**Uninstall:**
|
|
51
|
+
```bash
|
|
52
|
+
claude mcp remove geometra
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or manually add to `.mcp.json` (project-level) or `~/.claude/settings.json` (global):
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"geometra": {
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": ["-y", "@geometra/mcp"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To uninstall manually, remove the `geometra` entry from the config file.
|
|
68
|
+
|
|
69
|
+
</details>
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary>Claude Desktop</summary>
|
|
73
|
+
|
|
74
|
+
Add to your Claude Desktop MCP config:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"geometra": {
|
|
80
|
+
"command": "npx",
|
|
81
|
+
"args": ["-y", "@geometra/mcp"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
To uninstall, remove the `geometra` entry from the config file.
|
|
88
|
+
|
|
89
|
+
</details>
|
|
90
|
+
|
|
91
|
+
<details>
|
|
92
|
+
<summary>OpenAI Codex</summary>
|
|
93
|
+
|
|
94
|
+
Add to your Codex MCP configuration:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"geometra": {
|
|
100
|
+
"command": "npx",
|
|
101
|
+
"args": ["-y", "@geometra/mcp"]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
To uninstall, remove the `geometra` entry from the config file.
|
|
108
|
+
|
|
109
|
+
</details>
|
|
110
|
+
|
|
111
|
+
<details>
|
|
112
|
+
<summary>Cursor</summary>
|
|
49
113
|
|
|
50
|
-
Add to
|
|
114
|
+
Open Settings → MCP → Add new MCP server, or add to `.cursor/mcp.json`:
|
|
51
115
|
|
|
52
116
|
```json
|
|
53
117
|
{
|
|
54
118
|
"mcpServers": {
|
|
55
119
|
"geometra": {
|
|
56
120
|
"command": "npx",
|
|
57
|
-
"args": ["@geometra/mcp"]
|
|
121
|
+
"args": ["-y", "@geometra/mcp"]
|
|
58
122
|
}
|
|
59
123
|
}
|
|
60
124
|
}
|
|
61
125
|
```
|
|
62
126
|
|
|
127
|
+
To uninstall, remove the entry from MCP settings.
|
|
128
|
+
|
|
129
|
+
</details>
|
|
130
|
+
|
|
131
|
+
<details>
|
|
132
|
+
<summary>Windsurf</summary>
|
|
133
|
+
|
|
134
|
+
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"mcpServers": {
|
|
139
|
+
"geometra": {
|
|
140
|
+
"command": "npx",
|
|
141
|
+
"args": ["-y", "@geometra/mcp"]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
To uninstall, remove the entry from the config file.
|
|
148
|
+
|
|
149
|
+
</details>
|
|
150
|
+
|
|
151
|
+
<details>
|
|
152
|
+
<summary>VS Code / Copilot</summary>
|
|
153
|
+
|
|
154
|
+
**One-line install:**
|
|
155
|
+
```bash
|
|
156
|
+
code --add-mcp '{"name":"geometra","command":"npx","args":["-y","@geometra/mcp"]}'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Or add to `.vscode/mcp.json`:
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"servers": {
|
|
163
|
+
"geometra": {
|
|
164
|
+
"command": "npx",
|
|
165
|
+
"args": ["-y", "@geometra/mcp"]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
To uninstall, remove the entry from MCP settings or delete the server from the MCP panel.
|
|
172
|
+
|
|
173
|
+
</details>
|
|
174
|
+
|
|
175
|
+
<details>
|
|
176
|
+
<summary>Other MCP clients</summary>
|
|
177
|
+
|
|
178
|
+
Any MCP client that supports stdio transport can use Geometra. The server config is:
|
|
179
|
+
|
|
180
|
+
```json
|
|
181
|
+
{
|
|
182
|
+
"command": "npx",
|
|
183
|
+
"args": ["-y", "@geometra/mcp"]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
To uninstall, remove the server entry from your client's MCP configuration.
|
|
188
|
+
|
|
189
|
+
</details>
|
|
190
|
+
|
|
63
191
|
### From source (this repo)
|
|
64
192
|
|
|
65
193
|
```bash
|
|
@@ -200,6 +328,11 @@ Typical batch:
|
|
|
200
328
|
|
|
201
329
|
Single action tools now default to terse summaries. Pass `detail: "verbose"` when you need a fuller current-UI fallback for debugging.
|
|
202
330
|
|
|
331
|
+
For the smallest long-form responses, prefer:
|
|
332
|
+
|
|
333
|
+
1. `detail: "minimal"` for structured step metadata instead of narrated deltas
|
|
334
|
+
2. `includeSteps: false` when you only need aggregate success/error counts plus the final validation/state payload
|
|
335
|
+
|
|
203
336
|
Typical field fill:
|
|
204
337
|
|
|
205
338
|
```json
|
|
@@ -211,7 +344,9 @@ Typical field fill:
|
|
|
211
344
|
{ "kind": "choice", "fieldLabel": "Will you require sponsorship?", "value": "No" },
|
|
212
345
|
{ "kind": "file", "fieldLabel": "Resume", "paths": ["/Users/you/resume.pdf"] }
|
|
213
346
|
],
|
|
214
|
-
"failOnInvalid": true
|
|
347
|
+
"failOnInvalid": true,
|
|
348
|
+
"detail": "minimal",
|
|
349
|
+
"includeSteps": false
|
|
215
350
|
}
|
|
216
351
|
```
|
|
217
352
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
function node(role, name, options) {
|
|
3
|
+
return {
|
|
4
|
+
role,
|
|
5
|
+
...(name ? { name } : {}),
|
|
6
|
+
...(options?.value ? { value: options.value } : {}),
|
|
7
|
+
...(options?.state ? { state: options.state } : {}),
|
|
8
|
+
...(options?.validation ? { validation: options.validation } : {}),
|
|
9
|
+
...(options?.meta ? { meta: options.meta } : {}),
|
|
10
|
+
bounds: { x: 0, y: 0, width: 120, height: 40 },
|
|
11
|
+
path: options?.path ?? [],
|
|
12
|
+
children: options?.children ?? [],
|
|
13
|
+
focusable: role !== 'group',
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const mockState = vi.hoisted(() => ({
|
|
17
|
+
currentA11yRoot: node('group', undefined, {
|
|
18
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
19
|
+
}),
|
|
20
|
+
session: {
|
|
21
|
+
tree: { kind: 'box' },
|
|
22
|
+
layout: { x: 0, y: 0, width: 1280, height: 800, children: [] },
|
|
23
|
+
url: 'ws://127.0.0.1:3200',
|
|
24
|
+
updateRevision: 1,
|
|
25
|
+
},
|
|
26
|
+
sendClick: vi.fn(async () => ({ status: 'updated', timeoutMs: 2000 })),
|
|
27
|
+
sendType: vi.fn(async () => ({ status: 'updated', timeoutMs: 2000 })),
|
|
28
|
+
sendKey: vi.fn(async () => ({ status: 'updated', timeoutMs: 2000 })),
|
|
29
|
+
sendFileUpload: vi.fn(async () => ({ status: 'updated', timeoutMs: 8000 })),
|
|
30
|
+
sendFieldText: vi.fn(async () => ({ status: 'updated', timeoutMs: 2000 })),
|
|
31
|
+
sendFieldChoice: vi.fn(async () => ({ status: 'updated', timeoutMs: 2000 })),
|
|
32
|
+
sendListboxPick: vi.fn(async () => ({ status: 'updated', timeoutMs: 4500 })),
|
|
33
|
+
sendSelectOption: vi.fn(async () => ({ status: 'updated', timeoutMs: 2000 })),
|
|
34
|
+
sendSetChecked: vi.fn(async () => ({ status: 'updated', timeoutMs: 2000 })),
|
|
35
|
+
sendWheel: vi.fn(async () => ({ status: 'updated', timeoutMs: 2000 })),
|
|
36
|
+
waitForUiCondition: vi.fn(async () => true),
|
|
37
|
+
}));
|
|
38
|
+
vi.mock('../session.js', () => ({
|
|
39
|
+
connect: vi.fn(),
|
|
40
|
+
connectThroughProxy: vi.fn(),
|
|
41
|
+
disconnect: vi.fn(),
|
|
42
|
+
getSession: vi.fn(() => mockState.session),
|
|
43
|
+
sendClick: mockState.sendClick,
|
|
44
|
+
sendType: mockState.sendType,
|
|
45
|
+
sendKey: mockState.sendKey,
|
|
46
|
+
sendFileUpload: mockState.sendFileUpload,
|
|
47
|
+
sendFieldText: mockState.sendFieldText,
|
|
48
|
+
sendFieldChoice: mockState.sendFieldChoice,
|
|
49
|
+
sendListboxPick: mockState.sendListboxPick,
|
|
50
|
+
sendSelectOption: mockState.sendSelectOption,
|
|
51
|
+
sendSetChecked: mockState.sendSetChecked,
|
|
52
|
+
sendWheel: mockState.sendWheel,
|
|
53
|
+
buildA11yTree: vi.fn(() => mockState.currentA11yRoot),
|
|
54
|
+
buildCompactUiIndex: vi.fn(() => ({ nodes: [], context: {} })),
|
|
55
|
+
buildPageModel: vi.fn(() => ({
|
|
56
|
+
viewport: { width: 1280, height: 800 },
|
|
57
|
+
archetypes: ['form'],
|
|
58
|
+
summary: { landmarkCount: 0, formCount: 1, dialogCount: 0, listCount: 0, focusableCount: 2 },
|
|
59
|
+
primaryActions: [],
|
|
60
|
+
landmarks: [],
|
|
61
|
+
forms: [],
|
|
62
|
+
dialogs: [],
|
|
63
|
+
lists: [],
|
|
64
|
+
})),
|
|
65
|
+
expandPageSection: vi.fn(() => null),
|
|
66
|
+
buildUiDelta: vi.fn(() => ({})),
|
|
67
|
+
hasUiDelta: vi.fn(() => false),
|
|
68
|
+
nodeIdForPath: vi.fn((path) => `n:${path.length > 0 ? path.join('.') : 'root'}`),
|
|
69
|
+
summarizeCompactIndex: vi.fn(() => ''),
|
|
70
|
+
summarizePageModel: vi.fn(() => ''),
|
|
71
|
+
summarizeUiDelta: vi.fn(() => ''),
|
|
72
|
+
waitForUiCondition: mockState.waitForUiCondition,
|
|
73
|
+
}));
|
|
74
|
+
const { createServer } = await import('../server.js');
|
|
75
|
+
function getToolHandler(name) {
|
|
76
|
+
const server = createServer();
|
|
77
|
+
return server._registeredTools[name].handler;
|
|
78
|
+
}
|
|
79
|
+
describe('batch MCP result shaping', () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.clearAllMocks();
|
|
82
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
83
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 420 },
|
|
84
|
+
children: [
|
|
85
|
+
node('textbox', 'Mission', {
|
|
86
|
+
value: 'Ship calm developer tools across browsers and platforms.',
|
|
87
|
+
path: [0],
|
|
88
|
+
}),
|
|
89
|
+
node('textbox', 'Email', {
|
|
90
|
+
value: 'taylor@example.com',
|
|
91
|
+
path: [1],
|
|
92
|
+
}),
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('keeps fill_fields minimal output structured and does not echo long essay text', async () => {
|
|
97
|
+
const longAnswer = 'A'.repeat(180);
|
|
98
|
+
const handler = getToolHandler('geometra_fill_fields');
|
|
99
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
100
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 420 },
|
|
101
|
+
children: [
|
|
102
|
+
node('textbox', 'Mission', { value: longAnswer, path: [0] }),
|
|
103
|
+
node('textbox', 'Email', { value: 'taylor@example.com', path: [1] }),
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
const result = await handler({
|
|
107
|
+
fields: [
|
|
108
|
+
{ kind: 'text', fieldLabel: 'Mission', value: longAnswer },
|
|
109
|
+
{ kind: 'text', fieldLabel: 'Email', value: 'taylor@example.com' },
|
|
110
|
+
],
|
|
111
|
+
stopOnError: true,
|
|
112
|
+
failOnInvalid: false,
|
|
113
|
+
includeSteps: true,
|
|
114
|
+
detail: 'minimal',
|
|
115
|
+
});
|
|
116
|
+
const text = result.content[0].text;
|
|
117
|
+
const payload = JSON.parse(text);
|
|
118
|
+
const steps = payload.steps;
|
|
119
|
+
expect(text).not.toContain(longAnswer);
|
|
120
|
+
expect(payload).toMatchObject({
|
|
121
|
+
completed: true,
|
|
122
|
+
fieldCount: 2,
|
|
123
|
+
successCount: 2,
|
|
124
|
+
errorCount: 0,
|
|
125
|
+
});
|
|
126
|
+
expect(steps[0]).toMatchObject({
|
|
127
|
+
index: 0,
|
|
128
|
+
kind: 'text',
|
|
129
|
+
ok: true,
|
|
130
|
+
fieldLabel: 'Mission',
|
|
131
|
+
valueLength: 180,
|
|
132
|
+
wait: 'updated',
|
|
133
|
+
readback: { role: 'textbox', valueLength: 180 },
|
|
134
|
+
});
|
|
135
|
+
expect(steps[1]).toMatchObject({
|
|
136
|
+
index: 1,
|
|
137
|
+
kind: 'text',
|
|
138
|
+
ok: true,
|
|
139
|
+
fieldLabel: 'Email',
|
|
140
|
+
value: 'taylor@example.com',
|
|
141
|
+
wait: 'updated',
|
|
142
|
+
readback: { role: 'textbox', value: 'taylor@example.com' },
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
it('lets run_actions omit step listings while keeping capped final validation state', async () => {
|
|
146
|
+
const handler = getToolHandler('geometra_run_actions');
|
|
147
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
148
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 2400 },
|
|
149
|
+
children: [
|
|
150
|
+
node('textbox', 'Full name', {
|
|
151
|
+
value: '',
|
|
152
|
+
path: [0],
|
|
153
|
+
state: { invalid: true, required: true },
|
|
154
|
+
validation: { error: 'Enter your full name.' },
|
|
155
|
+
}),
|
|
156
|
+
node('textbox', 'Email', {
|
|
157
|
+
value: '',
|
|
158
|
+
path: [1],
|
|
159
|
+
state: { invalid: true, required: true },
|
|
160
|
+
validation: { error: 'Enter your email.' },
|
|
161
|
+
}),
|
|
162
|
+
node('textbox', 'Phone', {
|
|
163
|
+
value: '',
|
|
164
|
+
path: [2],
|
|
165
|
+
state: { invalid: true, required: true },
|
|
166
|
+
validation: { error: 'Enter your phone number.' },
|
|
167
|
+
}),
|
|
168
|
+
node('textbox', 'Location', {
|
|
169
|
+
value: '',
|
|
170
|
+
path: [3],
|
|
171
|
+
state: { invalid: true, required: true },
|
|
172
|
+
validation: { error: 'Choose a location.' },
|
|
173
|
+
}),
|
|
174
|
+
node('textbox', 'LinkedIn', {
|
|
175
|
+
value: '',
|
|
176
|
+
path: [4],
|
|
177
|
+
state: { invalid: true },
|
|
178
|
+
validation: { error: 'Enter a valid URL.' },
|
|
179
|
+
}),
|
|
180
|
+
node('alert', 'Your form needs corrections', { path: [5] }),
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
const result = await handler({
|
|
184
|
+
actions: [{ type: 'click', x: 320, y: 540 }],
|
|
185
|
+
stopOnError: true,
|
|
186
|
+
includeSteps: false,
|
|
187
|
+
detail: 'minimal',
|
|
188
|
+
});
|
|
189
|
+
const payload = JSON.parse(result.content[0].text);
|
|
190
|
+
const final = payload.final;
|
|
191
|
+
expect(payload).toMatchObject({
|
|
192
|
+
completed: true,
|
|
193
|
+
stepCount: 1,
|
|
194
|
+
successCount: 1,
|
|
195
|
+
errorCount: 0,
|
|
196
|
+
});
|
|
197
|
+
expect(payload).not.toHaveProperty('steps');
|
|
198
|
+
expect(final).toMatchObject({
|
|
199
|
+
pageUrl: 'https://jobs.example.com/application',
|
|
200
|
+
alertCount: 1,
|
|
201
|
+
invalidCount: 5,
|
|
202
|
+
});
|
|
203
|
+
expect(final.invalidFields.length).toBe(4);
|
|
204
|
+
expect(final.alerts.length).toBe(1);
|
|
205
|
+
});
|
|
206
|
+
});
|
package/dist/server.js
CHANGED
|
@@ -145,7 +145,7 @@ const batchActionSchema = z.discriminatedUnion('type', [
|
|
|
145
145
|
}),
|
|
146
146
|
]);
|
|
147
147
|
export function createServer() {
|
|
148
|
-
const server = new McpServer({ name: 'geometra', version: '1.19.
|
|
148
|
+
const server = new McpServer({ name: 'geometra', version: '1.19.11' }, { capabilities: { tools: {} } });
|
|
149
149
|
// ── connect ──────────────────────────────────────────────────
|
|
150
150
|
server.tool('geometra_connect', `Connect to a Geometra WebSocket peer, or start \`geometra-proxy\` automatically for a normal web page.
|
|
151
151
|
|
|
@@ -309,8 +309,13 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
309
309
|
.optional()
|
|
310
310
|
.default(false)
|
|
311
311
|
.describe('Return an error if invalid fields remain after filling'),
|
|
312
|
+
includeSteps: z
|
|
313
|
+
.boolean()
|
|
314
|
+
.optional()
|
|
315
|
+
.default(true)
|
|
316
|
+
.describe('Include per-field step results in the JSON payload (default true). Set false for the smallest batch response.'),
|
|
312
317
|
detail: detailInput(),
|
|
313
|
-
}, async ({ fields, stopOnError, failOnInvalid, detail }) => {
|
|
318
|
+
}, async ({ fields, stopOnError, failOnInvalid, includeSteps, detail }) => {
|
|
314
319
|
const session = getSession();
|
|
315
320
|
if (!session)
|
|
316
321
|
return err('Not connected. Call geometra_connect first.');
|
|
@@ -319,8 +324,10 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
319
324
|
for (let index = 0; index < fields.length; index++) {
|
|
320
325
|
const field = fields[index];
|
|
321
326
|
try {
|
|
322
|
-
const
|
|
323
|
-
steps.push(
|
|
327
|
+
const result = await executeFillField(session, field, detail);
|
|
328
|
+
steps.push(detail === 'verbose'
|
|
329
|
+
? { index, kind: field.kind, ok: true, summary: result.summary }
|
|
330
|
+
: { index, kind: field.kind, ok: true, ...result.compact });
|
|
324
331
|
}
|
|
325
332
|
catch (e) {
|
|
326
333
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -334,12 +341,16 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
334
341
|
const after = sessionA11y(session);
|
|
335
342
|
const signals = after ? collectSessionSignals(after) : undefined;
|
|
336
343
|
const invalidRemaining = signals?.invalidFields.length ?? 0;
|
|
344
|
+
const successCount = steps.filter(step => step.ok === true).length;
|
|
345
|
+
const errorCount = steps.length - successCount;
|
|
337
346
|
const payload = {
|
|
338
347
|
completed: stoppedAt === undefined && steps.length === fields.length,
|
|
339
348
|
fieldCount: fields.length,
|
|
340
|
-
|
|
349
|
+
successCount,
|
|
350
|
+
errorCount,
|
|
351
|
+
...(includeSteps ? { steps } : {}),
|
|
341
352
|
...(stoppedAt !== undefined ? { stoppedAt } : {}),
|
|
342
|
-
...(signals ? { final: sessionSignalsPayload(signals) } : {}),
|
|
353
|
+
...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
|
|
343
354
|
};
|
|
344
355
|
if (failOnInvalid && invalidRemaining > 0) {
|
|
345
356
|
return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
@@ -351,8 +362,13 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
351
362
|
Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_listbox_option\`, \`select_option\`, \`set_checked\`, \`wheel\`, \`wait_for\`, and \`fill_fields\`.`, {
|
|
352
363
|
actions: z.array(batchActionSchema).min(1).max(80).describe('Ordered high-level action steps to run sequentially'),
|
|
353
364
|
stopOnError: z.boolean().optional().default(true).describe('Stop at the first failing step (default true)'),
|
|
365
|
+
includeSteps: z
|
|
366
|
+
.boolean()
|
|
367
|
+
.optional()
|
|
368
|
+
.default(true)
|
|
369
|
+
.describe('Include per-action step results in the JSON payload (default true). Set false for the smallest batch response.'),
|
|
354
370
|
detail: detailInput(),
|
|
355
|
-
}, async ({ actions, stopOnError, detail }) => {
|
|
371
|
+
}, async ({ actions, stopOnError, includeSteps, detail }) => {
|
|
356
372
|
const session = getSession();
|
|
357
373
|
if (!session)
|
|
358
374
|
return err('Not connected. Call geometra_connect first.');
|
|
@@ -361,8 +377,10 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
361
377
|
for (let index = 0; index < actions.length; index++) {
|
|
362
378
|
const action = actions[index];
|
|
363
379
|
try {
|
|
364
|
-
const
|
|
365
|
-
steps.push(
|
|
380
|
+
const result = await executeBatchAction(session, action, detail, includeSteps);
|
|
381
|
+
steps.push(detail === 'verbose'
|
|
382
|
+
? { index, type: action.type, ok: true, summary: result.summary }
|
|
383
|
+
: { index, type: action.type, ok: true, ...result.compact });
|
|
366
384
|
}
|
|
367
385
|
catch (e) {
|
|
368
386
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -374,12 +392,16 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
374
392
|
}
|
|
375
393
|
}
|
|
376
394
|
const after = sessionA11y(session);
|
|
395
|
+
const successCount = steps.filter(step => step.ok === true).length;
|
|
396
|
+
const errorCount = steps.length - successCount;
|
|
377
397
|
const payload = {
|
|
378
398
|
completed: stoppedAt === undefined && steps.length === actions.length,
|
|
379
399
|
stepCount: actions.length,
|
|
380
|
-
|
|
400
|
+
successCount,
|
|
401
|
+
errorCount,
|
|
402
|
+
...(includeSteps ? { steps } : {}),
|
|
381
403
|
...(stoppedAt !== undefined ? { stoppedAt } : {}),
|
|
382
|
-
...(after ? { final: sessionSignalsPayload(collectSessionSignals(after)) } : {}),
|
|
404
|
+
...(after ? { final: sessionSignalsPayload(collectSessionSignals(after), detail) } : {}),
|
|
383
405
|
};
|
|
384
406
|
return ok(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
385
407
|
});
|
|
@@ -897,7 +919,7 @@ function truncateInlineText(text, max) {
|
|
|
897
919
|
return undefined;
|
|
898
920
|
return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized;
|
|
899
921
|
}
|
|
900
|
-
function sessionSignalsPayload(signals) {
|
|
922
|
+
function sessionSignalsPayload(signals, detail = 'minimal') {
|
|
901
923
|
return {
|
|
902
924
|
...(signals.pageUrl ? { pageUrl: signals.pageUrl } : {}),
|
|
903
925
|
...(signals.scrollX !== undefined || signals.scrollY !== undefined
|
|
@@ -906,26 +928,85 @@ function sessionSignalsPayload(signals) {
|
|
|
906
928
|
...(signals.focus ? { focus: signals.focus } : {}),
|
|
907
929
|
dialogCount: signals.dialogCount,
|
|
908
930
|
busyCount: signals.busyCount,
|
|
909
|
-
|
|
910
|
-
|
|
931
|
+
alertCount: signals.alerts.length,
|
|
932
|
+
invalidCount: signals.invalidFields.length,
|
|
933
|
+
alerts: detail === 'verbose' ? signals.alerts : signals.alerts.slice(0, 2),
|
|
934
|
+
invalidFields: detail === 'verbose' ? signals.invalidFields : signals.invalidFields.slice(0, 4),
|
|
911
935
|
};
|
|
912
936
|
}
|
|
913
|
-
|
|
937
|
+
function compactTextValue(value, inlineLimit = 48) {
|
|
938
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
939
|
+
if (!normalized)
|
|
940
|
+
return { valueLength: value.length };
|
|
941
|
+
return normalized.length <= inlineLimit
|
|
942
|
+
? { value: normalized }
|
|
943
|
+
: { valueLength: value.length };
|
|
944
|
+
}
|
|
945
|
+
function fieldStatePayload(session, fieldLabel) {
|
|
946
|
+
const a11y = sessionA11y(session);
|
|
947
|
+
if (!a11y)
|
|
948
|
+
return undefined;
|
|
949
|
+
const matches = findNodes(a11y, {
|
|
950
|
+
name: fieldLabel,
|
|
951
|
+
role: 'combobox',
|
|
952
|
+
});
|
|
953
|
+
if (matches.length === 0) {
|
|
954
|
+
matches.push(...findNodes(a11y, { name: fieldLabel, role: 'textbox' }));
|
|
955
|
+
}
|
|
956
|
+
if (matches.length === 0) {
|
|
957
|
+
matches.push(...findNodes(a11y, { name: fieldLabel, role: 'button' }));
|
|
958
|
+
}
|
|
959
|
+
const match = matches[0];
|
|
960
|
+
if (!match)
|
|
961
|
+
return undefined;
|
|
962
|
+
const valuePayload = match.value ? compactTextValue(match.value, 64) : {};
|
|
963
|
+
return {
|
|
964
|
+
role: match.role,
|
|
965
|
+
...valuePayload,
|
|
966
|
+
...(match.state && Object.keys(match.state).length > 0 ? { state: match.state } : {}),
|
|
967
|
+
...(match.validation?.error ? { error: truncateInlineText(match.validation.error, 120) } : {}),
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function waitStatusPayload(wait) {
|
|
971
|
+
return wait ? { wait: wait.status } : {};
|
|
972
|
+
}
|
|
973
|
+
function compactFilterPayload(filter) {
|
|
974
|
+
return Object.fromEntries(Object.entries(filter).filter(([, value]) => value !== undefined));
|
|
975
|
+
}
|
|
976
|
+
async function executeBatchAction(session, action, detail, includeSteps) {
|
|
914
977
|
switch (action.type) {
|
|
915
978
|
case 'click': {
|
|
916
979
|
const before = sessionA11y(session);
|
|
917
980
|
const wait = await sendClick(session, action.x, action.y, action.timeoutMs);
|
|
918
|
-
return
|
|
981
|
+
return {
|
|
982
|
+
summary: `Clicked at (${action.x}, ${action.y}).\n${postActionSummary(session, before, wait, detail)}`,
|
|
983
|
+
compact: {
|
|
984
|
+
at: { x: action.x, y: action.y },
|
|
985
|
+
...waitStatusPayload(wait),
|
|
986
|
+
},
|
|
987
|
+
};
|
|
919
988
|
}
|
|
920
989
|
case 'type': {
|
|
921
990
|
const before = sessionA11y(session);
|
|
922
991
|
const wait = await sendType(session, action.text, action.timeoutMs);
|
|
923
|
-
return
|
|
992
|
+
return {
|
|
993
|
+
summary: `Typed "${action.text}".\n${postActionSummary(session, before, wait, detail)}`,
|
|
994
|
+
compact: {
|
|
995
|
+
...compactTextValue(action.text),
|
|
996
|
+
...waitStatusPayload(wait),
|
|
997
|
+
},
|
|
998
|
+
};
|
|
924
999
|
}
|
|
925
1000
|
case 'key': {
|
|
926
1001
|
const before = sessionA11y(session);
|
|
927
1002
|
const wait = await sendKey(session, action.key, { shift: action.shift, ctrl: action.ctrl, meta: action.meta, alt: action.alt }, action.timeoutMs);
|
|
928
|
-
return
|
|
1003
|
+
return {
|
|
1004
|
+
summary: `Pressed ${formatKeyCombo(action.key, action)}.\n${postActionSummary(session, before, wait, detail)}`,
|
|
1005
|
+
compact: {
|
|
1006
|
+
key: formatKeyCombo(action.key, action),
|
|
1007
|
+
...waitStatusPayload(wait),
|
|
1008
|
+
},
|
|
1009
|
+
};
|
|
929
1010
|
}
|
|
930
1011
|
case 'upload_files': {
|
|
931
1012
|
const before = sessionA11y(session);
|
|
@@ -936,7 +1017,16 @@ async function executeBatchAction(session, action, detail) {
|
|
|
936
1017
|
strategy: action.strategy,
|
|
937
1018
|
drop: action.dropX !== undefined && action.dropY !== undefined ? { x: action.dropX, y: action.dropY } : undefined,
|
|
938
1019
|
}, action.timeoutMs ?? 8_000);
|
|
939
|
-
return
|
|
1020
|
+
return {
|
|
1021
|
+
summary: `Uploaded ${action.paths.length} file(s).\n${postActionSummary(session, before, wait, detail)}`,
|
|
1022
|
+
compact: {
|
|
1023
|
+
fileCount: action.paths.length,
|
|
1024
|
+
...(action.fieldLabel ? { fieldLabel: action.fieldLabel } : {}),
|
|
1025
|
+
...(action.strategy ? { strategy: action.strategy } : {}),
|
|
1026
|
+
...waitStatusPayload(wait),
|
|
1027
|
+
...(action.fieldLabel ? { readback: fieldStatePayload(session, action.fieldLabel) } : {}),
|
|
1028
|
+
},
|
|
1029
|
+
};
|
|
940
1030
|
}
|
|
941
1031
|
case 'pick_listbox_option': {
|
|
942
1032
|
const before = sessionA11y(session);
|
|
@@ -948,7 +1038,15 @@ async function executeBatchAction(session, action, detail) {
|
|
|
948
1038
|
}, action.timeoutMs);
|
|
949
1039
|
const summary = postActionSummary(session, before, wait, detail);
|
|
950
1040
|
const fieldSummary = action.fieldLabel ? summarizeFieldLabelState(session, action.fieldLabel) : undefined;
|
|
951
|
-
return
|
|
1041
|
+
return {
|
|
1042
|
+
summary: [`Picked listbox option "${action.label}".`, fieldSummary, summary].filter(Boolean).join('\n'),
|
|
1043
|
+
compact: {
|
|
1044
|
+
label: action.label,
|
|
1045
|
+
...(action.fieldLabel ? { fieldLabel: action.fieldLabel } : {}),
|
|
1046
|
+
...waitStatusPayload(wait),
|
|
1047
|
+
...(action.fieldLabel ? { readback: fieldStatePayload(session, action.fieldLabel) } : {}),
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
952
1050
|
}
|
|
953
1051
|
case 'select_option': {
|
|
954
1052
|
if (action.value === undefined && action.label === undefined && action.index === undefined) {
|
|
@@ -960,7 +1058,16 @@ async function executeBatchAction(session, action, detail) {
|
|
|
960
1058
|
label: action.label,
|
|
961
1059
|
index: action.index,
|
|
962
1060
|
}, action.timeoutMs);
|
|
963
|
-
return
|
|
1061
|
+
return {
|
|
1062
|
+
summary: `Selected option.\n${postActionSummary(session, before, wait, detail)}`,
|
|
1063
|
+
compact: {
|
|
1064
|
+
at: { x: action.x, y: action.y },
|
|
1065
|
+
...(action.value !== undefined ? { value: action.value } : {}),
|
|
1066
|
+
...(action.label !== undefined ? { label: action.label } : {}),
|
|
1067
|
+
...(action.index !== undefined ? { index: action.index } : {}),
|
|
1068
|
+
...waitStatusPayload(wait),
|
|
1069
|
+
},
|
|
1070
|
+
};
|
|
964
1071
|
}
|
|
965
1072
|
case 'set_checked': {
|
|
966
1073
|
const before = sessionA11y(session);
|
|
@@ -969,7 +1076,15 @@ async function executeBatchAction(session, action, detail) {
|
|
|
969
1076
|
exact: action.exact,
|
|
970
1077
|
controlType: action.controlType,
|
|
971
1078
|
}, action.timeoutMs);
|
|
972
|
-
return
|
|
1079
|
+
return {
|
|
1080
|
+
summary: `Set ${action.controlType ?? 'checkbox/radio'} "${action.label}" to ${String(action.checked ?? true)}.\n${postActionSummary(session, before, wait, detail)}`,
|
|
1081
|
+
compact: {
|
|
1082
|
+
label: action.label,
|
|
1083
|
+
checked: action.checked ?? true,
|
|
1084
|
+
...(action.controlType ? { controlType: action.controlType } : {}),
|
|
1085
|
+
...waitStatusPayload(wait),
|
|
1086
|
+
},
|
|
1087
|
+
};
|
|
973
1088
|
}
|
|
974
1089
|
case 'wheel': {
|
|
975
1090
|
const before = sessionA11y(session);
|
|
@@ -978,7 +1093,15 @@ async function executeBatchAction(session, action, detail) {
|
|
|
978
1093
|
x: action.x,
|
|
979
1094
|
y: action.y,
|
|
980
1095
|
}, action.timeoutMs);
|
|
981
|
-
return
|
|
1096
|
+
return {
|
|
1097
|
+
summary: `Wheel delta (${action.deltaX ?? 0}, ${action.deltaY}).\n${postActionSummary(session, before, wait, detail)}`,
|
|
1098
|
+
compact: {
|
|
1099
|
+
deltaY: action.deltaY,
|
|
1100
|
+
...(action.deltaX !== undefined ? { deltaX: action.deltaX } : {}),
|
|
1101
|
+
...(action.x !== undefined && action.y !== undefined ? { at: { x: action.x, y: action.y } } : {}),
|
|
1102
|
+
...waitStatusPayload(wait),
|
|
1103
|
+
},
|
|
1104
|
+
};
|
|
982
1105
|
}
|
|
983
1106
|
case 'wait_for': {
|
|
984
1107
|
if (!session.tree || !session.layout)
|
|
@@ -1016,24 +1139,64 @@ async function executeBatchAction(session, action, detail) {
|
|
|
1016
1139
|
throw new Error(`Timed out after ${timeoutMs}ms waiting for ${present ? 'presence' : 'absence'} of ${JSON.stringify(filter)}`);
|
|
1017
1140
|
}
|
|
1018
1141
|
if (!present) {
|
|
1019
|
-
return
|
|
1142
|
+
return {
|
|
1143
|
+
summary: `Condition satisfied after ${elapsedMs}ms: no nodes matched ${JSON.stringify(filter)}.`,
|
|
1144
|
+
compact: {
|
|
1145
|
+
present,
|
|
1146
|
+
elapsedMs,
|
|
1147
|
+
filter: compactFilterPayload(filter),
|
|
1148
|
+
},
|
|
1149
|
+
};
|
|
1020
1150
|
}
|
|
1021
1151
|
const after = sessionA11y(session);
|
|
1022
1152
|
if (!after) {
|
|
1023
|
-
return
|
|
1153
|
+
return {
|
|
1154
|
+
summary: `Condition satisfied after ${elapsedMs}ms for ${JSON.stringify(filter)}.`,
|
|
1155
|
+
compact: {
|
|
1156
|
+
present,
|
|
1157
|
+
elapsedMs,
|
|
1158
|
+
filter: compactFilterPayload(filter),
|
|
1159
|
+
},
|
|
1160
|
+
};
|
|
1024
1161
|
}
|
|
1025
1162
|
const matches = findNodes(after, filter);
|
|
1026
1163
|
if (detail === 'verbose') {
|
|
1027
|
-
return
|
|
1164
|
+
return {
|
|
1165
|
+
summary: JSON.stringify(matches.slice(0, 8).map(node => formatNode(node, after.bounds)), null, 2),
|
|
1166
|
+
compact: {
|
|
1167
|
+
present,
|
|
1168
|
+
elapsedMs,
|
|
1169
|
+
matchCount: matches.length,
|
|
1170
|
+
filter: compactFilterPayload(filter),
|
|
1171
|
+
},
|
|
1172
|
+
};
|
|
1028
1173
|
}
|
|
1029
|
-
return
|
|
1174
|
+
return {
|
|
1175
|
+
summary: `Condition satisfied after ${elapsedMs}ms with ${matches.length} matching node(s).`,
|
|
1176
|
+
compact: {
|
|
1177
|
+
present,
|
|
1178
|
+
elapsedMs,
|
|
1179
|
+
matchCount: matches.length,
|
|
1180
|
+
filter: compactFilterPayload(filter),
|
|
1181
|
+
},
|
|
1182
|
+
};
|
|
1030
1183
|
}
|
|
1031
1184
|
case 'fill_fields': {
|
|
1032
|
-
const
|
|
1033
|
-
for (
|
|
1034
|
-
|
|
1185
|
+
const steps = [];
|
|
1186
|
+
for (let index = 0; index < action.fields.length; index++) {
|
|
1187
|
+
const field = action.fields[index];
|
|
1188
|
+
const result = await executeFillField(session, field, detail);
|
|
1189
|
+
steps.push(detail === 'verbose'
|
|
1190
|
+
? { index, kind: field.kind, ok: true, summary: result.summary }
|
|
1191
|
+
: { index, kind: field.kind, ok: true, ...result.compact });
|
|
1035
1192
|
}
|
|
1036
|
-
return
|
|
1193
|
+
return {
|
|
1194
|
+
summary: steps.map(step => String(step.summary ?? '')).filter(Boolean).join('\n'),
|
|
1195
|
+
compact: {
|
|
1196
|
+
fieldCount: action.fields.length,
|
|
1197
|
+
...(includeSteps ? { steps } : {}),
|
|
1198
|
+
},
|
|
1199
|
+
};
|
|
1037
1200
|
}
|
|
1038
1201
|
}
|
|
1039
1202
|
}
|
|
@@ -1043,36 +1206,68 @@ async function executeFillField(session, field, detail) {
|
|
|
1043
1206
|
const before = sessionA11y(session);
|
|
1044
1207
|
const wait = await sendFieldText(session, field.fieldLabel, field.value, { exact: field.exact }, field.timeoutMs);
|
|
1045
1208
|
const fieldSummary = summarizeFieldLabelState(session, field.fieldLabel);
|
|
1046
|
-
return
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1209
|
+
return {
|
|
1210
|
+
summary: [
|
|
1211
|
+
`Filled text field "${field.fieldLabel}".`,
|
|
1212
|
+
fieldSummary,
|
|
1213
|
+
postActionSummary(session, before, wait, detail),
|
|
1214
|
+
].filter(Boolean).join('\n'),
|
|
1215
|
+
compact: {
|
|
1216
|
+
fieldLabel: field.fieldLabel,
|
|
1217
|
+
...compactTextValue(field.value),
|
|
1218
|
+
...waitStatusPayload(wait),
|
|
1219
|
+
readback: fieldStatePayload(session, field.fieldLabel),
|
|
1220
|
+
},
|
|
1221
|
+
};
|
|
1051
1222
|
}
|
|
1052
1223
|
case 'choice': {
|
|
1053
1224
|
const before = sessionA11y(session);
|
|
1054
1225
|
const wait = await sendFieldChoice(session, field.fieldLabel, field.value, { exact: field.exact, query: field.query }, field.timeoutMs);
|
|
1055
1226
|
const fieldSummary = summarizeFieldLabelState(session, field.fieldLabel);
|
|
1056
|
-
return
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1227
|
+
return {
|
|
1228
|
+
summary: [
|
|
1229
|
+
`Set choice field "${field.fieldLabel}" to "${field.value}".`,
|
|
1230
|
+
fieldSummary,
|
|
1231
|
+
postActionSummary(session, before, wait, detail),
|
|
1232
|
+
].filter(Boolean).join('\n'),
|
|
1233
|
+
compact: {
|
|
1234
|
+
fieldLabel: field.fieldLabel,
|
|
1235
|
+
value: field.value,
|
|
1236
|
+
...waitStatusPayload(wait),
|
|
1237
|
+
readback: fieldStatePayload(session, field.fieldLabel),
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1061
1240
|
}
|
|
1062
1241
|
case 'toggle': {
|
|
1063
1242
|
const before = sessionA11y(session);
|
|
1064
1243
|
const wait = await sendSetChecked(session, field.label, { checked: field.checked, exact: field.exact, controlType: field.controlType }, field.timeoutMs);
|
|
1065
|
-
return
|
|
1244
|
+
return {
|
|
1245
|
+
summary: `Set ${field.controlType ?? 'checkbox/radio'} "${field.label}" to ${String(field.checked ?? true)}.\n${postActionSummary(session, before, wait, detail)}`,
|
|
1246
|
+
compact: {
|
|
1247
|
+
label: field.label,
|
|
1248
|
+
checked: field.checked ?? true,
|
|
1249
|
+
...(field.controlType ? { controlType: field.controlType } : {}),
|
|
1250
|
+
...waitStatusPayload(wait),
|
|
1251
|
+
},
|
|
1252
|
+
};
|
|
1066
1253
|
}
|
|
1067
1254
|
case 'file': {
|
|
1068
1255
|
const before = sessionA11y(session);
|
|
1069
1256
|
const wait = await sendFileUpload(session, field.paths, { fieldLabel: field.fieldLabel, exact: field.exact }, field.timeoutMs ?? 8_000);
|
|
1070
1257
|
const fieldSummary = summarizeFieldLabelState(session, field.fieldLabel);
|
|
1071
|
-
return
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1258
|
+
return {
|
|
1259
|
+
summary: [
|
|
1260
|
+
`Uploaded ${field.paths.length} file(s) to "${field.fieldLabel}".`,
|
|
1261
|
+
fieldSummary,
|
|
1262
|
+
postActionSummary(session, before, wait, detail),
|
|
1263
|
+
].filter(Boolean).join('\n'),
|
|
1264
|
+
compact: {
|
|
1265
|
+
fieldLabel: field.fieldLabel,
|
|
1266
|
+
fileCount: field.paths.length,
|
|
1267
|
+
...waitStatusPayload(wait),
|
|
1268
|
+
readback: fieldStatePayload(session, field.fieldLabel),
|
|
1269
|
+
},
|
|
1270
|
+
};
|
|
1076
1271
|
}
|
|
1077
1272
|
}
|
|
1078
1273
|
}
|
|
@@ -1134,29 +1329,20 @@ export function findNodes(node, filter) {
|
|
|
1134
1329
|
return matches;
|
|
1135
1330
|
}
|
|
1136
1331
|
function summarizeFieldLabelState(session, fieldLabel) {
|
|
1137
|
-
const
|
|
1138
|
-
if (!
|
|
1139
|
-
return undefined;
|
|
1140
|
-
const matches = findNodes(a11y, {
|
|
1141
|
-
name: fieldLabel,
|
|
1142
|
-
role: 'combobox',
|
|
1143
|
-
});
|
|
1144
|
-
if (matches.length === 0) {
|
|
1145
|
-
matches.push(...findNodes(a11y, { name: fieldLabel, role: 'textbox' }));
|
|
1146
|
-
}
|
|
1147
|
-
if (matches.length === 0) {
|
|
1148
|
-
matches.push(...findNodes(a11y, { name: fieldLabel, role: 'button' }));
|
|
1149
|
-
}
|
|
1150
|
-
const match = matches[0];
|
|
1151
|
-
if (!match)
|
|
1332
|
+
const payload = fieldStatePayload(session, fieldLabel);
|
|
1333
|
+
if (!payload)
|
|
1152
1334
|
return undefined;
|
|
1153
1335
|
const parts = [`Field "${fieldLabel}"`];
|
|
1154
|
-
if (
|
|
1155
|
-
parts.push(`
|
|
1156
|
-
if (
|
|
1157
|
-
parts.push(`
|
|
1158
|
-
if (
|
|
1159
|
-
parts.push(`
|
|
1336
|
+
if (payload.role)
|
|
1337
|
+
parts.push(`role=${String(payload.role)}`);
|
|
1338
|
+
if (payload.value)
|
|
1339
|
+
parts.push(`value=${JSON.stringify(payload.value)}`);
|
|
1340
|
+
if (payload.valueLength)
|
|
1341
|
+
parts.push(`valueLength=${String(payload.valueLength)}`);
|
|
1342
|
+
if (payload.state)
|
|
1343
|
+
parts.push(`state=${JSON.stringify(payload.state)}`);
|
|
1344
|
+
if (payload.error)
|
|
1345
|
+
parts.push(`error=${JSON.stringify(payload.error)}`);
|
|
1160
1346
|
return parts.join(' ');
|
|
1161
1347
|
}
|
|
1162
1348
|
function formatNode(node, viewport) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geometra/mcp",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.11",
|
|
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.11",
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
35
35
|
"ws": "^8.18.0",
|
|
36
36
|
"zod": "^3.23.0"
|