@geometra/mcp 1.19.10 → 1.19.12

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[];
@@ -368,9 +419,15 @@ export declare function buildPageModel(root: A11yNode, options?: {
368
419
  export declare function expandPageSection(root: A11yNode, id: string, options?: {
369
420
  maxHeadings?: number;
370
421
  maxFields?: number;
422
+ fieldOffset?: number;
423
+ onlyRequiredFields?: boolean;
424
+ onlyInvalidFields?: boolean;
371
425
  maxActions?: number;
426
+ actionOffset?: number;
372
427
  maxLists?: number;
428
+ listOffset?: number;
373
429
  maxItems?: number;
430
+ itemOffset?: number;
374
431
  maxTextPreview?: number;
375
432
  includeBounds?: boolean;
376
433
  }): PageSectionDetail | null;
package/dist/session.js CHANGED
@@ -743,8 +743,103 @@ function primaryAction(node) {
743
743
  bounds: cloneBounds(node.bounds),
744
744
  };
745
745
  }
746
- function toFieldModel(node, includeBounds = true) {
746
+ function buildVisibility(bounds, viewport) {
747
+ const visibleLeft = Math.max(0, bounds.x);
748
+ const visibleTop = Math.max(0, bounds.y);
749
+ const visibleRight = Math.min(viewport.width, bounds.x + bounds.width);
750
+ const visibleBottom = Math.min(viewport.height, bounds.y + bounds.height);
751
+ const hasVisibleIntersection = visibleRight > visibleLeft && visibleBottom > visibleTop;
752
+ const fullyVisible = bounds.x >= 0 &&
753
+ bounds.y >= 0 &&
754
+ bounds.x + bounds.width <= viewport.width &&
755
+ bounds.y + bounds.height <= viewport.height;
756
+ return {
757
+ intersectsViewport: hasVisibleIntersection,
758
+ fullyVisible,
759
+ offscreenAbove: bounds.y + bounds.height <= 0,
760
+ offscreenBelow: bounds.y >= viewport.height,
761
+ offscreenLeft: bounds.x + bounds.width <= 0,
762
+ offscreenRight: bounds.x >= viewport.width,
763
+ };
764
+ }
765
+ function buildScrollHint(bounds, viewport) {
766
+ const visibility = buildVisibility(bounds, viewport);
767
+ return {
768
+ status: visibility.fullyVisible ? 'visible' : visibility.intersectsViewport ? 'partial' : 'offscreen',
769
+ revealDeltaX: Math.round(bounds.x + bounds.width / 2 - viewport.width / 2),
770
+ revealDeltaY: Math.round(bounds.y + bounds.height / 2 - viewport.height / 2),
771
+ };
772
+ }
773
+ function ancestorNodes(root, path) {
774
+ const out = [];
775
+ let current = root;
776
+ for (const index of path) {
777
+ out.push(current);
778
+ if (!current.children[index])
779
+ break;
780
+ current = current.children[index];
781
+ }
782
+ return out;
783
+ }
784
+ function countGroupedChoiceControls(node) {
785
+ return collectDescendants(node, candidate => candidate.role === 'radio' || candidate.role === 'checkbox' || candidate.role === 'button').length;
786
+ }
787
+ function nearestPromptText(container, target) {
788
+ const candidates = collectDescendants(container, candidate => (candidate.role === 'heading' || candidate.role === 'text') &&
789
+ !!sanitizeInlineName(candidate.name, 120) &&
790
+ pathKey(candidate.path) !== pathKey(target.path));
791
+ const normalizedTarget = normalizeUiText(target.name ?? '');
792
+ const best = candidates
793
+ .filter(candidate => candidate.bounds.y <= target.bounds.y + 8)
794
+ .map(candidate => {
795
+ const text = sanitizeInlineName(candidate.name, 120);
796
+ if (!text)
797
+ return null;
798
+ if (normalizeUiText(text) === normalizedTarget)
799
+ return null;
800
+ const dy = Math.max(0, target.bounds.y - candidate.bounds.y);
801
+ const dx = Math.abs(target.bounds.x - candidate.bounds.x);
802
+ const headingBonus = candidate.role === 'heading' ? -32 : 0;
803
+ return { text, score: dy * 4 + dx + headingBonus };
804
+ })
805
+ .filter((candidate) => !!candidate)
806
+ .sort((a, b) => a.score - b.score)[0];
807
+ return best?.text;
808
+ }
809
+ function nodeContext(root, node) {
810
+ const ancestors = ancestorNodes(root, node.path);
811
+ let prompt;
812
+ for (let index = ancestors.length - 1; index >= 0; index--) {
813
+ const ancestor = ancestors[index];
814
+ const grouped = countGroupedChoiceControls(ancestor) >= 2;
815
+ if (grouped || ancestor.role === 'group' || ancestor.role === 'form' || ancestor.role === 'dialog') {
816
+ prompt = nearestPromptText(ancestor, node);
817
+ if (prompt)
818
+ break;
819
+ }
820
+ }
821
+ let section;
822
+ for (let index = ancestors.length - 1; index >= 0; index--) {
823
+ const ancestor = ancestors[index];
824
+ const kind = sectionKindForNode(ancestor);
825
+ if (!kind)
826
+ continue;
827
+ section = sectionDisplayName(ancestor, kind);
828
+ if (section)
829
+ break;
830
+ }
831
+ if (!prompt && !section)
832
+ return undefined;
833
+ return {
834
+ ...(prompt ? { prompt } : {}),
835
+ ...(section ? { section } : {}),
836
+ };
837
+ }
838
+ function toFieldModel(root, node, includeBounds = true) {
747
839
  const value = sanitizeInlineName(node.value, 120);
840
+ const context = nodeContext(root, node);
841
+ const visibility = buildVisibility(node.bounds, root.bounds);
842
+ const scrollHint = buildScrollHint(node.bounds, root.bounds);
748
843
  return {
749
844
  id: nodeIdForPath(node.path),
750
845
  role: node.role,
@@ -752,15 +847,24 @@ function toFieldModel(node, includeBounds = true) {
752
847
  ...(value ? { value } : {}),
753
848
  ...(cloneState(node.state) ? { state: cloneState(node.state) } : {}),
754
849
  ...(cloneValidation(node.validation) ? { validation: cloneValidation(node.validation) } : {}),
850
+ ...(context ? { context } : {}),
851
+ visibility,
852
+ scrollHint,
755
853
  ...(includeBounds ? { bounds: cloneBounds(node.bounds) } : {}),
756
854
  };
757
855
  }
758
- function toActionModel(node, includeBounds = true) {
856
+ function toActionModel(root, node, includeBounds = true) {
857
+ const context = nodeContext(root, node);
858
+ const visibility = buildVisibility(node.bounds, root.bounds);
859
+ const scrollHint = buildScrollHint(node.bounds, root.bounds);
759
860
  return {
760
861
  id: nodeIdForPath(node.path),
761
862
  role: node.role,
762
863
  ...(sanitizeInlineName(node.name, 80) ? { name: sanitizeInlineName(node.name, 80) } : {}),
763
864
  ...(cloneState(node.state) ? { state: cloneState(node.state) } : {}),
865
+ ...(context ? { context } : {}),
866
+ visibility,
867
+ scrollHint,
764
868
  ...(includeBounds ? { bounds: cloneBounds(node.bounds) } : {}),
765
869
  };
766
870
  }
@@ -931,9 +1035,15 @@ export function expandPageSection(root, id, options) {
931
1035
  return null;
932
1036
  const maxHeadings = options?.maxHeadings ?? 6;
933
1037
  const maxFields = options?.maxFields ?? 18;
1038
+ const fieldOffset = Math.max(0, options?.fieldOffset ?? 0);
1039
+ const onlyRequiredFields = options?.onlyRequiredFields ?? false;
1040
+ const onlyInvalidFields = options?.onlyInvalidFields ?? false;
934
1041
  const maxActions = options?.maxActions ?? 12;
1042
+ const actionOffset = Math.max(0, options?.actionOffset ?? 0);
935
1043
  const maxLists = options?.maxLists ?? 8;
1044
+ const listOffset = Math.max(0, options?.listOffset ?? 0);
936
1045
  const maxItems = options?.maxItems ?? 20;
1046
+ const itemOffset = Math.max(0, options?.itemOffset ?? 0);
937
1047
  const maxTextPreview = options?.maxTextPreview ?? 6;
938
1048
  const includeBounds = options?.includeBounds ?? false;
939
1049
  const headingsAll = sortByBounds(collectDescendants(node, candidate => candidate.role === 'heading' && !!sanitizeInlineName(candidate.name, 80)));
@@ -943,6 +1053,19 @@ export function expandPageSection(root, id, options) {
943
1053
  const itemsAll = actualKind === 'list'
944
1054
  ? sortByBounds(collectDescendants(node, candidate => candidate.role === 'listitem'))
945
1055
  : [];
1056
+ const requiredFieldCount = fieldsAll.filter(field => field.state?.required).length;
1057
+ const invalidFieldCount = fieldsAll.filter(field => field.state?.invalid).length;
1058
+ const filteredFields = fieldsAll.filter(field => {
1059
+ if (onlyRequiredFields && !field.state?.required)
1060
+ return false;
1061
+ if (onlyInvalidFields && !field.state?.invalid)
1062
+ return false;
1063
+ return true;
1064
+ });
1065
+ const pageFields = filteredFields.slice(fieldOffset, fieldOffset + maxFields);
1066
+ const pageActions = actionsAll.slice(actionOffset, actionOffset + maxActions);
1067
+ const pageLists = nestedListsAll.slice(listOffset, listOffset + maxLists);
1068
+ const pageItems = itemsAll.slice(itemOffset, itemOffset + maxItems);
946
1069
  const name = sectionDisplayName(node, actualKind);
947
1070
  return {
948
1071
  id: sectionIdForPath(actualKind, node.path),
@@ -953,15 +1076,49 @@ export function expandPageSection(root, id, options) {
953
1076
  summary: {
954
1077
  headingCount: headingsAll.length,
955
1078
  fieldCount: fieldsAll.length,
1079
+ requiredFieldCount,
1080
+ invalidFieldCount,
956
1081
  actionCount: actionsAll.length,
957
1082
  listCount: nestedListsAll.length,
958
1083
  itemCount: itemsAll.length,
959
1084
  },
1085
+ page: {
1086
+ fields: {
1087
+ offset: fieldOffset,
1088
+ returned: pageFields.length,
1089
+ total: filteredFields.length,
1090
+ hasMore: fieldOffset + pageFields.length < filteredFields.length,
1091
+ },
1092
+ actions: {
1093
+ offset: actionOffset,
1094
+ returned: pageActions.length,
1095
+ total: actionsAll.length,
1096
+ hasMore: actionOffset + pageActions.length < actionsAll.length,
1097
+ },
1098
+ lists: {
1099
+ offset: listOffset,
1100
+ returned: pageLists.length,
1101
+ total: nestedListsAll.length,
1102
+ hasMore: listOffset + pageLists.length < nestedListsAll.length,
1103
+ },
1104
+ items: {
1105
+ offset: itemOffset,
1106
+ returned: pageItems.length,
1107
+ total: itemsAll.length,
1108
+ hasMore: itemOffset + pageItems.length < itemsAll.length,
1109
+ },
1110
+ },
960
1111
  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 => ({
1112
+ fields: pageFields.map(field => toFieldModel(root, field, includeBounds)),
1113
+ actions: pageActions.map(action => toActionModel(root, action, includeBounds)),
1114
+ lists: pageLists.map(list => ({
1115
+ id: sectionIdForPath('list', list.path),
1116
+ role: list.role,
1117
+ ...(sectionDisplayName(list, 'list') ? { name: sectionDisplayName(list, 'list') } : {}),
1118
+ bounds: cloneBounds(list.bounds),
1119
+ itemCount: collectDescendants(list, candidate => candidate.role === 'listitem').length,
1120
+ })),
1121
+ items: pageItems.map(item => ({
965
1122
  id: nodeIdForPath(item.path),
966
1123
  ...(listItemName(item) ? { name: listItemName(item) } : {}),
967
1124
  ...(includeBounds ? { bounds: cloneBounds(item.bounds) } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.19.10",
3
+ "version": "1.19.12",
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.10",
33
+ "@geometra/proxy": "^1.19.12",
34
34
  "@modelcontextprotocol/sdk": "^1.12.1",
35
35
  "ws": "^8.18.0",
36
36
  "zod": "^3.23.0"