@remcp/runtime 0.2.68 → 0.2.70
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/package.json +1 -1
- package/src/extended/browser.mjs +79 -1
- package/src/extended/catalog.mjs +2 -2
package/package.json
CHANGED
package/src/extended/browser.mjs
CHANGED
|
@@ -1,10 +1,86 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
5
|
import { resolveSafePath } from '../util.mjs';
|
|
3
|
-
import { clamp, jsonResult, optionalString, requireEnum, unavailable } from './common.mjs';
|
|
6
|
+
import { clamp, commandExists, jsonResult, optionalString, requireEnum, spawnDetached, unavailable } from './common.mjs';
|
|
4
7
|
|
|
5
8
|
const DEFAULT_ENDPOINT = 'http://127.0.0.1:9222';
|
|
6
9
|
const MAX_DISCOVERY_BYTES = 8 * 1024 * 1024;
|
|
7
10
|
|
|
11
|
+
let browserLaunchPromise = null;
|
|
12
|
+
|
|
13
|
+
function browserExecutable() {
|
|
14
|
+
const configured = optionalString(process.env.REMCP_BROWSER_BINARY);
|
|
15
|
+
const candidates = configured ? [configured] : process.platform === 'darwin'
|
|
16
|
+
? [
|
|
17
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
18
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
19
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
20
|
+
]
|
|
21
|
+
: process.platform === 'win32'
|
|
22
|
+
? [
|
|
23
|
+
path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
24
|
+
path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
25
|
+
path.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
26
|
+
path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
27
|
+
]
|
|
28
|
+
: ['/opt/google/chrome/chrome','/usr/bin/google-chrome','/usr/bin/google-chrome-stable','chromium','chromium-browser','microsoft-edge','microsoft-edge-stable'];
|
|
29
|
+
return candidates.find(candidate => path.isAbsolute(candidate) ? existsSync(candidate) : commandExists(candidate)) || null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function browserAutoLaunchAvailable() {
|
|
33
|
+
return Boolean(browserExecutable());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function browserDataDir() {
|
|
37
|
+
const configured = optionalString(process.env.REMCP_BROWSER_DATA_DIR);
|
|
38
|
+
if (configured) return configured;
|
|
39
|
+
if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Application Support', 'ReMCP', 'browser-cdp');
|
|
40
|
+
if (process.platform === 'win32') return path.join(process.env.LOCALAPPDATA || os.homedir(), 'ReMCP', 'browser-cdp');
|
|
41
|
+
return path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'remcp', 'browser-cdp');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function waitForBrowserEndpoint(timeoutMs = 5000) {
|
|
45
|
+
const deadline = Date.now() + timeoutMs;
|
|
46
|
+
do {
|
|
47
|
+
if (await browserCapabilityAvailable(undefined, 250)) return true;
|
|
48
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
49
|
+
} while (Date.now() < deadline);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function ensureBrowserEndpoint(endpoint) {
|
|
54
|
+
if (endpoint || process.env.REMCP_CDP_URL) return;
|
|
55
|
+
if (await browserCapabilityAvailable(undefined, 250)) return;
|
|
56
|
+
if (!browserLaunchPromise) {
|
|
57
|
+
browserLaunchPromise = (async () => {
|
|
58
|
+
const executable = browserExecutable();
|
|
59
|
+
if (!executable) throw new Error('Browser control is unavailable: no supported Chrome, Edge, or Chromium executable was found');
|
|
60
|
+
const profile = browserDataDir();
|
|
61
|
+
mkdirSync(profile, { recursive:true, mode:0o700 });
|
|
62
|
+
const args = [
|
|
63
|
+
'--remote-debugging-address=127.0.0.1',
|
|
64
|
+
'--remote-debugging-port=9222',
|
|
65
|
+
`--user-data-dir=${profile}`,
|
|
66
|
+
'--no-first-run',
|
|
67
|
+
'--no-default-browser-check',
|
|
68
|
+
'about:blank',
|
|
69
|
+
];
|
|
70
|
+
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) args.unshift('--headless=new');
|
|
71
|
+
spawnDetached(executable, args);
|
|
72
|
+
if (!await waitForBrowserEndpoint(5000)) throw new Error('ReMCP launched a browser but its local CDP endpoint did not become ready');
|
|
73
|
+
})().finally(() => { browserLaunchPromise = null; });
|
|
74
|
+
}
|
|
75
|
+
await browserLaunchPromise;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function browserControlAvailable(endpoint, timeoutMs = 500) {
|
|
79
|
+
if (await browserCapabilityAvailable(endpoint, timeoutMs)) return true;
|
|
80
|
+
if (endpoint || process.env.REMCP_CDP_URL) return false;
|
|
81
|
+
return browserAutoLaunchAvailable();
|
|
82
|
+
}
|
|
83
|
+
|
|
8
84
|
function boundedText(value, max = 1000) {
|
|
9
85
|
const text = String(value ?? '');
|
|
10
86
|
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
@@ -66,6 +142,7 @@ export async function browserCapabilityAvailable(endpoint, timeoutMs = 500) {
|
|
|
66
142
|
}
|
|
67
143
|
|
|
68
144
|
export async function listBrowserTargets(endpoint) {
|
|
145
|
+
await ensureBrowserEndpoint(endpoint);
|
|
69
146
|
const list = await requestJson('/json/list', endpoint);
|
|
70
147
|
return Array.isArray(list) ? list : [];
|
|
71
148
|
}
|
|
@@ -242,6 +319,7 @@ export async function browserNavigate(args = {}) {
|
|
|
242
319
|
const action = requireEnum(args.action || (url ? 'url' : 'reload'), 'action', ['url','new_tab','back','forward','reload']);
|
|
243
320
|
if (['url','new_tab'].includes(action) && !url) throw new Error('url is required when action=url or new_tab');
|
|
244
321
|
if (action === 'new_tab') {
|
|
322
|
+
await ensureBrowserEndpoint(args.endpoint);
|
|
245
323
|
const created = await requestJson(`/json/new?${encodeURIComponent(url)}`, args.endpoint, { method:'PUT', timeout_ms:args.timeout_ms });
|
|
246
324
|
return jsonResult({
|
|
247
325
|
target_id:created?.id || null,
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -4,7 +4,7 @@ const JSON_OUTPUT_SCHEMA = {
|
|
|
4
4
|
additionalProperties: true,
|
|
5
5
|
description: 'Structured tool result. Array/scalar payloads are exposed under data for compatibility with MCP clients that require object-root output schemas.',
|
|
6
6
|
};
|
|
7
|
-
import {
|
|
7
|
+
import { browserControlAvailable, browserHandlers } from './browser.mjs';
|
|
8
8
|
import { isWaylandSession } from '../screenshot-portal.mjs';
|
|
9
9
|
import { waylandPortalCandidate } from '../wayland-remote-desktop.mjs';
|
|
10
10
|
import { commandExists, runFile } from './common.mjs';
|
|
@@ -606,7 +606,7 @@ export async function capabilitySnapshot() {
|
|
|
606
606
|
const wayland = linux && isWaylandSession();
|
|
607
607
|
const portalInput = wayland && waylandPortalCandidate();
|
|
608
608
|
const x11Input = linux && !wayland && (commandExists('wdotool') || commandExists('xdotool'));
|
|
609
|
-
const browserCdp = await
|
|
609
|
+
const browserCdp = await browserControlAvailable(undefined, 500);
|
|
610
610
|
return {
|
|
611
611
|
windows: win || mac || commandExists('wdotool') || commandExists('wmctrl') || (wayland && pyAtSpi),
|
|
612
612
|
ui: win || mac || pyAtSpi,
|