kl-mcp-client 2.1.0__tar.gz → 2.1.3__tar.gz
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-2.1.0 → kl_mcp_client-2.1.3}/PKG-INFO +1 -1
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client/asyncio/client.py +1 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client/asyncio/tools.py +47 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client/tools.py +151 -45
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client.egg-info/PKG-INFO +1 -1
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/pyproject.toml +1 -1
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/README.md +0 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client/__init__.py +0 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client/__version__.py +0 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client/asyncio/__init__.py +0 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client/client.py +0 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client.egg-info/SOURCES.txt +0 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client.egg-info/dependency_links.txt +0 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client.egg-info/requires.txt +0 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/kl_mcp_client.egg-info/top_level.txt +0 -0
- {kl_mcp_client-2.1.0 → kl_mcp_client-2.1.3}/setup.cfg +0 -0
|
@@ -272,3 +272,50 @@ class MCPTools:
|
|
|
272
272
|
},
|
|
273
273
|
)
|
|
274
274
|
return res.get("structuredContent", {})
|
|
275
|
+
|
|
276
|
+
# CLEAN TEXT / READ MODE
|
|
277
|
+
# ======================================================
|
|
278
|
+
async def get_clean_text(self, sessionId: str) -> Dict[str, Any]:
|
|
279
|
+
"""
|
|
280
|
+
Lấy toàn bộ visible text đã được clean trên trang hiện tại.
|
|
281
|
+
- Bỏ script/style/iframe/svg/canvas
|
|
282
|
+
- Chỉ text nhìn thấy (display/visibility/opacity)
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
{
|
|
286
|
+
"text": "...",
|
|
287
|
+
"length": 12345
|
|
288
|
+
}
|
|
289
|
+
"""
|
|
290
|
+
res = await self.client.call_tool(
|
|
291
|
+
"getCleanText",
|
|
292
|
+
{"sessionId": sessionId},
|
|
293
|
+
)
|
|
294
|
+
return res.get("structuredContent", {})
|
|
295
|
+
|
|
296
|
+
# ======================================================
|
|
297
|
+
# KEYBOARD (ASYNC)
|
|
298
|
+
# ======================================================
|
|
299
|
+
async def send_key(
|
|
300
|
+
self,
|
|
301
|
+
sessionId: str,
|
|
302
|
+
key: str,
|
|
303
|
+
) -> Dict[str, Any]:
|
|
304
|
+
"""
|
|
305
|
+
Send a keyboard key to the active page (async).
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
sessionId: MCP browser session
|
|
309
|
+
key: Keyboard key (e.g. "Enter", "Tab", "Escape", "ArrowDown",
|
|
310
|
+
"Ctrl+a", "Ctrl+Enter")
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
structuredContent from MCP server
|
|
314
|
+
"""
|
|
315
|
+
return await self.call_tool_structured(
|
|
316
|
+
"sendKey",
|
|
317
|
+
{
|
|
318
|
+
"sessionId": sessionId,
|
|
319
|
+
"key": key,
|
|
320
|
+
},
|
|
321
|
+
)
|
|
@@ -126,27 +126,63 @@ class MCPTools:
|
|
|
126
126
|
|
|
127
127
|
@_ensure_client
|
|
128
128
|
def upload_file(
|
|
129
|
-
self,
|
|
129
|
+
self,
|
|
130
|
+
sessionId: str,
|
|
131
|
+
selector: str,
|
|
132
|
+
file_path: str,
|
|
130
133
|
) -> Dict[str, Any]:
|
|
131
134
|
"""
|
|
132
|
-
Upload file vào input[type=file]
|
|
135
|
+
Upload file (kể cả video lớn) vào input[type=file] theo luồng mới:
|
|
136
|
+
1. Multipart upload file lên MCP server
|
|
137
|
+
2. Nhận uploadId
|
|
138
|
+
3. Gọi MCP tool uploadFile với uploadId
|
|
139
|
+
|
|
133
140
|
Args:
|
|
134
141
|
sessionId: MCP browser session
|
|
135
142
|
selector: CSS selector, ví dụ 'input[type=file]'
|
|
136
|
-
|
|
137
|
-
base64data: dữ liệu base64 (không kèm header)
|
|
138
|
-
Returns:
|
|
139
|
-
structured result từ server
|
|
143
|
+
file_path: đường dẫn file local (video, pdf, doc, ...)
|
|
140
144
|
"""
|
|
141
|
-
|
|
145
|
+
|
|
146
|
+
if not file_path:
|
|
147
|
+
return {"ok": False, "error": "file_path is required"}
|
|
148
|
+
|
|
149
|
+
# --------------------------------------------------
|
|
150
|
+
# 1️⃣ Multipart upload file lên MCP server
|
|
151
|
+
# --------------------------------------------------
|
|
152
|
+
try:
|
|
153
|
+
with open(file_path, "rb") as f:
|
|
154
|
+
resp = self.client.http.post(
|
|
155
|
+
"/upload",
|
|
156
|
+
files={"file": f},
|
|
157
|
+
timeout=300, # upload file lớn
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
return {"ok": False, "error": f"upload http failed: {e}"}
|
|
161
|
+
|
|
162
|
+
if resp.status_code != 200:
|
|
163
|
+
return {
|
|
164
|
+
"ok": False,
|
|
165
|
+
"error": f"upload http error {resp.status_code}: {resp.text}",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
data = resp.json()
|
|
169
|
+
upload_id = data.get("uploadId")
|
|
170
|
+
if not upload_id:
|
|
171
|
+
return {"ok": False, "error": "uploadId not returned from server"}
|
|
172
|
+
|
|
173
|
+
# --------------------------------------------------
|
|
174
|
+
# 2️⃣ Gọi MCP tool uploadFile (PATH MODE)
|
|
175
|
+
# --------------------------------------------------
|
|
176
|
+
result = self.client.call_tool(
|
|
142
177
|
"uploadFile",
|
|
143
178
|
{
|
|
144
179
|
"sessionId": sessionId,
|
|
145
180
|
"selector": selector,
|
|
146
|
-
"
|
|
147
|
-
"data": base64data,
|
|
181
|
+
"uploadId": upload_id,
|
|
148
182
|
},
|
|
149
|
-
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return result.get("structuredContent", {})
|
|
150
186
|
|
|
151
187
|
@_ensure_client
|
|
152
188
|
def wait_for_selector(
|
|
@@ -229,6 +265,16 @@ class MCPTools:
|
|
|
229
265
|
{"sessionId": sessionId, "selector": selector, "args": args or {}},
|
|
230
266
|
)
|
|
231
267
|
|
|
268
|
+
@_ensure_client
|
|
269
|
+
def find_element_by_prompt(self, sessionId: str, prompt: str) -> Dict[str, Any]:
|
|
270
|
+
"""
|
|
271
|
+
Gọi tool findElementByPrompt trên MCP server.
|
|
272
|
+
Trả về structuredContent gồm: html, nodeId.
|
|
273
|
+
"""
|
|
274
|
+
return self.client.call_tool(
|
|
275
|
+
"findElementByPrompt", {"sessionId": sessionId, "prompt": prompt}
|
|
276
|
+
).get("structuredContent", {})
|
|
277
|
+
|
|
232
278
|
# ======================================================
|
|
233
279
|
# AI / CONTENT PARSING
|
|
234
280
|
# ======================================================
|
|
@@ -254,65 +300,125 @@ class MCPTools:
|
|
|
254
300
|
"prompt": prompt,
|
|
255
301
|
},
|
|
256
302
|
).get("structuredContent", {})
|
|
303
|
+
|
|
257
304
|
# ======================================================
|
|
258
|
-
#
|
|
305
|
+
# CLEAN TEXT / READ MODE
|
|
259
306
|
# ======================================================
|
|
260
307
|
@_ensure_client
|
|
261
|
-
def
|
|
262
|
-
self,
|
|
263
|
-
sessionId: str,
|
|
264
|
-
x: float,
|
|
265
|
-
y: float,
|
|
266
|
-
) -> Dict[str, Any]:
|
|
308
|
+
def get_clean_text(self, sessionId: str) -> Dict[str, Any]:
|
|
267
309
|
"""
|
|
268
|
-
|
|
310
|
+
Lấy toàn bộ visible text đã được clean trên trang hiện tại.
|
|
311
|
+
- Bỏ script/style/iframe/svg/canvas
|
|
312
|
+
- Chỉ text nhìn thấy (display/visibility/opacity)
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
{
|
|
316
|
+
"text": "...",
|
|
317
|
+
"length": 12345
|
|
318
|
+
}
|
|
269
319
|
"""
|
|
270
320
|
return self.client.call_tool(
|
|
271
|
-
"
|
|
272
|
-
{
|
|
273
|
-
"sessionId": sessionId,
|
|
274
|
-
"action": "click",
|
|
275
|
-
"x": float(x),
|
|
276
|
-
"y": float(y),
|
|
277
|
-
},
|
|
321
|
+
"getCleanText",
|
|
322
|
+
{"sessionId": sessionId},
|
|
278
323
|
).get("structuredContent", {})
|
|
324
|
+
|
|
279
325
|
@_ensure_client
|
|
280
|
-
def
|
|
326
|
+
def evaluate_stream(
|
|
281
327
|
self,
|
|
282
328
|
sessionId: str,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
to_x: float,
|
|
286
|
-
to_y: float,
|
|
329
|
+
expression: str,
|
|
330
|
+
chunkSize: int = 100,
|
|
287
331
|
) -> Dict[str, Any]:
|
|
288
332
|
"""
|
|
289
|
-
|
|
333
|
+
Evaluate JS expression theo chế độ STREAM.
|
|
334
|
+
Dùng khi kết quả là array lớn (DOM, list, table, ...)
|
|
335
|
+
|
|
336
|
+
Returns (init response):
|
|
337
|
+
{
|
|
338
|
+
"stream_id": "...",
|
|
339
|
+
"total": 1234,
|
|
340
|
+
"chunk_size": 100
|
|
341
|
+
}
|
|
290
342
|
"""
|
|
291
343
|
return self.client.call_tool(
|
|
292
|
-
"
|
|
344
|
+
"evaluate.stream",
|
|
293
345
|
{
|
|
294
346
|
"sessionId": sessionId,
|
|
295
|
-
"
|
|
296
|
-
"
|
|
297
|
-
"to": {"x": float(to_x), "y": float(to_y)},
|
|
347
|
+
"expression": expression,
|
|
348
|
+
"chunkSize": int(chunkSize),
|
|
298
349
|
},
|
|
299
350
|
).get("structuredContent", {})
|
|
351
|
+
|
|
300
352
|
@_ensure_client
|
|
301
|
-
def
|
|
353
|
+
def stream_pull(
|
|
302
354
|
self,
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
355
|
+
stream_id: str,
|
|
356
|
+
offset: int = 0,
|
|
357
|
+
limit: int = 100,
|
|
306
358
|
) -> Dict[str, Any]:
|
|
307
359
|
"""
|
|
308
|
-
|
|
360
|
+
Kéo 1 chunk từ stream đã tạo bởi evaluate.stream
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
{
|
|
364
|
+
"items": [...],
|
|
365
|
+
"offset": 0,
|
|
366
|
+
"has_more": true
|
|
367
|
+
}
|
|
309
368
|
"""
|
|
310
369
|
return self.client.call_tool(
|
|
311
|
-
"
|
|
370
|
+
"stream.pull",
|
|
312
371
|
{
|
|
313
|
-
"
|
|
314
|
-
"
|
|
315
|
-
"
|
|
316
|
-
"y": float(y),
|
|
372
|
+
"stream_id": stream_id,
|
|
373
|
+
"offset": int(offset),
|
|
374
|
+
"limit": int(limit),
|
|
317
375
|
},
|
|
318
376
|
).get("structuredContent", {})
|
|
377
|
+
|
|
378
|
+
@_ensure_client
|
|
379
|
+
def evaluate_stream_all(
|
|
380
|
+
self,
|
|
381
|
+
sessionId: str,
|
|
382
|
+
expression: str,
|
|
383
|
+
chunkSize: int = 100,
|
|
384
|
+
max_items: Optional[int] = None,
|
|
385
|
+
):
|
|
386
|
+
"""
|
|
387
|
+
Helper: evaluate.stream + tự động pull toàn bộ dữ liệu.
|
|
388
|
+
|
|
389
|
+
⚠️ Chỉ dùng khi bạn THỰC SỰ cần full data.
|
|
390
|
+
"""
|
|
391
|
+
init = self.evaluate_stream(
|
|
392
|
+
sessionId=sessionId,
|
|
393
|
+
expression=expression,
|
|
394
|
+
chunkSize=chunkSize,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
stream_id = init.get("stream_id")
|
|
398
|
+
total = init.get("total", 0)
|
|
399
|
+
|
|
400
|
+
if not stream_id:
|
|
401
|
+
return []
|
|
402
|
+
|
|
403
|
+
items = []
|
|
404
|
+
offset = 0
|
|
405
|
+
|
|
406
|
+
while True:
|
|
407
|
+
chunk = self.stream_pull(
|
|
408
|
+
stream_id=stream_id,
|
|
409
|
+
offset=offset,
|
|
410
|
+
limit=chunkSize,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
part = chunk.get("items", [])
|
|
414
|
+
items.extend(part)
|
|
415
|
+
|
|
416
|
+
if max_items and len(items) >= max_items:
|
|
417
|
+
return items[:max_items]
|
|
418
|
+
|
|
419
|
+
if not chunk.get("has_more"):
|
|
420
|
+
break
|
|
421
|
+
|
|
422
|
+
offset += chunkSize
|
|
423
|
+
|
|
424
|
+
return items
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|