Spaces:
Sleeping
Sleeping
apple muncy
commited on
Commit
Β·
8a76396
1
Parent(s):
4f3d774
add main.py and update to mcp 1.12.4
Browse filesSigned-off-by: apple muncy <[email protected]>
- main.py +365 -0
- requirements.txt +1 -1
main.py
ADDED
@@ -0,0 +1,365 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
"""
|
3 |
+
Simple MCP client example with OAuth authentication support.
|
4 |
+
|
5 |
+
This client connects to an MCP server using streamable HTTP transport with OAuth.
|
6 |
+
|
7 |
+
"""
|
8 |
+
|
9 |
+
import asyncio
|
10 |
+
import os
|
11 |
+
import threading
|
12 |
+
import time
|
13 |
+
import webbrowser
|
14 |
+
from datetime import timedelta
|
15 |
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
16 |
+
from typing import Any
|
17 |
+
from urllib.parse import parse_qs, urlparse
|
18 |
+
|
19 |
+
from mcp.client.auth import OAuthClientProvider, TokenStorage
|
20 |
+
from mcp.client.session import ClientSession
|
21 |
+
from mcp.client.sse import sse_client
|
22 |
+
from mcp.client.streamable_http import streamablehttp_client
|
23 |
+
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
|
24 |
+
|
25 |
+
|
26 |
+
class InMemoryTokenStorage(TokenStorage):
|
27 |
+
"""Simple in-memory token storage implementation."""
|
28 |
+
|
29 |
+
def __init__(self):
|
30 |
+
self._tokens: OAuthToken | None = None
|
31 |
+
self._client_info: OAuthClientInformationFull | None = None
|
32 |
+
|
33 |
+
async def get_tokens(self) -> OAuthToken | None:
|
34 |
+
return self._tokens
|
35 |
+
|
36 |
+
async def set_tokens(self, tokens: OAuthToken) -> None:
|
37 |
+
self._tokens = tokens
|
38 |
+
|
39 |
+
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
40 |
+
return self._client_info
|
41 |
+
|
42 |
+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
|
43 |
+
self._client_info = client_info
|
44 |
+
|
45 |
+
|
46 |
+
class CallbackHandler(BaseHTTPRequestHandler):
|
47 |
+
"""Simple HTTP handler to capture OAuth callback."""
|
48 |
+
|
49 |
+
def __init__(self, request, client_address, server, callback_data):
|
50 |
+
"""Initialize with callback data storage."""
|
51 |
+
self.callback_data = callback_data
|
52 |
+
super().__init__(request, client_address, server)
|
53 |
+
|
54 |
+
def do_GET(self):
|
55 |
+
"""Handle GET request from OAuth redirect."""
|
56 |
+
parsed = urlparse(self.path)
|
57 |
+
query_params = parse_qs(parsed.query)
|
58 |
+
|
59 |
+
if "code" in query_params:
|
60 |
+
self.callback_data["authorization_code"] = query_params["code"][0]
|
61 |
+
self.callback_data["state"] = query_params.get("state", [None])[0]
|
62 |
+
self.send_response(200)
|
63 |
+
self.send_header("Content-type", "text/html")
|
64 |
+
self.end_headers()
|
65 |
+
self.wfile.write(b"""
|
66 |
+
<html>
|
67 |
+
<body>
|
68 |
+
<h1>Authorization Successful!</h1>
|
69 |
+
<p>You can close this window and return to the terminal.</p>
|
70 |
+
<script>setTimeout(() => window.close(), 2000);</script>
|
71 |
+
</body>
|
72 |
+
</html>
|
73 |
+
""")
|
74 |
+
elif "error" in query_params:
|
75 |
+
self.callback_data["error"] = query_params["error"][0]
|
76 |
+
self.send_response(400)
|
77 |
+
self.send_header("Content-type", "text/html")
|
78 |
+
self.end_headers()
|
79 |
+
self.wfile.write(
|
80 |
+
f"""
|
81 |
+
<html>
|
82 |
+
<body>
|
83 |
+
<h1>Authorization Failed</h1>
|
84 |
+
<p>Error: {query_params["error"][0]}</p>
|
85 |
+
<p>You can close this window and return to the terminal.</p>
|
86 |
+
</body>
|
87 |
+
</html>
|
88 |
+
""".encode()
|
89 |
+
)
|
90 |
+
else:
|
91 |
+
self.send_response(404)
|
92 |
+
self.end_headers()
|
93 |
+
|
94 |
+
def log_message(self, format, *args):
|
95 |
+
"""Suppress default logging."""
|
96 |
+
pass
|
97 |
+
|
98 |
+
|
99 |
+
class CallbackServer:
|
100 |
+
"""Simple server to handle OAuth callbacks."""
|
101 |
+
|
102 |
+
def __init__(self, port=3000):
|
103 |
+
self.port = port
|
104 |
+
self.server = None
|
105 |
+
self.thread = None
|
106 |
+
self.callback_data = {"authorization_code": None, "state": None, "error": None}
|
107 |
+
|
108 |
+
def _create_handler_with_data(self):
|
109 |
+
"""Create a handler class with access to callback data."""
|
110 |
+
callback_data = self.callback_data
|
111 |
+
|
112 |
+
class DataCallbackHandler(CallbackHandler):
|
113 |
+
def __init__(self, request, client_address, server):
|
114 |
+
super().__init__(request, client_address, server, callback_data)
|
115 |
+
|
116 |
+
return DataCallbackHandler
|
117 |
+
|
118 |
+
def start(self):
|
119 |
+
"""Start the callback server in a background thread."""
|
120 |
+
handler_class = self._create_handler_with_data()
|
121 |
+
self.server = HTTPServer(("0.0.0.0", self.port), handler_class)
|
122 |
+
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
123 |
+
self.thread.start()
|
124 |
+
print(f"π₯οΈ Started callback server on http://0.0.0.0:{self.port}")
|
125 |
+
|
126 |
+
def stop(self):
|
127 |
+
"""Stop the callback server."""
|
128 |
+
if self.server:
|
129 |
+
self.server.shutdown()
|
130 |
+
self.server.server_close()
|
131 |
+
if self.thread:
|
132 |
+
self.thread.join(timeout=1)
|
133 |
+
|
134 |
+
def wait_for_callback(self, timeout=300):
|
135 |
+
"""Wait for OAuth callback with timeout."""
|
136 |
+
start_time = time.time()
|
137 |
+
while time.time() - start_time < timeout:
|
138 |
+
if self.callback_data["authorization_code"]:
|
139 |
+
return self.callback_data["authorization_code"]
|
140 |
+
elif self.callback_data["error"]:
|
141 |
+
raise Exception(f"OAuth error: {self.callback_data['error']}")
|
142 |
+
time.sleep(0.1)
|
143 |
+
raise Exception("Timeout waiting for OAuth callback")
|
144 |
+
|
145 |
+
def get_state(self):
|
146 |
+
"""Get the received state parameter."""
|
147 |
+
return self.callback_data["state"]
|
148 |
+
|
149 |
+
|
150 |
+
class SimpleAuthClient:
|
151 |
+
"""Simple MCP client with auth support."""
|
152 |
+
|
153 |
+
def __init__(self, server_url: str, transport_type: str = "streamable_http"):
|
154 |
+
self.server_url = server_url
|
155 |
+
self.transport_type = transport_type
|
156 |
+
self.session: ClientSession | None = None
|
157 |
+
|
158 |
+
async def connect(self):
|
159 |
+
"""Connect to the MCP server."""
|
160 |
+
print(f"π Attempting to connect to {self.server_url}...")
|
161 |
+
|
162 |
+
try:
|
163 |
+
callback_server = CallbackServer(port=3030)
|
164 |
+
callback_server.start()
|
165 |
+
|
166 |
+
async def callback_handler() -> tuple[str, str | None]:
|
167 |
+
"""Wait for OAuth callback and return auth code and state."""
|
168 |
+
print("β³ Waiting for authorization callback...")
|
169 |
+
try:
|
170 |
+
auth_code = callback_server.wait_for_callback(timeout=300)
|
171 |
+
return auth_code, callback_server.get_state()
|
172 |
+
finally:
|
173 |
+
callback_server.stop()
|
174 |
+
|
175 |
+
client_metadata_dict = {
|
176 |
+
"client_name": "Simple Auth Client",
|
177 |
+
"redirect_uris": ["http://localhost:3030/callback"],
|
178 |
+
"grant_types": ["authorization_code", "refresh_token"],
|
179 |
+
"response_types": ["code"],
|
180 |
+
"token_endpoint_auth_method": "client_secret_post",
|
181 |
+
}
|
182 |
+
|
183 |
+
async def _default_redirect_handler(authorization_url: str) -> None:
|
184 |
+
"""Default redirect handler that opens the URL in a browser."""
|
185 |
+
print(f"Opening browser for authorization: {authorization_url}")
|
186 |
+
webbrowser.open(authorization_url)
|
187 |
+
|
188 |
+
# Create OAuth authentication handler using the new interface
|
189 |
+
oauth_auth = OAuthClientProvider(
|
190 |
+
server_url=self.server_url.replace("/mcp", ""),
|
191 |
+
client_metadata=OAuthClientMetadata.model_validate(
|
192 |
+
client_metadata_dict
|
193 |
+
),
|
194 |
+
storage=InMemoryTokenStorage(),
|
195 |
+
redirect_handler=_default_redirect_handler,
|
196 |
+
callback_handler=callback_handler,
|
197 |
+
)
|
198 |
+
|
199 |
+
# Create transport with auth handler based on transport type
|
200 |
+
if self.transport_type == "sse":
|
201 |
+
print("π‘ Opening SSE transport connection with auth...")
|
202 |
+
async with sse_client(
|
203 |
+
url=self.server_url,
|
204 |
+
auth=oauth_auth,
|
205 |
+
timeout=60,
|
206 |
+
) as (read_stream, write_stream):
|
207 |
+
await self._run_session(read_stream, write_stream, None)
|
208 |
+
else:
|
209 |
+
print("π‘ Opening StreamableHTTP transport connection with auth...")
|
210 |
+
async with streamablehttp_client(
|
211 |
+
url=self.server_url,
|
212 |
+
auth=oauth_auth,
|
213 |
+
timeout=timedelta(seconds=60),
|
214 |
+
) as (read_stream, write_stream, get_session_id):
|
215 |
+
await self._run_session(read_stream, write_stream, get_session_id)
|
216 |
+
|
217 |
+
except Exception as e:
|
218 |
+
print(f"β Failed to connect: {e}")
|
219 |
+
import traceback
|
220 |
+
|
221 |
+
traceback.print_exc()
|
222 |
+
|
223 |
+
async def _run_session(self, read_stream, write_stream, get_session_id):
|
224 |
+
"""Run the MCP session with the given streams."""
|
225 |
+
print("π€ Initializing MCP session...")
|
226 |
+
async with ClientSession(read_stream, write_stream) as session:
|
227 |
+
self.session = session
|
228 |
+
print("β‘ Starting session initialization...")
|
229 |
+
await session.initialize()
|
230 |
+
print("β¨ Session initialization complete!")
|
231 |
+
|
232 |
+
print(f"\nβ
Connected to MCP server at {self.server_url}")
|
233 |
+
if get_session_id:
|
234 |
+
session_id = get_session_id()
|
235 |
+
if session_id:
|
236 |
+
print(f"Session ID: {session_id}")
|
237 |
+
|
238 |
+
# Run interactive loop
|
239 |
+
await self.interactive_loop()
|
240 |
+
|
241 |
+
async def list_tools(self):
|
242 |
+
"""List available tools from the server."""
|
243 |
+
if not self.session:
|
244 |
+
print("β Not connected to server")
|
245 |
+
return
|
246 |
+
|
247 |
+
try:
|
248 |
+
result = await self.session.list_tools()
|
249 |
+
if hasattr(result, "tools") and result.tools:
|
250 |
+
print("\nπ Available tools:")
|
251 |
+
for i, tool in enumerate(result.tools, 1):
|
252 |
+
print(f"{i}. {tool.name}")
|
253 |
+
if tool.description:
|
254 |
+
print(f" Description: {tool.description}")
|
255 |
+
print()
|
256 |
+
else:
|
257 |
+
print("No tools available")
|
258 |
+
except Exception as e:
|
259 |
+
print(f"β Failed to list tools: {e}")
|
260 |
+
|
261 |
+
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None):
|
262 |
+
"""Call a specific tool."""
|
263 |
+
if not self.session:
|
264 |
+
print("β Not connected to server")
|
265 |
+
return
|
266 |
+
|
267 |
+
try:
|
268 |
+
result = await self.session.call_tool(tool_name, arguments or {})
|
269 |
+
print(f"\nπ§ Tool '{tool_name}' result:")
|
270 |
+
if hasattr(result, "content"):
|
271 |
+
for content in result.content:
|
272 |
+
if content.type == "text":
|
273 |
+
print(content.text)
|
274 |
+
else:
|
275 |
+
print(content)
|
276 |
+
else:
|
277 |
+
print(result)
|
278 |
+
except Exception as e:
|
279 |
+
print(f"β Failed to call tool '{tool_name}': {e}")
|
280 |
+
|
281 |
+
async def interactive_loop(self):
|
282 |
+
"""Run interactive command loop."""
|
283 |
+
print("\nπ― Interactive MCP Client")
|
284 |
+
print("Commands:")
|
285 |
+
print(" list - List available tools")
|
286 |
+
print(" call <tool_name> [args] - Call a tool")
|
287 |
+
print(" quit - Exit the client")
|
288 |
+
print()
|
289 |
+
|
290 |
+
while True:
|
291 |
+
try:
|
292 |
+
command = input("mcp> ").strip()
|
293 |
+
|
294 |
+
if not command:
|
295 |
+
continue
|
296 |
+
|
297 |
+
if command == "quit":
|
298 |
+
break
|
299 |
+
|
300 |
+
elif command == "list":
|
301 |
+
await self.list_tools()
|
302 |
+
|
303 |
+
elif command.startswith("call "):
|
304 |
+
parts = command.split(maxsplit=2)
|
305 |
+
tool_name = parts[1] if len(parts) > 1 else ""
|
306 |
+
|
307 |
+
if not tool_name:
|
308 |
+
print("β Please specify a tool name")
|
309 |
+
continue
|
310 |
+
|
311 |
+
# Parse arguments (simple JSON-like format)
|
312 |
+
arguments = {}
|
313 |
+
if len(parts) > 2:
|
314 |
+
import json
|
315 |
+
|
316 |
+
try:
|
317 |
+
arguments = json.loads(parts[2])
|
318 |
+
except json.JSONDecodeError:
|
319 |
+
print("β Invalid arguments format (expected JSON)")
|
320 |
+
continue
|
321 |
+
|
322 |
+
await self.call_tool(tool_name, arguments)
|
323 |
+
|
324 |
+
else:
|
325 |
+
print(
|
326 |
+
"β Unknown command. Try 'list', 'call <tool_name>', or 'quit'"
|
327 |
+
)
|
328 |
+
|
329 |
+
except KeyboardInterrupt:
|
330 |
+
print("\n\nπ Goodbye!")
|
331 |
+
break
|
332 |
+
except EOFError:
|
333 |
+
break
|
334 |
+
|
335 |
+
|
336 |
+
async def main():
|
337 |
+
"""Main entry point."""
|
338 |
+
# Default server URL - can be overridden with environment variable
|
339 |
+
# Most MCP streamable HTTP servers use /mcp as the endpoint
|
340 |
+
host_server = os.getenv("MCP_SERVER_HOST", "applemuncy-rs.hf.space")
|
341 |
+
server_url = os.getenv("MCP_SERVER_PORT", 443)
|
342 |
+
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable_http")
|
343 |
+
server_url = (
|
344 |
+
f"https://{host_server}/mcp" # :{server_url}/mcp"
|
345 |
+
if transport_type == "streamable_http"
|
346 |
+
else f"http://localhost:{server_url}/sse"
|
347 |
+
)
|
348 |
+
|
349 |
+
print("π Simple MCP Auth Client")
|
350 |
+
print(f"Connecting to: {host_server}")
|
351 |
+
print(f"Connecting to: {server_url}")
|
352 |
+
print(f"Transport type: {transport_type}")
|
353 |
+
|
354 |
+
# Start connection flow - OAuth will be handled automatically
|
355 |
+
client = SimpleAuthClient(server_url, transport_type)
|
356 |
+
await client.connect()
|
357 |
+
|
358 |
+
|
359 |
+
def cli():
|
360 |
+
"""CLI entry point for uv script."""
|
361 |
+
asyncio.run(main())
|
362 |
+
|
363 |
+
|
364 |
+
if __name__ == "__main__":
|
365 |
+
cli()
|
requirements.txt
CHANGED
@@ -1 +1 @@
|
|
1 |
-
mcp==1.12.
|
|
|
1 |
+
mcp==1.12.4
|