@geometra/mcp 1.19.11 → 1.19.13
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 +47 -22
- package/dist/__tests__/connect-utils.test.js +44 -2
- package/dist/__tests__/server-batch-results.test.js +219 -3
- package/dist/__tests__/session-model.test.js +230 -1
- package/dist/proxy-spawn.js +80 -4
- package/dist/server.d.ts +1 -0
- package/dist/server.js +455 -18
- package/dist/session.d.ts +87 -0
- package/dist/session.js +350 -6
- package/package.json +2 -2
package/dist/session.d.ts
CHANGED
|
@@ -61,6 +61,23 @@ export interface CompactUiContext {
|
|
|
61
61
|
scrollY?: number;
|
|
62
62
|
focusedNode?: CompactUiNode;
|
|
63
63
|
}
|
|
64
|
+
export interface NodeContextModel {
|
|
65
|
+
prompt?: string;
|
|
66
|
+
section?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface NodeVisibilityModel {
|
|
69
|
+
intersectsViewport: boolean;
|
|
70
|
+
fullyVisible: boolean;
|
|
71
|
+
offscreenAbove: boolean;
|
|
72
|
+
offscreenBelow: boolean;
|
|
73
|
+
offscreenLeft: boolean;
|
|
74
|
+
offscreenRight: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface NodeScrollHintModel {
|
|
77
|
+
status: 'visible' | 'partial' | 'offscreen';
|
|
78
|
+
revealDeltaX: number;
|
|
79
|
+
revealDeltaY: number;
|
|
80
|
+
}
|
|
64
81
|
export type PageSectionKind = 'landmark' | 'form' | 'dialog' | 'list';
|
|
65
82
|
export type PageArchetype = 'shell' | 'form' | 'dialog' | 'results' | 'content' | 'dashboard';
|
|
66
83
|
interface PageSectionSummaryBase {
|
|
@@ -136,6 +153,9 @@ export interface PageFieldModel {
|
|
|
136
153
|
value?: string;
|
|
137
154
|
state?: A11yNode['state'];
|
|
138
155
|
validation?: A11yNode['validation'];
|
|
156
|
+
context?: NodeContextModel;
|
|
157
|
+
visibility?: NodeVisibilityModel;
|
|
158
|
+
scrollHint?: NodeScrollHintModel;
|
|
139
159
|
bounds?: {
|
|
140
160
|
x: number;
|
|
141
161
|
y: number;
|
|
@@ -148,6 +168,9 @@ export interface PageActionModel {
|
|
|
148
168
|
role: string;
|
|
149
169
|
name?: string;
|
|
150
170
|
state?: A11yNode['state'];
|
|
171
|
+
context?: NodeContextModel;
|
|
172
|
+
visibility?: NodeVisibilityModel;
|
|
173
|
+
scrollHint?: NodeScrollHintModel;
|
|
151
174
|
bounds?: {
|
|
152
175
|
x: number;
|
|
153
176
|
y: number;
|
|
@@ -179,10 +202,38 @@ export interface PageSectionDetail {
|
|
|
179
202
|
summary: {
|
|
180
203
|
headingCount: number;
|
|
181
204
|
fieldCount: number;
|
|
205
|
+
requiredFieldCount: number;
|
|
206
|
+
invalidFieldCount: number;
|
|
182
207
|
actionCount: number;
|
|
183
208
|
listCount: number;
|
|
184
209
|
itemCount: number;
|
|
185
210
|
};
|
|
211
|
+
page: {
|
|
212
|
+
fields: {
|
|
213
|
+
offset: number;
|
|
214
|
+
returned: number;
|
|
215
|
+
total: number;
|
|
216
|
+
hasMore: boolean;
|
|
217
|
+
};
|
|
218
|
+
actions: {
|
|
219
|
+
offset: number;
|
|
220
|
+
returned: number;
|
|
221
|
+
total: number;
|
|
222
|
+
hasMore: boolean;
|
|
223
|
+
};
|
|
224
|
+
lists: {
|
|
225
|
+
offset: number;
|
|
226
|
+
returned: number;
|
|
227
|
+
total: number;
|
|
228
|
+
hasMore: boolean;
|
|
229
|
+
};
|
|
230
|
+
items: {
|
|
231
|
+
offset: number;
|
|
232
|
+
returned: number;
|
|
233
|
+
total: number;
|
|
234
|
+
hasMore: boolean;
|
|
235
|
+
};
|
|
236
|
+
};
|
|
186
237
|
headings: PageHeadingModel[];
|
|
187
238
|
fields: PageFieldModel[];
|
|
188
239
|
actions: PageActionModel[];
|
|
@@ -190,6 +241,30 @@ export interface PageSectionDetail {
|
|
|
190
241
|
items: PageListItemModel[];
|
|
191
242
|
textPreview: string[];
|
|
192
243
|
}
|
|
244
|
+
export type FormSchemaFieldKind = 'text' | 'choice' | 'toggle' | 'multi_choice';
|
|
245
|
+
export interface FormSchemaField {
|
|
246
|
+
id: string;
|
|
247
|
+
kind: FormSchemaFieldKind;
|
|
248
|
+
label: string;
|
|
249
|
+
required?: boolean;
|
|
250
|
+
invalid?: boolean;
|
|
251
|
+
controlType?: 'checkbox' | 'radio';
|
|
252
|
+
value?: string;
|
|
253
|
+
valueLength?: number;
|
|
254
|
+
checked?: boolean;
|
|
255
|
+
values?: string[];
|
|
256
|
+
optionCount?: number;
|
|
257
|
+
options?: string[];
|
|
258
|
+
context?: NodeContextModel;
|
|
259
|
+
}
|
|
260
|
+
export interface FormSchemaModel {
|
|
261
|
+
formId: string;
|
|
262
|
+
name?: string;
|
|
263
|
+
fieldCount: number;
|
|
264
|
+
requiredCount: number;
|
|
265
|
+
invalidCount: number;
|
|
266
|
+
fields: FormSchemaField[];
|
|
267
|
+
}
|
|
193
268
|
export interface UiNodeUpdate {
|
|
194
269
|
before: CompactUiNode;
|
|
195
270
|
after: CompactUiNode;
|
|
@@ -362,15 +437,27 @@ export declare function buildPageModel(root: A11yNode, options?: {
|
|
|
362
437
|
maxPrimaryActions?: number;
|
|
363
438
|
maxSectionsPerKind?: number;
|
|
364
439
|
}): PageModel;
|
|
440
|
+
export declare function buildFormSchemas(root: A11yNode, options?: {
|
|
441
|
+
formId?: string;
|
|
442
|
+
maxFields?: number;
|
|
443
|
+
onlyRequiredFields?: boolean;
|
|
444
|
+
onlyInvalidFields?: boolean;
|
|
445
|
+
}): FormSchemaModel[];
|
|
365
446
|
/**
|
|
366
447
|
* Expand a page-model section by stable ID into richer, on-demand details.
|
|
367
448
|
*/
|
|
368
449
|
export declare function expandPageSection(root: A11yNode, id: string, options?: {
|
|
369
450
|
maxHeadings?: number;
|
|
370
451
|
maxFields?: number;
|
|
452
|
+
fieldOffset?: number;
|
|
453
|
+
onlyRequiredFields?: boolean;
|
|
454
|
+
onlyInvalidFields?: boolean;
|
|
371
455
|
maxActions?: number;
|
|
456
|
+
actionOffset?: number;
|
|
372
457
|
maxLists?: number;
|
|
458
|
+
listOffset?: number;
|
|
373
459
|
maxItems?: number;
|
|
460
|
+
itemOffset?: number;
|
|
374
461
|
maxTextPreview?: number;
|
|
375
462
|
includeBounds?: boolean;
|
|
376
463
|
}): PageSectionDetail | null;
|
package/dist/session.js
CHANGED
|
@@ -387,6 +387,9 @@ function decodePath(encoded) {
|
|
|
387
387
|
export function nodeIdForPath(path) {
|
|
388
388
|
return `n:${encodePath(path)}`;
|
|
389
389
|
}
|
|
390
|
+
function formFieldIdForPath(path) {
|
|
391
|
+
return `ff:${encodePath(path)}`;
|
|
392
|
+
}
|
|
390
393
|
function sectionPrefix(kind) {
|
|
391
394
|
if (kind === 'landmark')
|
|
392
395
|
return 'lm';
|
|
@@ -743,8 +746,111 @@ function primaryAction(node) {
|
|
|
743
746
|
bounds: cloneBounds(node.bounds),
|
|
744
747
|
};
|
|
745
748
|
}
|
|
746
|
-
function
|
|
749
|
+
function buildVisibility(bounds, viewport) {
|
|
750
|
+
const visibleLeft = Math.max(0, bounds.x);
|
|
751
|
+
const visibleTop = Math.max(0, bounds.y);
|
|
752
|
+
const visibleRight = Math.min(viewport.width, bounds.x + bounds.width);
|
|
753
|
+
const visibleBottom = Math.min(viewport.height, bounds.y + bounds.height);
|
|
754
|
+
const hasVisibleIntersection = visibleRight > visibleLeft && visibleBottom > visibleTop;
|
|
755
|
+
const fullyVisible = bounds.x >= 0 &&
|
|
756
|
+
bounds.y >= 0 &&
|
|
757
|
+
bounds.x + bounds.width <= viewport.width &&
|
|
758
|
+
bounds.y + bounds.height <= viewport.height;
|
|
759
|
+
return {
|
|
760
|
+
intersectsViewport: hasVisibleIntersection,
|
|
761
|
+
fullyVisible,
|
|
762
|
+
offscreenAbove: bounds.y + bounds.height <= 0,
|
|
763
|
+
offscreenBelow: bounds.y >= viewport.height,
|
|
764
|
+
offscreenLeft: bounds.x + bounds.width <= 0,
|
|
765
|
+
offscreenRight: bounds.x >= viewport.width,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function buildScrollHint(bounds, viewport) {
|
|
769
|
+
const visibility = buildVisibility(bounds, viewport);
|
|
770
|
+
return {
|
|
771
|
+
status: visibility.fullyVisible ? 'visible' : visibility.intersectsViewport ? 'partial' : 'offscreen',
|
|
772
|
+
revealDeltaX: Math.round(bounds.x + bounds.width / 2 - viewport.width / 2),
|
|
773
|
+
revealDeltaY: Math.round(bounds.y + bounds.height / 2 - viewport.height / 2),
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function ancestorNodes(root, path) {
|
|
777
|
+
const out = [];
|
|
778
|
+
let current = root;
|
|
779
|
+
for (const index of path) {
|
|
780
|
+
out.push(current);
|
|
781
|
+
if (!current.children[index])
|
|
782
|
+
break;
|
|
783
|
+
current = current.children[index];
|
|
784
|
+
}
|
|
785
|
+
return out;
|
|
786
|
+
}
|
|
787
|
+
function countGroupedChoiceControls(node) {
|
|
788
|
+
return collectDescendants(node, candidate => candidate.role === 'radio' || candidate.role === 'checkbox' || candidate.role === 'button').length;
|
|
789
|
+
}
|
|
790
|
+
function nearestPromptText(container, target) {
|
|
791
|
+
const candidates = collectDescendants(container, candidate => (candidate.role === 'heading' || candidate.role === 'text') &&
|
|
792
|
+
!!sanitizeInlineName(candidate.name, 120) &&
|
|
793
|
+
pathKey(candidate.path) !== pathKey(target.path));
|
|
794
|
+
const normalizedTarget = normalizeUiText(target.name ?? '');
|
|
795
|
+
const best = candidates
|
|
796
|
+
.filter(candidate => candidate.bounds.y <= target.bounds.y + 8)
|
|
797
|
+
.map(candidate => {
|
|
798
|
+
const text = sanitizeInlineName(candidate.name, 120);
|
|
799
|
+
if (!text)
|
|
800
|
+
return null;
|
|
801
|
+
if (normalizeUiText(text) === normalizedTarget)
|
|
802
|
+
return null;
|
|
803
|
+
const dy = Math.max(0, target.bounds.y - candidate.bounds.y);
|
|
804
|
+
const dx = Math.abs(target.bounds.x - candidate.bounds.x);
|
|
805
|
+
const headingBonus = candidate.role === 'heading' ? -32 : 0;
|
|
806
|
+
const questionBonus = /\?\s*$/.test(text) ? -160 : 0;
|
|
807
|
+
const lengthPenalty = text.length > 90 ? 80 : text.length > 60 ? 40 : text.length > 45 ? 20 : 0;
|
|
808
|
+
return { text, score: dy * 4 + dx + headingBonus + questionBonus + lengthPenalty };
|
|
809
|
+
})
|
|
810
|
+
.filter((candidate) => !!candidate)
|
|
811
|
+
.sort((a, b) => a.score - b.score)[0];
|
|
812
|
+
return best?.text;
|
|
813
|
+
}
|
|
814
|
+
function nodeContext(root, node) {
|
|
815
|
+
const ancestors = ancestorNodes(root, node.path);
|
|
816
|
+
let prompt;
|
|
817
|
+
const promptEligibleNode = node.role === 'radio' || node.role === 'button';
|
|
818
|
+
if (promptEligibleNode) {
|
|
819
|
+
for (let index = ancestors.length - 1; index >= 0; index--) {
|
|
820
|
+
const ancestor = ancestors[index];
|
|
821
|
+
const grouped = countGroupedChoiceControls(ancestor) >= 2;
|
|
822
|
+
const eligiblePromptContainer = (ancestor.role === 'group' && ancestor.path.length > 0) ||
|
|
823
|
+
ancestor.role === 'dialog' ||
|
|
824
|
+
(ancestor.role === 'form' && grouped);
|
|
825
|
+
if (eligiblePromptContainer) {
|
|
826
|
+
prompt = nearestPromptText(ancestor, node);
|
|
827
|
+
if (prompt)
|
|
828
|
+
break;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
let section;
|
|
833
|
+
for (let index = ancestors.length - 1; index >= 0; index--) {
|
|
834
|
+
const ancestor = ancestors[index];
|
|
835
|
+
const kind = sectionKindForNode(ancestor);
|
|
836
|
+
if (!kind)
|
|
837
|
+
continue;
|
|
838
|
+
section = sectionDisplayName(ancestor, kind);
|
|
839
|
+
if (section)
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
if (!prompt && !section)
|
|
843
|
+
return undefined;
|
|
844
|
+
return {
|
|
845
|
+
...(prompt ? { prompt } : {}),
|
|
846
|
+
...(section ? { section } : {}),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function toFieldModel(root, node, includeBounds = true) {
|
|
747
850
|
const value = sanitizeInlineName(node.value, 120);
|
|
851
|
+
const context = nodeContext(root, node);
|
|
852
|
+
const visibility = buildVisibility(node.bounds, root.bounds);
|
|
853
|
+
const scrollHint = buildScrollHint(node.bounds, root.bounds);
|
|
748
854
|
return {
|
|
749
855
|
id: nodeIdForPath(node.path),
|
|
750
856
|
role: node.role,
|
|
@@ -752,18 +858,194 @@ function toFieldModel(node, includeBounds = true) {
|
|
|
752
858
|
...(value ? { value } : {}),
|
|
753
859
|
...(cloneState(node.state) ? { state: cloneState(node.state) } : {}),
|
|
754
860
|
...(cloneValidation(node.validation) ? { validation: cloneValidation(node.validation) } : {}),
|
|
861
|
+
...(context ? { context } : {}),
|
|
862
|
+
visibility,
|
|
863
|
+
scrollHint,
|
|
755
864
|
...(includeBounds ? { bounds: cloneBounds(node.bounds) } : {}),
|
|
756
865
|
};
|
|
757
866
|
}
|
|
758
|
-
function toActionModel(node, includeBounds = true) {
|
|
867
|
+
function toActionModel(root, node, includeBounds = true) {
|
|
868
|
+
const context = nodeContext(root, node);
|
|
869
|
+
const visibility = buildVisibility(node.bounds, root.bounds);
|
|
870
|
+
const scrollHint = buildScrollHint(node.bounds, root.bounds);
|
|
759
871
|
return {
|
|
760
872
|
id: nodeIdForPath(node.path),
|
|
761
873
|
role: node.role,
|
|
762
874
|
...(sanitizeInlineName(node.name, 80) ? { name: sanitizeInlineName(node.name, 80) } : {}),
|
|
763
875
|
...(cloneState(node.state) ? { state: cloneState(node.state) } : {}),
|
|
876
|
+
...(context ? { context } : {}),
|
|
877
|
+
visibility,
|
|
878
|
+
scrollHint,
|
|
764
879
|
...(includeBounds ? { bounds: cloneBounds(node.bounds) } : {}),
|
|
765
880
|
};
|
|
766
881
|
}
|
|
882
|
+
function compactSchemaContext(context, label) {
|
|
883
|
+
if (!context)
|
|
884
|
+
return undefined;
|
|
885
|
+
const out = {};
|
|
886
|
+
if (context.prompt && normalizeUiText(context.prompt) !== normalizeUiText(label))
|
|
887
|
+
out.prompt = context.prompt;
|
|
888
|
+
if (context.section)
|
|
889
|
+
out.section = context.section;
|
|
890
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
891
|
+
}
|
|
892
|
+
function compactSchemaValue(value, inlineLimit = 80) {
|
|
893
|
+
const normalized = sanitizeInlineName(value, Math.max(120, inlineLimit + 32));
|
|
894
|
+
if (!normalized)
|
|
895
|
+
return {};
|
|
896
|
+
return normalized.length <= inlineLimit
|
|
897
|
+
? { value: normalized }
|
|
898
|
+
: { valueLength: normalized.length };
|
|
899
|
+
}
|
|
900
|
+
function schemaOptionLabel(node) {
|
|
901
|
+
return sanitizeFieldName(node.name, 80) ?? sanitizeInlineName(node.name, 80);
|
|
902
|
+
}
|
|
903
|
+
function isGroupedChoiceControl(node) {
|
|
904
|
+
return node.role === 'radio' || node.role === 'checkbox' || (node.role === 'button' && node.focusable);
|
|
905
|
+
}
|
|
906
|
+
function groupedChoiceForNode(root, formNode, seed) {
|
|
907
|
+
const context = nodeContext(root, seed);
|
|
908
|
+
const prompt = context?.prompt;
|
|
909
|
+
if (!prompt)
|
|
910
|
+
return null;
|
|
911
|
+
const matchesPrompt = (candidate) => {
|
|
912
|
+
if (!isGroupedChoiceControl(candidate))
|
|
913
|
+
return false;
|
|
914
|
+
return nodeContext(root, candidate)?.prompt === prompt;
|
|
915
|
+
};
|
|
916
|
+
const ancestors = ancestorNodes(root, seed.path);
|
|
917
|
+
for (let index = ancestors.length - 1; index >= 0; index--) {
|
|
918
|
+
const ancestor = ancestors[index];
|
|
919
|
+
if (ancestor.role === 'form')
|
|
920
|
+
continue;
|
|
921
|
+
const controls = sortByBounds(collectDescendants(ancestor, matchesPrompt));
|
|
922
|
+
if (controls.length >= 2) {
|
|
923
|
+
return { container: ancestor, prompt, controls };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (seed.role !== 'radio' && seed.role !== 'button')
|
|
927
|
+
return null;
|
|
928
|
+
const controls = sortByBounds(collectDescendants(formNode, matchesPrompt));
|
|
929
|
+
return controls.length >= 2 ? { container: formNode, prompt, controls } : null;
|
|
930
|
+
}
|
|
931
|
+
function simpleSchemaField(root, node) {
|
|
932
|
+
const context = nodeContext(root, node);
|
|
933
|
+
const label = fieldLabel(node) ?? sanitizeInlineName(node.name, 80) ?? context?.prompt;
|
|
934
|
+
if (!label)
|
|
935
|
+
return null;
|
|
936
|
+
return {
|
|
937
|
+
id: formFieldIdForPath(node.path),
|
|
938
|
+
kind: node.role === 'combobox' ? 'choice' : 'text',
|
|
939
|
+
label,
|
|
940
|
+
...(node.state?.required ? { required: true } : {}),
|
|
941
|
+
...(node.state?.invalid ? { invalid: true } : {}),
|
|
942
|
+
...compactSchemaValue(node.value, 72),
|
|
943
|
+
...(compactSchemaContext(context, label) ? { context: compactSchemaContext(context, label) } : {}),
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
function groupedSchemaField(root, grouped) {
|
|
947
|
+
const optionEntries = grouped.controls
|
|
948
|
+
.map(control => ({
|
|
949
|
+
label: schemaOptionLabel(control),
|
|
950
|
+
selected: control.state?.checked === true || control.state?.selected === true,
|
|
951
|
+
role: control.role,
|
|
952
|
+
}))
|
|
953
|
+
.filter((entry) => !!entry.label);
|
|
954
|
+
if (optionEntries.length < 2)
|
|
955
|
+
return null;
|
|
956
|
+
const options = dedupeStrings(optionEntries.map(entry => entry.label), 16);
|
|
957
|
+
const selectedOptions = dedupeStrings(optionEntries.filter(entry => entry.selected).map(entry => entry.label), 16);
|
|
958
|
+
const radioLike = optionEntries.every(entry => entry.role === 'radio' || entry.role === 'button');
|
|
959
|
+
const context = nodeContext(root, grouped.controls[0]);
|
|
960
|
+
return {
|
|
961
|
+
id: formFieldIdForPath(grouped.container.path),
|
|
962
|
+
kind: radioLike ? 'choice' : 'multi_choice',
|
|
963
|
+
label: grouped.prompt,
|
|
964
|
+
...(grouped.controls.some(control => control.state?.required) ? { required: true } : {}),
|
|
965
|
+
...(grouped.controls.some(control => control.state?.invalid) ? { invalid: true } : {}),
|
|
966
|
+
...(radioLike
|
|
967
|
+
? {
|
|
968
|
+
...(selectedOptions[0] ? { value: selectedOptions[0] } : {}),
|
|
969
|
+
}
|
|
970
|
+
: {
|
|
971
|
+
...(selectedOptions.length > 0 ? { values: selectedOptions } : {}),
|
|
972
|
+
}),
|
|
973
|
+
optionCount: options.length,
|
|
974
|
+
options,
|
|
975
|
+
...(compactSchemaContext(context, grouped.prompt) ? { context: compactSchemaContext(context, grouped.prompt) } : {}),
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function toggleSchemaField(root, node) {
|
|
979
|
+
const label = schemaOptionLabel(node);
|
|
980
|
+
if (!label)
|
|
981
|
+
return null;
|
|
982
|
+
const context = nodeContext(root, node);
|
|
983
|
+
const controlType = node.role === 'radio' ? 'radio' : 'checkbox';
|
|
984
|
+
return {
|
|
985
|
+
id: formFieldIdForPath(node.path),
|
|
986
|
+
kind: 'toggle',
|
|
987
|
+
label,
|
|
988
|
+
controlType,
|
|
989
|
+
...(node.state?.required ? { required: true } : {}),
|
|
990
|
+
...(node.state?.invalid ? { invalid: true } : {}),
|
|
991
|
+
...(node.state?.checked !== undefined ? { checked: node.state.checked === true } : {}),
|
|
992
|
+
...(compactSchemaContext(context, label) ? { context: compactSchemaContext(context, label) } : {}),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
function buildFormSchemaForNode(root, formNode, options) {
|
|
996
|
+
const candidates = sortByBounds(collectDescendants(formNode, candidate => candidate.role === 'textbox' ||
|
|
997
|
+
candidate.role === 'combobox' ||
|
|
998
|
+
candidate.role === 'checkbox' ||
|
|
999
|
+
candidate.role === 'radio' ||
|
|
1000
|
+
(candidate.role === 'button' && candidate.focusable)));
|
|
1001
|
+
const consumed = new Set();
|
|
1002
|
+
const fields = [];
|
|
1003
|
+
for (const candidate of candidates) {
|
|
1004
|
+
const candidateKey = pathKey(candidate.path);
|
|
1005
|
+
if (consumed.has(candidateKey))
|
|
1006
|
+
continue;
|
|
1007
|
+
if (candidate.role === 'textbox' || candidate.role === 'combobox') {
|
|
1008
|
+
const field = simpleSchemaField(root, candidate);
|
|
1009
|
+
if (field)
|
|
1010
|
+
fields.push(field);
|
|
1011
|
+
consumed.add(candidateKey);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const grouped = groupedChoiceForNode(root, formNode, candidate);
|
|
1015
|
+
if (grouped && grouped.controls.some(control => pathKey(control.path) === candidateKey)) {
|
|
1016
|
+
const field = groupedSchemaField(root, grouped);
|
|
1017
|
+
for (const control of grouped.controls)
|
|
1018
|
+
consumed.add(pathKey(control.path));
|
|
1019
|
+
if (field)
|
|
1020
|
+
fields.push(field);
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
if (candidate.role === 'checkbox' || candidate.role === 'radio') {
|
|
1024
|
+
const field = toggleSchemaField(root, candidate);
|
|
1025
|
+
if (field)
|
|
1026
|
+
fields.push(field);
|
|
1027
|
+
consumed.add(candidateKey);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const filteredFields = fields.filter(field => {
|
|
1031
|
+
if (options?.onlyRequiredFields && !field.required)
|
|
1032
|
+
return false;
|
|
1033
|
+
if (options?.onlyInvalidFields && !field.invalid)
|
|
1034
|
+
return false;
|
|
1035
|
+
return true;
|
|
1036
|
+
});
|
|
1037
|
+
const maxFields = options?.maxFields ?? filteredFields.length;
|
|
1038
|
+
const pageFields = filteredFields.slice(0, maxFields);
|
|
1039
|
+
const name = sectionDisplayName(formNode, 'form');
|
|
1040
|
+
return {
|
|
1041
|
+
formId: sectionIdForPath('form', formNode.path),
|
|
1042
|
+
...(name ? { name } : {}),
|
|
1043
|
+
fieldCount: fields.length,
|
|
1044
|
+
requiredCount: fields.filter(field => field.required).length,
|
|
1045
|
+
invalidCount: fields.filter(field => field.invalid).length,
|
|
1046
|
+
fields: pageFields,
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
767
1049
|
function toLandmarkModel(node) {
|
|
768
1050
|
const name = sectionDisplayName(node, 'landmark');
|
|
769
1051
|
return {
|
|
@@ -887,6 +1169,15 @@ export function buildPageModel(root, options) {
|
|
|
887
1169
|
archetypes: inferPageArchetypes(baseModel),
|
|
888
1170
|
};
|
|
889
1171
|
}
|
|
1172
|
+
export function buildFormSchemas(root, options) {
|
|
1173
|
+
const forms = sortByBounds([
|
|
1174
|
+
...(root.role === 'form' ? [root] : []),
|
|
1175
|
+
...collectDescendants(root, candidate => candidate.role === 'form'),
|
|
1176
|
+
]);
|
|
1177
|
+
return forms
|
|
1178
|
+
.filter(form => !options?.formId || sectionIdForPath('form', form.path) === options.formId)
|
|
1179
|
+
.map(form => buildFormSchemaForNode(root, form, options));
|
|
1180
|
+
}
|
|
890
1181
|
function headingModels(node, maxHeadings, includeBounds) {
|
|
891
1182
|
const headings = sortByBounds(collectDescendants(node, candidate => candidate.role === 'heading' && !!sanitizeInlineName(candidate.name, 80)));
|
|
892
1183
|
return headings.slice(0, maxHeadings).map(heading => ({
|
|
@@ -931,9 +1222,15 @@ export function expandPageSection(root, id, options) {
|
|
|
931
1222
|
return null;
|
|
932
1223
|
const maxHeadings = options?.maxHeadings ?? 6;
|
|
933
1224
|
const maxFields = options?.maxFields ?? 18;
|
|
1225
|
+
const fieldOffset = Math.max(0, options?.fieldOffset ?? 0);
|
|
1226
|
+
const onlyRequiredFields = options?.onlyRequiredFields ?? false;
|
|
1227
|
+
const onlyInvalidFields = options?.onlyInvalidFields ?? false;
|
|
934
1228
|
const maxActions = options?.maxActions ?? 12;
|
|
1229
|
+
const actionOffset = Math.max(0, options?.actionOffset ?? 0);
|
|
935
1230
|
const maxLists = options?.maxLists ?? 8;
|
|
1231
|
+
const listOffset = Math.max(0, options?.listOffset ?? 0);
|
|
936
1232
|
const maxItems = options?.maxItems ?? 20;
|
|
1233
|
+
const itemOffset = Math.max(0, options?.itemOffset ?? 0);
|
|
937
1234
|
const maxTextPreview = options?.maxTextPreview ?? 6;
|
|
938
1235
|
const includeBounds = options?.includeBounds ?? false;
|
|
939
1236
|
const headingsAll = sortByBounds(collectDescendants(node, candidate => candidate.role === 'heading' && !!sanitizeInlineName(candidate.name, 80)));
|
|
@@ -943,6 +1240,19 @@ export function expandPageSection(root, id, options) {
|
|
|
943
1240
|
const itemsAll = actualKind === 'list'
|
|
944
1241
|
? sortByBounds(collectDescendants(node, candidate => candidate.role === 'listitem'))
|
|
945
1242
|
: [];
|
|
1243
|
+
const requiredFieldCount = fieldsAll.filter(field => field.state?.required).length;
|
|
1244
|
+
const invalidFieldCount = fieldsAll.filter(field => field.state?.invalid).length;
|
|
1245
|
+
const filteredFields = fieldsAll.filter(field => {
|
|
1246
|
+
if (onlyRequiredFields && !field.state?.required)
|
|
1247
|
+
return false;
|
|
1248
|
+
if (onlyInvalidFields && !field.state?.invalid)
|
|
1249
|
+
return false;
|
|
1250
|
+
return true;
|
|
1251
|
+
});
|
|
1252
|
+
const pageFields = filteredFields.slice(fieldOffset, fieldOffset + maxFields);
|
|
1253
|
+
const pageActions = actionsAll.slice(actionOffset, actionOffset + maxActions);
|
|
1254
|
+
const pageLists = nestedListsAll.slice(listOffset, listOffset + maxLists);
|
|
1255
|
+
const pageItems = itemsAll.slice(itemOffset, itemOffset + maxItems);
|
|
946
1256
|
const name = sectionDisplayName(node, actualKind);
|
|
947
1257
|
return {
|
|
948
1258
|
id: sectionIdForPath(actualKind, node.path),
|
|
@@ -953,15 +1263,49 @@ export function expandPageSection(root, id, options) {
|
|
|
953
1263
|
summary: {
|
|
954
1264
|
headingCount: headingsAll.length,
|
|
955
1265
|
fieldCount: fieldsAll.length,
|
|
1266
|
+
requiredFieldCount,
|
|
1267
|
+
invalidFieldCount,
|
|
956
1268
|
actionCount: actionsAll.length,
|
|
957
1269
|
listCount: nestedListsAll.length,
|
|
958
1270
|
itemCount: itemsAll.length,
|
|
959
1271
|
},
|
|
1272
|
+
page: {
|
|
1273
|
+
fields: {
|
|
1274
|
+
offset: fieldOffset,
|
|
1275
|
+
returned: pageFields.length,
|
|
1276
|
+
total: filteredFields.length,
|
|
1277
|
+
hasMore: fieldOffset + pageFields.length < filteredFields.length,
|
|
1278
|
+
},
|
|
1279
|
+
actions: {
|
|
1280
|
+
offset: actionOffset,
|
|
1281
|
+
returned: pageActions.length,
|
|
1282
|
+
total: actionsAll.length,
|
|
1283
|
+
hasMore: actionOffset + pageActions.length < actionsAll.length,
|
|
1284
|
+
},
|
|
1285
|
+
lists: {
|
|
1286
|
+
offset: listOffset,
|
|
1287
|
+
returned: pageLists.length,
|
|
1288
|
+
total: nestedListsAll.length,
|
|
1289
|
+
hasMore: listOffset + pageLists.length < nestedListsAll.length,
|
|
1290
|
+
},
|
|
1291
|
+
items: {
|
|
1292
|
+
offset: itemOffset,
|
|
1293
|
+
returned: pageItems.length,
|
|
1294
|
+
total: itemsAll.length,
|
|
1295
|
+
hasMore: itemOffset + pageItems.length < itemsAll.length,
|
|
1296
|
+
},
|
|
1297
|
+
},
|
|
960
1298
|
headings: headingModels(node, maxHeadings, includeBounds),
|
|
961
|
-
fields:
|
|
962
|
-
actions:
|
|
963
|
-
lists:
|
|
964
|
-
|
|
1299
|
+
fields: pageFields.map(field => toFieldModel(root, field, includeBounds)),
|
|
1300
|
+
actions: pageActions.map(action => toActionModel(root, action, includeBounds)),
|
|
1301
|
+
lists: pageLists.map(list => ({
|
|
1302
|
+
id: sectionIdForPath('list', list.path),
|
|
1303
|
+
role: list.role,
|
|
1304
|
+
...(sectionDisplayName(list, 'list') ? { name: sectionDisplayName(list, 'list') } : {}),
|
|
1305
|
+
bounds: cloneBounds(list.bounds),
|
|
1306
|
+
itemCount: collectDescendants(list, candidate => candidate.role === 'listitem').length,
|
|
1307
|
+
})),
|
|
1308
|
+
items: pageItems.map(item => ({
|
|
965
1309
|
id: nodeIdForPath(item.path),
|
|
966
1310
|
...(listItemName(item) ? { name: listItemName(item) } : {}),
|
|
967
1311
|
...(includeBounds ? { bounds: cloneBounds(item.bounds) } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geometra/mcp",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.13",
|
|
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.13",
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
35
35
|
"ws": "^8.18.0",
|
|
36
36
|
"zod": "^3.23.0"
|