| 278 | raise |
| 279 | |
| 280 | def list_tools(self) -> List[Dict[str, Any]]: |
| 281 | async def fetch(): |
| 282 | if not self._client: |
| 283 | raise RuntimeError("MCP client not connected") |
| 284 | tools = await self._client.list_tools() |
| 285 | # Convert MCP tool objects to OpenAI function format (dicts) |
| 286 | return [ |
| 287 | { |
| 288 | "type": "function", |
| 289 | "function": { |
| 290 | "name": tool.name, |
| 291 | "description": tool.description or "", |
| 292 | "parameters": ( |
| 293 | tool.inputSchema if hasattr(tool, "inputSchema") else {} |
| 294 | ), |
| 295 | }, |
| 296 | } |
| 297 | for tool in tools |
| 298 | ] |
| 299 | |
| 300 | try: |
| 301 | # Use lock to ensure thread-safe access to the event loop |
| 302 | with self._loop_lock: |
| 303 | if self._loop is None: |
| 304 | raise RuntimeError("MCP client not connected") |
| 305 | tools = self._loop.run_until_complete(fetch()) |
| 306 | self._update_session_id_from_transport() |
| 307 | if not isinstance(tools, list): |
| 308 | raise RuntimeError("MCP did not return a list of tools.") |
| 309 | return tools |
| 310 | except Exception as e: |
| 311 | raise RuntimeError(f"Failed to fetch tools from MCP: {e}") |
| 312 | |
| 313 | def make_mcp_tool_function( |
| 314 | self, tool_name: str, param_names: list[str] |