@geometra/mcp 1.64.0 → 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 +29 -7
- package/dist/index.js +2 -5
- package/dist/proxy-spawn.d.ts +29 -0
- package/dist/proxy-spawn.js +49 -17
- package/dist/server.js +849 -219
- package/dist/session-state.js +262 -80
- package/dist/session.d.ts +93 -13
- package/dist/session.js +1365 -321
- package/dist/state-privacy.d.ts +23 -0
- package/dist/state-privacy.js +171 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +32 -0
- package/package.json +4 -4
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
|
|
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;
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
13
|
-
disconnect({ sessionId: session.id, closeProxy: false });
|
|
14
|
-
}
|
|
15
|
-
disconnect({ closeProxy: true });
|
|
12
|
+
shutdownAllSessionsAndProxies();
|
|
16
13
|
}
|
|
17
14
|
catch {
|
|
18
15
|
/* ignore */
|
package/dist/proxy-spawn.d.ts
CHANGED
|
@@ -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
|
}>;
|
package/dist/proxy-spawn.js
CHANGED
|
@@ -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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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?.
|
|
249
|
-
child.stderr?.
|
|
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
|
-
|
|
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 => {
|