chrome-devtools-mcp-canpoint 0.1.0__py3-none-any.whl
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.
- chrome_devtools_mcp_canpoint/__init__.py +3 -0
- chrome_devtools_mcp_canpoint/cli.py +372 -0
- chrome_devtools_mcp_canpoint-0.1.0.dist-info/METADATA +147 -0
- chrome_devtools_mcp_canpoint-0.1.0.dist-info/RECORD +7 -0
- chrome_devtools_mcp_canpoint-0.1.0.dist-info/WHEEL +5 -0
- chrome_devtools_mcp_canpoint-0.1.0.dist-info/entry_points.txt +2 -0
- chrome_devtools_mcp_canpoint-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import atexit
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import signal
|
|
8
|
+
import socket
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
import urllib.error
|
|
15
|
+
import urllib.request
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Mapping, Sequence
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_CHROME_PATHS = (
|
|
22
|
+
Path(r"C:\Program Files\Google\Chrome\Application\chrome.exe"),
|
|
23
|
+
Path(r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"),
|
|
24
|
+
)
|
|
25
|
+
DEFAULT_SESSION_ROOT_NAME = ".chrome-mcp-sessions"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ChromeSessionConfig:
|
|
30
|
+
chrome_path: Path
|
|
31
|
+
port: int
|
|
32
|
+
user_data_dir: Path
|
|
33
|
+
headless: bool
|
|
34
|
+
extra_args: tuple[str, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def find_free_port(host: str = "127.0.0.1") -> int:
|
|
38
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
39
|
+
sock.bind((host, 0))
|
|
40
|
+
return int(sock.getsockname()[1])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def devtools_url(port: int) -> str:
|
|
44
|
+
return f"http://127.0.0.1:{port}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_chrome_args(config: ChromeSessionConfig) -> list[str]:
|
|
48
|
+
args = [
|
|
49
|
+
str(config.chrome_path),
|
|
50
|
+
f"--remote-debugging-port={config.port}",
|
|
51
|
+
f"--user-data-dir={config.user_data_dir}",
|
|
52
|
+
"--no-first-run",
|
|
53
|
+
"--no-default-browser-check",
|
|
54
|
+
"--disable-background-networking",
|
|
55
|
+
"--disable-sync",
|
|
56
|
+
]
|
|
57
|
+
if config.headless:
|
|
58
|
+
args.append("--headless=new")
|
|
59
|
+
args.extend(config.extra_args)
|
|
60
|
+
return args
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def downstream_env(base_env: Mapping[str, str], port: int, user_data_dir: Path) -> dict[str, str]:
|
|
64
|
+
env = dict(base_env)
|
|
65
|
+
url = devtools_url(port)
|
|
66
|
+
env.update(
|
|
67
|
+
{
|
|
68
|
+
"CHROME_DEVTOOLS_URL": url,
|
|
69
|
+
"CHROME_REMOTE_DEBUGGING_PORT": str(port),
|
|
70
|
+
"CHROME_USER_DATA_DIR": str(user_data_dir),
|
|
71
|
+
"BROWSER_URL": url,
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
return env
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def expand_downstream_command(
|
|
78
|
+
command: Sequence[str], port: int, user_data_dir: Path
|
|
79
|
+
) -> list[str]:
|
|
80
|
+
replacements = {
|
|
81
|
+
"browser_url": devtools_url(port),
|
|
82
|
+
"devtools_url": devtools_url(port),
|
|
83
|
+
"port": str(port),
|
|
84
|
+
"user_data_dir": str(user_data_dir),
|
|
85
|
+
}
|
|
86
|
+
return [arg.format(**replacements) for arg in command]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def resolve_session_root(value: str | None) -> Path:
|
|
90
|
+
if value:
|
|
91
|
+
return Path(value).resolve()
|
|
92
|
+
return (Path.cwd() / DEFAULT_SESSION_ROOT_NAME).resolve()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def resolve_chrome_path(value: str | None) -> Path:
|
|
96
|
+
candidates: list[Path] = []
|
|
97
|
+
if value:
|
|
98
|
+
candidates.append(Path(value))
|
|
99
|
+
if os.environ.get("CHROME_PATH"):
|
|
100
|
+
candidates.append(Path(os.environ["CHROME_PATH"]))
|
|
101
|
+
candidates.extend(DEFAULT_CHROME_PATHS)
|
|
102
|
+
|
|
103
|
+
for candidate in candidates:
|
|
104
|
+
if candidate.exists():
|
|
105
|
+
return candidate
|
|
106
|
+
|
|
107
|
+
raise FileNotFoundError(
|
|
108
|
+
"Chrome executable not found. Pass --chrome-path or set CHROME_PATH."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def wait_for_devtools(port: int, timeout_seconds: float) -> None:
|
|
113
|
+
deadline = time.monotonic() + timeout_seconds
|
|
114
|
+
endpoint = f"{devtools_url(port)}/json/version"
|
|
115
|
+
last_error: Exception | None = None
|
|
116
|
+
|
|
117
|
+
while time.monotonic() < deadline:
|
|
118
|
+
try:
|
|
119
|
+
with urllib.request.urlopen(endpoint, timeout=1) as response:
|
|
120
|
+
if response.status == 200:
|
|
121
|
+
return
|
|
122
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
123
|
+
last_error = exc
|
|
124
|
+
time.sleep(0.1)
|
|
125
|
+
|
|
126
|
+
message = f"Timed out waiting for Chrome DevTools at {endpoint}"
|
|
127
|
+
if last_error:
|
|
128
|
+
message = f"{message}: {last_error}"
|
|
129
|
+
raise TimeoutError(message)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def bridge_stream(source, target) -> None:
|
|
133
|
+
source_fd = source.fileno()
|
|
134
|
+
target_fd = target.fileno()
|
|
135
|
+
try:
|
|
136
|
+
while True:
|
|
137
|
+
chunk = os.read(source_fd, 64 * 1024)
|
|
138
|
+
if not chunk:
|
|
139
|
+
break
|
|
140
|
+
os.write(target_fd, chunk)
|
|
141
|
+
finally:
|
|
142
|
+
try:
|
|
143
|
+
target.close()
|
|
144
|
+
except OSError:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def terminate_process(process: subprocess.Popen[bytes], timeout_seconds: float = 5) -> None:
|
|
149
|
+
if process.poll() is not None:
|
|
150
|
+
return
|
|
151
|
+
if os.name == "nt":
|
|
152
|
+
subprocess.run(
|
|
153
|
+
["taskkill", "/PID", str(process.pid), "/T", "/F"],
|
|
154
|
+
stdout=subprocess.DEVNULL,
|
|
155
|
+
stderr=subprocess.DEVNULL,
|
|
156
|
+
check=False,
|
|
157
|
+
)
|
|
158
|
+
try:
|
|
159
|
+
process.wait(timeout=timeout_seconds)
|
|
160
|
+
except subprocess.TimeoutExpired:
|
|
161
|
+
process.kill()
|
|
162
|
+
process.wait(timeout=timeout_seconds)
|
|
163
|
+
return
|
|
164
|
+
process.terminate()
|
|
165
|
+
try:
|
|
166
|
+
process.wait(timeout=timeout_seconds)
|
|
167
|
+
except subprocess.TimeoutExpired:
|
|
168
|
+
process.kill()
|
|
169
|
+
process.wait(timeout=timeout_seconds)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def terminate_chrome_profile_processes(user_data_dir: Path) -> None:
|
|
173
|
+
if os.name != "nt":
|
|
174
|
+
return
|
|
175
|
+
powershell_path = (
|
|
176
|
+
Path(os.environ.get("SystemRoot", r"C:\Windows"))
|
|
177
|
+
/ "System32"
|
|
178
|
+
/ "WindowsPowerShell"
|
|
179
|
+
/ "v1.0"
|
|
180
|
+
/ "powershell.exe"
|
|
181
|
+
)
|
|
182
|
+
if not powershell_path.exists():
|
|
183
|
+
return
|
|
184
|
+
profile_marker = str(user_data_dir)
|
|
185
|
+
command = (
|
|
186
|
+
"Get-CimInstance Win32_Process -Filter \"name = 'chrome.exe'\" | "
|
|
187
|
+
"Where-Object { $_.CommandLine -like ('*' + $env:CHROME_MCP_PROFILE_MARKER + '*') } | "
|
|
188
|
+
"ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"
|
|
189
|
+
)
|
|
190
|
+
env = dict(os.environ)
|
|
191
|
+
env["CHROME_MCP_PROFILE_MARKER"] = profile_marker
|
|
192
|
+
subprocess.run(
|
|
193
|
+
[str(powershell_path), "-NoProfile", "-Command", command],
|
|
194
|
+
stdout=subprocess.DEVNULL,
|
|
195
|
+
stderr=subprocess.DEVNULL,
|
|
196
|
+
env=env,
|
|
197
|
+
check=False,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def remove_directory_with_retries(path: Path, attempts: int = 20, delay_seconds: float = 0.25) -> None:
|
|
202
|
+
for attempt in range(attempts):
|
|
203
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
204
|
+
if not path.exists():
|
|
205
|
+
return
|
|
206
|
+
if attempt < attempts - 1:
|
|
207
|
+
time.sleep(delay_seconds)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def run_downstream(command: Sequence[str], env: Mapping[str, str]) -> int:
|
|
211
|
+
downstream = subprocess.Popen(
|
|
212
|
+
list(command),
|
|
213
|
+
stdin=subprocess.PIPE,
|
|
214
|
+
stdout=subprocess.PIPE,
|
|
215
|
+
stderr=None,
|
|
216
|
+
env=dict(env),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
stdin_thread = threading.Thread(
|
|
220
|
+
target=bridge_stream,
|
|
221
|
+
args=(sys.stdin.buffer, downstream.stdin),
|
|
222
|
+
daemon=True,
|
|
223
|
+
)
|
|
224
|
+
stdout_thread = threading.Thread(
|
|
225
|
+
target=bridge_stream,
|
|
226
|
+
args=(downstream.stdout, sys.stdout.buffer),
|
|
227
|
+
daemon=True,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def stop_downstream_when_input_closes() -> None:
|
|
231
|
+
stdin_thread.join()
|
|
232
|
+
if downstream.poll() is None:
|
|
233
|
+
time.sleep(0.5)
|
|
234
|
+
if downstream.poll() is None:
|
|
235
|
+
terminate_process(downstream)
|
|
236
|
+
|
|
237
|
+
stdin_thread.start()
|
|
238
|
+
stdout_thread.start()
|
|
239
|
+
input_watcher = threading.Thread(target=stop_downstream_when_input_closes, daemon=True)
|
|
240
|
+
input_watcher.start()
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
return downstream.wait()
|
|
244
|
+
except KeyboardInterrupt:
|
|
245
|
+
terminate_process(downstream)
|
|
246
|
+
return 130
|
|
247
|
+
finally:
|
|
248
|
+
stdout_thread.join(timeout=1)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
252
|
+
parser = argparse.ArgumentParser(
|
|
253
|
+
description="Start a session-local Chrome instance and run a downstream MCP command."
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"--chrome-path",
|
|
257
|
+
help="Path to chrome.exe. Defaults to CHROME_PATH or common Windows install paths.",
|
|
258
|
+
)
|
|
259
|
+
parser.add_argument(
|
|
260
|
+
"--user-data-dir",
|
|
261
|
+
help="Exact profile directory to use. Overrides --session-root.",
|
|
262
|
+
)
|
|
263
|
+
parser.add_argument(
|
|
264
|
+
"--session-root",
|
|
265
|
+
default=None,
|
|
266
|
+
help="Directory for generated session profiles. Defaults to .chrome-mcp-sessions in the current working directory.",
|
|
267
|
+
)
|
|
268
|
+
parser.add_argument(
|
|
269
|
+
"--keep-profile",
|
|
270
|
+
action="store_true",
|
|
271
|
+
help="Do not delete the session profile after the downstream command exits.",
|
|
272
|
+
)
|
|
273
|
+
parser.add_argument(
|
|
274
|
+
"--headless",
|
|
275
|
+
action="store_true",
|
|
276
|
+
help="Start Chrome in headless mode.",
|
|
277
|
+
)
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"--devtools-timeout",
|
|
280
|
+
type=float,
|
|
281
|
+
default=15.0,
|
|
282
|
+
help="Seconds to wait for Chrome DevTools to become ready.",
|
|
283
|
+
)
|
|
284
|
+
parser.add_argument(
|
|
285
|
+
"--chrome-arg",
|
|
286
|
+
action="append",
|
|
287
|
+
default=[],
|
|
288
|
+
help="Extra argument passed to Chrome. Repeat for multiple values.",
|
|
289
|
+
)
|
|
290
|
+
parser.add_argument(
|
|
291
|
+
"command",
|
|
292
|
+
nargs=argparse.REMAINDER,
|
|
293
|
+
help="Downstream MCP command. Prefix it with --, for example: -- npx chrome-devtools-mcp",
|
|
294
|
+
)
|
|
295
|
+
return parser
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def normalize_command(command: Sequence[str]) -> list[str]:
|
|
299
|
+
normalized = list(command)
|
|
300
|
+
if normalized and normalized[0] == "--":
|
|
301
|
+
normalized = normalized[1:]
|
|
302
|
+
if not normalized:
|
|
303
|
+
raise ValueError("Missing downstream MCP command after --.")
|
|
304
|
+
return normalized
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
308
|
+
parser = create_parser()
|
|
309
|
+
args = parser.parse_args(argv)
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
command = normalize_command(args.command)
|
|
313
|
+
chrome_path = resolve_chrome_path(args.chrome_path)
|
|
314
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
315
|
+
parser.error(str(exc))
|
|
316
|
+
|
|
317
|
+
port = find_free_port()
|
|
318
|
+
generated_session_dir = args.user_data_dir is None
|
|
319
|
+
user_data_dir = (
|
|
320
|
+
Path(args.user_data_dir)
|
|
321
|
+
if args.user_data_dir
|
|
322
|
+
else resolve_session_root(args.session_root) / uuid.uuid4().hex
|
|
323
|
+
)
|
|
324
|
+
user_data_dir.mkdir(parents=True, exist_ok=True)
|
|
325
|
+
|
|
326
|
+
config = ChromeSessionConfig(
|
|
327
|
+
chrome_path=chrome_path,
|
|
328
|
+
port=port,
|
|
329
|
+
user_data_dir=user_data_dir,
|
|
330
|
+
headless=args.headless,
|
|
331
|
+
extra_args=tuple(args.chrome_arg),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
chrome = subprocess.Popen(
|
|
335
|
+
build_chrome_args(config),
|
|
336
|
+
stdin=subprocess.DEVNULL,
|
|
337
|
+
stdout=subprocess.DEVNULL,
|
|
338
|
+
stderr=subprocess.DEVNULL,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def cleanup() -> None:
|
|
342
|
+
terminate_process(chrome)
|
|
343
|
+
terminate_chrome_profile_processes(user_data_dir)
|
|
344
|
+
if generated_session_dir and not args.keep_profile:
|
|
345
|
+
remove_directory_with_retries(user_data_dir)
|
|
346
|
+
|
|
347
|
+
atexit.register(cleanup)
|
|
348
|
+
|
|
349
|
+
previous_sigterm = signal.getsignal(signal.SIGTERM)
|
|
350
|
+
|
|
351
|
+
def handle_sigterm(signum, frame) -> None:
|
|
352
|
+
cleanup()
|
|
353
|
+
if callable(previous_sigterm):
|
|
354
|
+
previous_sigterm(signum, frame)
|
|
355
|
+
raise SystemExit(143)
|
|
356
|
+
|
|
357
|
+
signal.signal(signal.SIGTERM, handle_sigterm)
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
wait_for_devtools(port, args.devtools_timeout)
|
|
361
|
+
env = downstream_env(os.environ, port, user_data_dir)
|
|
362
|
+
expanded_command = expand_downstream_command(command, port, user_data_dir)
|
|
363
|
+
return run_downstream(expanded_command, env)
|
|
364
|
+
except KeyboardInterrupt:
|
|
365
|
+
return 130
|
|
366
|
+
finally:
|
|
367
|
+
cleanup()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
if __name__ == "__main__":
|
|
371
|
+
raise SystemExit(main())
|
|
372
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chrome-devtools-mcp-canpoint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Session-local Chrome launcher for Chrome DevTools MCP workflows.
|
|
5
|
+
Author: whqp
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/whqp/chrome-devtools-mcp-canpoint
|
|
8
|
+
Project-URL: Issues, https://github.com/whqp/chrome-devtools-mcp-canpoint/issues
|
|
9
|
+
Keywords: mcp,chrome,devtools,browser,automation
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# chrome-devtools-mcp-canpoint
|
|
25
|
+
|
|
26
|
+
Session-local Chrome launcher for Chrome DevTools MCP workflows.
|
|
27
|
+
|
|
28
|
+
This wrapper starts one dedicated Chrome process per MCP server process. Each
|
|
29
|
+
session gets its own random remote debugging port and a generated user data
|
|
30
|
+
directory under the current working directory, so multiple agent sessions do not
|
|
31
|
+
fight over port `9222` or a shared Chrome profile.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Run it directly with `uvx`:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
uvx chrome-devtools-mcp-canpoint -- <downstream-mcp-command>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
uvx chrome-devtools-mcp-canpoint -- npx -y chrome-devtools-mcp@latest --browser-url={browser_url}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If Chrome is not installed in the default Windows location, pass it explicitly:
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
uvx chrome-devtools-mcp-canpoint --chrome-path "D:\Apps\Chrome\chrome.exe" -- npx -y chrome-devtools-mcp@latest --browser-url={browser_url}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Codex MCP Config
|
|
54
|
+
|
|
55
|
+
After publishing to PyPI, configure Codex like this:
|
|
56
|
+
|
|
57
|
+
```toml
|
|
58
|
+
[mcp_servers.chrome-devtools]
|
|
59
|
+
command = "uvx"
|
|
60
|
+
args = [
|
|
61
|
+
"chrome-devtools-mcp-canpoint",
|
|
62
|
+
"--",
|
|
63
|
+
"npx",
|
|
64
|
+
"-y",
|
|
65
|
+
"chrome-devtools-mcp@latest",
|
|
66
|
+
"--browser-url={browser_url}",
|
|
67
|
+
"--no-usage-statistics"
|
|
68
|
+
]
|
|
69
|
+
startup_timeout_sec = 60
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Session Profile Directory
|
|
73
|
+
|
|
74
|
+
By default, generated Chrome profiles are created under:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
<current-working-directory>/.chrome-mcp-sessions/<uuid>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Override the parent directory with `--session-root`:
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
uvx chrome-devtools-mcp-canpoint --session-root .\.chrome-mcp-sessions -- npx -y chrome-devtools-mcp@latest --browser-url={browser_url}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use `--user-data-dir` only when you want an exact profile directory instead of a
|
|
87
|
+
generated per-session subdirectory.
|
|
88
|
+
|
|
89
|
+
## Environment Passed to the Downstream MCP
|
|
90
|
+
|
|
91
|
+
The downstream command receives these variables:
|
|
92
|
+
|
|
93
|
+
- `CHROME_DEVTOOLS_URL`: `http://127.0.0.1:<random-port>`
|
|
94
|
+
- `BROWSER_URL`: same value as `CHROME_DEVTOOLS_URL`
|
|
95
|
+
- `CHROME_REMOTE_DEBUGGING_PORT`: selected port
|
|
96
|
+
- `CHROME_USER_DATA_DIR`: session profile path
|
|
97
|
+
|
|
98
|
+
Configure the real Chrome MCP package to use one of these values as its browser
|
|
99
|
+
endpoint. This wrapper also expands placeholders in downstream command
|
|
100
|
+
arguments:
|
|
101
|
+
|
|
102
|
+
- `{browser_url}` or `{devtools_url}`: `http://127.0.0.1:<random-port>`
|
|
103
|
+
- `{port}`: selected port
|
|
104
|
+
- `{user_data_dir}`: generated profile path
|
|
105
|
+
|
|
106
|
+
## Cleanup
|
|
107
|
+
|
|
108
|
+
When the downstream MCP exits, this wrapper terminates only the Chrome process it
|
|
109
|
+
started. Temporary profiles are deleted by default.
|
|
110
|
+
|
|
111
|
+
Use `--keep-profile` when debugging:
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
uvx chrome-devtools-mcp-canpoint --keep-profile -- npx -y chrome-devtools-mcp@latest --browser-url={browser_url}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Options
|
|
118
|
+
|
|
119
|
+
```powershell
|
|
120
|
+
uvx chrome-devtools-mcp-canpoint --help
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Useful options:
|
|
124
|
+
|
|
125
|
+
- `--chrome-path`: path to `chrome.exe`
|
|
126
|
+
- `--session-root`: parent directory for generated project-local profiles
|
|
127
|
+
- `--user-data-dir`: explicit profile directory instead of a generated one
|
|
128
|
+
- `--keep-profile`: leave the temporary profile on disk
|
|
129
|
+
- `--headless`: start Chrome with `--headless=new`
|
|
130
|
+
- `--chrome-arg`: pass extra arguments to Chrome, repeatable
|
|
131
|
+
- `--devtools-timeout`: seconds to wait for `/json/version`
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
Use the project virtual environment for local development:
|
|
136
|
+
|
|
137
|
+
```powershell
|
|
138
|
+
.\.venv\Scripts\python.exe -m pip install -e .
|
|
139
|
+
.\.venv\Scripts\python.exe -m unittest discover -v
|
|
140
|
+
.\.venv\Scripts\chrome-devtools-mcp-canpoint.exe --help
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Build distributions:
|
|
144
|
+
|
|
145
|
+
```powershell
|
|
146
|
+
.\.venv\Scripts\python.exe -m build
|
|
147
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
chrome_devtools_mcp_canpoint/__init__.py,sha256=tAlW8QtOcLPqyI-AddCRncDxr23oR2rfsWETqLVUEpc,94
|
|
2
|
+
chrome_devtools_mcp_canpoint/cli.py,sha256=_hA1UuRpE1CiZahUznjpYnqN8VGBSQceUzzeicfXBsE,11200
|
|
3
|
+
chrome_devtools_mcp_canpoint-0.1.0.dist-info/METADATA,sha256=ItPsjPblnZlciEDdH0e_T2AmjoZrQbg1zE2_NeBQDrI,4546
|
|
4
|
+
chrome_devtools_mcp_canpoint-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
chrome_devtools_mcp_canpoint-0.1.0.dist-info/entry_points.txt,sha256=95pH29FYx0d12PWVv6k1_0bXPIRt9tRLhguxdJ6vuj8,87
|
|
6
|
+
chrome_devtools_mcp_canpoint-0.1.0.dist-info/top_level.txt,sha256=SLopCQhQJP1Iue2eIKRg9EOGopWRFR4NGWk1otaI1tY,29
|
|
7
|
+
chrome_devtools_mcp_canpoint-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chrome_devtools_mcp_canpoint
|