@geometra/mcp 1.44.0 → 1.46.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { valuesEquivalent } from '../server.js';
3
+ describe('valuesEquivalent', () => {
4
+ describe('plain strings', () => {
5
+ it('matches identical strings case-insensitively', () => {
6
+ expect(valuesEquivalent('Charlie', 'charlie')).toBe(true);
7
+ });
8
+ it('matches with whitespace collapse', () => {
9
+ expect(valuesEquivalent('Charlie Greenman', 'Charlie Greenman')).toBe(true);
10
+ });
11
+ it('rejects unrelated strings', () => {
12
+ expect(valuesEquivalent('Charlie', 'Alice')).toBe(false);
13
+ });
14
+ });
15
+ describe('phone numbers', () => {
16
+ it('matches identical phone signatures', () => {
17
+ expect(valuesEquivalent('9296081737', '9296081737')).toBe(true);
18
+ });
19
+ // Bug surfaced by JobForge round-2 marathon — Cloudflare FDE NYC #312.
20
+ // Greenhouse formats `+1-929-608-1737` → `(929) 608-1737`. The expected
21
+ // side carries the country code; the actual readback drops it. The
22
+ // strict-equality digit comparison previously rejected this as a
23
+ // mismatch even though the underlying value is correct.
24
+ it('matches phone numbers when expected has country code and actual does not', () => {
25
+ expect(valuesEquivalent('+1-929-608-1737', '(929) 608-1737')).toBe(true);
26
+ });
27
+ it('matches phone numbers when actual has country code and expected does not', () => {
28
+ expect(valuesEquivalent('(929) 608-1737', '+1-929-608-1737')).toBe(true);
29
+ });
30
+ it('matches across many ATS auto-format variants', () => {
31
+ const expected = '+19296081737';
32
+ expect(valuesEquivalent(expected, '(929) 608-1737')).toBe(true);
33
+ expect(valuesEquivalent(expected, '929-608-1737')).toBe(true);
34
+ expect(valuesEquivalent(expected, '929.608.1737')).toBe(true);
35
+ expect(valuesEquivalent(expected, '929 608 1737')).toBe(true);
36
+ expect(valuesEquivalent(expected, '+1 (929) 608-1737')).toBe(true);
37
+ });
38
+ it('rejects two phone numbers that differ in the local 10 digits', () => {
39
+ expect(valuesEquivalent('+1-929-608-1737', '(415) 555-0123')).toBe(false);
40
+ });
41
+ it('rejects suffix-match below the 7-digit local minimum', () => {
42
+ // Pure 6-digit IDs must not match against a longer phone signature
43
+ // even if the IDs happen to be a digit-suffix.
44
+ expect(valuesEquivalent('19296081737', '081737')).toBe(false);
45
+ });
46
+ });
47
+ describe('formatted numbers', () => {
48
+ it('matches a salary across comma formatting', () => {
49
+ expect(valuesEquivalent('$160000', '$160,000')).toBe(true);
50
+ });
51
+ });
52
+ });
package/dist/server.d.ts CHANGED
@@ -21,5 +21,6 @@ interface NodeFilter {
21
21
  busy?: boolean;
22
22
  }
23
23
  export declare function createServer(): McpServer;
24
+ export declare function valuesEquivalent(expected: string, actual: string): boolean;
24
25
  export declare function findNodes(node: A11yNode, filter: NodeFilter): A11yNode[];
25
26
  export {};
package/dist/server.js CHANGED
@@ -2600,7 +2600,26 @@ function fieldStatePayload(session, fieldLabel) {
2600
2600
  };
2601
2601
  }
2602
2602
  function waitStatusPayload(wait) {
2603
- return wait ? { wait: wait.status } : {};
2603
+ if (!wait)
2604
+ return {};
2605
+ const payload = { wait: wait.status };
2606
+ // Surface navigation info from proxy click handlers so callers can tell
2607
+ // when a click triggered a full-page nav (form submit → thank-you page).
2608
+ // Without this, the proxy session may die on the next request and the
2609
+ // caller would see session_not_found with no clue WHY. Bug surfaced by
2610
+ // JobForge round-2 marathon — Cloudflare FDE NYC #312 and Airtable PM
2611
+ // AI #94 both had Submit-clicks that navigated and tore down the proxy.
2612
+ if (wait.result && typeof wait.result === 'object') {
2613
+ const result = wait.result;
2614
+ if (result.navigated === true) {
2615
+ payload.navigated = true;
2616
+ if (typeof result.pageUrl === 'string')
2617
+ payload.pageUrl = result.pageUrl;
2618
+ if (typeof result.urlBefore === 'string')
2619
+ payload.urlBefore = result.urlBefore;
2620
+ }
2621
+ }
2622
+ return payload;
2604
2623
  }
2605
2624
  function compactFilterPayload(filter) {
2606
2625
  return Object.fromEntries(Object.entries(filter).filter(([, value]) => value !== undefined));
@@ -3582,12 +3601,32 @@ function looksLikeFormattedNumber(value) {
3582
3601
  function digitSignature(value) {
3583
3602
  return value.replace(/\D+/g, '');
3584
3603
  }
3585
- function valuesEquivalent(expected, actual) {
3604
+ export function valuesEquivalent(expected, actual) {
3586
3605
  if (expected.toLowerCase() === actual.toLowerCase())
3587
3606
  return true;
3588
- // Both look like phones → compare digit signatures only
3607
+ // Both look like phones → compare digit signatures, but allow one to be
3608
+ // a suffix of the other so that an explicit country code on the expected
3609
+ // side ("+1-929-608-1737" → digits "19296081737") still matches an ATS
3610
+ // readback that omits it ("(929) 608-1737" → digits "9296081737"). The
3611
+ // suffix check is direction-agnostic — either side may carry the country
3612
+ // code. Without this, Greenhouse/Workday/Lever auto-formatted phone
3613
+ // fields false-flag as mismatched even though the value is correct.
3614
+ // Bug surfaced by JobForge round-2 marathon — Cloudflare FDE NYC #312.
3589
3615
  if (looksLikePhoneNumber(expected) && looksLikePhoneNumber(actual)) {
3590
- return digitSignature(expected) === digitSignature(actual);
3616
+ const eDigits = digitSignature(expected);
3617
+ const aDigits = digitSignature(actual);
3618
+ if (!eDigits || !aDigits)
3619
+ return false;
3620
+ if (eDigits === aDigits)
3621
+ return true;
3622
+ // Suffix tolerance for country-code drift — only accept if the longer
3623
+ // signature ends with the shorter, AND the shorter is at least 7 digits
3624
+ // (NANP local minimum) so we don't false-match on tiny extension ids.
3625
+ const longer = eDigits.length >= aDigits.length ? eDigits : aDigits;
3626
+ const shorter = eDigits.length >= aDigits.length ? aDigits : eDigits;
3627
+ if (shorter.length >= 7 && longer.endsWith(shorter))
3628
+ return true;
3629
+ return false;
3591
3630
  }
3592
3631
  // Both look like formatted numbers → compare digit signatures only
3593
3632
  if (looksLikeFormattedNumber(expected) || looksLikeFormattedNumber(actual)) {
package/dist/session.js CHANGED
@@ -645,19 +645,46 @@ export function connect(url, opts) {
645
645
  let resolved = false;
646
646
  let lastMessageAt = Date.now();
647
647
  let heartbeatInterval = null;
648
+ let pendingPongBy = null;
649
+ // Heartbeat: send a real WS-level ping every 15s and only tear the socket
650
+ // down if the peer fails to respond to two consecutive pings (i.e. ~45s of
651
+ // true unresponsiveness). Previous versions used a dumb idle timer that
652
+ // closed the socket after 30s of no inbound frames — which killed sessions
653
+ // during normal form-submission flows where the DOM is legitimately idle
654
+ // for 20-30+ seconds while the backend processes (Greenhouse submit →
655
+ // security-code dialog is the canonical repro). A real ping/pong cycle
656
+ // distinguishes a silent-but-healthy session from a dead one.
648
657
  function startHeartbeat() {
649
658
  if (heartbeatInterval)
650
659
  return;
651
660
  heartbeatInterval = setInterval(() => {
652
- if (Date.now() - lastMessageAt > 30_000) {
661
+ // If we're waiting on a pong and it's overdue, the peer is dead.
662
+ if (pendingPongBy !== null && Date.now() > pendingPongBy) {
653
663
  try {
654
664
  ws.close();
655
665
  }
656
666
  catch { /* ignore */ }
667
+ return;
668
+ }
669
+ // Only send a new ping if we haven't heard anything for a while,
670
+ // to avoid spamming a chatty session.
671
+ if (Date.now() - lastMessageAt > 10_000) {
672
+ try {
673
+ ws.ping();
674
+ // Allow 30s for the pong before declaring the peer dead.
675
+ pendingPongBy = Date.now() + 30_000;
676
+ }
677
+ catch {
678
+ /* if ping throws, the socket is already gone — let 'close' handle */
679
+ }
657
680
  }
658
- }, 30_000);
681
+ }, 15_000);
659
682
  heartbeatInterval.unref();
660
683
  }
684
+ ws.on('pong', () => {
685
+ lastMessageAt = Date.now();
686
+ pendingPongBy = null;
687
+ });
661
688
  const timeout = setTimeout(() => {
662
689
  if (!resolved) {
663
690
  resolved = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.44.0",
3
+ "version": "1.46.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",