patchright-cli 0.7.0__py3-none-any.whl → 0.9.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.
@@ -1,3 +1,3 @@
1
1
  """patchright-cli — Undetected browser automation CLI using Patchright."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.9.0"
patchright_cli/cli.py CHANGED
@@ -6,6 +6,7 @@ receives the result, and prints it.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import argparse
9
10
  import json
10
11
  import os
11
12
  import socket
@@ -16,7 +17,7 @@ from pathlib import Path
16
17
  import click
17
18
 
18
19
  from patchright_cli import __version__
19
- from patchright_cli.daemon import DEFAULT_PORT, ensure_daemon_running
20
+ from patchright_cli.daemon import DEFAULT_PORT, ensure_daemon_running, read_daemon_token
20
21
 
21
22
 
22
23
  def _soften_output_encoding(*streams) -> None:
@@ -248,6 +249,36 @@ def _handle_install(args: list) -> None:
248
249
  click.echo(f"\npatchright-cli v{__version__} skills installed.")
249
250
 
250
251
 
252
+ def _handle_mcp(args: list[str]) -> None:
253
+ """Handle the optional MCP server command."""
254
+ parser = argparse.ArgumentParser(
255
+ prog="patchright-cli mcp",
256
+ description="Run the MCP server. Stdio is the default transport.",
257
+ )
258
+ parser.add_argument("--http", action="store_true", help="Use Streamable HTTP instead of stdio.")
259
+ parser.add_argument("--host", default="127.0.0.1", help="HTTP bind host.")
260
+ parser.add_argument("--port", type=int, default=8000, help="HTTP bind port.")
261
+ options = parser.parse_args(args)
262
+
263
+ if not 1 <= options.port <= 65535:
264
+ parser.error("--port must be between 1 and 65535")
265
+ if not options.http and (options.host != "127.0.0.1" or options.port != 8000):
266
+ parser.error("--host and --port require --http")
267
+
268
+ try:
269
+ from patchright_cli.mcp_server import run_mcp_server
270
+ except ImportError as exc:
271
+ if exc.name not in {"fastmcp", "mcp", "pydantic"}:
272
+ raise
273
+ click.echo(
274
+ "MCP support is not installed. Install it with: pip install 'patchright-cli[mcp]'",
275
+ err=True,
276
+ )
277
+ raise SystemExit(1) from exc
278
+
279
+ run_mcp_server(http=options.http, host=options.host, port=options.port)
280
+
281
+
251
282
  _VERSION_CHECK_INTERVAL = 24 * 60 * 60
252
283
  _PYPI_URL = "https://pypi.org/pypi/patchright-cli/json"
253
284
 
@@ -361,6 +392,11 @@ def _send_command(command: str, args: list, options: dict, port: int = DEFAULT_P
361
392
  "options": options,
362
393
  "cwd": os.getcwd(),
363
394
  }
395
+ # A daemon that predates tokens writes no token file and accepts the
396
+ # message without one, so a missing file is not an error here.
397
+ token = read_daemon_token(port)
398
+ if token:
399
+ msg["token"] = token
364
400
  data = json.dumps(msg).encode("utf-8")
365
401
 
366
402
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -395,7 +431,7 @@ def _recv_exact(sock: socket.socket, n: int) -> bytes:
395
431
 
396
432
  COMMANDS_HELP = {
397
433
  # Core
398
- "open": "open [url] Open browser (starts daemon if needed)",
434
+ "open": "open [url] Open browser (starts daemon if needed) [--block-networks=link-local|private] [--web-only]",
399
435
  "attach": "attach --cdp=<url> [--context=new|host] Attach to running Chrome via CDP",
400
436
  "goto": "goto <url> Navigate to URL",
401
437
  "click": "click <ref> [button] Click element [--modifiers=Alt,Shift]",
@@ -510,6 +546,7 @@ COMMANDS_HELP = {
510
546
  "codegen-stop": "codegen-stop [file] Stop recording and save script",
511
547
  # Setup
512
548
  "install": "install --skills Install skill files for AI agents",
549
+ "mcp": "mcp [--http] [--host=HOST] [--port=N] Start MCP server (stdio by default)",
513
550
  }
514
551
 
515
552
  ALL_COMMANDS = list(COMMANDS_HELP.keys())
@@ -616,7 +653,7 @@ def _print_help():
616
653
  ("Profiles", ["profile-list", "profile-delete"]),
617
654
  ("Dashboard", ["show"]),
618
655
  ("Codegen", ["codegen", "codegen-stop"]),
619
- ("Setup", ["install"]),
656
+ ("Setup", ["install", "mcp"]),
620
657
  ]
621
658
  for cat_name, cmds in categories:
622
659
  click.echo(f"\n {cat_name}:")
@@ -630,6 +667,13 @@ def main():
630
667
  _soften_output_encoding(sys.stdout, sys.stderr)
631
668
  argv = sys.argv[1:]
632
669
 
670
+ # MCP is a self-contained subcommand with its own transport options.
671
+ # Dispatch it before global option parsing so `patchright-cli mcp --help`
672
+ # shows MCP help rather than the top-level CLI help.
673
+ if argv and argv[0] == "mcp":
674
+ _handle_mcp(argv[1:])
675
+ return
676
+
633
677
  # Parse global options manually (before the command)
634
678
  headless = False
635
679
  persistent = False
@@ -649,6 +693,10 @@ def main():
649
693
  i = 0
650
694
  while i < len(argv):
651
695
  arg = argv[i]
696
+ if arg == "--":
697
+ # End of options: the rest is positional, even text that looks like a flag.
698
+ remaining.extend(argv[i:])
699
+ break
652
700
  if arg == "--headless":
653
701
  headless = True
654
702
  elif arg == "--persistent":
@@ -779,6 +827,10 @@ def main():
779
827
  _handle_install(args + [k if v is True else f"{k}={v}" for k, v in extra_opts.items()])
780
828
  sys.exit(0)
781
829
 
830
+ if command == "mcp":
831
+ _handle_mcp(args)
832
+ sys.exit(0)
833
+
782
834
  _warn_stale_skills()
783
835
 
784
836
  # Only on `open`: one browser launch already costs seconds, and this keeps
@@ -789,8 +841,13 @@ def main():
789
841
  # Separate --key=value and --flag from positional args
790
842
  positional_args = []
791
843
  cmd_opts: dict = {}
844
+ end_of_options = False
792
845
  for a in args:
793
- if a.startswith("--") and "=" in a:
846
+ if end_of_options:
847
+ positional_args.append(a)
848
+ elif a == "--":
849
+ end_of_options = True
850
+ elif a.startswith("--") and "=" in a:
794
851
  k, v = a[2:].split("=", 1)
795
852
  cmd_opts[k] = v
796
853
  elif a.startswith("--"):
@@ -812,7 +869,7 @@ def main():
812
869
  except FileNotFoundError:
813
870
  click.echo(f"File not found: {file_opt}", err=True)
814
871
  sys.exit(1)
815
- elif args and args[0] == "-":
872
+ elif args and args[0] == "-" and not end_of_options:
816
873
  # Read JS from stdin
817
874
  args = [sys.stdin.read()]
818
875
  elif not args and not sys.stdin.isatty():