@geometra/mcp 1.25.0 → 1.27.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/index.js CHANGED
@@ -8,7 +8,7 @@ function cleanupActiveSession() {
8
8
  return;
9
9
  cleanedUp = true;
10
10
  try {
11
- disconnect();
11
+ disconnect({ closeProxy: true });
12
12
  }
13
13
  catch {
14
14
  /* ignore */
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@ import { performance } from 'node:perf_hooks';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { z } from 'zod';
5
5
  import { formatConnectFailureMessage, isHttpUrl, normalizeConnectTarget } from './connect-utils.js';
6
- import { connect, connectThroughProxy, disconnect, getSession, listSessions, getDefaultSessionId, prewarmProxy, sendClick, sendFillFields, sendType, sendKey, sendFileUpload, sendFieldText, sendFieldChoice, sendListboxPick, sendSelectOption, sendSetChecked, sendWheel, sendScreenshot, sendPdfGenerate, buildA11yTree, buildCompactUiIndex, buildFormRequiredSnapshot, buildPageModel, buildFormSchemas, expandPageSection, buildUiDelta, hasUiDelta, nodeIdForPath, nodeContextForNode, summarizeCompactIndex, summarizePageModel, summarizeUiDelta, waitForUiCondition, } from './session.js';
6
+ import { connect, connectThroughProxy, disconnect, getSession, listSessions, getDefaultSessionId, prewarmProxy, sendClick, sendFillFields, sendType, sendKey, sendFileUpload, sendFieldText, sendFieldChoice, sendListboxPick, sendSelectOption, sendSetChecked, sendWheel, sendScreenshot, sendPdfGenerate, buildA11yTree, buildCompactUiIndex, buildFormRequiredSnapshot, buildPageModel, buildFormSchemas, expandPageSection, buildUiDelta, hasUiDelta, nodeIdForPath, nodeContextForNode, parseSectionId, findNodeByPath, summarizeCompactIndex, summarizePageModel, summarizeUiDelta, waitForUiCondition, } from './session.js';
7
7
  function checkedStateInput() {
8
8
  return z
9
9
  .union([z.boolean(), z.literal('mixed')])
@@ -1755,7 +1755,7 @@ Use this for dropdowns, location pickers, or any scrollable list where items are
1755
1755
  maxScrollSteps: z.number().int().min(1).max(50).optional().default(20).describe('Max scroll steps before stopping (default 20)'),
1756
1756
  scrollDelta: z.number().optional().default(300).describe('Vertical scroll delta per step (default 300)'),
1757
1757
  sessionId: sessionIdInput,
1758
- }, async ({ listId: _listId, role, scrollX, scrollY, maxItems, maxScrollSteps, scrollDelta, sessionId }) => {
1758
+ }, async ({ listId, role, scrollX, scrollY, maxItems, maxScrollSteps, scrollDelta, sessionId }) => {
1759
1759
  const session = getSession(sessionId);
1760
1760
  if (!session)
1761
1761
  return err('Not connected. Call geometra_connect first.');
@@ -1767,7 +1767,17 @@ Use this for dropdowns, location pickers, or any scrollable list where items are
1767
1767
  const a11y = await sessionA11yWhenReady(session);
1768
1768
  if (!a11y)
1769
1769
  break;
1770
- const items = findNodes(a11y, { role: itemRole });
1770
+ // Scope to subtree if listId is provided
1771
+ let searchRoot = a11y;
1772
+ if (listId) {
1773
+ const parsed = parseSectionId(listId);
1774
+ if (parsed) {
1775
+ const node = findNodeByPath(a11y, parsed.path);
1776
+ if (node)
1777
+ searchRoot = node;
1778
+ }
1779
+ }
1780
+ const items = findNodes(searchRoot, { role: itemRole });
1771
1781
  let newCount = 0;
1772
1782
  for (const item of items) {
1773
1783
  const id = nodeIdForPath(item.path);
package/dist/session.d.ts CHANGED
@@ -594,6 +594,10 @@ export declare function sendNavigate(session: Session, url: string, timeoutMs?:
594
594
  */
595
595
  export declare function buildA11yTree(tree: Record<string, unknown>, layout: Record<string, unknown>): A11yNode;
596
596
  export declare function nodeIdForPath(path: number[]): string;
597
+ export declare function parseSectionId(id: string): {
598
+ kind: PageSectionKind;
599
+ path: number[];
600
+ } | null;
597
601
  /**
598
602
  * Flat list of actionable / semantic nodes in the viewport, sorted with focusable first
599
603
  * then top-to-bottom reading order. Intended to minimize LLM tokens vs a full nested tree.
@@ -608,6 +612,7 @@ export declare function buildCompactUiIndex(root: A11yNode, options?: {
608
612
  context: CompactUiContext;
609
613
  };
610
614
  export declare function summarizeCompactIndex(nodes: CompactUiNode[], maxLines?: number): string;
615
+ export declare function findNodeByPath(root: A11yNode, path: number[]): A11yNode | null;
611
616
  export declare function nodeContextForNode(root: A11yNode, node: A11yNode): NodeContextModel | undefined;
612
617
  export declare function buildPageModel(root: A11yNode, options?: {
613
618
  maxPrimaryActions?: number;
package/dist/session.js CHANGED
@@ -8,6 +8,9 @@ let nextSessionId = 0;
8
8
  function generateSessionId() { return `s${++nextSessionId}`; }
9
9
  let reusableProxies = [];
10
10
  const REUSABLE_PROXY_POOL_LIMIT = 6;
11
+ /** Close idle reusable proxies after 5 minutes of inactivity. */
12
+ const REUSABLE_PROXY_IDLE_TTL_MS = 5 * 60 * 1000;
13
+ let idleProxyTimer = null;
11
14
  const trackedReusableProxyChildren = new WeakSet();
12
15
  const ACTION_UPDATE_TIMEOUT_MS = 2000;
13
16
  const LISTBOX_UPDATE_TIMEOUT_MS = 4500;
@@ -66,9 +69,11 @@ function closeReusableProxy(entry) {
66
69
  catch {
67
70
  /* ignore */
68
71
  }
72
+ ensureIdleProxyTimer();
69
73
  return;
70
74
  }
71
75
  void entry.runtime?.close().catch(() => { });
76
+ ensureIdleProxyTimer();
72
77
  }
73
78
  function closeReusableProxies() {
74
79
  clearReusableProxiesIfExited();
@@ -87,6 +92,25 @@ function closeReusableProxies() {
87
92
  void entry.runtime?.close().catch(() => { });
88
93
  }
89
94
  }
95
+ function evictIdleReusableProxies() {
96
+ clearReusableProxiesIfExited();
97
+ const now = Date.now();
98
+ const stale = reusableProxies.filter(entry => !reusableProxyEntryIsActive(entry) && (now - entry.lastUsedAt) > REUSABLE_PROXY_IDLE_TTL_MS);
99
+ for (const entry of stale) {
100
+ closeReusableProxy(entry);
101
+ }
102
+ ensureIdleProxyTimer();
103
+ }
104
+ function ensureIdleProxyTimer() {
105
+ if (reusableProxies.length > 0 && !idleProxyTimer) {
106
+ idleProxyTimer = setInterval(evictIdleReusableProxies, 60_000);
107
+ idleProxyTimer.unref();
108
+ }
109
+ else if (reusableProxies.length === 0 && idleProxyTimer) {
110
+ clearInterval(idleProxyTimer);
111
+ idleProxyTimer = null;
112
+ }
113
+ }
90
114
  function enforceReusableProxyPoolLimit() {
91
115
  clearReusableProxiesIfExited();
92
116
  if (reusableProxies.length <= REUSABLE_PROXY_POOL_LIMIT)
@@ -139,6 +163,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
139
163
  child.once('error', clear);
140
164
  }
141
165
  enforceReusableProxyPoolLimit();
166
+ ensureIdleProxyTimer();
142
167
  return;
143
168
  }
144
169
  reusableProxies.push({
@@ -153,6 +178,7 @@ function setReusableProxy(proxy, wsUrl, opts) {
153
178
  lastUsedAt: now,
154
179
  });
155
180
  enforceReusableProxyPoolLimit();
181
+ ensureIdleProxyTimer();
156
182
  }
157
183
  function rememberReusableProxyPageUrl(session) {
158
184
  const entry = reusableProxyEntryForSession(session);
@@ -1046,7 +1072,7 @@ function sectionPrefix(kind) {
1046
1072
  function sectionIdForPath(kind, path) {
1047
1073
  return `${sectionPrefix(kind)}:${encodePath(path)}`;
1048
1074
  }
1049
- function parseSectionId(id) {
1075
+ export function parseSectionId(id) {
1050
1076
  const [prefix, encoded] = id.split(':', 2);
1051
1077
  if (!prefix || !encoded)
1052
1078
  return null;
@@ -1314,7 +1340,7 @@ function firstNamedDescendant(node, allowedRoles) {
1314
1340
  }
1315
1341
  return undefined;
1316
1342
  }
1317
- function findNodeByPath(root, path) {
1343
+ export function findNodeByPath(root, path) {
1318
1344
  let current = root;
1319
1345
  for (const index of path) {
1320
1346
  if (!current.children[index])
@@ -2054,10 +2080,25 @@ export function buildPageModel(root, options) {
2054
2080
  };
2055
2081
  }
2056
2082
  export function buildFormSchemas(root, options) {
2057
- const forms = sortByBounds([
2083
+ const explicitForms = [
2058
2084
  ...(root.role === 'form' ? [root] : []),
2059
2085
  ...collectDescendants(root, candidate => candidate.role === 'form'),
2060
- ]);
2086
+ ];
2087
+ // Infer forms from group/region containers with 2+ form fields (e.g. Ashby-style UIs without <form>)
2088
+ const inferredForms = collectDescendants(root, candidate => {
2089
+ if (candidate.role !== 'group' && candidate.role !== 'region')
2090
+ return false;
2091
+ // Skip descendants of explicit forms
2092
+ for (const form of explicitForms) {
2093
+ if (candidate.path.length > form.path.length &&
2094
+ form.path.every((v, i) => candidate.path[i] === v)) {
2095
+ return false;
2096
+ }
2097
+ }
2098
+ const fields = collectDescendants(candidate, child => FORM_FIELD_ROLES.has(child.role));
2099
+ return fields.length >= 2;
2100
+ });
2101
+ const forms = sortByBounds([...explicitForms, ...inferredForms]);
2061
2102
  return forms
2062
2103
  .filter(form => !options?.formId || sectionIdForPath('form', form.path) === options.formId)
2063
2104
  .map(form => buildFormSchemaForNode(root, form, options));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.25.0",
3
+ "version": "1.27.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",