@geometra/mcp 1.63.2 → 1.64.0
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/dist/server.js +754 -353
- package/dist/session.d.ts +96 -0
- package/dist/session.js +245 -8
- package/package.json +2 -2
package/dist/session.d.ts
CHANGED
|
@@ -29,6 +29,17 @@ export interface A11yNode {
|
|
|
29
29
|
scrollX?: number;
|
|
30
30
|
scrollY?: number;
|
|
31
31
|
controlTag?: string;
|
|
32
|
+
/** Stable authored DOM identity emitted by the proxy extractor (`id:` or `name:`). */
|
|
33
|
+
controlKey?: string;
|
|
34
|
+
controlId?: string;
|
|
35
|
+
controlName?: string;
|
|
36
|
+
options?: Array<{
|
|
37
|
+
value: string;
|
|
38
|
+
label: string;
|
|
39
|
+
disabled: boolean;
|
|
40
|
+
selected: boolean;
|
|
41
|
+
index: number;
|
|
42
|
+
}>;
|
|
32
43
|
placeholder?: string;
|
|
33
44
|
inputPattern?: string;
|
|
34
45
|
inputType?: string;
|
|
@@ -284,8 +295,19 @@ export interface PageSectionDetail {
|
|
|
284
295
|
export type FormSchemaFieldKind = 'text' | 'choice' | 'toggle' | 'multi_choice';
|
|
285
296
|
export type FormSchemaChoiceType = 'select' | 'group' | 'listbox';
|
|
286
297
|
export type FormSchemaContextMode = 'auto' | 'always' | 'none';
|
|
298
|
+
export interface FormSchemaOption {
|
|
299
|
+
/** Stable within the authored control identity; index disambiguates duplicate values/labels. */
|
|
300
|
+
id: string;
|
|
301
|
+
value: string;
|
|
302
|
+
label: string;
|
|
303
|
+
index: number;
|
|
304
|
+
disabled?: boolean;
|
|
305
|
+
selected?: boolean;
|
|
306
|
+
}
|
|
287
307
|
export interface FormSchemaField {
|
|
288
308
|
id: string;
|
|
309
|
+
/** Authored DOM identity used for exact proxy-side resolution before accessible-label fallback. */
|
|
310
|
+
fieldKey?: string;
|
|
289
311
|
kind: FormSchemaFieldKind;
|
|
290
312
|
label: string;
|
|
291
313
|
required?: boolean;
|
|
@@ -299,6 +321,7 @@ export interface FormSchemaField {
|
|
|
299
321
|
values?: string[];
|
|
300
322
|
optionCount?: number;
|
|
301
323
|
options?: string[];
|
|
324
|
+
optionDetails?: FormSchemaOption[];
|
|
302
325
|
aliases?: Record<string, string[]>;
|
|
303
326
|
format?: {
|
|
304
327
|
placeholder?: string;
|
|
@@ -346,6 +369,60 @@ export interface FormSchemaBuildOptions {
|
|
|
346
369
|
includeOptions?: boolean;
|
|
347
370
|
includeContext?: FormSchemaContextMode;
|
|
348
371
|
}
|
|
372
|
+
export interface FormGraphSource {
|
|
373
|
+
id: string;
|
|
374
|
+
kind: 'html';
|
|
375
|
+
title?: string;
|
|
376
|
+
url?: string;
|
|
377
|
+
}
|
|
378
|
+
export interface FormGraphSourceAnchor {
|
|
379
|
+
sourceId: string;
|
|
380
|
+
kind: 'html';
|
|
381
|
+
fieldName?: string;
|
|
382
|
+
pointer?: string;
|
|
383
|
+
}
|
|
384
|
+
export interface FormGraphField {
|
|
385
|
+
id: string;
|
|
386
|
+
path: string;
|
|
387
|
+
label: string;
|
|
388
|
+
kind: 'boolean' | 'enum' | 'text' | 'textarea' | 'email' | 'phone' | 'date' | 'number';
|
|
389
|
+
required?: boolean;
|
|
390
|
+
reviewRequired?: boolean;
|
|
391
|
+
aliases?: string[];
|
|
392
|
+
options?: Array<{
|
|
393
|
+
value: string;
|
|
394
|
+
label: string;
|
|
395
|
+
}>;
|
|
396
|
+
constraints?: {
|
|
397
|
+
pattern?: string;
|
|
398
|
+
};
|
|
399
|
+
sourceAnchors?: FormGraphSourceAnchor[];
|
|
400
|
+
metadata?: Record<string, unknown>;
|
|
401
|
+
}
|
|
402
|
+
export interface FormGraphModel {
|
|
403
|
+
formgraph: '0.1';
|
|
404
|
+
id: string;
|
|
405
|
+
title: string;
|
|
406
|
+
description?: string;
|
|
407
|
+
sources: FormGraphSource[];
|
|
408
|
+
fields: FormGraphField[];
|
|
409
|
+
evidence: [];
|
|
410
|
+
dependencies: [];
|
|
411
|
+
review: {
|
|
412
|
+
autoSubmitAllowed: false;
|
|
413
|
+
requiredBeforeSubmit: true;
|
|
414
|
+
};
|
|
415
|
+
metadata: {
|
|
416
|
+
producer: 'geometra';
|
|
417
|
+
geometra: {
|
|
418
|
+
formId: string;
|
|
419
|
+
fieldCount: number;
|
|
420
|
+
requiredCount: number;
|
|
421
|
+
invalidCount: number;
|
|
422
|
+
sections?: FormSchemaSection[];
|
|
423
|
+
};
|
|
424
|
+
};
|
|
425
|
+
}
|
|
349
426
|
export interface UiNodeUpdate {
|
|
350
427
|
before: CompactUiNode;
|
|
351
428
|
after: CompactUiNode;
|
|
@@ -460,12 +537,14 @@ export interface UpdateWaitResult {
|
|
|
460
537
|
export type ProxyFillField = {
|
|
461
538
|
kind: 'auto';
|
|
462
539
|
fieldId?: string;
|
|
540
|
+
fieldKey?: string;
|
|
463
541
|
fieldLabel: string;
|
|
464
542
|
value: string | boolean;
|
|
465
543
|
exact?: boolean;
|
|
466
544
|
} | {
|
|
467
545
|
kind: 'text';
|
|
468
546
|
fieldId?: string;
|
|
547
|
+
fieldKey?: string;
|
|
469
548
|
fieldLabel: string;
|
|
470
549
|
value: string;
|
|
471
550
|
exact?: boolean;
|
|
@@ -474,13 +553,17 @@ export type ProxyFillField = {
|
|
|
474
553
|
} | {
|
|
475
554
|
kind: 'choice';
|
|
476
555
|
fieldId?: string;
|
|
556
|
+
fieldKey?: string;
|
|
477
557
|
fieldLabel: string;
|
|
478
558
|
value: string;
|
|
559
|
+
optionIndex?: number;
|
|
479
560
|
query?: string;
|
|
480
561
|
exact?: boolean;
|
|
481
562
|
choiceType?: FormSchemaChoiceType;
|
|
482
563
|
} | {
|
|
483
564
|
kind: 'toggle';
|
|
565
|
+
fieldId?: string;
|
|
566
|
+
fieldKey?: string;
|
|
484
567
|
label: string;
|
|
485
568
|
checked?: boolean;
|
|
486
569
|
exact?: boolean;
|
|
@@ -488,6 +571,7 @@ export type ProxyFillField = {
|
|
|
488
571
|
} | {
|
|
489
572
|
kind: 'file';
|
|
490
573
|
fieldId?: string;
|
|
574
|
+
fieldKey?: string;
|
|
491
575
|
fieldLabel: string;
|
|
492
576
|
paths: string[];
|
|
493
577
|
exact?: boolean;
|
|
@@ -625,6 +709,8 @@ export declare function sendFileUpload(session: Session, paths: string[], opts?:
|
|
|
625
709
|
x: number;
|
|
626
710
|
y: number;
|
|
627
711
|
};
|
|
712
|
+
fieldId?: string;
|
|
713
|
+
fieldKey?: string;
|
|
628
714
|
fieldLabel?: string;
|
|
629
715
|
exact?: boolean;
|
|
630
716
|
strategy?: 'auto' | 'chooser' | 'hidden' | 'drop';
|
|
@@ -637,6 +723,7 @@ export declare function sendFileUpload(session: Session, paths: string[], opts?:
|
|
|
637
723
|
export declare function sendFieldText(session: Session, fieldLabel: string, value: string, opts?: {
|
|
638
724
|
exact?: boolean;
|
|
639
725
|
fieldId?: string;
|
|
726
|
+
fieldKey?: string;
|
|
640
727
|
typingDelayMs?: number;
|
|
641
728
|
imeFriendly?: boolean;
|
|
642
729
|
}, timeoutMs?: number): Promise<UpdateWaitResult>;
|
|
@@ -646,6 +733,8 @@ export declare function sendFieldChoice(session: Session, fieldLabel: string, va
|
|
|
646
733
|
query?: string;
|
|
647
734
|
choiceType?: FormSchemaChoiceType;
|
|
648
735
|
fieldId?: string;
|
|
736
|
+
fieldKey?: string;
|
|
737
|
+
optionIndex?: number;
|
|
649
738
|
}, timeoutMs?: number): Promise<UpdateWaitResult>;
|
|
650
739
|
/** Fill several semantic form fields in one proxy-side batch. */
|
|
651
740
|
export declare function sendFillFields(session: Session, fields: ProxyFillField[], timeoutMs?: number): Promise<UpdateWaitResult>;
|
|
@@ -668,6 +757,8 @@ export declare function sendListboxPick(session: Session, label: string, opts?:
|
|
|
668
757
|
};
|
|
669
758
|
fieldLabel?: string;
|
|
670
759
|
query?: string;
|
|
760
|
+
fieldId?: string;
|
|
761
|
+
fieldKey?: string;
|
|
671
762
|
}, timeoutMs?: number): Promise<UpdateWaitResult>;
|
|
672
763
|
/** Native `<select>` only: click the control center, then pick by value, label text, or zero-based index. */
|
|
673
764
|
export declare function sendSelectOption(session: Session, x: number, y: number, option: {
|
|
@@ -680,6 +771,9 @@ export declare function sendSetChecked(session: Session, label: string, opts?: {
|
|
|
680
771
|
checked?: boolean;
|
|
681
772
|
exact?: boolean;
|
|
682
773
|
controlType?: 'checkbox' | 'radio';
|
|
774
|
+
fieldKey?: string;
|
|
775
|
+
contextText?: string;
|
|
776
|
+
sectionText?: string;
|
|
683
777
|
}, timeoutMs?: number): Promise<UpdateWaitResult>;
|
|
684
778
|
/** Mouse wheel / scroll. Optional `x`,`y` move pointer before scrolling. */
|
|
685
779
|
export declare function sendWheel(session: Session, deltaY: number, opts?: {
|
|
@@ -732,6 +826,8 @@ export declare function buildPageModel(root: A11yNode, options?: {
|
|
|
732
826
|
blockDetection?: boolean;
|
|
733
827
|
}): PageModel;
|
|
734
828
|
export declare function buildFormSchemas(root: A11yNode, options?: FormSchemaBuildOptions): FormSchemaModel[];
|
|
829
|
+
export declare function buildFormGraphs(root: A11yNode, options?: FormSchemaBuildOptions): FormGraphModel[];
|
|
830
|
+
export declare function formSchemaToFormGraph(schema: FormSchemaModel, pageUrl?: string): FormGraphModel;
|
|
735
831
|
/**
|
|
736
832
|
* Required-field snapshot for automation: every required field in a form, including
|
|
737
833
|
* offscreen entries, annotated with visibility and scroll hints so agents do not
|
package/dist/session.js
CHANGED
|
@@ -37,6 +37,7 @@ const FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS = 225;
|
|
|
37
37
|
const FILL_BATCH_FILE_FIELD_TIMEOUT_MS = 5000;
|
|
38
38
|
const FILL_BATCH_MAX_TIMEOUT_MS = 60_000;
|
|
39
39
|
const SESSION_RECONNECT_TIMEOUT_MS = 5_000;
|
|
40
|
+
const PROXY_PROTOCOL_VERSION = 2;
|
|
40
41
|
let nextRequestSequence = 0;
|
|
41
42
|
function invalidateSessionCaches(session) {
|
|
42
43
|
session.cachedA11y = null;
|
|
@@ -854,7 +855,7 @@ export function connect(url, opts) {
|
|
|
854
855
|
if (!opts?.skipInitialResize) {
|
|
855
856
|
const width = opts?.width ?? 1024;
|
|
856
857
|
const height = opts?.height ?? 768;
|
|
857
|
-
ws.send(JSON.stringify({ type: 'resize', width, height }));
|
|
858
|
+
ws.send(JSON.stringify({ type: 'resize', width, height, protocolVersion: PROXY_PROTOCOL_VERSION }));
|
|
858
859
|
}
|
|
859
860
|
if (opts?.awaitInitialFrame === false && !resolved) {
|
|
860
861
|
resolved = true;
|
|
@@ -1279,7 +1280,7 @@ async function ensureSessionConnected(session) {
|
|
|
1279
1280
|
async function sendResizeAndWaitForUpdate(session, width, height, timeoutMs = 5_000) {
|
|
1280
1281
|
await ensureSessionConnected(session);
|
|
1281
1282
|
const startRevision = session.updateRevision;
|
|
1282
|
-
session.ws.send(JSON.stringify({ type: 'resize', width, height }));
|
|
1283
|
+
session.ws.send(JSON.stringify({ type: 'resize', width, height, protocolVersion: PROXY_PROTOCOL_VERSION }));
|
|
1283
1284
|
return await waitForNextUpdate(session, timeoutMs, undefined, startRevision);
|
|
1284
1285
|
}
|
|
1285
1286
|
/**
|
|
@@ -1303,6 +1304,7 @@ export function sendType(session, text, timeoutMs) {
|
|
|
1303
1304
|
for (const char of text) {
|
|
1304
1305
|
const keyEvent = {
|
|
1305
1306
|
type: 'key',
|
|
1307
|
+
protocolVersion: PROXY_PROTOCOL_VERSION,
|
|
1306
1308
|
eventType: 'onKeyDown',
|
|
1307
1309
|
key: char,
|
|
1308
1310
|
code: `Key${char.toUpperCase()}`,
|
|
@@ -1345,6 +1347,10 @@ export function sendFileUpload(session, paths, opts, timeoutMs) {
|
|
|
1345
1347
|
}
|
|
1346
1348
|
if (opts?.fieldLabel)
|
|
1347
1349
|
payload.fieldLabel = opts.fieldLabel;
|
|
1350
|
+
if (opts?.fieldId)
|
|
1351
|
+
payload.fieldId = opts.fieldId;
|
|
1352
|
+
if (opts?.fieldKey)
|
|
1353
|
+
payload.fieldKey = opts.fieldKey;
|
|
1348
1354
|
if (opts?.exact !== undefined)
|
|
1349
1355
|
payload.exact = opts.exact;
|
|
1350
1356
|
if (opts?.strategy)
|
|
@@ -1366,6 +1372,8 @@ export function sendFieldText(session, fieldLabel, value, opts, timeoutMs) {
|
|
|
1366
1372
|
payload.exact = opts.exact;
|
|
1367
1373
|
if (opts?.fieldId)
|
|
1368
1374
|
payload.fieldId = opts.fieldId;
|
|
1375
|
+
if (opts?.fieldKey)
|
|
1376
|
+
payload.fieldKey = opts.fieldKey;
|
|
1369
1377
|
if (opts?.typingDelayMs !== undefined)
|
|
1370
1378
|
payload.typingDelayMs = opts.typingDelayMs;
|
|
1371
1379
|
if (opts?.imeFriendly !== undefined)
|
|
@@ -1387,6 +1395,10 @@ export function sendFieldChoice(session, fieldLabel, value, opts, timeoutMs = LI
|
|
|
1387
1395
|
payload.choiceType = opts.choiceType;
|
|
1388
1396
|
if (opts?.fieldId)
|
|
1389
1397
|
payload.fieldId = opts.fieldId;
|
|
1398
|
+
if (opts?.fieldKey)
|
|
1399
|
+
payload.fieldKey = opts.fieldKey;
|
|
1400
|
+
if (opts?.optionIndex !== undefined)
|
|
1401
|
+
payload.optionIndex = opts.optionIndex;
|
|
1390
1402
|
return sendAndWaitForUpdate(session, payload, timeoutMs);
|
|
1391
1403
|
}
|
|
1392
1404
|
/** Fill several semantic form fields in one proxy-side batch. */
|
|
@@ -1425,6 +1437,10 @@ export function sendListboxPick(session, label, opts, timeoutMs = LISTBOX_UPDATE
|
|
|
1425
1437
|
payload.fieldLabel = opts.fieldLabel;
|
|
1426
1438
|
if (opts?.query)
|
|
1427
1439
|
payload.query = opts.query;
|
|
1440
|
+
if (opts?.fieldId)
|
|
1441
|
+
payload.fieldId = opts.fieldId;
|
|
1442
|
+
if (opts?.fieldKey)
|
|
1443
|
+
payload.fieldKey = opts.fieldKey;
|
|
1428
1444
|
return sendAndWaitForUpdate(session, payload, timeoutMs);
|
|
1429
1445
|
}
|
|
1430
1446
|
/** Native `<select>` only: click the control center, then pick by value, label text, or zero-based index. */
|
|
@@ -1445,6 +1461,12 @@ export function sendSetChecked(session, label, opts, timeoutMs) {
|
|
|
1445
1461
|
payload.exact = opts.exact;
|
|
1446
1462
|
if (opts?.controlType)
|
|
1447
1463
|
payload.controlType = opts.controlType;
|
|
1464
|
+
if (opts?.fieldKey)
|
|
1465
|
+
payload.fieldKey = opts.fieldKey;
|
|
1466
|
+
if (opts?.contextText)
|
|
1467
|
+
payload.contextText = opts.contextText;
|
|
1468
|
+
if (opts?.sectionText)
|
|
1469
|
+
payload.sectionText = opts.sectionText;
|
|
1448
1470
|
return sendAndWaitForUpdate(session, payload, timeoutMs);
|
|
1449
1471
|
}
|
|
1450
1472
|
/** Mouse wheel / scroll. Optional `x`,`y` move pointer before scrolling. */
|
|
@@ -2223,6 +2245,26 @@ function buildFieldFormat(node) {
|
|
|
2223
2245
|
format.autocomplete = m.autocomplete;
|
|
2224
2246
|
return Object.keys(format).length > 0 ? format : undefined;
|
|
2225
2247
|
}
|
|
2248
|
+
function nativeSchemaOptions(node, fieldIdentity) {
|
|
2249
|
+
const options = node.meta?.options;
|
|
2250
|
+
if (!options)
|
|
2251
|
+
return undefined;
|
|
2252
|
+
return options.map(option => ({
|
|
2253
|
+
id: `${fieldIdentity}:option:${option.index}`,
|
|
2254
|
+
value: option.value,
|
|
2255
|
+
label: option.label,
|
|
2256
|
+
index: option.index,
|
|
2257
|
+
...(option.disabled ? { disabled: true } : {}),
|
|
2258
|
+
...(option.selected ? { selected: true } : {}),
|
|
2259
|
+
}));
|
|
2260
|
+
}
|
|
2261
|
+
function uniqueSchemaFieldKey(root, node) {
|
|
2262
|
+
const fieldKey = node.meta?.controlKey;
|
|
2263
|
+
if (!fieldKey)
|
|
2264
|
+
return undefined;
|
|
2265
|
+
const matches = collectDescendants(root, candidate => candidate.meta?.controlKey === fieldKey);
|
|
2266
|
+
return matches.length === 1 ? fieldKey : undefined;
|
|
2267
|
+
}
|
|
2226
2268
|
function simpleSchemaField(root, node) {
|
|
2227
2269
|
const context = nodeContextForNode(root, node);
|
|
2228
2270
|
const label = fieldLabel(node) ?? sanitizeInlineName(node.name, 80) ?? context?.prompt;
|
|
@@ -2251,15 +2293,25 @@ function simpleSchemaField(root, node) {
|
|
|
2251
2293
|
? 'select'
|
|
2252
2294
|
: 'listbox'
|
|
2253
2295
|
: undefined;
|
|
2296
|
+
const fieldKey = uniqueSchemaFieldKey(root, node);
|
|
2297
|
+
const id = fieldKey ?? formFieldIdForPath(node.path);
|
|
2254
2298
|
const format = buildFieldFormat(node);
|
|
2299
|
+
const optionDetails = classifiedChoiceType === 'select' ? nativeSchemaOptions(node, id) : undefined;
|
|
2300
|
+
const enabledOptions = optionDetails
|
|
2301
|
+
? dedupeStrings(optionDetails.filter(option => !option.disabled).map(option => option.label), 64)
|
|
2302
|
+
: undefined;
|
|
2255
2303
|
return {
|
|
2256
|
-
id
|
|
2304
|
+
id,
|
|
2305
|
+
...(fieldKey ? { fieldKey } : {}),
|
|
2257
2306
|
kind: classifiedRole === 'combobox' ? 'choice' : 'text',
|
|
2258
2307
|
label,
|
|
2259
2308
|
...(classifiedChoiceType ? { choiceType: classifiedChoiceType } : {}),
|
|
2260
2309
|
...(node.state?.required ? { required: true } : {}),
|
|
2261
2310
|
...(node.state?.invalid ? { invalid: true } : {}),
|
|
2262
2311
|
...compactSchemaValue(node.value, 72),
|
|
2312
|
+
...(optionDetails ? { optionCount: optionDetails.length, optionDetails } : {}),
|
|
2313
|
+
...(enabledOptions && enabledOptions.length > 0 ? { options: enabledOptions } : {}),
|
|
2314
|
+
...(enabledOptions && computeOptionAliases(enabledOptions) ? { aliases: computeOptionAliases(enabledOptions) } : {}),
|
|
2263
2315
|
...(format ? { format } : {}),
|
|
2264
2316
|
...(compactSchemaContext(context, label) ? { context: compactSchemaContext(context, label) } : {}),
|
|
2265
2317
|
};
|
|
@@ -2304,8 +2356,10 @@ function toggleSchemaField(root, node) {
|
|
|
2304
2356
|
return null;
|
|
2305
2357
|
const context = nodeContextForNode(root, node);
|
|
2306
2358
|
const controlType = node.role === 'radio' ? 'radio' : 'checkbox';
|
|
2359
|
+
const fieldKey = uniqueSchemaFieldKey(root, node);
|
|
2307
2360
|
return {
|
|
2308
|
-
id: formFieldIdForPath(node.path),
|
|
2361
|
+
id: fieldKey ?? formFieldIdForPath(node.path),
|
|
2362
|
+
...(fieldKey ? { fieldKey } : {}),
|
|
2309
2363
|
kind: 'toggle',
|
|
2310
2364
|
label,
|
|
2311
2365
|
controlType,
|
|
@@ -2330,7 +2384,10 @@ function detectFormSections(formNode, fields) {
|
|
|
2330
2384
|
return [];
|
|
2331
2385
|
const fieldIdToPath = new Map();
|
|
2332
2386
|
for (const field of fields) {
|
|
2333
|
-
const
|
|
2387
|
+
const authoredMatches = field.fieldKey
|
|
2388
|
+
? collectDescendants(formNode, node => node.meta?.controlKey === field.fieldKey)
|
|
2389
|
+
: [];
|
|
2390
|
+
const parsed = authoredMatches.length === 1 ? authoredMatches[0].path : parseFormFieldId(field.id);
|
|
2334
2391
|
if (parsed)
|
|
2335
2392
|
fieldIdToPath.set(field.id, parsed);
|
|
2336
2393
|
}
|
|
@@ -2428,6 +2485,7 @@ function presentFormSchemaFields(fields, options) {
|
|
|
2428
2485
|
next.booleanChoice = true;
|
|
2429
2486
|
if (!includeOptions) {
|
|
2430
2487
|
delete next.options;
|
|
2488
|
+
delete next.optionDetails;
|
|
2431
2489
|
delete next.aliases;
|
|
2432
2490
|
}
|
|
2433
2491
|
if (includeContext === 'none') {
|
|
@@ -2761,6 +2819,135 @@ export function buildFormSchemas(root, options) {
|
|
|
2761
2819
|
.filter(form => !options?.formId || sectionIdForPath('form', form.path) === options.formId)
|
|
2762
2820
|
.map(form => buildFormSchemaForNode(root, form, options));
|
|
2763
2821
|
}
|
|
2822
|
+
export function buildFormGraphs(root, options) {
|
|
2823
|
+
const pageUrl = typeof root.meta?.pageUrl === 'string' && root.meta.pageUrl.length > 0 ? root.meta.pageUrl : undefined;
|
|
2824
|
+
return buildFormSchemas(root, {
|
|
2825
|
+
...options,
|
|
2826
|
+
includeOptions: true,
|
|
2827
|
+
}).map(schema => formSchemaToFormGraph(schema, pageUrl));
|
|
2828
|
+
}
|
|
2829
|
+
export function formSchemaToFormGraph(schema, pageUrl) {
|
|
2830
|
+
const sourceId = 'geometra-page';
|
|
2831
|
+
const formSlug = slugPathSegment(schema.name ?? schema.formId);
|
|
2832
|
+
const pathCounts = new Map();
|
|
2833
|
+
return {
|
|
2834
|
+
formgraph: '0.1',
|
|
2835
|
+
id: `geometra:${schema.formId}`,
|
|
2836
|
+
title: schema.name ?? `Geometra form ${schema.formId}`,
|
|
2837
|
+
description: 'FormGraph-compatible projection of a Geometra form schema.',
|
|
2838
|
+
sources: [
|
|
2839
|
+
{
|
|
2840
|
+
id: sourceId,
|
|
2841
|
+
kind: 'html',
|
|
2842
|
+
title: schema.name ?? 'Geometra-discovered web form',
|
|
2843
|
+
...(pageUrl ? { url: pageUrl } : {}),
|
|
2844
|
+
},
|
|
2845
|
+
],
|
|
2846
|
+
fields: schema.fields.map(field => formSchemaFieldToFormGraphField(field, {
|
|
2847
|
+
formSlug,
|
|
2848
|
+
sourceId,
|
|
2849
|
+
pathCounts,
|
|
2850
|
+
})),
|
|
2851
|
+
evidence: [],
|
|
2852
|
+
dependencies: [],
|
|
2853
|
+
review: {
|
|
2854
|
+
autoSubmitAllowed: false,
|
|
2855
|
+
requiredBeforeSubmit: true,
|
|
2856
|
+
},
|
|
2857
|
+
metadata: {
|
|
2858
|
+
producer: 'geometra',
|
|
2859
|
+
geometra: {
|
|
2860
|
+
formId: schema.formId,
|
|
2861
|
+
fieldCount: schema.fieldCount,
|
|
2862
|
+
requiredCount: schema.requiredCount,
|
|
2863
|
+
invalidCount: schema.invalidCount,
|
|
2864
|
+
...(schema.sections ? { sections: schema.sections } : {}),
|
|
2865
|
+
},
|
|
2866
|
+
},
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
function formSchemaFieldToFormGraphField(field, opts) {
|
|
2870
|
+
const basePath = `web.forms.${opts.formSlug}.${slugPathSegment(field.label || field.id)}`;
|
|
2871
|
+
const path = uniquePath(basePath, opts.pathCounts);
|
|
2872
|
+
const aliases = fieldAliases(field);
|
|
2873
|
+
const options = field.optionDetails
|
|
2874
|
+
?.filter(option => !option.disabled)
|
|
2875
|
+
.map(option => ({ value: option.value, label: option.label }))
|
|
2876
|
+
?? field.options?.map(option => ({ value: option, label: option }));
|
|
2877
|
+
const inputType = field.format?.inputType?.toLowerCase();
|
|
2878
|
+
const metadata = {
|
|
2879
|
+
geometra: {
|
|
2880
|
+
fieldId: field.id,
|
|
2881
|
+
...(field.fieldKey ? { fieldKey: field.fieldKey } : {}),
|
|
2882
|
+
kind: field.kind,
|
|
2883
|
+
...(field.choiceType ? { choiceType: field.choiceType } : {}),
|
|
2884
|
+
...(field.controlType ? { controlType: field.controlType } : {}),
|
|
2885
|
+
...(field.booleanChoice ? { booleanChoice: true } : {}),
|
|
2886
|
+
...(field.invalid ? { invalid: true } : {}),
|
|
2887
|
+
...(field.context ? { context: field.context } : {}),
|
|
2888
|
+
...(field.optionDetails ? { optionDetails: field.optionDetails } : {}),
|
|
2889
|
+
},
|
|
2890
|
+
};
|
|
2891
|
+
return {
|
|
2892
|
+
id: field.fieldKey ?? field.id,
|
|
2893
|
+
path,
|
|
2894
|
+
label: field.label,
|
|
2895
|
+
kind: formGraphFieldKind(field),
|
|
2896
|
+
...(field.required ? { required: true } : {}),
|
|
2897
|
+
...(field.invalid ? { reviewRequired: true } : {}),
|
|
2898
|
+
...(aliases.length > 0 ? { aliases } : {}),
|
|
2899
|
+
...(options && options.length > 0 ? { options } : {}),
|
|
2900
|
+
...(field.format?.pattern ? { constraints: { pattern: field.format.pattern } } : {}),
|
|
2901
|
+
sourceAnchors: [
|
|
2902
|
+
{
|
|
2903
|
+
sourceId: opts.sourceId,
|
|
2904
|
+
kind: 'html',
|
|
2905
|
+
fieldName: field.fieldKey ?? field.id,
|
|
2906
|
+
pointer: `geometra:${field.fieldKey ?? field.id}`,
|
|
2907
|
+
},
|
|
2908
|
+
],
|
|
2909
|
+
...(inputType ? { metadata: { ...metadata, inputType } } : { metadata }),
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
function formGraphFieldKind(field) {
|
|
2913
|
+
if (field.kind === 'toggle' || field.booleanChoice)
|
|
2914
|
+
return 'boolean';
|
|
2915
|
+
if (field.kind === 'choice' || field.kind === 'multi_choice')
|
|
2916
|
+
return 'enum';
|
|
2917
|
+
const inputType = field.format?.inputType?.toLowerCase();
|
|
2918
|
+
if (inputType === 'email')
|
|
2919
|
+
return 'email';
|
|
2920
|
+
if (inputType === 'tel' || inputType === 'phone')
|
|
2921
|
+
return 'phone';
|
|
2922
|
+
if (inputType === 'date')
|
|
2923
|
+
return 'date';
|
|
2924
|
+
if (inputType === 'number')
|
|
2925
|
+
return 'number';
|
|
2926
|
+
if (field.valueLength !== undefined && field.valueLength > 120)
|
|
2927
|
+
return 'textarea';
|
|
2928
|
+
return 'text';
|
|
2929
|
+
}
|
|
2930
|
+
function fieldAliases(field) {
|
|
2931
|
+
const values = new Set();
|
|
2932
|
+
if (field.format?.placeholder)
|
|
2933
|
+
values.add(field.format.placeholder);
|
|
2934
|
+
if (field.context?.prompt && normalizeUiText(field.context.prompt) !== normalizeUiText(field.label)) {
|
|
2935
|
+
values.add(field.context.prompt);
|
|
2936
|
+
}
|
|
2937
|
+
return [...values].filter(value => value.trim().length > 0);
|
|
2938
|
+
}
|
|
2939
|
+
function uniquePath(basePath, seen) {
|
|
2940
|
+
const count = seen.get(basePath) ?? 0;
|
|
2941
|
+
seen.set(basePath, count + 1);
|
|
2942
|
+
return count === 0 ? basePath : `${basePath}.${count + 1}`;
|
|
2943
|
+
}
|
|
2944
|
+
function slugPathSegment(value) {
|
|
2945
|
+
const normalized = normalizeUiText(value)
|
|
2946
|
+
.toLowerCase()
|
|
2947
|
+
.replace(/[^a-z0-9]+/g, '.')
|
|
2948
|
+
.replace(/^\.+|\.+$/g, '');
|
|
2949
|
+
return normalized || 'field';
|
|
2950
|
+
}
|
|
2764
2951
|
/**
|
|
2765
2952
|
* Required-field snapshot for automation: every required field in a form, including
|
|
2766
2953
|
* offscreen entries, annotated with visibility and scroll hints so agents do not
|
|
@@ -2779,7 +2966,10 @@ export function buildFormRequiredSnapshot(root, options) {
|
|
|
2779
2966
|
const formNode = parsedForm ? findNodeByPath(root, parsedForm.path) : null;
|
|
2780
2967
|
const fields = schema.fields
|
|
2781
2968
|
.map(field => {
|
|
2782
|
-
const
|
|
2969
|
+
const authoredMatches = field.fieldKey && formNode
|
|
2970
|
+
? collectDescendants(formNode, node => node.meta?.controlKey === field.fieldKey)
|
|
2971
|
+
: [];
|
|
2972
|
+
const fieldPath = authoredMatches.length === 1 ? authoredMatches[0].path : parseFormFieldId(field.id);
|
|
2783
2973
|
const target = fieldPath ? findNodeByPath(root, fieldPath) ?? formNode : formNode;
|
|
2784
2974
|
if (!target)
|
|
2785
2975
|
return null;
|
|
@@ -3183,6 +3373,32 @@ function normalizeCheckedState(value) {
|
|
|
3183
3373
|
return false;
|
|
3184
3374
|
return undefined;
|
|
3185
3375
|
}
|
|
3376
|
+
function normalizeSemanticOptions(value) {
|
|
3377
|
+
if (!Array.isArray(value))
|
|
3378
|
+
return undefined;
|
|
3379
|
+
const options = [];
|
|
3380
|
+
const seenIndices = new Set();
|
|
3381
|
+
for (const entry of value.slice(0, 500)) {
|
|
3382
|
+
if (!entry || typeof entry !== 'object')
|
|
3383
|
+
continue;
|
|
3384
|
+
const record = entry;
|
|
3385
|
+
if (typeof record.value !== 'string' || typeof record.label !== 'string')
|
|
3386
|
+
continue;
|
|
3387
|
+
if (typeof record.index !== 'number' || !Number.isInteger(record.index) || record.index < 0)
|
|
3388
|
+
continue;
|
|
3389
|
+
if (seenIndices.has(record.index))
|
|
3390
|
+
continue;
|
|
3391
|
+
seenIndices.add(record.index);
|
|
3392
|
+
options.push({
|
|
3393
|
+
value: record.value,
|
|
3394
|
+
label: record.label,
|
|
3395
|
+
disabled: record.disabled === true,
|
|
3396
|
+
selected: record.selected === true,
|
|
3397
|
+
index: record.index,
|
|
3398
|
+
});
|
|
3399
|
+
}
|
|
3400
|
+
return options;
|
|
3401
|
+
}
|
|
3186
3402
|
function normalizeA11yRoleHint(value) {
|
|
3187
3403
|
if (typeof value !== 'string')
|
|
3188
3404
|
return undefined;
|
|
@@ -3239,6 +3455,15 @@ function walkNode(element, layout, path) {
|
|
|
3239
3455
|
meta.scrollY = semantic.scrollY;
|
|
3240
3456
|
if (typeof semantic?.tag === 'string' && semantic.tag.trim().length > 0)
|
|
3241
3457
|
meta.controlTag = semantic.tag;
|
|
3458
|
+
if (typeof semantic?.controlKey === 'string' && semantic.controlKey.trim().length > 0)
|
|
3459
|
+
meta.controlKey = semantic.controlKey.trim();
|
|
3460
|
+
if (typeof semantic?.controlId === 'string' && semantic.controlId.trim().length > 0)
|
|
3461
|
+
meta.controlId = semantic.controlId.trim();
|
|
3462
|
+
if (typeof semantic?.controlName === 'string' && semantic.controlName.trim().length > 0)
|
|
3463
|
+
meta.controlName = semantic.controlName.trim();
|
|
3464
|
+
const semanticOptions = normalizeSemanticOptions(semantic?.options);
|
|
3465
|
+
if (semanticOptions)
|
|
3466
|
+
meta.options = semanticOptions;
|
|
3242
3467
|
if (typeof semantic?.placeholder === 'string')
|
|
3243
3468
|
meta.placeholder = semantic.placeholder;
|
|
3244
3469
|
if (typeof semantic?.inputPattern === 'string')
|
|
@@ -3352,8 +3577,14 @@ async function sendAndWaitForUpdate(session, message, timeoutMs = ACTION_UPDATE_
|
|
|
3352
3577
|
await ensureSessionConnected(session);
|
|
3353
3578
|
const requestId = `req-${++nextRequestSequence}`;
|
|
3354
3579
|
const startRevision = session.updateRevision;
|
|
3355
|
-
session.ws.send(JSON.stringify({ ...message, requestId }));
|
|
3356
|
-
|
|
3580
|
+
session.ws.send(JSON.stringify({ ...message, requestId, protocolVersion: PROXY_PROTOCOL_VERSION }));
|
|
3581
|
+
const requiresExactFieldIdentity = typeof message.fieldKey === 'string' || (message.type === 'fillFields' &&
|
|
3582
|
+
Array.isArray(message.fields) &&
|
|
3583
|
+
message.fields.some(field => typeof field === 'object' && field !== null && typeof field.fieldKey === 'string'));
|
|
3584
|
+
return await waitForNextUpdate(session, timeoutMs, requestId, startRevision, {
|
|
3585
|
+
...opts,
|
|
3586
|
+
...(requiresExactFieldIdentity ? { requiredProtocolVersion: PROXY_PROTOCOL_VERSION } : {}),
|
|
3587
|
+
});
|
|
3357
3588
|
}
|
|
3358
3589
|
function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, requestId, startRevision = session.updateRevision, opts) {
|
|
3359
3590
|
return new Promise((resolve, reject) => {
|
|
@@ -3380,6 +3611,12 @@ function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, reques
|
|
|
3380
3611
|
return;
|
|
3381
3612
|
}
|
|
3382
3613
|
if (msg.type === 'ack' && messageRequestId === requestId) {
|
|
3614
|
+
const peerProtocolVersion = typeof msg.protocolVersion === 'number' ? msg.protocolVersion : undefined;
|
|
3615
|
+
if (opts?.requiredProtocolVersion !== undefined && (peerProtocolVersion === undefined || peerProtocolVersion < opts.requiredProtocolVersion)) {
|
|
3616
|
+
cleanup();
|
|
3617
|
+
reject(new Error(`Proxy protocol ${peerProtocolVersion ?? 'unknown'} cannot guarantee exact field identity; protocol ${opts.requiredProtocolVersion}+ is required. Update and reconnect the Geometra proxy.`));
|
|
3618
|
+
return;
|
|
3619
|
+
}
|
|
3383
3620
|
ackSeen = true;
|
|
3384
3621
|
ackResult = msg.result;
|
|
3385
3622
|
if (!opts?.requireUpdateOnAck || session.updateRevision > startRevision) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geometra/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.64.0",
|
|
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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"ui-testing"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@geometra/proxy": "^1.
|
|
35
|
+
"@geometra/proxy": "^1.64.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
37
37
|
"@razroo/parallel-mcp": "^0.1.0",
|
|
38
38
|
"ws": "^8.18.0",
|