@mehmoodqureshi/chrome-mcp 0.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/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/shared/download.d.ts +15 -0
- package/dist/shared/download.js +0 -0
- package/dist/shared/protocol.d.ts +114 -0
- package/dist/shared/protocol.js +55 -0
- package/dist/src/bridge/auth.d.ts +32 -0
- package/dist/src/bridge/auth.js +76 -0
- package/dist/src/bridge/connection.d.ts +48 -0
- package/dist/src/bridge/connection.js +192 -0
- package/dist/src/bridge/datadir.d.ts +8 -0
- package/dist/src/bridge/datadir.js +22 -0
- package/dist/src/bridge/server.d.ts +58 -0
- package/dist/src/bridge/server.js +178 -0
- package/dist/src/cli.d.ts +11 -0
- package/dist/src/cli.js +93 -0
- package/dist/src/config.d.ts +42 -0
- package/dist/src/config.js +188 -0
- package/dist/src/executor/cdp-executor.d.ts +131 -0
- package/dist/src/executor/cdp-executor.js +422 -0
- package/dist/src/executor/extension-executor.d.ts +102 -0
- package/dist/src/executor/extension-executor.js +124 -0
- package/dist/src/executor/manager.d.ts +43 -0
- package/dist/src/executor/manager.js +94 -0
- package/dist/src/executor/select.d.ts +23 -0
- package/dist/src/executor/select.js +53 -0
- package/dist/src/executor/stub-executor.d.ts +60 -0
- package/dist/src/executor/stub-executor.js +118 -0
- package/dist/src/executor/types.d.ts +192 -0
- package/dist/src/executor/types.js +24 -0
- package/dist/src/mcp/envelopes.d.ts +13 -0
- package/dist/src/mcp/envelopes.js +30 -0
- package/dist/src/mcp/helpers.d.ts +37 -0
- package/dist/src/mcp/helpers.js +71 -0
- package/dist/src/mcp/markdown-extract.d.ts +9 -0
- package/dist/src/mcp/markdown-extract.js +61 -0
- package/dist/src/mcp/server.d.ts +18 -0
- package/dist/src/mcp/server.js +82 -0
- package/dist/src/mcp/tools.d.ts +32 -0
- package/dist/src/mcp/tools.js +267 -0
- package/dist/src/mcp/validators.d.ts +32 -0
- package/dist/src/mcp/validators.js +104 -0
- package/dist/src/security/policy.d.ts +48 -0
- package/dist/src/security/policy.js +155 -0
- package/docs/BLUEPRINT.md +596 -0
- package/extension-dist/background.js +567 -0
- package/extension-dist/manifest.json +12 -0
- package/extension-dist/options.html +32 -0
- package/extension-dist/options.js +37 -0
- package/package.json +69 -0
- package/scripts/postinstall.js +50 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* src/config.ts — the single source for runtime configuration.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the WebSocket port, the data dir (where the 0600 handshake lives),
|
|
6
|
+
* and the security `Policy` from one place: defaults < env < CLI flags < policy
|
|
7
|
+
* file. Nothing else in the codebase should read `process.argv` or invent its
|
|
8
|
+
* own port/policy — they take a `CliConfig`.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.HELP_TEXT = void 0;
|
|
12
|
+
exports.resolveDataDir = resolveDataDir;
|
|
13
|
+
exports.parseArgs = parseArgs;
|
|
14
|
+
const node_os_1 = require("node:os");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const node_fs_1 = require("node:fs");
|
|
17
|
+
const protocol_1 = require("../shared/protocol");
|
|
18
|
+
const policy_1 = require("./security/policy");
|
|
19
|
+
/** Resolve the data dir: `$CHROME_MCP_DATA` or `~/.chrome-mcp`. */
|
|
20
|
+
function resolveDataDir() {
|
|
21
|
+
return process.env.CHROME_MCP_DATA ?? (0, node_path_1.join)((0, node_os_1.homedir)(), '.chrome-mcp');
|
|
22
|
+
}
|
|
23
|
+
function readPolicyFile(path) {
|
|
24
|
+
const raw = (0, node_fs_1.readFileSync)(path, 'utf8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
27
|
+
throw new Error(`policy file ${path} did not contain a JSON object`);
|
|
28
|
+
}
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse argv (the slice AFTER `node script`, i.e. `process.argv.slice(2)`) plus
|
|
33
|
+
* env into a fully-resolved `CliConfig`. Pure except for the optional policy-file
|
|
34
|
+
* read triggered by `--policy <path>`.
|
|
35
|
+
*/
|
|
36
|
+
function parseArgs(argv) {
|
|
37
|
+
let wsPort = envInt('CHROME_MCP_WS_PORT') ?? protocol_1.DEFAULT_WS_PORT;
|
|
38
|
+
let cdpFallback = true;
|
|
39
|
+
let cdpEndpoint;
|
|
40
|
+
let prefer = 'extension';
|
|
41
|
+
let headless = false;
|
|
42
|
+
let printPairing = false;
|
|
43
|
+
let showHelp = false;
|
|
44
|
+
let showVersion = false;
|
|
45
|
+
let logLevel = 'info';
|
|
46
|
+
// Policy assembled from flags, layered over an optional file.
|
|
47
|
+
let policyFile;
|
|
48
|
+
const policyFlags = {};
|
|
49
|
+
for (let i = 0; i < argv.length; i++) {
|
|
50
|
+
const arg = argv[i];
|
|
51
|
+
switch (arg) {
|
|
52
|
+
case '-h':
|
|
53
|
+
case '--help':
|
|
54
|
+
showHelp = true;
|
|
55
|
+
break;
|
|
56
|
+
case '-v':
|
|
57
|
+
case '--version':
|
|
58
|
+
showVersion = true;
|
|
59
|
+
break;
|
|
60
|
+
case '--port':
|
|
61
|
+
wsPort = requireInt(argv[++i], '--port');
|
|
62
|
+
break;
|
|
63
|
+
case '--data-dir':
|
|
64
|
+
process.env.CHROME_MCP_DATA = requireValue(argv[++i], '--data-dir');
|
|
65
|
+
break;
|
|
66
|
+
case '--policy':
|
|
67
|
+
policyFile = readPolicyFile(requireValue(argv[++i], '--policy'));
|
|
68
|
+
break;
|
|
69
|
+
case '--unsafe-all-domains':
|
|
70
|
+
policyFlags.allowDomains = ['*'];
|
|
71
|
+
break;
|
|
72
|
+
case '--allow-domain':
|
|
73
|
+
(policyFlags.allowDomains ??= []).push(requireValue(argv[++i], '--allow-domain'));
|
|
74
|
+
break;
|
|
75
|
+
case '--enable-mutations':
|
|
76
|
+
policyFlags.enableMutations = true;
|
|
77
|
+
break;
|
|
78
|
+
case '--unsafe-enable-eval':
|
|
79
|
+
policyFlags.allowEval = true;
|
|
80
|
+
break;
|
|
81
|
+
case '--enable-downloads':
|
|
82
|
+
policyFlags.allowDownloads = true;
|
|
83
|
+
break;
|
|
84
|
+
case '--allow-all-tabs':
|
|
85
|
+
policyFlags.allowAllTabs = true;
|
|
86
|
+
break;
|
|
87
|
+
case '--no-cdp-fallback':
|
|
88
|
+
cdpFallback = false;
|
|
89
|
+
break;
|
|
90
|
+
case '--cdp-endpoint':
|
|
91
|
+
cdpEndpoint = requireValue(argv[++i], '--cdp-endpoint');
|
|
92
|
+
break;
|
|
93
|
+
case '--prefer':
|
|
94
|
+
prefer = requirePreference(argv[++i]);
|
|
95
|
+
break;
|
|
96
|
+
case '--headless':
|
|
97
|
+
headless = true;
|
|
98
|
+
break;
|
|
99
|
+
case '--print-pairing':
|
|
100
|
+
printPairing = true;
|
|
101
|
+
break;
|
|
102
|
+
case '--log-level':
|
|
103
|
+
logLevel = requireLogLevel(argv[++i]);
|
|
104
|
+
break;
|
|
105
|
+
default:
|
|
106
|
+
throw new Error(`unknown argument: ${arg}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// File first, then flags win.
|
|
110
|
+
const policy = (0, policy_1.resolvePolicy)({ ...policyFile, ...policyFlags });
|
|
111
|
+
return {
|
|
112
|
+
wsPort,
|
|
113
|
+
dataDir: resolveDataDir(),
|
|
114
|
+
policy,
|
|
115
|
+
cdpFallback,
|
|
116
|
+
cdpEndpoint,
|
|
117
|
+
prefer,
|
|
118
|
+
headless,
|
|
119
|
+
printPairing,
|
|
120
|
+
showHelp,
|
|
121
|
+
showVersion,
|
|
122
|
+
logLevel,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Small parsing helpers
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
function envInt(name) {
|
|
129
|
+
const raw = process.env[name];
|
|
130
|
+
if (raw === undefined)
|
|
131
|
+
return undefined;
|
|
132
|
+
const n = Number.parseInt(raw, 10);
|
|
133
|
+
if (!Number.isInteger(n) || n < 0)
|
|
134
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
135
|
+
return n;
|
|
136
|
+
}
|
|
137
|
+
function requireValue(value, flag) {
|
|
138
|
+
if (value === undefined)
|
|
139
|
+
throw new Error(`${flag} requires a value`);
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
function requireInt(value, flag) {
|
|
143
|
+
const n = Number.parseInt(requireValue(value, flag), 10);
|
|
144
|
+
if (!Number.isInteger(n) || n < 0)
|
|
145
|
+
throw new Error(`${flag} must be a non-negative integer`);
|
|
146
|
+
return n;
|
|
147
|
+
}
|
|
148
|
+
function requirePreference(value) {
|
|
149
|
+
if (value === 'extension' || value === 'cdp')
|
|
150
|
+
return value;
|
|
151
|
+
throw new Error('--prefer must be "extension" or "cdp"');
|
|
152
|
+
}
|
|
153
|
+
function requireLogLevel(value) {
|
|
154
|
+
if (value === 'silent' || value === 'info' || value === 'debug')
|
|
155
|
+
return value;
|
|
156
|
+
throw new Error('--log-level must be "silent", "info", or "debug"');
|
|
157
|
+
}
|
|
158
|
+
/** Help text for `--help`. */
|
|
159
|
+
exports.HELP_TEXT = `chrome-mcp — drive a real Chrome browser over MCP.
|
|
160
|
+
|
|
161
|
+
Usage: chrome-mcp [options]
|
|
162
|
+
|
|
163
|
+
Connection:
|
|
164
|
+
--port <n> WebSocket bridge port (default ${protocol_1.DEFAULT_WS_PORT}; 0 = ephemeral)
|
|
165
|
+
--data-dir <path> Override the data dir (default ~/.chrome-mcp)
|
|
166
|
+
--print-pairing Write the handshake and print its path, then exit
|
|
167
|
+
|
|
168
|
+
Backend:
|
|
169
|
+
--no-cdp-fallback Do not launch/attach Chromium when no extension is paired
|
|
170
|
+
--cdp-endpoint <url> Attach to an existing Chrome (e.g. http://127.0.0.1:9222)
|
|
171
|
+
--prefer <which> "extension" (default) or "cdp"
|
|
172
|
+
--headless Run the CDP-fallback Chromium headless
|
|
173
|
+
|
|
174
|
+
Security (default: deny-all safe mode):
|
|
175
|
+
--policy <file> Load a JSON policy file
|
|
176
|
+
--allow-domain <glob> Add a domain to the allowlist (repeatable)
|
|
177
|
+
--unsafe-all-domains Allow every domain (loud footgun)
|
|
178
|
+
--enable-mutations Enable click/type/navigate/… (off by default)
|
|
179
|
+
--unsafe-enable-eval Enable the eval primitive (off by default)
|
|
180
|
+
--enable-downloads Enable download_file (off by default)
|
|
181
|
+
--allow-all-tabs Relax tab list/select to all tabs
|
|
182
|
+
|
|
183
|
+
Misc:
|
|
184
|
+
--log-level <lvl> silent | info | debug (default info)
|
|
185
|
+
-h, --help Show this help
|
|
186
|
+
-v, --version Show version
|
|
187
|
+
`;
|
|
188
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/executor/cdp-executor.ts — the Playwright-driven fallback Executor.
|
|
3
|
+
*
|
|
4
|
+
* Used when no extension is paired. Two acquisition modes:
|
|
5
|
+
* - 'connect': attach over CDP to a Chrome we did NOT launch (never closed by
|
|
6
|
+
* us — its lifecycle is the user's).
|
|
7
|
+
* - 'launch': spawn a dedicated persistent Chromium under a profile that is
|
|
8
|
+
* NOT the user's real one (would conflict with the extension-driven Chrome).
|
|
9
|
+
*
|
|
10
|
+
* Ported from linkedin-mcp/src/driver/browser.ts: the stale-profile-lock
|
|
11
|
+
* recovery (a SIGKILLed MCP child orphans `SingletonLock`), connect-once reuse,
|
|
12
|
+
* stealth init on the launch path only, and tab resolution. `tabId`s are stamped
|
|
13
|
+
* `cdp:<sessionId>:<n>` so a handle never mis-routes across a backend switch.
|
|
14
|
+
*/
|
|
15
|
+
import { type ActionOk, type BackendKind, type DownloadResult, type EvalResult, type Executor, type ExecutorStatus, type KeyModifier, type NavResult, type ScreenshotResult, type TabId, type TabInfo, type Target, type WaitResult, type WaitUntil } from './types';
|
|
16
|
+
export interface CdpOptions {
|
|
17
|
+
mode: 'connect' | 'launch';
|
|
18
|
+
cdpEndpoint?: string;
|
|
19
|
+
userDataDir: string;
|
|
20
|
+
headless?: boolean;
|
|
21
|
+
downloadDir?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class CdpExecutor implements Executor {
|
|
24
|
+
private readonly opts;
|
|
25
|
+
readonly backend: BackendKind;
|
|
26
|
+
private readonly sessionId;
|
|
27
|
+
private readonly profileDir;
|
|
28
|
+
private cdpBrowser;
|
|
29
|
+
private context;
|
|
30
|
+
private launching;
|
|
31
|
+
/** Whether WE own (launched) the browser — only then may we close it. */
|
|
32
|
+
private launched;
|
|
33
|
+
private readonly tabs;
|
|
34
|
+
private seq;
|
|
35
|
+
constructor(opts: CdpOptions);
|
|
36
|
+
status(): ExecutorStatus;
|
|
37
|
+
ensureReady(): Promise<void>;
|
|
38
|
+
ping(): Promise<boolean>;
|
|
39
|
+
dispose(): Promise<void>;
|
|
40
|
+
private getContext;
|
|
41
|
+
private doConnect;
|
|
42
|
+
private doLaunch;
|
|
43
|
+
private launchWithLockRecovery;
|
|
44
|
+
private idFor;
|
|
45
|
+
private contentPages;
|
|
46
|
+
private resolveTab;
|
|
47
|
+
private locator;
|
|
48
|
+
tabsList(): Promise<TabInfo[]>;
|
|
49
|
+
tabSelect(tabId: TabId): Promise<TabInfo>;
|
|
50
|
+
tabNew(url?: string): Promise<TabInfo>;
|
|
51
|
+
tabClose(tabId: TabId): Promise<{
|
|
52
|
+
closed: true;
|
|
53
|
+
tabId: TabId;
|
|
54
|
+
}>;
|
|
55
|
+
private toState;
|
|
56
|
+
navigate(args: {
|
|
57
|
+
url: string;
|
|
58
|
+
tabId?: TabId;
|
|
59
|
+
waitUntil?: WaitUntil;
|
|
60
|
+
}): Promise<NavResult>;
|
|
61
|
+
back(tabId?: TabId): Promise<NavResult>;
|
|
62
|
+
forward(tabId?: TabId): Promise<NavResult>;
|
|
63
|
+
reload(args?: {
|
|
64
|
+
tabId?: TabId;
|
|
65
|
+
waitUntil?: WaitUntil;
|
|
66
|
+
}): Promise<NavResult>;
|
|
67
|
+
private ok;
|
|
68
|
+
click(t: Target, opts?: {
|
|
69
|
+
tabId?: TabId;
|
|
70
|
+
button?: 'left' | 'right' | 'middle';
|
|
71
|
+
clickCount?: number;
|
|
72
|
+
}): Promise<ActionOk>;
|
|
73
|
+
type(t: Target, text: string, opts?: {
|
|
74
|
+
tabId?: TabId;
|
|
75
|
+
clear?: boolean;
|
|
76
|
+
pressEnter?: boolean;
|
|
77
|
+
keyEvents?: boolean;
|
|
78
|
+
}): Promise<ActionOk>;
|
|
79
|
+
fill(t: Target, value: string, opts?: {
|
|
80
|
+
tabId?: TabId;
|
|
81
|
+
}): Promise<ActionOk>;
|
|
82
|
+
press(key: string, opts?: {
|
|
83
|
+
tabId?: TabId;
|
|
84
|
+
modifiers?: KeyModifier[];
|
|
85
|
+
}): Promise<ActionOk>;
|
|
86
|
+
hover(t: Target, opts?: {
|
|
87
|
+
tabId?: TabId;
|
|
88
|
+
}): Promise<ActionOk>;
|
|
89
|
+
scroll(opts: {
|
|
90
|
+
tabId?: TabId;
|
|
91
|
+
x?: number;
|
|
92
|
+
y?: number;
|
|
93
|
+
deltaX?: number;
|
|
94
|
+
deltaY?: number;
|
|
95
|
+
target?: Target;
|
|
96
|
+
}): Promise<ActionOk>;
|
|
97
|
+
getText(t?: Target, opts?: {
|
|
98
|
+
tabId?: TabId;
|
|
99
|
+
}): Promise<{
|
|
100
|
+
text: string;
|
|
101
|
+
ref?: string;
|
|
102
|
+
}>;
|
|
103
|
+
getHtml(t?: Target, opts?: {
|
|
104
|
+
tabId?: TabId;
|
|
105
|
+
outer?: boolean;
|
|
106
|
+
}): Promise<{
|
|
107
|
+
html: string;
|
|
108
|
+
}>;
|
|
109
|
+
screenshot(opts?: {
|
|
110
|
+
tabId?: TabId;
|
|
111
|
+
fullPage?: boolean;
|
|
112
|
+
target?: Target;
|
|
113
|
+
}): Promise<ScreenshotResult>;
|
|
114
|
+
eval(expression: string, opts?: {
|
|
115
|
+
tabId?: TabId;
|
|
116
|
+
awaitPromise?: boolean;
|
|
117
|
+
}): Promise<EvalResult>;
|
|
118
|
+
waitFor(opts: {
|
|
119
|
+
tabId?: TabId;
|
|
120
|
+
selector?: string;
|
|
121
|
+
textContains?: string;
|
|
122
|
+
gone?: boolean;
|
|
123
|
+
timeoutMs?: number;
|
|
124
|
+
}): Promise<WaitResult>;
|
|
125
|
+
download(args: {
|
|
126
|
+
url?: string;
|
|
127
|
+
target?: Target;
|
|
128
|
+
tabId?: TabId;
|
|
129
|
+
suggestedName?: string;
|
|
130
|
+
}): Promise<DownloadResult>;
|
|
131
|
+
}
|