kl-mcp-client 2.1.12__py3-none-any.whl → 2.1.14__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.
kl_mcp_client/tools.py CHANGED
@@ -1,8 +1,19 @@
1
+ import logging
1
2
  from functools import wraps
2
3
  from typing import Any, Dict, Optional
3
4
 
4
5
  from .client import MCPClient
5
6
 
7
+ # ======================================================
8
+ # LOGGER
9
+ # ======================================================
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # ======================================================
14
+ # DECORATOR
15
+ # ======================================================
16
+
6
17
 
7
18
  def _ensure_client(func):
8
19
  """Decorator kiểm tra self.client != None trước khi gọi tool."""
@@ -10,6 +21,7 @@ def _ensure_client(func):
10
21
  @wraps(func)
11
22
  def wrapper(self, *args, **kwargs):
12
23
  if self.client is None:
24
+ logger.error("MCP client not connected")
13
25
  return {
14
26
  "ok": False,
15
27
  "error": "MCP client not connected. Call connect mcp server first.",
@@ -19,361 +31,320 @@ def _ensure_client(func):
19
31
  return wrapper
20
32
 
21
33
 
34
+ # ======================================================
35
+ # CLASS
36
+ # ======================================================
37
+
22
38
  class MCPTools:
23
39
  """
24
40
  Wrapper chuẩn cho Google ADK + MCP Server.
25
- - Tool nào trả text/html/... → dùng structuredContent
26
- - Tool screenshot → trả đúng content để ADK Web hiển thị image
27
41
  """
28
42
 
43
+ # ======================================================
44
+ # INIT / CONNECT
45
+ # ======================================================
46
+
29
47
  def __init__(self):
30
48
  self.client = None
49
+ logger.info("MCPTools initialized")
31
50
 
32
- # ======================================================
33
- # SESSION MANAGEMENT
34
- # ======================================================
35
51
  def connect_mcp(self, mcpUrl: str) -> Dict[str, Any]:
36
- # sid = self.client.create_session(mcpUrl)
52
+ logger.info("connect_mcp | %s", mcpUrl)
53
+
37
54
  self.client = MCPClient(
38
- base_url=mcpUrl, headers=None, timeout=30, retries=2)
55
+ base_url=mcpUrl,
56
+ headers=None,
57
+ timeout=30,
58
+ retries=2
59
+ )
39
60
  return {"ok": True, "cdpUrl": "http://localhost:9222"}
40
61
 
62
+ # ======================================================
63
+ # SESSION MANAGEMENT
64
+ # ======================================================
65
+
41
66
  @_ensure_client
42
- def create_session(self, cdpUrl: str) -> Dict[str, Any]:
67
+ def create_session(self, cdpUrl: str):
68
+ logger.info("create_session | %s", cdpUrl)
43
69
  sid = self.client.create_session(cdpUrl)
44
70
  return {"sessionId": sid}
45
71
 
46
72
  @_ensure_client
47
- def close_session(self, sessionId: str) -> Dict[str, Any]:
73
+ def close_session(self, sessionId: str):
74
+ logger.info("close_session | %s", sessionId)
48
75
  ok = self.client.close_session(sessionId)
49
76
  return {"ok": bool(ok)}
50
77
 
51
78
  @_ensure_client
52
- def list_sessions(self) -> Dict[str, Any]:
79
+ def list_sessions(self):
80
+ logger.debug("list_sessions")
53
81
  return {"sessions": self.client.list_local_sessions()}
54
82
 
55
83
  # ======================================================
56
- # NAVIGATION & DOM
84
+ # TAB MANAGEMENT
57
85
  # ======================================================
86
+
58
87
  @_ensure_client
59
- def open_page(self, sessionId: str, url: str) -> Dict[str, Any]:
88
+ def new_tab(self, sessionId: str, url: Optional[str] = "about:blank"):
89
+ logger.info("new_tab | %s", url)
60
90
  return self.client.call_tool(
61
- "openPage", {"sessionId": sessionId, "url": url}
91
+ "newTab", {"sessionId": sessionId, "url": url}
62
92
  ).get("structuredContent", {})
63
93
 
64
94
  @_ensure_client
65
- def get_html(self, sessionId: str) -> Dict[str, Any]:
66
- return self.client.call_tool("getHTML", {"sessionId": sessionId}).get(
67
- "structuredContent", {}
68
- )
69
-
70
- @_ensure_client
71
- def screenshot(self, sessionId: str) -> Dict[str, Any]:
72
- """
73
- Trả về đúng phần IMAGE content:
74
- {
75
- "type": "image",
76
- "mimeType": "image/png",
77
- "data": "<base64>"
78
- }
79
- """
80
- full = self.client.call_tool("screenshot", {"sessionId": sessionId})
81
- return full["content"][0]
82
-
83
- @_ensure_client
84
- def click(self, sessionId: str, selector: str) -> Dict[str, Any]:
95
+ def switch_tab(self, sessionId: str, targetId: str):
96
+ logger.info("switch_tab | %s", targetId)
85
97
  return self.client.call_tool(
86
- "click", {"sessionId": sessionId, "selector": selector}
98
+ "switchTab", {"sessionId": sessionId, "targetId": targetId}
87
99
  ).get("structuredContent", {})
88
100
 
89
101
  @_ensure_client
90
- def type(self, sessionId: str, selector: str, text: str) -> Dict[str, Any]:
102
+ def close_tab(self, sessionId: str, tabId: str):
103
+ logger.info("close_tab | %s", tabId)
91
104
  return self.client.call_tool(
92
- "type", {"sessionId": sessionId,
93
- "selector": selector, "text": text}
105
+ "closeTab", {"sessionId": sessionId, "tabId": tabId}
94
106
  ).get("structuredContent", {})
95
107
 
96
108
  @_ensure_client
97
- def evaluate(self, sessionId: str, expression: str) -> Dict[str, Any]:
109
+ def current_tab(self, sessionId: str):
110
+ logger.debug("current_tab")
98
111
  return self.client.call_tool(
99
- "evaluate", {"sessionId": sessionId, "expression": expression}
112
+ "currentTab", {"sessionId": sessionId}
100
113
  ).get("structuredContent", {})
101
114
 
102
115
  # ======================================================
103
- # ELEMENT UTILITIES
116
+ # NAVIGATION / DOM
104
117
  # ======================================================
105
- @_ensure_client
106
- def find_element(self, sessionId: str, selector: str) -> Dict[str, Any]:
107
- return self.client.call_tool(
108
- "findElement", {"sessionId": sessionId, "selector": selector}
109
- ).get("structuredContent", {})
110
118
 
111
119
  @_ensure_client
112
- def find_all(self, sessionId: str, selector: str) -> Dict[str, Any]:
120
+ def open_page(self, sessionId: str, url: str):
121
+ logger.info("open_page | %s", url)
113
122
  return self.client.call_tool(
114
- "findAll", {"sessionId": sessionId, "selector": selector}
123
+ "openPage", {"sessionId": sessionId, "url": url}
115
124
  ).get("structuredContent", {})
116
125
 
117
126
  @_ensure_client
118
- def get_bounding_box(self, sessionId: str, selector: str) -> Dict[str, Any]:
127
+ def get_html(self, sessionId: str):
128
+ logger.debug("get_html")
119
129
  return self.client.call_tool(
120
- "getBoundingBox", {"sessionId": sessionId, "selector": selector}
130
+ "getHTML", {"sessionId": sessionId}
121
131
  ).get("structuredContent", {})
122
132
 
123
133
  @_ensure_client
124
- def click_bounding_box(self, sessionId: str, selector: str) -> Dict[str, Any]:
134
+ def evaluate(self, sessionId: str, expression: str):
135
+ logger.debug("evaluate")
125
136
  return self.client.call_tool(
126
- "clickBoundingBox", {"sessionId": sessionId, "selector": selector}
137
+ "evaluate", {"sessionId": sessionId, "expression": expression}
127
138
  ).get("structuredContent", {})
128
139
 
129
140
  @_ensure_client
130
- def upload_file(
131
- self,
132
- sessionId: str,
133
- selector: str,
134
- file_path: str,
135
- ) -> Dict[str, Any]:
136
- """
137
- Upload file (kể cả video lớn) vào input[type=file] theo luồng mới:
138
- 1. Multipart upload file lên MCP server
139
- 2. Nhận uploadId
140
- 3. Gọi MCP tool uploadFile với uploadId
141
-
142
- Args:
143
- sessionId: MCP browser session
144
- selector: CSS selector, ví dụ 'input[type=file]'
145
- file_path: đường dẫn file local (video, pdf, doc, ...)
146
- """
147
-
148
- if not file_path:
149
- return {"ok": False, "error": "file_path is required"}
150
-
151
- # --------------------------------------------------
152
- # 1️⃣ Multipart upload file lên MCP server
153
- # --------------------------------------------------
154
- try:
155
- with open(file_path, "rb") as f:
156
- resp = self.client.http.post(
157
- "/upload",
158
- files={"file": f},
159
- timeout=300, # upload file lớn
160
- )
161
- except Exception as e:
162
- return {"ok": False, "error": f"upload http failed: {e}"}
163
-
164
- if resp.status_code != 200:
165
- return {
166
- "ok": False,
167
- "error": f"upload http error {resp.status_code}: {resp.text}",
168
- }
169
-
170
- data = resp.json()
171
- upload_id = data.get("uploadId")
172
- if not upload_id:
173
- return {"ok": False, "error": "uploadId not returned from server"}
174
-
175
- # --------------------------------------------------
176
- # 2️⃣ Gọi MCP tool uploadFile (PATH MODE)
177
- # --------------------------------------------------
178
- result = self.client.call_tool(
179
- "uploadFile",
180
- {
181
- "sessionId": sessionId,
182
- "selector": selector,
183
- "uploadId": upload_id,
184
- },
141
+ def screenshot(self, sessionId: str):
142
+ logger.info("screenshot")
143
+ full = self.client.call_tool(
144
+ "screenshot", {"sessionId": sessionId}
185
145
  )
186
-
187
- return result.get("structuredContent", {})
146
+ return full["content"][0]
188
147
 
189
148
  @_ensure_client
190
149
  def wait_for_selector(
191
150
  self, sessionId: str, selector: str, timeoutMs: Optional[int] = None
192
- ) -> Dict[str, Any]:
151
+ ):
152
+ logger.debug("wait_for_selector | %s", selector)
193
153
  args = {"sessionId": sessionId, "selector": selector}
194
154
  if timeoutMs is not None:
195
- args["timeoutMs"] = int(timeoutMs)
155
+ args["timeout"] = int(timeoutMs)
196
156
 
197
- return self.client.call_tool("waitForSelector", args).get(
198
- "structuredContent", {}
199
- )
157
+ return self.client.call_tool(
158
+ "waitForSelector", args
159
+ ).get("structuredContent", {})
200
160
 
201
161
  # ======================================================
202
- # TAB MANAGEMENT
162
+ # ELEMENT UTILITIES
203
163
  # ======================================================
164
+
204
165
  @_ensure_client
205
- def new_tab(
206
- self, sessionId: str, url: Optional[str] = "about:blank"
207
- ) -> Dict[str, Any]:
166
+ def find_element(self, sessionId: str, selector: str):
167
+ logger.debug("find_element | %s", selector)
208
168
  return self.client.call_tool(
209
- "newTab", {"sessionId": sessionId, "url": url}
169
+ "findElement", {"sessionId": sessionId, "selector": selector}
210
170
  ).get("structuredContent", {})
211
171
 
212
172
  @_ensure_client
213
- def switch_tab(self, sessionId: str, targetId: str) -> Dict[str, Any]:
173
+ def find_all(self, sessionId: str, selector: str):
174
+ logger.debug("find_all | %s", selector)
214
175
  return self.client.call_tool(
215
- "switchTab", {"sessionId": sessionId, "targetId": targetId}
176
+ "findAll", {"sessionId": sessionId, "selector": selector}
216
177
  ).get("structuredContent", {})
217
178
 
218
- # ======================================================
219
- # ADVANCED ACTIONS
220
- # ======================================================
221
179
  @_ensure_client
222
- def click_to_text(self, sessionId: str, text: str) -> dict:
180
+ def get_bounding_box(self, sessionId: str, selector: str):
181
+ logger.debug("get_bounding_box | %s", selector)
223
182
  return self.client.call_tool(
224
- "clickToText", {"sessionId": sessionId, "text": text}
183
+ "getBoundingBox", {"sessionId": sessionId, "selector": selector}
225
184
  ).get("structuredContent", {})
226
185
 
227
186
  @_ensure_client
228
- def find_element_xpath(self, sessionId: str, xpath: str) -> Dict[str, Any]:
187
+ def click_bounding_box(self, sessionId: str, selector: str):
188
+ logger.debug("click_bounding_box | %s", selector)
229
189
  return self.client.call_tool(
230
- "findElementByXPath", {"sessionId": sessionId, "xpath": xpath}
190
+ "clickBoundingBox", {"sessionId": sessionId, "selector": selector}
231
191
  ).get("structuredContent", {})
232
192
 
193
+ # ======================================================
194
+ # BASIC ACTIONS
195
+ # ======================================================
196
+
233
197
  @_ensure_client
234
- def find_element_by_text(self, sessionId: str, text: str) -> Dict[str, Any]:
198
+ def click(self, sessionId: str, selector: str):
199
+ logger.debug("click | %s", selector)
235
200
  return self.client.call_tool(
236
- "findElementByText", {"sessionId": sessionId, "text": text}
201
+ "click", {"sessionId": sessionId, "selector": selector}
237
202
  ).get("structuredContent", {})
238
203
 
239
204
  @_ensure_client
240
- def click_by_node_id(self, sessionId: str, nodeId: int) -> Dict[str, Any]:
205
+ def type(self, sessionId: str, selector: str, text: str):
206
+ logger.debug("type | %s", selector)
241
207
  return self.client.call_tool(
242
- "clickByNodeId", {"sessionId": sessionId, "nodeId": nodeId}
208
+ "type", {"sessionId": sessionId,
209
+ "selector": selector, "text": text}
243
210
  ).get("structuredContent", {})
244
211
 
212
+ # ======================================================
213
+ # ADVANCED FIND / CLICK
214
+ # ======================================================
215
+
245
216
  @_ensure_client
246
- def import_cookies(self, sessionId: str, cookies: dict) -> Dict[str, Any]:
217
+ def click_to_text(self, sessionId: str, text: str):
218
+ logger.debug("click_to_text | %s", text)
247
219
  return self.client.call_tool(
248
- "importCookies", {"sessionId": sessionId, "cookies": cookies}
220
+ "clickToText", {"sessionId": sessionId, "text": text}
249
221
  ).get("structuredContent", {})
250
222
 
251
223
  @_ensure_client
252
- def get_dom_tree(self, sessionId, args=None):
224
+ def find_element_xpath(self, sessionId: str, xpath: str):
225
+ logger.debug("find_element_xpath")
253
226
  return self.client.call_tool(
254
- "getDomTree", {"sessionId": sessionId, "args": args or {}}
255
- )
227
+ "findElementByXPath", {"sessionId": sessionId, "xpath": xpath}
228
+ ).get("structuredContent", {})
256
229
 
257
230
  @_ensure_client
258
- def get_clickable(self, sessionId, args=None):
231
+ def find_element_by_text(self, sessionId: str, text: str):
232
+ logger.debug("find_element_by_text | %s", text)
259
233
  return self.client.call_tool(
260
- "getClickable", {"sessionId": sessionId, "args": args or {}}
261
- )
234
+ "findElementByText", {"sessionId": sessionId, "text": text}
235
+ ).get("structuredContent", {})
262
236
 
263
237
  @_ensure_client
264
- def selector_map(self, sessionId, selector, args=None):
238
+ def click_by_node_id(self, sessionId: str, nodeId: int):
239
+ logger.debug("click_by_node_id | %s", nodeId)
265
240
  return self.client.call_tool(
266
- "selectorMap",
267
- {"sessionId": sessionId, "selector": selector, "args": args or {}},
268
- )
241
+ "clickByNodeId", {"sessionId": sessionId, "nodeId": nodeId}
242
+ ).get("structuredContent", {})
269
243
 
270
244
  @_ensure_client
271
- def find_element_by_prompt(self, sessionId: str, prompt: str) -> Dict[str, Any]:
272
- """
273
- Gọi tool findElementByPrompt trên MCP server.
274
- Trả về structuredContent gồm: html, nodeId.
275
- """
245
+ def find_element_by_prompt(self, sessionId: str, prompt: str):
246
+ logger.info("find_element_by_prompt")
276
247
  return self.client.call_tool(
277
248
  "findElementByPrompt", {"sessionId": sessionId, "prompt": prompt}
278
249
  ).get("structuredContent", {})
279
250
 
280
251
  # ======================================================
281
- # AI / CONTENT PARSING
252
+ # FILE / COOKIE
282
253
  # ======================================================
254
+
283
255
  @_ensure_client
284
- def parse_html_by_prompt(self, html: str, prompt: str) -> Dict[str, Any]:
285
- """
286
- Parse HTML content using AI with dynamic prompt-defined structure.
256
+ def upload_file(self, sessionId: str, selector: str, file_path: str):
257
+ logger.info("upload_file | %s", file_path)
287
258
 
288
- Args:
289
- html: Raw HTML string (client-provided)
290
- prompt: Instruction that defines what to extract and output structure
291
- Example:
292
- - "Hãy lấy nội dung bài viết, struct trả về { content }"
293
- - "Hãy lấy số lượng like, share, comment, trả JSON { like, share, comment }"
259
+ if not file_path:
260
+ return {"ok": False, "error": "file_path is required"}
261
+
262
+ try:
263
+ with open(file_path, "rb") as f:
264
+ resp = self.client.http.post(
265
+ "/upload",
266
+ files={"file": f},
267
+ timeout=300,
268
+ )
269
+ except Exception as e:
270
+ logger.exception("upload http failed")
271
+ return {"ok": False, "error": f"upload http failed: {e}"}
272
+
273
+ if resp.status_code != 200:
274
+ return {
275
+ "ok": False,
276
+ "error": f"http {resp.status_code}: {resp.text}",
277
+ }
278
+
279
+ upload_id = resp.json().get("uploadId")
280
+ if not upload_id:
281
+ return {"ok": False, "error": "uploadId not returned"}
294
282
 
295
- Returns:
296
- structuredContent (dynamic JSON defined by prompt)
297
- """
298
283
  return self.client.call_tool(
299
- "parseHTMLByPrompt",
284
+ "uploadFile",
300
285
  {
301
- "html": html,
302
- "prompt": prompt,
286
+ "sessionId": sessionId,
287
+ "selector": selector,
288
+ "uploadId": upload_id,
303
289
  },
304
290
  ).get("structuredContent", {})
305
291
 
292
+ @_ensure_client
293
+ def import_cookies(self, sessionId: str, cookies: dict):
294
+ logger.info("import_cookies")
295
+ return self.client.call_tool(
296
+ "importCookies", {"sessionId": sessionId, "cookies": cookies}
297
+ ).get("structuredContent", {})
298
+
306
299
  # ======================================================
307
- # CLEAN TEXT / READ MODE
300
+ # AI / PARSING
308
301
  # ======================================================
302
+
309
303
  @_ensure_client
310
- def get_clean_text(self, sessionId: str) -> Dict[str, Any]:
311
- """
312
- Lấy toàn bộ visible text đã được clean trên trang hiện tại.
313
- - Bỏ script/style/iframe/svg/canvas
314
- - Chỉ text nhìn thấy (display/visibility/opacity)
304
+ def parse_html_by_prompt(self, html: str, prompt: str):
305
+ logger.info("parse_html_by_prompt")
306
+ return self.client.call_tool(
307
+ "parseHTMLByPrompt",
308
+ {"html": html, "prompt": prompt}
309
+ ).get("structuredContent", {})
315
310
 
316
- Returns:
317
- {
318
- "text": "...",
319
- "length": 12345
320
- }
321
- """
311
+ # ======================================================
312
+ # CLEAN TEXT
313
+ # ======================================================
314
+
315
+ @_ensure_client
316
+ def get_clean_text(self, sessionId: str):
317
+ logger.debug("get_clean_text")
322
318
  return self.client.call_tool(
323
319
  "getCleanText",
324
- {"sessionId": sessionId},
320
+ {"sessionId": sessionId}
325
321
  ).get("structuredContent", {})
326
322
 
323
+ # ======================================================
324
+ # STREAM
325
+ # ======================================================
326
+
327
327
  @_ensure_client
328
- def evaluate_stream(
329
- self,
330
- sessionId: str,
331
- expression: str,
332
- chunkSize: int = 100,
333
- ) -> Dict[str, Any]:
334
- """
335
- Evaluate JS expression theo chế độ STREAM.
336
- Dùng khi kết quả là array lớn (DOM, list, table, ...)
337
-
338
- Returns (init response):
339
- {
340
- "stream_id": "...",
341
- "total": 1234,
342
- "chunk_size": 100
343
- }
344
- """
328
+ def evaluate_stream(self, sessionId: str, expression: str, chunkSize: int = 100):
329
+ logger.debug("evaluate_stream")
345
330
  return self.client.call_tool(
346
331
  "evaluate.stream",
347
332
  {
348
333
  "sessionId": sessionId,
349
334
  "expression": expression,
350
- "chunkSize": int(chunkSize),
335
+ "chunkSize": chunkSize,
351
336
  },
352
337
  ).get("structuredContent", {})
353
338
 
354
339
  @_ensure_client
355
- def stream_pull(
356
- self,
357
- stream_id: str,
358
- offset: int = 0,
359
- limit: int = 100,
360
- ) -> Dict[str, Any]:
361
- """
362
- Kéo 1 chunk từ stream đã tạo bởi evaluate.stream
363
-
364
- Returns:
365
- {
366
- "items": [...],
367
- "offset": 0,
368
- "has_more": true
369
- }
370
- """
340
+ def stream_pull(self, stream_id: str, offset: int = 0, limit: int = 100):
341
+ logger.debug("stream_pull")
371
342
  return self.client.call_tool(
372
343
  "stream.pull",
373
344
  {
374
345
  "stream_id": stream_id,
375
- "offset": int(offset),
376
- "limit": int(limit),
346
+ "offset": offset,
347
+ "limit": limit,
377
348
  },
378
349
  ).get("structuredContent", {})
379
350
 
@@ -385,19 +356,10 @@ class MCPTools:
385
356
  chunkSize: int = 100,
386
357
  max_items: Optional[int] = None,
387
358
  ):
388
- """
389
- Helper: evaluate.stream + tự động pull toàn bộ dữ liệu.
390
-
391
- ⚠️ Chỉ dùng khi bạn THỰC SỰ cần full data.
392
- """
393
- init = self.evaluate_stream(
394
- sessionId=sessionId,
395
- expression=expression,
396
- chunkSize=chunkSize,
397
- )
359
+ logger.info("evaluate_stream_all")
398
360
 
361
+ init = self.evaluate_stream(sessionId, expression, chunkSize)
399
362
  stream_id = init.get("stream_id")
400
- total = init.get("total", 0)
401
363
 
402
364
  if not stream_id:
403
365
  return []
@@ -406,14 +368,8 @@ class MCPTools:
406
368
  offset = 0
407
369
 
408
370
  while True:
409
- chunk = self.stream_pull(
410
- stream_id=stream_id,
411
- offset=offset,
412
- limit=chunkSize,
413
- )
414
-
415
- part = chunk.get("items", [])
416
- items.extend(part)
371
+ chunk = self.stream_pull(stream_id, offset, chunkSize)
372
+ items.extend(chunk.get("items", []))
417
373
 
418
374
  if max_items and len(items) >= max_items:
419
375
  return items[:max_items]
@@ -425,33 +381,22 @@ class MCPTools:
425
381
 
426
382
  return items
427
383
 
428
- @_ensure_client
429
- def wait_for_selector(
430
- self, sessionId: str, selector: str, timeoutMs: Optional[int] = None
431
- ) -> Dict[str, Any]:
432
- args = {
433
- "sessionId": sessionId,
434
- "selector": selector,
435
- }
436
- if timeoutMs is not None:
437
- args["timeout"] = int(timeoutMs)
438
-
439
- return self.client.call_tool("waitForSelector", args).get(
440
- "structuredContent", {}
441
- )
384
+ # ======================================================
385
+ # KEYBOARD
386
+ # ======================================================
442
387
 
443
388
  @_ensure_client
444
- def send_keys(
445
- self, sessionId: str, key: str, interval: int = 100
446
- ) -> Dict[str, Any]:
447
- """
448
- Gửi phím: enter, tab, esc, ctrl+c, ctrl+shift+tab, ...
449
- """
389
+ def send_keys(self, sessionId: str, key: str, interval: int = 100):
390
+ logger.debug("send_keys | %s", key)
450
391
  return self.client.call_tool(
451
392
  "sendKeys",
452
- {"sessionId": sessionId, "text": key, "interval": interval},
393
+ {"sessionId": sessionId, "text": key, "interval": interval}
453
394
  ).get("structuredContent", {})
454
395
 
396
+ # ======================================================
397
+ # PERFORM / MOUSE
398
+ # ======================================================
399
+
455
400
  @_ensure_client
456
401
  def perform(
457
402
  self,
@@ -463,18 +408,10 @@ class MCPTools:
463
408
  y: Optional[float] = None,
464
409
  from_point: Optional[dict] = None,
465
410
  to_point: Optional[dict] = None,
466
- ) -> Dict[str, Any]:
467
- """
468
- action:
469
- - click (x,y)
470
- - move / hover (x,y)
471
- - drag (from -> to)
472
- - type (target, value)
473
- """
474
- args = {
475
- "sessionId": sessionId,
476
- "action": action,
477
- }
411
+ ):
412
+ logger.debug("perform | %s", action)
413
+
414
+ args = {"sessionId": sessionId, "action": action}
478
415
 
479
416
  if target is not None:
480
417
  args["target"] = target
@@ -489,7 +426,9 @@ class MCPTools:
489
426
  if to_point is not None:
490
427
  args["to"] = to_point
491
428
 
492
- return self.client.call_tool("perform", args).get("structuredContent", {})
429
+ return self.client.call_tool(
430
+ "perform", args
431
+ ).get("structuredContent", {})
493
432
 
494
433
  @_ensure_client
495
434
  def drag_and_drop(
@@ -500,6 +439,7 @@ class MCPTools:
500
439
  to_x: float,
501
440
  to_y: float,
502
441
  ):
442
+ logger.debug("drag_and_drop")
503
443
  return self.perform(
504
444
  sessionId=sessionId,
505
445
  action="drag",
@@ -507,7 +447,9 @@ class MCPTools:
507
447
  to_point={"x": to_x, "y": to_y},
508
448
  )
509
449
 
450
+ @_ensure_client
510
451
  def hover(self, sessionId: str, x: float, y: float):
452
+ logger.debug("hover")
511
453
  return self.perform(
512
454
  sessionId=sessionId,
513
455
  action="hover",
@@ -523,24 +465,11 @@ class MCPTools:
523
465
  x: Optional[float] = None,
524
466
  y: Optional[float] = None,
525
467
  selector: Optional[str] = None,
526
- position: Optional[str] = None, # "top" | "bottom"
527
- ) -> Dict[str, Any]:
528
- """
529
- Scroll trang web.
530
-
531
- Cách dùng:
532
- - Scroll theo pixel:
533
- scroll(sessionId, y=500)
534
- - Scroll tới element:
535
- scroll(sessionId, selector="#footer")
536
- - Scroll top / bottom:
537
- scroll(sessionId, position="top")
538
- scroll(sessionId, position="bottom")
539
- """
540
-
541
- args: Dict[str, Any] = {
542
- "sessionId": sessionId,
543
- }
468
+ position: Optional[str] = None,
469
+ ):
470
+ logger.debug("scroll")
471
+
472
+ args = {"sessionId": sessionId}
544
473
 
545
474
  if x is not None:
546
475
  args["x"] = float(x)
@@ -552,60 +481,48 @@ class MCPTools:
552
481
  args["position"] = position
553
482
 
554
483
  if len(args) == 1:
555
- return {
556
- "ok": False,
557
- "error": "scroll requires x/y or selector or position",
558
- }
484
+ return {"ok": False, "error": "scroll requires x/y or selector or position"}
559
485
 
560
486
  return self.client.call_tool(
561
- "scroll",
562
- args,
487
+ "scroll", args
563
488
  ).get("structuredContent", {})
564
- # ======================================================
489
+
490
+ # ======================================================
565
491
  # BROWSER RUNTIME (NO SESSION)
566
492
  # ======================================================
567
493
 
568
494
  @_ensure_client
569
- def create_browser(self, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
570
- """
571
- Start browser runtime (idempotent).
572
- Không tạo session, chỉ đảm bảo browser đang RUNNING.
573
-
574
- payload: map cho ThirdPartyOpenRequest
575
- """
495
+ def create_browser(self, payload: Optional[Dict[str, Any]] = None):
496
+ logger.info("create_browser")
576
497
  return self.client.call_tool(
577
- "createBrowser",
578
- payload or {},
498
+ "createBrowser", payload or {}
579
499
  ).get("structuredContent", {})
580
500
 
581
501
  @_ensure_client
582
- def release_browser(self, pop_name: str) -> Dict[str, Any]:
583
- """
584
- Release / stop browser runtime.
585
- """
502
+ def release_browser(self, pop_name: str):
503
+ logger.info("release_browser | %s", pop_name)
586
504
  return self.client.call_tool(
587
- "releaseBrowser",
588
- {
589
- "pod_name": pop_name
590
- },
505
+ "releaseBrowser", {"pod_name": pop_name}
591
506
  ).get("structuredContent", {})
507
+
592
508
  # ======================================================
593
- # VIEWPORT (Playwright-style)
509
+ # VIEWPORT
594
510
  # ======================================================
595
511
 
596
512
  @_ensure_client
597
- def get_viewport(self, sessionId: str) -> Dict[str, Any]:
598
- """
599
- Get current browser viewport.
600
- Equivalent to Playwright page.viewportSize().
601
- """
513
+ def get_viewport(self, sessionId: str):
514
+ logger.debug("get_viewport")
602
515
  return self.client.call_tool(
603
- "viewport",
604
- {
605
- "sessionId": sessionId,
606
- },
516
+ "viewport", {"sessionId": sessionId}
607
517
  ).get("structuredContent", {})
608
518
 
519
+ @_ensure_client
520
+ def current_url(self, sessionId: str):
521
+ logger.debug("current_url")
522
+ return self.client.call_tool(
523
+ "getCurrentUrl", {"sessionId": sessionId}
524
+ ).get("structuredContent", {}).get("url")
525
+
609
526
  @_ensure_client
610
527
  def set_viewport(
611
528
  self,
@@ -615,16 +532,8 @@ class MCPTools:
615
532
  height: int,
616
533
  deviceScaleFactor: float = 1.0,
617
534
  mobile: bool = False,
618
- ) -> Dict[str, Any]:
619
- """
620
- Set browser viewport (Playwright-like).
621
-
622
- Args:
623
- width: viewport width
624
- height: viewport height
625
- deviceScaleFactor: default 1.0
626
- mobile: mobile emulation flag
627
- """
535
+ ):
536
+ logger.info("set_viewport | %sx%s", width, height)
628
537
  return self.client.call_tool(
629
538
  "viewport",
630
539
  {
@@ -637,23 +546,3 @@ class MCPTools:
637
546
  },
638
547
  },
639
548
  ).get("structuredContent", {})
640
- # ======================================================
641
- # CURRENT TAB
642
- # ======================================================
643
-
644
- @_ensure_client
645
- def current_tab(self, sessionId: str) -> Dict[str, Any]:
646
- """
647
- Get current active browser tab ID.
648
-
649
- Returns:
650
- {
651
- "tabId": "<targetId>"
652
- }
653
- """
654
- return self.client.call_tool(
655
- "currentTab",
656
- {
657
- "sessionId": sessionId,
658
- },
659
- ).get("structuredContent", {})