@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/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 toFieldModel(node, includeBounds = true) {
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: fieldsAll.slice(0, maxFields).map(field => toFieldModel(field, includeBounds)),
962
- actions: actionsAll.slice(0, maxActions).map(action => toActionModel(action, includeBounds)),
963
- lists: nestedListSummaries(node, maxLists, node.path),
964
- items: itemsAll.slice(0, maxItems).map(item => ({
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.11",
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.11",
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"