open-swarm 0.1.1745275181__py3-none-any.whl → 0.1.1748636295__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.
- open_swarm-0.1.1748636295.dist-info/METADATA +257 -0
- open_swarm-0.1.1748636295.dist-info/RECORD +89 -0
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/WHEEL +2 -1
- open_swarm-0.1.1748636295.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1748636295.dist-info/top_level.txt +1 -0
- swarm/__init__.py +2 -0
- swarm/agent/agent.py +49 -0
- swarm/auth.py +48 -113
- swarm/consumers.py +0 -19
- swarm/core.py +411 -0
- swarm/extensions/blueprint/__init__.py +16 -30
- swarm/extensions/blueprint/agent_utils.py +45 -0
- swarm/extensions/blueprint/blueprint_base.py +562 -0
- swarm/extensions/blueprint/blueprint_discovery.py +112 -0
- swarm/extensions/blueprint/django_utils.py +79 -181
- swarm/extensions/blueprint/interactive_mode.py +72 -67
- swarm/extensions/blueprint/output_utils.py +82 -0
- swarm/{core → extensions/blueprint}/spinner.py +21 -30
- swarm/extensions/cli/cli_args.py +0 -6
- swarm/extensions/cli/commands/blueprint_management.py +9 -47
- swarm/extensions/cli/commands/config_management.py +6 -5
- swarm/extensions/cli/commands/edit_config.py +7 -16
- swarm/extensions/cli/commands/list_blueprints.py +1 -1
- swarm/extensions/cli/commands/validate_env.py +4 -11
- swarm/extensions/cli/commands/validate_envvars.py +6 -6
- swarm/extensions/cli/interactive_shell.py +2 -16
- swarm/extensions/config/config_loader.py +345 -107
- swarm/{core → extensions/config}/config_manager.py +38 -50
- swarm/{core → extensions/config}/server_config.py +0 -32
- swarm/extensions/launchers/build_launchers.py +14 -0
- swarm/{core → extensions/launchers}/build_swarm_wrapper.py +0 -0
- swarm/extensions/launchers/swarm_api.py +64 -8
- swarm/extensions/launchers/swarm_cli.py +300 -8
- swarm/extensions/mcp/__init__.py +1 -0
- swarm/extensions/mcp/cache_utils.py +32 -0
- swarm/extensions/mcp/mcp_client.py +233 -0
- swarm/extensions/mcp/mcp_tool_provider.py +135 -0
- swarm/extensions/mcp/mcp_utils.py +260 -0
- swarm/llm/chat_completion.py +166 -0
- swarm/serializers.py +5 -96
- swarm/settings.py +133 -85
- swarm/types.py +91 -0
- swarm/urls.py +74 -57
- swarm/utils/context_utils.py +4 -10
- swarm/utils/general_utils.py +0 -21
- swarm/utils/redact.py +36 -23
- swarm/views/api_views.py +39 -48
- swarm/views/chat_views.py +76 -236
- swarm/views/core_views.py +87 -80
- swarm/views/model_views.py +121 -64
- swarm/views/utils.py +439 -65
- swarm/views/web_views.py +2 -2
- open_swarm-0.1.1745275181.dist-info/METADATA +0 -874
- open_swarm-0.1.1745275181.dist-info/RECORD +0 -319
- open_swarm-0.1.1745275181.dist-info/entry_points.txt +0 -4
- swarm/blueprints/README.md +0 -68
- swarm/blueprints/blueprint_audit_status.json +0 -27
- swarm/blueprints/chatbot/README.md +0 -40
- swarm/blueprints/chatbot/blueprint_chatbot.py +0 -471
- swarm/blueprints/chatbot/metadata.json +0 -23
- swarm/blueprints/chatbot/templates/chatbot/chatbot.html +0 -33
- swarm/blueprints/chucks_angels/README.md +0 -11
- swarm/blueprints/chucks_angels/blueprint_chucks_angels.py +0 -7
- swarm/blueprints/chucks_angels/test_basic.py +0 -3
- swarm/blueprints/codey/CODEY.md +0 -15
- swarm/blueprints/codey/README.md +0 -115
- swarm/blueprints/codey/blueprint_codey.py +0 -1072
- swarm/blueprints/codey/codey_cli.py +0 -373
- swarm/blueprints/codey/instructions.md +0 -17
- swarm/blueprints/codey/metadata.json +0 -23
- swarm/blueprints/common/operation_box_utils.py +0 -83
- swarm/blueprints/digitalbutlers/README.md +0 -11
- swarm/blueprints/digitalbutlers/__init__.py +0 -1
- swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +0 -7
- swarm/blueprints/digitalbutlers/test_basic.py +0 -3
- swarm/blueprints/divine_code/README.md +0 -3
- swarm/blueprints/divine_code/__init__.py +0 -10
- swarm/blueprints/divine_code/apps.py +0 -11
- swarm/blueprints/divine_code/blueprint_divine_code.py +0 -270
- swarm/blueprints/django_chat/apps.py +0 -6
- swarm/blueprints/django_chat/blueprint_django_chat.py +0 -268
- swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +0 -37
- swarm/blueprints/django_chat/urls.py +0 -8
- swarm/blueprints/django_chat/views.py +0 -32
- swarm/blueprints/echocraft/blueprint_echocraft.py +0 -384
- swarm/blueprints/flock/README.md +0 -11
- swarm/blueprints/flock/__init__.py +0 -8
- swarm/blueprints/flock/blueprint_flock.py +0 -7
- swarm/blueprints/flock/test_basic.py +0 -3
- swarm/blueprints/geese/README.md +0 -10
- swarm/blueprints/geese/__init__.py +0 -8
- swarm/blueprints/geese/blueprint_geese.py +0 -384
- swarm/blueprints/geese/geese_cli.py +0 -102
- swarm/blueprints/jeeves/README.md +0 -41
- swarm/blueprints/jeeves/blueprint_jeeves.py +0 -722
- swarm/blueprints/jeeves/jeeves_cli.py +0 -55
- swarm/blueprints/jeeves/metadata.json +0 -24
- swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +0 -473
- swarm/blueprints/messenger/templates/messenger/messenger.html +0 -46
- swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +0 -423
- swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +0 -340
- swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +0 -265
- swarm/blueprints/omniplex/blueprint_omniplex.py +0 -298
- swarm/blueprints/poets/blueprint_poets.py +0 -546
- swarm/blueprints/poets/poets_cli.py +0 -23
- swarm/blueprints/rue_code/README.md +0 -8
- swarm/blueprints/rue_code/blueprint_rue_code.py +0 -448
- swarm/blueprints/rue_code/rue_code_cli.py +0 -43
- swarm/blueprints/stewie/apps.py +0 -12
- swarm/blueprints/stewie/blueprint_family_ties.py +0 -349
- swarm/blueprints/stewie/models.py +0 -19
- swarm/blueprints/stewie/serializers.py +0 -10
- swarm/blueprints/stewie/settings.py +0 -17
- swarm/blueprints/stewie/urls.py +0 -11
- swarm/blueprints/stewie/views.py +0 -26
- swarm/blueprints/suggestion/blueprint_suggestion.py +0 -222
- swarm/blueprints/whinge_surf/README.md +0 -22
- swarm/blueprints/whinge_surf/__init__.py +0 -1
- swarm/blueprints/whinge_surf/blueprint_whinge_surf.py +0 -565
- swarm/blueprints/whinge_surf/whinge_surf_cli.py +0 -99
- swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
- swarm/blueprints/whiskeytango_foxtrot/apps.py +0 -11
- swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +0 -339
- swarm/blueprints/zeus/__init__.py +0 -2
- swarm/blueprints/zeus/apps.py +0 -4
- swarm/blueprints/zeus/blueprint_zeus.py +0 -270
- swarm/blueprints/zeus/zeus_cli.py +0 -13
- swarm/cli/async_input.py +0 -65
- swarm/cli/async_input_demo.py +0 -32
- swarm/core/agent_utils.py +0 -21
- swarm/core/blueprint_base.py +0 -769
- swarm/core/blueprint_discovery.py +0 -125
- swarm/core/blueprint_runner.py +0 -59
- swarm/core/blueprint_ux.py +0 -109
- swarm/core/build_launchers.py +0 -15
- swarm/core/cli/__init__.py +0 -1
- swarm/core/cli/commands/__init__.py +0 -1
- swarm/core/cli/commands/blueprint_management.py +0 -7
- swarm/core/cli/interactive_shell.py +0 -14
- swarm/core/cli/main.py +0 -50
- swarm/core/cli/utils/__init__.py +0 -1
- swarm/core/cli/utils/discover_commands.py +0 -18
- swarm/core/config_loader.py +0 -122
- swarm/core/output_utils.py +0 -193
- swarm/core/session_logger.py +0 -42
- swarm/core/slash_commands.py +0 -89
- swarm/core/swarm_api.py +0 -68
- swarm/core/swarm_cli.py +0 -216
- swarm/core/utils/__init__.py +0 -0
- swarm/extensions/blueprint/cli_handler.py +0 -197
- swarm/extensions/blueprint/runnable_blueprint.py +0 -42
- swarm/extensions/cli/utils/__init__.py +0 -1
- swarm/extensions/cli/utils/async_input.py +0 -46
- swarm/extensions/cli/utils/prompt_user.py +0 -3
- swarm/management/__init__.py +0 -0
- swarm/management/commands/__init__.py +0 -0
- swarm/management/commands/runserver.py +0 -58
- swarm/middleware.py +0 -65
- swarm/permissions.py +0 -38
- swarm/static/contrib/fonts/fontawesome-webfont.ttf +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff2 +0 -7
- swarm/static/contrib/markedjs/marked.min.js +0 -6
- swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +0 -27
- swarm/static/contrib/tabler-icons/alert-triangle.svg +0 -21
- swarm/static/contrib/tabler-icons/archive.svg +0 -21
- swarm/static/contrib/tabler-icons/artboard.svg +0 -27
- swarm/static/contrib/tabler-icons/automatic-gearbox.svg +0 -23
- swarm/static/contrib/tabler-icons/box-multiple.svg +0 -19
- swarm/static/contrib/tabler-icons/carambola.svg +0 -19
- swarm/static/contrib/tabler-icons/copy.svg +0 -20
- swarm/static/contrib/tabler-icons/download.svg +0 -21
- swarm/static/contrib/tabler-icons/edit.svg +0 -21
- swarm/static/contrib/tabler-icons/filled/carambola.svg +0 -13
- swarm/static/contrib/tabler-icons/filled/paint.svg +0 -13
- swarm/static/contrib/tabler-icons/headset.svg +0 -22
- swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/message-chatbot.svg +0 -22
- swarm/static/contrib/tabler-icons/message-star.svg +0 -22
- swarm/static/contrib/tabler-icons/message-x.svg +0 -23
- swarm/static/contrib/tabler-icons/message.svg +0 -21
- swarm/static/contrib/tabler-icons/paperclip.svg +0 -18
- swarm/static/contrib/tabler-icons/playlist-add.svg +0 -22
- swarm/static/contrib/tabler-icons/robot.svg +0 -26
- swarm/static/contrib/tabler-icons/search.svg +0 -19
- swarm/static/contrib/tabler-icons/settings.svg +0 -20
- swarm/static/contrib/tabler-icons/thumb-down.svg +0 -19
- swarm/static/contrib/tabler-icons/thumb-up.svg +0 -19
- swarm/static/css/dropdown.css +0 -22
- swarm/static/htmx/htmx.min.js +0 -0
- swarm/static/js/dropdown.js +0 -23
- swarm/static/rest_mode/css/base.css +0 -470
- swarm/static/rest_mode/css/chat-history.css +0 -286
- swarm/static/rest_mode/css/chat.css +0 -251
- swarm/static/rest_mode/css/chatbot.css +0 -74
- swarm/static/rest_mode/css/chatgpt.css +0 -62
- swarm/static/rest_mode/css/colors/corporate.css +0 -74
- swarm/static/rest_mode/css/colors/pastel.css +0 -81
- swarm/static/rest_mode/css/colors/tropical.css +0 -82
- swarm/static/rest_mode/css/general.css +0 -142
- swarm/static/rest_mode/css/layout.css +0 -167
- swarm/static/rest_mode/css/layouts/messenger-layout.css +0 -17
- swarm/static/rest_mode/css/layouts/minimalist-layout.css +0 -57
- swarm/static/rest_mode/css/layouts/mobile-layout.css +0 -8
- swarm/static/rest_mode/css/messages.css +0 -84
- swarm/static/rest_mode/css/messenger.css +0 -135
- swarm/static/rest_mode/css/settings.css +0 -91
- swarm/static/rest_mode/css/simple.css +0 -44
- swarm/static/rest_mode/css/slack.css +0 -58
- swarm/static/rest_mode/css/style.css +0 -156
- swarm/static/rest_mode/css/theme.css +0 -30
- swarm/static/rest_mode/css/toast.css +0 -40
- swarm/static/rest_mode/js/auth.js +0 -9
- swarm/static/rest_mode/js/blueprint.js +0 -41
- swarm/static/rest_mode/js/blueprintUtils.js +0 -12
- swarm/static/rest_mode/js/chatLogic.js +0 -79
- swarm/static/rest_mode/js/debug.js +0 -63
- swarm/static/rest_mode/js/events.js +0 -98
- swarm/static/rest_mode/js/main.js +0 -19
- swarm/static/rest_mode/js/messages.js +0 -264
- swarm/static/rest_mode/js/messengerLogic.js +0 -355
- swarm/static/rest_mode/js/modules/apiService.js +0 -84
- swarm/static/rest_mode/js/modules/blueprintManager.js +0 -162
- swarm/static/rest_mode/js/modules/chatHistory.js +0 -110
- swarm/static/rest_mode/js/modules/debugLogger.js +0 -14
- swarm/static/rest_mode/js/modules/eventHandlers.js +0 -107
- swarm/static/rest_mode/js/modules/messageProcessor.js +0 -120
- swarm/static/rest_mode/js/modules/state.js +0 -7
- swarm/static/rest_mode/js/modules/userInteractions.js +0 -29
- swarm/static/rest_mode/js/modules/validation.js +0 -23
- swarm/static/rest_mode/js/rendering.js +0 -119
- swarm/static/rest_mode/js/settings.js +0 -130
- swarm/static/rest_mode/js/sidebar.js +0 -94
- swarm/static/rest_mode/js/simpleLogic.js +0 -37
- swarm/static/rest_mode/js/slackLogic.js +0 -66
- swarm/static/rest_mode/js/splash.js +0 -76
- swarm/static/rest_mode/js/theme.js +0 -111
- swarm/static/rest_mode/js/toast.js +0 -36
- swarm/static/rest_mode/js/ui.js +0 -265
- swarm/static/rest_mode/js/validation.js +0 -57
- swarm/static/rest_mode/svg/animated_spinner.svg +0 -12
- swarm/static/rest_mode/svg/arrow_down.svg +0 -5
- swarm/static/rest_mode/svg/arrow_left.svg +0 -5
- swarm/static/rest_mode/svg/arrow_right.svg +0 -5
- swarm/static/rest_mode/svg/arrow_up.svg +0 -5
- swarm/static/rest_mode/svg/attach.svg +0 -8
- swarm/static/rest_mode/svg/avatar.svg +0 -7
- swarm/static/rest_mode/svg/canvas.svg +0 -6
- swarm/static/rest_mode/svg/chat_history.svg +0 -4
- swarm/static/rest_mode/svg/close.svg +0 -5
- swarm/static/rest_mode/svg/copy.svg +0 -4
- swarm/static/rest_mode/svg/dark_mode.svg +0 -3
- swarm/static/rest_mode/svg/edit.svg +0 -5
- swarm/static/rest_mode/svg/layout.svg +0 -9
- swarm/static/rest_mode/svg/logo.svg +0 -29
- swarm/static/rest_mode/svg/logout.svg +0 -5
- swarm/static/rest_mode/svg/mobile.svg +0 -5
- swarm/static/rest_mode/svg/new_chat.svg +0 -4
- swarm/static/rest_mode/svg/not_visible.svg +0 -5
- swarm/static/rest_mode/svg/plus.svg +0 -7
- swarm/static/rest_mode/svg/run_code.svg +0 -6
- swarm/static/rest_mode/svg/save.svg +0 -4
- swarm/static/rest_mode/svg/search.svg +0 -6
- swarm/static/rest_mode/svg/settings.svg +0 -4
- swarm/static/rest_mode/svg/speaker.svg +0 -5
- swarm/static/rest_mode/svg/stop.svg +0 -6
- swarm/static/rest_mode/svg/thumbs_down.svg +0 -3
- swarm/static/rest_mode/svg/thumbs_up.svg +0 -3
- swarm/static/rest_mode/svg/toggle_off.svg +0 -6
- swarm/static/rest_mode/svg/toggle_on.svg +0 -6
- swarm/static/rest_mode/svg/trash.svg +0 -10
- swarm/static/rest_mode/svg/undo.svg +0 -3
- swarm/static/rest_mode/svg/visible.svg +0 -8
- swarm/static/rest_mode/svg/voice.svg +0 -10
- swarm/templates/account/login.html +0 -22
- swarm/templates/account/signup.html +0 -32
- swarm/templates/base.html +0 -30
- swarm/templates/chat.html +0 -43
- swarm/templates/index.html +0 -35
- swarm/templates/rest_mode/components/chat_sidebar.html +0 -55
- swarm/templates/rest_mode/components/header.html +0 -45
- swarm/templates/rest_mode/components/main_chat_pane.html +0 -41
- swarm/templates/rest_mode/components/settings_dialog.html +0 -97
- swarm/templates/rest_mode/components/splash_screen.html +0 -7
- swarm/templates/rest_mode/components/top_bar.html +0 -28
- swarm/templates/rest_mode/message_ui.html +0 -50
- swarm/templates/rest_mode/slackbot.html +0 -30
- swarm/templates/simple_blueprint_page.html +0 -24
- swarm/templates/websocket_partials/final_system_message.html +0 -3
- swarm/templates/websocket_partials/system_message.html +0 -4
- swarm/templates/websocket_partials/user_message.html +0 -5
- swarm/utils/ansi_box.py +0 -34
- swarm/utils/disable_tracing.py +0 -38
- swarm/utils/log_utils.py +0 -63
- swarm/utils/openai_patch.py +0 -33
- swarm/ux/ansi_box.py +0 -43
- swarm/ux/spinner.py +0 -53
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/licenses/LICENSE +0 -0
- /swarm/{core → extensions/blueprint}/blueprint_utils.py +0 -0
- /swarm/{core → extensions/blueprint}/common_utils.py +0 -0
- /swarm/{core → extensions/config}/setup_wizard.py +0 -0
- /swarm/{blueprints/rue_code → extensions/config/utils}/__init__.py +0 -0
- /swarm/{core → extensions/config}/utils/logger.py +0 -0
- /swarm/{core → extensions/launchers}/swarm_wrapper.py +0 -0
@@ -0,0 +1 @@
|
|
1
|
+
# This is the __init__.py for the 'mcp' package.
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# cache_utils.py
|
2
|
+
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
class DummyCache:
|
6
|
+
"""A dummy cache that performs no operations."""
|
7
|
+
def get(self, key: str, default: Any = None) -> Any:
|
8
|
+
return default
|
9
|
+
|
10
|
+
def set(self, key: str, value: Any, timeout: int = None) -> None:
|
11
|
+
pass
|
12
|
+
|
13
|
+
def get_cache():
|
14
|
+
"""
|
15
|
+
Attempts to retrieve Django's cache. If Django isn't available or configured,
|
16
|
+
returns a DummyCache instance.
|
17
|
+
"""
|
18
|
+
try:
|
19
|
+
import django
|
20
|
+
from django.conf import settings
|
21
|
+
from django.core.cache import cache as django_cache
|
22
|
+
from django.core.exceptions import ImproperlyConfigured
|
23
|
+
|
24
|
+
if not settings.configured:
|
25
|
+
# Django settings are not configured; return DummyCache
|
26
|
+
return DummyCache()
|
27
|
+
|
28
|
+
return django_cache
|
29
|
+
|
30
|
+
except (ImportError, ImproperlyConfigured):
|
31
|
+
# Django is not installed or not properly configured; use DummyCache
|
32
|
+
return DummyCache()
|
@@ -0,0 +1,233 @@
|
|
1
|
+
"""
|
2
|
+
MCP Client Module
|
3
|
+
|
4
|
+
Manages connections and interactions with MCP servers using the MCP Python SDK.
|
5
|
+
Redirects MCP server stderr to log files unless debug mode is enabled.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import logging
|
10
|
+
import os
|
11
|
+
from typing import Any, Dict, List, Callable
|
12
|
+
from contextlib import contextmanager
|
13
|
+
import sys
|
14
|
+
|
15
|
+
from mcp import ClientSession, StdioServerParameters # type: ignore
|
16
|
+
from mcp.client.stdio import stdio_client # type: ignore
|
17
|
+
from swarm.types import Tool
|
18
|
+
from .cache_utils import get_cache
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
logger.setLevel(logging.DEBUG)
|
22
|
+
|
23
|
+
class MCPClient:
|
24
|
+
"""
|
25
|
+
Manages connections and interactions with MCP servers using the MCP Python SDK.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, server_config: Dict[str, Any], timeout: int = 15, debug: bool = False):
|
29
|
+
"""
|
30
|
+
Initialize the MCPClient with server configuration.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
server_config (dict): Configuration dictionary for the MCP server.
|
34
|
+
timeout (int): Timeout for operations in seconds.
|
35
|
+
debug (bool): If True, MCP server stderr goes to console; otherwise, to log file.
|
36
|
+
"""
|
37
|
+
self.command = server_config.get("command", "npx")
|
38
|
+
self.args = server_config.get("args", [])
|
39
|
+
self.env = {**os.environ.copy(), **server_config.get("env", {})}
|
40
|
+
self.timeout = timeout
|
41
|
+
self.debug = debug
|
42
|
+
self._tool_cache: Dict[str, Tool] = {}
|
43
|
+
|
44
|
+
# Initialize cache using the helper
|
45
|
+
self.cache = get_cache()
|
46
|
+
|
47
|
+
logger.info(f"Initialized MCPClient with command={self.command}, args={self.args}, debug={self.debug}")
|
48
|
+
|
49
|
+
@contextmanager
|
50
|
+
def _redirect_stderr(self):
|
51
|
+
import sys, os
|
52
|
+
if not self.debug:
|
53
|
+
old_stderr = sys.stderr
|
54
|
+
sys.stderr = open(os.devnull, "w")
|
55
|
+
try:
|
56
|
+
yield
|
57
|
+
finally:
|
58
|
+
sys.stderr.close()
|
59
|
+
sys.stderr = old_stderr
|
60
|
+
else:
|
61
|
+
yield
|
62
|
+
|
63
|
+
async def list_tools(self) -> List[Tool]:
|
64
|
+
"""
|
65
|
+
Discover tools from the MCP server and cache their schemas.
|
66
|
+
|
67
|
+
Returns:
|
68
|
+
List[Tool]: A list of discovered tools with schemas.
|
69
|
+
"""
|
70
|
+
logger.debug(f"Entering list_tools for command={self.command}, args={self.args}")
|
71
|
+
|
72
|
+
# Attempt to retrieve tools from cache
|
73
|
+
args_string = "_".join(self.args)
|
74
|
+
cache_key = f"mcp_tools_{self.command}_{args_string}"
|
75
|
+
cached_tools = self.cache.get(cache_key)
|
76
|
+
|
77
|
+
if cached_tools:
|
78
|
+
logger.debug("Retrieved tools from cache")
|
79
|
+
tools = []
|
80
|
+
for tool_data in cached_tools:
|
81
|
+
tool_name = tool_data["name"]
|
82
|
+
tool = Tool(
|
83
|
+
name=tool_name,
|
84
|
+
description=tool_data["description"],
|
85
|
+
input_schema=tool_data.get("input_schema", {}),
|
86
|
+
func=self._create_tool_callable(tool_name),
|
87
|
+
)
|
88
|
+
tools.append(tool)
|
89
|
+
logger.debug(f"Returning {len(tools)} cached tools")
|
90
|
+
return tools
|
91
|
+
|
92
|
+
server_params = StdioServerParameters(command=self.command, args=self.args, env=self.env)
|
93
|
+
logger.debug("Opening stdio_client connection")
|
94
|
+
async with stdio_client(server_params) as (read, write):
|
95
|
+
logger.debug("Opening ClientSession")
|
96
|
+
async with ClientSession(read, write) as session:
|
97
|
+
try:
|
98
|
+
logger.info("Initializing session for tool discovery")
|
99
|
+
await asyncio.wait_for(session.initialize(), timeout=self.timeout)
|
100
|
+
logger.info("Initializing session for tool discovery")
|
101
|
+
await asyncio.wait_for(session.initialize(), timeout=self.timeout)
|
102
|
+
logger.info("Capabilities initialized. Entering tool discovery.")
|
103
|
+
logger.info("Requesting tool list from MCP server...")
|
104
|
+
tools_response = await asyncio.wait_for(session.list_tools(), timeout=self.timeout)
|
105
|
+
logger.debug("Tool list received from MCP server")
|
106
|
+
|
107
|
+
serialized_tools = [
|
108
|
+
{
|
109
|
+
'name': tool.name,
|
110
|
+
'description': tool.description,
|
111
|
+
'input_schema': tool.inputSchema,
|
112
|
+
}
|
113
|
+
for tool in tools_response.tools
|
114
|
+
]
|
115
|
+
|
116
|
+
self.cache.set(cache_key, serialized_tools, 3600)
|
117
|
+
logger.debug(f"Cached {len(serialized_tools)} tools.")
|
118
|
+
|
119
|
+
tools = []
|
120
|
+
for tool in tools_response.tools:
|
121
|
+
input_schema = tool.inputSchema or {}
|
122
|
+
cached_tool = Tool(
|
123
|
+
name=tool.name,
|
124
|
+
description=tool.description,
|
125
|
+
input_schema=input_schema,
|
126
|
+
func=self._create_tool_callable(tool.name),
|
127
|
+
)
|
128
|
+
self._tool_cache[tool.name] = cached_tool
|
129
|
+
tools.append(cached_tool)
|
130
|
+
logger.debug(f"Discovered tool: {tool.name} with schema: {input_schema}")
|
131
|
+
|
132
|
+
logger.debug(f"Returning {len(tools)} tools from MCP server")
|
133
|
+
return tools
|
134
|
+
|
135
|
+
except asyncio.TimeoutError:
|
136
|
+
logger.error(f"Timeout after {self.timeout}s waiting for tool list")
|
137
|
+
raise RuntimeError("Tool list request timed out")
|
138
|
+
except Exception as e:
|
139
|
+
logger.error(f"Error listing tools: {e}")
|
140
|
+
raise RuntimeError("Failed to list tools") from e
|
141
|
+
|
142
|
+
async def _do_list_resources(self) -> Any:
|
143
|
+
server_params = StdioServerParameters(command=self.command, args=self.args, env=self.env)
|
144
|
+
logger.debug("Opening stdio_client connection for resources")
|
145
|
+
async with stdio_client(server_params) as (read, write):
|
146
|
+
logger.debug("Opening ClientSession for resources")
|
147
|
+
async with ClientSession(read, write) as session:
|
148
|
+
logger.info("Requesting resource list from MCP server...")
|
149
|
+
with self._redirect_stderr():
|
150
|
+
# Ensure we initialize the session before listing resources
|
151
|
+
logger.debug("Initializing session before listing resources")
|
152
|
+
await asyncio.wait_for(session.initialize(), timeout=self.timeout)
|
153
|
+
resources_response = await asyncio.wait_for(session.list_resources(), timeout=self.timeout)
|
154
|
+
logger.debug("Resource list received from MCP server")
|
155
|
+
return resources_response
|
156
|
+
|
157
|
+
def _create_tool_callable(self, tool_name: str) -> Callable[..., Any]:
|
158
|
+
"""
|
159
|
+
Dynamically create a callable function for the specified tool.
|
160
|
+
"""
|
161
|
+
async def dynamic_tool_func(**kwargs) -> Any:
|
162
|
+
logger.debug(f"Creating tool callable for '{tool_name}'")
|
163
|
+
server_params = StdioServerParameters(command=self.command, args=self.args, env=self.env)
|
164
|
+
async with stdio_client(server_params) as (read, write):
|
165
|
+
async with ClientSession(read, write) as session:
|
166
|
+
try:
|
167
|
+
logger.debug(f"Initializing session for tool '{tool_name}'")
|
168
|
+
await asyncio.wait_for(session.initialize(), timeout=self.timeout)
|
169
|
+
if tool_name in self._tool_cache:
|
170
|
+
tool = self._tool_cache[tool_name]
|
171
|
+
self._validate_input_schema(tool.input_schema, kwargs)
|
172
|
+
logger.info(f"Calling tool '{tool_name}' with arguments: {kwargs}")
|
173
|
+
result = await asyncio.wait_for(session.call_tool(tool_name, kwargs), timeout=self.timeout)
|
174
|
+
logger.info(f"Tool '{tool_name}' executed successfully: {result}")
|
175
|
+
return result
|
176
|
+
except asyncio.TimeoutError:
|
177
|
+
logger.error(f"Timeout after {self.timeout}s executing tool '{tool_name}'")
|
178
|
+
raise RuntimeError(f"Tool '{tool_name}' execution timed out")
|
179
|
+
except Exception as e:
|
180
|
+
logger.error(f"Failed to execute tool '{tool_name}': {e}")
|
181
|
+
raise RuntimeError(f"Tool execution failed: {e}") from e
|
182
|
+
|
183
|
+
return dynamic_tool_func
|
184
|
+
|
185
|
+
def _validate_input_schema(self, schema: Dict[str, Any], kwargs: Dict[str, Any]):
|
186
|
+
"""
|
187
|
+
Validate the provided arguments against the input schema.
|
188
|
+
"""
|
189
|
+
if not schema:
|
190
|
+
logger.debug("No input schema available for validation. Skipping.")
|
191
|
+
return
|
192
|
+
|
193
|
+
required_params = schema.get("required", [])
|
194
|
+
for param in required_params:
|
195
|
+
if param not in kwargs:
|
196
|
+
raise ValueError(f"Missing required parameter: '{param}'")
|
197
|
+
|
198
|
+
logger.debug(f"Validated input against schema: {schema} with arguments: {kwargs}")
|
199
|
+
|
200
|
+
async def list_resources(self) -> Any:
|
201
|
+
"""
|
202
|
+
Discover resources from the MCP server using the internal method with enforced timeout.
|
203
|
+
"""
|
204
|
+
return await asyncio.wait_for(self._do_list_resources(), timeout=self.timeout)
|
205
|
+
|
206
|
+
async def get_resource(self, resource_uri: str) -> Any:
|
207
|
+
"""
|
208
|
+
Retrieve a specific resource from the MCP server.
|
209
|
+
|
210
|
+
Args:
|
211
|
+
resource_uri (str): The URI of the resource to retrieve.
|
212
|
+
|
213
|
+
Returns:
|
214
|
+
Any: The resource retrieval response.
|
215
|
+
"""
|
216
|
+
server_params = StdioServerParameters(command=self.command, args=self.args, env=self.env)
|
217
|
+
logger.debug("Opening stdio_client connection for resource retrieval")
|
218
|
+
async with stdio_client(server_params) as (read, write):
|
219
|
+
logger.debug("Opening ClientSession for resource retrieval")
|
220
|
+
async with ClientSession(read, write) as session:
|
221
|
+
try:
|
222
|
+
logger.debug(f"Initializing session for resource retrieval of {resource_uri}")
|
223
|
+
await asyncio.wait_for(session.initialize(), timeout=self.timeout)
|
224
|
+
logger.info(f"Retrieving resource '{resource_uri}' from MCP server")
|
225
|
+
response = await asyncio.wait_for(session.read_resource(resource_uri), timeout=self.timeout)
|
226
|
+
logger.info(f"Resource '{resource_uri}' retrieved successfully")
|
227
|
+
return response
|
228
|
+
except asyncio.TimeoutError:
|
229
|
+
logger.error(f"Timeout retrieving resource '{resource_uri}' after {self.timeout}s")
|
230
|
+
raise RuntimeError(f"Resource '{resource_uri}' retrieval timed out")
|
231
|
+
except Exception as e:
|
232
|
+
logger.error(f"Failed to retrieve resource '{resource_uri}': {e}")
|
233
|
+
raise RuntimeError(f"Resource retrieval failed: {e}") from e
|
@@ -0,0 +1,135 @@
|
|
1
|
+
"""
|
2
|
+
MCPToolProvider Module for Open-Swarm
|
3
|
+
|
4
|
+
This module is responsible for discovering tools from MCP (Model Context Protocol) servers
|
5
|
+
and integrating them into the Open-Swarm framework as `Tool` instances. It handles
|
6
|
+
communication with MCP servers, constructs callable functions for dynamic tools, and
|
7
|
+
ensures that these tools are properly validated and integrated into the agent's function list.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import logging
|
11
|
+
from typing import List, Dict, Any
|
12
|
+
|
13
|
+
from swarm.settings import DEBUG
|
14
|
+
from swarm.types import Tool, Agent
|
15
|
+
from swarm.extensions.mcp.mcp_client import MCPClient
|
16
|
+
|
17
|
+
from .cache_utils import get_cache
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
|
21
|
+
stream_handler = logging.StreamHandler()
|
22
|
+
formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s - %(message)s")
|
23
|
+
stream_handler.setFormatter(formatter)
|
24
|
+
if not logger.handlers:
|
25
|
+
logger.addHandler(stream_handler)
|
26
|
+
|
27
|
+
|
28
|
+
class MCPToolProvider:
|
29
|
+
"""
|
30
|
+
Singleton MCPToolProvider to discover tools from an MCP server and convert them into `Tool` instances.
|
31
|
+
"""
|
32
|
+
_instances: Dict[str, "MCPToolProvider"] = {}
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def get_instance(cls, server_name: str, server_config: Dict[str, Any], timeout: int = 15, debug: bool = False) -> "MCPToolProvider":
|
36
|
+
"""Get or create a singleton instance for the given server name."""
|
37
|
+
if server_name not in cls._instances:
|
38
|
+
cls._instances[server_name] = cls(server_name, server_config, timeout, debug)
|
39
|
+
return cls._instances[server_name]
|
40
|
+
|
41
|
+
def __init__(self, server_name: str, server_config: Dict[str, Any], timeout: int = 15, debug: bool = False):
|
42
|
+
"""
|
43
|
+
Initialize an MCPToolProvider instance with a configurable timeout.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
server_name (str): The name of the MCP server.
|
47
|
+
server_config (dict): Configuration dictionary for the specific server.
|
48
|
+
timeout (int): Timeout in seconds for MCP operations (default 15, overridden by caller if provided).
|
49
|
+
debug (bool): If True, MCP server stderr goes to stderr; otherwise, to log file.
|
50
|
+
"""
|
51
|
+
if server_name in self._instances:
|
52
|
+
raise ValueError(f"MCPToolProvider for '{server_name}' already initialized. Use get_instance().")
|
53
|
+
self.server_name = server_name
|
54
|
+
self.client = MCPClient(server_config=server_config, timeout=timeout, debug=debug)
|
55
|
+
self.cache = get_cache()
|
56
|
+
logger.debug(f"Initialized MCPToolProvider for server '{self.server_name}' with timeout {timeout}s.")
|
57
|
+
|
58
|
+
async def discover_tools(self, agent: Agent) -> List[Tool]:
|
59
|
+
"""
|
60
|
+
Discover tools from the MCP server and return them as a list of `Tool` instances.
|
61
|
+
Utilizes Django cache to persist tool metadata if available.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
agent (Agent): The agent for which tools are being discovered.
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
List[Tool]: A list of discovered `Tool` instances.
|
68
|
+
|
69
|
+
Raises:
|
70
|
+
RuntimeError: If tool discovery from the MCP server fails.
|
71
|
+
"""
|
72
|
+
cache_key = f"mcp_tools_{self.server_name}"
|
73
|
+
cached_tools = self.cache.get(cache_key)
|
74
|
+
|
75
|
+
if cached_tools:
|
76
|
+
logger.debug(f"Retrieved tools for server '{self.server_name}' from cache.")
|
77
|
+
tools = []
|
78
|
+
for tool_data in cached_tools:
|
79
|
+
tool_name = tool_data["name"]
|
80
|
+
tool = Tool(
|
81
|
+
name=tool_name,
|
82
|
+
description=tool_data["description"],
|
83
|
+
input_schema=tool_data.get("input_schema", {}),
|
84
|
+
func=self._create_tool_callable(tool_name),
|
85
|
+
)
|
86
|
+
tools.append(tool)
|
87
|
+
return tools
|
88
|
+
|
89
|
+
logger.debug(f"Starting tool discovery from MCP server '{self.server_name}' for agent '{agent.name}'.")
|
90
|
+
try:
|
91
|
+
tools = await self.client.list_tools()
|
92
|
+
logger.debug(f"Discovered tools from MCP server '{self.server_name}': {[tool.name for tool in tools]}")
|
93
|
+
|
94
|
+
# Serialize tools for caching
|
95
|
+
serialized_tools = [
|
96
|
+
{
|
97
|
+
'name': tool.name,
|
98
|
+
'description': tool.description,
|
99
|
+
'input_schema': tool.input_schema,
|
100
|
+
}
|
101
|
+
for tool in tools
|
102
|
+
]
|
103
|
+
|
104
|
+
# Cache the tools for 1 hour (3600 seconds)
|
105
|
+
self.cache.set(cache_key, serialized_tools, 3600)
|
106
|
+
logger.debug(f"Cached tools for MCP server '{self.server_name}'.")
|
107
|
+
|
108
|
+
return tools
|
109
|
+
|
110
|
+
except Exception as e:
|
111
|
+
logger.error(f"Failed to discover tools from MCP server '{self.server_name}': {e}", exc_info=True)
|
112
|
+
raise RuntimeError(f"Tool discovery failed for MCP server '{self.server_name}': {e}") from e
|
113
|
+
|
114
|
+
def _create_tool_callable(self, tool_name: str):
|
115
|
+
"""
|
116
|
+
Create a callable function for a dynamically discovered tool.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
tool_name (str): The name of the tool.
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Callable: An async callable function for the tool.
|
123
|
+
"""
|
124
|
+
async def dynamic_tool_func(**kwargs) -> Any:
|
125
|
+
try:
|
126
|
+
logger.info(f"Executing tool '{tool_name}' with arguments: {kwargs}")
|
127
|
+
tool_callable = self.client._create_tool_callable(tool_name)
|
128
|
+
result = await tool_callable(**kwargs)
|
129
|
+
logger.info(f"Tool '{tool_name}' executed successfully: {result}")
|
130
|
+
return result
|
131
|
+
except Exception as e:
|
132
|
+
logger.error(f"Error executing tool '{tool_name}': {e}")
|
133
|
+
raise RuntimeError(f"Tool execution failed: {e}") from e
|
134
|
+
|
135
|
+
return dynamic_tool_func
|
@@ -0,0 +1,260 @@
|
|
1
|
+
"""
|
2
|
+
Utilities for MCP server interactions in the Swarm framework.
|
3
|
+
Handles discovery and merging of tools and resources from MCP servers.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
from typing import List, Dict, Any, Optional, cast
|
8
|
+
import asyncio # Needed for async operations
|
9
|
+
|
10
|
+
# Import necessary types from the core swarm types
|
11
|
+
from swarm.types import Agent, AgentFunction
|
12
|
+
# Import the MCPToolProvider which handles communication with MCP servers
|
13
|
+
from .mcp_tool_provider import MCPToolProvider
|
14
|
+
|
15
|
+
# Configure module-level logging
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
# Ensure logger level is set appropriately (e.g., DEBUG for development)
|
18
|
+
# logger.setLevel(logging.DEBUG) # Uncomment for verbose logging
|
19
|
+
# Add handler if not already configured by root logger setup
|
20
|
+
if not logger.handlers:
|
21
|
+
stream_handler = logging.StreamHandler()
|
22
|
+
formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s")
|
23
|
+
stream_handler.setFormatter(formatter)
|
24
|
+
logger.addHandler(stream_handler)
|
25
|
+
|
26
|
+
# Dictionary to manage locks for concurrent discovery per agent (optional)
|
27
|
+
# _discovery_locks: Dict[str, asyncio.Lock] = {}
|
28
|
+
|
29
|
+
async def discover_and_merge_agent_tools(agent: Agent, config: Dict[str, Any], debug: bool = False) -> List[AgentFunction]:
|
30
|
+
"""
|
31
|
+
Discover tools from MCP servers listed in the agent's config and merge
|
32
|
+
them with the agent's statically defined functions.
|
33
|
+
|
34
|
+
Handles deduplication of discovered tools based on name.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
agent: The agent instance for which to discover tools.
|
38
|
+
config: The main Swarm configuration dictionary containing MCP server details.
|
39
|
+
debug: If True, enable detailed debugging logs.
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
List[AgentFunction]: A combined list containing the agent's static functions
|
43
|
+
and unique tools discovered from its associated MCP servers.
|
44
|
+
Returns the agent's static functions if no MCP servers are defined.
|
45
|
+
Returns an empty list if the agent is None.
|
46
|
+
"""
|
47
|
+
if not agent:
|
48
|
+
logger.error("Cannot discover tools: Agent object is None.")
|
49
|
+
return []
|
50
|
+
# Use agent's name for logging clarity
|
51
|
+
agent_name = getattr(agent, "name", "UnnamedAgent")
|
52
|
+
|
53
|
+
logger.debug(f"Starting tool discovery for agent '{agent_name}'.")
|
54
|
+
# Get the list of MCP servers associated with the agent
|
55
|
+
mcp_server_names = getattr(agent, "mcp_servers", [])
|
56
|
+
|
57
|
+
# Retrieve the agent's statically defined functions
|
58
|
+
static_functions = getattr(agent, "functions", []) or []
|
59
|
+
if not isinstance(static_functions, list):
|
60
|
+
logger.warning(f"Agent '{agent_name}' functions attribute is not a list ({type(static_functions)}). Treating as empty.")
|
61
|
+
static_functions = []
|
62
|
+
|
63
|
+
# If no MCP servers are listed for the agent, return only static functions
|
64
|
+
if not mcp_server_names:
|
65
|
+
func_names = [getattr(f, 'name', getattr(f, '__name__', '<unknown>')) for f in static_functions]
|
66
|
+
logger.debug(f"Agent '{agent_name}' has no MCP servers listed. Returning {len(static_functions)} static functions: {func_names}")
|
67
|
+
return static_functions
|
68
|
+
|
69
|
+
# List to hold tools discovered from all MCP servers
|
70
|
+
all_discovered_tools: List[AgentFunction] = []
|
71
|
+
# Set to keep track of discovered tool names for deduplication
|
72
|
+
discovered_tool_names = set()
|
73
|
+
|
74
|
+
# Iterate through each MCP server listed for the agent
|
75
|
+
for server_name in mcp_server_names:
|
76
|
+
if not isinstance(server_name, str):
|
77
|
+
logger.warning(f"Invalid MCP server name type for agent '{agent_name}': {type(server_name)}. Skipping.")
|
78
|
+
continue
|
79
|
+
|
80
|
+
logger.debug(f"Discovering tools from MCP server '{server_name}' for agent '{agent_name}'.")
|
81
|
+
# Get the configuration for the specific MCP server from the main config
|
82
|
+
server_config = config.get("mcpServers", {}).get(server_name)
|
83
|
+
if not server_config:
|
84
|
+
logger.warning(f"MCP server '{server_name}' configuration not found in main config for agent '{agent_name}'. Skipping.")
|
85
|
+
continue
|
86
|
+
|
87
|
+
try:
|
88
|
+
# Get an instance of the MCPToolProvider for this server
|
89
|
+
# Timeout can be adjusted based on expected MCP response time
|
90
|
+
provider = MCPToolProvider.get_instance(server_name, server_config, timeout=15, debug=debug)
|
91
|
+
# Call the provider to discover tools (this interacts with the MCP server)
|
92
|
+
discovered_tools_from_server = await provider.discover_tools(agent)
|
93
|
+
|
94
|
+
# Validate the response from the provider
|
95
|
+
if not isinstance(discovered_tools_from_server, list):
|
96
|
+
logger.warning(f"Invalid tools format received from MCP server '{server_name}' for agent '{agent_name}': Expected list, got {type(discovered_tools_from_server)}. Skipping.")
|
97
|
+
continue
|
98
|
+
|
99
|
+
server_tool_count = 0
|
100
|
+
for tool in discovered_tools_from_server:
|
101
|
+
# Attempt to get tool name for deduplication and logging
|
102
|
+
tool_name = getattr(tool, 'name', None) # Assuming tool objects have a 'name' attribute
|
103
|
+
if not tool_name:
|
104
|
+
logger.warning(f"Discovered tool from '{server_name}' is missing a 'name'. Skipping.")
|
105
|
+
continue
|
106
|
+
|
107
|
+
# Deduplication: Add tool only if its name hasn't been seen before
|
108
|
+
if tool_name not in discovered_tool_names:
|
109
|
+
# Ensure 'requires_approval' attribute exists (defaulting to True if missing)
|
110
|
+
if not hasattr(tool, "requires_approval"):
|
111
|
+
logger.debug(f"Tool '{tool_name}' from '{server_name}' missing 'requires_approval', defaulting to True.")
|
112
|
+
try:
|
113
|
+
setattr(tool, "requires_approval", True)
|
114
|
+
except AttributeError:
|
115
|
+
logger.warning(f"Could not set 'requires_approval' on tool '{tool_name}'.")
|
116
|
+
|
117
|
+
all_discovered_tools.append(tool)
|
118
|
+
discovered_tool_names.add(tool_name)
|
119
|
+
server_tool_count += 1
|
120
|
+
else:
|
121
|
+
logger.debug(f"Tool '{tool_name}' from '{server_name}' is a duplicate. Skipping.")
|
122
|
+
|
123
|
+
tool_names_log = [getattr(t, 'name', '<noname>') for t in discovered_tools_from_server]
|
124
|
+
logger.debug(f"Discovered {server_tool_count} unique tools from '{server_name}': {tool_names_log}")
|
125
|
+
|
126
|
+
except Exception as e:
|
127
|
+
# Log errors during discovery for a specific server but continue with others
|
128
|
+
logger.error(f"Failed to discover tools from MCP server '{server_name}' for agent '{agent_name}': {e}", exc_info=debug) # Show traceback if debug
|
129
|
+
|
130
|
+
# Combine static functions with the unique discovered tools
|
131
|
+
# Static functions take precedence if names conflict (though deduplication above is based on discovered names)
|
132
|
+
final_functions = static_functions + all_discovered_tools
|
133
|
+
|
134
|
+
# Log final combined list details if debugging
|
135
|
+
if debug:
|
136
|
+
static_names = [getattr(f, 'name', getattr(f, '__name__', '<unknown>')) for f in static_functions]
|
137
|
+
discovered_names = list(discovered_tool_names) # Names of unique discovered tools
|
138
|
+
combined_names = [getattr(f, 'name', getattr(f, '__name__', '<unknown>')) for f in final_functions]
|
139
|
+
logger.debug(f"[DEBUG] Agent '{agent_name}' - Static functions: {static_names}")
|
140
|
+
logger.debug(f"[DEBUG] Agent '{agent_name}' - Unique discovered tools: {discovered_names}")
|
141
|
+
logger.debug(f"[DEBUG] Agent '{agent_name}' - Final combined functions: {combined_names}")
|
142
|
+
|
143
|
+
logger.debug(f"Agent '{agent_name}' total functions/tools after merge: {len(final_functions)} (Static: {len(static_functions)}, Discovered: {len(all_discovered_tools)})")
|
144
|
+
return final_functions
|
145
|
+
|
146
|
+
|
147
|
+
async def discover_and_merge_agent_resources(agent: Agent, config: Dict[str, Any], debug: bool = False) -> List[Dict[str, Any]]:
|
148
|
+
"""
|
149
|
+
Discover resources from MCP servers listed in the agent's config and merge
|
150
|
+
them with the agent's statically defined resources.
|
151
|
+
|
152
|
+
Handles deduplication of discovered resources based on their 'uri'.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
agent: The agent instance for which to discover resources.
|
156
|
+
config: The main Swarm configuration dictionary containing MCP server details.
|
157
|
+
debug: If True, enable detailed debugging logs.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
List[Dict[str, Any]]: A combined list containing the agent's static resources
|
161
|
+
and unique resources discovered from its associated MCP servers.
|
162
|
+
Returns the agent's static resources if no MCP servers are defined.
|
163
|
+
Returns an empty list if the agent is None.
|
164
|
+
"""
|
165
|
+
if not agent:
|
166
|
+
logger.error("Cannot discover resources: Agent object is None.")
|
167
|
+
return []
|
168
|
+
agent_name = getattr(agent, "name", "UnnamedAgent")
|
169
|
+
|
170
|
+
logger.debug(f"Starting resource discovery for agent '{agent_name}'.")
|
171
|
+
mcp_server_names = getattr(agent, "mcp_servers", [])
|
172
|
+
|
173
|
+
# Get static resources, ensure it's a list
|
174
|
+
static_resources = getattr(agent, "resources", []) or []
|
175
|
+
if not isinstance(static_resources, list):
|
176
|
+
logger.warning(f"Agent '{agent_name}' resources attribute is not a list ({type(static_resources)}). Treating as empty.")
|
177
|
+
static_resources = []
|
178
|
+
# Ensure static resources are dicts (basic check)
|
179
|
+
static_resources = [r for r in static_resources if isinstance(r, dict)]
|
180
|
+
|
181
|
+
if not mcp_server_names:
|
182
|
+
res_names = [r.get('name', '<unnamed>') for r in static_resources]
|
183
|
+
logger.debug(f"Agent '{agent_name}' has no MCP servers listed. Returning {len(static_resources)} static resources: {res_names}")
|
184
|
+
return static_resources
|
185
|
+
|
186
|
+
# List to hold resources discovered from all MCP servers
|
187
|
+
all_discovered_resources: List[Dict[str, Any]] = []
|
188
|
+
|
189
|
+
# Iterate through each MCP server listed for the agent
|
190
|
+
for server_name in mcp_server_names:
|
191
|
+
if not isinstance(server_name, str):
|
192
|
+
logger.warning(f"Invalid MCP server name type for agent '{agent_name}': {type(server_name)}. Skipping.")
|
193
|
+
continue
|
194
|
+
|
195
|
+
logger.debug(f"Discovering resources from MCP server '{server_name}' for agent '{agent_name}'.")
|
196
|
+
server_config = config.get("mcpServers", {}).get(server_name)
|
197
|
+
if not server_config:
|
198
|
+
logger.warning(f"MCP server '{server_name}' configuration not found for agent '{agent_name}'. Skipping.")
|
199
|
+
continue
|
200
|
+
|
201
|
+
try:
|
202
|
+
provider = MCPToolProvider.get_instance(server_name, server_config, timeout=15, debug=debug)
|
203
|
+
# Fetch resources using the provider's client
|
204
|
+
# Assuming provider.client has a method like list_resources() that returns {'resources': [...]}
|
205
|
+
resources_response = await provider.client.list_resources()
|
206
|
+
|
207
|
+
# Validate the structure of the response
|
208
|
+
if not isinstance(resources_response, dict) or "resources" not in resources_response:
|
209
|
+
logger.warning(f"Invalid resources response format from MCP server '{server_name}' for agent '{agent_name}'. Expected dict with 'resources' key, got: {type(resources_response)}")
|
210
|
+
continue
|
211
|
+
|
212
|
+
resources_from_server = resources_response["resources"]
|
213
|
+
if not isinstance(resources_from_server, list):
|
214
|
+
logger.warning(f"Invalid 'resources' format in response from '{server_name}': Expected list, got {type(resources_from_server)}.")
|
215
|
+
continue
|
216
|
+
|
217
|
+
# Filter for valid resource dictionaries (must be dict and have 'uri')
|
218
|
+
valid_resources = [res for res in resources_from_server if isinstance(res, dict) and 'uri' in res]
|
219
|
+
invalid_count = len(resources_from_server) - len(valid_resources)
|
220
|
+
if invalid_count > 0:
|
221
|
+
logger.warning(f"Filtered out {invalid_count} invalid resource entries from '{server_name}'.")
|
222
|
+
|
223
|
+
all_discovered_resources.extend(valid_resources)
|
224
|
+
res_names_log = [r.get('name', '<unnamed>') for r in valid_resources]
|
225
|
+
logger.debug(f"Discovered {len(valid_resources)} valid resources from '{server_name}': {res_names_log}")
|
226
|
+
|
227
|
+
except AttributeError:
|
228
|
+
logger.error(f"MCPToolProvider client for '{server_name}' does not have a 'list_resources' method.", exc_info=debug)
|
229
|
+
except Exception as e:
|
230
|
+
logger.error(f"Failed to discover resources from MCP server '{server_name}' for agent '{agent_name}': {e}", exc_info=debug)
|
231
|
+
|
232
|
+
# Deduplicate discovered resources based on 'uri'
|
233
|
+
# Use a dictionary to keep only the first occurrence of each URI
|
234
|
+
unique_discovered_resources_map: Dict[str, Dict[str, Any]] = {}
|
235
|
+
for resource in all_discovered_resources:
|
236
|
+
uri = resource.get('uri') # URI is expected from validation above
|
237
|
+
if uri not in unique_discovered_resources_map:
|
238
|
+
unique_discovered_resources_map[uri] = resource
|
239
|
+
|
240
|
+
unique_discovered_resources_list = list(unique_discovered_resources_map.values())
|
241
|
+
|
242
|
+
# Combine static resources with unique discovered resources
|
243
|
+
# Create a map of static resource URIs to prevent duplicates if they also exist in discovered
|
244
|
+
static_resource_uris = {res.get('uri') for res in static_resources if res.get('uri')}
|
245
|
+
final_resources = static_resources + [
|
246
|
+
res for res in unique_discovered_resources_list if res.get('uri') not in static_resource_uris
|
247
|
+
]
|
248
|
+
|
249
|
+
if debug:
|
250
|
+
static_names = [r.get('name', '<unnamed>') for r in static_resources]
|
251
|
+
discovered_names = [r.get('name', '<unnamed>') for r in all_discovered_resources] # Before dedupe
|
252
|
+
unique_discovered_names = [r.get('name', '<unnamed>') for r in unique_discovered_resources_list] # After dedupe
|
253
|
+
combined_names = [r.get('name', '<unnamed>') for r in final_resources]
|
254
|
+
logger.debug(f"[DEBUG] Agent '{agent_name}' - Static resources: {static_names}")
|
255
|
+
logger.debug(f"[DEBUG] Agent '{agent_name}' - Discovered resources (before URI dedupe): {discovered_names}")
|
256
|
+
logger.debug(f"[DEBUG] Agent '{agent_name}' - Unique discovered resources (after URI dedupe): {unique_discovered_names}")
|
257
|
+
logger.debug(f"[DEBUG] Agent '{agent_name}' - Final combined resources: {combined_names}")
|
258
|
+
|
259
|
+
logger.debug(f"Agent '{agent_name}' total resources after merge: {len(final_resources)} (Static: {len(static_resources)}, Unique Discovered: {len(unique_discovered_resources_list)})")
|
260
|
+
return final_resources
|