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.
- patchright_cli/__init__.py +1 -1
- patchright_cli/cli.py +44 -1
- patchright_cli/daemon.py +24 -24
- patchright_cli/mcp_server.py +1325 -0
- {patchright_cli-0.7.0.dist-info → patchright_cli-0.8.0.dist-info}/METADATA +32 -1
- {patchright_cli-0.7.0.dist-info → patchright_cli-0.8.0.dist-info}/RECORD +9 -8
- {patchright_cli-0.7.0.dist-info → patchright_cli-0.8.0.dist-info}/WHEEL +0 -0
- {patchright_cli-0.7.0.dist-info → patchright_cli-0.8.0.dist-info}/entry_points.txt +0 -0
- {patchright_cli-0.7.0.dist-info → patchright_cli-0.8.0.dist-info}/licenses/LICENSE +0 -0
patchright_cli/__init__.py
CHANGED
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}"}
|