@geometra/mcp 1.20.0 → 1.21.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/server.js CHANGED
@@ -761,6 +761,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
761
761
  const wait = await sendFillFields(session, directFields);
762
762
  const ackResult = parseProxyFillAckResult(wait.result);
763
763
  if (ackResult && ackResult.invalidCount === 0) {
764
+ recordWorkflowFill(session, undefined, undefined, valuesById, valuesByLabel, 0, directFields.length);
764
765
  return ok(JSON.stringify({
765
766
  ...connection,
766
767
  completed: true,
@@ -777,6 +778,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
777
778
  const afterDirect = sessionA11y(session);
778
779
  const directSignals = afterDirect ? collectSessionSignals(afterDirect) : undefined;
779
780
  if (directSignals && directSignals.invalidFields.length === 0) {
781
+ recordWorkflowFill(session, undefined, undefined, valuesById, valuesByLabel, 0, directFields.length);
780
782
  return ok(JSON.stringify({
781
783
  ...connection,
782
784
  completed: true,
@@ -872,8 +874,12 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
872
874
  fieldCount: planned.fields.length,
873
875
  successCount: planned.fields.length,
874
876
  errorCount: 0,
877
+ minConfidence: planned.planned.length > 0
878
+ ? Number(Math.min(...planned.planned.map(p => p.confidence)).toFixed(2))
879
+ : undefined,
875
880
  ...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
876
881
  };
882
+ recordWorkflowFill(session, schema.formId, schema.name, valuesById, valuesByLabel, invalidRemaining, planned.fields.length);
877
883
  if (failOnInvalid && invalidRemaining > 0) {
878
884
  return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
879
885
  }
@@ -885,15 +891,18 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
885
891
  const startIndex = resumeFromIndex ?? 0;
886
892
  for (let index = startIndex; index < planned.fields.length; index++) {
887
893
  const field = planned.fields[index];
894
+ const plan = planned.planned[index];
895
+ const confidence = plan?.confidence;
896
+ const matchMethod = plan?.matchMethod;
888
897
  try {
889
898
  const result = await executeFillField(session, field, detail);
890
899
  steps.push(detail === 'verbose'
891
- ? { index, kind: field.kind, ok: true, summary: result.summary }
892
- : { index, kind: field.kind, ok: true, ...result.compact });
900
+ ? { index, kind: field.kind, ok: true, ...(confidence !== undefined ? { confidence, matchMethod } : {}), summary: result.summary }
901
+ : { index, kind: field.kind, ok: true, ...(confidence !== undefined ? { confidence, matchMethod } : {}), ...result.compact });
893
902
  }
894
903
  catch (e) {
895
904
  const message = e instanceof Error ? e.message : String(e);
896
- steps.push({ index, kind: field.kind, ok: false, error: message });
905
+ steps.push({ index, kind: field.kind, ok: false, ...(confidence !== undefined ? { confidence, matchMethod } : {}), error: message });
897
906
  if (stopOnError) {
898
907
  stoppedAt = index;
899
908
  break;
@@ -914,11 +923,15 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
914
923
  fieldCount: planned.fields.length,
915
924
  successCount,
916
925
  errorCount,
926
+ minConfidence: planned.planned.length > 0
927
+ ? Number(Math.min(...planned.planned.map(p => p.confidence)).toFixed(2))
928
+ : undefined,
917
929
  ...(startIndex > 0 ? { resumedFromIndex: startIndex } : {}),
918
930
  ...(includeSteps ? { steps } : {}),
919
931
  ...(stoppedAt !== undefined ? { stoppedAt, resumeFromIndex: stoppedAt + 1 } : {}),
920
932
  ...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
921
933
  };
934
+ recordWorkflowFill(session, schema.formId, schema.name, valuesById, valuesByLabel, invalidRemaining, planned.fields.length);
922
935
  if (failOnInvalid && invalidRemaining > 0) {
923
936
  return err(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
924
937
  }
@@ -1713,6 +1726,43 @@ For a token-efficient semantic view, use geometra_snapshot (default compact). Fo
1713
1726
  return err('Not connected. Call geometra_connect first.');
1714
1727
  return ok(JSON.stringify(session.layout, null, 2));
1715
1728
  });
1729
+ // ── workflow state ───────────────────────────────────────────
1730
+ server.tool('geometra_workflow_state', `Get the accumulated workflow state across page navigations. Shows which pages/forms have been filled, what values were submitted, and the fill status per page.
1731
+
1732
+ Use this after navigating to a new page in a multi-step flow (e.g. job applications) to understand what has been completed so far. Pass \`clear: true\` to reset the workflow state.`, {
1733
+ clear: z.boolean().optional().default(false).describe('Reset the workflow state'),
1734
+ }, async ({ clear }) => {
1735
+ const session = getSession();
1736
+ if (!session)
1737
+ return err('Not connected. Call geometra_connect first.');
1738
+ if (clear) {
1739
+ session.workflowState = undefined;
1740
+ return ok(JSON.stringify({ cleared: true }));
1741
+ }
1742
+ if (!session.workflowState || session.workflowState.pages.length === 0) {
1743
+ return ok(JSON.stringify({
1744
+ pageCount: 0,
1745
+ message: 'No workflow state recorded yet. Fill a form with geometra_fill_form to start tracking.',
1746
+ }));
1747
+ }
1748
+ const state = session.workflowState;
1749
+ const totalFields = state.pages.reduce((sum, p) => sum + p.fieldCount, 0);
1750
+ const totalInvalid = state.pages.reduce((sum, p) => sum + p.invalidCount, 0);
1751
+ return ok(JSON.stringify({
1752
+ pageCount: state.pages.length,
1753
+ totalFieldsFilled: totalFields,
1754
+ totalInvalidRemaining: totalInvalid,
1755
+ elapsedMs: Date.now() - state.startedAt,
1756
+ pages: state.pages.map(p => ({
1757
+ pageUrl: p.pageUrl,
1758
+ ...(p.formId ? { formId: p.formId } : {}),
1759
+ ...(p.formName ? { formName: p.formName } : {}),
1760
+ fieldCount: p.fieldCount,
1761
+ invalidCount: p.invalidCount,
1762
+ filledValues: p.filledValues,
1763
+ })),
1764
+ }));
1765
+ });
1716
1766
  // ── disconnect ───────────────────────────────────────────────
1717
1767
  server.tool('geometra_disconnect', `Disconnect from the Geometra server. Proxy-backed sessions keep compatible browsers alive by default so the next geometra_connect can reuse them quickly; pass closeBrowser=true to fully tear down the warm proxy/browser pool.`, {
1718
1768
  closeBrowser: z.boolean().optional().default(false).describe('Fully close the spawned proxy/browser instead of keeping it warm for reuse'),
@@ -2031,6 +2081,12 @@ function collectSessionSignals(root) {
2031
2081
  };
2032
2082
  const seenAlerts = new Set();
2033
2083
  const seenInvalidIds = new Set();
2084
+ const captchaPattern = /recaptcha|g-recaptcha|hcaptcha|h-captcha|turnstile|cf-turnstile|captcha/i;
2085
+ const captchaTypes = {
2086
+ recaptcha: 'recaptcha', 'g-recaptcha': 'recaptcha',
2087
+ hcaptcha: 'hcaptcha', 'h-captcha': 'hcaptcha',
2088
+ turnstile: 'turnstile', 'cf-turnstile': 'turnstile',
2089
+ };
2034
2090
  function walk(node) {
2035
2091
  if (!signals.focus && node.state?.focused) {
2036
2092
  signals.focus = {
@@ -2063,6 +2119,14 @@ function collectSessionSignals(root) {
2063
2119
  });
2064
2120
  }
2065
2121
  }
2122
+ if (!signals.captchaDetected) {
2123
+ const text = [node.name, node.value].filter(Boolean).join(' ');
2124
+ if (captchaPattern.test(text)) {
2125
+ signals.captchaDetected = true;
2126
+ const match = text.toLowerCase().match(/recaptcha|g-recaptcha|hcaptcha|h-captcha|turnstile|cf-turnstile/);
2127
+ signals.captchaType = match ? (captchaTypes[match[0]] ?? 'unknown') : 'unknown';
2128
+ }
2129
+ }
2066
2130
  for (const child of node.children)
2067
2131
  walk(child);
2068
2132
  }
@@ -2124,6 +2188,7 @@ function sessionSignalsPayload(signals, detail = 'minimal') {
2124
2188
  ? { scroll: { x: signals.scrollX ?? 0, y: signals.scrollY ?? 0 } }
2125
2189
  : {}),
2126
2190
  ...(signals.focus ? { focus: signals.focus } : {}),
2191
+ ...(signals.captchaDetected ? { captchaDetected: true, captchaType: signals.captchaType ?? 'unknown' } : {}),
2127
2192
  dialogCount: signals.dialogCount,
2128
2193
  busyCount: signals.busyCount,
2129
2194
  alertCount: signals.alerts.length,
@@ -2442,7 +2507,7 @@ function planFormFill(schema, opts) {
2442
2507
  else
2443
2508
  fieldsByLabel.set(key, [field]);
2444
2509
  }
2445
- const planned = [];
2510
+ const allPlanned = [];
2446
2511
  const seenFieldIds = new Set();
2447
2512
  for (const [fieldId, value] of Object.entries(opts.valuesById ?? {})) {
2448
2513
  const field = fieldById.get(fieldId);
@@ -2451,7 +2516,8 @@ function planFormFill(schema, opts) {
2451
2516
  const next = plannedFillInputsForField(field, value);
2452
2517
  if ('error' in next)
2453
2518
  return { ok: false, error: next.error };
2454
- planned.push(...next);
2519
+ for (const n of next)
2520
+ allPlanned.push({ field: n, confidence: 1.0, matchMethod: 'id' });
2455
2521
  seenFieldIds.add(field.id);
2456
2522
  }
2457
2523
  for (const [label, value] of Object.entries(opts.valuesByLabel ?? {})) {
@@ -2468,10 +2534,14 @@ function planFormFill(schema, opts) {
2468
2534
  const next = plannedFillInputsForField(field, value);
2469
2535
  if ('error' in next)
2470
2536
  return { ok: false, error: next.error };
2471
- planned.push(...next);
2537
+ const isExact = field.label === label;
2538
+ const confidence = isExact ? 0.95 : 0.8;
2539
+ const matchMethod = isExact ? 'label-exact' : 'label-normalized';
2540
+ for (const n of next)
2541
+ allPlanned.push({ field: n, confidence, matchMethod });
2472
2542
  seenFieldIds.add(field.id);
2473
2543
  }
2474
- return { ok: true, fields: planned };
2544
+ return { ok: true, fields: allPlanned.map(p => p.field), planned: allPlanned };
2475
2545
  }
2476
2546
  function isResolvedFillFieldInput(field) {
2477
2547
  if (field.kind === 'toggle')
@@ -3073,6 +3143,31 @@ function ok(text, screenshot) {
3073
3143
  }
3074
3144
  return { content };
3075
3145
  }
3146
+ function recordWorkflowFill(session, formId, formName, valuesById, valuesByLabel, invalidCount, fieldCount) {
3147
+ if (!session.workflowState) {
3148
+ session.workflowState = { pages: [], startedAt: Date.now() };
3149
+ }
3150
+ const filledValues = {};
3151
+ for (const [k, v] of Object.entries(valuesById ?? {})) {
3152
+ if (typeof v === 'string' || typeof v === 'boolean')
3153
+ filledValues[k] = v;
3154
+ }
3155
+ for (const [k, v] of Object.entries(valuesByLabel ?? {})) {
3156
+ if (typeof v === 'string' || typeof v === 'boolean')
3157
+ filledValues[k] = v;
3158
+ }
3159
+ const a11y = sessionA11y(session);
3160
+ const pageUrl = a11y?.meta?.pageUrl ?? session.url;
3161
+ session.workflowState.pages.push({
3162
+ pageUrl,
3163
+ formId,
3164
+ formName,
3165
+ filledValues,
3166
+ filledAt: Date.now(),
3167
+ fieldCount,
3168
+ invalidCount,
3169
+ });
3170
+ }
3076
3171
  async function captureScreenshotBase64(session) {
3077
3172
  try {
3078
3173
  const wait = await sendScreenshot(session);
package/dist/session.d.ts CHANGED
@@ -121,6 +121,11 @@ export interface PageDialogModel extends PageSectionSummaryBase {
121
121
  export interface PageListModel extends PageSectionSummaryBase {
122
122
  itemCount: number;
123
123
  }
124
+ export interface CaptchaDetection {
125
+ detected: boolean;
126
+ type?: 'recaptcha' | 'hcaptcha' | 'turnstile' | 'cloudflare-challenge' | 'unknown';
127
+ hint?: string;
128
+ }
124
129
  export interface PageModel {
125
130
  viewport: {
126
131
  width: number;
@@ -134,6 +139,7 @@ export interface PageModel {
134
139
  listCount: number;
135
140
  focusableCount: number;
136
141
  };
142
+ captcha?: CaptchaDetection;
137
143
  primaryActions: PagePrimaryAction[];
138
144
  landmarks: PageLandmark[];
139
145
  forms: PageFormModel[];
@@ -343,6 +349,19 @@ export interface UiDelta {
343
349
  viewport?: UiViewportChange;
344
350
  focus?: UiFocusChange;
345
351
  }
352
+ export interface WorkflowPageEntry {
353
+ pageUrl: string;
354
+ formId?: string;
355
+ formName?: string;
356
+ filledValues: Record<string, string | boolean>;
357
+ filledAt: number;
358
+ fieldCount: number;
359
+ invalidCount: number;
360
+ }
361
+ export interface WorkflowState {
362
+ pages: WorkflowPageEntry[];
363
+ startedAt: number;
364
+ }
346
365
  export interface Session {
347
366
  ws: WebSocket;
348
367
  layout: Record<string, unknown> | null;
@@ -360,6 +379,7 @@ export interface Session {
360
379
  revision: number;
361
380
  forms: FormSchemaModel[];
362
381
  }>;
382
+ workflowState?: WorkflowState;
363
383
  }
364
384
  export interface SessionConnectTrace {
365
385
  mode: 'direct-ws' | 'fresh-proxy' | 'reused-proxy';
@@ -563,10 +583,6 @@ export declare function buildCompactUiIndex(root: A11yNode, options?: {
563
583
  };
564
584
  export declare function summarizeCompactIndex(nodes: CompactUiNode[], maxLines?: number): string;
565
585
  export declare function nodeContextForNode(root: A11yNode, node: A11yNode): NodeContextModel | undefined;
566
- /**
567
- * Build a summary-first, stable-ID webpage model from the accessibility tree.
568
- * Use {@link expandPageSection} to fetch details for a specific section on demand.
569
- */
570
586
  export declare function buildPageModel(root: A11yNode, options?: {
571
587
  maxPrimaryActions?: number;
572
588
  maxSectionsPerKind?: number;
package/dist/session.js CHANGED
@@ -1845,6 +1845,47 @@ function inferPageArchetypes(model) {
1845
1845
  * Build a summary-first, stable-ID webpage model from the accessibility tree.
1846
1846
  * Use {@link expandPageSection} to fetch details for a specific section on demand.
1847
1847
  */
1848
+ const CAPTCHA_PATTERNS = [
1849
+ { pattern: /recaptcha|g-recaptcha/i, type: 'recaptcha', hint: 'Google reCAPTCHA detected' },
1850
+ { pattern: /hcaptcha|h-captcha/i, type: 'hcaptcha', hint: 'hCaptcha detected' },
1851
+ { pattern: /turnstile|cf-turnstile/i, type: 'turnstile', hint: 'Cloudflare Turnstile detected' },
1852
+ { pattern: /cloudflare.*challenge|challenge-platform|just a moment/i, type: 'cloudflare-challenge', hint: 'Cloudflare challenge page detected' },
1853
+ { pattern: /captcha/i, type: 'unknown', hint: 'CAPTCHA element detected' },
1854
+ ];
1855
+ function detectCaptcha(root) {
1856
+ let found;
1857
+ function walk(node) {
1858
+ if (found)
1859
+ return;
1860
+ const text = [node.name, node.value, node.role].filter(Boolean).join(' ');
1861
+ for (const { pattern, type, hint } of CAPTCHA_PATTERNS) {
1862
+ if (pattern.test(text)) {
1863
+ found = { detected: true, type, hint };
1864
+ return;
1865
+ }
1866
+ }
1867
+ // Check iframe placeholders (common for reCAPTCHA/hCaptcha/Turnstile)
1868
+ if (node.meta && typeof node.meta.frameUrl === 'string') {
1869
+ const frameUrl = node.meta.frameUrl;
1870
+ for (const { pattern, type, hint } of CAPTCHA_PATTERNS) {
1871
+ if (pattern.test(frameUrl)) {
1872
+ found = { detected: true, type, hint };
1873
+ return;
1874
+ }
1875
+ }
1876
+ }
1877
+ for (const child of node.children)
1878
+ walk(child);
1879
+ }
1880
+ walk(root);
1881
+ // Also check the page URL for Cloudflare challenge pages
1882
+ if (!found && root.meta?.pageUrl) {
1883
+ if (/challenge|cdn-cgi.*challenge/i.test(root.meta.pageUrl)) {
1884
+ found = { detected: true, type: 'cloudflare-challenge', hint: 'Cloudflare challenge page URL detected' };
1885
+ }
1886
+ }
1887
+ return found ?? { detected: false };
1888
+ }
1848
1889
  export function buildPageModel(root, options) {
1849
1890
  const maxPrimaryActions = options?.maxPrimaryActions ?? 6;
1850
1891
  const maxSectionsPerKind = options?.maxSectionsPerKind ?? 8;
@@ -1928,8 +1969,10 @@ export function buildPageModel(root, options) {
1928
1969
  dialogs: sortByBounds(dialogs).slice(0, maxSectionsPerKind),
1929
1970
  lists: sortByBounds(lists).slice(0, maxSectionsPerKind),
1930
1971
  };
1972
+ const captcha = detectCaptcha(root);
1931
1973
  return {
1932
1974
  ...baseModel,
1975
+ ...(captcha.detected ? { captcha } : {}),
1933
1976
  archetypes: inferPageArchetypes(baseModel),
1934
1977
  };
1935
1978
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.20.0",
3
+ "version": "1.21.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",