optexity-browser-use 0.9.5__py3-none-any.whl

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.
Files changed (147) hide show
  1. browser_use/__init__.py +157 -0
  2. browser_use/actor/__init__.py +11 -0
  3. browser_use/actor/element.py +1175 -0
  4. browser_use/actor/mouse.py +134 -0
  5. browser_use/actor/page.py +561 -0
  6. browser_use/actor/playground/flights.py +41 -0
  7. browser_use/actor/playground/mixed_automation.py +54 -0
  8. browser_use/actor/playground/playground.py +236 -0
  9. browser_use/actor/utils.py +176 -0
  10. browser_use/agent/cloud_events.py +282 -0
  11. browser_use/agent/gif.py +424 -0
  12. browser_use/agent/judge.py +170 -0
  13. browser_use/agent/message_manager/service.py +473 -0
  14. browser_use/agent/message_manager/utils.py +52 -0
  15. browser_use/agent/message_manager/views.py +98 -0
  16. browser_use/agent/prompts.py +413 -0
  17. browser_use/agent/service.py +2316 -0
  18. browser_use/agent/system_prompt.md +185 -0
  19. browser_use/agent/system_prompt_flash.md +10 -0
  20. browser_use/agent/system_prompt_no_thinking.md +183 -0
  21. browser_use/agent/views.py +743 -0
  22. browser_use/browser/__init__.py +41 -0
  23. browser_use/browser/cloud/cloud.py +203 -0
  24. browser_use/browser/cloud/views.py +89 -0
  25. browser_use/browser/events.py +578 -0
  26. browser_use/browser/profile.py +1158 -0
  27. browser_use/browser/python_highlights.py +548 -0
  28. browser_use/browser/session.py +3225 -0
  29. browser_use/browser/session_manager.py +399 -0
  30. browser_use/browser/video_recorder.py +162 -0
  31. browser_use/browser/views.py +200 -0
  32. browser_use/browser/watchdog_base.py +260 -0
  33. browser_use/browser/watchdogs/__init__.py +0 -0
  34. browser_use/browser/watchdogs/aboutblank_watchdog.py +253 -0
  35. browser_use/browser/watchdogs/crash_watchdog.py +335 -0
  36. browser_use/browser/watchdogs/default_action_watchdog.py +2729 -0
  37. browser_use/browser/watchdogs/dom_watchdog.py +817 -0
  38. browser_use/browser/watchdogs/downloads_watchdog.py +1277 -0
  39. browser_use/browser/watchdogs/local_browser_watchdog.py +461 -0
  40. browser_use/browser/watchdogs/permissions_watchdog.py +43 -0
  41. browser_use/browser/watchdogs/popups_watchdog.py +143 -0
  42. browser_use/browser/watchdogs/recording_watchdog.py +126 -0
  43. browser_use/browser/watchdogs/screenshot_watchdog.py +62 -0
  44. browser_use/browser/watchdogs/security_watchdog.py +280 -0
  45. browser_use/browser/watchdogs/storage_state_watchdog.py +335 -0
  46. browser_use/cli.py +2359 -0
  47. browser_use/code_use/__init__.py +16 -0
  48. browser_use/code_use/formatting.py +192 -0
  49. browser_use/code_use/namespace.py +665 -0
  50. browser_use/code_use/notebook_export.py +276 -0
  51. browser_use/code_use/service.py +1340 -0
  52. browser_use/code_use/system_prompt.md +574 -0
  53. browser_use/code_use/utils.py +150 -0
  54. browser_use/code_use/views.py +171 -0
  55. browser_use/config.py +505 -0
  56. browser_use/controller/__init__.py +3 -0
  57. browser_use/dom/enhanced_snapshot.py +161 -0
  58. browser_use/dom/markdown_extractor.py +169 -0
  59. browser_use/dom/playground/extraction.py +312 -0
  60. browser_use/dom/playground/multi_act.py +32 -0
  61. browser_use/dom/serializer/clickable_elements.py +200 -0
  62. browser_use/dom/serializer/code_use_serializer.py +287 -0
  63. browser_use/dom/serializer/eval_serializer.py +478 -0
  64. browser_use/dom/serializer/html_serializer.py +212 -0
  65. browser_use/dom/serializer/paint_order.py +197 -0
  66. browser_use/dom/serializer/serializer.py +1170 -0
  67. browser_use/dom/service.py +825 -0
  68. browser_use/dom/utils.py +129 -0
  69. browser_use/dom/views.py +906 -0
  70. browser_use/exceptions.py +5 -0
  71. browser_use/filesystem/__init__.py +0 -0
  72. browser_use/filesystem/file_system.py +619 -0
  73. browser_use/init_cmd.py +376 -0
  74. browser_use/integrations/gmail/__init__.py +24 -0
  75. browser_use/integrations/gmail/actions.py +115 -0
  76. browser_use/integrations/gmail/service.py +225 -0
  77. browser_use/llm/__init__.py +155 -0
  78. browser_use/llm/anthropic/chat.py +242 -0
  79. browser_use/llm/anthropic/serializer.py +312 -0
  80. browser_use/llm/aws/__init__.py +36 -0
  81. browser_use/llm/aws/chat_anthropic.py +242 -0
  82. browser_use/llm/aws/chat_bedrock.py +289 -0
  83. browser_use/llm/aws/serializer.py +257 -0
  84. browser_use/llm/azure/chat.py +91 -0
  85. browser_use/llm/base.py +57 -0
  86. browser_use/llm/browser_use/__init__.py +3 -0
  87. browser_use/llm/browser_use/chat.py +201 -0
  88. browser_use/llm/cerebras/chat.py +193 -0
  89. browser_use/llm/cerebras/serializer.py +109 -0
  90. browser_use/llm/deepseek/chat.py +212 -0
  91. browser_use/llm/deepseek/serializer.py +109 -0
  92. browser_use/llm/exceptions.py +29 -0
  93. browser_use/llm/google/__init__.py +3 -0
  94. browser_use/llm/google/chat.py +542 -0
  95. browser_use/llm/google/serializer.py +120 -0
  96. browser_use/llm/groq/chat.py +229 -0
  97. browser_use/llm/groq/parser.py +158 -0
  98. browser_use/llm/groq/serializer.py +159 -0
  99. browser_use/llm/messages.py +238 -0
  100. browser_use/llm/models.py +271 -0
  101. browser_use/llm/oci_raw/__init__.py +10 -0
  102. browser_use/llm/oci_raw/chat.py +443 -0
  103. browser_use/llm/oci_raw/serializer.py +229 -0
  104. browser_use/llm/ollama/chat.py +97 -0
  105. browser_use/llm/ollama/serializer.py +143 -0
  106. browser_use/llm/openai/chat.py +264 -0
  107. browser_use/llm/openai/like.py +15 -0
  108. browser_use/llm/openai/serializer.py +165 -0
  109. browser_use/llm/openrouter/chat.py +211 -0
  110. browser_use/llm/openrouter/serializer.py +26 -0
  111. browser_use/llm/schema.py +176 -0
  112. browser_use/llm/views.py +48 -0
  113. browser_use/logging_config.py +330 -0
  114. browser_use/mcp/__init__.py +18 -0
  115. browser_use/mcp/__main__.py +12 -0
  116. browser_use/mcp/client.py +544 -0
  117. browser_use/mcp/controller.py +264 -0
  118. browser_use/mcp/server.py +1114 -0
  119. browser_use/observability.py +204 -0
  120. browser_use/py.typed +0 -0
  121. browser_use/sandbox/__init__.py +41 -0
  122. browser_use/sandbox/sandbox.py +637 -0
  123. browser_use/sandbox/views.py +132 -0
  124. browser_use/screenshots/__init__.py +1 -0
  125. browser_use/screenshots/service.py +52 -0
  126. browser_use/sync/__init__.py +6 -0
  127. browser_use/sync/auth.py +357 -0
  128. browser_use/sync/service.py +161 -0
  129. browser_use/telemetry/__init__.py +51 -0
  130. browser_use/telemetry/service.py +112 -0
  131. browser_use/telemetry/views.py +101 -0
  132. browser_use/tokens/__init__.py +0 -0
  133. browser_use/tokens/custom_pricing.py +24 -0
  134. browser_use/tokens/mappings.py +4 -0
  135. browser_use/tokens/service.py +580 -0
  136. browser_use/tokens/views.py +108 -0
  137. browser_use/tools/registry/service.py +572 -0
  138. browser_use/tools/registry/views.py +174 -0
  139. browser_use/tools/service.py +1675 -0
  140. browser_use/tools/utils.py +82 -0
  141. browser_use/tools/views.py +100 -0
  142. browser_use/utils.py +670 -0
  143. optexity_browser_use-0.9.5.dist-info/METADATA +344 -0
  144. optexity_browser_use-0.9.5.dist-info/RECORD +147 -0
  145. optexity_browser_use-0.9.5.dist-info/WHEEL +4 -0
  146. optexity_browser_use-0.9.5.dist-info/entry_points.txt +3 -0
  147. optexity_browser_use-0.9.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,544 @@
1
+ """MCP (Model Context Protocol) client integration for browser-use.
2
+
3
+ This module provides integration between external MCP servers and browser-use's action registry.
4
+ MCP tools are dynamically discovered and registered as browser-use actions.
5
+
6
+ Example usage:
7
+ from browser_use import Tools
8
+ from browser_use.mcp.client import MCPClient
9
+
10
+ tools = Tools()
11
+
12
+ # Connect to an MCP server
13
+ mcp_client = MCPClient(
14
+ server_name="my-server",
15
+ command="npx",
16
+ args=["@mycompany/mcp-server@latest"]
17
+ )
18
+
19
+ # Register all MCP tools as browser-use actions
20
+ await mcp_client.register_to_tools(tools)
21
+
22
+ # Now use with Agent as normal - MCP tools are available as actions
23
+ """
24
+
25
+ import asyncio
26
+ import logging
27
+ import time
28
+ from typing import Any
29
+
30
+ from pydantic import BaseModel, ConfigDict, Field, create_model
31
+
32
+ from browser_use.agent.views import ActionResult
33
+ from browser_use.telemetry import MCPClientTelemetryEvent, ProductTelemetry
34
+ from browser_use.tools.registry.service import Registry
35
+ from browser_use.tools.service import Tools
36
+ from browser_use.utils import get_browser_use_version
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ # Import MCP SDK
41
+ from mcp import ClientSession, StdioServerParameters, types
42
+ from mcp.client.stdio import stdio_client
43
+
44
+ MCP_AVAILABLE = True
45
+
46
+
47
+ class MCPClient:
48
+ """Client for connecting to MCP servers and exposing their tools as browser-use actions."""
49
+
50
+ def __init__(
51
+ self,
52
+ server_name: str,
53
+ command: str,
54
+ args: list[str] | None = None,
55
+ env: dict[str, str] | None = None,
56
+ ):
57
+ """Initialize MCP client.
58
+
59
+ Args:
60
+ server_name: Name of the MCP server (for logging and identification)
61
+ command: Command to start the MCP server (e.g., "npx", "python")
62
+ args: Arguments for the command (e.g., ["@playwright/mcp@latest"])
63
+ env: Environment variables for the server process
64
+ """
65
+ self.server_name = server_name
66
+ self.command = command
67
+ self.args = args or []
68
+ self.env = env
69
+
70
+ self.session: ClientSession | None = None
71
+ self._stdio_task = None
72
+ self._read_stream = None
73
+ self._write_stream = None
74
+ self._tools: dict[str, types.Tool] = {}
75
+ self._registered_actions: set[str] = set()
76
+ self._connected = False
77
+ self._disconnect_event = asyncio.Event()
78
+ self._telemetry = ProductTelemetry()
79
+
80
+ async def connect(self) -> None:
81
+ """Connect to the MCP server and discover available tools."""
82
+ if self._connected:
83
+ logger.debug(f'Already connected to {self.server_name}')
84
+ return
85
+
86
+ start_time = time.time()
87
+ error_msg = None
88
+
89
+ try:
90
+ logger.info(f"🔌 Connecting to MCP server '{self.server_name}': {self.command} {' '.join(self.args)}")
91
+
92
+ # Create server parameters
93
+ server_params = StdioServerParameters(command=self.command, args=self.args, env=self.env)
94
+
95
+ # Start stdio client in background task
96
+ self._stdio_task = asyncio.create_task(self._run_stdio_client(server_params))
97
+
98
+ # Wait for connection to be established
99
+ retries = 0
100
+ max_retries = 100 # 10 second timeout (increased for parallel test execution)
101
+ while not self._connected and retries < max_retries:
102
+ await asyncio.sleep(0.1)
103
+ retries += 1
104
+
105
+ if not self._connected:
106
+ error_msg = f"Failed to connect to MCP server '{self.server_name}' after {max_retries * 0.1} seconds"
107
+ raise RuntimeError(error_msg)
108
+
109
+ logger.info(f"📦 Discovered {len(self._tools)} tools from '{self.server_name}': {list(self._tools.keys())}")
110
+
111
+ except Exception as e:
112
+ error_msg = str(e)
113
+ raise
114
+ finally:
115
+ # Capture telemetry for connect action
116
+ duration = time.time() - start_time
117
+ self._telemetry.capture(
118
+ MCPClientTelemetryEvent(
119
+ server_name=self.server_name,
120
+ command=self.command,
121
+ tools_discovered=len(self._tools),
122
+ version=get_browser_use_version(),
123
+ action='connect',
124
+ duration_seconds=duration,
125
+ error_message=error_msg,
126
+ )
127
+ )
128
+
129
+ async def _run_stdio_client(self, server_params: StdioServerParameters):
130
+ """Run the stdio client connection in a background task."""
131
+ try:
132
+ async with stdio_client(server_params) as (read_stream, write_stream):
133
+ self._read_stream = read_stream
134
+ self._write_stream = write_stream
135
+
136
+ # Create and initialize session
137
+ async with ClientSession(read_stream, write_stream) as session:
138
+ self.session = session
139
+
140
+ # Initialize the connection
141
+ await session.initialize()
142
+
143
+ # Discover available tools
144
+ tools_response = await session.list_tools()
145
+ self._tools = {tool.name: tool for tool in tools_response.tools}
146
+
147
+ # Mark as connected
148
+ self._connected = True
149
+
150
+ # Keep the connection alive until disconnect is called
151
+ await self._disconnect_event.wait()
152
+
153
+ except Exception as e:
154
+ logger.error(f'MCP server connection error: {e}')
155
+ self._connected = False
156
+ raise
157
+ finally:
158
+ self._connected = False
159
+ self.session = None
160
+
161
+ async def disconnect(self) -> None:
162
+ """Disconnect from the MCP server."""
163
+ if not self._connected:
164
+ return
165
+
166
+ start_time = time.time()
167
+ error_msg = None
168
+
169
+ try:
170
+ logger.info(f"🔌 Disconnecting from MCP server '{self.server_name}'")
171
+
172
+ # Signal disconnect
173
+ self._connected = False
174
+ self._disconnect_event.set()
175
+
176
+ # Wait for stdio task to finish
177
+ if self._stdio_task:
178
+ try:
179
+ await asyncio.wait_for(self._stdio_task, timeout=2.0)
180
+ except TimeoutError:
181
+ logger.warning(f"Timeout waiting for MCP server '{self.server_name}' to disconnect")
182
+ self._stdio_task.cancel()
183
+ try:
184
+ await self._stdio_task
185
+ except asyncio.CancelledError:
186
+ pass
187
+
188
+ self._tools.clear()
189
+ self._registered_actions.clear()
190
+
191
+ except Exception as e:
192
+ error_msg = str(e)
193
+ logger.error(f'Error disconnecting from MCP server: {e}')
194
+ finally:
195
+ # Capture telemetry for disconnect action
196
+ duration = time.time() - start_time
197
+ self._telemetry.capture(
198
+ MCPClientTelemetryEvent(
199
+ server_name=self.server_name,
200
+ command=self.command,
201
+ tools_discovered=0, # Tools cleared on disconnect
202
+ version=get_browser_use_version(),
203
+ action='disconnect',
204
+ duration_seconds=duration,
205
+ error_message=error_msg,
206
+ )
207
+ )
208
+ self._telemetry.flush()
209
+
210
+ async def register_to_tools(
211
+ self,
212
+ tools: Tools,
213
+ tool_filter: list[str] | None = None,
214
+ prefix: str | None = None,
215
+ ) -> None:
216
+ """Register MCP tools as actions in the browser-use tools.
217
+
218
+ Args:
219
+ tools: Browser-use tools to register actions to
220
+ tool_filter: Optional list of tool names to register (None = all tools)
221
+ prefix: Optional prefix to add to action names (e.g., "playwright_")
222
+ """
223
+ if not self._connected:
224
+ await self.connect()
225
+
226
+ registry = tools.registry
227
+
228
+ for tool_name, tool in self._tools.items():
229
+ # Skip if not in filter
230
+ if tool_filter and tool_name not in tool_filter:
231
+ continue
232
+
233
+ # Apply prefix if specified
234
+ action_name = f'{prefix}{tool_name}' if prefix else tool_name
235
+
236
+ # Skip if already registered
237
+ if action_name in self._registered_actions:
238
+ continue
239
+
240
+ # Register the tool as an action
241
+ self._register_tool_as_action(registry, action_name, tool)
242
+ self._registered_actions.add(action_name)
243
+
244
+ logger.info(f"✅ Registered {len(self._registered_actions)} MCP tools from '{self.server_name}' as browser-use actions")
245
+
246
+ def _register_tool_as_action(self, registry: Registry, action_name: str, tool: Any) -> None:
247
+ """Register a single MCP tool as a browser-use action.
248
+
249
+ Args:
250
+ registry: Browser-use registry to register action to
251
+ action_name: Name for the registered action
252
+ tool: MCP Tool object with schema information
253
+ """
254
+ # Parse tool parameters to create Pydantic model
255
+ param_fields = {}
256
+
257
+ if tool.inputSchema:
258
+ # MCP tools use JSON Schema for parameters
259
+ properties = tool.inputSchema.get('properties', {})
260
+ required = set(tool.inputSchema.get('required', []))
261
+
262
+ for param_name, param_schema in properties.items():
263
+ # Convert JSON Schema type to Python type
264
+ param_type = self._json_schema_to_python_type(param_schema, f'{action_name}_{param_name}')
265
+
266
+ # Determine if field is required and handle defaults
267
+ if param_name in required:
268
+ default = ... # Required field
269
+ else:
270
+ # Optional field - make type optional and handle default
271
+ param_type = param_type | None
272
+ if 'default' in param_schema:
273
+ default = param_schema['default']
274
+ else:
275
+ default = None
276
+
277
+ # Add field with description if available
278
+ field_kwargs = {}
279
+ if 'description' in param_schema:
280
+ field_kwargs['description'] = param_schema['description']
281
+
282
+ param_fields[param_name] = (param_type, Field(default, **field_kwargs))
283
+
284
+ # Create Pydantic model for the tool parameters
285
+ if param_fields:
286
+ # Create a BaseModel class with proper configuration
287
+ class ConfiguredBaseModel(BaseModel):
288
+ model_config = ConfigDict(extra='forbid', validate_by_name=True, validate_by_alias=True)
289
+
290
+ param_model = create_model(f'{action_name}_Params', __base__=ConfiguredBaseModel, **param_fields)
291
+ else:
292
+ # No parameters - create empty model
293
+ param_model = None
294
+
295
+ # Determine if this is a browser-specific tool
296
+ is_browser_tool = tool.name.startswith('browser_') or 'page' in tool.name.lower()
297
+
298
+ # Set up action filters
299
+ domains = None
300
+ # Note: page_filter has been removed since we no longer use Page objects
301
+ # Browser tools filtering would need to be done via domain filters instead
302
+
303
+ # Create async wrapper function for the MCP tool
304
+ # Need to define function with explicit parameters to satisfy registry validation
305
+ if param_model:
306
+ # Type 1: Function takes param model as first parameter
307
+ async def mcp_action_wrapper(params: param_model) -> ActionResult: # type: ignore[no-redef]
308
+ """Wrapper function that calls the MCP tool."""
309
+ if not self.session or not self._connected:
310
+ return ActionResult(error=f"MCP server '{self.server_name}' not connected", success=False)
311
+
312
+ # Convert pydantic model to dict for MCP call
313
+ tool_params = params.model_dump(exclude_none=True)
314
+
315
+ logger.debug(f"🔧 Calling MCP tool '{tool.name}' with params: {tool_params}")
316
+
317
+ start_time = time.time()
318
+ error_msg = None
319
+
320
+ try:
321
+ # Call the MCP tool
322
+ result = await self.session.call_tool(tool.name, tool_params)
323
+
324
+ # Convert MCP result to ActionResult
325
+ extracted_content = self._format_mcp_result(result)
326
+
327
+ return ActionResult(
328
+ extracted_content=extracted_content,
329
+ long_term_memory=f"Used MCP tool '{tool.name}' from {self.server_name}",
330
+ )
331
+
332
+ except Exception as e:
333
+ error_msg = f"MCP tool '{tool.name}' failed: {str(e)}"
334
+ logger.error(error_msg)
335
+ return ActionResult(error=error_msg, success=False)
336
+ finally:
337
+ # Capture telemetry for tool call
338
+ duration = time.time() - start_time
339
+ self._telemetry.capture(
340
+ MCPClientTelemetryEvent(
341
+ server_name=self.server_name,
342
+ command=self.command,
343
+ tools_discovered=len(self._tools),
344
+ version=get_browser_use_version(),
345
+ action='tool_call',
346
+ tool_name=tool.name,
347
+ duration_seconds=duration,
348
+ error_message=error_msg,
349
+ )
350
+ )
351
+ else:
352
+ # No parameters - empty function signature
353
+ async def mcp_action_wrapper() -> ActionResult: # type: ignore[no-redef]
354
+ """Wrapper function that calls the MCP tool."""
355
+ if not self.session or not self._connected:
356
+ return ActionResult(error=f"MCP server '{self.server_name}' not connected", success=False)
357
+
358
+ logger.debug(f"🔧 Calling MCP tool '{tool.name}' with no params")
359
+
360
+ start_time = time.time()
361
+ error_msg = None
362
+
363
+ try:
364
+ # Call the MCP tool with empty params
365
+ result = await self.session.call_tool(tool.name, {})
366
+
367
+ # Convert MCP result to ActionResult
368
+ extracted_content = self._format_mcp_result(result)
369
+
370
+ return ActionResult(
371
+ extracted_content=extracted_content,
372
+ long_term_memory=f"Used MCP tool '{tool.name}' from {self.server_name}",
373
+ )
374
+
375
+ except Exception as e:
376
+ error_msg = f"MCP tool '{tool.name}' failed: {str(e)}"
377
+ logger.error(error_msg)
378
+ return ActionResult(error=error_msg, success=False)
379
+ finally:
380
+ # Capture telemetry for tool call
381
+ duration = time.time() - start_time
382
+ self._telemetry.capture(
383
+ MCPClientTelemetryEvent(
384
+ server_name=self.server_name,
385
+ command=self.command,
386
+ tools_discovered=len(self._tools),
387
+ version=get_browser_use_version(),
388
+ action='tool_call',
389
+ tool_name=tool.name,
390
+ duration_seconds=duration,
391
+ error_message=error_msg,
392
+ )
393
+ )
394
+
395
+ # Set function metadata for better debugging
396
+ mcp_action_wrapper.__name__ = action_name
397
+ mcp_action_wrapper.__qualname__ = f'mcp.{self.server_name}.{action_name}'
398
+
399
+ # Register the action with browser-use
400
+ description = tool.description or f'MCP tool from {self.server_name}: {tool.name}'
401
+
402
+ # Use the registry's action decorator
403
+ registry.action(description=description, param_model=param_model, domains=domains)(mcp_action_wrapper)
404
+
405
+ logger.debug(f"✅ Registered MCP tool '{tool.name}' as action '{action_name}'")
406
+
407
+ def _format_mcp_result(self, result: Any) -> str:
408
+ """Format MCP tool result into a string for ActionResult.
409
+
410
+ Args:
411
+ result: Raw result from MCP tool call
412
+
413
+ Returns:
414
+ Formatted string representation of the result
415
+ """
416
+ # Handle different MCP result formats
417
+ if hasattr(result, 'content'):
418
+ # Structured content response
419
+ if isinstance(result.content, list):
420
+ # Multiple content items
421
+ parts = []
422
+ for item in result.content:
423
+ if hasattr(item, 'text'):
424
+ parts.append(item.text)
425
+ elif hasattr(item, 'type') and item.type == 'text':
426
+ parts.append(str(item))
427
+ else:
428
+ parts.append(str(item))
429
+ return '\n'.join(parts)
430
+ else:
431
+ return str(result.content)
432
+ elif isinstance(result, list):
433
+ # List of content items
434
+ parts = []
435
+ for item in result:
436
+ if hasattr(item, 'text'):
437
+ parts.append(item.text)
438
+ else:
439
+ parts.append(str(item))
440
+ return '\n'.join(parts)
441
+ else:
442
+ # Direct result or unknown format
443
+ return str(result)
444
+
445
+ def _json_schema_to_python_type(self, schema: dict, model_name: str = 'NestedModel') -> Any:
446
+ """Convert JSON Schema type to Python type.
447
+
448
+ Args:
449
+ schema: JSON Schema definition
450
+ model_name: Name for nested models
451
+
452
+ Returns:
453
+ Python type corresponding to the schema
454
+ """
455
+ json_type = schema.get('type', 'string')
456
+
457
+ # Basic type mapping
458
+ type_mapping = {
459
+ 'string': str,
460
+ 'number': float,
461
+ 'integer': int,
462
+ 'boolean': bool,
463
+ 'array': list,
464
+ 'null': type(None),
465
+ }
466
+
467
+ # Handle enums (they're still strings)
468
+ if 'enum' in schema:
469
+ return str
470
+
471
+ # Handle objects with nested properties
472
+ if json_type == 'object':
473
+ properties = schema.get('properties', {})
474
+ if properties:
475
+ # Create nested pydantic model for objects with properties
476
+ nested_fields = {}
477
+ required_fields = set(schema.get('required', []))
478
+
479
+ for prop_name, prop_schema in properties.items():
480
+ # Recursively process nested properties
481
+ prop_type = self._json_schema_to_python_type(prop_schema, f'{model_name}_{prop_name}')
482
+
483
+ # Determine if field is required and handle defaults
484
+ if prop_name in required_fields:
485
+ default = ... # Required field
486
+ else:
487
+ # Optional field - make type optional and handle default
488
+ prop_type = prop_type | None
489
+ if 'default' in prop_schema:
490
+ default = prop_schema['default']
491
+ else:
492
+ default = None
493
+
494
+ # Add field with description if available
495
+ field_kwargs = {}
496
+ if 'description' in prop_schema:
497
+ field_kwargs['description'] = prop_schema['description']
498
+
499
+ nested_fields[prop_name] = (prop_type, Field(default, **field_kwargs))
500
+
501
+ # Create a BaseModel class with proper configuration
502
+ class ConfiguredBaseModel(BaseModel):
503
+ model_config = ConfigDict(extra='forbid', validate_by_name=True, validate_by_alias=True)
504
+
505
+ try:
506
+ # Create and return nested pydantic model
507
+ return create_model(model_name, __base__=ConfiguredBaseModel, **nested_fields)
508
+ except Exception as e:
509
+ logger.error(f'Failed to create nested model {model_name}: {e}')
510
+ logger.debug(f'Fields: {nested_fields}')
511
+ # Fallback to basic dict if model creation fails
512
+ return dict
513
+ else:
514
+ # Object without properties - just return dict
515
+ return dict
516
+
517
+ # Handle arrays with specific item types
518
+ if json_type == 'array':
519
+ if 'items' in schema:
520
+ # Get the item type recursively
521
+ item_type = self._json_schema_to_python_type(schema['items'], f'{model_name}_item')
522
+ # Return properly typed list
523
+ return list[item_type]
524
+ else:
525
+ # Array without item type specification
526
+ return list
527
+
528
+ # Get base type for non-object types
529
+ base_type = type_mapping.get(json_type, str)
530
+
531
+ # Handle nullable/optional types
532
+ if schema.get('nullable', False) or json_type == 'null':
533
+ return base_type | None
534
+
535
+ return base_type
536
+
537
+ async def __aenter__(self):
538
+ """Async context manager entry."""
539
+ await self.connect()
540
+ return self
541
+
542
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
543
+ """Async context manager exit."""
544
+ await self.disconnect()