@browserbridge/bbx 1.0.1 → 1.1.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 +4 -4
- package/package.json +11 -13
- package/packages/agent-client/src/cli-helpers.js +33 -0
- package/packages/agent-client/src/cli.js +116 -41
- package/packages/agent-client/src/client.js +29 -4
- package/packages/agent-client/src/command-registry.js +3 -0
- package/packages/agent-client/src/detect.js +159 -48
- package/packages/agent-client/src/install.js +24 -1
- package/packages/agent-client/src/mcp-config.js +29 -10
- package/packages/agent-client/src/setup-status.js +12 -4
- package/packages/mcp-server/src/bin.js +57 -5
- package/packages/mcp-server/src/handlers.js +28 -7
- package/packages/mcp-server/src/server.js +12 -2
- package/packages/native-host/bin/bridge-daemon.js +33 -4
- package/packages/native-host/bin/install-manifest.js +24 -2
- package/packages/native-host/src/config.js +131 -6
- package/packages/native-host/src/daemon-process.js +396 -0
- package/packages/native-host/src/daemon.js +217 -68
- package/packages/native-host/src/framing.js +131 -11
- package/packages/native-host/src/install-manifest.js +121 -7
- package/packages/native-host/src/native-host.js +110 -73
- package/packages/protocol/src/capabilities.js +3 -0
- package/packages/protocol/src/defaults.js +1 -0
- package/packages/protocol/src/errors.js +4 -0
- package/packages/protocol/src/payload-cost.js +19 -6
- package/packages/protocol/src/protocol.js +143 -7
- package/packages/protocol/src/registry.js +11 -0
- package/packages/protocol/src/summary.js +18 -10
- package/packages/protocol/src/types.js +28 -3
- package/skills/browser-bridge/SKILL.md +2 -1
- package/skills/browser-bridge/references/interaction.md +1 -0
- package/skills/browser-bridge/references/protocol.md +2 -1
- package/CHANGELOG.md +0 -55
- package/assets/banner.jpg +0 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +0 -65
- package/docs/api-reference.md +0 -157
- package/docs/cli-guide.md +0 -128
- package/docs/index.md +0 -25
- package/docs/manual-setup.md +0 -140
- package/docs/mcp-vs-cli.md +0 -258
- package/docs/publishing.md +0 -112
- package/docs/quickstart.md +0 -104
- package/docs/troubleshooting.md +0 -59
- package/docs/unpacked-extension.md +0 -72
- package/docs/usage-scenarios.md +0 -136
- package/manifest.json +0 -38
- package/packages/extension/assets/icon-128.png +0 -0
- package/packages/extension/assets/icon-16.png +0 -0
- package/packages/extension/assets/icon-32.png +0 -0
- package/packages/extension/assets/icon-48.png +0 -0
- package/packages/extension/src/background-helpers.js +0 -474
- package/packages/extension/src/background-routing.js +0 -89
- package/packages/extension/src/background.js +0 -3490
- package/packages/extension/src/content-script-helpers.js +0 -282
- package/packages/extension/src/content-script.js +0 -2043
- package/packages/extension/src/debugger-coordinator.js +0 -188
- package/packages/extension/src/sidepanel-helpers.js +0 -104
- package/packages/extension/ui/popup.html +0 -35
- package/packages/extension/ui/popup.js +0 -298
- package/packages/extension/ui/sidepanel.html +0 -102
- package/packages/extension/ui/sidepanel.js +0 -1771
- package/packages/extension/ui/ui.css +0 -1160
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
4
3
|
import fs from 'node:fs';
|
|
5
4
|
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
|
|
8
7
|
/** @typedef {import('./mcp-config.js').McpClientName} McpClientName */
|
|
9
8
|
/** @typedef {import('./install.js').SupportedTarget} SupportedTarget */
|
|
9
|
+
/** @typedef {() => boolean | Promise<boolean>} Detector */
|
|
10
10
|
|
|
11
11
|
const home = os.homedir();
|
|
12
12
|
const platform = process.platform;
|
|
13
|
+
const WINDOWS_EXECUTABLE_EXTENSIONS = new Set(
|
|
14
|
+
(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
15
|
+
.split(';')
|
|
16
|
+
.map((extension) => extension.trim().toLowerCase())
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
);
|
|
19
|
+
const DEFAULT_COMMAND_NAMES =
|
|
20
|
+
platform === 'darwin'
|
|
21
|
+
? ['codex', 'claude', 'opencode', 'agy']
|
|
22
|
+
: platform === 'linux'
|
|
23
|
+
? ['codex', 'claude', 'cursor', 'code', 'opencode', 'agy', 'windsurf']
|
|
24
|
+
: ['codex', 'claude', 'cursor', 'opencode', 'agy', 'windsurf'];
|
|
25
|
+
|
|
26
|
+
const PATH_DELIMITER = platform === 'win32' ? ';' : ':';
|
|
27
|
+
|
|
28
|
+
/** @type {Promise<Set<string>> | null} */
|
|
29
|
+
let availableCommandsPromise = null;
|
|
13
30
|
|
|
14
31
|
/**
|
|
15
32
|
* @returns {string}
|
|
@@ -26,12 +43,12 @@ function getVsCodeUserDataDir() {
|
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
/**
|
|
29
|
-
* @param {string}
|
|
30
|
-
* @returns {boolean}
|
|
46
|
+
* @param {string} targetPath
|
|
47
|
+
* @returns {Promise<boolean>}
|
|
31
48
|
*/
|
|
32
|
-
function fsExists(
|
|
49
|
+
async function fsExists(targetPath) {
|
|
33
50
|
try {
|
|
34
|
-
fs.
|
|
51
|
+
await fs.promises.access(targetPath, fs.constants.F_OK);
|
|
35
52
|
return true;
|
|
36
53
|
} catch {
|
|
37
54
|
return false;
|
|
@@ -39,24 +56,102 @@ function fsExists(p) {
|
|
|
39
56
|
}
|
|
40
57
|
|
|
41
58
|
/**
|
|
42
|
-
* @param {string}
|
|
43
|
-
* @returns {
|
|
59
|
+
* @param {string} command
|
|
60
|
+
* @returns {string}
|
|
44
61
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
function normalizeCommandName(command) {
|
|
63
|
+
return platform === 'win32' ? command.toLowerCase() : command;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {string} entryName
|
|
68
|
+
* @returns {string | null}
|
|
69
|
+
*/
|
|
70
|
+
function getCommandNameFromPathEntry(entryName) {
|
|
71
|
+
if (platform !== 'win32') {
|
|
72
|
+
return entryName;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const extension = path.extname(entryName).toLowerCase();
|
|
76
|
+
if (!WINDOWS_EXECUTABLE_EXTENSIONS.has(extension)) {
|
|
77
|
+
return null;
|
|
53
78
|
}
|
|
79
|
+
return entryName.slice(0, -extension.length).toLowerCase();
|
|
54
80
|
}
|
|
55
81
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
/**
|
|
83
|
+
* @param {readonly string[]} commands
|
|
84
|
+
* @returns {Promise<Set<string>>}
|
|
85
|
+
*/
|
|
86
|
+
async function resolveAvailableCommands(commands) {
|
|
87
|
+
const resolved = new Set();
|
|
88
|
+
const unresolved = new Set(commands.map((command) => normalizeCommandName(command)));
|
|
89
|
+
const pathEntries = (process.env.PATH || '').split(PATH_DELIMITER).filter(Boolean);
|
|
90
|
+
const executeAccessMode = platform === 'win32' ? fs.constants.F_OK : fs.constants.X_OK;
|
|
91
|
+
|
|
92
|
+
for (const directory of pathEntries) {
|
|
93
|
+
if (unresolved.size === 0) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = await fs.promises.readdir(directory);
|
|
100
|
+
} catch {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const matches = await Promise.all(
|
|
105
|
+
entries.map(async (entryName) => {
|
|
106
|
+
const commandName = getCommandNameFromPathEntry(entryName);
|
|
107
|
+
if (!commandName || !unresolved.has(commandName)) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await fs.promises.access(path.join(directory, entryName), executeAccessMode);
|
|
113
|
+
return commandName;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
for (const commandName of matches) {
|
|
121
|
+
if (!commandName) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
unresolved.delete(commandName);
|
|
125
|
+
resolved.add(commandName);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return resolved;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @returns {Promise<Set<string>>}
|
|
134
|
+
*/
|
|
135
|
+
function getAvailableCommands() {
|
|
136
|
+
if (!availableCommandsPromise) {
|
|
137
|
+
availableCommandsPromise = resolveAvailableCommands(DEFAULT_COMMAND_NAMES);
|
|
138
|
+
}
|
|
139
|
+
return availableCommandsPromise;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @param {string} command
|
|
144
|
+
* @returns {Promise<boolean>}
|
|
145
|
+
*/
|
|
146
|
+
async function commandExists(command) {
|
|
147
|
+
const availableCommands = await getAvailableCommands();
|
|
148
|
+
return availableCommands.has(normalizeCommandName(command));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @returns {Promise<boolean>} */
|
|
152
|
+
async function detectCopilot() {
|
|
153
|
+
if (await fsExists(path.join(getVsCodeUserDataDir(), 'User'))) return true;
|
|
154
|
+
if (await fsExists(path.join(home, '.vscode'))) return true;
|
|
60
155
|
if (platform === 'darwin') return fsExists('/Applications/Visual Studio Code.app');
|
|
61
156
|
if (platform === 'win32') {
|
|
62
157
|
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
@@ -65,47 +160,47 @@ function detectCopilot() {
|
|
|
65
160
|
return commandExists('code');
|
|
66
161
|
}
|
|
67
162
|
|
|
68
|
-
/** @returns {boolean} */
|
|
69
|
-
function detectCursor() {
|
|
70
|
-
if (fsExists(path.join(home, '.cursor'))) return true;
|
|
163
|
+
/** @returns {Promise<boolean>} */
|
|
164
|
+
async function detectCursor() {
|
|
165
|
+
if (await fsExists(path.join(home, '.cursor'))) return true;
|
|
71
166
|
if (platform === 'darwin') return fsExists('/Applications/Cursor.app');
|
|
72
167
|
return commandExists('cursor');
|
|
73
168
|
}
|
|
74
169
|
|
|
75
|
-
/** @returns {boolean} */
|
|
76
|
-
function detectWindsurf() {
|
|
77
|
-
if (fsExists(path.join(home, '.codeium', 'windsurf'))) return true;
|
|
170
|
+
/** @returns {Promise<boolean>} */
|
|
171
|
+
async function detectWindsurf() {
|
|
172
|
+
if (await fsExists(path.join(home, '.codeium', 'windsurf'))) return true;
|
|
78
173
|
if (platform === 'darwin') return fsExists('/Applications/Windsurf.app');
|
|
79
174
|
return commandExists('windsurf');
|
|
80
175
|
}
|
|
81
176
|
|
|
82
|
-
/** @returns {boolean} */
|
|
83
|
-
function detectClaude() {
|
|
84
|
-
if (fsExists(path.join(home, '.claude'))) return true;
|
|
85
|
-
if (fsExists(path.join(home, '.claude.json'))) return true;
|
|
177
|
+
/** @returns {Promise<boolean>} */
|
|
178
|
+
async function detectClaude() {
|
|
179
|
+
if (await fsExists(path.join(home, '.claude'))) return true;
|
|
180
|
+
if (await fsExists(path.join(home, '.claude.json'))) return true;
|
|
86
181
|
return commandExists('claude');
|
|
87
182
|
}
|
|
88
183
|
|
|
89
|
-
/** @returns {boolean} */
|
|
90
|
-
function detectCodex() {
|
|
91
|
-
if (fsExists(path.join(home, '.codex'))) return true;
|
|
184
|
+
/** @returns {Promise<boolean>} */
|
|
185
|
+
async function detectCodex() {
|
|
186
|
+
if (await fsExists(path.join(home, '.codex'))) return true;
|
|
92
187
|
return commandExists('codex');
|
|
93
188
|
}
|
|
94
189
|
|
|
95
|
-
/** @returns {boolean} */
|
|
96
|
-
function detectOpencode() {
|
|
97
|
-
if (fsExists(path.join(home, '.config', 'opencode'))) return true;
|
|
98
|
-
if (fsExists(path.join(home, '.opencode'))) return true;
|
|
190
|
+
/** @returns {Promise<boolean>} */
|
|
191
|
+
async function detectOpencode() {
|
|
192
|
+
if (await fsExists(path.join(home, '.config', 'opencode'))) return true;
|
|
193
|
+
if (await fsExists(path.join(home, '.opencode'))) return true;
|
|
99
194
|
return commandExists('opencode');
|
|
100
195
|
}
|
|
101
196
|
|
|
102
|
-
/** @returns {boolean} */
|
|
103
|
-
function detectAntigravity() {
|
|
104
|
-
if (fsExists(path.join(home, '.gemini', 'antigravity'))) return true;
|
|
197
|
+
/** @returns {Promise<boolean>} */
|
|
198
|
+
async function detectAntigravity() {
|
|
199
|
+
if (await fsExists(path.join(home, '.gemini', 'antigravity'))) return true;
|
|
105
200
|
return commandExists('agy');
|
|
106
201
|
}
|
|
107
202
|
|
|
108
|
-
/** @type {Record<string,
|
|
203
|
+
/** @type {Record<string, Detector>} */
|
|
109
204
|
const DETECTORS = {
|
|
110
205
|
codex: detectCodex,
|
|
111
206
|
claude: detectClaude,
|
|
@@ -138,26 +233,42 @@ const SKILL_TARGET_KEYS = [
|
|
|
138
233
|
'windsurf',
|
|
139
234
|
];
|
|
140
235
|
|
|
236
|
+
/**
|
|
237
|
+
* @template {string} T
|
|
238
|
+
* @param {readonly T[]} keys
|
|
239
|
+
* @param {Record<string, Detector>} detectors
|
|
240
|
+
* @returns {Promise<T[]>}
|
|
241
|
+
*/
|
|
242
|
+
async function detectTargets(keys, detectors) {
|
|
243
|
+
const detectionResults = await Promise.all(
|
|
244
|
+
keys.map(async (name) => ({
|
|
245
|
+
name,
|
|
246
|
+
detected: await (detectors[name]?.() ?? false),
|
|
247
|
+
}))
|
|
248
|
+
);
|
|
249
|
+
return detectionResults.filter((entry) => entry.detected).map((entry) => entry.name);
|
|
250
|
+
}
|
|
251
|
+
|
|
141
252
|
/**
|
|
142
253
|
* Detect which MCP clients are installed on this machine.
|
|
143
254
|
*
|
|
144
|
-
* @param {Record<string,
|
|
145
|
-
* @returns {McpClientName[]}
|
|
255
|
+
* @param {Record<string, Detector>} [detectors=DETECTORS]
|
|
256
|
+
* @returns {Promise<McpClientName[]>}
|
|
146
257
|
*/
|
|
147
|
-
export function detectMcpClients(detectors = DETECTORS) {
|
|
148
|
-
return MCP_CLIENT_KEYS
|
|
258
|
+
export async function detectMcpClients(detectors = DETECTORS) {
|
|
259
|
+
return detectTargets(MCP_CLIENT_KEYS, detectors);
|
|
149
260
|
}
|
|
150
261
|
|
|
151
262
|
/**
|
|
152
263
|
* Detect which skill targets are installed on this machine.
|
|
153
264
|
* Always includes 'agents' as a generic fallback.
|
|
154
265
|
*
|
|
155
|
-
* @param {Record<string,
|
|
156
|
-
* @returns {SupportedTarget[]}
|
|
266
|
+
* @param {Record<string, Detector>} [detectors=DETECTORS]
|
|
267
|
+
* @returns {Promise<SupportedTarget[]>}
|
|
157
268
|
*/
|
|
158
|
-
export function detectSkillTargets(detectors = DETECTORS) {
|
|
269
|
+
export async function detectSkillTargets(detectors = DETECTORS) {
|
|
159
270
|
/** @type {SupportedTarget[]} */
|
|
160
|
-
const detected = SKILL_TARGET_KEYS
|
|
271
|
+
const detected = await detectTargets(SKILL_TARGET_KEYS, detectors);
|
|
161
272
|
detected.push('agents');
|
|
162
273
|
return detected;
|
|
163
274
|
}
|
|
@@ -157,6 +157,8 @@ export function parseInstallAgentArgs(args, cwd = process.cwd()) {
|
|
|
157
157
|
export async function installAgentFiles(options) {
|
|
158
158
|
/** @type {string[]} */
|
|
159
159
|
const created = [];
|
|
160
|
+
/** @type {Array<{ path: string, existedBefore: boolean }>} */
|
|
161
|
+
const attempted = [];
|
|
160
162
|
/** @type {Set<string>} */
|
|
161
163
|
const seenTargets = new Set();
|
|
162
164
|
|
|
@@ -168,7 +170,14 @@ export async function installAgentFiles(options) {
|
|
|
168
170
|
continue;
|
|
169
171
|
}
|
|
170
172
|
seenTargets.add(skillTargetDir);
|
|
171
|
-
await
|
|
173
|
+
const existedBefore = await pathExists(skillTargetDir);
|
|
174
|
+
attempted.push({ path: skillTargetDir, existedBefore });
|
|
175
|
+
try {
|
|
176
|
+
await installManagedSkill(skillName, target, skillTargetDir);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
await rollbackInstalledSkillDirs(attempted);
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
172
181
|
created.push(skillTargetDir);
|
|
173
182
|
}
|
|
174
183
|
}
|
|
@@ -176,6 +185,20 @@ export async function installAgentFiles(options) {
|
|
|
176
185
|
return created;
|
|
177
186
|
}
|
|
178
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Remove any newly created managed skill directories after a failed install so a
|
|
190
|
+
* later retry starts cleanly. Pre-existing directories are left untouched.
|
|
191
|
+
*
|
|
192
|
+
* @param {Array<{ path: string, existedBefore: boolean }>} attempted
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
*/
|
|
195
|
+
async function rollbackInstalledSkillDirs(attempted) {
|
|
196
|
+
const rollbackPaths = attempted
|
|
197
|
+
.filter((entry) => !entry.existedBefore)
|
|
198
|
+
.map((entry) => fs.promises.rm(entry.path, { recursive: true, force: true }));
|
|
199
|
+
await Promise.allSettled(rollbackPaths);
|
|
200
|
+
}
|
|
201
|
+
|
|
179
202
|
/**
|
|
180
203
|
* Write MCP config for the given clients.
|
|
181
204
|
*
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* @typedef {'codex' | 'claude' | 'cursor' | 'copilot' | 'opencode' | 'antigravity' | 'windsurf' | 'agents'} McpClientName
|
|
@@ -41,6 +42,8 @@ export function isMcpClientName(value) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
const BROWSER_BRIDGE_SERVER_NAME = 'browser-bridge';
|
|
45
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
46
|
+
const mcpServerBinPath = path.join(packageRoot, 'packages', 'mcp-server', 'src', 'bin.js');
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
49
|
* @returns {string}
|
|
@@ -93,17 +96,29 @@ export function getMcpConfigShape(clientName) {
|
|
|
93
96
|
* }}
|
|
94
97
|
*/
|
|
95
98
|
function createBaseServerConfig(clientName) {
|
|
99
|
+
const windowsCommand =
|
|
100
|
+
process.platform === 'win32'
|
|
101
|
+
? {
|
|
102
|
+
command: process.execPath,
|
|
103
|
+
args: [mcpServerBinPath],
|
|
104
|
+
env: {},
|
|
105
|
+
}
|
|
106
|
+
: {
|
|
107
|
+
command: 'bbx',
|
|
108
|
+
args: ['mcp', 'serve'],
|
|
109
|
+
env: {},
|
|
110
|
+
};
|
|
111
|
+
|
|
96
112
|
if (clientName === 'opencode') {
|
|
97
113
|
return {
|
|
98
114
|
type: 'local',
|
|
99
|
-
command:
|
|
115
|
+
command:
|
|
116
|
+
process.platform === 'win32'
|
|
117
|
+
? [process.execPath, mcpServerBinPath]
|
|
118
|
+
: ['bbx', 'mcp', 'serve'],
|
|
100
119
|
};
|
|
101
120
|
}
|
|
102
|
-
return
|
|
103
|
-
command: 'bbx',
|
|
104
|
-
args: ['mcp', 'serve'],
|
|
105
|
-
env: {},
|
|
106
|
-
};
|
|
121
|
+
return windowsCommand;
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
/** @type {Record<McpClientName, { key: string, includeType: boolean, legacyKeys?: string[], keepEmptyBlock?: boolean }>} */
|
|
@@ -129,11 +144,13 @@ const MCP_CONFIG_SHAPES = {
|
|
|
129
144
|
*/
|
|
130
145
|
export function buildMcpConfig(clientName) {
|
|
131
146
|
if (clientName === 'codex') {
|
|
147
|
+
const command = process.platform === 'win32' ? process.execPath : 'bbx';
|
|
148
|
+
const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
|
|
132
149
|
return {
|
|
133
150
|
mcp_servers: {
|
|
134
151
|
[BROWSER_BRIDGE_SERVER_NAME]: {
|
|
135
|
-
command
|
|
136
|
-
args
|
|
152
|
+
command,
|
|
153
|
+
args,
|
|
137
154
|
},
|
|
138
155
|
},
|
|
139
156
|
};
|
|
@@ -262,10 +279,12 @@ export async function getMcpConfigPaths(clientName, options) {
|
|
|
262
279
|
* @returns {string}
|
|
263
280
|
*/
|
|
264
281
|
function formatCodexServerBlock() {
|
|
282
|
+
const command = process.platform === 'win32' ? process.execPath : 'bbx';
|
|
283
|
+
const args = process.platform === 'win32' ? [mcpServerBinPath] : ['mcp', 'serve'];
|
|
265
284
|
return [
|
|
266
285
|
`[mcp_servers."${BROWSER_BRIDGE_SERVER_NAME}"]`,
|
|
267
|
-
|
|
268
|
-
|
|
286
|
+
`command = ${JSON.stringify(command)}`,
|
|
287
|
+
`args = ${JSON.stringify(args)}`,
|
|
269
288
|
'',
|
|
270
289
|
].join('\n');
|
|
271
290
|
}
|
|
@@ -57,8 +57,8 @@ const SKILL_TARGET_LABELS = {
|
|
|
57
57
|
* global?: boolean,
|
|
58
58
|
* cwd?: string,
|
|
59
59
|
* projectPath?: string,
|
|
60
|
-
* mcpDetectors?: Record<string, () => boolean
|
|
61
|
-
* skillDetectors?: Record<string, () => boolean
|
|
60
|
+
* mcpDetectors?: Record<string, () => boolean | Promise<boolean>>,
|
|
61
|
+
* skillDetectors?: Record<string, () => boolean | Promise<boolean>>,
|
|
62
62
|
* access?: (targetPath: string) => Promise<void>,
|
|
63
63
|
* readFile?: (targetPath: string, encoding: BufferEncoding) => Promise<string>
|
|
64
64
|
* }} SetupStatusOptions
|
|
@@ -76,8 +76,16 @@ export async function collectSetupStatus(options = {}) {
|
|
|
76
76
|
const projectPath = options.projectPath || cwd;
|
|
77
77
|
const access = options.access || fs.promises.access.bind(fs.promises);
|
|
78
78
|
const readFile = options.readFile || fs.promises.readFile.bind(fs.promises);
|
|
79
|
-
const
|
|
80
|
-
|
|
79
|
+
const [detectedMcpClientResult, detectedSkillTargetResult] = await Promise.allSettled([
|
|
80
|
+
detectMcpClients(options.mcpDetectors),
|
|
81
|
+
detectSkillTargets(options.skillDetectors),
|
|
82
|
+
]);
|
|
83
|
+
const detectedMcpClientNames =
|
|
84
|
+
detectedMcpClientResult.status === 'fulfilled' ? detectedMcpClientResult.value : [];
|
|
85
|
+
const detectedSkillTargetNames =
|
|
86
|
+
detectedSkillTargetResult.status === 'fulfilled' ? detectedSkillTargetResult.value : [];
|
|
87
|
+
const detectedMcpClients = new Set(detectedMcpClientNames);
|
|
88
|
+
const detectedSkillTargets = new Set(detectedSkillTargetNames);
|
|
81
89
|
for (const clientName of detectedMcpClients) {
|
|
82
90
|
if (SUPPORTED_TARGETS.includes(/** @type {SupportedTarget} */ (clientName))) {
|
|
83
91
|
detectedSkillTargets.add(/** @type {SupportedTarget} */ (clientName));
|
|
@@ -1,10 +1,62 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @ts-check
|
|
3
3
|
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
4
7
|
import { startBridgeMcpServer } from './server.js';
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* start?: () => Promise<void>,
|
|
12
|
+
* argv?: string[],
|
|
13
|
+
* stdout?: { write: (chunk: string) => unknown },
|
|
14
|
+
* stderr?: { write: (chunk: string) => unknown },
|
|
15
|
+
* exit?: (code: number) => unknown,
|
|
16
|
+
* }} BridgeMcpCliOptions
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const HELP_FLAGS = new Set(['help', '--help', '-h']);
|
|
20
|
+
|
|
21
|
+
const MCP_USAGE = [
|
|
22
|
+
'Usage: bbx-mcp [--help]',
|
|
23
|
+
'',
|
|
24
|
+
'Start the Browser Bridge MCP stdio server.',
|
|
25
|
+
].join('\n');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start the MCP server CLI and report startup failures to stderr.
|
|
29
|
+
*
|
|
30
|
+
* @param {BridgeMcpCliOptions} [options]
|
|
31
|
+
* @returns {Promise<number>}
|
|
32
|
+
*/
|
|
33
|
+
export async function runBridgeMcpCli(options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
start = startBridgeMcpServer,
|
|
36
|
+
argv = process.argv.slice(2),
|
|
37
|
+
stdout = process.stdout,
|
|
38
|
+
stderr = process.stderr,
|
|
39
|
+
exit = process.exit,
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
if (argv.some((arg) => HELP_FLAGS.has(arg))) {
|
|
43
|
+
stdout.write(`${MCP_USAGE}\n`);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await start();
|
|
49
|
+
return 0;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
52
|
+
stderr.write(`${message}\n`);
|
|
53
|
+
exit(1);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const entryPointUrl = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : null;
|
|
59
|
+
|
|
60
|
+
if (entryPointUrl === import.meta.url) {
|
|
61
|
+
void runBridgeMcpCli();
|
|
62
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import {
|
|
4
5
|
bridgeMethodNeedsTab,
|
|
6
|
+
createRuntimeContext,
|
|
5
7
|
DEFAULT_CONSOLE_LIMIT,
|
|
6
8
|
DEFAULT_MAX_HTML_LENGTH,
|
|
7
9
|
DEFAULT_NETWORK_LIMIT,
|
|
@@ -9,7 +11,7 @@ import {
|
|
|
9
11
|
getBudgetPreset,
|
|
10
12
|
getErrorRecovery,
|
|
11
13
|
isBudgetPresetName,
|
|
12
|
-
|
|
14
|
+
METHOD_SET,
|
|
13
15
|
summarizeBatchErrorItem,
|
|
14
16
|
summarizeBatchResponseItem,
|
|
15
17
|
} from '../../protocol/src/index.js';
|
|
@@ -20,11 +22,13 @@ import {
|
|
|
20
22
|
withBridgeClient,
|
|
21
23
|
} from '../../agent-client/src/runtime.js';
|
|
22
24
|
import { annotateBridgeSummary, summarizeBridgeResponse } from '../../agent-client/src/subagent.js';
|
|
25
|
+
import { collectSetupStatus } from '../../agent-client/src/setup-status.js';
|
|
23
26
|
|
|
24
27
|
/** @typedef {import('../../protocol/src/types.js').BridgeMethod} BridgeMethod */
|
|
25
28
|
/** @typedef {import('../../protocol/src/types.js').BridgeResponse} BridgeResponse */
|
|
26
29
|
|
|
27
30
|
const REQUEST_SOURCE = 'mcp';
|
|
31
|
+
const HOME_DIR = os.homedir();
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* @typedef {{
|
|
@@ -594,6 +598,7 @@ export const INPUT_ACTION_METHODS = {
|
|
|
594
598
|
focus: 'input.focus',
|
|
595
599
|
type: 'input.type',
|
|
596
600
|
press_key: 'input.press_key',
|
|
601
|
+
cdp_press_key: 'cdp.dispatch_key_event',
|
|
597
602
|
set_checked: 'input.set_checked',
|
|
598
603
|
select_option: 'input.select_option',
|
|
599
604
|
hover: 'input.hover',
|
|
@@ -602,7 +607,7 @@ export const INPUT_ACTION_METHODS = {
|
|
|
602
607
|
};
|
|
603
608
|
|
|
604
609
|
/**
|
|
605
|
-
* @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, clear?: boolean, submit?: boolean, key?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
610
|
+
* @param {{ action: string, elementRef?: string, selector?: string, button?: string, clickCount?: number, text?: string, clear?: boolean, submit?: boolean, key?: string, code?: string, modifiers?: string[], checked?: boolean, values?: string[], labels?: string[], indexes?: number[], duration?: number, sourceElementRef?: string, sourceSelector?: string, destinationElementRef?: string, destinationSelector?: string, offsetX?: number, offsetY?: number, tabId?: number, budgetPreset?: 'quick' | 'normal' | 'deep' }} args
|
|
606
611
|
* @returns {Promise<ToolResult>}
|
|
607
612
|
*/
|
|
608
613
|
export async function handleInputTool(args) {
|
|
@@ -681,6 +686,23 @@ export async function handleInputTool(args) {
|
|
|
681
686
|
);
|
|
682
687
|
return summarizeToolResponse(response, 'input.press_key');
|
|
683
688
|
}
|
|
689
|
+
case 'cdp_press_key': {
|
|
690
|
+
const response = await requestBridge(
|
|
691
|
+
client,
|
|
692
|
+
'cdp.dispatch_key_event',
|
|
693
|
+
{
|
|
694
|
+
key: args.key,
|
|
695
|
+
code: args.code,
|
|
696
|
+
modifiers: args.modifiers,
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
tabId: requestedTabId,
|
|
700
|
+
source: REQUEST_SOURCE,
|
|
701
|
+
tokenBudget: getToolTokenBudget(args),
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
return summarizeToolResponse(response, 'cdp.dispatch_key_event');
|
|
705
|
+
}
|
|
684
706
|
case 'set_checked': {
|
|
685
707
|
const response = await requestBridge(
|
|
686
708
|
client,
|
|
@@ -896,7 +918,6 @@ export async function handleCaptureTool(args) {
|
|
|
896
918
|
*/
|
|
897
919
|
export async function handleSkillTool() {
|
|
898
920
|
try {
|
|
899
|
-
const { createRuntimeContext } = await import('../../protocol/src/index.js');
|
|
900
921
|
const ctx = createRuntimeContext();
|
|
901
922
|
return createToolResult('Runtime context retrieved.', {
|
|
902
923
|
ok: true,
|
|
@@ -914,8 +935,7 @@ export async function handleSkillTool() {
|
|
|
914
935
|
* @returns {Promise<ToolResult>}
|
|
915
936
|
*/
|
|
916
937
|
export async function handleSetupTool(args) {
|
|
917
|
-
const
|
|
918
|
-
const projectPath = args.global !== false ? (await import('node:os')).homedir() : process.cwd();
|
|
938
|
+
const projectPath = args.global !== false ? HOME_DIR : process.cwd();
|
|
919
939
|
const status = await collectSetupStatus({
|
|
920
940
|
global: args.global !== false,
|
|
921
941
|
cwd: process.cwd(),
|
|
@@ -993,7 +1013,7 @@ export async function handleBatchTool(args) {
|
|
|
993
1013
|
};
|
|
994
1014
|
}
|
|
995
1015
|
|
|
996
|
-
if (!
|
|
1016
|
+
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (call.method))) {
|
|
997
1017
|
return {
|
|
998
1018
|
method: call.method,
|
|
999
1019
|
tabId: null,
|
|
@@ -1065,7 +1085,7 @@ export async function handleBatchTool(args) {
|
|
|
1065
1085
|
* @returns {Promise<ToolResult>}
|
|
1066
1086
|
*/
|
|
1067
1087
|
export async function handleRawCallTool(args) {
|
|
1068
|
-
if (!
|
|
1088
|
+
if (!METHOD_SET.has(/** @type {BridgeMethod} */ (args.method))) {
|
|
1069
1089
|
return summarizeToolError(`Unknown bridge method "${args.method}".`);
|
|
1070
1090
|
}
|
|
1071
1091
|
|
|
@@ -1247,6 +1267,7 @@ export async function handleInvestigateTool(args) {
|
|
|
1247
1267
|
scope: scopeName,
|
|
1248
1268
|
heuristicFallback: true,
|
|
1249
1269
|
steps: stepResults,
|
|
1270
|
+
failedSteps,
|
|
1250
1271
|
},
|
|
1251
1272
|
!allOk
|
|
1252
1273
|
);
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
DEFAULT_WAIT_TIMEOUT_MS,
|
|
38
38
|
getMethodsByMaxComplexity,
|
|
39
39
|
} from '../../protocol/src/index.js';
|
|
40
|
+
import { applyWindowsTcpTransportDefaults } from '../../native-host/src/config.js';
|
|
40
41
|
|
|
41
42
|
export const BUDGET_PRESET_DESCRIPTION = `Budget preset: "quick", "normal", or "deep" (defaults: query ${BUDGET_PRESETS.normal.maxNodes} nodes / depth ${BUDGET_PRESETS.normal.maxDepth} / text ${BUDGET_PRESETS.normal.textBudget}). Numeric fields override the preset when both are provided.`;
|
|
42
43
|
export const TAB_ID_DESCRIPTION =
|
|
@@ -369,7 +370,7 @@ export function createBridgeMcpServer() {
|
|
|
369
370
|
{
|
|
370
371
|
title: 'Browser Input',
|
|
371
372
|
description:
|
|
372
|
-
'Simulate user input: click, focus, type, press keys, set checked, select options, hover, drag, or scroll into view. Reuse elementRef from prior queries.',
|
|
373
|
+
'Simulate user input: click, focus, type, press keys, CDP key events, set checked, select options, hover, drag, or scroll into view. Reuse elementRef from prior queries.',
|
|
373
374
|
inputSchema: {
|
|
374
375
|
action: z
|
|
375
376
|
.enum([
|
|
@@ -377,6 +378,7 @@ export function createBridgeMcpServer() {
|
|
|
377
378
|
'focus',
|
|
378
379
|
'type',
|
|
379
380
|
'press_key',
|
|
381
|
+
'cdp_press_key',
|
|
380
382
|
'set_checked',
|
|
381
383
|
'select_option',
|
|
382
384
|
'hover',
|
|
@@ -402,7 +404,14 @@ export function createBridgeMcpServer() {
|
|
|
402
404
|
text: z.string().optional().describe('Text to type (for type action)'),
|
|
403
405
|
clear: z.boolean().optional().describe('Clear field before typing (default: false)'),
|
|
404
406
|
submit: z.boolean().optional().describe('Press Enter after typing (default: false)'),
|
|
405
|
-
key: z
|
|
407
|
+
key: z
|
|
408
|
+
.string()
|
|
409
|
+
.optional()
|
|
410
|
+
.describe('Key to press (e.g., "Escape", "Enter", "Tab", "ArrowDown")'),
|
|
411
|
+
code: z
|
|
412
|
+
.string()
|
|
413
|
+
.optional()
|
|
414
|
+
.describe('Optional physical key code for cdp_press_key (e.g., "Escape", "KeyA")'),
|
|
406
415
|
modifiers: z
|
|
407
416
|
.array(z.enum(['Alt', 'Control', 'Meta', 'Shift']))
|
|
408
417
|
.optional()
|
|
@@ -652,6 +661,7 @@ export function createBridgeMcpServer() {
|
|
|
652
661
|
* @returns {Promise<void>}
|
|
653
662
|
*/
|
|
654
663
|
export async function startBridgeMcpServer() {
|
|
664
|
+
applyWindowsTcpTransportDefaults();
|
|
655
665
|
const server = createBridgeMcpServer();
|
|
656
666
|
const transport = new StdioServerTransport();
|
|
657
667
|
await server.connect(transport);
|