massgen 0.0.3__py3-none-any.whl → 0.1.0__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.

Potentially problematic release.


This version of massgen might be problematic. Click here for more details.

Files changed (268) hide show
  1. massgen/__init__.py +142 -8
  2. massgen/adapters/__init__.py +29 -0
  3. massgen/adapters/ag2_adapter.py +483 -0
  4. massgen/adapters/base.py +183 -0
  5. massgen/adapters/tests/__init__.py +0 -0
  6. massgen/adapters/tests/test_ag2_adapter.py +439 -0
  7. massgen/adapters/tests/test_agent_adapter.py +128 -0
  8. massgen/adapters/utils/__init__.py +2 -0
  9. massgen/adapters/utils/ag2_utils.py +236 -0
  10. massgen/adapters/utils/tests/__init__.py +0 -0
  11. massgen/adapters/utils/tests/test_ag2_utils.py +138 -0
  12. massgen/agent_config.py +329 -55
  13. massgen/api_params_handler/__init__.py +10 -0
  14. massgen/api_params_handler/_api_params_handler_base.py +99 -0
  15. massgen/api_params_handler/_chat_completions_api_params_handler.py +176 -0
  16. massgen/api_params_handler/_claude_api_params_handler.py +113 -0
  17. massgen/api_params_handler/_response_api_params_handler.py +130 -0
  18. massgen/backend/__init__.py +39 -4
  19. massgen/backend/azure_openai.py +385 -0
  20. massgen/backend/base.py +341 -69
  21. massgen/backend/base_with_mcp.py +1102 -0
  22. massgen/backend/capabilities.py +386 -0
  23. massgen/backend/chat_completions.py +577 -130
  24. massgen/backend/claude.py +1033 -537
  25. massgen/backend/claude_code.py +1203 -0
  26. massgen/backend/cli_base.py +209 -0
  27. massgen/backend/docs/BACKEND_ARCHITECTURE.md +126 -0
  28. massgen/backend/{CLAUDE_API_RESEARCH.md → docs/CLAUDE_API_RESEARCH.md} +18 -18
  29. massgen/backend/{GEMINI_API_DOCUMENTATION.md → docs/GEMINI_API_DOCUMENTATION.md} +9 -9
  30. massgen/backend/docs/Gemini MCP Integration Analysis.md +1050 -0
  31. massgen/backend/docs/MCP_IMPLEMENTATION_CLAUDE_BACKEND.md +177 -0
  32. massgen/backend/docs/MCP_INTEGRATION_RESPONSE_BACKEND.md +352 -0
  33. massgen/backend/docs/OPENAI_GPT5_MODELS.md +211 -0
  34. massgen/backend/{OPENAI_RESPONSES_API_FORMAT.md → docs/OPENAI_RESPONSE_API_TOOL_CALLS.md} +3 -3
  35. massgen/backend/docs/OPENAI_response_streaming.md +20654 -0
  36. massgen/backend/docs/inference_backend.md +257 -0
  37. massgen/backend/docs/permissions_and_context_files.md +1085 -0
  38. massgen/backend/external.py +126 -0
  39. massgen/backend/gemini.py +1850 -241
  40. massgen/backend/grok.py +40 -156
  41. massgen/backend/inference.py +156 -0
  42. massgen/backend/lmstudio.py +171 -0
  43. massgen/backend/response.py +1095 -322
  44. massgen/chat_agent.py +131 -113
  45. massgen/cli.py +1560 -275
  46. massgen/config_builder.py +2396 -0
  47. massgen/configs/BACKEND_CONFIGURATION.md +458 -0
  48. massgen/configs/README.md +559 -216
  49. massgen/configs/ag2/ag2_case_study.yaml +27 -0
  50. massgen/configs/ag2/ag2_coder.yaml +34 -0
  51. massgen/configs/ag2/ag2_coder_case_study.yaml +36 -0
  52. massgen/configs/ag2/ag2_gemini.yaml +27 -0
  53. massgen/configs/ag2/ag2_groupchat.yaml +108 -0
  54. massgen/configs/ag2/ag2_groupchat_gpt.yaml +118 -0
  55. massgen/configs/ag2/ag2_single_agent.yaml +21 -0
  56. massgen/configs/basic/multi/fast_timeout_example.yaml +37 -0
  57. massgen/configs/basic/multi/gemini_4o_claude.yaml +31 -0
  58. massgen/configs/basic/multi/gemini_gpt5nano_claude.yaml +36 -0
  59. massgen/configs/{gemini_4o_claude.yaml → basic/multi/geminicode_4o_claude.yaml} +3 -3
  60. massgen/configs/basic/multi/geminicode_gpt5nano_claude.yaml +36 -0
  61. massgen/configs/basic/multi/glm_gemini_claude.yaml +25 -0
  62. massgen/configs/basic/multi/gpt4o_audio_generation.yaml +30 -0
  63. massgen/configs/basic/multi/gpt4o_image_generation.yaml +31 -0
  64. massgen/configs/basic/multi/gpt5nano_glm_qwen.yaml +26 -0
  65. massgen/configs/basic/multi/gpt5nano_image_understanding.yaml +26 -0
  66. massgen/configs/{three_agents_default.yaml → basic/multi/three_agents_default.yaml} +8 -4
  67. massgen/configs/basic/multi/three_agents_opensource.yaml +27 -0
  68. massgen/configs/basic/multi/three_agents_vllm.yaml +20 -0
  69. massgen/configs/basic/multi/two_agents_gemini.yaml +19 -0
  70. massgen/configs/{two_agents.yaml → basic/multi/two_agents_gpt5.yaml} +14 -6
  71. massgen/configs/basic/multi/two_agents_opensource_lmstudio.yaml +31 -0
  72. massgen/configs/basic/multi/two_qwen_vllm_sglang.yaml +28 -0
  73. massgen/configs/{single_agent.yaml → basic/single/single_agent.yaml} +1 -1
  74. massgen/configs/{single_flash2.5.yaml → basic/single/single_flash2.5.yaml} +1 -2
  75. massgen/configs/basic/single/single_gemini2.5pro.yaml +16 -0
  76. massgen/configs/basic/single/single_gpt4o_audio_generation.yaml +22 -0
  77. massgen/configs/basic/single/single_gpt4o_image_generation.yaml +22 -0
  78. massgen/configs/basic/single/single_gpt4o_video_generation.yaml +24 -0
  79. massgen/configs/basic/single/single_gpt5nano.yaml +20 -0
  80. massgen/configs/basic/single/single_gpt5nano_file_search.yaml +18 -0
  81. massgen/configs/basic/single/single_gpt5nano_image_understanding.yaml +17 -0
  82. massgen/configs/basic/single/single_gptoss120b.yaml +15 -0
  83. massgen/configs/basic/single/single_openrouter_audio_understanding.yaml +15 -0
  84. massgen/configs/basic/single/single_qwen_video_understanding.yaml +15 -0
  85. massgen/configs/debug/code_execution/command_filtering_blacklist.yaml +29 -0
  86. massgen/configs/debug/code_execution/command_filtering_whitelist.yaml +28 -0
  87. massgen/configs/debug/code_execution/docker_verification.yaml +29 -0
  88. massgen/configs/debug/skip_coordination_test.yaml +27 -0
  89. massgen/configs/debug/test_sdk_migration.yaml +17 -0
  90. massgen/configs/docs/DISCORD_MCP_SETUP.md +208 -0
  91. massgen/configs/docs/TWITTER_MCP_ENESCINAR_SETUP.md +82 -0
  92. massgen/configs/providers/azure/azure_openai_multi.yaml +21 -0
  93. massgen/configs/providers/azure/azure_openai_single.yaml +19 -0
  94. massgen/configs/providers/claude/claude.yaml +14 -0
  95. massgen/configs/providers/gemini/gemini_gpt5nano.yaml +28 -0
  96. massgen/configs/providers/local/lmstudio.yaml +11 -0
  97. massgen/configs/providers/openai/gpt5.yaml +46 -0
  98. massgen/configs/providers/openai/gpt5_nano.yaml +46 -0
  99. massgen/configs/providers/others/grok_single_agent.yaml +19 -0
  100. massgen/configs/providers/others/zai_coding_team.yaml +108 -0
  101. massgen/configs/providers/others/zai_glm45.yaml +12 -0
  102. massgen/configs/{creative_team.yaml → teams/creative/creative_team.yaml} +16 -6
  103. massgen/configs/{travel_planning.yaml → teams/creative/travel_planning.yaml} +16 -6
  104. massgen/configs/{news_analysis.yaml → teams/research/news_analysis.yaml} +16 -6
  105. massgen/configs/{research_team.yaml → teams/research/research_team.yaml} +15 -7
  106. massgen/configs/{technical_analysis.yaml → teams/research/technical_analysis.yaml} +16 -6
  107. massgen/configs/tools/code-execution/basic_command_execution.yaml +25 -0
  108. massgen/configs/tools/code-execution/code_execution_use_case_simple.yaml +41 -0
  109. massgen/configs/tools/code-execution/docker_claude_code.yaml +32 -0
  110. massgen/configs/tools/code-execution/docker_multi_agent.yaml +32 -0
  111. massgen/configs/tools/code-execution/docker_simple.yaml +29 -0
  112. massgen/configs/tools/code-execution/docker_with_resource_limits.yaml +32 -0
  113. massgen/configs/tools/code-execution/multi_agent_playwright_automation.yaml +57 -0
  114. massgen/configs/tools/filesystem/cc_gpt5_gemini_filesystem.yaml +34 -0
  115. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +68 -0
  116. massgen/configs/tools/filesystem/claude_code_flash2.5.yaml +43 -0
  117. massgen/configs/tools/filesystem/claude_code_flash2.5_gptoss.yaml +49 -0
  118. massgen/configs/tools/filesystem/claude_code_gpt5nano.yaml +31 -0
  119. massgen/configs/tools/filesystem/claude_code_single.yaml +40 -0
  120. massgen/configs/tools/filesystem/fs_permissions_test.yaml +87 -0
  121. massgen/configs/tools/filesystem/gemini_gemini_workspace_cleanup.yaml +54 -0
  122. massgen/configs/tools/filesystem/gemini_gpt5_filesystem_casestudy.yaml +30 -0
  123. massgen/configs/tools/filesystem/gemini_gpt5nano_file_context_path.yaml +43 -0
  124. massgen/configs/tools/filesystem/gemini_gpt5nano_protected_paths.yaml +45 -0
  125. massgen/configs/tools/filesystem/gpt5mini_cc_fs_context_path.yaml +31 -0
  126. massgen/configs/tools/filesystem/grok4_gpt5_gemini_filesystem.yaml +32 -0
  127. massgen/configs/tools/filesystem/multiturn/grok4_gpt5_claude_code_filesystem_multiturn.yaml +58 -0
  128. massgen/configs/tools/filesystem/multiturn/grok4_gpt5_gemini_filesystem_multiturn.yaml +58 -0
  129. massgen/configs/tools/filesystem/multiturn/two_claude_code_filesystem_multiturn.yaml +47 -0
  130. massgen/configs/tools/filesystem/multiturn/two_gemini_flash_filesystem_multiturn.yaml +48 -0
  131. massgen/configs/tools/mcp/claude_code_discord_mcp_example.yaml +27 -0
  132. massgen/configs/tools/mcp/claude_code_simple_mcp.yaml +35 -0
  133. massgen/configs/tools/mcp/claude_code_twitter_mcp_example.yaml +32 -0
  134. massgen/configs/tools/mcp/claude_mcp_example.yaml +24 -0
  135. massgen/configs/tools/mcp/claude_mcp_test.yaml +27 -0
  136. massgen/configs/tools/mcp/five_agents_travel_mcp_test.yaml +157 -0
  137. massgen/configs/tools/mcp/five_agents_weather_mcp_test.yaml +103 -0
  138. massgen/configs/tools/mcp/gemini_mcp_example.yaml +24 -0
  139. massgen/configs/tools/mcp/gemini_mcp_filesystem_test.yaml +23 -0
  140. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_sharing.yaml +23 -0
  141. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_single_agent.yaml +17 -0
  142. massgen/configs/tools/mcp/gemini_mcp_filesystem_test_with_claude_code.yaml +24 -0
  143. massgen/configs/tools/mcp/gemini_mcp_test.yaml +27 -0
  144. massgen/configs/tools/mcp/gemini_notion_mcp.yaml +52 -0
  145. massgen/configs/tools/mcp/gpt5_nano_mcp_example.yaml +24 -0
  146. massgen/configs/tools/mcp/gpt5_nano_mcp_test.yaml +27 -0
  147. massgen/configs/tools/mcp/gpt5mini_claude_code_discord_mcp_example.yaml +38 -0
  148. massgen/configs/tools/mcp/gpt_oss_mcp_example.yaml +25 -0
  149. massgen/configs/tools/mcp/gpt_oss_mcp_test.yaml +28 -0
  150. massgen/configs/tools/mcp/grok3_mini_mcp_example.yaml +24 -0
  151. massgen/configs/tools/mcp/grok3_mini_mcp_test.yaml +27 -0
  152. massgen/configs/tools/mcp/multimcp_gemini.yaml +111 -0
  153. massgen/configs/tools/mcp/qwen_api_mcp_example.yaml +25 -0
  154. massgen/configs/tools/mcp/qwen_api_mcp_test.yaml +28 -0
  155. massgen/configs/tools/mcp/qwen_local_mcp_example.yaml +24 -0
  156. massgen/configs/tools/mcp/qwen_local_mcp_test.yaml +27 -0
  157. massgen/configs/tools/planning/five_agents_discord_mcp_planning_mode.yaml +140 -0
  158. massgen/configs/tools/planning/five_agents_filesystem_mcp_planning_mode.yaml +151 -0
  159. massgen/configs/tools/planning/five_agents_notion_mcp_planning_mode.yaml +151 -0
  160. massgen/configs/tools/planning/five_agents_twitter_mcp_planning_mode.yaml +155 -0
  161. massgen/configs/tools/planning/gpt5_mini_case_study_mcp_planning_mode.yaml +73 -0
  162. massgen/configs/tools/web-search/claude_streamable_http_test.yaml +43 -0
  163. massgen/configs/tools/web-search/gemini_streamable_http_test.yaml +43 -0
  164. massgen/configs/tools/web-search/gpt5_mini_streamable_http_test.yaml +43 -0
  165. massgen/configs/tools/web-search/gpt_oss_streamable_http_test.yaml +44 -0
  166. massgen/configs/tools/web-search/grok3_mini_streamable_http_test.yaml +43 -0
  167. massgen/configs/tools/web-search/qwen_api_streamable_http_test.yaml +44 -0
  168. massgen/configs/tools/web-search/qwen_local_streamable_http_test.yaml +43 -0
  169. massgen/coordination_tracker.py +708 -0
  170. massgen/docker/README.md +462 -0
  171. massgen/filesystem_manager/__init__.py +21 -0
  172. massgen/filesystem_manager/_base.py +9 -0
  173. massgen/filesystem_manager/_code_execution_server.py +545 -0
  174. massgen/filesystem_manager/_docker_manager.py +477 -0
  175. massgen/filesystem_manager/_file_operation_tracker.py +248 -0
  176. massgen/filesystem_manager/_filesystem_manager.py +813 -0
  177. massgen/filesystem_manager/_path_permission_manager.py +1261 -0
  178. massgen/filesystem_manager/_workspace_tools_server.py +1815 -0
  179. massgen/formatter/__init__.py +10 -0
  180. massgen/formatter/_chat_completions_formatter.py +284 -0
  181. massgen/formatter/_claude_formatter.py +235 -0
  182. massgen/formatter/_formatter_base.py +156 -0
  183. massgen/formatter/_response_formatter.py +263 -0
  184. massgen/frontend/__init__.py +1 -2
  185. massgen/frontend/coordination_ui.py +471 -286
  186. massgen/frontend/displays/base_display.py +56 -11
  187. massgen/frontend/displays/create_coordination_table.py +1956 -0
  188. massgen/frontend/displays/rich_terminal_display.py +1259 -619
  189. massgen/frontend/displays/simple_display.py +9 -4
  190. massgen/frontend/displays/terminal_display.py +27 -68
  191. massgen/logger_config.py +681 -0
  192. massgen/mcp_tools/README.md +232 -0
  193. massgen/mcp_tools/__init__.py +105 -0
  194. massgen/mcp_tools/backend_utils.py +1035 -0
  195. massgen/mcp_tools/circuit_breaker.py +195 -0
  196. massgen/mcp_tools/client.py +894 -0
  197. massgen/mcp_tools/config_validator.py +138 -0
  198. massgen/mcp_tools/docs/circuit_breaker.md +646 -0
  199. massgen/mcp_tools/docs/client.md +950 -0
  200. massgen/mcp_tools/docs/config_validator.md +478 -0
  201. massgen/mcp_tools/docs/exceptions.md +1165 -0
  202. massgen/mcp_tools/docs/security.md +854 -0
  203. massgen/mcp_tools/exceptions.py +338 -0
  204. massgen/mcp_tools/hooks.py +212 -0
  205. massgen/mcp_tools/security.py +780 -0
  206. massgen/message_templates.py +342 -64
  207. massgen/orchestrator.py +1515 -241
  208. massgen/stream_chunk/__init__.py +35 -0
  209. massgen/stream_chunk/base.py +92 -0
  210. massgen/stream_chunk/multimodal.py +237 -0
  211. massgen/stream_chunk/text.py +162 -0
  212. massgen/tests/mcp_test_server.py +150 -0
  213. massgen/tests/multi_turn_conversation_design.md +0 -8
  214. massgen/tests/test_azure_openai_backend.py +156 -0
  215. massgen/tests/test_backend_capabilities.py +262 -0
  216. massgen/tests/test_backend_event_loop_all.py +179 -0
  217. massgen/tests/test_chat_completions_refactor.py +142 -0
  218. massgen/tests/test_claude_backend.py +15 -28
  219. massgen/tests/test_claude_code.py +268 -0
  220. massgen/tests/test_claude_code_context_sharing.py +233 -0
  221. massgen/tests/test_claude_code_orchestrator.py +175 -0
  222. massgen/tests/test_cli_backends.py +180 -0
  223. massgen/tests/test_code_execution.py +679 -0
  224. massgen/tests/test_external_agent_backend.py +134 -0
  225. massgen/tests/test_final_presentation_fallback.py +237 -0
  226. massgen/tests/test_gemini_planning_mode.py +351 -0
  227. massgen/tests/test_grok_backend.py +7 -10
  228. massgen/tests/test_http_mcp_server.py +42 -0
  229. massgen/tests/test_integration_simple.py +198 -0
  230. massgen/tests/test_mcp_blocking.py +125 -0
  231. massgen/tests/test_message_context_building.py +29 -47
  232. massgen/tests/test_orchestrator_final_presentation.py +48 -0
  233. massgen/tests/test_path_permission_manager.py +2087 -0
  234. massgen/tests/test_rich_terminal_display.py +14 -13
  235. massgen/tests/test_timeout.py +133 -0
  236. massgen/tests/test_v3_3agents.py +11 -12
  237. massgen/tests/test_v3_simple.py +8 -13
  238. massgen/tests/test_v3_three_agents.py +11 -18
  239. massgen/tests/test_v3_two_agents.py +8 -13
  240. massgen/token_manager/__init__.py +7 -0
  241. massgen/token_manager/token_manager.py +400 -0
  242. massgen/utils.py +52 -16
  243. massgen/v1/agent.py +45 -91
  244. massgen/v1/agents.py +18 -53
  245. massgen/v1/backends/gemini.py +50 -153
  246. massgen/v1/backends/grok.py +21 -54
  247. massgen/v1/backends/oai.py +39 -111
  248. massgen/v1/cli.py +36 -93
  249. massgen/v1/config.py +8 -12
  250. massgen/v1/logging.py +43 -127
  251. massgen/v1/main.py +18 -32
  252. massgen/v1/orchestrator.py +68 -209
  253. massgen/v1/streaming_display.py +62 -163
  254. massgen/v1/tools.py +8 -12
  255. massgen/v1/types.py +9 -23
  256. massgen/v1/utils.py +5 -23
  257. massgen-0.1.0.dist-info/METADATA +1245 -0
  258. massgen-0.1.0.dist-info/RECORD +273 -0
  259. massgen-0.1.0.dist-info/entry_points.txt +2 -0
  260. massgen/frontend/logging/__init__.py +0 -9
  261. massgen/frontend/logging/realtime_logger.py +0 -197
  262. massgen-0.0.3.dist-info/METADATA +0 -568
  263. massgen-0.0.3.dist-info/RECORD +0 -76
  264. massgen-0.0.3.dist-info/entry_points.txt +0 -2
  265. /massgen/backend/{Function calling openai responses.md → docs/Function calling openai responses.md} +0 -0
  266. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/WHEEL +0 -0
  267. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/licenses/LICENSE +0 -0
  268. {massgen-0.0.3.dist-info → massgen-0.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1035 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Backend utilities for MCP integration.
4
+ Contains all utilities that backends need for MCP functionality.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import json
10
+ import random
11
+ import time
12
+ from typing import Any, AsyncGenerator, Awaitable, Callable, Literal
13
+
14
+ from ..logger_config import log_mcp_activity, logger
15
+
16
+ # Module-level constants
17
+ DEFAULT_MAX_RETRIES = 3
18
+ DEFAULT_RETRY_BASE_DELAY = 0.5
19
+ DEFAULT_RETRY_JITTER_MIN = 0.1
20
+ DEFAULT_RETRY_JITTER_MAX = 0.3
21
+ DEFAULT_MESSAGE_HISTORY_LIMIT = 200
22
+ DEFAULT_TIMEOUT_SECONDS = 30
23
+ DEFAULT_CIRCUIT_BREAKER_MAX_FAILURES = 3
24
+ DEFAULT_CIRCUIT_BREAKER_RESET_TIME = 30
25
+ DEFAULT_CIRCUIT_BREAKER_BACKOFF_MULTIPLIER = 2
26
+ DEFAULT_CIRCUIT_BREAKER_MAX_BACKOFF_MULTIPLIER = 8
27
+
28
+ # Import MCP exceptions
29
+ try:
30
+ from .circuit_breaker import CircuitBreakerConfig
31
+ from .client import MCPClient
32
+ from .exceptions import (
33
+ MCPAuthenticationError,
34
+ MCPConfigurationError,
35
+ MCPConnectionError,
36
+ MCPError,
37
+ MCPResourceError,
38
+ MCPServerError,
39
+ MCPTimeoutError,
40
+ MCPValidationError,
41
+ )
42
+ except ImportError:
43
+ MCPError = Exception
44
+ MCPConnectionError = ConnectionError
45
+ MCPTimeoutError = TimeoutError
46
+ MCPServerError = Exception
47
+ MCPValidationError = ValueError
48
+ MCPAuthenticationError = Exception
49
+ MCPResourceError = Exception
50
+ MCPConfigurationError = Exception
51
+ CircuitBreakerConfig = None
52
+ MCPClient = None
53
+
54
+ # Import hook system
55
+ try:
56
+ from .hooks import FunctionHook, HookType
57
+ except ImportError:
58
+ HookType = None
59
+ FunctionHook = None
60
+
61
+
62
+ class Function:
63
+ """Enhanced function wrapper for MCP tools across all backend APIs."""
64
+
65
+ def __init__(
66
+ self,
67
+ name: str,
68
+ description: str,
69
+ parameters: dict[str, Any],
70
+ entrypoint: Callable[[str], Awaitable[Any]],
71
+ hooks: dict | None = None,
72
+ ) -> None:
73
+ # Validate and sanitize inputs
74
+ self.name = name if name else "unknown_function"
75
+ self.description = description if description and isinstance(description, str) else f"Function: {self.name}"
76
+ self.parameters = parameters if parameters and isinstance(parameters, dict) else {"type": "object", "properties": {}}
77
+ self.entrypoint = entrypoint
78
+ self.hooks = hooks or ({hook_type: [] for hook_type in HookType} if HookType else {})
79
+
80
+ # Context for hook execution
81
+ self._backend_name = None
82
+ self._agent_id = None
83
+
84
+ async def call(self, input_str: str) -> Any:
85
+ """Call the function with hook integration."""
86
+ # Fast path: no hooks registered
87
+ if not HookType or not self.hooks.get(HookType.PRE_CALL):
88
+ return await self.entrypoint(input_str)
89
+
90
+ # Build context for hooks
91
+ context = {"function_name": self.name, "timestamp": time.time(), "backend": self._backend_name or "unknown", "agent_id": self._agent_id}
92
+
93
+ # Execute PRE_CALL hooks
94
+ modified_args = input_str
95
+ for hook in self.hooks.get(HookType.PRE_CALL, []):
96
+ try:
97
+ hook_result = await hook.execute(function_name=self.name, arguments=modified_args, context=context)
98
+
99
+ # Check if hook blocks execution
100
+ if not hook_result.allowed:
101
+ # Return proper CallToolResult format matching permission_wrapper.py
102
+ reason = hook_result.metadata.get("reason", f"Hook '{hook.name}' blocked function call")
103
+ error_msg = f"Permission denied for tool '{self.name}': {reason}"
104
+ logger.warning(f"🚫 [Function] {error_msg}")
105
+
106
+ # Import MCP types for proper result formatting
107
+ try:
108
+ from mcp import types as mcp_types
109
+
110
+ # Return CallToolResult with error flag - same format as permission_wrapper.py
111
+ return mcp_types.CallToolResult(content=[mcp_types.TextContent(type="text", text=f"Error: {error_msg}")], isError=True)
112
+ except ImportError:
113
+ # Fallback if MCP types not available
114
+ logger.error("MCP types not available, returning string error")
115
+ return f"Error: {error_msg}"
116
+
117
+ # Check if hook modified arguments
118
+ if hook_result.modified_args is not None:
119
+ modified_args = hook_result.modified_args
120
+
121
+ except Exception as e:
122
+ logger.error(f"Hook {hook.name} failed for {self.name}: {e}")
123
+
124
+ # Execute the actual function
125
+ return await self.entrypoint(modified_args)
126
+
127
+ def to_openai_format(self) -> dict[str, Any]:
128
+ """Convert function to OpenAI Response API format."""
129
+ return {
130
+ "type": "function",
131
+ "name": self.name,
132
+ "description": self.description,
133
+ "parameters": self.parameters,
134
+ }
135
+
136
+ def to_chat_completions_format(self) -> dict[str, Any]:
137
+ """Convert to Chat Completions API format."""
138
+ return {
139
+ "type": "function",
140
+ "function": {
141
+ "name": self.name or "unknown_function",
142
+ "description": self.description or f"Function: {self.name}",
143
+ "parameters": self.parameters or {"type": "object", "properties": {}},
144
+ },
145
+ }
146
+
147
+ def to_claude_format(self) -> dict[str, Any]:
148
+ """Convert to Claude API format."""
149
+ return {
150
+ "name": self.name,
151
+ "description": self.description,
152
+ "input_schema": self.parameters,
153
+ }
154
+
155
+ def __repr__(self) -> str:
156
+ """String representation of Function."""
157
+ return f"Function(name='{self.name}', description='{self.description[:50]}...')"
158
+
159
+
160
+ class MCPErrorHandler:
161
+ """Standardized MCP error handling utilities."""
162
+
163
+ @staticmethod
164
+ def get_error_details(error: Exception, context: str | None = None, *, log: bool = False) -> tuple[str, str, str]:
165
+ """Return standardized MCP error info and optionally log.
166
+
167
+ Returns:
168
+ Tuple of (log_type, user_message, error_category)
169
+ """
170
+ if isinstance(error, MCPConnectionError):
171
+ details = ("connection error", "MCP connection failed", "connection")
172
+ elif isinstance(error, MCPTimeoutError):
173
+ details = ("timeout error", "MCP session timeout", "timeout")
174
+ elif isinstance(error, MCPServerError):
175
+ details = ("server error", "MCP server error", "server")
176
+ elif isinstance(error, MCPValidationError):
177
+ details = ("validation error", "MCP validation failed", "validation")
178
+ elif isinstance(error, MCPAuthenticationError):
179
+ details = ("authentication error", "MCP authentication failed", "auth")
180
+ elif isinstance(error, MCPResourceError):
181
+ details = ("resource error", "MCP resource unavailable", "resource")
182
+ elif isinstance(error, MCPError):
183
+ details = ("MCP error", "MCP error", "general")
184
+ else:
185
+ details = ("unexpected error", "MCP connection failed", "unknown")
186
+
187
+ if log:
188
+ log_type, user_message, error_category = details
189
+ logger.warning(f"MCP {log_type}: {error}", extra={"context": context or "none"})
190
+
191
+ return details
192
+
193
+ @staticmethod
194
+ def is_transient_error(error: Exception) -> bool:
195
+ """Determine if an error is transient and should be retried."""
196
+ if isinstance(error, (MCPConnectionError, MCPTimeoutError)):
197
+ return True
198
+ elif isinstance(error, MCPServerError):
199
+ error_str = str(error).lower()
200
+ return any(
201
+ keyword in error_str
202
+ for keyword in [
203
+ "timeout",
204
+ "connection",
205
+ "network",
206
+ "temporary",
207
+ "unavailable",
208
+ "503",
209
+ "502",
210
+ "504",
211
+ "500",
212
+ "retry",
213
+ ]
214
+ )
215
+ elif isinstance(error, (ConnectionError, TimeoutError, OSError)):
216
+ return True
217
+ elif isinstance(error, MCPResourceError):
218
+ return True
219
+ return False
220
+
221
+ @staticmethod
222
+ def log_error(
223
+ error: Exception,
224
+ context: str,
225
+ level: str = "auto",
226
+ backend_name: str | None = None,
227
+ agent_id: str | None = None,
228
+ ) -> None:
229
+ """Log MCP error with appropriate level and context."""
230
+ log_type, user_message, error_category = MCPErrorHandler.get_error_details(error)
231
+
232
+ # Auto-determine level
233
+ if level == "auto":
234
+ level = "warning" if error_category in ["connection", "timeout", "resource"] else "error"
235
+
236
+ # Single log call with level suffix
237
+ log_message = f"MCP {log_type} during {context}: {error}"
238
+ log_mcp_activity(
239
+ backend_name,
240
+ f"error ({level})",
241
+ {"message": log_message},
242
+ agent_id=agent_id,
243
+ )
244
+
245
+ @staticmethod
246
+ def get_retry_delay(attempt: int, base_delay: float = DEFAULT_RETRY_BASE_DELAY) -> float:
247
+ """Calculate retry delay with exponential backoff and jitter."""
248
+ # Exponential backoff
249
+ backoff_delay = base_delay * (2**attempt)
250
+
251
+ # Add jitter
252
+ jitter = random.uniform(DEFAULT_RETRY_JITTER_MIN, DEFAULT_RETRY_JITTER_MAX) * backoff_delay
253
+
254
+ return backoff_delay + jitter
255
+
256
+ @staticmethod
257
+ def is_auth_or_resource_error(error: Exception) -> bool:
258
+ """Check if error is authentication or resource related (non-retryable)."""
259
+ return isinstance(error, (MCPAuthenticationError, MCPResourceError))
260
+
261
+
262
+ class MCPRetryHandler:
263
+ """Handles MCP retry logic with user feedback."""
264
+
265
+ @staticmethod
266
+ async def handle_retry_error(
267
+ error: Exception,
268
+ retry_count: int,
269
+ max_retries: int,
270
+ stream_chunk_class,
271
+ backend_name: str | None = None,
272
+ agent_id: str | None = None,
273
+ ) -> tuple[bool, AsyncGenerator]:
274
+ """Handle MCP retry errors with specific messaging and fallback logic."""
275
+ log_type, user_message, _ = MCPErrorHandler.get_error_details(error)
276
+
277
+ # Log the retry attempt
278
+ log_mcp_activity(
279
+ backend_name,
280
+ f"{log_type} on retry",
281
+ {"attempt": retry_count, "error": str(error)},
282
+ agent_id=agent_id,
283
+ )
284
+
285
+ # Check if we've exhausted retries
286
+ if retry_count >= max_retries:
287
+
288
+ async def error_chunks():
289
+ yield stream_chunk_class(
290
+ type="content",
291
+ content=f"\n⚠️ {user_message} after {max_retries} attempts; falling back to workflow tools\n",
292
+ )
293
+
294
+ return False, error_chunks()
295
+
296
+ # Continue retrying
297
+ async def empty_chunks():
298
+ yield
299
+ return
300
+
301
+ return True, empty_chunks()
302
+
303
+ @staticmethod
304
+ async def handle_error_and_fallback(
305
+ error: Exception,
306
+ tool_call_count: int,
307
+ stream_chunk_class,
308
+ backend_name: str | None = None,
309
+ agent_id: str | None = None,
310
+ ) -> AsyncGenerator:
311
+ """Handle MCP errors with specific messaging and fallback to non-MCP tools."""
312
+ log_type, user_message, _ = MCPErrorHandler.get_error_details(error)
313
+
314
+ # Log with specific error type
315
+ log_mcp_activity(
316
+ backend_name,
317
+ "tool call failed",
318
+ {
319
+ "call_number": tool_call_count,
320
+ "error_type": log_type,
321
+ "error": str(error),
322
+ },
323
+ agent_id=agent_id,
324
+ )
325
+
326
+ # Yield user-friendly error message
327
+ yield stream_chunk_class(
328
+ type="content",
329
+ content=f"\n⚠️ {user_message} ({error}); continuing without MCP tools\n",
330
+ )
331
+
332
+
333
+ class MCPMessageManager:
334
+ """Message history management utilities for MCP integration."""
335
+
336
+ @staticmethod
337
+ def trim_message_history(messages: list[dict[str, Any]], max_items: int = DEFAULT_MESSAGE_HISTORY_LIMIT) -> list[dict[str, Any]]:
338
+ """Trim message history to prevent unbounded growth in MCP execution loop."""
339
+ if max_items <= 0 or len(messages) <= max_items:
340
+ return messages
341
+
342
+ preserved = []
343
+ remaining = messages
344
+
345
+ # Preserve system message if it's the first message
346
+ if messages and messages[0].get("role") == "system":
347
+ preserved = [messages[0]]
348
+ remaining = messages[1:]
349
+
350
+ # Keep the most recent items within the limit
351
+ allowed = max_items - len(preserved)
352
+ trimmed_tail = remaining[-allowed:] if allowed > 0 else []
353
+
354
+ result = preserved + trimmed_tail
355
+
356
+ if len(messages) > len(result):
357
+ logger.debug(
358
+ "MCP trimmed message history",
359
+ extra={
360
+ "original_count": len(messages),
361
+ "trimmed_count": len(result),
362
+ "limit": max_items,
363
+ },
364
+ )
365
+
366
+ return result
367
+
368
+
369
+ class MCPConfigHelper:
370
+ """MCP configuration management utilities."""
371
+
372
+ @staticmethod
373
+ def extract_tool_filtering_params(config: dict[str, Any]) -> tuple[list | None, list | None]:
374
+ """Extract allowed_tools and exclude_tools from configuration."""
375
+ allowed_tools = config.get("allowed_tools")
376
+ exclude_tools = config.get("exclude_tools")
377
+
378
+ # Normalize to lists if provided
379
+ if allowed_tools is not None and not isinstance(allowed_tools, list):
380
+ if isinstance(allowed_tools, str):
381
+ allowed_tools = [allowed_tools]
382
+ else:
383
+ logger.warning(
384
+ "MCP invalid allowed_tools type",
385
+ extra={"type": type(allowed_tools).__name__, "action": "ignoring"},
386
+ )
387
+ allowed_tools = None
388
+
389
+ if exclude_tools is not None and not isinstance(exclude_tools, list):
390
+ if isinstance(exclude_tools, str):
391
+ exclude_tools = [exclude_tools]
392
+ else:
393
+ logger.warning(
394
+ "MCP invalid exclude_tools type",
395
+ extra={"type": type(exclude_tools).__name__, "action": "ignoring"},
396
+ )
397
+ exclude_tools = None
398
+
399
+ return allowed_tools, exclude_tools
400
+
401
+ @staticmethod
402
+ def build_circuit_breaker_config(
403
+ transport_type: str = "mcp_tools",
404
+ backend_name: str | None = None,
405
+ agent_id: str | None = None,
406
+ ) -> Any | None:
407
+ """Build circuit breaker configuration for transport type."""
408
+ if CircuitBreakerConfig is None:
409
+ log_mcp_activity(backend_name, "CircuitBreakerConfig unavailable", {}, agent_id=agent_id)
410
+ return None
411
+
412
+ try:
413
+ # Standard configuration for MCP tools (stdio/streamable-http)
414
+ config = CircuitBreakerConfig(
415
+ max_failures=DEFAULT_CIRCUIT_BREAKER_MAX_FAILURES,
416
+ reset_time_seconds=DEFAULT_CIRCUIT_BREAKER_RESET_TIME,
417
+ backoff_multiplier=DEFAULT_CIRCUIT_BREAKER_BACKOFF_MULTIPLIER,
418
+ max_backoff_multiplier=DEFAULT_CIRCUIT_BREAKER_MAX_BACKOFF_MULTIPLIER,
419
+ )
420
+
421
+ log_mcp_activity(
422
+ backend_name,
423
+ "created circuit breaker config",
424
+ {"transport_type": transport_type},
425
+ agent_id=agent_id,
426
+ )
427
+ return config
428
+ except Exception as e:
429
+ log_mcp_activity(
430
+ backend_name,
431
+ "failed to create circuit breaker config",
432
+ {"error": str(e)},
433
+ agent_id=agent_id,
434
+ )
435
+ return None
436
+
437
+
438
+ class MCPCircuitBreakerManager:
439
+ """Circuit breaker management utilities for MCP integration."""
440
+
441
+ @staticmethod
442
+ def apply_circuit_breaker_filtering(
443
+ servers: list[dict[str, Any]],
444
+ circuit_breaker,
445
+ backend_name: str | None = None,
446
+ agent_id: str | None = None,
447
+ ) -> list[dict[str, Any]]:
448
+ """Apply circuit breaker filtering to servers.
449
+
450
+ Args:
451
+ servers: List of server configurations
452
+ circuit_breaker: Circuit breaker instance
453
+ backend_name: Optional backend name for logging context
454
+ agent_id: Optional agent ID for logging context
455
+
456
+ Returns:
457
+ List of servers that pass circuit breaker filtering
458
+ """
459
+ if not circuit_breaker:
460
+ return servers
461
+
462
+ filtered_servers = []
463
+ for server in servers:
464
+ server_name = server.get("name", "unnamed")
465
+ if not circuit_breaker.should_skip_server(server_name, agent_id=agent_id):
466
+ filtered_servers.append(server)
467
+ else:
468
+ log_mcp_activity(
469
+ backend_name,
470
+ "circuit breaker skipping server",
471
+ {"server_name": server_name, "reason": "circuit_open"},
472
+ agent_id=agent_id,
473
+ )
474
+
475
+ return filtered_servers
476
+
477
+ @staticmethod
478
+ async def record_event(
479
+ servers: list[dict[str, Any]],
480
+ circuit_breaker,
481
+ event: Literal["success", "failure"],
482
+ error_message: str | None = None,
483
+ backend_name: str | None = None,
484
+ agent_id: str | None = None,
485
+ ) -> None:
486
+ """Record circuit breaker events for servers.
487
+
488
+ Args:
489
+ servers: List of server configurations
490
+ event: Event type ("success" or "failure")
491
+ circuit_breaker: Circuit breaker instance
492
+ error_message: Optional error message for failure events
493
+ backend_name: Optional backend name for logging context
494
+ agent_id: Optional agent ID for logging context
495
+ """
496
+ if not circuit_breaker:
497
+ return
498
+
499
+ count = 0
500
+ for server in servers:
501
+ server_name = server.get("name", "unnamed")
502
+ try:
503
+ if event == "success":
504
+ circuit_breaker.record_success(server_name, agent_id=agent_id)
505
+ else:
506
+ circuit_breaker.record_failure(server_name, agent_id=agent_id)
507
+ count += 1
508
+ except Exception as cb_error:
509
+ log_mcp_activity(
510
+ backend_name,
511
+ "circuit breaker record failed",
512
+ {
513
+ "event": event,
514
+ "server_name": server_name,
515
+ "error": str(cb_error),
516
+ },
517
+ agent_id=agent_id,
518
+ )
519
+
520
+ if count > 0:
521
+ if event == "success":
522
+ log_mcp_activity(
523
+ backend_name,
524
+ "circuit breaker recorded success",
525
+ {"server_count": count},
526
+ agent_id=agent_id,
527
+ )
528
+ else:
529
+ log_mcp_activity(
530
+ backend_name,
531
+ "circuit breaker recorded failure",
532
+ {"server_count": count, "error": error_message},
533
+ agent_id=agent_id,
534
+ )
535
+
536
+
537
+ class MCPResourceManager:
538
+ """Resource management utilities for MCP integration."""
539
+
540
+ @staticmethod
541
+ async def setup_mcp_client(
542
+ servers: list[dict[str, Any]],
543
+ allowed_tools: list[str] | None,
544
+ exclude_tools: list[str] | None,
545
+ circuit_breaker=None,
546
+ timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
547
+ backend_name: str | None = None,
548
+ agent_id: str | None = None,
549
+ ) -> Any | None:
550
+ """Setup MCP client for stdio/streamable-http servers with circuit breaker protection.
551
+
552
+ Args:
553
+ servers: List of server configurations
554
+ allowed_tools: Optional list of allowed tool names
555
+ exclude_tools: Optional list of excluded tool names
556
+ circuit_breaker: Optional circuit breaker for failure tracking
557
+ timeout_seconds: Connection timeout in seconds
558
+ backend_name: Optional backend name for logging context
559
+ agent_id: Optional agent ID for logging context
560
+
561
+ Returns:
562
+ Connected MCPClient or None if setup failed
563
+ """
564
+ if MCPClient is None:
565
+ log_mcp_activity(
566
+ backend_name,
567
+ "MCPClient unavailable",
568
+ {"functionality": "disabled"},
569
+ agent_id=agent_id,
570
+ )
571
+ return None
572
+
573
+ # Normalize and filter servers
574
+ normalized_servers = MCPSetupManager.normalize_mcp_servers(servers, backend_name, agent_id)
575
+ stdio_streamable_servers = MCPSetupManager.separate_stdio_streamable_servers(normalized_servers, backend_name, agent_id)
576
+
577
+ if not stdio_streamable_servers:
578
+ log_mcp_activity(
579
+ backend_name,
580
+ "no stdio/streamable-http servers configured",
581
+ {},
582
+ agent_id=agent_id,
583
+ )
584
+ return None
585
+
586
+ # Apply circuit breaker filtering if available
587
+ if circuit_breaker:
588
+ filtered_servers = MCPCircuitBreakerManager.apply_circuit_breaker_filtering(stdio_streamable_servers, circuit_breaker, backend_name, agent_id)
589
+ else:
590
+ filtered_servers = stdio_streamable_servers
591
+
592
+ if not filtered_servers:
593
+ log_mcp_activity(
594
+ backend_name,
595
+ "all servers filtered by circuit breaker",
596
+ {"transport_types": ["stdio", "streamable-http"]},
597
+ agent_id=agent_id,
598
+ )
599
+ return None
600
+
601
+ # Retry logic with exponential backoff
602
+ max_retries = DEFAULT_MAX_RETRIES
603
+ for retry in range(max_retries):
604
+ try:
605
+ if retry > 0:
606
+ delay = MCPErrorHandler.get_retry_delay(retry - 1)
607
+ log_mcp_activity(
608
+ backend_name,
609
+ "connection retry",
610
+ {
611
+ "attempt": retry,
612
+ "max_retries": max_retries - 1,
613
+ "delay_seconds": delay,
614
+ },
615
+ agent_id=agent_id,
616
+ )
617
+ await asyncio.sleep(delay)
618
+
619
+ client = await MCPClient.create_and_connect(
620
+ filtered_servers,
621
+ timeout_seconds=timeout_seconds,
622
+ allowed_tools=allowed_tools,
623
+ exclude_tools=exclude_tools,
624
+ )
625
+
626
+ # Record success in circuit breaker
627
+ if circuit_breaker:
628
+ await MCPCircuitBreakerManager.record_event(
629
+ filtered_servers,
630
+ circuit_breaker,
631
+ "success",
632
+ backend_name=backend_name,
633
+ agent_id=agent_id,
634
+ )
635
+
636
+ log_mcp_activity(
637
+ backend_name,
638
+ "connection successful",
639
+ {"attempt": retry + 1},
640
+ agent_id=agent_id,
641
+ )
642
+ return client
643
+
644
+ except (MCPConnectionError, MCPTimeoutError, MCPServerError) as e:
645
+ if retry < max_retries - 1: # Not last attempt
646
+ MCPErrorHandler.log_error(e, f"MCP connection attempt {retry + 1}")
647
+ continue
648
+
649
+ # Record failure and re-raise
650
+ if circuit_breaker:
651
+ await MCPCircuitBreakerManager.record_event(
652
+ filtered_servers,
653
+ circuit_breaker,
654
+ "failure",
655
+ str(e),
656
+ backend_name,
657
+ agent_id,
658
+ )
659
+
660
+ log_mcp_activity(
661
+ backend_name,
662
+ "connection failed after retries",
663
+ {"max_retries": max_retries, "error": str(e)},
664
+ agent_id=agent_id,
665
+ )
666
+ return None
667
+ except Exception as e:
668
+ MCPErrorHandler.log_error(
669
+ e,
670
+ f"Unexpected error during MCP connection attempt {retry + 1}",
671
+ "error",
672
+ )
673
+ if retry < max_retries - 1:
674
+ continue
675
+ return None
676
+
677
+ return None
678
+
679
+ @staticmethod
680
+ def convert_tools_to_functions(mcp_client, backend_name: str | None = None, agent_id: str | None = None, hook_manager=None) -> dict[str, Function]:
681
+ """Convert MCP tools to Function objects with hook support.
682
+
683
+ Args:
684
+ mcp_client: Connected MCPClient instance
685
+ backend_name: Optional backend name for logging context
686
+ agent_id: Optional agent ID for logging context
687
+ hook_manager: Optional hook manager for function hooks
688
+
689
+ Returns:
690
+ Dictionary mapping tool names to Function objects
691
+ """
692
+ if not mcp_client or not hasattr(mcp_client, "tools"):
693
+ return {}
694
+
695
+ functions = {}
696
+ hook_mgr = hook_manager # No fallback to global - each agent must provide its own
697
+
698
+ for tool_name, tool in mcp_client.tools.items():
699
+ try:
700
+ # Fix closure bug by using default parameter to capture tool_name
701
+ def create_tool_entrypoint(captured_tool_name: str = tool_name):
702
+ async def tool_entrypoint(input_str: str) -> Any:
703
+ try:
704
+ arguments = json.loads(input_str)
705
+ except (json.JSONDecodeError, ValueError) as e:
706
+ log_mcp_activity(
707
+ backend_name,
708
+ "invalid JSON arguments for tool",
709
+ {"tool_name": captured_tool_name, "error": str(e)},
710
+ agent_id=agent_id,
711
+ )
712
+ raise MCPValidationError(
713
+ f"Invalid JSON arguments for tool {captured_tool_name}: {e}",
714
+ field="arguments",
715
+ value=input_str,
716
+ )
717
+ return await mcp_client.call_tool(captured_tool_name, arguments)
718
+
719
+ return tool_entrypoint
720
+
721
+ entrypoint = create_tool_entrypoint()
722
+
723
+ # Validate and sanitize tool description
724
+ description = tool.description
725
+ if description is None or not isinstance(description, str):
726
+ description = f"MCP tool: {tool_name}"
727
+ log_mcp_activity(
728
+ backend_name,
729
+ "tool description sanitized",
730
+ {"tool_name": tool_name, "original": tool.description},
731
+ agent_id=agent_id,
732
+ )
733
+
734
+ # Validate and sanitize tool parameters
735
+ parameters = tool.inputSchema
736
+ if parameters is None or not isinstance(parameters, dict):
737
+ parameters = {"type": "object", "properties": {}}
738
+ log_mcp_activity(
739
+ backend_name,
740
+ "tool parameters sanitized",
741
+ {"tool_name": tool_name, "original": tool.inputSchema},
742
+ agent_id=agent_id,
743
+ )
744
+
745
+ # Get hooks for this function
746
+ function_hooks = hook_mgr.get_hooks_for_function(tool_name) if hook_mgr else {}
747
+
748
+ function = Function(
749
+ name=tool_name,
750
+ description=description,
751
+ parameters=parameters,
752
+ entrypoint=entrypoint,
753
+ hooks=function_hooks,
754
+ )
755
+
756
+ # Set backend context
757
+ function._backend_name = backend_name
758
+ function._agent_id = agent_id
759
+
760
+ functions[function.name] = function
761
+
762
+ except Exception as e:
763
+ log_mcp_activity(
764
+ backend_name,
765
+ "failed to register tool",
766
+ {"tool_name": tool_name, "error": str(e)},
767
+ agent_id=agent_id,
768
+ )
769
+
770
+ log_mcp_activity(
771
+ backend_name,
772
+ "registered tools as Function objects",
773
+ {"tool_count": len(functions)},
774
+ agent_id=agent_id,
775
+ )
776
+ return functions
777
+
778
+ @staticmethod
779
+ async def cleanup_mcp_client(client, backend_name: str | None = None, agent_id: str | None = None) -> None:
780
+ """Clean up MCP client connections.
781
+
782
+ Args:
783
+ client: MCPClient instance to clean up
784
+ backend_name: Optional backend name for logging context
785
+ agent_id: Optional agent ID for logging context
786
+ """
787
+ if client:
788
+ try:
789
+ await client.disconnect()
790
+ log_mcp_activity(backend_name, "client cleanup completed", {}, agent_id=agent_id)
791
+ except Exception as e:
792
+ log_mcp_activity(
793
+ backend_name,
794
+ "error during client cleanup",
795
+ {"error": str(e)},
796
+ agent_id=agent_id,
797
+ )
798
+
799
+ @staticmethod
800
+ async def setup_mcp_context_manager(
801
+ backend_instance,
802
+ backend_name: str | None = None,
803
+ agent_id: str | None = None,
804
+ ):
805
+ """Setup MCP tools if configured during context manager entry."""
806
+ if hasattr(backend_instance, "mcp_servers") and backend_instance.mcp_servers and not backend_instance._mcp_initialized:
807
+ try:
808
+ await backend_instance._setup_mcp_tools()
809
+ except Exception as e:
810
+ log_mcp_activity(
811
+ backend_name,
812
+ "setup failed during context entry",
813
+ {"error": str(e)},
814
+ agent_id=agent_id,
815
+ )
816
+
817
+ return backend_instance
818
+
819
+ @staticmethod
820
+ async def cleanup_mcp_context_manager(
821
+ backend_instance,
822
+ logger_instance=None,
823
+ backend_name: str | None = None,
824
+ agent_id: str | None = None,
825
+ ) -> None:
826
+ """Clean up MCP resources during context manager exit."""
827
+ log = logger_instance or logger
828
+
829
+ try:
830
+ if hasattr(backend_instance, "cleanup_mcp"):
831
+ await backend_instance.cleanup_mcp()
832
+ except Exception as e:
833
+ log.error(f"Error during MCP cleanup for backend '{backend_name}': {e}")
834
+ log_mcp_activity(
835
+ backend_name,
836
+ "error during cleanup",
837
+ {"error": str(e)},
838
+ agent_id=agent_id,
839
+ )
840
+
841
+
842
+ class MCPSetupManager:
843
+ """MCP setup and initialization utilities."""
844
+
845
+ @staticmethod
846
+ def normalize_mcp_servers(servers: Any, backend_name: str | None = None, agent_id: str | None = None) -> list[dict[str, Any]]:
847
+ """Validate and normalize mcp_servers into a list of dicts.
848
+
849
+ Args:
850
+ servers: MCP servers configuration (list, dict, or None)
851
+ backend_name: Optional backend name for logging context
852
+ agent_id: Optional agent ID for logging context
853
+
854
+ Returns:
855
+ Normalized list of server dictionaries
856
+ """
857
+ if not servers:
858
+ return []
859
+
860
+ # Support both list and dict formats
861
+ if isinstance(servers, dict):
862
+ if "type" in servers:
863
+ servers = [servers]
864
+ else:
865
+ converted = []
866
+ for name, server_config in servers.items():
867
+ if isinstance(server_config, dict):
868
+ server = server_config.copy()
869
+ server["name"] = name
870
+ converted.append(server)
871
+ servers = converted
872
+
873
+ if not isinstance(servers, list):
874
+ log_mcp_activity(
875
+ backend_name,
876
+ "invalid mcp_servers type",
877
+ {"type": type(servers).__name__, "expected": "list or dict"},
878
+ agent_id=agent_id,
879
+ )
880
+ return []
881
+
882
+ normalized = []
883
+ for i, server in enumerate(servers):
884
+ if not isinstance(server, dict):
885
+ log_mcp_activity(
886
+ backend_name,
887
+ "skipping invalid server",
888
+ {"index": i, "server": str(server)},
889
+ agent_id=agent_id,
890
+ )
891
+ continue
892
+
893
+ if "type" not in server:
894
+ log_mcp_activity(
895
+ backend_name,
896
+ "server missing type field",
897
+ {"index": i},
898
+ agent_id=agent_id,
899
+ )
900
+ continue
901
+
902
+ # Add default name if missing
903
+ if "name" not in server:
904
+ server = server.copy()
905
+ server["name"] = f"server_{i}"
906
+
907
+ normalized.append(server)
908
+
909
+ return normalized
910
+
911
+ @staticmethod
912
+ def separate_stdio_streamable_servers(
913
+ servers: list[dict[str, Any]],
914
+ backend_name: str | None = None,
915
+ agent_id: str | None = None,
916
+ ) -> list[dict[str, Any]]:
917
+ """Extract only stdio and streamable-http servers.
918
+
919
+ Args:
920
+ servers: List of server configurations
921
+ backend_name: Optional backend name for logging context
922
+ agent_id: Optional agent ID for logging context
923
+
924
+ Returns:
925
+ List containing only stdio and streamable-http servers
926
+ """
927
+ stdio_streamable = []
928
+
929
+ for server in servers:
930
+ transport_type = server.get("type", "").lower()
931
+ if transport_type in ["stdio", "streamable-http"]:
932
+ stdio_streamable.append(server)
933
+
934
+ return stdio_streamable
935
+
936
+
937
+ class MCPExecutionManager:
938
+ """MCP function execution utilities with retry logic."""
939
+
940
+ @staticmethod
941
+ async def execute_function_with_retry(
942
+ function_name: str,
943
+ args: dict[str, Any],
944
+ functions: dict[str, Function],
945
+ max_retries: int = DEFAULT_MAX_RETRIES,
946
+ stats_callback: Callable | None = None,
947
+ circuit_breaker_callback: Callable | None = None,
948
+ logger_instance=None,
949
+ ) -> Any:
950
+ """Execute MCP function with exponential backoff retry logic.
951
+
952
+ Args:
953
+ function_name: Name of the MCP function to call
954
+ args: Function arguments as dictionary
955
+ functions: Dictionary of available Function objects
956
+ max_retries: Maximum number of retry attempts
957
+ stats_callback: Optional callback for tracking stats (call_count, failures)
958
+ circuit_breaker_callback: Optional callback for circuit breaker events
959
+ logger_instance: Logger instance to use (defaults to module logger)
960
+
961
+ Returns:
962
+ Function result or structured error payload if all retries fail
963
+ """
964
+ log = logger_instance or logger
965
+
966
+ # Track call attempt
967
+ if stats_callback:
968
+ call_index = await stats_callback("increment_calls")
969
+ else:
970
+ call_index = 1
971
+
972
+ for attempt in range(max_retries + 1):
973
+ try:
974
+ # Convert args to JSON string for the function call
975
+ arguments_json = json.dumps(args)
976
+
977
+ # Execute the MCP function
978
+ result = await functions[function_name].call(arguments_json)
979
+
980
+ # Successful execution
981
+ if attempt > 0:
982
+ log.info(
983
+ "MCP function succeeded on retry",
984
+ extra={
985
+ "function_name": function_name,
986
+ "call_index": call_index,
987
+ "retry_attempt": attempt,
988
+ },
989
+ )
990
+
991
+ return result
992
+
993
+ except Exception as e:
994
+ # Check if this is a non-retryable error
995
+ if MCPErrorHandler.is_auth_or_resource_error(e):
996
+ MCPErrorHandler.log_error(e, f"function call {function_name}")
997
+ if circuit_breaker_callback:
998
+ await circuit_breaker_callback("failure", str(e))
999
+ if stats_callback:
1000
+ await stats_callback("increment_failures")
1001
+ return {
1002
+ "error": str(e),
1003
+ "type": "auth_resource_error",
1004
+ "function": function_name,
1005
+ }
1006
+
1007
+ is_last_attempt = attempt == max_retries
1008
+
1009
+ if MCPErrorHandler.is_transient_error(e) and not is_last_attempt:
1010
+ # Calculate exponential backoff with jitter
1011
+ delay = MCPErrorHandler.get_retry_delay(attempt)
1012
+
1013
+ MCPErrorHandler.log_error(e, f"function call {function_name} (attempt {attempt + 1})")
1014
+ log.info("MCP retrying function call", extra={"delay_seconds": delay})
1015
+
1016
+ await asyncio.sleep(delay)
1017
+ continue
1018
+ else:
1019
+ # Final failure
1020
+ MCPErrorHandler.log_error(e, f"function call {function_name} (final)")
1021
+ if circuit_breaker_callback:
1022
+ await circuit_breaker_callback("failure", str(e))
1023
+ if stats_callback:
1024
+ await stats_callback("increment_failures")
1025
+
1026
+ return {
1027
+ "error": str(e),
1028
+ "type": "execution_error",
1029
+ "function": function_name,
1030
+ }
1031
+ return {
1032
+ "error": "Max retries exceeded",
1033
+ "type": "retry_exhausted",
1034
+ "function": function_name,
1035
+ }