aip-agents-binary 0.5.20__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 (280) hide show
  1. aip_agents/__init__.py +65 -0
  2. aip_agents/a2a/__init__.py +19 -0
  3. aip_agents/a2a/server/__init__.py +10 -0
  4. aip_agents/a2a/server/base_executor.py +1086 -0
  5. aip_agents/a2a/server/google_adk_executor.py +198 -0
  6. aip_agents/a2a/server/langflow_executor.py +180 -0
  7. aip_agents/a2a/server/langgraph_executor.py +270 -0
  8. aip_agents/a2a/types.py +232 -0
  9. aip_agents/agent/__init__.py +27 -0
  10. aip_agents/agent/base_agent.py +970 -0
  11. aip_agents/agent/base_langgraph_agent.py +2942 -0
  12. aip_agents/agent/google_adk_agent.py +926 -0
  13. aip_agents/agent/google_adk_constants.py +6 -0
  14. aip_agents/agent/hitl/__init__.py +24 -0
  15. aip_agents/agent/hitl/config.py +28 -0
  16. aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
  17. aip_agents/agent/hitl/manager.py +532 -0
  18. aip_agents/agent/hitl/models.py +18 -0
  19. aip_agents/agent/hitl/prompt/__init__.py +9 -0
  20. aip_agents/agent/hitl/prompt/base.py +42 -0
  21. aip_agents/agent/hitl/prompt/deferred.py +73 -0
  22. aip_agents/agent/hitl/registry.py +149 -0
  23. aip_agents/agent/interface.py +138 -0
  24. aip_agents/agent/interfaces.py +65 -0
  25. aip_agents/agent/langflow_agent.py +464 -0
  26. aip_agents/agent/langgraph_memory_enhancer_agent.py +433 -0
  27. aip_agents/agent/langgraph_react_agent.py +2514 -0
  28. aip_agents/agent/system_instruction_context.py +34 -0
  29. aip_agents/clients/__init__.py +10 -0
  30. aip_agents/clients/langflow/__init__.py +10 -0
  31. aip_agents/clients/langflow/client.py +477 -0
  32. aip_agents/clients/langflow/types.py +18 -0
  33. aip_agents/constants.py +23 -0
  34. aip_agents/credentials/manager.py +132 -0
  35. aip_agents/examples/__init__.py +5 -0
  36. aip_agents/examples/compare_streaming_client.py +783 -0
  37. aip_agents/examples/compare_streaming_server.py +142 -0
  38. aip_agents/examples/demo_memory_recall.py +401 -0
  39. aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
  40. aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
  41. aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
  42. aip_agents/examples/hello_world_a2a_google_adk_server.py +79 -0
  43. aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
  44. aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
  45. aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
  46. aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
  47. aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
  48. aip_agents/examples/hello_world_a2a_langchain_reference_server.py +105 -0
  49. aip_agents/examples/hello_world_a2a_langchain_server.py +79 -0
  50. aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +78 -0
  51. aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
  52. aip_agents/examples/hello_world_a2a_langflow_server.py +82 -0
  53. aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
  54. aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
  55. aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +92 -0
  56. aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
  57. aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
  58. aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
  59. aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
  60. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
  61. aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
  62. aip_agents/examples/hello_world_a2a_langgraph_server.py +84 -0
  63. aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +79 -0
  64. aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +132 -0
  65. aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
  66. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
  67. aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +251 -0
  68. aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
  69. aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +80 -0
  70. aip_agents/examples/hello_world_google_adk.py +41 -0
  71. aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
  72. aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
  73. aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
  74. aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
  75. aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
  76. aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
  77. aip_agents/examples/hello_world_google_adk_stream.py +44 -0
  78. aip_agents/examples/hello_world_langchain.py +28 -0
  79. aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
  80. aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
  81. aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
  82. aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
  83. aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
  84. aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
  85. aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
  86. aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
  87. aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
  88. aip_agents/examples/hello_world_langchain_stream.py +36 -0
  89. aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
  90. aip_agents/examples/hello_world_langflow_agent.py +163 -0
  91. aip_agents/examples/hello_world_langgraph.py +39 -0
  92. aip_agents/examples/hello_world_langgraph_bosa_twitter.py +41 -0
  93. aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
  94. aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
  95. aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
  96. aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
  97. aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
  98. aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
  99. aip_agents/examples/hello_world_langgraph_stream.py +43 -0
  100. aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
  101. aip_agents/examples/hello_world_model_switch_cli.py +210 -0
  102. aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
  103. aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
  104. aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
  105. aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
  106. aip_agents/examples/hello_world_pii_logger.py +21 -0
  107. aip_agents/examples/hello_world_sentry.py +133 -0
  108. aip_agents/examples/hello_world_step_limits.py +273 -0
  109. aip_agents/examples/hello_world_stock_a2a_server.py +103 -0
  110. aip_agents/examples/hello_world_tool_output_client.py +46 -0
  111. aip_agents/examples/hello_world_tool_output_server.py +114 -0
  112. aip_agents/examples/hitl_demo.py +724 -0
  113. aip_agents/examples/mcp_configs/configs.py +63 -0
  114. aip_agents/examples/mcp_servers/common.py +76 -0
  115. aip_agents/examples/mcp_servers/mcp_name.py +29 -0
  116. aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
  117. aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
  118. aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
  119. aip_agents/examples/mcp_servers/mcp_time.py +10 -0
  120. aip_agents/examples/pii_demo_langgraph_client.py +69 -0
  121. aip_agents/examples/pii_demo_langgraph_server.py +126 -0
  122. aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
  123. aip_agents/examples/pii_demo_multi_agent_server.py +247 -0
  124. aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
  125. aip_agents/examples/todolist_planning_a2a_langgraph_server.py +88 -0
  126. aip_agents/examples/tools/__init__.py +27 -0
  127. aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
  128. aip_agents/examples/tools/adk_weather_tool.py +60 -0
  129. aip_agents/examples/tools/data_generator_tool.py +103 -0
  130. aip_agents/examples/tools/data_visualization_tool.py +312 -0
  131. aip_agents/examples/tools/image_artifact_tool.py +136 -0
  132. aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
  133. aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
  134. aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
  135. aip_agents/examples/tools/langchain_weather_tool.py +48 -0
  136. aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
  137. aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
  138. aip_agents/examples/tools/pii_demo_tools.py +189 -0
  139. aip_agents/examples/tools/random_chart_tool.py +142 -0
  140. aip_agents/examples/tools/serper_tool.py +202 -0
  141. aip_agents/examples/tools/stock_tools.py +82 -0
  142. aip_agents/examples/tools/table_generator_tool.py +167 -0
  143. aip_agents/examples/tools/time_tool.py +82 -0
  144. aip_agents/examples/tools/weather_forecast_tool.py +38 -0
  145. aip_agents/executor/agent_executor.py +473 -0
  146. aip_agents/executor/base.py +48 -0
  147. aip_agents/mcp/__init__.py +1 -0
  148. aip_agents/mcp/client/__init__.py +14 -0
  149. aip_agents/mcp/client/base_mcp_client.py +369 -0
  150. aip_agents/mcp/client/connection_manager.py +193 -0
  151. aip_agents/mcp/client/google_adk/__init__.py +11 -0
  152. aip_agents/mcp/client/google_adk/client.py +381 -0
  153. aip_agents/mcp/client/langchain/__init__.py +11 -0
  154. aip_agents/mcp/client/langchain/client.py +265 -0
  155. aip_agents/mcp/client/persistent_session.py +359 -0
  156. aip_agents/mcp/client/session_pool.py +351 -0
  157. aip_agents/mcp/client/transports.py +215 -0
  158. aip_agents/mcp/utils/__init__.py +7 -0
  159. aip_agents/mcp/utils/config_validator.py +139 -0
  160. aip_agents/memory/__init__.py +14 -0
  161. aip_agents/memory/adapters/__init__.py +10 -0
  162. aip_agents/memory/adapters/base_adapter.py +717 -0
  163. aip_agents/memory/adapters/mem0.py +84 -0
  164. aip_agents/memory/base.py +84 -0
  165. aip_agents/memory/constants.py +49 -0
  166. aip_agents/memory/factory.py +86 -0
  167. aip_agents/memory/guidance.py +20 -0
  168. aip_agents/memory/simple_memory.py +47 -0
  169. aip_agents/middleware/__init__.py +17 -0
  170. aip_agents/middleware/base.py +88 -0
  171. aip_agents/middleware/manager.py +128 -0
  172. aip_agents/middleware/todolist.py +274 -0
  173. aip_agents/schema/__init__.py +69 -0
  174. aip_agents/schema/a2a.py +56 -0
  175. aip_agents/schema/agent.py +111 -0
  176. aip_agents/schema/hitl.py +157 -0
  177. aip_agents/schema/langgraph.py +37 -0
  178. aip_agents/schema/model_id.py +97 -0
  179. aip_agents/schema/step_limit.py +108 -0
  180. aip_agents/schema/storage.py +40 -0
  181. aip_agents/sentry/__init__.py +11 -0
  182. aip_agents/sentry/sentry.py +151 -0
  183. aip_agents/storage/__init__.py +41 -0
  184. aip_agents/storage/base.py +85 -0
  185. aip_agents/storage/clients/__init__.py +12 -0
  186. aip_agents/storage/clients/minio_client.py +318 -0
  187. aip_agents/storage/config.py +62 -0
  188. aip_agents/storage/providers/__init__.py +15 -0
  189. aip_agents/storage/providers/base.py +106 -0
  190. aip_agents/storage/providers/memory.py +114 -0
  191. aip_agents/storage/providers/object_storage.py +214 -0
  192. aip_agents/tools/__init__.py +33 -0
  193. aip_agents/tools/bosa_tools.py +105 -0
  194. aip_agents/tools/browser_use/__init__.py +82 -0
  195. aip_agents/tools/browser_use/action_parser.py +103 -0
  196. aip_agents/tools/browser_use/browser_use_tool.py +1112 -0
  197. aip_agents/tools/browser_use/llm_config.py +120 -0
  198. aip_agents/tools/browser_use/minio_storage.py +198 -0
  199. aip_agents/tools/browser_use/schemas.py +119 -0
  200. aip_agents/tools/browser_use/session.py +76 -0
  201. aip_agents/tools/browser_use/session_errors.py +132 -0
  202. aip_agents/tools/browser_use/steel_session_recording.py +317 -0
  203. aip_agents/tools/browser_use/streaming.py +813 -0
  204. aip_agents/tools/browser_use/structured_data_parser.py +257 -0
  205. aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
  206. aip_agents/tools/browser_use/types.py +78 -0
  207. aip_agents/tools/code_sandbox/__init__.py +26 -0
  208. aip_agents/tools/code_sandbox/constant.py +13 -0
  209. aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +257 -0
  210. aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +411 -0
  211. aip_agents/tools/constants.py +165 -0
  212. aip_agents/tools/document_loader/__init__.py +44 -0
  213. aip_agents/tools/document_loader/base_reader.py +302 -0
  214. aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
  215. aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
  216. aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
  217. aip_agents/tools/document_loader/pdf_splitter.py +169 -0
  218. aip_agents/tools/gl_connector/__init__.py +5 -0
  219. aip_agents/tools/gl_connector/tool.py +351 -0
  220. aip_agents/tools/memory_search/__init__.py +22 -0
  221. aip_agents/tools/memory_search/base.py +200 -0
  222. aip_agents/tools/memory_search/mem0.py +258 -0
  223. aip_agents/tools/memory_search/schema.py +48 -0
  224. aip_agents/tools/memory_search_tool.py +26 -0
  225. aip_agents/tools/time_tool.py +117 -0
  226. aip_agents/tools/tool_config_injector.py +300 -0
  227. aip_agents/tools/web_search/__init__.py +15 -0
  228. aip_agents/tools/web_search/serper_tool.py +187 -0
  229. aip_agents/types/__init__.py +70 -0
  230. aip_agents/types/a2a_events.py +13 -0
  231. aip_agents/utils/__init__.py +79 -0
  232. aip_agents/utils/a2a_connector.py +1757 -0
  233. aip_agents/utils/artifact_helpers.py +502 -0
  234. aip_agents/utils/constants.py +22 -0
  235. aip_agents/utils/datetime/__init__.py +34 -0
  236. aip_agents/utils/datetime/normalization.py +231 -0
  237. aip_agents/utils/datetime/timezone.py +206 -0
  238. aip_agents/utils/env_loader.py +27 -0
  239. aip_agents/utils/event_handler_registry.py +58 -0
  240. aip_agents/utils/file_prompt_utils.py +176 -0
  241. aip_agents/utils/final_response_builder.py +211 -0
  242. aip_agents/utils/formatter_llm_client.py +231 -0
  243. aip_agents/utils/langgraph/__init__.py +19 -0
  244. aip_agents/utils/langgraph/converter.py +128 -0
  245. aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
  246. aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
  247. aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
  248. aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1071 -0
  249. aip_agents/utils/langgraph/tool_output_management.py +967 -0
  250. aip_agents/utils/logger.py +195 -0
  251. aip_agents/utils/metadata/__init__.py +27 -0
  252. aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
  253. aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
  254. aip_agents/utils/metadata/activity_narrative/builder.py +817 -0
  255. aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
  256. aip_agents/utils/metadata/activity_narrative/context.py +49 -0
  257. aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
  258. aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
  259. aip_agents/utils/metadata/schemas/__init__.py +16 -0
  260. aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
  261. aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
  262. aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
  263. aip_agents/utils/metadata_helper.py +358 -0
  264. aip_agents/utils/name_preprocessor/__init__.py +17 -0
  265. aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
  266. aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
  267. aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
  268. aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
  269. aip_agents/utils/pii/__init__.py +25 -0
  270. aip_agents/utils/pii/pii_handler.py +397 -0
  271. aip_agents/utils/pii/pii_helper.py +207 -0
  272. aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
  273. aip_agents/utils/reference_helper.py +273 -0
  274. aip_agents/utils/sse_chunk_transformer.py +831 -0
  275. aip_agents/utils/step_limit_manager.py +265 -0
  276. aip_agents/utils/token_usage_helper.py +156 -0
  277. aip_agents_binary-0.5.20.dist-info/METADATA +681 -0
  278. aip_agents_binary-0.5.20.dist-info/RECORD +280 -0
  279. aip_agents_binary-0.5.20.dist-info/WHEEL +5 -0
  280. aip_agents_binary-0.5.20.dist-info/top_level.txt +1 -0
@@ -0,0 +1,369 @@
1
+ """Base MCP Client for aip-agents with Session Persistence.
2
+
3
+ This base class provides persistent session management for MCP connections and serves
4
+ as the foundation for SDK-specific MCP clients (LangChain, Google ADK, etc.). It handles
5
+ session pooling, initialization, tool discovery, and cleanup while leaving SDK-specific
6
+ tool conversion to subclasses.
7
+
8
+ Authors:
9
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
10
+ """
11
+
12
+ import asyncio
13
+ from abc import ABC, abstractmethod
14
+ from typing import Any
15
+
16
+ from gllm_tools.mcp.client.client import MCPClient
17
+ from gllm_tools.mcp.client.config import MCPConfiguration
18
+ from gllm_tools.mcp.client.resource import MCPResource
19
+ from mcp.types import CallToolResult, Tool
20
+
21
+ from aip_agents.mcp.client.persistent_session import PersistentMCPSession
22
+ from aip_agents.mcp.client.session_pool import MCPSessionPool
23
+ from aip_agents.utils.logger import get_logger
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class BaseMCPClient(MCPClient, ABC):
29
+ """Base MCP Client with persistent session management for aip-agents.
30
+
31
+ This class provides:
32
+ - Persistent session management across tool calls
33
+ - One-time tool registration and caching
34
+ - Automatic connection health monitoring and reconnection
35
+ - Centralized cleanup of all MCP resources
36
+ - Generic tool discovery from all configured MCP servers
37
+
38
+ Subclasses should implement SDK-specific tool conversion in _process_tool() method.
39
+ """
40
+
41
+ def __init__(self, servers: dict[str, MCPConfiguration]):
42
+ """Initialize the base MCP client with session pool.
43
+
44
+ Args:
45
+ servers (dict[str, MCPConfiguration]): Dictionary of MCP server configurations by server name
46
+ """
47
+ super().__init__(servers)
48
+ self.session_pool = MCPSessionPool()
49
+ self._tools_cache: list[Tool] = []
50
+ self._initialized = False
51
+ # Guard against concurrent initialization
52
+ self._init_lock: asyncio.Lock = asyncio.Lock()
53
+
54
+ async def initialize(self) -> None:
55
+ """Initialize all MCP sessions and cache tools once.
56
+
57
+ This method is idempotent and only performs initialization if not already done.
58
+ It establishes persistent connections to all configured MCP servers and caches
59
+ available tools for efficient access.
60
+
61
+ Raises:
62
+ Exception: If any session initialization fails
63
+ """
64
+ if self._initialized:
65
+ logger.debug("BaseMCPClient already initialized, skipping")
66
+ return
67
+
68
+ # Ensure only one concurrent initializer proceeds
69
+ async with self._init_lock:
70
+ if self._initialized:
71
+ logger.debug("BaseMCPClient already initialized (post-lock), skipping")
72
+ return
73
+
74
+ logger.info(f"Initializing BaseMCPClient with {len(self.servers)} MCP servers")
75
+
76
+ try:
77
+ # Initialize all persistent sessions concurrently
78
+ await self.session_pool.initialize_all_sessions(self.servers)
79
+
80
+ # Cache all available tools from all sessions
81
+ self._tools_cache = await self.session_pool.get_all_tools()
82
+
83
+ self._initialized = True
84
+ logger.info(
85
+ f"BaseMCPClient initialization complete: "
86
+ f"{len(self._tools_cache)} tools cached from {self.session_pool.session_count} sessions"
87
+ )
88
+ except Exception as e:
89
+ logger.error(f"Failed to initialize BaseMCPClient: {e}", exc_info=True)
90
+ # Cleanup any partially initialized sessions on failure
91
+ await self.cleanup()
92
+ raise
93
+
94
+ @abstractmethod
95
+ async def get_tools(self, server: str | None = None) -> list[Any]:
96
+ """Get framework-specific tools from MCP servers.
97
+
98
+ This method must be implemented by subclasses to provide framework-specific
99
+ tool conversion (e.g., StructuredTool for LangChain, FunctionTool for Google ADK).
100
+
101
+ Args:
102
+ server (str | None): Optional server name to filter tools from a specific server.
103
+ If None, returns tools from all configured servers.
104
+
105
+ Returns:
106
+ list[Any]: List of framework-specific tool objects.
107
+ """
108
+ pass # pragma: no cover # Abstract method - cannot be executed directly
109
+
110
+ async def get_raw_mcp_tools(self, server: str | None = None) -> list[Tool]:
111
+ """Get raw MCP tools - for subclasses to perform framework-specific conversions.
112
+
113
+ This method provides access to the cached raw MCP Tool objects.
114
+ Subclasses use this to convert to framework-specific tools.
115
+
116
+ Args:
117
+ server (str | None): Optional server name to filter tools from a specific server.
118
+ If None, returns tools from all configured servers.
119
+
120
+ Returns:
121
+ list[Tool]: List of raw MCP Tool objects. Empty list if not initialized or no tools available.
122
+ """
123
+ if not self._initialized:
124
+ await self.initialize()
125
+
126
+ if server:
127
+ if server not in self.servers:
128
+ logger.warning(f"Server '{server}' not found in configuration")
129
+ return []
130
+
131
+ try:
132
+ session = self.session_pool.get_session(server)
133
+ return await session.list_tools()
134
+ except KeyError:
135
+ logger.warning(f"No active session found for server '{server}'")
136
+ return []
137
+ except Exception as e:
138
+ logger.error(f"Failed to get tools from server '{server}': {e}")
139
+ return []
140
+ else:
141
+ return self._tools_cache.copy()
142
+
143
+ def get_tools_count(self, server: str | None = None) -> int:
144
+ """Get count of raw MCP tools without expensive copying.
145
+
146
+ This is an efficient way to get tool counts for logging/metrics
147
+ without the overhead of copying tool lists.
148
+
149
+ Args:
150
+ server (str | None): Optional server name to filter tools from a specific server.
151
+ If None, returns count from all configured servers.
152
+
153
+ Returns:
154
+ int: Count of raw MCP tools available.
155
+ """
156
+ if not self._initialized:
157
+ return 0
158
+
159
+ if server:
160
+ if server not in self.servers:
161
+ return 0
162
+ try:
163
+ session = self.session_pool.get_session(server)
164
+ return session.get_tools_count()
165
+ except KeyError:
166
+ return 0
167
+ except Exception as e:
168
+ logger.error(f"Failed to get tools count from server '{server}': {e}")
169
+ return 0
170
+ else:
171
+ return len(self._tools_cache)
172
+
173
+ async def get_session(self, server_name: str) -> PersistentMCPSession:
174
+ """Get a persistent session for a specific server.
175
+
176
+ Args:
177
+ server_name (str): The name of the MCP server
178
+
179
+ Returns:
180
+ PersistentMCPSession: Persistent MCP session for the specified server
181
+
182
+ Raises:
183
+ KeyError: If the server is not configured or no active session exists
184
+ """
185
+ if not self._initialized:
186
+ await self.initialize()
187
+
188
+ try:
189
+ return self.session_pool.get_session(server_name)
190
+ except KeyError:
191
+ logger.error(f"Server '{server_name}' not found in configuration")
192
+ raise
193
+ except Exception as e:
194
+ logger.error(f"Failed to get session for server '{server_name}': {e}")
195
+ raise
196
+
197
+ async def call_tool(self, server_name: str, tool_name: str, arguments: dict[str, Any]) -> CallToolResult:
198
+ """Execute a tool on a specific MCP server using persistent session.
199
+
200
+ Args:
201
+ server_name (str): The MCP server to execute the tool on
202
+ tool_name (str): The name of the tool to execute
203
+ arguments (dict[str, Any]): Arguments for the tool execution
204
+
205
+ Returns:
206
+ CallToolResult: The result of the tool execution
207
+
208
+ Raises:
209
+ KeyError: If the server doesn't exist
210
+ Exception: If tool execution fails
211
+ """
212
+ if not self._initialized:
213
+ await self.initialize()
214
+
215
+ try:
216
+ session = self.session_pool.get_session(server_name)
217
+ return await session.call_tool(tool_name, arguments)
218
+ except KeyError:
219
+ logger.error(f"Server '{server_name}' not found in configuration")
220
+ raise
221
+ except Exception as e:
222
+ logger.error(f"Failed to call tool '{tool_name}' on server '{server_name}': {e}")
223
+ raise
224
+
225
+ async def read_resource(self, server_name: str, resource_uri: str):
226
+ """Read an MCP resource from a specific server using persistent session.
227
+
228
+ Args:
229
+ server_name (str): The MCP server to read the resource from
230
+ resource_uri (str): The URI of the resource to read
231
+
232
+ Returns:
233
+ Any: The resource content
234
+
235
+ Raises:
236
+ KeyError: If the server doesn't exist
237
+ Exception: If resource reading fails
238
+ """
239
+ if not self._initialized:
240
+ await self.initialize()
241
+
242
+ try:
243
+ session = self.session_pool.get_session(server_name)
244
+ return await session.read_resource(resource_uri)
245
+ except KeyError:
246
+ logger.error(f"Server '{server_name}' not found in configuration")
247
+ raise
248
+ except Exception as e:
249
+ logger.error(f"Failed to read resource '{resource_uri}' from server '{server_name}': {e}")
250
+ raise
251
+
252
+ async def cleanup(self) -> None:
253
+ """Clean up all MCP resources and close sessions.
254
+
255
+ This method properly closes all persistent sessions and cleans up resources.
256
+ It can be called multiple times safely.
257
+ """
258
+ if self._initialized:
259
+ logger.info("Cleaning up BaseMCPClient resources")
260
+ cleanup_error = None
261
+ try:
262
+ await self.session_pool.close_all_sessions()
263
+ except TimeoutError as e:
264
+ cleanup_error = e
265
+ logger.warning(f"Timeout during BaseMCPClient cleanup: {e}")
266
+ except Exception as e:
267
+ cleanup_error = e
268
+ logger.error(f"Unexpected error during BaseMCPClient cleanup: {e}", exc_info=True)
269
+ # Don't re-raise - cleanup should be best-effort
270
+ finally:
271
+ # Always clear cache and mark as not initialized, even if session cleanup failed
272
+ self._tools_cache.clear()
273
+ self._initialized = False
274
+ if cleanup_error:
275
+ logger.info("BaseMCPClient cleanup completed (session cleanup had errors)")
276
+ else:
277
+ logger.info("BaseMCPClient cleanup completed successfully")
278
+ else:
279
+ logger.debug("BaseMCPClient cleanup called but not initialized, skipping")
280
+
281
+ @property
282
+ def is_initialized(self) -> bool:
283
+ """Check if the client is fully initialized.
284
+
285
+ Returns:
286
+ bool: True if sessions are initialized and tools are cached, False otherwise
287
+ """
288
+ return self._initialized
289
+
290
+ @property
291
+ def active_sessions(self) -> dict[str, PersistentMCPSession]:
292
+ """Get all active persistent sessions.
293
+
294
+ Returns:
295
+ dict[str, PersistentMCPSession]: Dictionary of active sessions by server name
296
+ """
297
+ if not self._initialized:
298
+ return {}
299
+ return self.session_pool.get_all_active_sessions()
300
+
301
+ def get_session_count(self) -> int:
302
+ """Get the number of active MCP sessions.
303
+
304
+ Returns:
305
+ Number of active persistent sessions
306
+ """
307
+ return self.session_pool.session_count
308
+
309
+ def _get_annotated_server(self, resource: MCPResource) -> str | None:
310
+ """Extract server name from resource annotations if valid.
311
+
312
+ Args:
313
+ resource (MCPResource): The MCP resource.
314
+
315
+ Returns:
316
+ str | None: Annotated server name if valid and configured, else None.
317
+ """
318
+ if not self.servers:
319
+ return None
320
+
321
+ annotations = getattr(resource, "annotations", None)
322
+ if annotations and hasattr(annotations, "server") and annotations.server:
323
+ annotated = annotations.server
324
+ if annotated in self.servers:
325
+ return annotated
326
+ logger.warning(f"Resource annotation requested server '{annotated}' which is not configured; falling back")
327
+ return None
328
+
329
+ def _get_default_server(self) -> str:
330
+ """Get the default server name (first configured server).
331
+
332
+ Returns:
333
+ str: First server name.
334
+
335
+ Raises:
336
+ ValueError: If no servers configured.
337
+ """
338
+ if not self.servers:
339
+ raise ValueError("No MCP servers configured; cannot resolve server for resource")
340
+
341
+ server_list = list(self.servers.keys())
342
+ if len(server_list) > 1:
343
+ logger.warning(
344
+ "Multiple MCP servers configured but resource provided no annotation; "
345
+ f"defaulting to first server '{server_list[0]}'"
346
+ )
347
+ return server_list[0]
348
+
349
+ def _determine_server_name_for_resource(self, resource: MCPResource) -> str:
350
+ """Determine the server name for an MCP resource.
351
+
352
+ Args:
353
+ resource (MCPResource): The MCP resource to determine server for.
354
+
355
+ Returns:
356
+ str: Server name string.
357
+
358
+ Raises:
359
+ ValueError: If cannot determine server.
360
+ """
361
+ try:
362
+ annotated = self._get_annotated_server(resource)
363
+ if annotated:
364
+ return annotated
365
+ return self._get_default_server()
366
+ except Exception as e:
367
+ error_msg = f"Failed to determine server for resource {resource.uri}: {e}"
368
+ logger.error(error_msg)
369
+ raise ValueError(error_msg) from e
@@ -0,0 +1,193 @@
1
+ """MCP Connection Manager for persistent connection lifecycle management.
2
+
3
+ This module implements the connection manager pattern inspired by mcp-use library
4
+ to avoid cancel scope issues and provide persistent connections.
5
+
6
+ Authors:
7
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
8
+ """
9
+
10
+ import asyncio
11
+ import inspect
12
+ from typing import Any
13
+
14
+ from gllm_tools.mcp.client.config import MCPConfiguration
15
+
16
+ from aip_agents.mcp.client.transports import TransportType, create_transport
17
+ from aip_agents.utils.logger import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class MCPConnectionManager:
23
+ """Manages MCP connection lifecycle following mcp-use patterns.
24
+
25
+ This connection manager handles the transport connection lifecycle in a background
26
+ task to avoid cancel scope issues during cleanup. It supports automatic transport
27
+ negotiation (HTTP -> SSE fallback) and graceful shutdown. Invalid explicit transports
28
+ are normalized via aliases (e.g., 'streamable_http' -> 'http') or fall back to
29
+ auto-detection with a warning.
30
+ """
31
+
32
+ TRANSPORT_ALIASES = {
33
+ "http": TransportType.HTTP,
34
+ "streamable-http": TransportType.HTTP,
35
+ "sse": TransportType.SSE,
36
+ "stdio": TransportType.STDIO,
37
+ }
38
+
39
+ def __init__(self, server_name: str, config: MCPConfiguration):
40
+ """Initialize connection manager.
41
+
42
+ Args:
43
+ server_name (str): Name of the MCP server
44
+ config (MCPConfiguration): MCP server configuration
45
+ """
46
+ self.server_name = server_name
47
+ self.config = config
48
+ self._task = None
49
+ self._connection = None
50
+ self._transport = None
51
+ self._stop_event = asyncio.Event()
52
+ self._ready_event = asyncio.Event()
53
+ self._done_event = asyncio.Event()
54
+ self._exception = None
55
+ self.transport_type = None
56
+ # Configurable retry settings (MCP-specific, defaults to reasonable values)
57
+ self.max_retries = config.get("max_retries", 3)
58
+ self.initial_retry_delay = config.get("initial_retry_delay", 1.0)
59
+
60
+ async def start(self) -> tuple[Any, Any]:
61
+ """Start connection in background task.
62
+
63
+ Returns:
64
+ tuple[Any, Any]: Tuple of (read_stream, write_stream) for ClientSession
65
+
66
+ Raises:
67
+ Exception: If connection establishment fails
68
+ """
69
+ logger.debug(f"Starting connection manager for {self.server_name}")
70
+ self._task = asyncio.create_task(self._connection_task())
71
+ await self._ready_event.wait()
72
+
73
+ if self._exception:
74
+ raise self._exception
75
+
76
+ return self._connection
77
+
78
+ async def stop(self) -> None:
79
+ """Stop connection gracefully."""
80
+ logger.debug(f"Stopping connection manager for {self.server_name}")
81
+ if self._task and not self._task.done():
82
+ self._stop_event.set()
83
+ try:
84
+ await asyncio.wait_for(self._task, timeout=5.0)
85
+ except TimeoutError:
86
+ logger.warning(f"Connection manager for {self.server_name} did not stop gracefully")
87
+ self._task.cancel()
88
+ await self._done_event.wait()
89
+
90
+ @property
91
+ def is_connected(self) -> bool:
92
+ """Check if connection is active.
93
+
94
+ Returns:
95
+ bool: True if connected, False otherwise
96
+ """
97
+ return (
98
+ self._connection is not None
99
+ and self._task is not None
100
+ and not self._task.done()
101
+ and not self._stop_event.is_set()
102
+ )
103
+
104
+ def _auto_detect_transport_type(self) -> TransportType:
105
+ """Auto-detect transport type based on configuration.
106
+
107
+ Returns:
108
+ TransportType: Detected transport type
109
+ """
110
+ if "command" in self.config:
111
+ return TransportType.STDIO
112
+ elif "url" in self.config:
113
+ url = self.config["url"]
114
+ if url.endswith("/sse"):
115
+ return TransportType.SSE
116
+ else:
117
+ return TransportType.HTTP
118
+ else:
119
+ return TransportType.STDIO
120
+
121
+ def _get_transport_type(self) -> TransportType:
122
+ """Determine the transport type to use, prioritizing explicit config with aliases.
123
+
124
+ Returns:
125
+ TransportType: Transport type enum
126
+
127
+ Notes:
128
+ Invalid explicit transports trigger a warning and fallback to auto-detection; no exception raised.
129
+ """
130
+ explicit_transport = self.config.get("transport", "").lower().replace("_", "-")
131
+ if explicit_transport in self.TRANSPORT_ALIASES:
132
+ return self.TRANSPORT_ALIASES[explicit_transport]
133
+
134
+ if explicit_transport:
135
+ logger.warning(f"Unknown explicit transport '{explicit_transport}'. Falling back to auto-detection.")
136
+
137
+ return self._auto_detect_transport_type()
138
+
139
+ async def _establish_connection(self) -> None:
140
+ """Establish connection based on transport preference with fallback.
141
+
142
+ Uses configurable retries with exponential backoff for transient failures.
143
+
144
+ Raises:
145
+ ConnectionError: If all connection attempts fail
146
+ """
147
+ self.transport_type = self._get_transport_type()
148
+ details = f"URL: {self.config.get('url', 'N/A')}, Command: {self.config.get('command', 'N/A')}"
149
+ logger.info(f"Establishing connection to {self.server_name} via {self.transport_type} ({details})")
150
+
151
+ retry_delay = self.initial_retry_delay
152
+ for attempt in range(self.max_retries):
153
+ self._transport = create_transport(self.server_name, self.config, self.transport_type)
154
+ try:
155
+ read_stream, write_stream, _ = await self._transport.connect()
156
+ self._connection = (read_stream, write_stream)
157
+ logger.info(f"Connection established on attempt {attempt + 1}")
158
+ return
159
+ except ValueError:
160
+ # Config validation errors should not be retried
161
+ raise
162
+ except Exception as e:
163
+ logger.warning(f"Connection attempt {attempt + 1} failed: {e}")
164
+ if attempt < self.max_retries - 1:
165
+ await asyncio.sleep(retry_delay)
166
+ retry_delay *= 2 # Exponential backoff
167
+ else:
168
+ raise ConnectionError(
169
+ f"Failed to establish connection to {self.server_name} "
170
+ f"after {self.max_retries} attempts: {str(e)}"
171
+ ) from e
172
+
173
+ async def _connection_task(self) -> None:
174
+ """Background task that manages the connection lifecycle."""
175
+ try:
176
+ await self._establish_connection()
177
+ self._ready_event.set()
178
+ await self._stop_event.wait()
179
+ except Exception as e:
180
+ logger.error(f"Connection failed for {self.server_name}: {e}", exc_info=True)
181
+ self._exception = e
182
+ self._ready_event.set()
183
+ finally:
184
+ if self._transport:
185
+ try:
186
+ close_result = self._transport.close()
187
+ if inspect.isawaitable(close_result):
188
+ await close_result
189
+ except Exception as exc: # pragma: no cover - defensive
190
+ logger.warning("Failed to close transport cleanly for %s: %s", self.server_name, exc)
191
+ self._connection = None
192
+ self._done_event.set()
193
+ logger.debug(f"Connection manager cleanup complete for {self.server_name}")
@@ -0,0 +1,11 @@
1
+ """Google ADK MCP Client.
2
+
3
+ This module provides a client for interacting with MCP servers using Google ADK.
4
+
5
+ Authors:
6
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
7
+ """
8
+
9
+ from aip_agents.mcp.client.google_adk.client import GoogleADKMCPClient
10
+
11
+ __all__ = ["GoogleADKMCPClient"]