kl-mcp-client 1.0.6__tar.gz → 1.1.0__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-1.0.6 → kl_mcp_client-1.1.0}/PKG-INFO +2 -2
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client/asyncio/client.py +17 -10
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client/client.py +42 -28
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client/tools.py +26 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client.egg-info/PKG-INFO +2 -2
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/pyproject.toml +2 -2
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/README.md +0 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client/__init__.py +0 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client/__version__.py +0 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client/asyncio/__init__.py +0 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client/asyncio/tools.py +0 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client.egg-info/SOURCES.txt +0 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client.egg-info/dependency_links.txt +0 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client.egg-info/requires.txt +0 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/kl_mcp_client.egg-info/top_level.txt +0 -0
- {kl_mcp_client-1.0.6 → kl_mcp_client-1.1.0}/setup.cfg +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# mcp_client_async.py
|
|
2
2
|
import uuid
|
|
3
3
|
import time
|
|
4
|
+
import asyncio
|
|
4
5
|
from typing import Any, Dict, List, Optional
|
|
5
6
|
|
|
6
7
|
import httpx
|
|
7
|
-
import asyncio
|
|
8
|
-
|
|
9
8
|
|
|
10
9
|
DEFAULT_HEADERS = {
|
|
11
10
|
"Content-Type": "application/json",
|
|
@@ -24,12 +23,18 @@ class MCPClient:
|
|
|
24
23
|
headers: Optional[Dict[str, str]] = None,
|
|
25
24
|
timeout: int = 30,
|
|
26
25
|
retries: int = 1,
|
|
26
|
+
proxies: Optional[Dict[str, str]] = None,
|
|
27
27
|
):
|
|
28
28
|
"""
|
|
29
29
|
base_url: full MCP HTTP endpoint e.g. http://localhost:3000/mcp
|
|
30
30
|
headers: extra headers (e.g. {"Authorization": "Bearer ..."})
|
|
31
31
|
timeout: request timeout seconds
|
|
32
32
|
retries: number of attempts for network errors
|
|
33
|
+
proxies: httpx proxies dict, e.g.
|
|
34
|
+
{
|
|
35
|
+
"http://": "http://127.0.0.1:8080",
|
|
36
|
+
"https://": "http://127.0.0.1:8080"
|
|
37
|
+
}
|
|
33
38
|
"""
|
|
34
39
|
self.base_url = base_url.rstrip("/")
|
|
35
40
|
self.headers = DEFAULT_HEADERS.copy()
|
|
@@ -40,13 +45,17 @@ class MCPClient:
|
|
|
40
45
|
self.retries = max(1, retries)
|
|
41
46
|
|
|
42
47
|
self._client = httpx.AsyncClient(
|
|
43
|
-
timeout=self.timeout,
|
|
44
48
|
headers=self.headers,
|
|
49
|
+
timeout=httpx.Timeout(self.timeout),
|
|
50
|
+
proxies=proxies,
|
|
45
51
|
)
|
|
46
52
|
|
|
47
53
|
# local session cache
|
|
48
54
|
self._sessions: Dict[str, Dict[str, Any]] = {}
|
|
49
55
|
|
|
56
|
+
# -------------------------
|
|
57
|
+
# JSON-RPC core
|
|
58
|
+
# -------------------------
|
|
50
59
|
async def _rpc(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
51
60
|
payload = {
|
|
52
61
|
"jsonrpc": "2.0",
|
|
@@ -55,15 +64,13 @@ class MCPClient:
|
|
|
55
64
|
"params": params,
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
last_exc = None
|
|
67
|
+
last_exc: Optional[Exception] = None
|
|
68
|
+
|
|
59
69
|
for attempt in range(self.retries):
|
|
60
70
|
try:
|
|
61
|
-
r = await self._client.post(
|
|
62
|
-
self.base_url,
|
|
63
|
-
json=payload,
|
|
64
|
-
)
|
|
71
|
+
r = await self._client.post(self.base_url, json=payload)
|
|
65
72
|
r.raise_for_status()
|
|
66
|
-
except
|
|
73
|
+
except httpx.HTTPError as e:
|
|
67
74
|
last_exc = e
|
|
68
75
|
await asyncio.sleep(0.2 * (attempt + 1))
|
|
69
76
|
continue
|
|
@@ -89,7 +96,7 @@ class MCPClient:
|
|
|
89
96
|
raise MCPError(f"Request failed after {self.retries} attempts: {last_exc}")
|
|
90
97
|
|
|
91
98
|
# -------------------------
|
|
92
|
-
#
|
|
99
|
+
# Tool wrappers
|
|
93
100
|
# -------------------------
|
|
94
101
|
|
|
95
102
|
async def call_tool_structured(
|
|
@@ -3,7 +3,7 @@ import time
|
|
|
3
3
|
import uuid
|
|
4
4
|
from typing import Any, Dict, List, Optional
|
|
5
5
|
|
|
6
|
-
import
|
|
6
|
+
import httpx
|
|
7
7
|
|
|
8
8
|
DEFAULT_HEADERS = {
|
|
9
9
|
"Content-Type": "application/json",
|
|
@@ -22,22 +22,43 @@ class MCPClient:
|
|
|
22
22
|
headers: Optional[Dict[str, str]] = None,
|
|
23
23
|
timeout: int = 30,
|
|
24
24
|
retries: int = 1,
|
|
25
|
+
proxies: Optional[Dict[str, str]] = None,
|
|
25
26
|
):
|
|
26
27
|
"""
|
|
27
28
|
base_url: full MCP HTTP endpoint e.g. http://localhost:3000/mcp
|
|
28
29
|
headers: extra headers (e.g. {"Authorization": "Bearer ..."})
|
|
29
30
|
timeout: request timeout seconds
|
|
30
31
|
retries: number of attempts for network errors
|
|
32
|
+
proxies: httpx proxies dict, e.g.
|
|
33
|
+
{
|
|
34
|
+
"http://": "http://127.0.0.1:8080",
|
|
35
|
+
"https://": "http://127.0.0.1:8080"
|
|
36
|
+
}
|
|
31
37
|
"""
|
|
32
38
|
self.base_url = base_url.rstrip("/")
|
|
33
39
|
self.headers = DEFAULT_HEADERS.copy()
|
|
34
40
|
if headers:
|
|
35
41
|
self.headers.update(headers)
|
|
42
|
+
|
|
36
43
|
self.timeout = timeout
|
|
37
44
|
self.retries = max(1, int(retries))
|
|
38
|
-
# local cache of sessions -> can be used to reuse session ids
|
|
39
45
|
self._sessions: Dict[str, Dict[str, Any]] = {}
|
|
40
46
|
|
|
47
|
+
# httpx client (sync)
|
|
48
|
+
self._client = httpx.Client(
|
|
49
|
+
base_url=self.base_url,
|
|
50
|
+
headers=self.headers,
|
|
51
|
+
timeout=httpx.Timeout(self.timeout),
|
|
52
|
+
proxies=proxies,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def close(self):
|
|
56
|
+
"""Close underlying httpx client."""
|
|
57
|
+
self._client.close()
|
|
58
|
+
|
|
59
|
+
# ---------------------------
|
|
60
|
+
# JSON-RPC core
|
|
61
|
+
# ---------------------------
|
|
41
62
|
def _rpc(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
42
63
|
payload = {
|
|
43
64
|
"jsonrpc": "2.0",
|
|
@@ -45,17 +66,14 @@ class MCPClient:
|
|
|
45
66
|
"method": method,
|
|
46
67
|
"params": params,
|
|
47
68
|
}
|
|
48
|
-
|
|
69
|
+
|
|
70
|
+
last_exc: Optional[Exception] = None
|
|
71
|
+
|
|
49
72
|
for attempt in range(self.retries):
|
|
50
73
|
try:
|
|
51
|
-
r =
|
|
52
|
-
self.base_url,
|
|
53
|
-
json=payload,
|
|
54
|
-
headers=self.headers,
|
|
55
|
-
timeout=self.timeout,
|
|
56
|
-
)
|
|
74
|
+
r = self._client.post("", json=payload)
|
|
57
75
|
r.raise_for_status()
|
|
58
|
-
except
|
|
76
|
+
except httpx.HTTPError as e:
|
|
59
77
|
last_exc = e
|
|
60
78
|
time.sleep(0.2 * (attempt + 1))
|
|
61
79
|
continue
|
|
@@ -76,59 +94,55 @@ class MCPClient:
|
|
|
76
94
|
"data": err.get("data"),
|
|
77
95
|
}
|
|
78
96
|
)
|
|
97
|
+
|
|
79
98
|
return data.get("result", {})
|
|
80
99
|
|
|
81
|
-
# all retries failed
|
|
82
100
|
raise MCPError(f"Request failed after {self.retries} attempts: {last_exc}")
|
|
83
101
|
|
|
84
|
-
#
|
|
102
|
+
# ---------------------------
|
|
103
|
+
# Tool wrappers
|
|
104
|
+
# ---------------------------
|
|
85
105
|
def call_tool_structured(
|
|
86
106
|
self, tool: str, arguments: Optional[Dict[str, Any]] = None
|
|
87
107
|
) -> Dict[str, Any]:
|
|
88
108
|
args = {"tool": tool, "arguments": arguments or {}}
|
|
89
109
|
res = self._rpc("callTool", args)
|
|
90
|
-
|
|
110
|
+
|
|
91
111
|
if isinstance(res, dict) and "structuredContent" in res:
|
|
92
112
|
return res["structuredContent"]
|
|
93
|
-
|
|
113
|
+
|
|
94
114
|
return res
|
|
95
115
|
|
|
96
|
-
# ----- Generic callTool wrapper -----
|
|
97
116
|
def call_tool(
|
|
98
117
|
self, tool: str, arguments: Optional[Dict[str, Any]] = None
|
|
99
118
|
) -> Dict[str, Any]:
|
|
100
119
|
args = {"tool": tool, "arguments": arguments or {}}
|
|
101
|
-
|
|
102
|
-
# # return structuredContent when possible
|
|
103
|
-
# if isinstance(res, dict) and "structuredContent" in res:
|
|
104
|
-
# return res["structuredContent"]
|
|
105
|
-
# # fallback: maybe top-level result
|
|
106
|
-
return res
|
|
120
|
+
return self._rpc("callTool", args)
|
|
107
121
|
|
|
108
|
-
#
|
|
122
|
+
# ---------------------------
|
|
123
|
+
# Session helpers
|
|
124
|
+
# ---------------------------
|
|
109
125
|
def create_session(self, cdpUrl: str) -> str:
|
|
110
126
|
result = self.call_tool_structured("createSession", {"cdpUrl": cdpUrl})
|
|
111
|
-
|
|
127
|
+
|
|
112
128
|
session_id = None
|
|
113
129
|
if isinstance(result, dict):
|
|
114
130
|
session_id = (
|
|
115
131
|
result.get("sessionId") or result.get("session_id") or result.get("id")
|
|
116
132
|
)
|
|
133
|
+
|
|
117
134
|
if not session_id:
|
|
118
|
-
# fallback: try raw result text or content
|
|
119
135
|
raise MCPError("createSession did not return sessionId")
|
|
120
|
-
|
|
136
|
+
|
|
121
137
|
self._sessions[session_id] = {"created_at": time.time()}
|
|
122
138
|
return session_id
|
|
123
139
|
|
|
124
140
|
def close_session(self, session_id: str) -> bool:
|
|
125
141
|
try:
|
|
126
142
|
self.call_tool_structured("closeSession", {"sessionId": session_id})
|
|
127
|
-
|
|
128
|
-
del self._sessions[session_id]
|
|
143
|
+
self._sessions.pop(session_id, None)
|
|
129
144
|
return True
|
|
130
145
|
except MCPError:
|
|
131
|
-
# still remove local record if present
|
|
132
146
|
self._sessions.pop(session_id, None)
|
|
133
147
|
raise
|
|
134
148
|
|
|
@@ -228,3 +228,29 @@ class MCPTools:
|
|
|
228
228
|
"selectorMap",
|
|
229
229
|
{"sessionId": sessionId, "selector": selector, "args": args or {}},
|
|
230
230
|
)
|
|
231
|
+
|
|
232
|
+
# ======================================================
|
|
233
|
+
# AI / CONTENT PARSING
|
|
234
|
+
# ======================================================
|
|
235
|
+
@_ensure_client
|
|
236
|
+
def parse_html_by_prompt(self, html: str, prompt: str) -> Dict[str, Any]:
|
|
237
|
+
"""
|
|
238
|
+
Parse HTML content using AI with dynamic prompt-defined structure.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
html: Raw HTML string (client-provided)
|
|
242
|
+
prompt: Instruction that defines what to extract and output structure
|
|
243
|
+
Example:
|
|
244
|
+
- "Hãy lấy nội dung bài viết, struct trả về { content }"
|
|
245
|
+
- "Hãy lấy số lượng like, share, comment, trả JSON { like, share, comment }"
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
structuredContent (dynamic JSON defined by prompt)
|
|
249
|
+
"""
|
|
250
|
+
return self.client.call_tool(
|
|
251
|
+
"parseHTMLByPrompt",
|
|
252
|
+
{
|
|
253
|
+
"html": html,
|
|
254
|
+
"prompt": prompt,
|
|
255
|
+
},
|
|
256
|
+
).get("structuredContent", {})
|
|
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
|