@geometra/mcp 1.19.16 → 1.19.18

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,118 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { WebSocketServer } from 'ws';
3
+ const mockState = vi.hoisted(() => ({
4
+ startEmbeddedGeometraProxy: vi.fn(),
5
+ spawnGeometraProxy: vi.fn(),
6
+ }));
7
+ vi.mock('../proxy-spawn.js', () => ({
8
+ startEmbeddedGeometraProxy: mockState.startEmbeddedGeometraProxy,
9
+ spawnGeometraProxy: mockState.spawnGeometraProxy,
10
+ }));
11
+ const { connectThroughProxy, disconnect } = await import('../session.js');
12
+ function frame(pageUrl) {
13
+ return {
14
+ type: 'frame',
15
+ layout: { x: 0, y: 0, width: 1280, height: 720, children: [] },
16
+ tree: {
17
+ kind: 'box',
18
+ props: {},
19
+ semantic: {
20
+ tag: 'body',
21
+ role: 'group',
22
+ pageUrl,
23
+ },
24
+ children: [],
25
+ },
26
+ };
27
+ }
28
+ async function createProxyPeer(options) {
29
+ const wss = new WebSocketServer({ port: 0 });
30
+ wss.on('connection', ws => {
31
+ ws.send(JSON.stringify(frame(options?.pageUrl ?? 'https://jobs.example.com/original')));
32
+ ws.on('message', raw => {
33
+ const msg = JSON.parse(String(raw));
34
+ if (msg.type === 'navigate') {
35
+ options?.onNavigate?.(ws, msg);
36
+ }
37
+ });
38
+ });
39
+ const port = await new Promise((resolve, reject) => {
40
+ wss.once('listening', () => {
41
+ const address = wss.address();
42
+ if (typeof address === 'object' && address)
43
+ resolve(address.port);
44
+ else
45
+ reject(new Error('Failed to resolve ephemeral WebSocket port'));
46
+ });
47
+ wss.once('error', reject);
48
+ });
49
+ return {
50
+ wss,
51
+ wsUrl: `ws://127.0.0.1:${port}`,
52
+ };
53
+ }
54
+ afterEach(async () => {
55
+ disconnect({ closeProxy: true });
56
+ vi.clearAllMocks();
57
+ });
58
+ async function closePeer(wss) {
59
+ for (const client of wss.clients) {
60
+ client.close();
61
+ }
62
+ await new Promise((resolve, reject) => wss.close(err => (err ? reject(err) : resolve())));
63
+ }
64
+ describe('connectThroughProxy recovery', () => {
65
+ it('restarts from a fresh proxy when a reused browser session was already closed', async () => {
66
+ const stalePeer = await createProxyPeer({
67
+ pageUrl: 'https://jobs.example.com/original',
68
+ onNavigate(ws, msg) {
69
+ ws.send(JSON.stringify({
70
+ type: 'error',
71
+ requestId: msg.requestId,
72
+ message: 'page.goto: Target page, context or browser has been closed',
73
+ }));
74
+ },
75
+ });
76
+ const freshPeer = await createProxyPeer({
77
+ pageUrl: 'https://jobs.example.com/recovered',
78
+ });
79
+ const staleRuntime = {
80
+ wsUrl: stalePeer.wsUrl,
81
+ closed: false,
82
+ close: vi.fn(async () => {
83
+ staleRuntime.closed = true;
84
+ }),
85
+ };
86
+ const freshRuntime = {
87
+ wsUrl: freshPeer.wsUrl,
88
+ closed: false,
89
+ close: vi.fn(async () => {
90
+ freshRuntime.closed = true;
91
+ }),
92
+ };
93
+ mockState.startEmbeddedGeometraProxy
94
+ .mockResolvedValueOnce({ runtime: staleRuntime, wsUrl: stalePeer.wsUrl })
95
+ .mockResolvedValueOnce({ runtime: freshRuntime, wsUrl: freshPeer.wsUrl });
96
+ mockState.spawnGeometraProxy.mockRejectedValue(new Error('spawn fallback should not be used'));
97
+ try {
98
+ const firstSession = await connectThroughProxy({
99
+ pageUrl: 'https://jobs.example.com/original',
100
+ headless: true,
101
+ });
102
+ expect(firstSession.proxyRuntime).toBe(staleRuntime);
103
+ const recoveredSession = await connectThroughProxy({
104
+ pageUrl: 'https://jobs.example.com/recovered',
105
+ headless: true,
106
+ });
107
+ expect(recoveredSession.proxyRuntime).toBe(freshRuntime);
108
+ expect(mockState.startEmbeddedGeometraProxy).toHaveBeenCalledTimes(2);
109
+ expect(staleRuntime.close).toHaveBeenCalledTimes(1);
110
+ expect(mockState.spawnGeometraProxy).not.toHaveBeenCalled();
111
+ }
112
+ finally {
113
+ disconnect({ closeProxy: true });
114
+ await closePeer(stalePeer.wss);
115
+ await closePeer(freshPeer.wss);
116
+ }
117
+ });
118
+ });
@@ -1,7 +1,14 @@
1
1
  import { type ChildProcess } from 'node:child_process';
2
+ export interface EmbeddedProxyRuntime {
3
+ wsUrl: string;
4
+ closed: boolean;
5
+ close: () => Promise<void>;
6
+ }
2
7
  /** Resolve bundled @geometra/proxy CLI entry (dist/index.js). */
3
8
  export declare function resolveProxyScriptPath(): string;
4
9
  export declare function resolveProxyScriptPathWith(customRequire: NodeRequire, moduleDir?: string): string;
10
+ export declare function resolveProxyRuntimePath(): string;
11
+ export declare function resolveProxyRuntimePathWith(customRequire: NodeRequire, moduleDir?: string): string;
5
12
  export interface SpawnProxyParams {
6
13
  pageUrl: string;
7
14
  port: number;
@@ -10,6 +17,10 @@ export interface SpawnProxyParams {
10
17
  height?: number;
11
18
  slowMo?: number;
12
19
  }
20
+ export declare function startEmbeddedGeometraProxy(opts: SpawnProxyParams): Promise<{
21
+ runtime: EmbeddedProxyRuntime;
22
+ wsUrl: string;
23
+ }>;
13
24
  export declare function parseProxyReadySignalLine(line: string): string | undefined;
14
25
  export declare function formatProxyStartupFailure(message: string, opts: SpawnProxyParams): string;
15
26
  /**
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from 'node:child_process';
2
2
  import { existsSync, realpathSync, rmSync } from 'node:fs';
3
3
  import { createRequire } from 'node:module';
4
4
  import path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  const require = createRequire(import.meta.url);
7
7
  const READY_SIGNAL_TYPE = 'geometra-proxy-ready';
8
8
  const READY_TIMEOUT_MS = 45_000;
@@ -12,21 +12,30 @@ export function resolveProxyScriptPath() {
12
12
  return resolveProxyScriptPathWith(require);
13
13
  }
14
14
  export function resolveProxyScriptPathWith(customRequire, moduleDir = MODULE_DIR) {
15
+ return resolveProxyDistPathWith(customRequire, moduleDir, 'index.js');
16
+ }
17
+ export function resolveProxyRuntimePath() {
18
+ return resolveProxyRuntimePathWith(require);
19
+ }
20
+ export function resolveProxyRuntimePathWith(customRequire, moduleDir = MODULE_DIR) {
21
+ return resolveProxyDistPathWith(customRequire, moduleDir, 'runtime.js');
22
+ }
23
+ function resolveProxyDistPathWith(customRequire, moduleDir, entryFile) {
15
24
  const errors = [];
16
- const workspaceDist = path.resolve(moduleDir, '../../packages/proxy/dist/index.js');
25
+ const workspaceDist = path.resolve(moduleDir, `../../packages/proxy/dist/${entryFile}`);
17
26
  const bundledDependencyDir = path.resolve(moduleDir, '../node_modules/@geometra/proxy');
18
27
  const packageDir = resolveProxyPackageDir(customRequire);
19
28
  if (packageDir) {
20
29
  if (shouldPreferWorkspaceDist(packageDir, bundledDependencyDir) && existsSync(workspaceDist)) {
21
30
  return workspaceDist;
22
31
  }
23
- const packagedDist = path.join(packageDir, 'dist/index.js');
32
+ const packagedDist = path.join(packageDir, 'dist', entryFile);
24
33
  if (existsSync(packagedDist))
25
34
  return packagedDist;
26
- const builtLocalDist = buildLocalProxyDistIfPossible(packageDir, errors);
35
+ const builtLocalDist = buildLocalProxyDistIfPossible(packageDir, entryFile, errors);
27
36
  if (builtLocalDist)
28
37
  return builtLocalDist;
29
- errors.push(`Resolved @geometra/proxy package at ${packageDir}, but dist/index.js was missing`);
38
+ errors.push(`Resolved @geometra/proxy package at ${packageDir}, but dist/${entryFile} was missing`);
30
39
  }
31
40
  else {
32
41
  errors.push('Could not find @geometra/proxy/package.json via Node module search paths');
@@ -34,24 +43,26 @@ export function resolveProxyScriptPathWith(customRequire, moduleDir = MODULE_DIR
34
43
  try {
35
44
  const pkgJson = customRequire.resolve('@geometra/proxy/package.json');
36
45
  const exportPackageDir = path.dirname(pkgJson);
37
- const packagedDist = path.join(exportPackageDir, 'dist/index.js');
46
+ const packagedDist = path.join(exportPackageDir, 'dist', entryFile);
38
47
  if (existsSync(packagedDist))
39
48
  return packagedDist;
40
- const builtLocalDist = buildLocalProxyDistIfPossible(exportPackageDir, errors);
49
+ const builtLocalDist = buildLocalProxyDistIfPossible(exportPackageDir, entryFile, errors);
41
50
  if (builtLocalDist)
42
51
  return builtLocalDist;
43
- errors.push(`Resolved @geometra/proxy/package.json at ${pkgJson}, but dist/index.js was missing`);
52
+ errors.push(`Resolved @geometra/proxy/package.json at ${pkgJson}, but dist/${entryFile} was missing`);
44
53
  }
45
54
  catch (err) {
46
55
  errors.push(err instanceof Error ? err.message : String(err));
47
56
  }
48
- try {
49
- return customRequire.resolve('@geometra/proxy');
50
- }
51
- catch (err) {
52
- errors.push(err instanceof Error ? err.message : String(err));
57
+ if (entryFile === 'index.js') {
58
+ try {
59
+ return customRequire.resolve('@geometra/proxy');
60
+ }
61
+ catch (err) {
62
+ errors.push(err instanceof Error ? err.message : String(err));
63
+ }
53
64
  }
54
- const packagedSiblingDist = path.resolve(moduleDir, '../../proxy/dist/index.js');
65
+ const packagedSiblingDist = path.resolve(moduleDir, `../../proxy/dist/${entryFile}`);
55
66
  if (existsSync(packagedSiblingDist)) {
56
67
  return packagedSiblingDist;
57
68
  }
@@ -60,7 +71,7 @@ export function resolveProxyScriptPathWith(customRequire, moduleDir = MODULE_DIR
60
71
  return workspaceDist;
61
72
  }
62
73
  errors.push(`Workspace fallback not found at ${workspaceDist}`);
63
- throw new Error(`Could not resolve @geometra/proxy. Install it with the MCP package: npm install @geometra/proxy. Resolution errors: ${errors.join(' | ')}`);
74
+ throw new Error(`Could not resolve @geometra/proxy dist/${entryFile}. Install it with the MCP package: npm install @geometra/proxy. Resolution errors: ${errors.join(' | ')}`);
64
75
  }
65
76
  function resolveProxyPackageDir(customRequire) {
66
77
  const searchRoots = customRequire.resolve.paths('@geometra/proxy') ?? [];
@@ -79,8 +90,8 @@ function shouldPreferWorkspaceDist(packageDir, bundledDependencyDir) {
79
90
  return false;
80
91
  }
81
92
  }
82
- function buildLocalProxyDistIfPossible(packageDir, errors) {
83
- const distEntry = path.join(packageDir, 'dist/index.js');
93
+ function buildLocalProxyDistIfPossible(packageDir, entryFile, errors) {
94
+ const distEntry = path.join(packageDir, 'dist', entryFile);
84
95
  const sourceEntry = path.join(packageDir, 'src/index.ts');
85
96
  const tsconfigPath = path.join(packageDir, 'tsconfig.build.json');
86
97
  if (!existsSync(sourceEntry) || !existsSync(tsconfigPath)) {
@@ -104,16 +115,32 @@ function buildLocalProxyDistIfPossible(packageDir, errors) {
104
115
  }
105
116
  if (existsSync(distEntry))
106
117
  return distEntry;
107
- const realDistEntry = path.join(realPackageDir, 'dist/index.js');
118
+ const realDistEntry = path.join(realPackageDir, 'dist', entryFile);
108
119
  if (existsSync(realDistEntry))
109
120
  return realDistEntry;
110
- errors.push(`Built local @geometra/proxy at ${realPackageDir}, but dist/index.js is still missing`);
121
+ errors.push(`Built local @geometra/proxy at ${realPackageDir}, but dist/${entryFile} is still missing`);
111
122
  }
112
123
  catch (err) {
113
124
  errors.push(err instanceof Error ? err.message : String(err));
114
125
  }
115
126
  return undefined;
116
127
  }
128
+ export async function startEmbeddedGeometraProxy(opts) {
129
+ const runtimePath = resolveProxyRuntimePath();
130
+ const runtimeModule = await import(pathToFileURL(runtimePath).href);
131
+ if (typeof runtimeModule.launchProxyRuntime !== 'function') {
132
+ throw new Error(`Resolved ${runtimePath}, but it did not export launchProxyRuntime()`);
133
+ }
134
+ const runtime = await runtimeModule.launchProxyRuntime({
135
+ url: opts.pageUrl,
136
+ port: opts.port,
137
+ width: opts.width,
138
+ height: opts.height,
139
+ headed: opts.headless !== true,
140
+ slowMo: opts.slowMo,
141
+ });
142
+ return { runtime, wsUrl: runtime.wsUrl };
143
+ }
117
144
  export function parseProxyReadySignalLine(line) {
118
145
  const trimmed = line.trim();
119
146
  if (!trimmed)
package/dist/server.js CHANGED
@@ -175,7 +175,7 @@ const batchActionSchema = z.discriminatedUnion('type', [
175
175
  }),
176
176
  ]);
177
177
  export function createServer() {
178
- const server = new McpServer({ name: 'geometra', version: '1.19.14' }, { capabilities: { tools: {} } });
178
+ const server = new McpServer({ name: 'geometra', version: '1.19.17' }, { capabilities: { tools: {} } });
179
179
  // ── connect ──────────────────────────────────────────────────
180
180
  server.tool('geometra_connect', `Connect to a Geometra WebSocket peer, or start \`geometra-proxy\` automatically for a normal web page.
181
181
 
package/dist/session.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
2
  import WebSocket from 'ws';
3
+ import { type EmbeddedProxyRuntime } from './proxy-spawn.js';
3
4
  /**
4
5
  * Parsed accessibility node from the UI tree + computed layout.
5
6
  * Mirrors the shape of @geometra/core's AccessibilityNode without importing it
@@ -325,6 +326,7 @@ export interface Session {
325
326
  updateRevision: number;
326
327
  /** Present when this session owns a child geometra-proxy process (pageUrl connect). */
327
328
  proxyChild?: ChildProcess;
329
+ proxyRuntime?: EmbeddedProxyRuntime;
328
330
  proxyReusable?: boolean;
329
331
  cachedA11y?: A11yNode | null;
330
332
  cachedA11yRevision?: number;
package/dist/session.js CHANGED
@@ -1,5 +1,5 @@
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
4
  let reusableProxy = null;
5
5
  const ACTION_UPDATE_TIMEOUT_MS = 2000;
@@ -19,14 +19,42 @@ function invalidateSessionCaches(session) {
19
19
  session.cachedFormSchemas?.clear();
20
20
  }
21
21
  function clearReusableProxyIfExited() {
22
- if (!reusableProxy?.child.killed && reusableProxy?.child.exitCode === null && reusableProxy?.child.signalCode === null) {
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;
23
29
  return;
24
30
  }
31
+ if (!reusableProxy.runtime.closed)
32
+ return;
25
33
  reusableProxy = null;
26
34
  }
27
- function setReusableProxy(child, wsUrl, opts) {
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
+ }
28
56
  reusableProxy = {
29
- child,
57
+ runtime: proxy.runtime,
30
58
  wsUrl,
31
59
  headless: opts.headless === true,
32
60
  slowMo: opts.slowMo ?? 0,
@@ -34,13 +62,6 @@ function setReusableProxy(child, wsUrl, opts) {
34
62
  height: opts.height ?? 720,
35
63
  pageUrl: opts.pageUrl,
36
64
  };
37
- const clear = () => {
38
- if (reusableProxy?.child === child)
39
- reusableProxy = null;
40
- };
41
- child.once('exit', clear);
42
- child.once('close', clear);
43
- child.once('error', clear);
44
65
  }
45
66
  function closeReusableProxy() {
46
67
  clearReusableProxyIfExited();
@@ -48,12 +69,16 @@ function closeReusableProxy() {
48
69
  reusableProxy = null;
49
70
  if (!proxy)
50
71
  return;
51
- try {
52
- proxy.child.kill('SIGTERM');
53
- }
54
- catch {
55
- /* ignore */
72
+ if (proxy.child) {
73
+ try {
74
+ proxy.child.kill('SIGTERM');
75
+ }
76
+ catch {
77
+ /* ignore */
78
+ }
79
+ return;
56
80
  }
81
+ void proxy.runtime.close().catch(() => { });
57
82
  }
58
83
  function rememberReusableProxyPageUrl(session) {
59
84
  const pageUrl = session.cachedA11y?.meta?.pageUrl;
@@ -61,6 +86,10 @@ function rememberReusableProxyPageUrl(session) {
61
86
  return;
62
87
  if (session.proxyChild && reusableProxy?.child === session.proxyChild) {
63
88
  reusableProxy.pageUrl = pageUrl;
89
+ return;
90
+ }
91
+ if (session.proxyRuntime && reusableProxy?.runtime === session.proxyRuntime) {
92
+ reusableProxy.pageUrl = pageUrl;
64
93
  }
65
94
  }
66
95
  function shutdownPreviousSession(opts) {
@@ -87,6 +116,122 @@ function shutdownPreviousSession(opts) {
87
116
  catch {
88
117
  /* ignore */
89
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(() => { });
129
+ }
130
+ }
131
+ function formatUnknownError(err) {
132
+ return err instanceof Error ? err.message : String(err);
133
+ }
134
+ async function attachToReusableProxy(options) {
135
+ if (!reusableProxy) {
136
+ throw new Error('Failed to attach to reusable proxy session');
137
+ }
138
+ const session = ((reusableProxy.child && activeSession?.proxyChild === reusableProxy.child) ||
139
+ (reusableProxy.runtime && activeSession?.proxyRuntime === reusableProxy.runtime))
140
+ ? activeSession
141
+ : await connect(reusableProxy.wsUrl, {
142
+ skipInitialResize: true,
143
+ closePreviousProxy: false,
144
+ awaitInitialFrame: options.awaitInitialFrame,
145
+ });
146
+ if (!session) {
147
+ throw new Error('Failed to attach to reusable proxy session');
148
+ }
149
+ session.proxyChild = reusableProxy.child;
150
+ session.proxyRuntime = reusableProxy.runtime;
151
+ session.proxyReusable = true;
152
+ const desiredWidth = options.width ?? reusableProxy.width;
153
+ const desiredHeight = options.height ?? reusableProxy.height;
154
+ if (desiredWidth !== reusableProxy.width || desiredHeight !== reusableProxy.height) {
155
+ await sendAndWaitForUpdate(session, {
156
+ type: 'resize',
157
+ width: desiredWidth,
158
+ height: desiredHeight,
159
+ }, 5_000);
160
+ reusableProxy.width = desiredWidth;
161
+ reusableProxy.height = desiredHeight;
162
+ }
163
+ const currentUrl = session.cachedA11y?.meta?.pageUrl ?? reusableProxy.pageUrl;
164
+ if (currentUrl !== options.pageUrl) {
165
+ await sendNavigate(session, options.pageUrl, 15_000);
166
+ if ((session.proxyChild && reusableProxy?.child === session.proxyChild) ||
167
+ (session.proxyRuntime && reusableProxy?.runtime === session.proxyRuntime)) {
168
+ reusableProxy.pageUrl = options.pageUrl;
169
+ }
170
+ }
171
+ return session;
172
+ }
173
+ async function startFreshProxySession(options) {
174
+ closeReusableProxy();
175
+ try {
176
+ const { runtime, wsUrl } = await startEmbeddedGeometraProxy({
177
+ pageUrl: options.pageUrl,
178
+ port: options.port ?? 0,
179
+ headless: options.headless,
180
+ width: options.width,
181
+ height: options.height,
182
+ slowMo: options.slowMo,
183
+ });
184
+ const session = await connect(wsUrl, {
185
+ skipInitialResize: true,
186
+ closePreviousProxy: false,
187
+ awaitInitialFrame: options.awaitInitialFrame,
188
+ });
189
+ session.proxyRuntime = runtime;
190
+ session.proxyReusable = true;
191
+ setReusableProxy({ runtime }, wsUrl, {
192
+ headless: options.headless,
193
+ slowMo: options.slowMo,
194
+ width: options.width,
195
+ height: options.height,
196
+ pageUrl: options.pageUrl,
197
+ });
198
+ return session;
199
+ }
200
+ catch (e) {
201
+ const { child, wsUrl } = await spawnGeometraProxy({
202
+ pageUrl: options.pageUrl,
203
+ port: options.port ?? 0,
204
+ headless: options.headless,
205
+ width: options.width,
206
+ height: options.height,
207
+ slowMo: options.slowMo,
208
+ });
209
+ try {
210
+ const session = await connect(wsUrl, {
211
+ skipInitialResize: true,
212
+ closePreviousProxy: false,
213
+ awaitInitialFrame: options.awaitInitialFrame,
214
+ });
215
+ session.proxyChild = child;
216
+ session.proxyReusable = true;
217
+ setReusableProxy({ child }, wsUrl, {
218
+ headless: options.headless,
219
+ slowMo: options.slowMo,
220
+ width: options.width,
221
+ height: options.height,
222
+ pageUrl: options.pageUrl,
223
+ });
224
+ return session;
225
+ }
226
+ catch (fallbackError) {
227
+ try {
228
+ child.kill('SIGTERM');
229
+ }
230
+ catch {
231
+ /* ignore */
232
+ }
233
+ throw fallbackError instanceof Error ? fallbackError : e;
234
+ }
90
235
  }
91
236
  }
92
237
  /**
@@ -173,6 +318,9 @@ export function connect(url, opts) {
173
318
  /* ignore */
174
319
  }
175
320
  }
321
+ if (session.proxyRuntime && !session.proxyReusable) {
322
+ void session.proxyRuntime.close().catch(() => { });
323
+ }
176
324
  }
177
325
  if (!resolved) {
178
326
  resolved = true;
@@ -190,75 +338,24 @@ export async function connectThroughProxy(options) {
190
338
  clearReusableProxyIfExited();
191
339
  const desiredHeadless = options.headless === true;
192
340
  const desiredSlowMo = options.slowMo ?? 0;
341
+ let reuseFailure;
193
342
  if (reusableProxy &&
194
343
  reusableProxy.headless === desiredHeadless &&
195
344
  reusableProxy.slowMo === desiredSlowMo) {
196
- const session = activeSession?.proxyChild === reusableProxy.child
197
- ? activeSession
198
- : await connect(reusableProxy.wsUrl, {
199
- skipInitialResize: true,
200
- closePreviousProxy: false,
201
- awaitInitialFrame: options.awaitInitialFrame,
202
- });
203
- if (!session) {
204
- throw new Error('Failed to attach to reusable proxy session');
205
- }
206
- session.proxyChild = reusableProxy.child;
207
- session.proxyReusable = true;
208
- const desiredWidth = options.width ?? reusableProxy.width;
209
- const desiredHeight = options.height ?? reusableProxy.height;
210
- if (desiredWidth !== reusableProxy.width || desiredHeight !== reusableProxy.height) {
211
- await sendAndWaitForUpdate(session, {
212
- type: 'resize',
213
- width: desiredWidth,
214
- height: desiredHeight,
215
- }, 5_000);
216
- reusableProxy.width = desiredWidth;
217
- reusableProxy.height = desiredHeight;
345
+ try {
346
+ return await attachToReusableProxy(options);
218
347
  }
219
- if (options.pageUrl) {
220
- const currentUrl = session.cachedA11y?.meta?.pageUrl ?? reusableProxy.pageUrl;
221
- if (currentUrl !== options.pageUrl) {
222
- await sendNavigate(session, options.pageUrl, 15_000);
223
- if (reusableProxy?.child === session.proxyChild) {
224
- reusableProxy.pageUrl = options.pageUrl;
225
- }
226
- }
348
+ catch (err) {
349
+ reuseFailure = err;
350
+ closeReusableProxy();
227
351
  }
228
- return session;
229
352
  }
230
- closeReusableProxy();
231
- const { child, wsUrl } = await spawnGeometraProxy({
232
- pageUrl: options.pageUrl,
233
- port: options.port ?? 0,
234
- headless: options.headless,
235
- width: options.width,
236
- height: options.height,
237
- slowMo: options.slowMo,
238
- });
239
353
  try {
240
- const session = await connect(wsUrl, {
241
- skipInitialResize: true,
242
- closePreviousProxy: false,
243
- awaitInitialFrame: options.awaitInitialFrame,
244
- });
245
- session.proxyChild = child;
246
- session.proxyReusable = true;
247
- setReusableProxy(child, wsUrl, {
248
- headless: options.headless,
249
- slowMo: options.slowMo,
250
- width: options.width,
251
- height: options.height,
252
- pageUrl: options.pageUrl,
253
- });
254
- return session;
354
+ return await startFreshProxySession(options);
255
355
  }
256
356
  catch (e) {
257
- try {
258
- child.kill('SIGTERM');
259
- }
260
- catch {
261
- /* ignore */
357
+ if (reuseFailure) {
358
+ throw new Error(`Failed to recover reusable browser session after it became stale: ${formatUnknownError(reuseFailure)}\nFresh proxy start also failed: ${formatUnknownError(e)}`);
262
359
  }
263
360
  throw e;
264
361
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.19.16",
3
+ "version": "1.19.18",
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.16",
33
+ "@geometra/proxy": "^1.19.18",
34
34
  "@modelcontextprotocol/sdk": "^1.12.1",
35
35
  "ws": "^8.18.0",
36
36
  "zod": "^3.23.0"