@geometra/mcp 1.19.14 → 1.19.15

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.
@@ -97,7 +97,7 @@ describe('proxy-backed MCP actions', () => {
97
97
  await new Promise((resolve, reject) => wss.close(err => (err ? reject(err) : resolve())));
98
98
  }
99
99
  });
100
- it('sends batched fillFields messages and resolves from the resulting update', async () => {
100
+ it('waits for the post-batch update before resolving fillFields acks', async () => {
101
101
  const wss = new WebSocketServer({ port: 0 });
102
102
  let seenMessage;
103
103
  wss.on('connection', ws => {
@@ -113,6 +113,16 @@ describe('proxy-backed MCP actions', () => {
113
113
  }
114
114
  if (msg.type === 'fillFields') {
115
115
  seenMessage = msg;
116
+ ws.send(JSON.stringify({
117
+ type: 'frame',
118
+ layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
119
+ tree: {
120
+ kind: 'box',
121
+ props: {},
122
+ semantic: { tag: 'body', role: 'group', ariaLabel: 'Filled' },
123
+ children: [],
124
+ },
125
+ }));
116
126
  ws.send(JSON.stringify({
117
127
  type: 'ack',
118
128
  requestId: msg.requestId,
@@ -124,16 +134,6 @@ describe('proxy-backed MCP actions', () => {
124
134
  busyCount: 0,
125
135
  },
126
136
  }));
127
- ws.send(JSON.stringify({
128
- type: 'frame',
129
- layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
130
- tree: {
131
- kind: 'box',
132
- props: {},
133
- semantic: { tag: 'body', role: 'group', ariaLabel: 'Filled' },
134
- children: [],
135
- },
136
- }));
137
137
  }
138
138
  });
139
139
  });
@@ -153,7 +153,7 @@ describe('proxy-backed MCP actions', () => {
153
153
  { kind: 'text', fieldLabel: 'Full name', value: 'Taylor Applicant' },
154
154
  { kind: 'choice', fieldLabel: 'Country', value: 'Germany' },
155
155
  ], 80)).resolves.toMatchObject({
156
- status: 'acknowledged',
156
+ status: 'updated',
157
157
  timeoutMs: 80,
158
158
  result: {
159
159
  pageUrl: 'https://jobs.example.com/application',
@@ -445,6 +445,63 @@ describe('query and reveal tools', () => {
445
445
  },
446
446
  });
447
447
  });
448
+ it('falls back to sequential fill when a batched fill ends without a clean ack and invalid fields remain', async () => {
449
+ const handler = getToolHandler('geometra_fill_form');
450
+ mockState.sendFillFields.mockResolvedValueOnce({
451
+ status: 'updated',
452
+ timeoutMs: 6000,
453
+ result: undefined,
454
+ });
455
+ mockState.formSchemas = [
456
+ {
457
+ formId: 'fm:0',
458
+ name: 'Application',
459
+ fieldCount: 2,
460
+ requiredCount: 2,
461
+ invalidCount: 2,
462
+ fields: [
463
+ { id: 'ff:0.0', kind: 'text', label: 'Full name', required: true, invalid: true },
464
+ { id: 'ff:0.1', kind: 'choice', label: 'Preferred location', required: true, invalid: true },
465
+ ],
466
+ },
467
+ ];
468
+ mockState.currentA11yRoot = node('group', undefined, {
469
+ meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 640 },
470
+ children: [
471
+ node('textbox', 'Full name', {
472
+ path: [0],
473
+ state: { required: true, invalid: true },
474
+ }),
475
+ node('combobox', 'Preferred location', {
476
+ path: [1],
477
+ value: 'Select',
478
+ state: { required: true, invalid: true },
479
+ }),
480
+ ],
481
+ });
482
+ const result = await handler({
483
+ valuesById: {
484
+ 'ff:0.0': 'Taylor Applicant',
485
+ 'ff:0.1': 'Berlin, Germany',
486
+ },
487
+ includeSteps: false,
488
+ detail: 'minimal',
489
+ failOnInvalid: false,
490
+ });
491
+ const payload = JSON.parse(result.content[0].text);
492
+ expect(mockState.sendFillFields).toHaveBeenCalledTimes(1);
493
+ expect(mockState.sendFieldText).toHaveBeenCalledWith(mockState.session, 'Full name', 'Taylor Applicant', { exact: undefined }, undefined);
494
+ expect(mockState.sendFieldChoice).toHaveBeenCalledWith(mockState.session, 'Preferred location', 'Berlin, Germany', { exact: undefined, query: undefined }, undefined);
495
+ expect(payload).toMatchObject({
496
+ completed: true,
497
+ execution: 'sequential',
498
+ formId: 'fm:0',
499
+ requestedValueCount: 2,
500
+ fieldCount: 2,
501
+ successCount: 2,
502
+ errorCount: 0,
503
+ });
504
+ });
448
505
  it('reveals an offscreen target with semantic scrolling instead of requiring manual wheels', async () => {
449
506
  const handler = getToolHandler('geometra_reveal');
450
507
  mockState.currentA11yRoot = node('group', undefined, {
package/dist/server.js CHANGED
@@ -418,10 +418,12 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
418
418
  return err(planned.error);
419
419
  if (!includeSteps) {
420
420
  let usedBatch = false;
421
+ let batchAckResult;
421
422
  try {
422
423
  const startRevision = session.updateRevision;
423
424
  const wait = await sendFillFields(session, planned.fields);
424
425
  const ackResult = parseProxyFillAckResult(wait.result);
426
+ batchAckResult = ackResult;
425
427
  if (ackResult && ackResult.invalidCount === 0) {
426
428
  usedBatch = true;
427
429
  const payload = {
@@ -447,6 +449,14 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
447
449
  return err(message);
448
450
  }
449
451
  }
452
+ if (usedBatch) {
453
+ const after = sessionA11y(session);
454
+ const signals = after ? collectSessionSignals(after) : undefined;
455
+ const invalidRemaining = signals?.invalidFields.length ?? 0;
456
+ if ((!batchAckResult || batchAckResult.invalidCount > 0) && invalidRemaining > 0) {
457
+ usedBatch = false;
458
+ }
459
+ }
450
460
  if (usedBatch) {
451
461
  const after = sessionA11y(session);
452
462
  const signals = after ? collectSessionSignals(after) : undefined;
package/dist/session.js CHANGED
@@ -4,11 +4,13 @@ let activeSession = null;
4
4
  const ACTION_UPDATE_TIMEOUT_MS = 2000;
5
5
  const LISTBOX_UPDATE_TIMEOUT_MS = 4500;
6
6
  const FILL_BATCH_BASE_TIMEOUT_MS = 2500;
7
- const FILL_BATCH_TEXT_FIELD_TIMEOUT_MS = 250;
8
- const FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS = 450;
9
- const FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS = 200;
7
+ const FILL_BATCH_TEXT_FIELD_TIMEOUT_MS = 275;
8
+ const FILL_BATCH_TEXT_LENGTH_TIMEOUT_MS = 120;
9
+ const FILL_BATCH_TEXT_LENGTH_SLICE = 80;
10
+ const FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS = 500;
11
+ const FILL_BATCH_TOGGLE_FIELD_TIMEOUT_MS = 225;
10
12
  const FILL_BATCH_FILE_FIELD_TIMEOUT_MS = 5000;
11
- const FILL_BATCH_MAX_TIMEOUT_MS = 30_000;
13
+ const FILL_BATCH_MAX_TIMEOUT_MS = 60_000;
12
14
  let nextRequestSequence = 0;
13
15
  function shutdownPreviousSession() {
14
16
  const prev = activeSession;
@@ -138,10 +140,13 @@ export function disconnect() {
138
140
  }
139
141
  function estimateFillBatchTimeout(fields) {
140
142
  let total = FILL_BATCH_BASE_TIMEOUT_MS;
143
+ let totalTextLength = 0;
141
144
  for (const field of fields) {
142
145
  switch (field.kind) {
143
146
  case 'text':
147
+ totalTextLength += field.value.length;
144
148
  total += FILL_BATCH_TEXT_FIELD_TIMEOUT_MS;
149
+ total += Math.ceil(Math.max(1, field.value.length) / FILL_BATCH_TEXT_LENGTH_SLICE) * FILL_BATCH_TEXT_LENGTH_TIMEOUT_MS;
145
150
  break;
146
151
  case 'choice':
147
152
  total += FILL_BATCH_CHOICE_FIELD_TIMEOUT_MS;
@@ -154,6 +159,9 @@ function estimateFillBatchTimeout(fields) {
154
159
  break;
155
160
  }
156
161
  }
162
+ if (fields.length >= 20 || totalTextLength >= 1500) {
163
+ total = Math.max(total, 30_000);
164
+ }
157
165
  return Math.min(total, FILL_BATCH_MAX_TIMEOUT_MS);
158
166
  }
159
167
  export function waitForUiCondition(session, predicate, timeoutMs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.19.14",
3
+ "version": "1.19.15",
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.14",
33
+ "@geometra/proxy": "^1.19.15",
34
34
  "@modelcontextprotocol/sdk": "^1.12.1",
35
35
  "ws": "^8.18.0",
36
36
  "zod": "^3.23.0"