@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('
|
|
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: '
|
|
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 =
|
|
8
|
-
const
|
|
9
|
-
const
|
|
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 =
|
|
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.
|
|
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.
|
|
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"
|