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,351 @@
1
+ """MCP Session Pool for centralized session management.
2
+
3
+ This module manages a pool of persistent MCP sessions, providing centralized
4
+ initialization, tool collection, and cleanup.
5
+
6
+ Authors:
7
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
8
+ """
9
+
10
+ import asyncio
11
+
12
+ from gllm_tools.mcp.client.config import MCPConfiguration
13
+ from mcp.types import Tool
14
+
15
+ from aip_agents.mcp.client.persistent_session import PersistentMCPSession
16
+ from aip_agents.mcp.utils.config_validator import validate_allowed_tools_config, validate_allowed_tools_list
17
+ from aip_agents.utils.logger import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class MCPSessionPool:
23
+ """Manages pool of persistent MCP sessions.
24
+
25
+ This pool provides centralized management of MCP sessions, including
26
+ initialization, tool collection, and resource cleanup. Sessions are
27
+ reused across the agent lifecycle.
28
+ """
29
+
30
+ def __init__(self):
31
+ """Initialize empty session pool."""
32
+ self.sessions: dict[str, PersistentMCPSession] = {}
33
+ self._lock = asyncio.Lock()
34
+ self._initialized = False
35
+
36
+ async def get_or_create_session(
37
+ self,
38
+ server_name: str,
39
+ config: MCPConfiguration,
40
+ allowed_tools: list[str] | None = None,
41
+ ) -> PersistentMCPSession:
42
+ """Get existing session or create new one.
43
+
44
+ Args:
45
+ server_name (str): Name of the MCP server
46
+ config (MCPConfiguration): MCP server configuration
47
+ allowed_tools (list[str] | None): Optional list of tool names to allow. None means all tools allowed.
48
+
49
+ Returns:
50
+ PersistentMCPSession: Persistent MCP session
51
+
52
+ Raises:
53
+ Exception: If session creation fails
54
+ """
55
+ async with self._lock:
56
+ if server_name in self.sessions:
57
+ return self._update_existing_session(server_name, allowed_tools)
58
+
59
+ return await self._create_and_initialize_session(server_name, config, allowed_tools)
60
+
61
+ def _update_existing_session(
62
+ self,
63
+ server_name: str,
64
+ allowed_tools: list[str] | None,
65
+ ) -> PersistentMCPSession:
66
+ """Update existing session's allowed_tools if changed.
67
+
68
+ Args:
69
+ server_name (str): Name of the MCP server
70
+ allowed_tools (list[str] | None): Optional list of tool names to allow
71
+
72
+ Returns:
73
+ PersistentMCPSession: Updated existing session
74
+ """
75
+ existing_session = self.sessions[server_name]
76
+ if existing_session.update_allowed_tools(allowed_tools):
77
+ logger.debug(f"Reconfigured allowed_tools for {server_name}")
78
+ else:
79
+ logger.debug(f"Reusing existing session for {server_name} (allowed_tools unchanged)")
80
+ return existing_session
81
+
82
+ async def _create_and_initialize_session(
83
+ self,
84
+ server_name: str,
85
+ config: MCPConfiguration,
86
+ allowed_tools: list[str] | None,
87
+ ) -> PersistentMCPSession:
88
+ """Create and initialize a new session.
89
+
90
+ Args:
91
+ server_name (str): Name of the MCP server
92
+ config (MCPConfiguration): MCP server configuration
93
+ allowed_tools (list[str] | None): Optional list of tool names to allow
94
+
95
+ Returns:
96
+ PersistentMCPSession: Newly created and initialized session
97
+
98
+ Raises:
99
+ Exception: If session creation or initialization fails
100
+ """
101
+ logger.info(f"Creating new session for {server_name}")
102
+ session = PersistentMCPSession(
103
+ server_name,
104
+ config,
105
+ allowed_tools=allowed_tools,
106
+ )
107
+
108
+ # Initialize session first, only store if successful
109
+ try:
110
+ await session.initialize()
111
+ # Only store session after successful initialization
112
+ self.sessions[server_name] = session
113
+ logger.info(f"Session created and cached for {server_name}")
114
+ return session
115
+ except Exception:
116
+ # Clean up session on failure
117
+ await session.disconnect()
118
+ raise
119
+
120
+ async def initialize_all_sessions(self, server_configs: dict[str, MCPConfiguration]) -> None:
121
+ """Initialize all sessions and cache tools.
122
+
123
+ This method initializes all configured MCP servers concurrently
124
+ for better performance.
125
+
126
+ Args:
127
+ server_configs (dict[str, MCPConfiguration]): Dictionary of server configurations
128
+
129
+ Raises:
130
+ Exception: If any session initialization fails
131
+ """
132
+ if self._initialized:
133
+ logger.debug("Session pool already initialized")
134
+ return
135
+
136
+ logger.info(f"Initializing session pool with {len(server_configs)} servers")
137
+
138
+ # Initialize all sessions concurrently
139
+ initialization_tasks = []
140
+ for server_name, config in server_configs.items():
141
+ allowed_tools = self._extract_allowed_tools(server_name, config)
142
+ task = self._initialize_single_session(server_name, config, allowed_tools)
143
+ initialization_tasks.append(task)
144
+
145
+ if initialization_tasks:
146
+ try:
147
+ await asyncio.gather(*initialization_tasks)
148
+ self._initialized = True
149
+ logger.info(f"Session pool initialized with {len(self.sessions)} active sessions")
150
+ except Exception as e:
151
+ logger.error(f"Failed to initialize session pool: {e}")
152
+ # Cleanup any partially initialized sessions
153
+ await self.close_all_sessions()
154
+ # Ensure _initialized remains False to allow retrying
155
+ self._initialized = False
156
+ raise
157
+ else:
158
+ self._initialized = True
159
+ logger.info("Session pool initialized (no servers configured)")
160
+
161
+ def _extract_allowed_tools(
162
+ self,
163
+ server_name: str,
164
+ config: MCPConfiguration,
165
+ ) -> list[str] | None:
166
+ """Extract allowed_tools from config.
167
+
168
+ Args:
169
+ server_name (str): Name of the MCP server
170
+ config (MCPConfiguration): MCP server configuration
171
+
172
+ Returns:
173
+ list[str] | None: List of allowed tool names, or None if no restriction
174
+
175
+ Raises:
176
+ ValueError: If allowed_tools is not a list of strings
177
+ """
178
+ allowed_tools: list[str] | None = None
179
+
180
+ # Try object attribute first (avoids double property call from hasattr+getattr)
181
+ try:
182
+ raw_allowed = config.allowed_tools
183
+ # Use unified validation logic
184
+ allowed_tools = validate_allowed_tools_list(raw_allowed, f"Server '{server_name}'")
185
+ except AttributeError:
186
+ # Check if config is dict-like
187
+ if isinstance(config, dict):
188
+ allowed_tools = validate_allowed_tools_config(config, server_name)
189
+
190
+ if allowed_tools:
191
+ logger.debug(f"Server '{server_name}' has {len(allowed_tools)} allowed tools")
192
+ else:
193
+ logger.debug(f"Server '{server_name}' allows all tools (no restriction)")
194
+
195
+ return allowed_tools
196
+
197
+ async def _initialize_single_session(
198
+ self,
199
+ server_name: str,
200
+ config: MCPConfiguration,
201
+ allowed_tools: list[str] | None = None,
202
+ ) -> None:
203
+ """Initialize a single session (internal method).
204
+
205
+ Args:
206
+ server_name (str): Name of the MCP server
207
+ config (MCPConfiguration): MCP server configuration
208
+ allowed_tools (list[str] | None): Optional list of tool names to allow
209
+ """
210
+ try:
211
+ await self.get_or_create_session(server_name, config, allowed_tools)
212
+ logger.debug(f"Session initialized for {server_name}")
213
+ except Exception as e:
214
+ logger.debug(f"Failed to initialize session for {server_name}: {e}")
215
+ raise
216
+
217
+ def get_all_active_sessions(self) -> dict[str, PersistentMCPSession]:
218
+ """Get all active sessions.
219
+
220
+ Returns:
221
+ dict[str, PersistentMCPSession]: Dictionary of active sessions by server name
222
+ """
223
+ return {name: session for name, session in self.sessions.items() if session.is_initialized}
224
+
225
+ @property
226
+ def active_sessions(self) -> list[str]:
227
+ """Get list of active session names.
228
+
229
+ Returns:
230
+ list[str]: List of active session names
231
+ """
232
+ return [name for name, session in self.sessions.items() if session.is_initialized]
233
+
234
+ async def get_all_tools(self) -> list[Tool]:
235
+ """Get all cached tools from all active sessions.
236
+
237
+ Returns:
238
+ list[Tool]: List of all available tools across all sessions
239
+ """
240
+ # Create snapshot of sessions under lock to prevent race conditions
241
+ async with self._lock:
242
+ session_snapshot = dict(self.sessions.items())
243
+
244
+ # Prepare tasks for active sessions
245
+ server_names = list(session_snapshot.keys())
246
+ tasks = [session_snapshot[name].list_tools() for name in server_names]
247
+
248
+ results = []
249
+ if tasks:
250
+ results = await asyncio.gather(*tasks, return_exceptions=True)
251
+
252
+ all_tools: list[Tool] = []
253
+ for server_name, result in zip(server_names, results, strict=False):
254
+ if isinstance(result, Exception):
255
+ logger.warning(f"Failed to get tools from {server_name}: {result}")
256
+ else:
257
+ all_tools.extend(result)
258
+ logger.debug(f"Added {len(result)} tools from {server_name}")
259
+
260
+ logger.debug(f"Total tools available: {len(all_tools)}")
261
+ return all_tools
262
+
263
+ async def close_session(self, server_name: str) -> None:
264
+ """Close specific session.
265
+
266
+ Args:
267
+ server_name (str): Name of the server session to close
268
+ """
269
+ async with self._lock:
270
+ if server_name in self.sessions:
271
+ try:
272
+ logger.info(f"Closing session for {server_name}")
273
+ await self.sessions[server_name].disconnect()
274
+ except Exception as e:
275
+ logger.warning(f"Error closing session {server_name}: {e}")
276
+ finally:
277
+ del self.sessions[server_name]
278
+ logger.info(f"Session {server_name} removed from pool")
279
+
280
+ async def close_all_sessions(self) -> None:
281
+ """Close all sessions gracefully.
282
+
283
+ This method ensures all resources are cleaned up properly.
284
+ """
285
+ async with self._lock:
286
+ logger.info("Closing all sessions in pool")
287
+
288
+ # Create list of sessions to close (to avoid modification during iteration)
289
+ sessions_to_close = list(self.sessions.keys())
290
+
291
+ # Close sessions concurrently for faster shutdown
292
+ close_tasks = []
293
+ for server_name in sessions_to_close:
294
+ task = self._close_single_session(server_name)
295
+ close_tasks.append(task)
296
+
297
+ if close_tasks:
298
+ await asyncio.gather(*close_tasks, return_exceptions=True)
299
+
300
+ # Clear all session data
301
+ self.sessions.clear()
302
+ self._initialized = False
303
+
304
+ logger.info("All sessions closed and pool cleared")
305
+
306
+ async def _close_single_session(self, server_name: str) -> None:
307
+ """Close a single session (internal method).
308
+
309
+ Args:
310
+ server_name (str): Name of the server session to close
311
+ """
312
+ try:
313
+ if server_name in self.sessions:
314
+ await self.sessions[server_name].disconnect()
315
+ logger.debug(f"Session {server_name} closed successfully")
316
+ except Exception as e:
317
+ logger.warning(f"Error closing session {server_name}: {e}")
318
+
319
+ @property
320
+ def is_initialized(self) -> bool:
321
+ """Check if session pool is initialized.
322
+
323
+ Returns:
324
+ bool: True if initialized, False otherwise
325
+ """
326
+ return self._initialized
327
+
328
+ @property
329
+ def session_count(self) -> int:
330
+ """Get number of active sessions.
331
+
332
+ Returns:
333
+ int: Number of active sessions
334
+ """
335
+ return len(self.sessions)
336
+
337
+ def get_session(self, server_name: str) -> PersistentMCPSession:
338
+ """Get specific session by name.
339
+
340
+ Args:
341
+ server_name (str): Name of the server
342
+
343
+ Returns:
344
+ PersistentMCPSession: The requested session
345
+
346
+ Raises:
347
+ KeyError: If session doesn't exist
348
+ """
349
+ if server_name not in self.sessions:
350
+ raise KeyError(f"Session '{server_name}' not found")
351
+ return self.sessions[server_name]
@@ -0,0 +1,215 @@
1
+ """MCP Transport Handlers.
2
+
3
+ This module provides abstract and concrete transport classes for STDIO, SSE, and streamable HTTP.
4
+ Each transport handles connection establishment specific to its protocol.
5
+
6
+ Authors:
7
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
8
+ """
9
+
10
+ from abc import ABC, abstractmethod
11
+ from collections.abc import AsyncIterator
12
+ from enum import StrEnum
13
+ from typing import Any, Protocol
14
+
15
+ from gllm_tools.mcp.client.config import MCPConfiguration
16
+ from mcp.client.sse import sse_client
17
+ from mcp.client.stdio import StdioServerParameters, stdio_client
18
+ from mcp.client.streamable_http import streamablehttp_client
19
+
20
+ from aip_agents.utils.logger import get_logger
21
+
22
+
23
+ class TransportContext(Protocol):
24
+ """Protocol defining the interface for async context managers used in MCP transport connections."""
25
+
26
+ async def __aenter__(self):
27
+ """Enter the async context, establishing the connection and returning read/write streams."""
28
+ ...
29
+
30
+ async def __aexit__(self, _exc_type, _exc_val, _exc_tb):
31
+ """Exit the async context, performing cleanup and closing the connection.
32
+
33
+ Args:
34
+ _exc_type: Exception type if an exception occurred.
35
+ _exc_val: Exception value if an exception occurred.
36
+ _exc_tb: Exception traceback if an exception occurred.
37
+ """
38
+ ...
39
+
40
+
41
+ logger = get_logger(__name__)
42
+
43
+
44
+ DEFAULT_TIMEOUT: float = 30.0
45
+ """Default connection timeout in seconds."""
46
+
47
+
48
+ class TransportType(StrEnum):
49
+ """Enum for supported MCP transport types."""
50
+
51
+ HTTP = "http"
52
+ SSE = "sse"
53
+ STDIO = "stdio"
54
+
55
+
56
+ class Transport(ABC):
57
+ """Abstract base class for MCP transports."""
58
+
59
+ def __init__(self, server_name: str, config: MCPConfiguration) -> None:
60
+ """Initialize the transport.
61
+
62
+ Args:
63
+ server_name (str): Name of the MCP server.
64
+ config (MCPConfiguration): Configuration for the transport.
65
+ """
66
+ self.server_name = server_name
67
+ self.config = config
68
+ self.ctx: Any = None
69
+
70
+ @abstractmethod
71
+ async def connect(self) -> tuple[AsyncIterator[bytes], AsyncIterator[bytes], TransportContext]:
72
+ """Establish connection and return read/write streams and context manager.
73
+
74
+ Returns:
75
+ tuple[AsyncIterator[bytes], AsyncIterator[bytes], Any]:
76
+ (read_stream, write_stream, ctx)
77
+ Where:
78
+ - read_stream: AsyncIterator[bytes] for reading from the server.
79
+ - write_stream: AsyncIterator[bytes] for writing to the server.
80
+ - ctx: The async context manager instance for cleanup via __aexit__.
81
+
82
+ Raises:
83
+ ValueError: If required config (e.g., URL or command) is missing.
84
+ ConnectionError: If connection establishment fails.
85
+ """
86
+ pass
87
+
88
+ async def close(self) -> None:
89
+ """Clean up the transport connection."""
90
+ if self.ctx:
91
+ try:
92
+ await self.ctx.__aexit__(None, None, None)
93
+ except Exception as e:
94
+ logger.warning(f"Error during transport cleanup for {self.server_name}: {e}")
95
+
96
+
97
+ class SSETransport(Transport):
98
+ """SSE transport handler."""
99
+
100
+ async def connect(self) -> tuple[AsyncIterator[bytes], AsyncIterator[bytes], TransportContext]:
101
+ """Connect using SSE transport.
102
+
103
+ Builds SSE URL from config, initializes client with timeout, and enters context.
104
+
105
+ Returns:
106
+ tuple[AsyncIterator[bytes], AsyncIterator[bytes], Any]: (read_stream, write_stream, ctx)
107
+
108
+ Raises:
109
+ ValueError: If URL is missing.
110
+ ConnectionError: If SSE connection fails.
111
+ """
112
+ base_url = self.config.get("url", "").rstrip("/")
113
+ if not base_url:
114
+ raise ValueError("URL is required for SSE transport")
115
+
116
+ url = f"{base_url}/sse" if not base_url.endswith("/sse") else base_url
117
+ timeout = self.config.get("timeout", DEFAULT_TIMEOUT)
118
+ headers = self.config.get("headers", {})
119
+ logger.debug(f"Attempting SSE connection to {url} with headers: {list(headers.keys())}")
120
+ try:
121
+ self.ctx = sse_client(url=url, timeout=timeout, sse_read_timeout=300.0, headers=headers)
122
+ read_stream, write_stream = await self.ctx.__aenter__()
123
+ logger.info(f"Connected to {self.server_name} via SSE")
124
+ return read_stream, write_stream, self.ctx
125
+ except Exception as e:
126
+ raise ConnectionError(f"SSE connection failed for {self.server_name}: {str(e)}") from e
127
+
128
+
129
+ class HTTPTransport(Transport):
130
+ """Streamable HTTP transport handler."""
131
+
132
+ async def connect(self) -> tuple[AsyncIterator[bytes], AsyncIterator[bytes], TransportContext]:
133
+ """Connect using streamable HTTP transport.
134
+
135
+ Builds MCP URL from config, initializes client with timeout, and enters context.
136
+
137
+ Returns:
138
+ tuple[AsyncIterator[bytes], AsyncIterator[bytes], Any]: (read_stream, write_stream, ctx)
139
+
140
+ Raises:
141
+ ValueError: If URL is missing.
142
+ ConnectionError: If HTTP connection fails.
143
+ """
144
+ base_url = self.config.get("url", "").rstrip("/")
145
+ if not base_url:
146
+ raise ValueError("URL is required for HTTP transport")
147
+
148
+ url = f"{base_url}/mcp" if not base_url.endswith("/mcp") else base_url
149
+ timeout = self.config.get("timeout", DEFAULT_TIMEOUT)
150
+ headers = self.config.get("headers", {})
151
+ logger.debug(f"Attempting streamable HTTP connection to {url} with headers: {list(headers.keys())}")
152
+ try:
153
+ self.ctx = streamablehttp_client(url=url, timeout=timeout, headers=headers)
154
+ read_stream, write_stream, _ = await self.ctx.__aenter__()
155
+ logger.info(f"Connected to {self.server_name} via HTTP")
156
+ return read_stream, write_stream, self.ctx
157
+ except Exception as e:
158
+ raise ConnectionError(f"HTTP connection failed for {self.server_name}: {str(e)}") from e
159
+
160
+
161
+ class StdioTransport(Transport):
162
+ """STDIO transport handler."""
163
+
164
+ async def connect(self) -> tuple[AsyncIterator[bytes], AsyncIterator[bytes], TransportContext]:
165
+ """Connect using STDIO transport.
166
+
167
+ Initializes stdio client from command/args/env in config and enters context.
168
+
169
+ Returns:
170
+ tuple[AsyncIterator[bytes], AsyncIterator[bytes], Any]: (read_stream, write_stream, ctx)
171
+
172
+ Raises:
173
+ ValueError: If command is missing.
174
+ ConnectionError: If STDIO connection fails.
175
+ """
176
+ command = self.config.get("command")
177
+ args = self.config.get("args", [])
178
+ env = self.config.get("env")
179
+
180
+ if not command:
181
+ raise ValueError("Command is required for stdio transport")
182
+
183
+ logger.debug(f"Attempting stdio connection with command: {command}, args: {args}")
184
+ try:
185
+ stdio_params = StdioServerParameters(command=command, args=args, env=env)
186
+ self.ctx = stdio_client(stdio_params)
187
+ read_stream, write_stream = await self.ctx.__aenter__()
188
+ logger.info(f"Connected to {self.server_name} via STDIO")
189
+ return read_stream, write_stream, self.ctx
190
+ except Exception as e:
191
+ raise ConnectionError(f"STDIO connection failed for {self.server_name}: {str(e)}") from e
192
+
193
+
194
+ def create_transport(server_name: str, config: MCPConfiguration, transport_type: TransportType | str) -> Transport:
195
+ """Factory to create the appropriate transport instance.
196
+
197
+ Args:
198
+ server_name (str): Server name
199
+ config (MCPConfiguration): Config
200
+ transport_type (str): Transport type ('http', 'sse', 'stdio')
201
+
202
+ Returns:
203
+ Transport: Concrete transport instance
204
+
205
+ Raises:
206
+ ValueError: If transport_type is unsupported.
207
+ """
208
+ if transport_type == TransportType.HTTP:
209
+ return HTTPTransport(server_name, config)
210
+ elif transport_type == TransportType.SSE:
211
+ return SSETransport(server_name, config)
212
+ elif transport_type == TransportType.STDIO:
213
+ return StdioTransport(server_name, config)
214
+ else:
215
+ raise ValueError(f"Unsupported transport type: {transport_type}")
@@ -0,0 +1,7 @@
1
+ """MCP utility modules.
2
+
3
+ Authors:
4
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
5
+ """
6
+
7
+ __all__: list[str] = []