gperdrizet commited on
Commit
7c27bf6
·
verified ·
1 Parent(s): 6d8aa95

Refactored client class.

Browse files
classes/__pycache__/client.cpython-310.pyc ADDED
Binary file (7.14 kB). View file
 
classes/client.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ '''Classes for handling MCP server connection and operations.'''
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any, Dict, List, Optional
6
+ from urllib.parse import urlparse
7
+ from dataclasses import dataclass
8
+
9
+ from mcp import ClientSession
10
+ from mcp.client.sse import sse_client
11
+
12
+
13
+ @dataclass
14
+ class ToolParameter:
15
+ '''Represents a parameter for a tool.
16
+
17
+ Attributes:
18
+ name: Parameter name
19
+ parameter_type: Parameter type (e.g., 'string', 'number')
20
+ description: Parameter description
21
+ required: Whether the parameter is required
22
+ default: Default value for the parameter
23
+ '''
24
+ name: str
25
+ parameter_type: str
26
+ description: str
27
+ required: bool = False
28
+ default: Any = None
29
+
30
+
31
+ @dataclass
32
+ class ToolDef:
33
+ '''Represents a tool definition.
34
+
35
+ Attributes:
36
+ name: Tool name
37
+ description: Tool description
38
+ parameters: List of ToolParameter objects
39
+ metadata: Optional dictionary of additional metadata
40
+ identifier: Tool identifier (defaults to name)
41
+ '''
42
+ name: str
43
+ description: str
44
+ parameters: List[ToolParameter]
45
+ metadata: Optional[Dict[str, Any]] = None
46
+ identifier: str = ''
47
+
48
+
49
+ class MCPConnectionError(Exception):
50
+ '''Exception raised when MCP connection fails'''
51
+ pass
52
+
53
+
54
+ class MCPTimeoutError(Exception):
55
+ '''Exception raised when MCP operation times out'''
56
+ pass
57
+
58
+
59
+ class MCPClientWrapper:
60
+ '''Main client wrapper class for interacting with Model Context Protocol (MCP) endpoints'''
61
+
62
+ def __init__(self, endpoint: str, timeout: float = 30.0, max_retries: int = 3):
63
+ '''Initialize MCP client with endpoint URL
64
+
65
+ Args:
66
+ endpoint: The MCP endpoint URL (must be http or https)
67
+ timeout: Connection timeout in seconds
68
+ max_retries: Maximum number of retry attempts
69
+ '''
70
+
71
+ if urlparse(endpoint).scheme not in ('http', 'https'):
72
+ raise ValueError(f'Endpoint {endpoint} is not a valid HTTP(S) URL')
73
+ self.endpoint = endpoint
74
+ self.timeout = timeout
75
+ self.max_retries = max_retries
76
+
77
+
78
+ async def _execute_with_retry(self, operation_name: str, operation_func):
79
+ '''Execute an operation with retry logic and proper error handling
80
+
81
+ Args:
82
+ operation_name: Name of the operation for logging
83
+ operation_func: Async function to execute
84
+
85
+ Returns:
86
+ Result of the operation
87
+
88
+ Raises:
89
+ MCPConnectionError: If connection fails after all retries
90
+ MCPTimeoutError: If operation times out
91
+ '''
92
+
93
+ logger = logging.getLogger(__name__ + '_execute_with_retry')
94
+
95
+ last_exception = None
96
+
97
+ for attempt in range(self.max_retries):
98
+ try:
99
+ logger.debug(
100
+ 'Attempting %s (attempt %s/%s)',
101
+ operation_name,
102
+ attempt + 1,
103
+ self.max_retries
104
+ )
105
+
106
+ # Execute with timeout
107
+ result = await asyncio.wait_for(operation_func(), timeout=self.timeout)
108
+ logger.debug('%s completed successfully', operation_name)
109
+ return result
110
+
111
+ except asyncio.TimeoutError as e:
112
+ last_exception = MCPTimeoutError(
113
+ f'{operation_name} timed out after {self.timeout} seconds'
114
+ )
115
+ logger.warning('%s timed out on attempt %s: %s', operation_name, attempt + 1, e)
116
+
117
+ except Exception as e: # pylint: disable=broad-exception-caught
118
+ last_exception = e
119
+ logger.warning('%s failed on attempt %s: %s', operation_name, attempt + 1, str(e))
120
+
121
+ # Don't retry on certain types of errors
122
+ if isinstance(e, (ValueError, TypeError)):
123
+ break
124
+
125
+ # Wait before retry (exponential backoff)
126
+ if attempt < self.max_retries - 1:
127
+ wait_time = 2 ** attempt
128
+ logger.debug('Waiting %s seconds before retry', wait_time)
129
+ await asyncio.sleep(wait_time)
130
+
131
+ # All retries failed
132
+ if isinstance(last_exception, MCPTimeoutError):
133
+ raise last_exception
134
+ else:
135
+ raise MCPConnectionError(
136
+ f'{operation_name} failed after {self.max_retries} attempts: {str(last_exception)}'
137
+ )
138
+
139
+ async def _safe_sse_operation(self, operation_func):
140
+ '''Safely execute an SSE operation with proper task cleanup
141
+
142
+ Args:
143
+ operation_func: Function that takes (streams, session) as arguments
144
+
145
+ Returns:
146
+ Result of the operation
147
+ '''
148
+
149
+ logger = logging.getLogger(__name__ + '_safe_sse_operation')
150
+
151
+ streams = None
152
+ session = None
153
+
154
+ try:
155
+ # Create SSE client with proper error handling
156
+ streams = sse_client(self.endpoint)
157
+
158
+ async with streams as stream_context:
159
+
160
+ # Create session with proper cleanup
161
+ session = ClientSession(*stream_context)
162
+
163
+ async with session as session_context:
164
+ await session_context.initialize()
165
+ return await operation_func(session_context)
166
+
167
+ except Exception as e:
168
+ logger.error('SSE operation failed: %s', str(e))
169
+
170
+ # Ensure proper cleanup of any remaining tasks
171
+ if session:
172
+ try:
173
+ # Cancel any pending tasks in the session
174
+ tasks = [task for task in asyncio.all_tasks() if not task.done()]
175
+ if tasks:
176
+ logger.debug('Cancelling %s pending tasks', len(tasks))
177
+ for task in tasks:
178
+ task.cancel()
179
+
180
+ # Wait for tasks to be cancelled
181
+ await asyncio.gather(*tasks, return_exceptions=True)
182
+
183
+ except Exception as cleanup_error: # pylint: disable=broad-exception-caught
184
+ logger.warning('Error during task cleanup: %s', cleanup_error)
185
+ raise
186
+
187
+ async def list_tools(self) -> List[ToolDef]:
188
+ '''List available tools from the MCP endpoint
189
+
190
+ Returns:
191
+ List of ToolDef objects describing available tools
192
+
193
+ Raises:
194
+ MCPConnectionError: If connection fails
195
+ MCPTimeoutError: If operation times out
196
+ '''
197
+ async def _list_tools_operation():
198
+ async def _operation(session):
199
+
200
+ tools_result = await session.list_tools()
201
+ tools = []
202
+
203
+ for tool in tools_result.tools:
204
+ parameters = []
205
+ required_params = tool.inputSchema.get('required', [])
206
+
207
+ for param_name, param_schema in tool.inputSchema.get('properties', {}).items():
208
+ parameters.append(
209
+ ToolParameter(
210
+ name=param_name,
211
+ parameter_type=param_schema.get('type', 'string'),
212
+ description=param_schema.get('description', ''),
213
+ required=param_name in required_params,
214
+ default=param_schema.get('default'),
215
+ )
216
+ )
217
+
218
+ tools.append(
219
+ ToolDef(
220
+ name=tool.name,
221
+ description=tool.description,
222
+ parameters=parameters,
223
+ metadata={'endpoint': self.endpoint},
224
+ identifier=tool.name # Using name as identifier
225
+ )
226
+ )
227
+ return tools
228
+
229
+ return await self._safe_sse_operation(_operation)
230
+
231
+ return await self._execute_with_retry('list_tools', _list_tools_operation)
rss_client.py CHANGED
@@ -1,17 +1,11 @@
1
  '''RSS MCP server demonstration client app.'''
2
 
3
- import asyncio
4
  import logging
5
  from pathlib import Path
6
  from logging.handlers import RotatingFileHandler
7
- from typing import Any, Dict, List, Optional
8
- from urllib.parse import urlparse
9
- from dataclasses import dataclass
10
 
11
  import gradio as gr
12
- from mcp import ClientSession
13
- from mcp.client.sse import sse_client
14
-
15
 
16
  # Make sure log directory exists
17
  Path('logs').mkdir(parents=True, exist_ok=True)
@@ -32,226 +26,12 @@ logging.basicConfig(
32
 
33
  logger = logging.getLogger(__name__)
34
 
35
-
36
- @dataclass
37
- class ToolParameter:
38
- '''Represents a parameter for a tool.
39
-
40
- Attributes:
41
- name: Parameter name
42
- parameter_type: Parameter type (e.g., 'string', 'number')
43
- description: Parameter description
44
- required: Whether the parameter is required
45
- default: Default value for the parameter
46
- '''
47
- name: str
48
- parameter_type: str
49
- description: str
50
- required: bool = False
51
- default: Any = None
52
-
53
-
54
- @dataclass
55
- class ToolDef:
56
- '''Represents a tool definition.
57
-
58
- Attributes:
59
- name: Tool name
60
- description: Tool description
61
- parameters: List of ToolParameter objects
62
- metadata: Optional dictionary of additional metadata
63
- identifier: Tool identifier (defaults to name)
64
- '''
65
- name: str
66
- description: str
67
- parameters: List[ToolParameter]
68
- metadata: Optional[Dict[str, Any]] = None
69
- identifier: str = ''
70
-
71
-
72
- class MCPConnectionError(Exception):
73
- '''Exception raised when MCP connection fails'''
74
- pass
75
-
76
-
77
- class MCPTimeoutError(Exception):
78
- '''Exception raised when MCP operation times out'''
79
- pass
80
-
81
-
82
- class MCPClientWrapper:
83
- '''Client for interacting with Model Context Protocol (MCP) endpoints'''
84
-
85
- def __init__(self, endpoint: str, timeout: float = 30.0, max_retries: int = 3):
86
- '''Initialize MCP client with endpoint URL
87
-
88
- Args:
89
- endpoint: The MCP endpoint URL (must be http or https)
90
- timeout: Connection timeout in seconds
91
- max_retries: Maximum number of retry attempts
92
- '''
93
- if urlparse(endpoint).scheme not in ('http', 'https'):
94
- raise ValueError(f'Endpoint {endpoint} is not a valid HTTP(S) URL')
95
- self.endpoint = endpoint
96
- self.timeout = timeout
97
- self.max_retries = max_retries
98
-
99
-
100
- async def _execute_with_retry(self, operation_name: str, operation_func):
101
- '''Execute an operation with retry logic and proper error handling
102
-
103
- Args:
104
- operation_name: Name of the operation for logging
105
- operation_func: Async function to execute
106
-
107
- Returns:
108
- Result of the operation
109
-
110
- Raises:
111
- MCPConnectionError: If connection fails after all retries
112
- MCPTimeoutError: If operation times out
113
- '''
114
- last_exception = None
115
-
116
- for attempt in range(self.max_retries):
117
- try:
118
- logger.debug(
119
- 'Attempting %s (attempt %s/%s)',
120
- operation_name,
121
- attempt + 1,
122
- self.max_retries
123
- )
124
-
125
- # Execute with timeout
126
- result = await asyncio.wait_for(operation_func(), timeout=self.timeout)
127
- logger.debug('%s completed successfully', operation_name)
128
- return result
129
-
130
- except asyncio.TimeoutError as e:
131
- last_exception = MCPTimeoutError(
132
- f'{operation_name} timed out after {self.timeout} seconds'
133
- )
134
- logger.warning('%s timed out on attempt %s: %s', operation_name, attempt + 1, e)
135
-
136
- except Exception as e: # pylint: disable=broad-exception-caught
137
- last_exception = e
138
- logger.warning('%s failed on attempt %s: %s', operation_name, attempt + 1, str(e))
139
-
140
- # Don't retry on certain types of errors
141
- if isinstance(e, (ValueError, TypeError)):
142
- break
143
-
144
- # Wait before retry (exponential backoff)
145
- if attempt < self.max_retries - 1:
146
- wait_time = 2 ** attempt
147
- logger.debug('Waiting %s seconds before retry', wait_time)
148
- await asyncio.sleep(wait_time)
149
-
150
- # All retries failed
151
- if isinstance(last_exception, MCPTimeoutError):
152
- raise last_exception
153
- else:
154
- raise MCPConnectionError(
155
- f'{operation_name} failed after {self.max_retries} attempts: {str(last_exception)}'
156
- )
157
-
158
- async def _safe_sse_operation(self, operation_func):
159
- '''Safely execute an SSE operation with proper task cleanup
160
-
161
- Args:
162
- operation_func: Function that takes (streams, session) as arguments
163
-
164
- Returns:
165
- Result of the operation
166
- '''
167
- streams = None
168
- session = None
169
-
170
- try:
171
- # Create SSE client with proper error handling
172
- streams = sse_client(self.endpoint)
173
-
174
- async with streams as stream_context:
175
-
176
- # Create session with proper cleanup
177
- session = ClientSession(*stream_context)
178
-
179
- async with session as session_context:
180
- await session_context.initialize()
181
- return await operation_func(session_context)
182
-
183
- except Exception as e:
184
- logger.error('SSE operation failed: %s', str(e))
185
-
186
- # Ensure proper cleanup of any remaining tasks
187
- if session:
188
- try:
189
- # Cancel any pending tasks in the session
190
- tasks = [task for task in asyncio.all_tasks() if not task.done()]
191
- if tasks:
192
- logger.debug('Cancelling %s pending tasks', len(tasks))
193
- for task in tasks:
194
- task.cancel()
195
-
196
- # Wait for tasks to be cancelled
197
- await asyncio.gather(*tasks, return_exceptions=True)
198
-
199
- except Exception as cleanup_error: # pylint: disable=broad-exception-caught
200
- logger.warning('Error during task cleanup: %s', cleanup_error)
201
- raise
202
-
203
- async def list_tools(self) -> List[ToolDef]:
204
- '''List available tools from the MCP endpoint
205
-
206
- Returns:
207
- List of ToolDef objects describing available tools
208
-
209
- Raises:
210
- MCPConnectionError: If connection fails
211
- MCPTimeoutError: If operation times out
212
- '''
213
- async def _list_tools_operation():
214
- async def _operation(session):
215
-
216
- tools_result = await session.list_tools()
217
- tools = []
218
-
219
- for tool in tools_result.tools:
220
- parameters = []
221
- required_params = tool.inputSchema.get('required', [])
222
-
223
- for param_name, param_schema in tool.inputSchema.get('properties', {}).items():
224
- parameters.append(
225
- ToolParameter(
226
- name=param_name,
227
- parameter_type=param_schema.get('type', 'string'),
228
- description=param_schema.get('description', ''),
229
- required=param_name in required_params,
230
- default=param_schema.get('default'),
231
- )
232
- )
233
-
234
- tools.append(
235
- ToolDef(
236
- name=tool.name,
237
- description=tool.description,
238
- parameters=parameters,
239
- metadata={'endpoint': self.endpoint},
240
- identifier=tool.name # Using name as identifier
241
- )
242
- )
243
- return tools
244
-
245
- return await self._safe_sse_operation(_operation)
246
-
247
- return await self._execute_with_retry('list_tools', _list_tools_operation)
248
-
249
  client = MCPClientWrapper('https://agents-mcp-hackathon-rss-mcp-server.hf.space/gradio_api/mcp/sse')
250
 
251
  # def gradio_interface():
252
  with gr.Blocks(title='MCP RSS client') as demo:
253
  gr.Markdown('# MCP RSS reader')
254
- gr.Markdown('Connect to the MCP RSS server')
255
 
256
  connect_btn = gr.Button('Connect')
257
  status = gr.Textbox(label='Connection Status', interactive=False, lines=50)
 
1
  '''RSS MCP server demonstration client app.'''
2
 
 
3
  import logging
4
  from pathlib import Path
5
  from logging.handlers import RotatingFileHandler
 
 
 
6
 
7
  import gradio as gr
8
+ from classes.client import MCPClientWrapper
 
 
9
 
10
  # Make sure log directory exists
11
  Path('logs').mkdir(parents=True, exist_ok=True)
 
26
 
27
  logger = logging.getLogger(__name__)
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  client = MCPClientWrapper('https://agents-mcp-hackathon-rss-mcp-server.hf.space/gradio_api/mcp/sse')
30
 
31
  # def gradio_interface():
32
  with gr.Blocks(title='MCP RSS client') as demo:
33
  gr.Markdown('# MCP RSS reader')
34
+ gr.Markdown('Connect to the MCP RSS server: https://huggingface.co/spaces/Agents-MCP-Hackathon/rss-mcp-server')
35
 
36
  connect_btn = gr.Button('Connect')
37
  status = gr.Textbox(label='Connection Status', interactive=False, lines=50)