@geometra/mcp 1.19.16 → 1.19.17

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.
@@ -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,16 @@ 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(() => { });
90
129
  }
91
130
  }
92
131
  /**
@@ -173,6 +212,9 @@ export function connect(url, opts) {
173
212
  /* ignore */
174
213
  }
175
214
  }
215
+ if (session.proxyRuntime && !session.proxyReusable) {
216
+ void session.proxyRuntime.close().catch(() => { });
217
+ }
176
218
  }
177
219
  if (!resolved) {
178
220
  resolved = true;
@@ -193,7 +235,8 @@ export async function connectThroughProxy(options) {
193
235
  if (reusableProxy &&
194
236
  reusableProxy.headless === desiredHeadless &&
195
237
  reusableProxy.slowMo === desiredSlowMo) {
196
- const session = activeSession?.proxyChild === reusableProxy.child
238
+ const session = ((reusableProxy.child && activeSession?.proxyChild === reusableProxy.child) ||
239
+ (reusableProxy.runtime && activeSession?.proxyRuntime === reusableProxy.runtime))
197
240
  ? activeSession
198
241
  : await connect(reusableProxy.wsUrl, {
199
242
  skipInitialResize: true,
@@ -204,6 +247,7 @@ export async function connectThroughProxy(options) {
204
247
  throw new Error('Failed to attach to reusable proxy session');
205
248
  }
206
249
  session.proxyChild = reusableProxy.child;
250
+ session.proxyRuntime = reusableProxy.runtime;
207
251
  session.proxyReusable = true;
208
252
  const desiredWidth = options.width ?? reusableProxy.width;
209
253
  const desiredHeight = options.height ?? reusableProxy.height;
@@ -220,7 +264,8 @@ export async function connectThroughProxy(options) {
220
264
  const currentUrl = session.cachedA11y?.meta?.pageUrl ?? reusableProxy.pageUrl;
221
265
  if (currentUrl !== options.pageUrl) {
222
266
  await sendNavigate(session, options.pageUrl, 15_000);
223
- if (reusableProxy?.child === session.proxyChild) {
267
+ if ((session.proxyChild && reusableProxy?.child === session.proxyChild) ||
268
+ (session.proxyRuntime && reusableProxy?.runtime === session.proxyRuntime)) {
224
269
  reusableProxy.pageUrl = options.pageUrl;
225
270
  }
226
271
  }
@@ -228,23 +273,23 @@ export async function connectThroughProxy(options) {
228
273
  return session;
229
274
  }
230
275
  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
276
  try {
277
+ const { runtime, wsUrl } = await startEmbeddedGeometraProxy({
278
+ pageUrl: options.pageUrl,
279
+ port: options.port ?? 0,
280
+ headless: options.headless,
281
+ width: options.width,
282
+ height: options.height,
283
+ slowMo: options.slowMo,
284
+ });
240
285
  const session = await connect(wsUrl, {
241
286
  skipInitialResize: true,
242
287
  closePreviousProxy: false,
243
288
  awaitInitialFrame: options.awaitInitialFrame,
244
289
  });
245
- session.proxyChild = child;
290
+ session.proxyRuntime = runtime;
246
291
  session.proxyReusable = true;
247
- setReusableProxy(child, wsUrl, {
292
+ setReusableProxy({ runtime }, wsUrl, {
248
293
  headless: options.headless,
249
294
  slowMo: options.slowMo,
250
295
  width: options.width,
@@ -254,13 +299,40 @@ export async function connectThroughProxy(options) {
254
299
  return session;
255
300
  }
256
301
  catch (e) {
302
+ const { child, wsUrl } = await spawnGeometraProxy({
303
+ pageUrl: options.pageUrl,
304
+ port: options.port ?? 0,
305
+ headless: options.headless,
306
+ width: options.width,
307
+ height: options.height,
308
+ slowMo: options.slowMo,
309
+ });
257
310
  try {
258
- child.kill('SIGTERM');
311
+ const session = await connect(wsUrl, {
312
+ skipInitialResize: true,
313
+ closePreviousProxy: false,
314
+ awaitInitialFrame: options.awaitInitialFrame,
315
+ });
316
+ session.proxyChild = child;
317
+ session.proxyReusable = true;
318
+ setReusableProxy({ child }, wsUrl, {
319
+ headless: options.headless,
320
+ slowMo: options.slowMo,
321
+ width: options.width,
322
+ height: options.height,
323
+ pageUrl: options.pageUrl,
324
+ });
325
+ return session;
259
326
  }
260
- catch {
261
- /* ignore */
327
+ catch (fallbackError) {
328
+ try {
329
+ child.kill('SIGTERM');
330
+ }
331
+ catch {
332
+ /* ignore */
333
+ }
334
+ throw fallbackError instanceof Error ? fallbackError : e;
262
335
  }
263
- throw e;
264
336
  }
265
337
  }
266
338
  export function getSession() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geometra/mcp",
3
- "version": "1.19.16",
3
+ "version": "1.19.17",
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.17",
34
34
  "@modelcontextprotocol/sdk": "^1.12.1",
35
35
  "ws": "^8.18.0",
36
36
  "zod": "^3.23.0"