kubectl-mcp-server 1.13.0__py3-none-any.whl → 1.14.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.
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/METADATA +1 -1
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/RECORD +14 -11
- kubectl_mcp_tool/__init__.py +1 -1
- kubectl_mcp_tool/cli/__init__.py +53 -1
- kubectl_mcp_tool/cli/cli.py +476 -17
- kubectl_mcp_tool/cli/errors.py +262 -0
- kubectl_mcp_tool/cli/output.py +377 -0
- kubectl_mcp_tool/tools/browser.py +316 -28
- tests/test_browser.py +167 -5
- tests/test_cli.py +299 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/WHEEL +0 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/entry_points.txt +0 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/licenses/LICENSE +0 -0
- {kubectl_mcp_server-1.13.0.dist-info → kubectl_mcp_server-1.14.0.dist-info}/top_level.txt +0 -0
|
@@ -5,7 +5,9 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
import shutil
|
|
7
7
|
import subprocess
|
|
8
|
-
|
|
8
|
+
import tempfile
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
9
11
|
|
|
10
12
|
from mcp.types import ToolAnnotations
|
|
11
13
|
|
|
@@ -14,9 +16,34 @@ logger = logging.getLogger("mcp-server")
|
|
|
14
16
|
BROWSER_ENABLED = os.environ.get("MCP_BROWSER_ENABLED", "").lower() in ("1", "true")
|
|
15
17
|
BROWSER_AVAILABLE = shutil.which("agent-browser") is not None
|
|
16
18
|
|
|
19
|
+
MCP_BROWSER_PROVIDER = os.environ.get("MCP_BROWSER_PROVIDER")
|
|
20
|
+
MCP_BROWSER_PROFILE = os.environ.get("MCP_BROWSER_PROFILE")
|
|
21
|
+
MCP_BROWSER_CDP_URL = os.environ.get("MCP_BROWSER_CDP_URL")
|
|
22
|
+
MCP_BROWSER_PROXY = os.environ.get("MCP_BROWSER_PROXY")
|
|
23
|
+
MCP_BROWSER_PROXY_BYPASS = os.environ.get("MCP_BROWSER_PROXY_BYPASS")
|
|
24
|
+
MCP_BROWSER_USER_AGENT = os.environ.get("MCP_BROWSER_USER_AGENT")
|
|
25
|
+
MCP_BROWSER_ARGS = os.environ.get("MCP_BROWSER_ARGS")
|
|
26
|
+
MCP_BROWSER_SESSION = os.environ.get("MCP_BROWSER_SESSION")
|
|
27
|
+
MCP_BROWSER_HEADED = os.environ.get("MCP_BROWSER_HEADED", "").lower() in ("1", "true")
|
|
28
|
+
|
|
29
|
+
MCP_BROWSER_DEBUG = os.environ.get("MCP_BROWSER_DEBUG", "").lower() in ("1", "true")
|
|
30
|
+
MCP_BROWSER_MAX_RETRIES = int(os.environ.get("MCP_BROWSER_MAX_RETRIES", "3"))
|
|
31
|
+
MCP_BROWSER_RETRY_DELAY = int(os.environ.get("MCP_BROWSER_RETRY_DELAY", "1000"))
|
|
32
|
+
MCP_BROWSER_TIMEOUT = int(os.environ.get("MCP_BROWSER_TIMEOUT", "60"))
|
|
33
|
+
|
|
34
|
+
TRANSIENT_ERRORS = [
|
|
35
|
+
"ECONNREFUSED",
|
|
36
|
+
"ETIMEDOUT",
|
|
37
|
+
"ECONNRESET",
|
|
38
|
+
"EPIPE",
|
|
39
|
+
"timeout",
|
|
40
|
+
"Timeout",
|
|
41
|
+
"connection refused",
|
|
42
|
+
"socket hang up",
|
|
43
|
+
]
|
|
44
|
+
|
|
17
45
|
|
|
18
46
|
def is_browser_available() -> bool:
|
|
19
|
-
"""Check if browser tools should be registered."""
|
|
20
47
|
if not BROWSER_ENABLED:
|
|
21
48
|
return False
|
|
22
49
|
if not BROWSER_AVAILABLE:
|
|
@@ -25,30 +52,109 @@ def is_browser_available() -> bool:
|
|
|
25
52
|
return True
|
|
26
53
|
|
|
27
54
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
55
|
+
def _debug(message: str) -> None:
|
|
56
|
+
if MCP_BROWSER_DEBUG:
|
|
57
|
+
logger.debug(f"[browser] {message}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_global_options() -> List[str]:
|
|
61
|
+
opts = []
|
|
62
|
+
if provider := os.environ.get("MCP_BROWSER_PROVIDER"):
|
|
63
|
+
opts.extend(["-p", provider])
|
|
64
|
+
if profile := os.environ.get("MCP_BROWSER_PROFILE"):
|
|
65
|
+
opts.extend(["--profile", os.path.expanduser(profile)])
|
|
66
|
+
if session := os.environ.get("MCP_BROWSER_SESSION"):
|
|
67
|
+
opts.extend(["--session", session])
|
|
68
|
+
if cdp := os.environ.get("MCP_BROWSER_CDP_URL"):
|
|
69
|
+
opts.extend(["--cdp", cdp])
|
|
70
|
+
if proxy := os.environ.get("MCP_BROWSER_PROXY"):
|
|
71
|
+
opts.extend(["--proxy", proxy])
|
|
72
|
+
if proxy_bypass := os.environ.get("MCP_BROWSER_PROXY_BYPASS"):
|
|
73
|
+
opts.extend(["--proxy-bypass", proxy_bypass])
|
|
74
|
+
if user_agent := os.environ.get("MCP_BROWSER_USER_AGENT"):
|
|
75
|
+
opts.extend(["--user-agent", user_agent])
|
|
76
|
+
if args := os.environ.get("MCP_BROWSER_ARGS"):
|
|
77
|
+
opts.extend(["--args", args])
|
|
78
|
+
if os.environ.get("MCP_BROWSER_HEADED", "").lower() in ("1", "true"):
|
|
79
|
+
opts.append("--headed")
|
|
80
|
+
return opts
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _is_transient_error(error_msg: str) -> bool:
|
|
84
|
+
return any(err.lower() in error_msg.lower() for err in TRANSIENT_ERRORS)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _run_browser(
|
|
88
|
+
args: List[str],
|
|
89
|
+
timeout: int = None,
|
|
90
|
+
use_global_opts: bool = True
|
|
91
|
+
) -> Dict[str, Any]:
|
|
92
|
+
timeout = timeout or MCP_BROWSER_TIMEOUT
|
|
93
|
+
|
|
94
|
+
cmd = ["agent-browser"]
|
|
95
|
+
if use_global_opts:
|
|
96
|
+
cmd.extend(_get_global_options())
|
|
97
|
+
cmd.extend(args)
|
|
98
|
+
|
|
99
|
+
_debug(f"Running: {' '.join(cmd)}")
|
|
100
|
+
|
|
31
101
|
try:
|
|
32
102
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
103
|
+
|
|
33
104
|
if result.returncode != 0:
|
|
34
|
-
|
|
105
|
+
error_msg = result.stderr.strip() or "Command failed"
|
|
106
|
+
_debug(f"Command failed: {error_msg}")
|
|
107
|
+
return {"success": False, "error": error_msg}
|
|
108
|
+
|
|
35
109
|
output = result.stdout.strip()
|
|
36
110
|
if "--json" in args:
|
|
37
111
|
try:
|
|
38
112
|
return {"success": True, "data": json.loads(output)}
|
|
39
113
|
except json.JSONDecodeError:
|
|
40
114
|
return {"success": True, "output": output}
|
|
115
|
+
|
|
41
116
|
return {"success": True, "output": output}
|
|
117
|
+
|
|
42
118
|
except subprocess.TimeoutExpired:
|
|
119
|
+
_debug(f"Command timed out after {timeout}s")
|
|
43
120
|
return {"success": False, "error": f"Command timed out after {timeout}s"}
|
|
44
121
|
except FileNotFoundError:
|
|
45
|
-
return {"success": False, "error": "agent-browser not found"}
|
|
122
|
+
return {"success": False, "error": "agent-browser not found. Install with: npm install -g agent-browser"}
|
|
46
123
|
except Exception as e:
|
|
124
|
+
_debug(f"Command error: {e}")
|
|
47
125
|
return {"success": False, "error": str(e)}
|
|
48
126
|
|
|
49
127
|
|
|
128
|
+
def _run_browser_with_retry(
|
|
129
|
+
args: List[str],
|
|
130
|
+
timeout: int = None,
|
|
131
|
+
max_retries: int = None,
|
|
132
|
+
use_global_opts: bool = True
|
|
133
|
+
) -> Dict[str, Any]:
|
|
134
|
+
max_retries = max_retries if max_retries is not None else MCP_BROWSER_MAX_RETRIES
|
|
135
|
+
delay_ms = MCP_BROWSER_RETRY_DELAY
|
|
136
|
+
|
|
137
|
+
for attempt in range(max_retries + 1):
|
|
138
|
+
result = _run_browser(args, timeout=timeout, use_global_opts=use_global_opts)
|
|
139
|
+
|
|
140
|
+
if result.get("success"):
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
error_msg = result.get("error", "")
|
|
144
|
+
if not _is_transient_error(error_msg):
|
|
145
|
+
return result
|
|
146
|
+
if attempt == max_retries:
|
|
147
|
+
_debug(f"Max retries ({max_retries}) exceeded")
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
wait_time = (delay_ms * (2 ** attempt)) / 1000
|
|
151
|
+
_debug(f"Transient error, retrying in {wait_time}s (attempt {attempt + 1}/{max_retries})")
|
|
152
|
+
time.sleep(wait_time)
|
|
153
|
+
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
50
157
|
def _get_ingress_url(service: str, namespace: str) -> Optional[str]:
|
|
51
|
-
"""Get ingress URL for a service."""
|
|
52
158
|
try:
|
|
53
159
|
from kubernetes import client, config
|
|
54
160
|
config.load_kube_config()
|
|
@@ -68,7 +174,6 @@ def _get_ingress_url(service: str, namespace: str) -> Optional[str]:
|
|
|
68
174
|
|
|
69
175
|
|
|
70
176
|
def _get_service_url(service: str, namespace: str) -> Optional[str]:
|
|
71
|
-
"""Get service URL (LoadBalancer or NodePort)."""
|
|
72
177
|
try:
|
|
73
178
|
from kubernetes import client, config
|
|
74
179
|
config.load_kube_config()
|
|
@@ -89,19 +194,55 @@ def _get_service_url(service: str, namespace: str) -> Optional[str]:
|
|
|
89
194
|
|
|
90
195
|
|
|
91
196
|
def register_browser_tools(server, non_destructive: bool):
|
|
92
|
-
|
|
197
|
+
_ = non_destructive
|
|
93
198
|
|
|
94
199
|
@server.tool(annotations=ToolAnnotations(title="Browser Open URL", readOnlyHint=True))
|
|
95
|
-
def browser_open(
|
|
96
|
-
|
|
97
|
-
|
|
200
|
+
def browser_open(
|
|
201
|
+
url: str,
|
|
202
|
+
wait_for: str = "networkidle",
|
|
203
|
+
headers: Optional[Dict[str, str]] = None,
|
|
204
|
+
session: Optional[str] = None,
|
|
205
|
+
headed: bool = False
|
|
206
|
+
) -> Dict[str, Any]:
|
|
207
|
+
"""Open a URL in the browser.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
url: URL to navigate to
|
|
211
|
+
wait_for: Load state to wait for (load, domcontentloaded, networkidle)
|
|
212
|
+
headers: HTTP headers to set for this origin (v0.7+)
|
|
213
|
+
session: Session name for isolation (v0.7+)
|
|
214
|
+
headed: Show browser window (v0.7+)
|
|
215
|
+
"""
|
|
216
|
+
# Build open command with optional headers
|
|
217
|
+
open_args = ["open", url]
|
|
218
|
+
|
|
219
|
+
if headers:
|
|
220
|
+
open_args.extend(["--headers", json.dumps(headers)])
|
|
221
|
+
if session:
|
|
222
|
+
open_args.extend(["--session", session])
|
|
223
|
+
if headed:
|
|
224
|
+
open_args.append("--headed")
|
|
225
|
+
|
|
226
|
+
result = _run_browser_with_retry(open_args)
|
|
98
227
|
if result.get("success") and wait_for:
|
|
99
228
|
_run_browser(["wait", "--load", wait_for])
|
|
100
229
|
return {**result, "url": url}
|
|
101
230
|
|
|
102
231
|
@server.tool(annotations=ToolAnnotations(title="Browser Snapshot", readOnlyHint=True))
|
|
103
|
-
def browser_snapshot(
|
|
104
|
-
|
|
232
|
+
def browser_snapshot(
|
|
233
|
+
interactive_only: bool = True,
|
|
234
|
+
compact: bool = True,
|
|
235
|
+
depth: Optional[int] = None,
|
|
236
|
+
selector: Optional[str] = None
|
|
237
|
+
) -> Dict[str, Any]:
|
|
238
|
+
"""Get accessibility tree snapshot of current page.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
interactive_only: Only show interactive elements (buttons, inputs, links)
|
|
242
|
+
compact: Remove empty structural elements
|
|
243
|
+
depth: Limit tree depth
|
|
244
|
+
selector: Scope to CSS selector
|
|
245
|
+
"""
|
|
105
246
|
args = ["snapshot", "--json"]
|
|
106
247
|
if interactive_only:
|
|
107
248
|
args.append("-i")
|
|
@@ -109,6 +250,8 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
109
250
|
args.append("-c")
|
|
110
251
|
if depth:
|
|
111
252
|
args.extend(["-d", str(depth)])
|
|
253
|
+
if selector:
|
|
254
|
+
args.extend(["-s", selector])
|
|
112
255
|
return _run_browser(args)
|
|
113
256
|
|
|
114
257
|
@server.tool(annotations=ToolAnnotations(title="Browser Click"))
|
|
@@ -122,11 +265,26 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
122
265
|
return _run_browser(["fill", ref, text])
|
|
123
266
|
|
|
124
267
|
@server.tool(annotations=ToolAnnotations(title="Browser Screenshot", readOnlyHint=True))
|
|
125
|
-
def browser_screenshot(
|
|
126
|
-
|
|
268
|
+
def browser_screenshot(
|
|
269
|
+
output_path: Optional[str] = None,
|
|
270
|
+
full_page: bool = False,
|
|
271
|
+
ref: Optional[str] = None
|
|
272
|
+
) -> Dict[str, Any]:
|
|
273
|
+
"""Take a screenshot of the current page or element.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
output_path: Path to save screenshot (auto-generated if None)
|
|
277
|
+
full_page: Capture full scrollable page
|
|
278
|
+
ref: Element ref to screenshot (from snapshot)
|
|
279
|
+
"""
|
|
280
|
+
if not output_path:
|
|
281
|
+
fd, output_path = tempfile.mkstemp(suffix=".png", prefix="screenshot_")
|
|
282
|
+
os.close(fd)
|
|
127
283
|
args = ["screenshot", output_path]
|
|
128
284
|
if full_page:
|
|
129
285
|
args.append("--full")
|
|
286
|
+
if ref:
|
|
287
|
+
args.extend(["--ref", ref])
|
|
130
288
|
result = _run_browser(args)
|
|
131
289
|
return {**result, "path": output_path}
|
|
132
290
|
|
|
@@ -141,7 +299,11 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
141
299
|
return _run_browser(["get", "url"])
|
|
142
300
|
|
|
143
301
|
@server.tool(annotations=ToolAnnotations(title="Browser Wait"))
|
|
144
|
-
def browser_wait(
|
|
302
|
+
def browser_wait(
|
|
303
|
+
selector: Optional[str] = None,
|
|
304
|
+
text: Optional[str] = None,
|
|
305
|
+
timeout_ms: int = 5000
|
|
306
|
+
) -> Dict[str, Any]:
|
|
145
307
|
"""Wait for element, text, or timeout."""
|
|
146
308
|
if text:
|
|
147
309
|
return _run_browser(["wait", "--text", text], timeout=timeout_ms // 1000 + 5)
|
|
@@ -155,6 +317,129 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
155
317
|
"""Close the browser."""
|
|
156
318
|
return _run_browser(["close"])
|
|
157
319
|
|
|
320
|
+
@server.tool(annotations=ToolAnnotations(title="Browser Connect CDP", readOnlyHint=True))
|
|
321
|
+
def browser_connect_cdp(
|
|
322
|
+
target: str,
|
|
323
|
+
session: Optional[str] = None
|
|
324
|
+
) -> Dict[str, Any]:
|
|
325
|
+
"""Connect to an existing browser via Chrome DevTools Protocol.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
target: CDP port number (e.g., 9222) or WebSocket URL (wss://...)
|
|
329
|
+
session: Optional session name for this connection
|
|
330
|
+
"""
|
|
331
|
+
args = ["connect", target]
|
|
332
|
+
if session:
|
|
333
|
+
args.extend(["--session", session])
|
|
334
|
+
return _run_browser_with_retry(args, use_global_opts=False)
|
|
335
|
+
|
|
336
|
+
@server.tool(annotations=ToolAnnotations(title="Browser Install"))
|
|
337
|
+
def browser_install(with_deps: bool = False) -> Dict[str, Any]:
|
|
338
|
+
"""Install Chromium browser for agent-browser.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
with_deps: Also install system dependencies (Linux only)
|
|
342
|
+
"""
|
|
343
|
+
args = ["install"]
|
|
344
|
+
if with_deps:
|
|
345
|
+
args.append("--with-deps")
|
|
346
|
+
return _run_browser(args, timeout=300, use_global_opts=False)
|
|
347
|
+
|
|
348
|
+
@server.tool(annotations=ToolAnnotations(title="Browser Set Provider"))
|
|
349
|
+
def browser_set_provider(
|
|
350
|
+
provider: str,
|
|
351
|
+
api_key: Optional[str] = None,
|
|
352
|
+
project_id: Optional[str] = None
|
|
353
|
+
) -> Dict[str, Any]:
|
|
354
|
+
"""Configure cloud browser provider for current session.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
provider: Provider name (browserbase, browseruse)
|
|
358
|
+
api_key: API key (or use env var BROWSERBASE_API_KEY / BROWSER_USE_API_KEY)
|
|
359
|
+
project_id: Project ID for Browserbase
|
|
360
|
+
"""
|
|
361
|
+
# Set environment variables for the session
|
|
362
|
+
env_updates = {}
|
|
363
|
+
if provider.lower() == "browserbase":
|
|
364
|
+
if api_key:
|
|
365
|
+
env_updates["BROWSERBASE_API_KEY"] = api_key
|
|
366
|
+
if project_id:
|
|
367
|
+
env_updates["BROWSERBASE_PROJECT_ID"] = project_id
|
|
368
|
+
elif provider.lower() == "browseruse":
|
|
369
|
+
if api_key:
|
|
370
|
+
env_updates["BROWSER_USE_API_KEY"] = api_key
|
|
371
|
+
|
|
372
|
+
# Update environment
|
|
373
|
+
for key, value in env_updates.items():
|
|
374
|
+
os.environ[key] = value
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
"success": True,
|
|
378
|
+
"provider": provider,
|
|
379
|
+
"message": f"Provider set to {provider}. Use -p {provider} flag or set MCP_BROWSER_PROVIDER={provider}"
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@server.tool(annotations=ToolAnnotations(title="Browser Session List", readOnlyHint=True))
|
|
383
|
+
def browser_session_list() -> Dict[str, Any]:
|
|
384
|
+
"""List active browser sessions."""
|
|
385
|
+
return _run_browser(["session", "list"], use_global_opts=False)
|
|
386
|
+
|
|
387
|
+
@server.tool(annotations=ToolAnnotations(title="Browser Session Switch"))
|
|
388
|
+
def browser_session_switch(session: str) -> Dict[str, Any]:
|
|
389
|
+
"""Switch to a different browser session.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
session: Session name to switch to
|
|
393
|
+
"""
|
|
394
|
+
# Update the environment variable for subsequent commands
|
|
395
|
+
os.environ["MCP_BROWSER_SESSION"] = session
|
|
396
|
+
return {
|
|
397
|
+
"success": True,
|
|
398
|
+
"session": session,
|
|
399
|
+
"message": f"Switched to session: {session}"
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
@server.tool(annotations=ToolAnnotations(title="Browser Open With Headers", readOnlyHint=True))
|
|
403
|
+
def browser_open_with_headers(
|
|
404
|
+
url: str,
|
|
405
|
+
headers: Dict[str, str],
|
|
406
|
+
wait_for: str = "networkidle"
|
|
407
|
+
) -> Dict[str, Any]:
|
|
408
|
+
"""Open URL with custom HTTP headers (useful for authentication).
|
|
409
|
+
|
|
410
|
+
Headers are scoped to the URL's origin for security.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
url: URL to navigate to
|
|
414
|
+
headers: HTTP headers to set (e.g., {"Authorization": "Bearer token"})
|
|
415
|
+
wait_for: Load state to wait for
|
|
416
|
+
"""
|
|
417
|
+
args = ["open", url, "--headers", json.dumps(headers)]
|
|
418
|
+
result = _run_browser_with_retry(args)
|
|
419
|
+
if result.get("success") and wait_for:
|
|
420
|
+
_run_browser(["wait", "--load", wait_for])
|
|
421
|
+
return {**result, "url": url, "headers_set": list(headers.keys())}
|
|
422
|
+
|
|
423
|
+
@server.tool(annotations=ToolAnnotations(title="Browser Set Viewport"))
|
|
424
|
+
def browser_set_viewport(
|
|
425
|
+
width: Optional[int] = None,
|
|
426
|
+
height: Optional[int] = None,
|
|
427
|
+
device: Optional[str] = None
|
|
428
|
+
) -> Dict[str, Any]:
|
|
429
|
+
"""Set browser viewport size or emulate a device.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
width: Viewport width in pixels
|
|
433
|
+
height: Viewport height in pixels
|
|
434
|
+
device: Device to emulate (e.g., "iPhone 14", "iPad Pro")
|
|
435
|
+
"""
|
|
436
|
+
if device:
|
|
437
|
+
return _run_browser(["set", "device", device])
|
|
438
|
+
elif width and height:
|
|
439
|
+
return _run_browser(["set", "viewport", str(width), str(height)])
|
|
440
|
+
else:
|
|
441
|
+
return {"success": False, "error": "Specify device or both width and height"}
|
|
442
|
+
|
|
158
443
|
@server.tool(annotations=ToolAnnotations(title="Test K8s Ingress", readOnlyHint=True))
|
|
159
444
|
def browser_test_ingress(
|
|
160
445
|
service_name: str,
|
|
@@ -170,7 +455,7 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
170
455
|
return {"success": False, "error": f"No external URL found for {service_name} in {namespace}"}
|
|
171
456
|
|
|
172
457
|
full_url = f"{url}{path}"
|
|
173
|
-
open_result =
|
|
458
|
+
open_result = _run_browser_with_retry(["open", full_url])
|
|
174
459
|
if not open_result.get("success"):
|
|
175
460
|
return {**open_result, "url": full_url}
|
|
176
461
|
|
|
@@ -209,7 +494,7 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
209
494
|
return {"success": False, "error": f"No external URL found for {service_name} in {namespace}"}
|
|
210
495
|
|
|
211
496
|
full_url = f"{url}{path}"
|
|
212
|
-
|
|
497
|
+
_run_browser_with_retry(["open", full_url])
|
|
213
498
|
_run_browser(["wait", "--load", "networkidle"])
|
|
214
499
|
_run_browser(["wait", "2000"])
|
|
215
500
|
|
|
@@ -231,7 +516,7 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
231
516
|
if dashboard_uid:
|
|
232
517
|
url = f"{grafana_url.rstrip('/')}/d/{dashboard_uid}"
|
|
233
518
|
|
|
234
|
-
|
|
519
|
+
_run_browser_with_retry(["open", url])
|
|
235
520
|
_run_browser(["wait", "--load", "networkidle"])
|
|
236
521
|
_run_browser(["wait", "3000"])
|
|
237
522
|
result = _run_browser(["screenshot", "--full", output_path])
|
|
@@ -249,7 +534,7 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
249
534
|
if app_name:
|
|
250
535
|
url = f"{argocd_url.rstrip('/')}/applications/{app_name}"
|
|
251
536
|
|
|
252
|
-
|
|
537
|
+
_run_browser_with_retry(["open", url])
|
|
253
538
|
_run_browser(["wait", "--load", "networkidle"])
|
|
254
539
|
_run_browser(["wait", "2000"])
|
|
255
540
|
result = _run_browser(["screenshot", "--full", output_path])
|
|
@@ -263,7 +548,7 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
263
548
|
check_elements: Optional[list] = None
|
|
264
549
|
) -> Dict[str, Any]:
|
|
265
550
|
"""Perform health check on a web application."""
|
|
266
|
-
open_result =
|
|
551
|
+
open_result = _run_browser_with_retry(["open", url])
|
|
267
552
|
if not open_result.get("success"):
|
|
268
553
|
return {**open_result, "url": url, "healthy": False}
|
|
269
554
|
|
|
@@ -302,7 +587,7 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
302
587
|
submit_ref: Optional[str] = None
|
|
303
588
|
) -> Dict[str, Any]:
|
|
304
589
|
"""Fill and submit a web form."""
|
|
305
|
-
|
|
590
|
+
_run_browser_with_retry(["open", url])
|
|
306
591
|
_run_browser(["wait", "--load", "networkidle"])
|
|
307
592
|
|
|
308
593
|
for ref, value in form_data.items():
|
|
@@ -355,17 +640,20 @@ def register_browser_tools(server, non_destructive: bool):
|
|
|
355
640
|
if not url:
|
|
356
641
|
return {"success": False, "error": f"Unknown provider: {provider}. Use eks, gke, aks, or do"}
|
|
357
642
|
|
|
358
|
-
result =
|
|
643
|
+
result = _run_browser_with_retry(["open", url])
|
|
359
644
|
return {**result, "provider": provider, "url": url}
|
|
360
645
|
|
|
361
646
|
@server.tool(annotations=ToolAnnotations(title="Browser PDF Export", readOnlyHint=True))
|
|
362
|
-
def browser_pdf_export(url: str, output_path: str =
|
|
647
|
+
def browser_pdf_export(url: str, output_path: Optional[str] = None) -> Dict[str, Any]:
|
|
363
648
|
"""Export a web page as PDF."""
|
|
364
|
-
|
|
649
|
+
if not output_path:
|
|
650
|
+
fd, output_path = tempfile.mkstemp(suffix=".pdf", prefix="page_")
|
|
651
|
+
os.close(fd)
|
|
652
|
+
_run_browser_with_retry(["open", url])
|
|
365
653
|
_run_browser(["wait", "--load", "networkidle"])
|
|
366
654
|
_run_browser(["wait", "2000"])
|
|
367
655
|
result = _run_browser(["pdf", output_path])
|
|
368
656
|
_run_browser(["close"])
|
|
369
657
|
return {**result, "url": url, "path": output_path}
|
|
370
658
|
|
|
371
|
-
logger.info("Registered
|
|
659
|
+
logger.info("Registered 26 browser automation tools (agent-browser v0.7+)")
|