agno 2.0.1__py3-none-any.whl → 2.3.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.
Files changed (314) hide show
  1. agno/agent/agent.py +6015 -2823
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +594 -186
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +2 -8
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +72 -0
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +999 -519
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +103 -31
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +139 -0
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +59 -5
  142. agno/models/openai/chat.py +69 -29
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +77 -1
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -178
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +248 -94
  205. agno/run/base.py +44 -5
  206. agno/run/team.py +238 -97
  207. agno/run/workflow.py +144 -33
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1610
  213. agno/tools/dalle.py +2 -4
  214. agno/tools/decorator.py +4 -2
  215. agno/tools/duckduckgo.py +15 -11
  216. agno/tools/e2b.py +14 -7
  217. agno/tools/eleven_labs.py +23 -25
  218. agno/tools/exa.py +21 -16
  219. agno/tools/file.py +153 -23
  220. agno/tools/file_generation.py +350 -0
  221. agno/tools/firecrawl.py +4 -4
  222. agno/tools/function.py +250 -30
  223. agno/tools/gmail.py +238 -14
  224. agno/tools/google_drive.py +270 -0
  225. agno/tools/googlecalendar.py +36 -8
  226. agno/tools/googlesheets.py +20 -5
  227. agno/tools/jira.py +20 -0
  228. agno/tools/knowledge.py +3 -3
  229. agno/tools/mcp/__init__.py +10 -0
  230. agno/tools/mcp/mcp.py +331 -0
  231. agno/tools/mcp/multi_mcp.py +347 -0
  232. agno/tools/mcp/params.py +24 -0
  233. agno/tools/mcp_toolbox.py +284 -0
  234. agno/tools/mem0.py +11 -17
  235. agno/tools/memori.py +1 -53
  236. agno/tools/memory.py +419 -0
  237. agno/tools/models/nebius.py +5 -5
  238. agno/tools/models_labs.py +20 -10
  239. agno/tools/notion.py +204 -0
  240. agno/tools/parallel.py +314 -0
  241. agno/tools/scrapegraph.py +58 -31
  242. agno/tools/searxng.py +2 -2
  243. agno/tools/serper.py +2 -2
  244. agno/tools/slack.py +18 -3
  245. agno/tools/spider.py +2 -2
  246. agno/tools/tavily.py +146 -0
  247. agno/tools/whatsapp.py +1 -1
  248. agno/tools/workflow.py +278 -0
  249. agno/tools/yfinance.py +12 -11
  250. agno/utils/agent.py +820 -0
  251. agno/utils/audio.py +27 -0
  252. agno/utils/common.py +90 -1
  253. agno/utils/events.py +217 -2
  254. agno/utils/gemini.py +180 -22
  255. agno/utils/hooks.py +57 -0
  256. agno/utils/http.py +111 -0
  257. agno/utils/knowledge.py +12 -5
  258. agno/utils/log.py +1 -0
  259. agno/utils/mcp.py +92 -2
  260. agno/utils/media.py +188 -10
  261. agno/utils/merge_dict.py +22 -1
  262. agno/utils/message.py +60 -0
  263. agno/utils/models/claude.py +40 -11
  264. agno/utils/print_response/agent.py +105 -21
  265. agno/utils/print_response/team.py +103 -38
  266. agno/utils/print_response/workflow.py +251 -34
  267. agno/utils/reasoning.py +22 -1
  268. agno/utils/serialize.py +32 -0
  269. agno/utils/streamlit.py +16 -10
  270. agno/utils/string.py +41 -0
  271. agno/utils/team.py +98 -9
  272. agno/utils/tools.py +1 -1
  273. agno/vectordb/base.py +23 -4
  274. agno/vectordb/cassandra/cassandra.py +65 -9
  275. agno/vectordb/chroma/chromadb.py +182 -38
  276. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  277. agno/vectordb/couchbase/couchbase.py +105 -10
  278. agno/vectordb/lancedb/lance_db.py +124 -133
  279. agno/vectordb/langchaindb/langchaindb.py +25 -7
  280. agno/vectordb/lightrag/lightrag.py +17 -3
  281. agno/vectordb/llamaindex/__init__.py +3 -0
  282. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  283. agno/vectordb/milvus/milvus.py +126 -9
  284. agno/vectordb/mongodb/__init__.py +7 -1
  285. agno/vectordb/mongodb/mongodb.py +112 -7
  286. agno/vectordb/pgvector/pgvector.py +142 -21
  287. agno/vectordb/pineconedb/pineconedb.py +80 -8
  288. agno/vectordb/qdrant/qdrant.py +125 -39
  289. agno/vectordb/redis/__init__.py +9 -0
  290. agno/vectordb/redis/redisdb.py +694 -0
  291. agno/vectordb/singlestore/singlestore.py +111 -25
  292. agno/vectordb/surrealdb/surrealdb.py +31 -5
  293. agno/vectordb/upstashdb/upstashdb.py +76 -8
  294. agno/vectordb/weaviate/weaviate.py +86 -15
  295. agno/workflow/__init__.py +2 -0
  296. agno/workflow/agent.py +299 -0
  297. agno/workflow/condition.py +112 -18
  298. agno/workflow/loop.py +69 -10
  299. agno/workflow/parallel.py +266 -118
  300. agno/workflow/router.py +110 -17
  301. agno/workflow/step.py +638 -129
  302. agno/workflow/steps.py +65 -6
  303. agno/workflow/types.py +61 -23
  304. agno/workflow/workflow.py +2085 -272
  305. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
  306. agno-2.3.0.dist-info/RECORD +577 -0
  307. agno/knowledge/reader/url_reader.py +0 -128
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -610
  310. agno/utils/models/aws_claude.py +0 -170
  311. agno-2.0.1.dist-info/RECORD +0 -515
  312. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  313. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,347 @@
1
+ import weakref
2
+ from contextlib import AsyncExitStack
3
+ from dataclasses import asdict
4
+ from datetime import timedelta
5
+ from types import TracebackType
6
+ from typing import List, Literal, Optional, Union
7
+
8
+ from agno.tools import Toolkit
9
+ from agno.tools.function import Function
10
+ from agno.tools.mcp.params import SSEClientParams, StreamableHTTPClientParams
11
+ from agno.utils.log import log_debug, log_error, log_info, log_warning
12
+ from agno.utils.mcp import get_entrypoint_for_tool, prepare_command
13
+
14
+ try:
15
+ from mcp import ClientSession, StdioServerParameters
16
+ from mcp.client.sse import sse_client
17
+ from mcp.client.stdio import get_default_environment, stdio_client
18
+ from mcp.client.streamable_http import streamablehttp_client
19
+ except (ImportError, ModuleNotFoundError):
20
+ raise ImportError("`mcp` not installed. Please install using `pip install mcp`")
21
+
22
+
23
+ class MultiMCPTools(Toolkit):
24
+ """
25
+ A toolkit for integrating multiple Model Context Protocol (MCP) servers with Agno agents.
26
+ This allows agents to access tools, resources, and prompts exposed by MCP servers.
27
+
28
+ Can be used in three ways:
29
+ 1. Direct initialization with a ClientSession
30
+ 2. As an async context manager with StdioServerParameters
31
+ 3. As an async context manager with SSE or Streamable HTTP endpoints
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ commands: Optional[List[str]] = None,
37
+ urls: Optional[List[str]] = None,
38
+ urls_transports: Optional[List[Literal["sse", "streamable-http"]]] = None,
39
+ *,
40
+ env: Optional[dict[str, str]] = None,
41
+ server_params_list: Optional[
42
+ list[Union[SSEClientParams, StdioServerParameters, StreamableHTTPClientParams]]
43
+ ] = None,
44
+ timeout_seconds: int = 10,
45
+ client=None,
46
+ include_tools: Optional[list[str]] = None,
47
+ exclude_tools: Optional[list[str]] = None,
48
+ refresh_connection: bool = False,
49
+ allow_partial_failure: bool = False,
50
+ **kwargs,
51
+ ):
52
+ """
53
+ Initialize the MCP toolkit.
54
+
55
+ Args:
56
+ commands: List of commands to run to start the servers. Should be used in conjunction with env.
57
+ urls: List of URLs for SSE and/or Streamable HTTP endpoints.
58
+ urls_transports: List of transports to use for the given URLs.
59
+ server_params_list: List of StdioServerParameters or SSEClientParams or StreamableHTTPClientParams for creating new sessions.
60
+ env: The environment variables to pass to the servers. Should be used in conjunction with commands.
61
+ client: The underlying MCP client (optional, used to prevent garbage collection).
62
+ timeout_seconds: Timeout in seconds for managing timeouts for Client Session if Agent or Tool doesn't respond.
63
+ include_tools: Optional list of tool names to include (if None, includes all).
64
+ exclude_tools: Optional list of tool names to exclude (if None, excludes none).
65
+ allow_partial_failure: If True, allows toolkit to initialize even if some MCP servers fail to connect. If False, any failure will raise an exception.
66
+ refresh_connection: If True, the connection and tools will be refreshed on each run
67
+ """
68
+ super().__init__(name="MultiMCPTools", **kwargs)
69
+
70
+ if urls_transports is not None:
71
+ if "sse" in urls_transports:
72
+ log_info("SSE as a standalone transport is deprecated. Please use Streamable HTTP instead.")
73
+
74
+ if urls is not None:
75
+ if urls_transports is None:
76
+ log_warning(
77
+ "The default transport 'streamable-http' will be used. You can explicitly set the transports by providing the urls_transports parameter."
78
+ )
79
+ else:
80
+ if len(urls) != len(urls_transports):
81
+ raise ValueError("urls and urls_transports must be of the same length")
82
+
83
+ # Set these after `__init__` to bypass the `_check_tools_filters`
84
+ # beacuse tools are not available until `initialize()` is called.
85
+ self.include_tools = include_tools
86
+ self.exclude_tools = exclude_tools
87
+ self.refresh_connection = refresh_connection
88
+
89
+ if server_params_list is None and commands is None and urls is None:
90
+ raise ValueError("Either server_params_list or commands or urls must be provided")
91
+
92
+ self.server_params_list: List[Union[SSEClientParams, StdioServerParameters, StreamableHTTPClientParams]] = (
93
+ server_params_list or []
94
+ )
95
+ self.timeout_seconds = timeout_seconds
96
+ self.commands: Optional[List[str]] = commands
97
+ self.urls: Optional[List[str]] = urls
98
+ # Merge provided env with system env
99
+ if env is not None:
100
+ env = {
101
+ **get_default_environment(),
102
+ **env,
103
+ }
104
+ else:
105
+ env = get_default_environment()
106
+
107
+ if commands is not None:
108
+ for command in commands:
109
+ parts = prepare_command(command)
110
+ cmd = parts[0]
111
+ arguments = parts[1:] if len(parts) > 1 else []
112
+ self.server_params_list.append(StdioServerParameters(command=cmd, args=arguments, env=env))
113
+
114
+ if urls is not None:
115
+ if urls_transports is not None:
116
+ for url, transport in zip(urls, urls_transports):
117
+ if transport == "streamable-http":
118
+ self.server_params_list.append(StreamableHTTPClientParams(url=url))
119
+ else:
120
+ self.server_params_list.append(SSEClientParams(url=url))
121
+ else:
122
+ for url in urls:
123
+ self.server_params_list.append(StreamableHTTPClientParams(url=url))
124
+
125
+ self._async_exit_stack = AsyncExitStack()
126
+
127
+ self._client = client
128
+
129
+ self._initialized = False
130
+ self._connection_task = None
131
+ self._successful_connections = 0
132
+ self._sessions: list[ClientSession] = []
133
+
134
+ self.allow_partial_failure = allow_partial_failure
135
+
136
+ def cleanup():
137
+ """Cancel active connections"""
138
+ if self._connection_task and not self._connection_task.done():
139
+ self._connection_task.cancel()
140
+
141
+ # Setup cleanup logic before the instance is garbage collected
142
+ self._cleanup_finalizer = weakref.finalize(self, cleanup)
143
+
144
+ @property
145
+ def initialized(self) -> bool:
146
+ return self._initialized
147
+
148
+ async def is_alive(self) -> bool:
149
+ try:
150
+ for session in self._sessions:
151
+ await session.send_ping()
152
+ return True
153
+ except (RuntimeError, BaseException):
154
+ return False
155
+
156
+ async def connect(self, force: bool = False):
157
+ """Initialize a MultiMCPTools instance and connect to the MCP servers"""
158
+
159
+ if force:
160
+ # Clean up the session and context so we force a new connection
161
+ self._sessions = []
162
+ self._successful_connections = 0
163
+ self._initialized = False
164
+ self._connection_task = None
165
+
166
+ if self._initialized:
167
+ return
168
+
169
+ try:
170
+ await self._connect()
171
+ except (RuntimeError, BaseException) as e:
172
+ log_error(f"Failed to connect to {str(self)}: {e}")
173
+
174
+ @classmethod
175
+ async def create_and_connect(
176
+ cls,
177
+ commands: Optional[List[str]] = None,
178
+ urls: Optional[List[str]] = None,
179
+ urls_transports: Optional[List[Literal["sse", "streamable-http"]]] = None,
180
+ *,
181
+ env: Optional[dict[str, str]] = None,
182
+ server_params_list: Optional[
183
+ List[Union[SSEClientParams, StdioServerParameters, StreamableHTTPClientParams]]
184
+ ] = None,
185
+ timeout_seconds: int = 5,
186
+ client=None,
187
+ include_tools: Optional[list[str]] = None,
188
+ exclude_tools: Optional[list[str]] = None,
189
+ refresh_connection: bool = False,
190
+ **kwargs,
191
+ ) -> "MultiMCPTools":
192
+ """Initialize a MultiMCPTools instance and connect to the MCP servers"""
193
+ instance = cls(
194
+ commands=commands,
195
+ urls=urls,
196
+ urls_transports=urls_transports,
197
+ env=env,
198
+ server_params_list=server_params_list,
199
+ timeout_seconds=timeout_seconds,
200
+ client=client,
201
+ include_tools=include_tools,
202
+ exclude_tools=exclude_tools,
203
+ refresh_connection=refresh_connection,
204
+ **kwargs,
205
+ )
206
+
207
+ await instance._connect()
208
+ return instance
209
+
210
+ async def _connect(self) -> None:
211
+ """Connects to the MCP servers and initializes the tools"""
212
+ if self._initialized:
213
+ return
214
+
215
+ server_connection_errors = []
216
+
217
+ for server_params in self.server_params_list:
218
+ try:
219
+ # Handle stdio connections
220
+ if isinstance(server_params, StdioServerParameters):
221
+ stdio_transport = await self._async_exit_stack.enter_async_context(stdio_client(server_params))
222
+ read, write = stdio_transport
223
+ session = await self._async_exit_stack.enter_async_context(
224
+ ClientSession(read, write, read_timeout_seconds=timedelta(seconds=self.timeout_seconds))
225
+ )
226
+ await self.initialize(session)
227
+ self._successful_connections += 1
228
+
229
+ # Handle SSE connections
230
+ elif isinstance(server_params, SSEClientParams):
231
+ client_connection = await self._async_exit_stack.enter_async_context(
232
+ sse_client(**asdict(server_params))
233
+ )
234
+ read, write = client_connection
235
+ session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
236
+ await self.initialize(session)
237
+ self._successful_connections += 1
238
+
239
+ # Handle Streamable HTTP connections
240
+ elif isinstance(server_params, StreamableHTTPClientParams):
241
+ client_connection = await self._async_exit_stack.enter_async_context(
242
+ streamablehttp_client(**asdict(server_params))
243
+ )
244
+ read, write = client_connection[0:2]
245
+ session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
246
+ await self.initialize(session)
247
+ self._successful_connections += 1
248
+
249
+ except Exception as e:
250
+ if not self.allow_partial_failure:
251
+ raise ValueError(f"MCP connection failed: {e}")
252
+
253
+ log_error(f"Failed to initialize MCP server with params {server_params}: {e}")
254
+ server_connection_errors.append(str(e))
255
+ continue
256
+
257
+ if self._successful_connections > 0:
258
+ await self.build_tools()
259
+
260
+ if self._successful_connections == 0 and server_connection_errors:
261
+ raise ValueError(f"All MCP connections failed: {server_connection_errors}")
262
+
263
+ if not self._initialized and self._successful_connections > 0:
264
+ self._initialized = True
265
+
266
+ async def close(self) -> None:
267
+ """Close the MCP connections and clean up resources"""
268
+ if not self._initialized:
269
+ return
270
+
271
+ try:
272
+ await self._async_exit_stack.aclose()
273
+ self._sessions = []
274
+ self._successful_connections = 0
275
+
276
+ except (RuntimeError, BaseException) as e:
277
+ log_error(f"Failed to close MCP connections: {e}")
278
+
279
+ self._initialized = False
280
+
281
+ async def __aenter__(self) -> "MultiMCPTools":
282
+ """Enter the async context manager."""
283
+ try:
284
+ await self._connect()
285
+ except (RuntimeError, BaseException) as e:
286
+ log_error(f"Failed to connect to {str(self)}: {e}")
287
+ return self
288
+
289
+ async def __aexit__(
290
+ self,
291
+ exc_type: Union[type[BaseException], None],
292
+ exc_val: Union[BaseException, None],
293
+ exc_tb: Union[TracebackType, None],
294
+ ):
295
+ """Exit the async context manager."""
296
+ await self._async_exit_stack.aclose()
297
+ self._initialized = False
298
+ self._successful_connections = 0
299
+
300
+ async def build_tools(self) -> None:
301
+ for session in self._sessions:
302
+ # Get the list of tools from the MCP server
303
+ available_tools = await session.list_tools()
304
+
305
+ # Filter tools based on include/exclude lists
306
+ filtered_tools = []
307
+ for tool in available_tools.tools:
308
+ if self.exclude_tools and tool.name in self.exclude_tools:
309
+ continue
310
+ if self.include_tools is None or tool.name in self.include_tools:
311
+ filtered_tools.append(tool)
312
+
313
+ # Register the tools with the toolkit
314
+ for tool in filtered_tools:
315
+ try:
316
+ # Get an entrypoint for the tool
317
+ entrypoint = get_entrypoint_for_tool(tool, session)
318
+
319
+ # Create a Function for the tool
320
+ f = Function(
321
+ name=tool.name,
322
+ description=tool.description,
323
+ parameters=tool.inputSchema,
324
+ entrypoint=entrypoint,
325
+ # Set skip_entrypoint_processing to True to avoid processing the entrypoint
326
+ skip_entrypoint_processing=True,
327
+ )
328
+
329
+ # Register the Function with the toolkit
330
+ self.functions[f.name] = f
331
+ log_debug(f"Function: {f.name} registered with {self.name}")
332
+ except Exception as e:
333
+ log_error(f"Failed to register tool {tool.name}: {e}")
334
+ raise
335
+
336
+ async def initialize(self, session: ClientSession) -> None:
337
+ """Initialize the MCP toolkit by getting available tools from the MCP server"""
338
+
339
+ try:
340
+ # Initialize the session if not already initialized
341
+ await session.initialize()
342
+
343
+ self._sessions.append(session)
344
+ self._initialized = True
345
+ except Exception as e:
346
+ log_error(f"Failed to get MCP tools: {e}")
347
+ raise
@@ -0,0 +1,24 @@
1
+ from dataclasses import dataclass
2
+ from datetime import timedelta
3
+ from typing import Any, Dict, Optional
4
+
5
+
6
+ @dataclass
7
+ class SSEClientParams:
8
+ """Parameters for SSE client connection."""
9
+
10
+ url: str
11
+ headers: Optional[Dict[str, Any]] = None
12
+ timeout: Optional[float] = 5
13
+ sse_read_timeout: Optional[float] = 60 * 5
14
+
15
+
16
+ @dataclass
17
+ class StreamableHTTPClientParams:
18
+ """Parameters for Streamable HTTP client connection."""
19
+
20
+ url: str
21
+ headers: Optional[Dict[str, Any]] = None
22
+ timeout: Optional[timedelta] = timedelta(seconds=30)
23
+ sse_read_timeout: Optional[timedelta] = timedelta(seconds=60 * 5)
24
+ terminate_on_close: Optional[bool] = None
@@ -0,0 +1,284 @@
1
+ from typing import Any, Callable, Dict, List, Literal, Optional, Union
2
+ from warnings import warn
3
+
4
+ from agno.tools.function import Function
5
+ from agno.tools.mcp import MCPTools
6
+ from agno.utils.log import logger
7
+
8
+ try:
9
+ from toolbox_core import ToolboxClient # type: ignore
10
+ except ImportError:
11
+ raise ImportError("`toolbox_core` not installed. Please install using `pip install -U toolbox-core`.")
12
+
13
+
14
+ class MCPToolsMeta(type):
15
+ """Metaclass for MCPTools to ensure proper initialization with AgentOS"""
16
+
17
+ @property
18
+ def __name__(cls):
19
+ return "MCPTools"
20
+
21
+
22
+ class MCPToolbox(MCPTools, metaclass=MCPToolsMeta):
23
+ """
24
+ A toolkit that combines MCPTools server connectivity with MCP Toolbox for Databases client (toolbox-core).
25
+
26
+ MCPToolbox connects to an MCP Toolbox server and registers all available tools, then uses
27
+ toolbox-core to filter those tools by toolset or tool name. This enables agents to
28
+ receive only the specific tools they need while maintaining full MCP execution capabilities.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ url: str,
34
+ toolsets: Optional[List[str]] = None,
35
+ tool_name: Optional[str] = None,
36
+ headers: Optional[Dict[str, Any]] = None,
37
+ transport: Literal["stdio", "sse", "streamable-http"] = "streamable-http",
38
+ append_mcp_to_url: bool = True,
39
+ **kwargs,
40
+ ):
41
+ """Initialize MCPToolbox with filtering capabilities.
42
+
43
+ Args:
44
+ url (str): Base URL for the toolbox service.
45
+ toolsets (Optional[List[str]], optional): List of toolset names to filter tools by. Defaults to None.
46
+ tool_name (Optional[str], optional): Single tool name to load. Defaults to None.
47
+ headers (Optional[Dict[str, Any]], optional): Headers for toolbox-core client requests. Defaults to None.
48
+ transport (Literal["stdio", "sse", "streamable-http"], optional): MCP transport protocol. Defaults to "streamable-http".
49
+ append_mcp_to_url (bool, optional): Whether to append "/mcp" to the URL if it doesn't end with it. Defaults to True.
50
+
51
+ """
52
+ if append_mcp_to_url and not url.endswith("/mcp"):
53
+ url = url + "/mcp"
54
+
55
+ super().__init__(url=url, transport=transport, **kwargs)
56
+
57
+ self.name = "toolbox_client"
58
+ self.toolbox_url = url
59
+ self.toolsets = toolsets
60
+ self.tool_name = tool_name
61
+ self.headers = headers
62
+ self._core_client_initialized = False
63
+
64
+ # Validate that only one of toolsets or tool_name is provided
65
+ filter_params = [toolsets, tool_name]
66
+ non_none_params = [p for p in filter_params if p is not None]
67
+ if len(non_none_params) > 1:
68
+ raise ValueError("Only one of toolsets or tool_name can be specified")
69
+
70
+ async def connect(self):
71
+ """Initialize MCPToolbox instance and connect to the MCP server."""
72
+ # First, connect to MCP server and load all available tools
73
+ await super().connect()
74
+
75
+ if self._core_client_initialized:
76
+ return
77
+
78
+ # Then, connect to the ToolboxClient and filter tools based on toolsets or tool_name
79
+ await self._connect_toolbox_client()
80
+
81
+ async def _connect_toolbox_client(self):
82
+ try:
83
+ if self.toolsets is not None or self.tool_name is not None:
84
+ self.__core_client = ToolboxClient(
85
+ url=self.toolbox_url,
86
+ client_headers=self.headers,
87
+ )
88
+ self._core_client_initialized = True
89
+
90
+ if self.toolsets is not None:
91
+ # Load multiple toolsets
92
+ all_functions = await self.load_multiple_toolsets(toolset_names=self.toolsets)
93
+ # Replace functions dict with filtered subset
94
+ filtered_functions = {func.name: func for func in all_functions}
95
+ self.functions = filtered_functions
96
+ elif self.tool_name is not None:
97
+ tool = await self.load_tool(tool_name=self.tool_name)
98
+ # Replace functions dict with just this single tool
99
+ self.functions = {tool.name: tool}
100
+ except Exception as e:
101
+ raise RuntimeError(f"Failed to connect to ToolboxClient: {e}") from e
102
+
103
+ def _handle_auth_params(
104
+ self,
105
+ auth_token_getters: dict[str, Callable[[], str]] = {},
106
+ auth_tokens: Optional[dict[str, Callable[[], str]]] = None,
107
+ auth_headers: Optional[dict[str, Callable[[], str]]] = None,
108
+ ):
109
+ """handle authentication parameters for toolbox-core client"""
110
+ if auth_tokens:
111
+ if auth_token_getters:
112
+ warn(
113
+ "Both `auth_token_getters` and `auth_tokens` are provided. `auth_tokens` is deprecated, and `auth_token_getters` will be used.",
114
+ DeprecationWarning,
115
+ )
116
+ else:
117
+ warn(
118
+ "Argument `auth_tokens` is deprecated. Use `auth_token_getters` instead.",
119
+ DeprecationWarning,
120
+ )
121
+ auth_token_getters = auth_tokens
122
+
123
+ if auth_headers:
124
+ if auth_token_getters:
125
+ warn(
126
+ "Both `auth_token_getters` and `auth_headers` are provided. `auth_headers` is deprecated, and `auth_token_getters` will be used.",
127
+ DeprecationWarning,
128
+ )
129
+ else:
130
+ warn(
131
+ "Argument `auth_headers` is deprecated. Use `auth_token_getters` instead.",
132
+ DeprecationWarning,
133
+ )
134
+ auth_token_getters = auth_headers
135
+ return auth_token_getters
136
+
137
+ async def load_tool(
138
+ self,
139
+ tool_name: str,
140
+ auth_token_getters: dict[str, Callable[[], str]] = {},
141
+ auth_tokens: Optional[dict[str, Callable[[], str]]] = None,
142
+ auth_headers: Optional[dict[str, Callable[[], str]]] = None,
143
+ bound_params: dict[str, Union[Any, Callable[[], Any]]] = {},
144
+ ) -> Function:
145
+ """Loads the tool with the given tool name from the Toolbox service.
146
+
147
+ Args:
148
+ tool_name (str): The name of the tool to load.
149
+ auth_token_getters (dict[str, Callable[[], str]], optional): A mapping of authentication source names to functions that retrieve ID tokens. Defaults to {}.
150
+ auth_tokens (Optional[dict[str, Callable[[], str]]], optional): Deprecated. Use `auth_token_getters` instead.
151
+ auth_headers (Optional[dict[str, Callable[[], str]]], optional): Deprecated. Use `auth_token_getters` instead.
152
+ bound_params (dict[str, Union[Any, Callable[[], Any]]], optional): A mapping of parameter names to their bound values. Defaults to {}.
153
+
154
+ Raises:
155
+ RuntimeError: If the tool is not found in the MCP functions registry.
156
+
157
+ Returns:
158
+ Function: The loaded tool function.
159
+ """
160
+ auth_token_getters = self._handle_auth_params(
161
+ auth_token_getters=auth_token_getters,
162
+ auth_tokens=auth_tokens,
163
+ auth_headers=auth_headers,
164
+ )
165
+
166
+ core_sync_tool = await self.__core_client.load_tool(
167
+ name=tool_name,
168
+ auth_token_getters=auth_token_getters,
169
+ bound_params=bound_params,
170
+ )
171
+ # Return the Function object from our MCP functions registry
172
+ if core_sync_tool._name in self.functions:
173
+ return self.functions[core_sync_tool._name]
174
+ else:
175
+ raise RuntimeError(f"Tool '{tool_name}' was not found in MCP functions registry")
176
+
177
+ async def load_toolset(
178
+ self,
179
+ toolset_name: Optional[str] = None,
180
+ auth_token_getters: dict[str, Callable[[], str]] = {},
181
+ auth_tokens: Optional[dict[str, Callable[[], str]]] = None,
182
+ auth_headers: Optional[dict[str, Callable[[], str]]] = None,
183
+ bound_params: dict[str, Union[Any, Callable[[], Any]]] = {},
184
+ strict: bool = False,
185
+ ) -> List[Function]:
186
+ """Loads tools from the configured toolset.
187
+
188
+ Args:
189
+ toolset_name (Optional[str], optional): The name of the toolset to load. Defaults to None.
190
+ auth_token_getters (dict[str, Callable[[], str]], optional): A mapping of authentication source names to functions that retrieve ID tokens. Defaults to {}.
191
+ auth_tokens (Optional[dict[str, Callable[[], str]]], optional): Deprecated. Use `auth_token_getters` instead.
192
+ auth_headers (Optional[dict[str, Callable[[], str]]], optional): Deprecated. Use `auth_token_getters` instead.
193
+ bound_params (dict[str, Union[Any, Callable[[], Any]]], optional): A mapping of parameter names to their bound values. Defaults to {}.
194
+ strict (bool, optional): If True, raises an error if *any* loaded tool instance fails
195
+ to utilize all of the given parameters or auth tokens. (if any
196
+ provided). If False (default), raises an error only if a
197
+ user-provided parameter or auth token cannot be applied to *any*
198
+ loaded tool across the set.
199
+
200
+ Returns:
201
+ List[Function]: A list of all tools loaded from the Toolbox.
202
+ """
203
+ auth_token_getters = self._handle_auth_params(
204
+ auth_token_getters=auth_token_getters,
205
+ auth_tokens=auth_tokens,
206
+ auth_headers=auth_headers,
207
+ )
208
+
209
+ core_sync_tools = await self.__core_client.load_toolset(
210
+ name=toolset_name,
211
+ auth_token_getters=auth_token_getters,
212
+ bound_params=bound_params,
213
+ strict=strict,
214
+ )
215
+
216
+ tools = []
217
+ for core_sync_tool in core_sync_tools:
218
+ if core_sync_tool._name in self.functions:
219
+ tools.append(self.functions[core_sync_tool._name])
220
+ else:
221
+ logger.debug(f"Tool '{core_sync_tool._name}' from toolset '{toolset_name}' not available in MCP server")
222
+ return tools
223
+
224
+ async def load_multiple_toolsets(
225
+ self,
226
+ toolset_names: List[str],
227
+ auth_token_getters: dict[str, Callable[[], str]] = {},
228
+ bound_params: dict[str, Union[Any, Callable[[], Any]]] = {},
229
+ strict: bool = False,
230
+ ) -> List[Function]:
231
+ """Load tools from multiple toolsets.
232
+
233
+ Args:
234
+ toolset_names (List[str]): A list of toolset names to load.
235
+ auth_token_getters (dict[str, Callable[[], str]], optional): A mapping of authentication source names to functions that retrieve ID tokens. Defaults to {}.
236
+ bound_params (dict[str, Union[Any, Callable[[], Any]]], optional): A mapping of parameter names to their bound values. Defaults to {}.
237
+ strict (bool, optional): If True, raises an error if *any* loaded tool instance fails to utilize all of the given parameters or auth tokens. Defaults to False.
238
+
239
+ Returns:
240
+ List[Function]: A list of all tools loaded from the specified toolsets.
241
+ """
242
+ all_tools = []
243
+ for toolset_name in toolset_names:
244
+ tools = await self.load_toolset(
245
+ toolset_name=toolset_name,
246
+ auth_token_getters=auth_token_getters,
247
+ bound_params=bound_params,
248
+ strict=strict,
249
+ )
250
+ all_tools.extend(tools)
251
+ return all_tools
252
+
253
+ async def close(self):
254
+ """Close the underlying asynchronous client."""
255
+ if self._core_client_initialized and hasattr(self, "_MCPToolbox__core_client"):
256
+ await self.__core_client.close()
257
+ await super().close()
258
+
259
+ async def load_toolset_safe(self, toolset_name: str) -> List[str]:
260
+ """Safely load a toolset and return tool names."""
261
+ try:
262
+ tools = await self.load_toolset(toolset_name)
263
+ return [tool.name for tool in tools]
264
+ except Exception as e:
265
+ raise RuntimeError(f"Failed to load toolset '{toolset_name}': {e}") from e
266
+
267
+ def get_client(self) -> ToolboxClient:
268
+ """Get the underlying ToolboxClient."""
269
+ if not self._core_client_initialized:
270
+ raise RuntimeError("ToolboxClient not initialized. Call connect() first.")
271
+ return self.__core_client
272
+
273
+ async def __aenter__(self):
274
+ """Initialize the direct toolbox client."""
275
+ await super().__aenter__()
276
+ await self.connect()
277
+ return self
278
+
279
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
280
+ """Clean up the toolbox client."""
281
+ # Close ToolboxClient first, then MCP client
282
+ if self._core_client_initialized and hasattr(self, "_MCPToolbox__core_client"):
283
+ await self.__core_client.close()
284
+ await super().__aexit__(exc_type, exc_val, exc_tb)