@geometra/mcp 1.19.15 → 1.19.17
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 +7 -6
- package/dist/__tests__/proxy-session-actions.test.js +63 -5
- package/dist/__tests__/server-batch-results.test.js +199 -16
- package/dist/__tests__/session-model.test.js +12 -3
- package/dist/proxy-spawn.d.ts +11 -0
- package/dist/proxy-spawn.js +46 -19
- package/dist/server.js +408 -35
- package/dist/session.d.ts +44 -7
- package/dist/session.js +311 -39
- package/package.json +2 -2
package/dist/session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
|
-
import { spawnGeometraProxy } from './proxy-spawn.js';
|
|
2
|
+
import { spawnGeometraProxy, startEmbeddedGeometraProxy } from './proxy-spawn.js';
|
|
3
3
|
let activeSession = null;
|
|
4
|
+
let reusableProxy = null;
|
|
4
5
|
const ACTION_UPDATE_TIMEOUT_MS = 2000;
|
|
5
6
|
const LISTBOX_UPDATE_TIMEOUT_MS = 4500;
|
|
6
7
|
const FILL_BATCH_BASE_TIMEOUT_MS = 2500;
|
|
@@ -12,7 +13,86 @@ const FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS = 225;
|
|
|
12
13
|
const FILL_BATCH_FILE_FIELD_TIMEOUT_MS = 5000;
|
|
13
14
|
const FILL_BATCH_MAX_TIMEOUT_MS = 60_000;
|
|
14
15
|
let nextRequestSequence = 0;
|
|
15
|
-
function
|
|
16
|
+
function invalidateSessionCaches(session) {
|
|
17
|
+
session.cachedA11y = null;
|
|
18
|
+
session.cachedA11yRevision = -1;
|
|
19
|
+
session.cachedFormSchemas?.clear();
|
|
20
|
+
}
|
|
21
|
+
function clearReusableProxyIfExited() {
|
|
22
|
+
if (!reusableProxy)
|
|
23
|
+
return;
|
|
24
|
+
if (reusableProxy.child) {
|
|
25
|
+
if (!reusableProxy.child.killed && reusableProxy.child.exitCode === null && reusableProxy.child.signalCode === null) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
reusableProxy = null;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (!reusableProxy.runtime.closed)
|
|
32
|
+
return;
|
|
33
|
+
reusableProxy = null;
|
|
34
|
+
}
|
|
35
|
+
function setReusableProxy(proxy, wsUrl, opts) {
|
|
36
|
+
if ('child' in proxy) {
|
|
37
|
+
const child = proxy.child;
|
|
38
|
+
reusableProxy = {
|
|
39
|
+
child,
|
|
40
|
+
wsUrl,
|
|
41
|
+
headless: opts.headless === true,
|
|
42
|
+
slowMo: opts.slowMo ?? 0,
|
|
43
|
+
width: opts.width ?? 1280,
|
|
44
|
+
height: opts.height ?? 720,
|
|
45
|
+
pageUrl: opts.pageUrl,
|
|
46
|
+
};
|
|
47
|
+
const clear = () => {
|
|
48
|
+
if (reusableProxy?.child === child)
|
|
49
|
+
reusableProxy = null;
|
|
50
|
+
};
|
|
51
|
+
child.once('exit', clear);
|
|
52
|
+
child.once('close', clear);
|
|
53
|
+
child.once('error', clear);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
reusableProxy = {
|
|
57
|
+
runtime: proxy.runtime,
|
|
58
|
+
wsUrl,
|
|
59
|
+
headless: opts.headless === true,
|
|
60
|
+
slowMo: opts.slowMo ?? 0,
|
|
61
|
+
width: opts.width ?? 1280,
|
|
62
|
+
height: opts.height ?? 720,
|
|
63
|
+
pageUrl: opts.pageUrl,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function closeReusableProxy() {
|
|
67
|
+
clearReusableProxyIfExited();
|
|
68
|
+
const proxy = reusableProxy;
|
|
69
|
+
reusableProxy = null;
|
|
70
|
+
if (!proxy)
|
|
71
|
+
return;
|
|
72
|
+
if (proxy.child) {
|
|
73
|
+
try {
|
|
74
|
+
proxy.child.kill('SIGTERM');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* ignore */
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
void proxy.runtime.close().catch(() => { });
|
|
82
|
+
}
|
|
83
|
+
function rememberReusableProxyPageUrl(session) {
|
|
84
|
+
const pageUrl = session.cachedA11y?.meta?.pageUrl;
|
|
85
|
+
if (!pageUrl)
|
|
86
|
+
return;
|
|
87
|
+
if (session.proxyChild && reusableProxy?.child === session.proxyChild) {
|
|
88
|
+
reusableProxy.pageUrl = pageUrl;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (session.proxyRuntime && reusableProxy?.runtime === session.proxyRuntime) {
|
|
92
|
+
reusableProxy.pageUrl = pageUrl;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function shutdownPreviousSession(opts) {
|
|
16
96
|
const prev = activeSession;
|
|
17
97
|
if (!prev)
|
|
18
98
|
return;
|
|
@@ -24,12 +104,28 @@ function shutdownPreviousSession() {
|
|
|
24
104
|
/* ignore */
|
|
25
105
|
}
|
|
26
106
|
if (prev.proxyChild) {
|
|
107
|
+
const shouldKeepProxy = prev.proxyReusable && opts?.closeProxy === false;
|
|
108
|
+
rememberReusableProxyPageUrl(prev);
|
|
109
|
+
if (shouldKeepProxy)
|
|
110
|
+
return;
|
|
111
|
+
if (reusableProxy?.child === prev.proxyChild)
|
|
112
|
+
reusableProxy = null;
|
|
27
113
|
try {
|
|
28
114
|
prev.proxyChild.kill('SIGTERM');
|
|
29
115
|
}
|
|
30
116
|
catch {
|
|
31
117
|
/* ignore */
|
|
32
118
|
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (prev.proxyRuntime) {
|
|
122
|
+
const shouldKeepProxy = prev.proxyReusable && opts?.closeProxy === false;
|
|
123
|
+
rememberReusableProxyPageUrl(prev);
|
|
124
|
+
if (shouldKeepProxy)
|
|
125
|
+
return;
|
|
126
|
+
if (reusableProxy?.runtime === prev.proxyRuntime)
|
|
127
|
+
reusableProxy = null;
|
|
128
|
+
void prev.proxyRuntime.close().catch(() => { });
|
|
33
129
|
}
|
|
34
130
|
}
|
|
35
131
|
/**
|
|
@@ -38,9 +134,22 @@ function shutdownPreviousSession() {
|
|
|
38
134
|
*/
|
|
39
135
|
export function connect(url, opts) {
|
|
40
136
|
return new Promise((resolve, reject) => {
|
|
41
|
-
|
|
137
|
+
clearReusableProxyIfExited();
|
|
138
|
+
if (reusableProxy && reusableProxy.wsUrl !== url) {
|
|
139
|
+
closeReusableProxy();
|
|
140
|
+
}
|
|
141
|
+
shutdownPreviousSession({ closeProxy: opts?.closePreviousProxy ?? true });
|
|
42
142
|
const ws = new WebSocket(url);
|
|
43
|
-
const session = {
|
|
143
|
+
const session = {
|
|
144
|
+
ws,
|
|
145
|
+
layout: null,
|
|
146
|
+
tree: null,
|
|
147
|
+
url,
|
|
148
|
+
updateRevision: 0,
|
|
149
|
+
cachedA11y: null,
|
|
150
|
+
cachedA11yRevision: -1,
|
|
151
|
+
cachedFormSchemas: new Map(),
|
|
152
|
+
};
|
|
44
153
|
let resolved = false;
|
|
45
154
|
const timeout = setTimeout(() => {
|
|
46
155
|
if (!resolved) {
|
|
@@ -50,11 +159,17 @@ export function connect(url, opts) {
|
|
|
50
159
|
}
|
|
51
160
|
}, 10_000);
|
|
52
161
|
ws.on('open', () => {
|
|
53
|
-
if (opts?.skipInitialResize)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
162
|
+
if (!opts?.skipInitialResize) {
|
|
163
|
+
const width = opts?.width ?? 1024;
|
|
164
|
+
const height = opts?.height ?? 768;
|
|
165
|
+
ws.send(JSON.stringify({ type: 'resize', width, height }));
|
|
166
|
+
}
|
|
167
|
+
if (opts?.awaitInitialFrame === false && !resolved) {
|
|
168
|
+
resolved = true;
|
|
169
|
+
clearTimeout(timeout);
|
|
170
|
+
activeSession = session;
|
|
171
|
+
resolve(session);
|
|
172
|
+
}
|
|
58
173
|
});
|
|
59
174
|
ws.on('message', (data) => {
|
|
60
175
|
try {
|
|
@@ -63,6 +178,7 @@ export function connect(url, opts) {
|
|
|
63
178
|
session.layout = msg.layout;
|
|
64
179
|
session.tree = msg.tree;
|
|
65
180
|
session.updateRevision++;
|
|
181
|
+
invalidateSessionCaches(session);
|
|
66
182
|
if (!resolved) {
|
|
67
183
|
resolved = true;
|
|
68
184
|
clearTimeout(timeout);
|
|
@@ -73,6 +189,7 @@ export function connect(url, opts) {
|
|
|
73
189
|
else if (msg.type === 'patch' && session.layout) {
|
|
74
190
|
applyPatches(session.layout, msg.patches);
|
|
75
191
|
session.updateRevision++;
|
|
192
|
+
invalidateSessionCaches(session);
|
|
76
193
|
}
|
|
77
194
|
}
|
|
78
195
|
catch { /* ignore malformed messages */ }
|
|
@@ -87,7 +204,7 @@ export function connect(url, opts) {
|
|
|
87
204
|
ws.on('close', () => {
|
|
88
205
|
if (activeSession === session) {
|
|
89
206
|
activeSession = null;
|
|
90
|
-
if (session.proxyChild) {
|
|
207
|
+
if (session.proxyChild && !session.proxyReusable) {
|
|
91
208
|
try {
|
|
92
209
|
session.proxyChild.kill('SIGTERM');
|
|
93
210
|
}
|
|
@@ -95,6 +212,9 @@ export function connect(url, opts) {
|
|
|
95
212
|
/* ignore */
|
|
96
213
|
}
|
|
97
214
|
}
|
|
215
|
+
if (session.proxyRuntime && !session.proxyReusable) {
|
|
216
|
+
void session.proxyRuntime.close().catch(() => { });
|
|
217
|
+
}
|
|
98
218
|
}
|
|
99
219
|
if (!resolved) {
|
|
100
220
|
resolved = true;
|
|
@@ -109,47 +229,135 @@ export function connect(url, opts) {
|
|
|
109
229
|
* process to the session so disconnect / reconnect can clean it up.
|
|
110
230
|
*/
|
|
111
231
|
export async function connectThroughProxy(options) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
232
|
+
clearReusableProxyIfExited();
|
|
233
|
+
const desiredHeadless = options.headless === true;
|
|
234
|
+
const desiredSlowMo = options.slowMo ?? 0;
|
|
235
|
+
if (reusableProxy &&
|
|
236
|
+
reusableProxy.headless === desiredHeadless &&
|
|
237
|
+
reusableProxy.slowMo === desiredSlowMo) {
|
|
238
|
+
const session = ((reusableProxy.child && activeSession?.proxyChild === reusableProxy.child) ||
|
|
239
|
+
(reusableProxy.runtime && activeSession?.proxyRuntime === reusableProxy.runtime))
|
|
240
|
+
? activeSession
|
|
241
|
+
: await connect(reusableProxy.wsUrl, {
|
|
242
|
+
skipInitialResize: true,
|
|
243
|
+
closePreviousProxy: false,
|
|
244
|
+
awaitInitialFrame: options.awaitInitialFrame,
|
|
245
|
+
});
|
|
246
|
+
if (!session) {
|
|
247
|
+
throw new Error('Failed to attach to reusable proxy session');
|
|
248
|
+
}
|
|
249
|
+
session.proxyChild = reusableProxy.child;
|
|
250
|
+
session.proxyRuntime = reusableProxy.runtime;
|
|
251
|
+
session.proxyReusable = true;
|
|
252
|
+
const desiredWidth = options.width ?? reusableProxy.width;
|
|
253
|
+
const desiredHeight = options.height ?? reusableProxy.height;
|
|
254
|
+
if (desiredWidth !== reusableProxy.width || desiredHeight !== reusableProxy.height) {
|
|
255
|
+
await sendAndWaitForUpdate(session, {
|
|
256
|
+
type: 'resize',
|
|
257
|
+
width: desiredWidth,
|
|
258
|
+
height: desiredHeight,
|
|
259
|
+
}, 5_000);
|
|
260
|
+
reusableProxy.width = desiredWidth;
|
|
261
|
+
reusableProxy.height = desiredHeight;
|
|
262
|
+
}
|
|
263
|
+
if (options.pageUrl) {
|
|
264
|
+
const currentUrl = session.cachedA11y?.meta?.pageUrl ?? reusableProxy.pageUrl;
|
|
265
|
+
if (currentUrl !== options.pageUrl) {
|
|
266
|
+
await sendNavigate(session, options.pageUrl, 15_000);
|
|
267
|
+
if ((session.proxyChild && reusableProxy?.child === session.proxyChild) ||
|
|
268
|
+
(session.proxyRuntime && reusableProxy?.runtime === session.proxyRuntime)) {
|
|
269
|
+
reusableProxy.pageUrl = options.pageUrl;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return session;
|
|
274
|
+
}
|
|
275
|
+
closeReusableProxy();
|
|
120
276
|
try {
|
|
121
|
-
const
|
|
122
|
-
|
|
277
|
+
const { runtime, wsUrl } = await startEmbeddedGeometraProxy({
|
|
278
|
+
pageUrl: options.pageUrl,
|
|
279
|
+
port: options.port ?? 0,
|
|
280
|
+
headless: options.headless,
|
|
281
|
+
width: options.width,
|
|
282
|
+
height: options.height,
|
|
283
|
+
slowMo: options.slowMo,
|
|
284
|
+
});
|
|
285
|
+
const session = await connect(wsUrl, {
|
|
286
|
+
skipInitialResize: true,
|
|
287
|
+
closePreviousProxy: false,
|
|
288
|
+
awaitInitialFrame: options.awaitInitialFrame,
|
|
289
|
+
});
|
|
290
|
+
session.proxyRuntime = runtime;
|
|
291
|
+
session.proxyReusable = true;
|
|
292
|
+
setReusableProxy({ runtime }, wsUrl, {
|
|
293
|
+
headless: options.headless,
|
|
294
|
+
slowMo: options.slowMo,
|
|
295
|
+
width: options.width,
|
|
296
|
+
height: options.height,
|
|
297
|
+
pageUrl: options.pageUrl,
|
|
298
|
+
});
|
|
123
299
|
return session;
|
|
124
300
|
}
|
|
125
301
|
catch (e) {
|
|
302
|
+
const { child, wsUrl } = await spawnGeometraProxy({
|
|
303
|
+
pageUrl: options.pageUrl,
|
|
304
|
+
port: options.port ?? 0,
|
|
305
|
+
headless: options.headless,
|
|
306
|
+
width: options.width,
|
|
307
|
+
height: options.height,
|
|
308
|
+
slowMo: options.slowMo,
|
|
309
|
+
});
|
|
126
310
|
try {
|
|
127
|
-
|
|
311
|
+
const session = await connect(wsUrl, {
|
|
312
|
+
skipInitialResize: true,
|
|
313
|
+
closePreviousProxy: false,
|
|
314
|
+
awaitInitialFrame: options.awaitInitialFrame,
|
|
315
|
+
});
|
|
316
|
+
session.proxyChild = child;
|
|
317
|
+
session.proxyReusable = true;
|
|
318
|
+
setReusableProxy({ child }, wsUrl, {
|
|
319
|
+
headless: options.headless,
|
|
320
|
+
slowMo: options.slowMo,
|
|
321
|
+
width: options.width,
|
|
322
|
+
height: options.height,
|
|
323
|
+
pageUrl: options.pageUrl,
|
|
324
|
+
});
|
|
325
|
+
return session;
|
|
128
326
|
}
|
|
129
|
-
catch {
|
|
130
|
-
|
|
327
|
+
catch (fallbackError) {
|
|
328
|
+
try {
|
|
329
|
+
child.kill('SIGTERM');
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
/* ignore */
|
|
333
|
+
}
|
|
334
|
+
throw fallbackError instanceof Error ? fallbackError : e;
|
|
131
335
|
}
|
|
132
|
-
throw e;
|
|
133
336
|
}
|
|
134
337
|
}
|
|
135
338
|
export function getSession() {
|
|
136
339
|
return activeSession;
|
|
137
340
|
}
|
|
138
|
-
export function disconnect() {
|
|
139
|
-
shutdownPreviousSession();
|
|
341
|
+
export function disconnect(opts) {
|
|
342
|
+
shutdownPreviousSession({ closeProxy: opts?.closeProxy ?? false });
|
|
343
|
+
if (opts?.closeProxy)
|
|
344
|
+
closeReusableProxy();
|
|
140
345
|
}
|
|
141
346
|
function estimateFillBatchTimeout(fields) {
|
|
142
347
|
let total = FILL_BATCH_BASE_TIMEOUT_MS;
|
|
143
348
|
let totalTextLength = 0;
|
|
144
349
|
for (const field of fields) {
|
|
145
350
|
switch (field.kind) {
|
|
351
|
+
case 'auto':
|
|
352
|
+
total += typeof field.value === 'boolean' ? FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS : FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS;
|
|
353
|
+
break;
|
|
146
354
|
case 'text':
|
|
147
355
|
totalTextLength += field.value.length;
|
|
148
356
|
total += FILL_BATCH_TEXT_FIELD_TIMEOUT_MS;
|
|
149
357
|
total += Math.ceil(Math.max(1, field.value.length) / FILL_BATCH_TEXT_LENGTH_SLICE) * FILL_BATCH_TEXT_LENGTH_TIMEOUT_MS;
|
|
150
358
|
break;
|
|
151
359
|
case 'choice':
|
|
152
|
-
total += FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS;
|
|
360
|
+
total += field.choiceType === 'group' ? FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS : FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS;
|
|
153
361
|
break;
|
|
154
362
|
case 'toggle':
|
|
155
363
|
total += FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS;
|
|
@@ -285,6 +493,8 @@ export function sendFieldText(session, fieldLabel, value, opts, timeoutMs) {
|
|
|
285
493
|
};
|
|
286
494
|
if (opts?.exact !== undefined)
|
|
287
495
|
payload.exact = opts.exact;
|
|
496
|
+
if (opts?.fieldId)
|
|
497
|
+
payload.fieldId = opts.fieldId;
|
|
288
498
|
return sendAndWaitForUpdate(session, payload, timeoutMs);
|
|
289
499
|
}
|
|
290
500
|
/** Choose a value for a labeled choice field (select, custom combobox, or radio-style group). */
|
|
@@ -298,6 +508,10 @@ export function sendFieldChoice(session, fieldLabel, value, opts, timeoutMs = LI
|
|
|
298
508
|
payload.exact = opts.exact;
|
|
299
509
|
if (opts?.query)
|
|
300
510
|
payload.query = opts.query;
|
|
511
|
+
if (opts?.choiceType)
|
|
512
|
+
payload.choiceType = opts.choiceType;
|
|
513
|
+
if (opts?.fieldId)
|
|
514
|
+
payload.fieldId = opts.fieldId;
|
|
301
515
|
return sendAndWaitForUpdate(session, payload, timeoutMs);
|
|
302
516
|
}
|
|
303
517
|
/** Fill several semantic form fields in one proxy-side batch. */
|
|
@@ -349,6 +563,13 @@ export function sendWheel(session, deltaY, opts, timeoutMs) {
|
|
|
349
563
|
...(opts?.y !== undefined ? { y: opts.y } : {}),
|
|
350
564
|
}, timeoutMs);
|
|
351
565
|
}
|
|
566
|
+
/** Navigate the proxy page to a new URL while keeping the browser process alive. */
|
|
567
|
+
export function sendNavigate(session, url, timeoutMs = 15_000) {
|
|
568
|
+
return sendAndWaitForUpdate(session, {
|
|
569
|
+
type: 'navigate',
|
|
570
|
+
url,
|
|
571
|
+
}, timeoutMs, { requireUpdateOnAck: true });
|
|
572
|
+
}
|
|
352
573
|
/**
|
|
353
574
|
* Build a flat accessibility tree from the raw UI tree + layout.
|
|
354
575
|
* This is a standalone reimplementation that works with raw JSON —
|
|
@@ -974,10 +1195,16 @@ function simpleSchemaField(root, node) {
|
|
|
974
1195
|
const label = fieldLabel(node) ?? sanitizeInlineName(node.name, 80) ?? context?.prompt;
|
|
975
1196
|
if (!label)
|
|
976
1197
|
return null;
|
|
1198
|
+
const choiceType = node.role === 'combobox'
|
|
1199
|
+
? node.meta?.controlTag === 'select'
|
|
1200
|
+
? 'select'
|
|
1201
|
+
: 'listbox'
|
|
1202
|
+
: undefined;
|
|
977
1203
|
return {
|
|
978
1204
|
id: formFieldIdForPath(node.path),
|
|
979
1205
|
kind: node.role === 'combobox' ? 'choice' : 'text',
|
|
980
1206
|
label,
|
|
1207
|
+
...(choiceType ? { choiceType } : {}),
|
|
981
1208
|
...(node.state?.required ? { required: true } : {}),
|
|
982
1209
|
...(node.state?.invalid ? { invalid: true } : {}),
|
|
983
1210
|
...compactSchemaValue(node.value, 72),
|
|
@@ -1002,6 +1229,7 @@ function groupedSchemaField(root, grouped) {
|
|
|
1002
1229
|
id: formFieldIdForPath(grouped.container.path),
|
|
1003
1230
|
kind: radioLike ? 'choice' : 'multi_choice',
|
|
1004
1231
|
label: grouped.prompt,
|
|
1232
|
+
...(radioLike ? { choiceType: 'group' } : {}),
|
|
1005
1233
|
...(grouped.controls.some(control => control.state?.required) ? { required: true } : {}),
|
|
1006
1234
|
...(grouped.controls.some(control => control.state?.invalid) ? { invalid: true } : {}),
|
|
1007
1235
|
...(radioLike
|
|
@@ -1068,7 +1296,7 @@ function buildFormSchemaForNode(root, formNode, options) {
|
|
|
1068
1296
|
consumed.add(candidateKey);
|
|
1069
1297
|
}
|
|
1070
1298
|
}
|
|
1071
|
-
const compactFields =
|
|
1299
|
+
const compactFields = presentFormSchemaFields(fields, options);
|
|
1072
1300
|
const filteredFields = compactFields.filter(field => {
|
|
1073
1301
|
if (options?.onlyRequiredFields && !field.required)
|
|
1074
1302
|
return false;
|
|
@@ -1089,14 +1317,35 @@ function buildFormSchemaForNode(root, formNode, options) {
|
|
|
1089
1317
|
};
|
|
1090
1318
|
}
|
|
1091
1319
|
function trimSchemaFieldContexts(fields) {
|
|
1320
|
+
return presentFormSchemaFields(fields, { includeOptions: true, includeContext: 'auto' });
|
|
1321
|
+
}
|
|
1322
|
+
function presentFormSchemaFields(fields, options) {
|
|
1323
|
+
const includeOptions = options?.includeOptions ?? false;
|
|
1324
|
+
const includeContext = options?.includeContext ?? 'auto';
|
|
1092
1325
|
const labelCounts = new Map();
|
|
1093
1326
|
for (const field of fields) {
|
|
1094
1327
|
const key = normalizeUiText(field.label);
|
|
1095
1328
|
labelCounts.set(key, (labelCounts.get(key) ?? 0) + 1);
|
|
1096
1329
|
}
|
|
1097
1330
|
return fields.map(field => {
|
|
1331
|
+
const booleanChoice = field.kind === 'choice' &&
|
|
1332
|
+
field.choiceType === 'group' &&
|
|
1333
|
+
field.optionCount === 2 &&
|
|
1334
|
+
field.options?.length === 2 &&
|
|
1335
|
+
field.options.every(option => ['yes', 'no'].includes(normalizeUiText(option).toLowerCase()));
|
|
1336
|
+
const next = { ...field };
|
|
1337
|
+
if (booleanChoice)
|
|
1338
|
+
next.booleanChoice = true;
|
|
1339
|
+
if (!includeOptions)
|
|
1340
|
+
delete next.options;
|
|
1341
|
+
if (includeContext === 'none') {
|
|
1342
|
+
delete next.context;
|
|
1343
|
+
return next;
|
|
1344
|
+
}
|
|
1098
1345
|
if (!field.context)
|
|
1099
|
-
return
|
|
1346
|
+
return next;
|
|
1347
|
+
if (includeContext === 'always')
|
|
1348
|
+
return next;
|
|
1100
1349
|
const trimmed = {};
|
|
1101
1350
|
if (field.context.prompt && normalizeUiText(field.context.prompt) !== normalizeUiText(field.label)) {
|
|
1102
1351
|
trimmed.prompt = field.context.prompt;
|
|
@@ -1105,10 +1354,11 @@ function trimSchemaFieldContexts(fields) {
|
|
|
1105
1354
|
trimmed.section = field.context.section;
|
|
1106
1355
|
}
|
|
1107
1356
|
if (Object.keys(trimmed).length === 0) {
|
|
1108
|
-
|
|
1109
|
-
return
|
|
1357
|
+
delete next.context;
|
|
1358
|
+
return next;
|
|
1110
1359
|
}
|
|
1111
|
-
|
|
1360
|
+
next.context = trimmed;
|
|
1361
|
+
return next;
|
|
1112
1362
|
});
|
|
1113
1363
|
}
|
|
1114
1364
|
function toLandmarkModel(node) {
|
|
@@ -1690,6 +1940,8 @@ function walkNode(element, layout, path) {
|
|
|
1690
1940
|
meta.scrollX = semantic.scrollX;
|
|
1691
1941
|
if (typeof semantic?.scrollY === 'number' && Number.isFinite(semantic.scrollY))
|
|
1692
1942
|
meta.scrollY = semantic.scrollY;
|
|
1943
|
+
if (typeof semantic?.tag === 'string' && semantic.tag.trim().length > 0)
|
|
1944
|
+
meta.controlTag = semantic.tag;
|
|
1693
1945
|
const children = [];
|
|
1694
1946
|
const elementChildren = element.children;
|
|
1695
1947
|
const layoutChildren = layout.children;
|
|
@@ -1789,7 +2041,7 @@ function applyPatches(layout, patches) {
|
|
|
1789
2041
|
node.height = patch.height;
|
|
1790
2042
|
}
|
|
1791
2043
|
}
|
|
1792
|
-
function sendAndWaitForUpdate(session, message, timeoutMs = ACTION_UPDATE_TIMEOUT_MS) {
|
|
2044
|
+
function sendAndWaitForUpdate(session, message, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, opts) {
|
|
1793
2045
|
return new Promise((resolve, reject) => {
|
|
1794
2046
|
if (session.ws.readyState !== WebSocket.OPEN) {
|
|
1795
2047
|
reject(new Error('Not connected'));
|
|
@@ -1798,11 +2050,14 @@ function sendAndWaitForUpdate(session, message, timeoutMs = ACTION_UPDATE_TIMEOU
|
|
|
1798
2050
|
const requestId = `req-${++nextRequestSequence}`;
|
|
1799
2051
|
const startRevision = session.updateRevision;
|
|
1800
2052
|
session.ws.send(JSON.stringify({ ...message, requestId }));
|
|
1801
|
-
waitForNextUpdate(session, timeoutMs, requestId, startRevision).then(resolve).catch(reject);
|
|
2053
|
+
waitForNextUpdate(session, timeoutMs, requestId, startRevision, opts).then(resolve).catch(reject);
|
|
1802
2054
|
});
|
|
1803
2055
|
}
|
|
1804
|
-
function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, requestId, startRevision = session.updateRevision) {
|
|
2056
|
+
function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, requestId, startRevision = session.updateRevision, opts) {
|
|
1805
2057
|
return new Promise((resolve, reject) => {
|
|
2058
|
+
let ackSeen = false;
|
|
2059
|
+
let ackResult;
|
|
2060
|
+
const ackPayload = () => (ackSeen && ackResult !== undefined ? { result: ackResult } : {});
|
|
1806
2061
|
const onMessage = (data) => {
|
|
1807
2062
|
try {
|
|
1808
2063
|
const msg = JSON.parse(String(data));
|
|
@@ -1813,13 +2068,26 @@ function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, reques
|
|
|
1813
2068
|
reject(new Error(typeof msg.message === 'string' ? msg.message : 'Geometra server error'));
|
|
1814
2069
|
return;
|
|
1815
2070
|
}
|
|
1816
|
-
if (msg.type === '
|
|
2071
|
+
if ((msg.type === 'frame' || (msg.type === 'patch' && session.layout)) && ackSeen && session.updateRevision > startRevision) {
|
|
1817
2072
|
cleanup();
|
|
1818
2073
|
resolve({
|
|
1819
|
-
status:
|
|
2074
|
+
status: 'updated',
|
|
1820
2075
|
timeoutMs,
|
|
1821
|
-
...(
|
|
2076
|
+
...ackPayload(),
|
|
1822
2077
|
});
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
if (msg.type === 'ack' && messageRequestId === requestId) {
|
|
2081
|
+
ackSeen = true;
|
|
2082
|
+
ackResult = msg.result;
|
|
2083
|
+
if (!opts?.requireUpdateOnAck || session.updateRevision > startRevision) {
|
|
2084
|
+
cleanup();
|
|
2085
|
+
resolve({
|
|
2086
|
+
status: session.updateRevision > startRevision ? 'updated' : 'acknowledged',
|
|
2087
|
+
timeoutMs,
|
|
2088
|
+
...ackPayload(),
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
1823
2091
|
}
|
|
1824
2092
|
return;
|
|
1825
2093
|
}
|
|
@@ -1851,7 +2119,11 @@ function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, reques
|
|
|
1851
2119
|
const timeout = setTimeout(() => {
|
|
1852
2120
|
cleanup();
|
|
1853
2121
|
if (requestId && session.updateRevision > startRevision) {
|
|
1854
|
-
resolve({ status: 'updated', timeoutMs });
|
|
2122
|
+
resolve({ status: 'updated', timeoutMs, ...ackPayload() });
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
if (requestId && ackSeen) {
|
|
2126
|
+
resolve({ status: 'acknowledged', timeoutMs, ...ackPayload() });
|
|
1855
2127
|
return;
|
|
1856
2128
|
}
|
|
1857
2129
|
resolve({ status: 'timed_out', timeoutMs });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geometra/mcp",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.17",
|
|
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.17",
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
35
35
|
"ws": "^8.18.0",
|
|
36
36
|
"zod": "^3.23.0"
|