apple muncy commited on
Commit
8a76396
Β·
1 Parent(s): 4f3d774

add main.py and update to mcp 1.12.4

Browse files

Signed-off-by: apple muncy <[email protected]>

Files changed (2) hide show
  1. main.py +365 -0
  2. 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.3
 
1
+ mcp==1.12.4