kl-mcp-client 2.1.12__py3-none-any.whl → 2.1.13__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/asyncio/tools.py +164 -253
- kl_mcp_client/tools.py +230 -348
- {kl_mcp_client-2.1.12.dist-info → kl_mcp_client-2.1.13.dist-info}/METADATA +1 -1
- kl_mcp_client-2.1.13.dist-info/RECORD +11 -0
- {kl_mcp_client-2.1.12.dist-info → kl_mcp_client-2.1.13.dist-info}/WHEEL +1 -1
- kl_mcp_client-2.1.12.dist-info/RECORD +0 -11
- {kl_mcp_client-2.1.12.dist-info → kl_mcp_client-2.1.13.dist-info}/top_level.txt +0 -0
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
|
-
|
|
52
|
+
logger.info("connect_mcp | %s", mcpUrl)
|
|
53
|
+
|
|
37
54
|
self.client = MCPClient(
|
|
38
|
-
base_url=mcpUrl,
|
|
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)
|
|
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)
|
|
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)
|
|
79
|
+
def list_sessions(self):
|
|
80
|
+
logger.debug("list_sessions")
|
|
53
81
|
return {"sessions": self.client.list_local_sessions()}
|
|
54
82
|
|
|
55
83
|
# ======================================================
|
|
56
|
-
#
|
|
84
|
+
# TAB MANAGEMENT
|
|
57
85
|
# ======================================================
|
|
86
|
+
|
|
58
87
|
@_ensure_client
|
|
59
|
-
def
|
|
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
|
-
"
|
|
91
|
+
"newTab", {"sessionId": sessionId, "url": url}
|
|
62
92
|
).get("structuredContent", {})
|
|
63
93
|
|
|
64
94
|
@_ensure_client
|
|
65
|
-
def
|
|
66
|
-
|
|
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
|
-
"
|
|
98
|
+
"switchTab", {"sessionId": sessionId, "targetId": targetId}
|
|
87
99
|
).get("structuredContent", {})
|
|
88
100
|
|
|
89
101
|
@_ensure_client
|
|
90
|
-
def
|
|
102
|
+
def close_tab(self, sessionId: str, tabId: str):
|
|
103
|
+
logger.info("close_tab | %s", tabId)
|
|
91
104
|
return self.client.call_tool(
|
|
92
|
-
"
|
|
93
|
-
"selector": selector, "text": text}
|
|
105
|
+
"closeTab", {"sessionId": sessionId, "tabId": tabId}
|
|
94
106
|
).get("structuredContent", {})
|
|
95
107
|
|
|
96
108
|
@_ensure_client
|
|
97
|
-
def
|
|
109
|
+
def current_tab(self, sessionId: str):
|
|
110
|
+
logger.debug("current_tab")
|
|
98
111
|
return self.client.call_tool(
|
|
99
|
-
"
|
|
112
|
+
"currentTab", {"sessionId": sessionId}
|
|
100
113
|
).get("structuredContent", {})
|
|
101
114
|
|
|
102
115
|
# ======================================================
|
|
103
|
-
#
|
|
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
|
|
120
|
+
def open_page(self, sessionId: str, url: str):
|
|
121
|
+
logger.info("open_page | %s", url)
|
|
113
122
|
return self.client.call_tool(
|
|
114
|
-
"
|
|
123
|
+
"openPage", {"sessionId": sessionId, "url": url}
|
|
115
124
|
).get("structuredContent", {})
|
|
116
125
|
|
|
117
126
|
@_ensure_client
|
|
118
|
-
def
|
|
127
|
+
def get_html(self, sessionId: str):
|
|
128
|
+
logger.debug("get_html")
|
|
119
129
|
return self.client.call_tool(
|
|
120
|
-
"
|
|
130
|
+
"getHTML", {"sessionId": sessionId}
|
|
121
131
|
).get("structuredContent", {})
|
|
122
132
|
|
|
123
133
|
@_ensure_client
|
|
124
|
-
def
|
|
134
|
+
def evaluate(self, sessionId: str, expression: str):
|
|
135
|
+
logger.debug("evaluate")
|
|
125
136
|
return self.client.call_tool(
|
|
126
|
-
"
|
|
137
|
+
"evaluate", {"sessionId": sessionId, "expression": expression}
|
|
127
138
|
).get("structuredContent", {})
|
|
128
139
|
|
|
129
140
|
@_ensure_client
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
)
|
|
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["
|
|
155
|
+
args["timeout"] = int(timeoutMs)
|
|
196
156
|
|
|
197
|
-
return self.client.call_tool(
|
|
198
|
-
"
|
|
199
|
-
)
|
|
157
|
+
return self.client.call_tool(
|
|
158
|
+
"waitForSelector", args
|
|
159
|
+
).get("structuredContent", {})
|
|
200
160
|
|
|
201
161
|
# ======================================================
|
|
202
|
-
#
|
|
162
|
+
# ELEMENT UTILITIES
|
|
203
163
|
# ======================================================
|
|
164
|
+
|
|
204
165
|
@_ensure_client
|
|
205
|
-
def
|
|
206
|
-
|
|
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
|
-
"
|
|
169
|
+
"findElement", {"sessionId": sessionId, "selector": selector}
|
|
210
170
|
).get("structuredContent", {})
|
|
211
171
|
|
|
212
172
|
@_ensure_client
|
|
213
|
-
def
|
|
173
|
+
def find_all(self, sessionId: str, selector: str):
|
|
174
|
+
logger.debug("find_all | %s", selector)
|
|
214
175
|
return self.client.call_tool(
|
|
215
|
-
"
|
|
176
|
+
"findAll", {"sessionId": sessionId, "selector": selector}
|
|
216
177
|
).get("structuredContent", {})
|
|
217
178
|
|
|
218
|
-
# ======================================================
|
|
219
|
-
# ADVANCED ACTIONS
|
|
220
|
-
# ======================================================
|
|
221
179
|
@_ensure_client
|
|
222
|
-
def
|
|
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
|
-
"
|
|
183
|
+
"getBoundingBox", {"sessionId": sessionId, "selector": selector}
|
|
225
184
|
).get("structuredContent", {})
|
|
226
185
|
|
|
227
186
|
@_ensure_client
|
|
228
|
-
def
|
|
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
|
-
"
|
|
190
|
+
"clickBoundingBox", {"sessionId": sessionId, "selector": selector}
|
|
231
191
|
).get("structuredContent", {})
|
|
232
192
|
|
|
193
|
+
# ======================================================
|
|
194
|
+
# BASIC ACTIONS
|
|
195
|
+
# ======================================================
|
|
196
|
+
|
|
233
197
|
@_ensure_client
|
|
234
|
-
def
|
|
198
|
+
def click(self, sessionId: str, selector: str):
|
|
199
|
+
logger.debug("click | %s", selector)
|
|
235
200
|
return self.client.call_tool(
|
|
236
|
-
"
|
|
201
|
+
"click", {"sessionId": sessionId, "selector": selector}
|
|
237
202
|
).get("structuredContent", {})
|
|
238
203
|
|
|
239
204
|
@_ensure_client
|
|
240
|
-
def
|
|
205
|
+
def type(self, sessionId: str, selector: str, text: str):
|
|
206
|
+
logger.debug("type | %s", selector)
|
|
241
207
|
return self.client.call_tool(
|
|
242
|
-
"
|
|
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
|
|
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
|
-
"
|
|
220
|
+
"clickToText", {"sessionId": sessionId, "text": text}
|
|
249
221
|
).get("structuredContent", {})
|
|
250
222
|
|
|
251
223
|
@_ensure_client
|
|
252
|
-
def
|
|
224
|
+
def find_element_xpath(self, sessionId: str, xpath: str):
|
|
225
|
+
logger.debug("find_element_xpath")
|
|
253
226
|
return self.client.call_tool(
|
|
254
|
-
"
|
|
255
|
-
)
|
|
227
|
+
"findElementByXPath", {"sessionId": sessionId, "xpath": xpath}
|
|
228
|
+
).get("structuredContent", {})
|
|
256
229
|
|
|
257
230
|
@_ensure_client
|
|
258
|
-
def
|
|
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
|
-
"
|
|
261
|
-
)
|
|
234
|
+
"findElementByText", {"sessionId": sessionId, "text": text}
|
|
235
|
+
).get("structuredContent", {})
|
|
262
236
|
|
|
263
237
|
@_ensure_client
|
|
264
|
-
def
|
|
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
|
-
"
|
|
267
|
-
|
|
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)
|
|
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
|
-
#
|
|
252
|
+
# FILE / COOKIE
|
|
282
253
|
# ======================================================
|
|
254
|
+
|
|
283
255
|
@_ensure_client
|
|
284
|
-
def
|
|
285
|
-
""
|
|
286
|
-
|
|
256
|
+
def upload_file(self, sessionId: str, selector: str, file_path: str):
|
|
257
|
+
logger.info("upload_file | %s", file_path)
|
|
258
|
+
|
|
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}"}
|
|
287
272
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
"
|
|
284
|
+
"uploadFile",
|
|
300
285
|
{
|
|
301
|
-
"
|
|
302
|
-
"
|
|
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
|
-
#
|
|
300
|
+
# AI / PARSING
|
|
308
301
|
# ======================================================
|
|
302
|
+
|
|
309
303
|
@_ensure_client
|
|
310
|
-
def
|
|
311
|
-
""
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
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":
|
|
335
|
+
"chunkSize": chunkSize,
|
|
351
336
|
},
|
|
352
337
|
).get("structuredContent", {})
|
|
353
338
|
|
|
354
339
|
@_ensure_client
|
|
355
|
-
def stream_pull(
|
|
356
|
-
|
|
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":
|
|
376
|
-
"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
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
467
|
-
""
|
|
468
|
-
|
|
469
|
-
|
|
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(
|
|
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,
|
|
527
|
-
)
|
|
528
|
-
""
|
|
529
|
-
|
|
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,58 +481,39 @@ 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)
|
|
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)
|
|
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
|
|
509
|
+
# VIEWPORT
|
|
594
510
|
# ======================================================
|
|
595
511
|
|
|
596
512
|
@_ensure_client
|
|
597
|
-
def get_viewport(self, sessionId: str)
|
|
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
|
|
|
609
519
|
@_ensure_client
|
|
@@ -615,16 +525,8 @@ class MCPTools:
|
|
|
615
525
|
height: int,
|
|
616
526
|
deviceScaleFactor: float = 1.0,
|
|
617
527
|
mobile: bool = False,
|
|
618
|
-
)
|
|
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
|
-
"""
|
|
528
|
+
):
|
|
529
|
+
logger.info("set_viewport | %sx%s", width, height)
|
|
628
530
|
return self.client.call_tool(
|
|
629
531
|
"viewport",
|
|
630
532
|
{
|
|
@@ -637,23 +539,3 @@ class MCPTools:
|
|
|
637
539
|
},
|
|
638
540
|
},
|
|
639
541
|
).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", {})
|