agno 2.2.1__py3-none-any.whl → 2.2.2__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 (67) hide show
  1. agno/agent/agent.py +735 -574
  2. agno/culture/manager.py +22 -24
  3. agno/db/async_postgres/__init__.py +1 -1
  4. agno/db/dynamo/dynamo.py +0 -2
  5. agno/db/firestore/firestore.py +0 -2
  6. agno/db/gcs_json/gcs_json_db.py +0 -4
  7. agno/db/gcs_json/utils.py +0 -24
  8. agno/db/in_memory/in_memory_db.py +0 -3
  9. agno/db/json/json_db.py +4 -10
  10. agno/db/json/utils.py +0 -24
  11. agno/db/mongo/mongo.py +0 -2
  12. agno/db/mysql/mysql.py +0 -3
  13. agno/db/postgres/__init__.py +1 -1
  14. agno/db/{async_postgres → postgres}/async_postgres.py +19 -22
  15. agno/db/postgres/postgres.py +7 -10
  16. agno/db/postgres/utils.py +106 -2
  17. agno/db/redis/redis.py +0 -2
  18. agno/db/singlestore/singlestore.py +0 -3
  19. agno/db/sqlite/__init__.py +2 -1
  20. agno/db/sqlite/async_sqlite.py +2269 -0
  21. agno/db/sqlite/sqlite.py +0 -2
  22. agno/db/sqlite/utils.py +96 -0
  23. agno/db/surrealdb/surrealdb.py +0 -6
  24. agno/knowledge/knowledge.py +3 -3
  25. agno/knowledge/reader/reader_factory.py +16 -0
  26. agno/knowledge/reader/tavily_reader.py +194 -0
  27. agno/memory/manager.py +28 -25
  28. agno/models/anthropic/claude.py +63 -6
  29. agno/models/base.py +251 -32
  30. agno/models/response.py +69 -0
  31. agno/os/router.py +7 -5
  32. agno/os/routers/memory/memory.py +2 -1
  33. agno/os/routers/memory/schemas.py +5 -2
  34. agno/os/schema.py +26 -20
  35. agno/os/utils.py +9 -2
  36. agno/run/agent.py +23 -30
  37. agno/run/base.py +17 -1
  38. agno/run/team.py +23 -29
  39. agno/run/workflow.py +17 -12
  40. agno/session/agent.py +3 -0
  41. agno/session/summary.py +4 -1
  42. agno/session/team.py +1 -1
  43. agno/team/team.py +591 -365
  44. agno/tools/dalle.py +2 -4
  45. agno/tools/eleven_labs.py +23 -25
  46. agno/tools/function.py +40 -0
  47. agno/tools/mcp/__init__.py +10 -0
  48. agno/tools/mcp/mcp.py +324 -0
  49. agno/tools/mcp/multi_mcp.py +347 -0
  50. agno/tools/mcp/params.py +24 -0
  51. agno/tools/slack.py +18 -3
  52. agno/tools/tavily.py +146 -0
  53. agno/utils/agent.py +366 -1
  54. agno/utils/mcp.py +92 -2
  55. agno/utils/media.py +166 -1
  56. agno/utils/print_response/workflow.py +17 -1
  57. agno/utils/team.py +89 -1
  58. agno/workflow/step.py +0 -1
  59. agno/workflow/types.py +10 -15
  60. {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/METADATA +28 -25
  61. {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/RECORD +64 -61
  62. agno/db/async_postgres/schemas.py +0 -139
  63. agno/db/async_postgres/utils.py +0 -347
  64. agno/tools/mcp.py +0 -679
  65. {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/WHEEL +0 -0
  66. {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/licenses/LICENSE +0 -0
  67. {agno-2.2.1.dist-info → agno-2.2.2.dist-info}/top_level.txt +0 -0
agno/tools/mcp.py DELETED
@@ -1,679 +0,0 @@
1
- import asyncio
2
- import weakref
3
- from contextlib import AsyncExitStack
4
- from dataclasses import asdict, dataclass
5
- from datetime import timedelta
6
- from types import TracebackType
7
- from typing import Any, Dict, List, Literal, Optional, Union
8
-
9
- from agno.tools import Toolkit
10
- from agno.tools.function import Function
11
- from agno.utils.log import log_debug, log_error, log_info, log_warning
12
- from agno.utils.mcp import get_entrypoint_for_tool
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
- def _prepare_command(command: str) -> list[str]:
24
- """Sanitize a command and split it into parts before using it to run a MCP server."""
25
- import os
26
- import shutil
27
- from shlex import split
28
-
29
- # Block dangerous characters
30
- if any(char in command for char in ["&", "|", ";", "`", "$", "(", ")"]):
31
- raise ValueError("MCP command can't contain shell metacharacters")
32
-
33
- parts = split(command)
34
- if not parts:
35
- raise ValueError("MCP command can't be empty")
36
-
37
- # Only allow specific executables
38
- ALLOWED_COMMANDS = {
39
- # Python
40
- "python",
41
- "python3",
42
- "uv",
43
- "uvx",
44
- "pipx",
45
- # Node
46
- "node",
47
- "npm",
48
- "npx",
49
- "yarn",
50
- "pnpm",
51
- "bun",
52
- # Other runtimes
53
- "deno",
54
- "java",
55
- "ruby",
56
- "docker",
57
- }
58
-
59
- executable = parts[0].split("/")[-1]
60
-
61
- # Check if it's a relative path starting with ./ or ../
62
- if executable.startswith("./") or executable.startswith("../"):
63
- # Allow relative paths to binaries
64
- return parts
65
-
66
- # Check if it's an absolute path to a binary
67
- if executable.startswith("/") and os.path.isfile(executable):
68
- # Allow absolute paths to existing files
69
- return parts
70
-
71
- # Check if it's a binary in current directory without ./
72
- if "/" not in executable and os.path.isfile(executable):
73
- # Allow binaries in current directory
74
- return parts
75
-
76
- # Check if it's a binary in PATH
77
- if shutil.which(executable):
78
- return parts
79
-
80
- if executable not in ALLOWED_COMMANDS:
81
- raise ValueError(f"MCP command needs to use one of the following executables: {ALLOWED_COMMANDS}")
82
-
83
- first_part = parts[0]
84
- executable = first_part.split("/")[-1]
85
-
86
- # Allow known commands
87
- if executable in ALLOWED_COMMANDS:
88
- return parts
89
-
90
- # Allow relative paths to custom binaries
91
- if first_part.startswith(("./", "../")):
92
- return parts
93
-
94
- # Allow absolute paths to existing files
95
- if first_part.startswith("/") and os.path.isfile(first_part):
96
- return parts
97
-
98
- # Allow binaries in current directory without ./
99
- if "/" not in first_part and os.path.isfile(first_part):
100
- return parts
101
-
102
- # Allow binaries in PATH
103
- if shutil.which(first_part):
104
- return parts
105
-
106
- raise ValueError(f"MCP command needs to use one of the following executables: {ALLOWED_COMMANDS}")
107
-
108
-
109
- @dataclass
110
- class SSEClientParams:
111
- """Parameters for SSE client connection."""
112
-
113
- url: str
114
- headers: Optional[Dict[str, Any]] = None
115
- timeout: Optional[float] = 5
116
- sse_read_timeout: Optional[float] = 60 * 5
117
-
118
-
119
- @dataclass
120
- class StreamableHTTPClientParams:
121
- """Parameters for Streamable HTTP client connection."""
122
-
123
- url: str
124
- headers: Optional[Dict[str, Any]] = None
125
- timeout: Optional[timedelta] = timedelta(seconds=30)
126
- sse_read_timeout: Optional[timedelta] = timedelta(seconds=60 * 5)
127
- terminate_on_close: Optional[bool] = None
128
-
129
-
130
- class MCPTools(Toolkit):
131
- """
132
- A toolkit for integrating Model Context Protocol (MCP) servers with Agno agents.
133
- This allows agents to access tools, resources, and prompts exposed by MCP servers.
134
-
135
- Can be used in three ways:
136
- 1. Direct initialization with a ClientSession
137
- 2. As an async context manager with StdioServerParameters
138
- 3. As an async context manager with SSE or Streamable HTTP client parameters
139
- """
140
-
141
- def __init__(
142
- self,
143
- command: Optional[str] = None,
144
- *,
145
- url: Optional[str] = None,
146
- env: Optional[dict[str, str]] = None,
147
- transport: Literal["stdio", "sse", "streamable-http"] = "stdio",
148
- server_params: Optional[Union[StdioServerParameters, SSEClientParams, StreamableHTTPClientParams]] = None,
149
- session: Optional[ClientSession] = None,
150
- timeout_seconds: int = 10,
151
- client=None,
152
- include_tools: Optional[list[str]] = None,
153
- exclude_tools: Optional[list[str]] = None,
154
- **kwargs,
155
- ):
156
- """
157
- Initialize the MCP toolkit.
158
-
159
- Args:
160
- session: An initialized MCP ClientSession connected to an MCP server
161
- server_params: Parameters for creating a new session
162
- command: The command to run to start the server. Should be used in conjunction with env.
163
- url: The URL endpoint for SSE or Streamable HTTP connection when transport is "sse" or "streamable-http".
164
- env: The environment variables to pass to the server. Should be used in conjunction with command.
165
- client: The underlying MCP client (optional, used to prevent garbage collection)
166
- timeout_seconds: Read timeout in seconds for the MCP client
167
- include_tools: Optional list of tool names to include (if None, includes all)
168
- exclude_tools: Optional list of tool names to exclude (if None, excludes none)
169
- transport: The transport protocol to use, either "stdio" or "sse" or "streamable-http"
170
- """
171
- super().__init__(name="MCPTools", **kwargs)
172
-
173
- if transport == "sse":
174
- log_info("SSE as a standalone transport is deprecated. Please use Streamable HTTP instead.")
175
-
176
- # Set these after `__init__` to bypass the `_check_tools_filters`
177
- # because tools are not available until `initialize()` is called.
178
- self.include_tools = include_tools
179
- self.exclude_tools = exclude_tools
180
-
181
- if session is None and server_params is None:
182
- if transport == "sse" and url is None:
183
- raise ValueError("One of 'url' or 'server_params' parameters must be provided when using SSE transport")
184
- if transport == "stdio" and command is None:
185
- raise ValueError(
186
- "One of 'command' or 'server_params' parameters must be provided when using stdio transport"
187
- )
188
- if transport == "streamable-http" and url is None:
189
- raise ValueError(
190
- "One of 'url' or 'server_params' parameters must be provided when using Streamable HTTP transport"
191
- )
192
-
193
- # Ensure the received server_params are valid for the given transport
194
- if server_params is not None:
195
- if transport == "sse":
196
- if not isinstance(server_params, SSEClientParams):
197
- raise ValueError(
198
- "If using the SSE transport, server_params must be an instance of SSEClientParams."
199
- )
200
- elif transport == "stdio":
201
- if not isinstance(server_params, StdioServerParameters):
202
- raise ValueError(
203
- "If using the stdio transport, server_params must be an instance of StdioServerParameters."
204
- )
205
- elif transport == "streamable-http":
206
- if not isinstance(server_params, StreamableHTTPClientParams):
207
- raise ValueError(
208
- "If using the streamable-http transport, server_params must be an instance of StreamableHTTPClientParams."
209
- )
210
-
211
- self.timeout_seconds = timeout_seconds
212
- self.session: Optional[ClientSession] = session
213
- self.server_params: Optional[Union[StdioServerParameters, SSEClientParams, StreamableHTTPClientParams]] = (
214
- server_params
215
- )
216
- self.transport = transport
217
- self.url = url
218
-
219
- # Merge provided env with system env
220
- if env is not None:
221
- env = {
222
- **get_default_environment(),
223
- **env,
224
- }
225
- else:
226
- env = get_default_environment()
227
-
228
- if command is not None and transport not in ["sse", "streamable-http"]:
229
- parts = _prepare_command(command)
230
- cmd = parts[0]
231
- arguments = parts[1:] if len(parts) > 1 else []
232
- self.server_params = StdioServerParameters(command=cmd, args=arguments, env=env)
233
-
234
- self._client = client
235
- self._context = None
236
- self._session_context = None
237
- self._initialized = False
238
- self._connection_task = None
239
-
240
- def cleanup():
241
- """Cancel active connections"""
242
- if self._connection_task and not self._connection_task.done():
243
- self._connection_task.cancel()
244
-
245
- # Setup cleanup logic before the instance is garbage collected
246
- self._cleanup_finalizer = weakref.finalize(self, cleanup)
247
-
248
- async def connect(self):
249
- """Initialize a MCPTools instance and connect to the contextual MCP server"""
250
- if self._initialized:
251
- return
252
-
253
- await self._connect()
254
-
255
- def _start_connection(self):
256
- """Ensure there are no active connections and setup a new one"""
257
- if self._connection_task is None or self._connection_task.done():
258
- self._connection_task = asyncio.create_task(self._connect()) # type: ignore
259
-
260
- async def _connect(self) -> None:
261
- """Connects to the MCP server and initializes the tools"""
262
- if self._initialized:
263
- return
264
-
265
- if self.session is not None:
266
- await self.initialize()
267
- return
268
-
269
- if not hasattr(self, "_active_contexts"):
270
- self._active_contexts: list[Any] = []
271
-
272
- # Create a new studio session
273
- if self.transport == "sse":
274
- sse_params = asdict(self.server_params) if self.server_params is not None else {} # type: ignore
275
- if "url" not in sse_params:
276
- sse_params["url"] = self.url
277
- self._context = sse_client(**sse_params) # type: ignore
278
- client_timeout = min(self.timeout_seconds, sse_params.get("timeout", self.timeout_seconds))
279
-
280
- # Create a new streamable HTTP session
281
- elif self.transport == "streamable-http":
282
- streamable_http_params = asdict(self.server_params) if self.server_params is not None else {} # type: ignore
283
- if "url" not in streamable_http_params:
284
- streamable_http_params["url"] = self.url
285
- self._context = streamablehttp_client(**streamable_http_params) # type: ignore
286
- params_timeout = streamable_http_params.get("timeout", self.timeout_seconds)
287
- if isinstance(params_timeout, timedelta):
288
- params_timeout = int(params_timeout.total_seconds())
289
- client_timeout = min(self.timeout_seconds, params_timeout)
290
-
291
- else:
292
- if self.server_params is None:
293
- raise ValueError("server_params must be provided when using stdio transport.")
294
- self._context = stdio_client(self.server_params) # type: ignore
295
- client_timeout = self.timeout_seconds
296
-
297
- session_params = await self._context.__aenter__() # type: ignore
298
- self._active_contexts.append(self._context)
299
- read, write = session_params[0:2]
300
-
301
- self._session_context = ClientSession(read, write, read_timeout_seconds=timedelta(seconds=client_timeout)) # type: ignore
302
- self.session = await self._session_context.__aenter__() # type: ignore
303
- self._active_contexts.append(self._session_context)
304
-
305
- # Initialize with the new session
306
- await self.initialize()
307
-
308
- async def close(self) -> None:
309
- """Close the MCP connection and clean up resources"""
310
- if self._session_context is not None:
311
- await self._session_context.__aexit__(None, None, None)
312
- self.session = None
313
- self._session_context = None
314
-
315
- if self._context is not None:
316
- await self._context.__aexit__(None, None, None)
317
- self._context = None
318
-
319
- self._initialized = False
320
-
321
- async def __aenter__(self) -> "MCPTools":
322
- await self._connect()
323
- return self
324
-
325
- async def __aexit__(self, _exc_type, _exc_val, _exc_tb):
326
- """Exit the async context manager."""
327
- if self._session_context is not None:
328
- await self._session_context.__aexit__(_exc_type, _exc_val, _exc_tb)
329
- self.session = None
330
- self._session_context = None
331
-
332
- if self._context is not None:
333
- await self._context.__aexit__(_exc_type, _exc_val, _exc_tb)
334
- self._context = None
335
-
336
- self._initialized = False
337
-
338
- async def initialize(self) -> None:
339
- """Initialize the MCP toolkit by getting available tools from the MCP server"""
340
- if self._initialized:
341
- return
342
-
343
- try:
344
- if self.session is None:
345
- raise ValueError("Failed to establish session connection")
346
-
347
- # Initialize the session if not already initialized
348
- await self.session.initialize()
349
-
350
- # Get the list of tools from the MCP server
351
- available_tools = await self.session.list_tools()
352
-
353
- self._check_tools_filters(
354
- available_tools=[tool.name for tool in available_tools.tools],
355
- include_tools=self.include_tools,
356
- exclude_tools=self.exclude_tools,
357
- )
358
-
359
- # Filter tools based on include/exclude lists
360
- filtered_tools = []
361
- for tool in available_tools.tools:
362
- if self.exclude_tools and tool.name in self.exclude_tools:
363
- continue
364
- if self.include_tools is None or tool.name in self.include_tools:
365
- filtered_tools.append(tool)
366
-
367
- # Register the tools with the toolkit
368
- for tool in filtered_tools:
369
- try:
370
- # Get an entrypoint for the tool
371
- entrypoint = get_entrypoint_for_tool(tool, self.session)
372
- # Create a Function for the tool
373
- f = Function(
374
- name=tool.name,
375
- description=tool.description,
376
- parameters=tool.inputSchema,
377
- entrypoint=entrypoint,
378
- # Set skip_entrypoint_processing to True to avoid processing the entrypoint
379
- skip_entrypoint_processing=True,
380
- )
381
-
382
- # Register the Function with the toolkit
383
- self.functions[f.name] = f
384
- log_debug(f"Function: {f.name} registered with {self.name}")
385
- except Exception as e:
386
- log_error(f"Failed to register tool {tool.name}: {e}")
387
-
388
- log_debug(f"{self.name} initialized with {len(filtered_tools)} tools")
389
- self._initialized = True
390
-
391
- except Exception as e:
392
- log_error(f"Failed to get MCP tools: {e}")
393
- raise
394
-
395
-
396
- class MultiMCPTools(Toolkit):
397
- """
398
- A toolkit for integrating multiple Model Context Protocol (MCP) servers with Agno agents.
399
- This allows agents to access tools, resources, and prompts exposed by MCP servers.
400
-
401
- Can be used in three ways:
402
- 1. Direct initialization with a ClientSession
403
- 2. As an async context manager with StdioServerParameters
404
- 3. As an async context manager with SSE or Streamable HTTP endpoints
405
- """
406
-
407
- def __init__(
408
- self,
409
- commands: Optional[List[str]] = None,
410
- urls: Optional[List[str]] = None,
411
- urls_transports: Optional[List[Literal["sse", "streamable-http"]]] = None,
412
- *,
413
- env: Optional[dict[str, str]] = None,
414
- server_params_list: Optional[
415
- list[Union[SSEClientParams, StdioServerParameters, StreamableHTTPClientParams]]
416
- ] = None,
417
- timeout_seconds: int = 5,
418
- client=None,
419
- include_tools: Optional[list[str]] = None,
420
- exclude_tools: Optional[list[str]] = None,
421
- allow_partial_failure: bool = False,
422
- **kwargs,
423
- ):
424
- """
425
- Initialize the MCP toolkit.
426
-
427
- Args:
428
- commands: List of commands to run to start the servers. Should be used in conjunction with env.
429
- urls: List of URLs for SSE and/or Streamable HTTP endpoints.
430
- urls_transports: List of transports to use for the given URLs.
431
- server_params_list: List of StdioServerParameters or SSEClientParams or StreamableHTTPClientParams for creating new sessions.
432
- env: The environment variables to pass to the servers. Should be used in conjunction with commands.
433
- client: The underlying MCP client (optional, used to prevent garbage collection).
434
- timeout_seconds: Timeout in seconds for managing timeouts for Client Session if Agent or Tool doesn't respond.
435
- include_tools: Optional list of tool names to include (if None, includes all).
436
- exclude_tools: Optional list of tool names to exclude (if None, excludes none).
437
- 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.
438
- """
439
- super().__init__(name="MultiMCPTools", **kwargs)
440
-
441
- if urls_transports is not None:
442
- if "sse" in urls_transports:
443
- log_info("SSE as a standalone transport is deprecated. Please use Streamable HTTP instead.")
444
-
445
- if urls is not None:
446
- if urls_transports is None:
447
- log_warning(
448
- "The default transport 'streamable-http' will be used. You can explicitly set the transports by providing the urls_transports parameter."
449
- )
450
- else:
451
- if len(urls) != len(urls_transports):
452
- raise ValueError("urls and urls_transports must be of the same length")
453
-
454
- # Set these after `__init__` to bypass the `_check_tools_filters`
455
- # beacuse tools are not available until `initialize()` is called.
456
- self.include_tools = include_tools
457
- self.exclude_tools = exclude_tools
458
-
459
- if server_params_list is None and commands is None and urls is None:
460
- raise ValueError("Either server_params_list or commands or urls must be provided")
461
-
462
- self.server_params_list: List[Union[SSEClientParams, StdioServerParameters, StreamableHTTPClientParams]] = (
463
- server_params_list or []
464
- )
465
- self.timeout_seconds = timeout_seconds
466
- self.commands: Optional[List[str]] = commands
467
- self.urls: Optional[List[str]] = urls
468
- # Merge provided env with system env
469
- if env is not None:
470
- env = {
471
- **get_default_environment(),
472
- **env,
473
- }
474
- else:
475
- env = get_default_environment()
476
-
477
- if commands is not None:
478
- for command in commands:
479
- parts = _prepare_command(command)
480
- cmd = parts[0]
481
- arguments = parts[1:] if len(parts) > 1 else []
482
- self.server_params_list.append(StdioServerParameters(command=cmd, args=arguments, env=env))
483
-
484
- if urls is not None:
485
- if urls_transports is not None:
486
- for url, transport in zip(urls, urls_transports):
487
- if transport == "streamable-http":
488
- self.server_params_list.append(StreamableHTTPClientParams(url=url))
489
- else:
490
- self.server_params_list.append(SSEClientParams(url=url))
491
- else:
492
- for url in urls:
493
- self.server_params_list.append(StreamableHTTPClientParams(url=url))
494
-
495
- self._async_exit_stack = AsyncExitStack()
496
- self._successful_connections = 0
497
-
498
- self._initialized = False
499
- self._connection_task = None
500
- self._active_contexts: list[Any] = []
501
- self._used_as_context_manager = False
502
-
503
- self._client = client
504
- self._initialized = False
505
- self.allow_partial_failure = allow_partial_failure
506
-
507
- def cleanup():
508
- """Cancel active connections"""
509
- if self._connection_task and not self._connection_task.done():
510
- self._connection_task.cancel()
511
-
512
- # Setup cleanup logic before the instance is garbage collected
513
- self._cleanup_finalizer = weakref.finalize(self, cleanup)
514
-
515
- async def connect(self):
516
- """Initialize a MultiMCPTools instance and connect to the MCP servers"""
517
- if self._initialized:
518
- return
519
-
520
- await self._connect()
521
-
522
- @classmethod
523
- async def create_and_connect(
524
- cls,
525
- commands: Optional[List[str]] = None,
526
- urls: Optional[List[str]] = None,
527
- urls_transports: Optional[List[Literal["sse", "streamable-http"]]] = None,
528
- *,
529
- env: Optional[dict[str, str]] = None,
530
- server_params_list: Optional[
531
- List[Union[SSEClientParams, StdioServerParameters, StreamableHTTPClientParams]]
532
- ] = None,
533
- timeout_seconds: int = 5,
534
- client=None,
535
- include_tools: Optional[list[str]] = None,
536
- exclude_tools: Optional[list[str]] = None,
537
- **kwargs,
538
- ) -> "MultiMCPTools":
539
- """Initialize a MultiMCPTools instance and connect to the MCP servers"""
540
- instance = cls(
541
- commands=commands,
542
- urls=urls,
543
- urls_transports=urls_transports,
544
- env=env,
545
- server_params_list=server_params_list,
546
- timeout_seconds=timeout_seconds,
547
- client=client,
548
- include_tools=include_tools,
549
- exclude_tools=exclude_tools,
550
- **kwargs,
551
- )
552
-
553
- await instance._connect()
554
- return instance
555
-
556
- def _start_connection(self):
557
- """Ensure there are no active connections and setup a new one"""
558
- if self._connection_task is None or self._connection_task.done():
559
- self._connection_task = asyncio.create_task(self._connect()) # type: ignore
560
-
561
- async def _connect(self) -> None:
562
- """Connects to the MCP servers and initializes the tools"""
563
- if self._initialized:
564
- return
565
-
566
- server_connection_errors = []
567
-
568
- for server_params in self.server_params_list:
569
- try:
570
- # Handle stdio connections
571
- if isinstance(server_params, StdioServerParameters):
572
- stdio_transport = await self._async_exit_stack.enter_async_context(stdio_client(server_params))
573
- read, write = stdio_transport
574
- session = await self._async_exit_stack.enter_async_context(
575
- ClientSession(read, write, read_timeout_seconds=timedelta(seconds=self.timeout_seconds))
576
- )
577
- await self.initialize(session)
578
- self._successful_connections += 1
579
-
580
- # Handle SSE connections
581
- elif isinstance(server_params, SSEClientParams):
582
- client_connection = await self._async_exit_stack.enter_async_context(
583
- sse_client(**asdict(server_params))
584
- )
585
- read, write = client_connection
586
- session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
587
- await self.initialize(session)
588
- self._successful_connections += 1
589
-
590
- # Handle Streamable HTTP connections
591
- elif isinstance(server_params, StreamableHTTPClientParams):
592
- client_connection = await self._async_exit_stack.enter_async_context(
593
- streamablehttp_client(**asdict(server_params))
594
- )
595
- read, write = client_connection[0:2]
596
- session = await self._async_exit_stack.enter_async_context(ClientSession(read, write))
597
- await self.initialize(session)
598
- self._successful_connections += 1
599
-
600
- except Exception as e:
601
- if not self.allow_partial_failure:
602
- raise ValueError(f"MCP connection failed: {e}")
603
-
604
- log_error(f"Failed to initialize MCP server with params {server_params}: {e}")
605
- server_connection_errors.append(str(e))
606
- continue
607
-
608
- if self._successful_connections == 0 and server_connection_errors:
609
- raise ValueError(f"All MCP connections failed: {server_connection_errors}")
610
-
611
- if not self._initialized and self._successful_connections > 0:
612
- self._initialized = True
613
-
614
- async def close(self) -> None:
615
- """Close the MCP connections and clean up resources"""
616
- await self._async_exit_stack.aclose()
617
- self._initialized = False
618
-
619
- async def __aenter__(self) -> "MultiMCPTools":
620
- """Enter the async context manager."""
621
- await self._connect()
622
- return self
623
-
624
- async def __aexit__(
625
- self,
626
- exc_type: Union[type[BaseException], None],
627
- exc_val: Union[BaseException, None],
628
- exc_tb: Union[TracebackType, None],
629
- ):
630
- """Exit the async context manager."""
631
- await self._async_exit_stack.aclose()
632
- self._initialized = False
633
- self._successful_connections = 0
634
-
635
- async def initialize(self, session: ClientSession) -> None:
636
- """Initialize the MCP toolkit by getting available tools from the MCP server"""
637
-
638
- try:
639
- # Initialize the session if not already initialized
640
- await session.initialize()
641
-
642
- # Get the list of tools from the MCP server
643
- available_tools = await session.list_tools()
644
-
645
- # Filter tools based on include/exclude lists
646
- filtered_tools = []
647
- for tool in available_tools.tools:
648
- if self.exclude_tools and tool.name in self.exclude_tools:
649
- continue
650
- if self.include_tools is None or tool.name in self.include_tools:
651
- filtered_tools.append(tool)
652
-
653
- # Register the tools with the toolkit
654
- for tool in filtered_tools:
655
- try:
656
- # Get an entrypoint for the tool
657
- entrypoint = get_entrypoint_for_tool(tool, session)
658
-
659
- # Create a Function for the tool
660
- f = Function(
661
- name=tool.name,
662
- description=tool.description,
663
- parameters=tool.inputSchema,
664
- entrypoint=entrypoint,
665
- # Set skip_entrypoint_processing to True to avoid processing the entrypoint
666
- skip_entrypoint_processing=True,
667
- )
668
-
669
- # Register the Function with the toolkit
670
- self.functions[f.name] = f
671
- log_debug(f"Function: {f.name} registered with {self.name}")
672
- except Exception as e:
673
- log_error(f"Failed to register tool {tool.name}: {e}")
674
-
675
- log_debug(f"{self.name} initialized with {len(filtered_tools)} tools from one MCP server")
676
- self._initialized = True
677
- except Exception as e:
678
- log_error(f"Failed to get MCP tools: {e}")
679
- raise
File without changes