@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.
- package/dist/__tests__/proxy-session-recovery.test.d.ts +1 -0
- package/dist/__tests__/proxy-session-recovery.test.js +118 -0
- package/dist/proxy-spawn.d.ts +11 -0
- package/dist/proxy-spawn.js +46 -19
- package/dist/server.js +1 -1
- package/dist/session.d.ts +2 -0
- package/dist/session.js +173 -76
- package/package.json +2 -2
|
@@ -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
|
+
});
|
package/dist/proxy-spawn.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/proxy-spawn.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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.
|
|
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.
|
|
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"
|