patchright-cli 0.7.0__py3-none-any.whl → 0.8.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.8.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
@@ -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
 
@@ -510,6 +541,7 @@ COMMANDS_HELP = {
510
541
  "codegen-stop": "codegen-stop [file] Stop recording and save script",
511
542
  # Setup
512
543
  "install": "install --skills Install skill files for AI agents",
544
+ "mcp": "mcp [--http] [--host=HOST] [--port=N] Start MCP server (stdio by default)",
513
545
  }
514
546
 
515
547
  ALL_COMMANDS = list(COMMANDS_HELP.keys())
@@ -616,7 +648,7 @@ def _print_help():
616
648
  ("Profiles", ["profile-list", "profile-delete"]),
617
649
  ("Dashboard", ["show"]),
618
650
  ("Codegen", ["codegen", "codegen-stop"]),
619
- ("Setup", ["install"]),
651
+ ("Setup", ["install", "mcp"]),
620
652
  ]
621
653
  for cat_name, cmds in categories:
622
654
  click.echo(f"\n {cat_name}:")
@@ -630,6 +662,13 @@ def main():
630
662
  _soften_output_encoding(sys.stdout, sys.stderr)
631
663
  argv = sys.argv[1:]
632
664
 
665
+ # MCP is a self-contained subcommand with its own transport options.
666
+ # Dispatch it before global option parsing so `patchright-cli mcp --help`
667
+ # shows MCP help rather than the top-level CLI help.
668
+ if argv and argv[0] == "mcp":
669
+ _handle_mcp(argv[1:])
670
+ return
671
+
633
672
  # Parse global options manually (before the command)
634
673
  headless = False
635
674
  persistent = False
@@ -779,6 +818,10 @@ def main():
779
818
  _handle_install(args + [k if v is True else f"{k}={v}" for k, v in extra_opts.items()])
780
819
  sys.exit(0)
781
820
 
821
+ if command == "mcp":
822
+ _handle_mcp(args)
823
+ sys.exit(0)
824
+
782
825
  _warn_stale_skills()
783
826
 
784
827
  # Only on `open`: one browser launch already costs seconds, and this keeps
patchright_cli/daemon.py CHANGED
@@ -2846,6 +2846,30 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
2846
2846
  handler = COMMAND_HANDLERS.get("show")
2847
2847
  return await handler(None, None, args, options, cwd, state)
2848
2848
 
2849
+ if cmd == "list":
2850
+ lines = ["### Sessions"]
2851
+ for sname, s in state.sessions.items():
2852
+ marker = " *" if sname == session_name else ""
2853
+ tab_count = len(s.pages)
2854
+ lines.append(f" - {sname}{marker}: {tab_count} tab(s)")
2855
+ return {"success": True, "output": "\n".join(lines)}
2856
+
2857
+ if cmd == "close-all":
2858
+ names = list(state.sessions.keys())
2859
+ for n in names:
2860
+ await state.close_session(n)
2861
+ return {"success": True, "output": f"Closed {len(names)} session(s)."}
2862
+
2863
+ if cmd == "kill-all":
2864
+ names = list(state.sessions.keys())
2865
+ for n in names:
2866
+ # close_session ends each Session the way it is allowed to
2867
+ # (Detach from a Host Context) and frees its Profile.
2868
+ await state.close_session(n)
2869
+ if state.shutdown_event:
2870
+ state.shutdown_event.set()
2871
+ return {"success": True, "output": f"Killed {len(names)} session(s) and stopping daemon."}
2872
+
2849
2873
  # All other commands require an existing session
2850
2874
  session = state.sessions.get(session_name)
2851
2875
  if session is None:
@@ -2857,14 +2881,6 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
2857
2881
  page = session.page
2858
2882
 
2859
2883
  # Commands that don't need a page
2860
- if cmd == "list":
2861
- lines = ["### Sessions"]
2862
- for sname, s in state.sessions.items():
2863
- marker = " *" if sname == session_name else ""
2864
- tab_count = len(s.pages)
2865
- lines.append(f" - {sname}{marker}: {tab_count} tab(s)")
2866
- return {"success": True, "output": "\n".join(lines)}
2867
-
2868
2884
  if cmd == "close":
2869
2885
  # `close` releases the tab it is addressed to. The browser goes when
2870
2886
  # the last tab does, so concurrent agents clean up without having to
@@ -2905,22 +2921,6 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
2905
2921
  return {"success": True, "output": f"Detached from '{session_name}' (Host Context left untouched)."}
2906
2922
  return {"success": True, "output": f"Detached from '{session_name}' (external browser kept running)."}
2907
2923
 
2908
- if cmd == "close-all":
2909
- names = list(state.sessions.keys())
2910
- for n in names:
2911
- await state.close_session(n)
2912
- return {"success": True, "output": f"Closed {len(names)} session(s)."}
2913
-
2914
- if cmd == "kill-all":
2915
- names = list(state.sessions.keys())
2916
- for n in names:
2917
- # close_session ends each Session the way it is allowed to
2918
- # (Detach from a Host Context) and frees its Profile.
2919
- await state.close_session(n)
2920
- if state.shutdown_event:
2921
- state.shutdown_event.set()
2922
- return {"success": True, "output": f"Killed {len(names)} session(s) and stopping daemon."}
2923
-
2924
2924
  handler = COMMAND_HANDLERS.get(cmd)
2925
2925
  if handler is None:
2926
2926
  return {"success": False, "output": f"Unknown command: {cmd}"}