@geometra/mcp 1.63.2 → 1.65.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/README.md CHANGED
@@ -18,14 +18,14 @@ Geometra proxy: Chromium → DOM geometry → same WebSocket as native →
18
18
 
19
19
  Use Geometra MCP when an LLM needs to explore, interpret, and operate a real UI with compact semantic state instead of repeatedly consuming large browser snapshots. Keep Playwright-style tooling for deterministic scripts, DOM-oriented test automation, and compatibility fallback paths while Geometra closes remaining live-site gaps.
20
20
 
21
- Proxy-backed sessions stay warm by default on disconnect, and MCP now keeps a small warm pool so compatible headed, headless, and stealth workflows do not immediately evict each other.
21
+ Proxy-backed browser sessions are isolated by default: each `pageUrl`/HTTP(S) connect gets a fresh Chromium that is destroyed on disconnect, and follow-up tools must use the returned `sessionId`. Pass `isolated: false` only when you intentionally want sequential warm-browser reuse, including shared cookies, localStorage, and page state. A browser prepared with `geometra_prepare_browser` is consumed only by a connect that explicitly opts into that warm pool.
22
22
 
23
23
  ## Tools
24
24
 
25
25
  | Tool | Description |
26
26
  |---|---|
27
27
  | `geometra_connect` | Connect with `url` (ws://…) **or** `pageUrl` (https://…) to auto-start geometra-proxy; pass `browserMode: "stock" | "cloakbrowser"` for explicit authorized testing modes; can inline `formSchema` and/or `pageModel`, or defer the page model for a faster first connect response |
28
- | `geometra_prepare_browser` | Pre-launch and pre-navigate a reusable proxy/browser for `pageUrl` without creating an active session; supports the same headed/headless/stealth browser settings as `geometra_connect` |
28
+ | `geometra_prepare_browser` | Pre-launch and pre-navigate a reusable proxy/browser for `pageUrl` without creating an active session; the consuming connect must pass `isolated: false` explicitly |
29
29
  | `geometra_query` | Find elements by stable id, role, name, text content, prompt/section/item context, current value, or semantic state such as `invalid`, `required`, or `busy` |
30
30
  | `geometra_wait_for` | Wait for a semantic condition instead of guessing sleeps (`busy`, `disabled`, alerts, values, etc.). **Strict parameters** — use `text` plus `present: false` to wait until a substring disappears (e.g. “Parsing your resume”); there is no `textGone` field |
31
31
  | `geometra_wait_for_resume_parse` | Convenience wait until a parsing banner is gone (defaults: `text`: `"Parsing"`, `present` implied false). Same engine as `geometra_wait_for` |
@@ -221,12 +221,20 @@ To uninstall, remove the server entry from your client's MCP configuration.
221
221
  ### From source (this repo)
222
222
 
223
223
  ```bash
224
- cd mcp
225
224
  npm install
226
- npm run build
227
- claude mcp add geometra -- node ./dist/index.js
225
+ npm run build -w @geometra/proxy
226
+ npm run build -w @geometra/mcp
227
+ claude mcp add geometra -- node "$(pwd)/mcp/dist/index.js"
228
228
  ```
229
229
 
230
+ Install from the repository root so npm links `@geometra/mcp` and `@geometra/proxy` to the current checkout instead of placing an independent registry copy under `mcp/node_modules`.
231
+
232
+ ### Session state privacy
233
+
234
+ Session lifecycle state is process-memory-only by default and does not create a state file. To opt into restart-persistent state, set `GEOMETRA_MCP_STATE_FILE` to a database path inside a private directory (mode `0700`). Geometra rejects symlinked or insecure state targets and keeps the database and SQLite sidecars at mode `0600`.
235
+
236
+ Retained lifecycle and workflow state stores URL origins rather than paths or query strings, and redacts submitted field values, file paths, credentials, payloads, and raw error messages. Existing legacy state files are not migrated or deleted automatically.
237
+
230
238
  ## Usage
231
239
 
232
240
  ### Native Geometra server
@@ -250,6 +258,9 @@ python3 -m http.server 8080
250
258
  In another terminal (from repo root after `npm install` / `bun install` and `bun run build`):
251
259
 
252
260
  ```bash
261
+ export GEOMETRA_PROXY_AUTH_TOKEN="replace-with-a-random-32-character-or-longer-token"
262
+ # Required only if you want file-upload actions:
263
+ export GEOMETRA_PROXY_FILE_ROOTS="/absolute/path/to/approved-files"
253
264
  npx geometra-proxy http://localhost:8080 --port 3200
254
265
  # Requires Chromium: npx playwright install chromium
255
266
  # Optional authorized stealth-testing prefetch: npx cloakbrowser install
@@ -259,7 +270,18 @@ npx geometra-proxy http://localhost:8080 --port 3200
259
270
 
260
271
  For blocked/challenge pages, keep `blockDetection: true` (default). Set `blockedSitePolicy` to `"continue"` (default), `"manual-handoff"` to return retry guidance for a visible user handoff, or `"error"` to stop immediately with structured `blockedSite` details.
261
272
 
262
- Point MCP at `ws://127.0.0.1:3200` instead of a native Geometra server. The proxy translates clicks and keyboard messages into Playwright actions and streams updated geometry.
273
+ Point MCP at the authenticated loopback socket instead of a native Geometra server:
274
+
275
+ ```text
276
+ geometra_connect({
277
+ url: "ws://127.0.0.1:3200",
278
+ authToken: "<GEOMETRA_PROXY_AUTH_TOKEN>"
279
+ })
280
+ ```
281
+
282
+ When MCP starts the proxy from `pageUrl`, it generates and carries the token
283
+ privately, so no auth parameter is needed. The proxy translates clicks and
284
+ keyboard messages into Playwright actions and streams updated geometry.
263
285
 
264
286
  Then in Claude Code (either backend):
265
287
 
@@ -302,7 +324,7 @@ No screenshots and no vision model in the loop. JSON in, JSON out.
302
324
  With `python3 -m http.server 8080` in `demos/proxy-mcp-sample` and `npx geometra-proxy http://localhost:8080 --port 3200` running:
303
325
 
304
326
  ```
305
- Agent: geometra_connect({ url: "ws://127.0.0.1:3200" })
327
+ Agent: geometra_connect({ url: "ws://127.0.0.1:3200", authToken: "<configured token>" })
306
328
  → Connected. UI includes textbox "Email", button "Save", …
307
329
 
308
330
  Agent: geometra_form_schema({})
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { createServer } from './server.js';
4
- import { disconnect, listSessions } from './session.js';
4
+ import { shutdownAllSessionsAndProxies } from './session.js';
5
5
  import { shutdownSessionLifecycleRegistry } from './session-state.js';
6
6
  let cleanedUp = false;
7
7
  function cleanupActiveSession() {
@@ -9,10 +9,7 @@ function cleanupActiveSession() {
9
9
  return;
10
10
  cleanedUp = true;
11
11
  try {
12
- for (const session of listSessions()) {
13
- disconnect({ sessionId: session.id, closeProxy: false });
14
- }
15
- disconnect({ closeProxy: true });
12
+ shutdownAllSessionsAndProxies();
16
13
  }
17
14
  catch {
18
15
  /* ignore */
@@ -1,6 +1,7 @@
1
1
  import { type ChildProcess } from 'node:child_process';
2
2
  export interface EmbeddedProxyRuntime {
3
3
  wsUrl: string;
4
+ authToken: string;
4
5
  ready: Promise<void>;
5
6
  getTrace?: () => Record<string, unknown>;
6
7
  closed: boolean;
@@ -33,6 +34,24 @@ export interface SpawnProxyParams {
33
34
  eagerInitialExtract?: boolean;
34
35
  proxy?: SpawnProxyConfig;
35
36
  }
37
+ export interface RuntimeDrainOptions {
38
+ /** Override the environment-derived debug decision, primarily for embedders and tests. */
39
+ debug?: boolean;
40
+ /** Receive already-prefixed proxy stderr chunks when debug forwarding is enabled. */
41
+ forwardStderr?: (text: string) => void;
42
+ }
43
+ export interface SpawnedProxyReadyOptions {
44
+ /** Override the production ready deadline, primarily for focused process tests. */
45
+ readyTimeoutMs?: number;
46
+ /** Override post-ready output handling, primarily for embedders and tests. */
47
+ runtimeDrains?: RuntimeDrainOptions;
48
+ }
49
+ /**
50
+ * Consume a spawned proxy's output after its ready signal so full stdio pipes
51
+ * cannot block the child. Runtime logs remain quiet unless debug forwarding is
52
+ * explicitly enabled.
53
+ */
54
+ export declare function attachRuntimeDrains(child: Pick<ChildProcess, 'stdout' | 'stderr'>, options?: RuntimeDrainOptions): void;
36
55
  export declare function resolveStealthMode(stealth?: boolean): boolean;
37
56
  export declare function startEmbeddedGeometraProxy(opts: SpawnProxyParams): Promise<{
38
57
  runtime: EmbeddedProxyRuntime;
@@ -48,4 +67,14 @@ export declare function formatProxyStartupFailure(message: string, opts: SpawnPr
48
67
  export declare function spawnGeometraProxy(opts: SpawnProxyParams): Promise<{
49
68
  child: ChildProcess;
50
69
  wsUrl: string;
70
+ authToken: string;
71
+ }>;
72
+ /**
73
+ * Wait for a spawned proxy's structured ready signal, then hand both output
74
+ * pipes to their lifetime drains before returning the child to its caller.
75
+ */
76
+ export declare function waitForSpawnedProxyReady(child: ChildProcess, opts: SpawnProxyParams, authToken: string, options?: SpawnedProxyReadyOptions): Promise<{
77
+ child: ChildProcess;
78
+ wsUrl: string;
79
+ authToken: string;
51
80
  }>;
@@ -1,4 +1,5 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
2
3
  import { existsSync, realpathSync, rmSync } from 'node:fs';
3
4
  import { createRequire } from 'node:module';
4
5
  import path from 'node:path';
@@ -126,6 +127,24 @@ function buildLocalProxyDistIfPossible(packageDir, entryFile, errors) {
126
127
  }
127
128
  return undefined;
128
129
  }
130
+ /**
131
+ * Consume a spawned proxy's output after its ready signal so full stdio pipes
132
+ * cannot block the child. Runtime logs remain quiet unless debug forwarding is
133
+ * explicitly enabled.
134
+ */
135
+ export function attachRuntimeDrains(child, options = {}) {
136
+ child.stdout?.resume();
137
+ const debug = options.debug ?? process.env.GEOMETRA_PROXY_DEBUG === '1';
138
+ if (!debug) {
139
+ child.stderr?.resume();
140
+ return;
141
+ }
142
+ const forwardStderr = options.forwardStderr ?? ((text) => { process.stderr.write(text); });
143
+ child.stderr?.on('data', chunk => {
144
+ const text = chunk.toString();
145
+ forwardStderr(/^\[geometra-proxy\](?:\s|$)/.test(text) ? text : `[geometra-proxy] ${text}`);
146
+ });
147
+ }
129
148
  export function resolveStealthMode(stealth) {
130
149
  if (stealth !== undefined)
131
150
  return stealth;
@@ -165,6 +184,10 @@ export async function startEmbeddedGeometraProxy(opts) {
165
184
  eagerInitialExtract: opts.eagerInitialExtract,
166
185
  ...(opts.proxy && { proxy: opts.proxy }),
167
186
  });
187
+ if (typeof runtime.authToken !== 'string' || runtime.authToken.length < 32) {
188
+ await runtime.close().catch(() => { });
189
+ throw new Error(`Resolved ${runtimePath}, but that proxy runtime does not implement authenticated controller capabilities. Rebuild or update @geometra/proxy.`);
190
+ }
168
191
  return { runtime, wsUrl: runtime.wsUrl };
169
192
  }
170
193
  export function parseProxyReadySignalLine(line, options) {
@@ -209,6 +232,7 @@ export function formatProxyStartupFailure(message, opts) {
209
232
  */
210
233
  export function spawnGeometraProxy(opts) {
211
234
  const script = resolveProxyScriptPath();
235
+ const authToken = randomBytes(32).toString('base64url');
212
236
  const args = [script, opts.pageUrl, '--port', String(opts.port)];
213
237
  if (opts.width != null && opts.width > 0)
214
238
  args.push('--width', String(opts.width));
@@ -226,27 +250,34 @@ export function spawnGeometraProxy(opts) {
226
250
  args.push('--no-stealth');
227
251
  if (opts.eagerInitialExtract === false)
228
252
  args.push('--lazy-initial-extract');
229
- if (opts.proxy?.server) {
230
- args.push('--proxy-server', opts.proxy.server);
231
- if (opts.proxy.username !== undefined)
232
- args.push('--proxy-username', opts.proxy.username);
233
- if (opts.proxy.password !== undefined)
234
- args.push('--proxy-password', opts.proxy.password);
235
- if (opts.proxy.bypass !== undefined)
236
- args.push('--proxy-bypass', opts.proxy.bypass);
237
- }
253
+ const childEnv = {
254
+ ...process.env,
255
+ GEOMETRA_PROXY_READY_JSON: '1',
256
+ GEOMETRA_PROXY_AUTH_TOKEN: authToken,
257
+ ...(opts.proxy?.server !== undefined ? { GEOMETRA_PROXY_SERVER: opts.proxy.server } : {}),
258
+ ...(opts.proxy?.username !== undefined ? { GEOMETRA_PROXY_USERNAME: opts.proxy.username } : {}),
259
+ ...(opts.proxy?.password !== undefined ? { GEOMETRA_PROXY_PASSWORD: opts.proxy.password } : {}),
260
+ ...(opts.proxy?.bypass !== undefined ? { GEOMETRA_PROXY_BYPASS: opts.proxy.bypass } : {}),
261
+ };
262
+ const child = spawn(process.execPath, args, {
263
+ stdio: ['ignore', 'pipe', 'pipe'],
264
+ env: childEnv,
265
+ });
266
+ return waitForSpawnedProxyReady(child, opts, authToken);
267
+ }
268
+ /**
269
+ * Wait for a spawned proxy's structured ready signal, then hand both output
270
+ * pipes to their lifetime drains before returning the child to its caller.
271
+ */
272
+ export function waitForSpawnedProxyReady(child, opts, authToken, options = {}) {
238
273
  return new Promise((resolve, reject) => {
239
- const child = spawn(process.execPath, args, {
240
- stdio: ['ignore', 'pipe', 'pipe'],
241
- env: { ...process.env, GEOMETRA_PROXY_READY_JSON: '1' },
242
- });
243
274
  let settled = false;
244
275
  let stdoutBuf = '';
245
276
  let stderrBuf = '';
246
277
  const cleanup = () => {
247
278
  clearTimeout(deadline);
248
- child.stdout?.removeAllListeners('data');
249
- child.stderr?.removeAllListeners('data');
279
+ child.stdout?.off('data', consumeStdout);
280
+ child.stderr?.off('data', consumeStderr);
250
281
  };
251
282
  const tryResolveReady = (line) => {
252
283
  const wsUrl = parseProxyReadySignalLine(line, { allowLegacy: false });
@@ -254,7 +285,8 @@ export function spawnGeometraProxy(opts) {
254
285
  return false;
255
286
  settled = true;
256
287
  cleanup();
257
- resolve({ child, wsUrl });
288
+ attachRuntimeDrains(child, options.runtimeDrains);
289
+ resolve({ child, wsUrl, authToken });
258
290
  return true;
259
291
  };
260
292
  const consumeStdout = (chunk) => {
@@ -282,7 +314,7 @@ export function spawnGeometraProxy(opts) {
282
314
  cleanup();
283
315
  reject(new Error(formatProxyStartupFailure('geometra-proxy did not emit a ready signal within 45s', opts)));
284
316
  }
285
- }, READY_TIMEOUT_MS);
317
+ }, options.readyTimeoutMs ?? READY_TIMEOUT_MS);
286
318
  child.stdout?.on('data', consumeStdout);
287
319
  child.stderr?.on('data', consumeStderr);
288
320
  child.on('error', err => {