kl-mcp-client 1.0.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.0/PKG-INFO +8 -0
- kl_mcp_client-1.0.0/README.md +0 -0
- kl_mcp_client-1.0.0/kl_mcp_client.egg-info/PKG-INFO +8 -0
- kl_mcp_client-1.0.0/kl_mcp_client.egg-info/SOURCES.txt +9 -0
- kl_mcp_client-1.0.0/kl_mcp_client.egg-info/dependency_links.txt +1 -0
- kl_mcp_client-1.0.0/kl_mcp_client.egg-info/top_level.txt +1 -0
- kl_mcp_client-1.0.0/mcp_client/__init__.py +0 -0
- kl_mcp_client-1.0.0/mcp_client/client.py +136 -0
- kl_mcp_client-1.0.0/mcp_client/tools.py +173 -0
- kl_mcp_client-1.0.0/pyproject.toml +14 -0
- kl_mcp_client-1.0.0/setup.cfg +4 -0
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp_client
|
|
File without changes
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# mcp_client.py
|
|
2
|
+
import time
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
DEFAULT_HEADERS = {
|
|
9
|
+
"Content-Type": "application/json",
|
|
10
|
+
"Accept": "application/json, text/event-stream",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MCPError(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MCPClient:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
base_url: str,
|
|
22
|
+
headers: Optional[Dict[str, str]] = None,
|
|
23
|
+
timeout: int = 30,
|
|
24
|
+
retries: int = 1,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
base_url: full MCP HTTP endpoint e.g. http://localhost:3000/mcp
|
|
28
|
+
headers: extra headers (e.g. {"Authorization": "Bearer ..."})
|
|
29
|
+
timeout: request timeout seconds
|
|
30
|
+
retries: number of attempts for network errors
|
|
31
|
+
"""
|
|
32
|
+
self.base_url = base_url.rstrip("/")
|
|
33
|
+
self.headers = DEFAULT_HEADERS.copy()
|
|
34
|
+
if headers:
|
|
35
|
+
self.headers.update(headers)
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
self.retries = max(1, int(retries))
|
|
38
|
+
# local cache of sessions -> can be used to reuse session ids
|
|
39
|
+
self._sessions: Dict[str, Dict[str, Any]] = {}
|
|
40
|
+
|
|
41
|
+
def _rpc(self, method: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
42
|
+
payload = {
|
|
43
|
+
"jsonrpc": "2.0",
|
|
44
|
+
"id": str(uuid.uuid4()),
|
|
45
|
+
"method": method,
|
|
46
|
+
"params": params,
|
|
47
|
+
}
|
|
48
|
+
last_exc = None
|
|
49
|
+
for attempt in range(self.retries):
|
|
50
|
+
try:
|
|
51
|
+
r = requests.post(
|
|
52
|
+
self.base_url,
|
|
53
|
+
json=payload,
|
|
54
|
+
headers=self.headers,
|
|
55
|
+
timeout=self.timeout,
|
|
56
|
+
)
|
|
57
|
+
r.raise_for_status()
|
|
58
|
+
except requests.RequestException as e:
|
|
59
|
+
last_exc = e
|
|
60
|
+
time.sleep(0.2 * (attempt + 1))
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
data = r.json()
|
|
65
|
+
except ValueError:
|
|
66
|
+
raise MCPError(
|
|
67
|
+
f"Invalid JSON response (status {r.status_code}): {r.text}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if "error" in data and data["error"] is not None:
|
|
71
|
+
err = data["error"]
|
|
72
|
+
raise MCPError(
|
|
73
|
+
{
|
|
74
|
+
"code": err.get("code"),
|
|
75
|
+
"message": err.get("message"),
|
|
76
|
+
"data": err.get("data"),
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
return data.get("result", {})
|
|
80
|
+
|
|
81
|
+
# all retries failed
|
|
82
|
+
raise MCPError(f"Request failed after {self.retries} attempts: {last_exc}")
|
|
83
|
+
|
|
84
|
+
# ----- Generic callTool wrapper -----
|
|
85
|
+
def call_tool_structured(
|
|
86
|
+
self, tool: str, arguments: Optional[Dict[str, Any]] = None
|
|
87
|
+
) -> Dict[str, Any]:
|
|
88
|
+
args = {"tool": tool, "arguments": arguments or {}}
|
|
89
|
+
res = self._rpc("callTool", args)
|
|
90
|
+
# return structuredContent when possible
|
|
91
|
+
if isinstance(res, dict) and "structuredContent" in res:
|
|
92
|
+
return res["structuredContent"]
|
|
93
|
+
# fallback: maybe top-level result
|
|
94
|
+
return res
|
|
95
|
+
|
|
96
|
+
# ----- Generic callTool wrapper -----
|
|
97
|
+
def call_tool(
|
|
98
|
+
self, tool: str, arguments: Optional[Dict[str, Any]] = None
|
|
99
|
+
) -> Dict[str, Any]:
|
|
100
|
+
args = {"tool": tool, "arguments": arguments or {}}
|
|
101
|
+
res = self._rpc("callTool", args)
|
|
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
|
|
107
|
+
|
|
108
|
+
# ----- Session helpers -----
|
|
109
|
+
def create_session(self, cdpUrl: str) -> str:
|
|
110
|
+
result = self.call_tool_structured("createSession", {"cdpUrl": cdpUrl})
|
|
111
|
+
# Many MCP servers put sessionId in structuredContent.sessionId or result.sessionId
|
|
112
|
+
session_id = None
|
|
113
|
+
if isinstance(result, dict):
|
|
114
|
+
session_id = (
|
|
115
|
+
result.get("sessionId") or result.get("session_id") or result.get("id")
|
|
116
|
+
)
|
|
117
|
+
if not session_id:
|
|
118
|
+
# fallback: try raw result text or content
|
|
119
|
+
raise MCPError("createSession did not return sessionId")
|
|
120
|
+
# store meta
|
|
121
|
+
self._sessions[session_id] = {"created_at": time.time()}
|
|
122
|
+
return session_id
|
|
123
|
+
|
|
124
|
+
def close_session(self, session_id: str) -> bool:
|
|
125
|
+
try:
|
|
126
|
+
self.call_tool_structured("closeSession", {"sessionId": session_id})
|
|
127
|
+
if session_id in self._sessions:
|
|
128
|
+
del self._sessions[session_id]
|
|
129
|
+
return True
|
|
130
|
+
except MCPError:
|
|
131
|
+
# still remove local record if present
|
|
132
|
+
self._sessions.pop(session_id, None)
|
|
133
|
+
raise
|
|
134
|
+
|
|
135
|
+
def list_local_sessions(self) -> List[str]:
|
|
136
|
+
return list(self._sessions.keys())
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .client import MCPClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MCPTools:
|
|
7
|
+
"""
|
|
8
|
+
Wrapper chuẩn cho Google ADK + MCP Server.
|
|
9
|
+
- Tool nào trả text/html/... → dùng structuredContent
|
|
10
|
+
- Tool screenshot → trả đúng content để ADK Web hiển thị image
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, client: MCPClient):
|
|
14
|
+
self.client = client
|
|
15
|
+
|
|
16
|
+
# ======================================================
|
|
17
|
+
# SESSION MANAGEMENT
|
|
18
|
+
# ======================================================
|
|
19
|
+
|
|
20
|
+
def create_session(self, cdpUrl: str) -> Dict[str, Any]:
|
|
21
|
+
sid = self.client.create_session(cdpUrl)
|
|
22
|
+
return {"sessionId": sid}
|
|
23
|
+
|
|
24
|
+
def close_session(self, sessionId: str) -> Dict[str, Any]:
|
|
25
|
+
ok = self.client.close_session(sessionId)
|
|
26
|
+
return {"ok": bool(ok)}
|
|
27
|
+
|
|
28
|
+
def list_sessions(self) -> Dict[str, Any]:
|
|
29
|
+
return {"sessions": self.client.list_local_sessions()}
|
|
30
|
+
|
|
31
|
+
# ======================================================
|
|
32
|
+
# NAVIGATION & DOM
|
|
33
|
+
# ======================================================
|
|
34
|
+
|
|
35
|
+
def open_page(self, sessionId: str, url: str) -> Dict[str, Any]:
|
|
36
|
+
return self.client.call_tool(
|
|
37
|
+
"openPage", {"sessionId": sessionId, "url": url}
|
|
38
|
+
).get("structuredContent", {})
|
|
39
|
+
|
|
40
|
+
def get_html(self, sessionId: str) -> Dict[str, Any]:
|
|
41
|
+
return self.client.call_tool("getHTML", {"sessionId": sessionId}).get(
|
|
42
|
+
"structuredContent", {}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def screenshot(self, sessionId: str) -> Dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Trả về đúng phần IMAGE content:
|
|
48
|
+
{
|
|
49
|
+
"type": "image",
|
|
50
|
+
"mimeType": "image/png",
|
|
51
|
+
"data": "<base64>"
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
full = self.client.call_tool("screenshot", {"sessionId": sessionId})
|
|
55
|
+
return full["content"][0]
|
|
56
|
+
|
|
57
|
+
def click(self, sessionId: str, selector: str) -> Dict[str, Any]:
|
|
58
|
+
return self.client.call_tool(
|
|
59
|
+
"click", {"sessionId": sessionId, "selector": selector}
|
|
60
|
+
).get("structuredContent", {})
|
|
61
|
+
|
|
62
|
+
def type(self, sessionId: str, selector: str, text: str) -> Dict[str, Any]:
|
|
63
|
+
return self.client.call_tool(
|
|
64
|
+
"type", {"sessionId": sessionId, "selector": selector, "text": text}
|
|
65
|
+
).get("structuredContent", {})
|
|
66
|
+
|
|
67
|
+
def evaluate(self, sessionId: str, expression: str) -> Dict[str, Any]:
|
|
68
|
+
return self.client.call_tool(
|
|
69
|
+
"evaluate", {"sessionId": sessionId, "expression": expression}
|
|
70
|
+
).get("structuredContent", {})
|
|
71
|
+
|
|
72
|
+
# ======================================================
|
|
73
|
+
# ELEMENT UTILITIES
|
|
74
|
+
# ======================================================
|
|
75
|
+
|
|
76
|
+
def find_element(self, sessionId: str, selector: str) -> Dict[str, Any]:
|
|
77
|
+
return self.client.call_tool(
|
|
78
|
+
"findElement", {"sessionId": sessionId, "selector": selector}
|
|
79
|
+
).get("structuredContent", {})
|
|
80
|
+
|
|
81
|
+
def find_all(self, sessionId: str, selector: str) -> Dict[str, Any]:
|
|
82
|
+
return self.client.call_tool(
|
|
83
|
+
"findAll", {"sessionId": sessionId, "selector": selector}
|
|
84
|
+
).get("structuredContent", {})
|
|
85
|
+
|
|
86
|
+
def get_bounding_box(self, sessionId: str, selector: str) -> Dict[str, Any]:
|
|
87
|
+
return self.client.call_tool(
|
|
88
|
+
"getBoundingBox", {"sessionId": sessionId, "selector": selector}
|
|
89
|
+
).get("structuredContent", {})
|
|
90
|
+
|
|
91
|
+
def click_bounding_box(self, sessionId: str, selector: str) -> Dict[str, Any]:
|
|
92
|
+
return self.client.call_tool(
|
|
93
|
+
"clickBoundingBox", {"sessionId": sessionId, "selector": selector}
|
|
94
|
+
).get("structuredContent", {})
|
|
95
|
+
|
|
96
|
+
def upload_file(
|
|
97
|
+
self, sessionId: str, selector: str, filename: str, base64data: str
|
|
98
|
+
) -> Dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Upload file vào input[type=file]
|
|
101
|
+
Args:
|
|
102
|
+
sessionId: MCP browser session
|
|
103
|
+
selector: CSS selector, ví dụ 'input[type=file]'
|
|
104
|
+
filename: tên file trên browser side
|
|
105
|
+
base64data: dữ liệu base64 (không kèm header)
|
|
106
|
+
Returns:
|
|
107
|
+
structured result từ server
|
|
108
|
+
"""
|
|
109
|
+
return self.client.call_tool(
|
|
110
|
+
"uploadFile",
|
|
111
|
+
{
|
|
112
|
+
"sessionId": sessionId,
|
|
113
|
+
"selector": selector,
|
|
114
|
+
"filename": filename,
|
|
115
|
+
"data": base64data,
|
|
116
|
+
},
|
|
117
|
+
).get("structuredContent", {})
|
|
118
|
+
|
|
119
|
+
def wait_for_selector(
|
|
120
|
+
self, sessionId: str, selector: str, timeoutMs: Optional[int] = None
|
|
121
|
+
) -> Dict[str, Any]:
|
|
122
|
+
args = {"sessionId": sessionId, "selector": selector}
|
|
123
|
+
if timeoutMs is not None:
|
|
124
|
+
args["timeoutMs"] = int(timeoutMs)
|
|
125
|
+
|
|
126
|
+
return self.client.call_tool("waitForSelector", args).get(
|
|
127
|
+
"structuredContent", {}
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# ======================================================
|
|
131
|
+
# TAB MANAGEMENT
|
|
132
|
+
# ======================================================
|
|
133
|
+
|
|
134
|
+
def new_tab(
|
|
135
|
+
self, sessionId: str, url: Optional[str] = "about:blank"
|
|
136
|
+
) -> Dict[str, Any]:
|
|
137
|
+
return self.client.call_tool(
|
|
138
|
+
"newTab", {"sessionId": sessionId, "url": url}
|
|
139
|
+
).get("structuredContent", {})
|
|
140
|
+
|
|
141
|
+
def switch_tab(self, sessionId: str, targetId: str) -> Dict[str, Any]:
|
|
142
|
+
return self.client.call_tool(
|
|
143
|
+
"switchTab", {"sessionId": sessionId, "targetId": targetId}
|
|
144
|
+
).get("structuredContent", {})
|
|
145
|
+
|
|
146
|
+
# ======================================================
|
|
147
|
+
# ADVANCED ACTIONS
|
|
148
|
+
# ======================================================
|
|
149
|
+
|
|
150
|
+
def click_to_text(self, sessionId: str, text: str) -> dict:
|
|
151
|
+
return self.client.call_tool(
|
|
152
|
+
"clickToText", {"sessionId": sessionId, "text": text}
|
|
153
|
+
).get("structuredContent", {})
|
|
154
|
+
|
|
155
|
+
def find_element_xpath(self, sessionId: str, xpath: str) -> Dict[str, Any]:
|
|
156
|
+
return self.client.call_tool(
|
|
157
|
+
"findElementByXPath", {"sessionId": sessionId, "xpath": xpath}
|
|
158
|
+
).get("structuredContent", {})
|
|
159
|
+
|
|
160
|
+
def find_element_by_text(self, sessionId: str, text: str) -> Dict[str, Any]:
|
|
161
|
+
return self.client.call_tool(
|
|
162
|
+
"findElementByText", {"sessionId": sessionId, "text": text}
|
|
163
|
+
).get("structuredContent", {})
|
|
164
|
+
|
|
165
|
+
def click_by_node_id(self, sessionId: str, nodeId: int) -> Dict[str, Any]:
|
|
166
|
+
return self.client.call_tool(
|
|
167
|
+
"clickByNodeId", {"sessionId": sessionId, "nodeId": nodeId}
|
|
168
|
+
).get("structuredContent", {})
|
|
169
|
+
|
|
170
|
+
def import_cookies(self, sessionId: str, cookies: dict) -> Dict[str, Any]:
|
|
171
|
+
return self.client.call_tool(
|
|
172
|
+
"importCookies", {"sessionId": sessionId, "cookies": cookies}
|
|
173
|
+
).get("structuredContent", {})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kl-mcp-client"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "MCP Client"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name="Kyle", email="hngan.it@gmail.com" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
requires-python = ">=3.7"
|