@geometra/mcp 1.19.15 → 1.19.17

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.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import WebSocket from 'ws';
2
- import { spawnGeometraProxy } from './proxy-spawn.js';
2
+ import { spawnGeometraProxy, startEmbeddedGeometraProxy } from './proxy-spawn.js';
3
3
  let activeSession = null;
4
+ let reusableProxy = null;
4
5
  const ACTION_UPDATE_TIMEOUT_MS = 2000;
5
6
  const LISTBOX_UPDATE_TIMEOUT_MS = 4500;
6
7
  const FILL_BATCH_BASE_TIMEOUT_MS = 2500;
@@ -12,7 +13,86 @@ const FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS = 225;
12
13
  const FILL_BATCH_FILE_FIELD_TIMEOUT_MS = 5000;
13
14
  const FILL_BATCH_MAX_TIMEOUT_MS = 60_000;
14
15
  let nextRequestSequence = 0;
15
- function shutdownPreviousSession() {
16
+ function invalidateSessionCaches(session) {
17
+ session.cachedA11y = null;
18
+ session.cachedA11yRevision = -1;
19
+ session.cachedFormSchemas?.clear();
20
+ }
21
+ function clearReusableProxyIfExited() {
22
+ if (!reusableProxy)
23
+ return;
24
+ if (reusableProxy.child) {
25
+ if (!reusableProxy.child.killed && reusableProxy.child.exitCode === null && reusableProxy.child.signalCode === null) {
26
+ return;
27
+ }
28
+ reusableProxy = null;
29
+ return;
30
+ }
31
+ if (!reusableProxy.runtime.closed)
32
+ return;
33
+ reusableProxy = null;
34
+ }
35
+ function setReusableProxy(proxy, wsUrl, opts) {
36
+ if ('child' in proxy) {
37
+ const child = proxy.child;
38
+ reusableProxy = {
39
+ child,
40
+ wsUrl,
41
+ headless: opts.headless === true,
42
+ slowMo: opts.slowMo ?? 0,
43
+ width: opts.width ?? 1280,
44
+ height: opts.height ?? 720,
45
+ pageUrl: opts.pageUrl,
46
+ };
47
+ const clear = () => {
48
+ if (reusableProxy?.child === child)
49
+ reusableProxy = null;
50
+ };
51
+ child.once('exit', clear);
52
+ child.once('close', clear);
53
+ child.once('error', clear);
54
+ return;
55
+ }
56
+ reusableProxy = {
57
+ runtime: proxy.runtime,
58
+ wsUrl,
59
+ headless: opts.headless === true,
60
+ slowMo: opts.slowMo ?? 0,
61
+ width: opts.width ?? 1280,
62
+ height: opts.height ?? 720,
63
+ pageUrl: opts.pageUrl,
64
+ };
65
+ }
66
+ function closeReusableProxy() {
67
+ clearReusableProxyIfExited();
68
+ const proxy = reusableProxy;
69
+ reusableProxy = null;
70
+ if (!proxy)
71
+ return;
72
+ if (proxy.child) {
73
+ try {
74
+ proxy.child.kill('SIGTERM');
75
+ }
76
+ catch {
77
+ /* ignore */
78
+ }
79
+ return;
80
+ }
81
+ void proxy.runtime.close().catch(() => { });
82
+ }
83
+ function rememberReusableProxyPageUrl(session) {
84
+ const pageUrl = session.cachedA11y?.meta?.pageUrl;
85
+ if (!pageUrl)
86
+ return;
87
+ if (session.proxyChild && reusableProxy?.child === session.proxyChild) {
88
+ reusableProxy.pageUrl = pageUrl;
89
+ return;
90
+ }
91
+ if (session.proxyRuntime && reusableProxy?.runtime === session.proxyRuntime) {
92
+ reusableProxy.pageUrl = pageUrl;
93
+ }
94
+ }
95
+ function shutdownPreviousSession(opts) {
16
96
  const prev = activeSession;
17
97
  if (!prev)
18
98
  return;
@@ -24,12 +104,28 @@ function shutdownPreviousSession() {
24
104
  /* ignore */
25
105
  }
26
106
  if (prev.proxyChild) {
107
+ const shouldKeepProxy = prev.proxyReusable && opts?.closeProxy === false;
108
+ rememberReusableProxyPageUrl(prev);
109
+ if (shouldKeepProxy)
110
+ return;
111
+ if (reusableProxy?.child === prev.proxyChild)
112
+ reusableProxy = null;
27
113
  try {
28
114
  prev.proxyChild.kill('SIGTERM');
29
115
  }
30
116
  catch {
31
117
  /* ignore */
32
118
  }
119
+ return;
120
+ }
121
+ if (prev.proxyRuntime) {
122
+ const shouldKeepProxy = prev.proxyReusable && opts?.closeProxy === false;
123
+ rememberReusableProxyPageUrl(prev);
124
+ if (shouldKeepProxy)
125
+ return;
126
+ if (reusableProxy?.runtime === prev.proxyRuntime)
127
+ reusableProxy = null;
128
+ void prev.proxyRuntime.close().catch(() => { });
33
129
  }
34
130
  }
35
131
  /**
@@ -38,9 +134,22 @@ function shutdownPreviousSession() {
38
134
  */
39
135
  export function connect(url, opts) {
40
136
  return new Promise((resolve, reject) => {
41
- shutdownPreviousSession();
137
+ clearReusableProxyIfExited();
138
+ if (reusableProxy && reusableProxy.wsUrl !== url) {
139
+ closeReusableProxy();
140
+ }
141
+ shutdownPreviousSession({ closeProxy: opts?.closePreviousProxy ?? true });
42
142
  const ws = new WebSocket(url);
43
- const session = { ws, layout: null, tree: null, url, updateRevision: 0 };
143
+ const session = {
144
+ ws,
145
+ layout: null,
146
+ tree: null,
147
+ url,
148
+ updateRevision: 0,
149
+ cachedA11y: null,
150
+ cachedA11yRevision: -1,
151
+ cachedFormSchemas: new Map(),
152
+ };
44
153
  let resolved = false;
45
154
  const timeout = setTimeout(() => {
46
155
  if (!resolved) {
@@ -50,11 +159,17 @@ export function connect(url, opts) {
50
159
  }
51
160
  }, 10_000);
52
161
  ws.on('open', () => {
53
- if (opts?.skipInitialResize)
54
- return;
55
- const width = opts?.width ?? 1024;
56
- const height = opts?.height ?? 768;
57
- ws.send(JSON.stringify({ type: 'resize', width, height }));
162
+ if (!opts?.skipInitialResize) {
163
+ const width = opts?.width ?? 1024;
164
+ const height = opts?.height ?? 768;
165
+ ws.send(JSON.stringify({ type: 'resize', width, height }));
166
+ }
167
+ if (opts?.awaitInitialFrame === false && !resolved) {
168
+ resolved = true;
169
+ clearTimeout(timeout);
170
+ activeSession = session;
171
+ resolve(session);
172
+ }
58
173
  });
59
174
  ws.on('message', (data) => {
60
175
  try {
@@ -63,6 +178,7 @@ export function connect(url, opts) {
63
178
  session.layout = msg.layout;
64
179
  session.tree = msg.tree;
65
180
  session.updateRevision++;
181
+ invalidateSessionCaches(session);
66
182
  if (!resolved) {
67
183
  resolved = true;
68
184
  clearTimeout(timeout);
@@ -73,6 +189,7 @@ export function connect(url, opts) {
73
189
  else if (msg.type === 'patch' && session.layout) {
74
190
  applyPatches(session.layout, msg.patches);
75
191
  session.updateRevision++;
192
+ invalidateSessionCaches(session);
76
193
  }
77
194
  }
78
195
  catch { /* ignore malformed messages */ }
@@ -87,7 +204,7 @@ export function connect(url, opts) {
87
204
  ws.on('close', () => {
88
205
  if (activeSession === session) {
89
206
  activeSession = null;
90
- if (session.proxyChild) {
207
+ if (session.proxyChild && !session.proxyReusable) {
91
208
  try {
92
209
  session.proxyChild.kill('SIGTERM');
93
210
  }
@@ -95,6 +212,9 @@ export function connect(url, opts) {
95
212
  /* ignore */
96
213
  }
97
214
  }
215
+ if (session.proxyRuntime && !session.proxyReusable) {
216
+ void session.proxyRuntime.close().catch(() => { });
217
+ }
98
218
  }
99
219
  if (!resolved) {
100
220
  resolved = true;
@@ -109,47 +229,135 @@ export function connect(url, opts) {
109
229
  * process to the session so disconnect / reconnect can clean it up.
110
230
  */
111
231
  export async function connectThroughProxy(options) {
112
- const { child, wsUrl } = await spawnGeometraProxy({
113
- pageUrl: options.pageUrl,
114
- port: options.port ?? 0,
115
- headless: options.headless,
116
- width: options.width,
117
- height: options.height,
118
- slowMo: options.slowMo,
119
- });
232
+ clearReusableProxyIfExited();
233
+ const desiredHeadless = options.headless === true;
234
+ const desiredSlowMo = options.slowMo ?? 0;
235
+ if (reusableProxy &&
236
+ reusableProxy.headless === desiredHeadless &&
237
+ reusableProxy.slowMo === desiredSlowMo) {
238
+ const session = ((reusableProxy.child && activeSession?.proxyChild === reusableProxy.child) ||
239
+ (reusableProxy.runtime && activeSession?.proxyRuntime === reusableProxy.runtime))
240
+ ? activeSession
241
+ : await connect(reusableProxy.wsUrl, {
242
+ skipInitialResize: true,
243
+ closePreviousProxy: false,
244
+ awaitInitialFrame: options.awaitInitialFrame,
245
+ });
246
+ if (!session) {
247
+ throw new Error('Failed to attach to reusable proxy session');
248
+ }
249
+ session.proxyChild = reusableProxy.child;
250
+ session.proxyRuntime = reusableProxy.runtime;
251
+ session.proxyReusable = true;
252
+ const desiredWidth = options.width ?? reusableProxy.width;
253
+ const desiredHeight = options.height ?? reusableProxy.height;
254
+ if (desiredWidth !== reusableProxy.width || desiredHeight !== reusableProxy.height) {
255
+ await sendAndWaitForUpdate(session, {
256
+ type: 'resize',
257
+ width: desiredWidth,
258
+ height: desiredHeight,
259
+ }, 5_000);
260
+ reusableProxy.width = desiredWidth;
261
+ reusableProxy.height = desiredHeight;
262
+ }
263
+ if (options.pageUrl) {
264
+ const currentUrl = session.cachedA11y?.meta?.pageUrl ?? reusableProxy.pageUrl;
265
+ if (currentUrl !== options.pageUrl) {
266
+ await sendNavigate(session, options.pageUrl, 15_000);
267
+ if ((session.proxyChild && reusableProxy?.child === session.proxyChild) ||
268
+ (session.proxyRuntime && reusableProxy?.runtime === session.proxyRuntime)) {
269
+ reusableProxy.pageUrl = options.pageUrl;
270
+ }
271
+ }
272
+ }
273
+ return session;
274
+ }
275
+ closeReusableProxy();
120
276
  try {
121
- const session = await connect(wsUrl, { skipInitialResize: true });
122
- session.proxyChild = child;
277
+ const { runtime, wsUrl } = await startEmbeddedGeometraProxy({
278
+ pageUrl: options.pageUrl,
279
+ port: options.port ?? 0,
280
+ headless: options.headless,
281
+ width: options.width,
282
+ height: options.height,
283
+ slowMo: options.slowMo,
284
+ });
285
+ const session = await connect(wsUrl, {
286
+ skipInitialResize: true,
287
+ closePreviousProxy: false,
288
+ awaitInitialFrame: options.awaitInitialFrame,
289
+ });
290
+ session.proxyRuntime = runtime;
291
+ session.proxyReusable = true;
292
+ setReusableProxy({ runtime }, wsUrl, {
293
+ headless: options.headless,
294
+ slowMo: options.slowMo,
295
+ width: options.width,
296
+ height: options.height,
297
+ pageUrl: options.pageUrl,
298
+ });
123
299
  return session;
124
300
  }
125
301
  catch (e) {
302
+ const { child, wsUrl } = await spawnGeometraProxy({
303
+ pageUrl: options.pageUrl,
304
+ port: options.port ?? 0,
305
+ headless: options.headless,
306
+ width: options.width,
307
+ height: options.height,
308
+ slowMo: options.slowMo,
309
+ });
126
310
  try {
127
- child.kill('SIGTERM');
311
+ const session = await connect(wsUrl, {
312
+ skipInitialResize: true,
313
+ closePreviousProxy: false,
314
+ awaitInitialFrame: options.awaitInitialFrame,
315
+ });
316
+ session.proxyChild = child;
317
+ session.proxyReusable = true;
318
+ setReusableProxy({ child }, wsUrl, {
319
+ headless: options.headless,
320
+ slowMo: options.slowMo,
321
+ width: options.width,
322
+ height: options.height,
323
+ pageUrl: options.pageUrl,
324
+ });
325
+ return session;
128
326
  }
129
- catch {
130
- /* ignore */
327
+ catch (fallbackError) {
328
+ try {
329
+ child.kill('SIGTERM');
330
+ }
331
+ catch {
332
+ /* ignore */
333
+ }
334
+ throw fallbackError instanceof Error ? fallbackError : e;
131
335
  }
132
- throw e;
133
336
  }
134
337
  }
135
338
  export function getSession() {
136
339
  return activeSession;
137
340
  }
138
- export function disconnect() {
139
- shutdownPreviousSession();
341
+ export function disconnect(opts) {
342
+ shutdownPreviousSession({ closeProxy: opts?.closeProxy ?? false });
343
+ if (opts?.closeProxy)
344
+ closeReusableProxy();
140
345
  }
141
346
  function estimateFillBatchTimeout(fields) {
142
347
  let total = FILL_BATCH_BASE_TIMEOUT_MS;
143
348
  let totalTextLength = 0;
144
349
  for (const field of fields) {
145
350
  switch (field.kind) {
351
+ case 'auto':
352
+ total += typeof field.value === 'boolean' ? FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS : FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS;
353
+ break;
146
354
  case 'text':
147
355
  totalTextLength += field.value.length;
148
356
  total += FILL_BATCH_TEXT_FIELD_TIMEOUT_MS;
149
357
  total += Math.ceil(Math.max(1, field.value.length) / FILL_BATCH_TEXT_LENGTH_SLICE) * FILL_BATCH_TEXT_LENGTH_TIMEOUT_MS;
150
358
  break;
151
359
  case 'choice':
152
- total += FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS;
360
+ total += field.choiceType === 'group' ? FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS : FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS;
153
361
  break;
154
362
  case 'toggle':
155
363
  total += FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS;
@@ -285,6 +493,8 @@ export function sendFieldText(session, fieldLabel, value, opts, timeoutMs) {
285
493
  };
286
494
  if (opts?.exact !== undefined)
287
495
  payload.exact = opts.exact;
496
+ if (opts?.fieldId)
497
+ payload.fieldId = opts.fieldId;
288
498
  return sendAndWaitForUpdate(session, payload, timeoutMs);
289
499
  }
290
500
  /** Choose a value for a labeled choice field (select, custom combobox, or radio-style group). */
@@ -298,6 +508,10 @@ export function sendFieldChoice(session, fieldLabel, value, opts, timeoutMs = LI
298
508
  payload.exact = opts.exact;
299
509
  if (opts?.query)
300
510
  payload.query = opts.query;
511
+ if (opts?.choiceType)
512
+ payload.choiceType = opts.choiceType;
513
+ if (opts?.fieldId)
514
+ payload.fieldId = opts.fieldId;
301
515
  return sendAndWaitForUpdate(session, payload, timeoutMs);
302
516
  }
303
517
  /** Fill several semantic form fields in one proxy-side batch. */
@@ -349,6 +563,13 @@ export function sendWheel(session, deltaY, opts, timeoutMs) {
349
563
  ...(opts?.y !== undefined ? { y: opts.y } : {}),
350
564
  }, timeoutMs);
351
565
  }
566
+ /** Navigate the proxy page to a new URL while keeping the browser process alive. */
567
+ export function sendNavigate(session, url, timeoutMs = 15_000) {
568
+ return sendAndWaitForUpdate(session, {
569
+ type: 'navigate',
570
+ url,
571
+ }, timeoutMs, { requireUpdateOnAck: true });
572
+ }
352
573
  /**
353
574
  * Build a flat accessibility tree from the raw UI tree + layout.
354
575
  * This is a standalone reimplementation that works with raw JSON —
@@ -974,10 +1195,16 @@ function simpleSchemaField(root, node) {
974
1195
  const label = fieldLabel(node) ?? sanitizeInlineName(node.name, 80) ?? context?.prompt;
975
1196
  if (!label)
976
1197
  return null;
1198
+ const choiceType = node.role === 'combobox'
1199
+ ? node.meta?.controlTag === 'select'
1200
+ ? 'select'
1201
+ : 'listbox'
1202
+ : undefined;
977
1203
  return {
978
1204
  id: formFieldIdForPath(node.path),
979
1205
  kind: node.role === 'combobox' ? 'choice' : 'text',
980
1206
  label,
1207
+ ...(choiceType ? { choiceType } : {}),
981
1208
  ...(node.state?.required ? { required: true } : {}),
982
1209
  ...(node.state?.invalid ? { invalid: true } : {}),
983
1210
  ...compactSchemaValue(node.value, 72),
@@ -1002,6 +1229,7 @@ function groupedSchemaField(root, grouped) {
1002
1229
  id: formFieldIdForPath(grouped.container.path),
1003
1230
  kind: radioLike ? 'choice' : 'multi_choice',
1004
1231
  label: grouped.prompt,
1232
+ ...(radioLike ? { choiceType: 'group' } : {}),
1005
1233
  ...(grouped.controls.some(control => control.state?.required) ? { required: true } : {}),
1006
1234
  ...(grouped.controls.some(control => control.state?.invalid) ? { invalid: true } : {}),
1007
1235
  ...(radioLike
@@ -1068,7 +1296,7 @@ function buildFormSchemaForNode(root, formNode, options) {
1068
1296
  consumed.add(candidateKey);
1069
1297
  }
1070
1298
  }
1071
- const compactFields = trimSchemaFieldContexts(fields);
1299
+ const compactFields = presentFormSchemaFields(fields, options);
1072
1300
  const filteredFields = compactFields.filter(field => {
1073
1301
  if (options?.onlyRequiredFields && !field.required)
1074
1302
  return false;
@@ -1089,14 +1317,35 @@ function buildFormSchemaForNode(root, formNode, options) {
1089
1317
  };
1090
1318
  }
1091
1319
  function trimSchemaFieldContexts(fields) {
1320
+ return presentFormSchemaFields(fields, { includeOptions: true, includeContext: 'auto' });
1321
+ }
1322
+ function presentFormSchemaFields(fields, options) {
1323
+ const includeOptions = options?.includeOptions ?? false;
1324
+ const includeContext = options?.includeContext ?? 'auto';
1092
1325
  const labelCounts = new Map();
1093
1326
  for (const field of fields) {
1094
1327
  const key = normalizeUiText(field.label);
1095
1328
  labelCounts.set(key, (labelCounts.get(key) ?? 0) + 1);
1096
1329
  }
1097
1330
  return fields.map(field => {
1331
+ const booleanChoice = field.kind === 'choice' &&
1332
+ field.choiceType === 'group' &&
1333
+ field.optionCount === 2 &&
1334
+ field.options?.length === 2 &&
1335
+ field.options.every(option => ['yes', 'no'].includes(normalizeUiText(option).toLowerCase()));
1336
+ const next = { ...field };
1337
+ if (booleanChoice)
1338
+ next.booleanChoice = true;
1339
+ if (!includeOptions)
1340
+ delete next.options;
1341
+ if (includeContext === 'none') {
1342
+ delete next.context;
1343
+ return next;
1344
+ }
1098
1345
  if (!field.context)
1099
- return field;
1346
+ return next;
1347
+ if (includeContext === 'always')
1348
+ return next;
1100
1349
  const trimmed = {};
1101
1350
  if (field.context.prompt && normalizeUiText(field.context.prompt) !== normalizeUiText(field.label)) {
1102
1351
  trimmed.prompt = field.context.prompt;
@@ -1105,10 +1354,11 @@ function trimSchemaFieldContexts(fields) {
1105
1354
  trimmed.section = field.context.section;
1106
1355
  }
1107
1356
  if (Object.keys(trimmed).length === 0) {
1108
- const { context: _context, ...rest } = field;
1109
- return rest;
1357
+ delete next.context;
1358
+ return next;
1110
1359
  }
1111
- return { ...field, context: trimmed };
1360
+ next.context = trimmed;
1361
+ return next;
1112
1362
  });
1113
1363
  }
1114
1364
  function toLandmarkModel(node) {
@@ -1690,6 +1940,8 @@ function walkNode(element, layout, path) {
1690
1940
  meta.scrollX = semantic.scrollX;
1691
1941
  if (typeof semantic?.scrollY === 'number' && Number.isFinite(semantic.scrollY))
1692
1942
  meta.scrollY = semantic.scrollY;
1943
+ if (typeof semantic?.tag === 'string' && semantic.tag.trim().length > 0)
1944
+ meta.controlTag = semantic.tag;
1693
1945
  const children = [];
1694
1946
  const elementChildren = element.children;
1695
1947
  const layoutChildren = layout.children;
@@ -1789,7 +2041,7 @@ function applyPatches(layout, patches) {
1789
2041
  node.height = patch.height;
1790
2042
  }
1791
2043
  }
1792
- function sendAndWaitForUpdate(session, message, timeoutMs = ACTION_UPDATE_TIMEOUT_MS) {
2044
+ function sendAndWaitForUpdate(session, message, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, opts) {
1793
2045
  return new Promise((resolve, reject) => {
1794
2046
  if (session.ws.readyState !== WebSocket.OPEN) {
1795
2047
  reject(new Error('Not connected'));
@@ -1798,11 +2050,14 @@ function sendAndWaitForUpdate(session, message, timeoutMs = ACTION_UPDATE_TIMEOU
1798
2050
  const requestId = `req-${++nextRequestSequence}`;
1799
2051
  const startRevision = session.updateRevision;
1800
2052
  session.ws.send(JSON.stringify({ ...message, requestId }));
1801
- waitForNextUpdate(session, timeoutMs, requestId, startRevision).then(resolve).catch(reject);
2053
+ waitForNextUpdate(session, timeoutMs, requestId, startRevision, opts).then(resolve).catch(reject);
1802
2054
  });
1803
2055
  }
1804
- function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, requestId, startRevision = session.updateRevision) {
2056
+ function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, requestId, startRevision = session.updateRevision, opts) {
1805
2057
  return new Promise((resolve, reject) => {
2058
+ let ackSeen = false;
2059
+ let ackResult;
2060
+ const ackPayload = () => (ackSeen && ackResult !== undefined ? { result: ackResult } : {});
1806
2061
  const onMessage = (data) => {
1807
2062
  try {
1808
2063
  const msg = JSON.parse(String(data));
@@ -1813,13 +2068,26 @@ function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, reques
1813
2068
  reject(new Error(typeof msg.message === 'string' ? msg.message : 'Geometra server error'));
1814
2069
  return;
1815
2070
  }
1816
- if (msg.type === 'ack' && messageRequestId === requestId) {
2071
+ if ((msg.type === 'frame' || (msg.type === 'patch' && session.layout)) && ackSeen && session.updateRevision > startRevision) {
1817
2072
  cleanup();
1818
2073
  resolve({
1819
- status: session.updateRevision > startRevision ? 'updated' : 'acknowledged',
2074
+ status: 'updated',
1820
2075
  timeoutMs,
1821
- ...(msg.result !== undefined ? { result: msg.result } : {}),
2076
+ ...ackPayload(),
1822
2077
  });
2078
+ return;
2079
+ }
2080
+ if (msg.type === 'ack' && messageRequestId === requestId) {
2081
+ ackSeen = true;
2082
+ ackResult = msg.result;
2083
+ if (!opts?.requireUpdateOnAck || session.updateRevision > startRevision) {
2084
+ cleanup();
2085
+ resolve({
2086
+ status: session.updateRevision > startRevision ? 'updated' : 'acknowledged',
2087
+ timeoutMs,
2088
+ ...ackPayload(),
2089
+ });
2090
+ }
1823
2091
  }
1824
2092
  return;
1825
2093
  }
@@ -1851,7 +2119,11 @@ function waitForNextUpdate(session, timeoutMs = ACTION_UPDATE_TIMEOUT_MS, reques
1851
2119
  const timeout = setTimeout(() => {
1852
2120
  cleanup();
1853
2121
  if (requestId && session.updateRevision > startRevision) {
1854
- resolve({ status: 'updated', timeoutMs });
2122
+ resolve({ status: 'updated', timeoutMs, ...ackPayload() });
2123
+ return;
2124
+ }
2125
+ if (requestId && ackSeen) {
2126
+ resolve({ status: 'acknowledged', timeoutMs, ...ackPayload() });
1855
2127
  return;
1856
2128
  }
1857
2129
  resolve({ status: 'timed_out', timeoutMs });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.19.15",
3
+ "version": "1.19.17",
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.15",
33
+ "@geometra/proxy": "^1.19.17",
34
34
  "@modelcontextprotocol/sdk": "^1.12.1",
35
35
  "ws": "^8.18.0",
36
36
  "zod": "^3.23.0"