agentpool 2.1.9__py3-none-any.whl → 2.5.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.
- acp/__init__.py +13 -4
- acp/acp_requests.py +20 -77
- acp/agent/connection.py +8 -0
- acp/agent/implementations/debug_server/debug_server.py +6 -2
- acp/agent/protocol.py +6 -0
- acp/bridge/README.md +15 -2
- acp/bridge/__init__.py +3 -2
- acp/bridge/__main__.py +60 -19
- acp/bridge/ws_server.py +173 -0
- acp/bridge/ws_server_cli.py +89 -0
- acp/client/connection.py +38 -29
- acp/client/implementations/default_client.py +3 -2
- acp/client/implementations/headless_client.py +2 -2
- acp/connection.py +2 -2
- acp/notifications.py +20 -50
- acp/schema/__init__.py +2 -0
- acp/schema/agent_responses.py +21 -0
- acp/schema/client_requests.py +3 -3
- acp/schema/session_state.py +63 -29
- acp/stdio.py +39 -9
- acp/task/supervisor.py +2 -2
- acp/transports.py +362 -2
- acp/utils.py +17 -4
- agentpool/__init__.py +6 -1
- agentpool/agents/__init__.py +2 -0
- agentpool/agents/acp_agent/acp_agent.py +407 -277
- agentpool/agents/acp_agent/acp_converters.py +196 -38
- agentpool/agents/acp_agent/client_handler.py +191 -26
- agentpool/agents/acp_agent/session_state.py +17 -6
- agentpool/agents/agent.py +607 -572
- agentpool/agents/agui_agent/__init__.py +0 -2
- agentpool/agents/agui_agent/agui_agent.py +176 -110
- agentpool/agents/agui_agent/agui_converters.py +0 -131
- agentpool/agents/agui_agent/helpers.py +3 -4
- agentpool/agents/base_agent.py +632 -17
- agentpool/agents/claude_code_agent/FORKING.md +191 -0
- agentpool/agents/claude_code_agent/__init__.py +13 -1
- agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
- agentpool/agents/claude_code_agent/converters.py +74 -143
- agentpool/agents/claude_code_agent/history.py +474 -0
- agentpool/agents/claude_code_agent/models.py +77 -0
- agentpool/agents/claude_code_agent/static_info.py +100 -0
- agentpool/agents/claude_code_agent/usage.py +242 -0
- agentpool/agents/context.py +40 -0
- agentpool/agents/events/__init__.py +24 -0
- agentpool/agents/events/builtin_handlers.py +67 -1
- agentpool/agents/events/event_emitter.py +32 -2
- agentpool/agents/events/events.py +104 -3
- agentpool/agents/events/infer_info.py +145 -0
- agentpool/agents/events/processors.py +254 -0
- agentpool/agents/interactions.py +41 -6
- agentpool/agents/modes.py +67 -0
- agentpool/agents/slashed_agent.py +5 -4
- agentpool/agents/tool_call_accumulator.py +213 -0
- agentpool/agents/tool_wrapping.py +18 -6
- agentpool/common_types.py +56 -21
- agentpool/config_resources/__init__.py +38 -1
- agentpool/config_resources/acp_assistant.yml +2 -2
- agentpool/config_resources/agents.yml +3 -0
- agentpool/config_resources/agents_template.yml +1 -0
- agentpool/config_resources/claude_code_agent.yml +10 -6
- agentpool/config_resources/external_acp_agents.yml +2 -1
- agentpool/delegation/base_team.py +4 -30
- agentpool/delegation/pool.py +136 -289
- agentpool/delegation/team.py +58 -57
- agentpool/delegation/teamrun.py +51 -55
- agentpool/diagnostics/__init__.py +53 -0
- agentpool/diagnostics/lsp_manager.py +1593 -0
- agentpool/diagnostics/lsp_proxy.py +41 -0
- agentpool/diagnostics/lsp_proxy_script.py +229 -0
- agentpool/diagnostics/models.py +398 -0
- agentpool/functional/run.py +10 -4
- agentpool/mcp_server/__init__.py +0 -2
- agentpool/mcp_server/client.py +76 -32
- agentpool/mcp_server/conversions.py +54 -13
- agentpool/mcp_server/manager.py +34 -54
- agentpool/mcp_server/registries/official_registry_client.py +35 -1
- agentpool/mcp_server/tool_bridge.py +186 -139
- agentpool/messaging/__init__.py +0 -2
- agentpool/messaging/compaction.py +72 -197
- agentpool/messaging/connection_manager.py +11 -10
- agentpool/messaging/event_manager.py +5 -5
- agentpool/messaging/message_container.py +6 -30
- agentpool/messaging/message_history.py +99 -8
- agentpool/messaging/messagenode.py +52 -14
- agentpool/messaging/messages.py +54 -35
- agentpool/messaging/processing.py +12 -22
- agentpool/models/__init__.py +1 -1
- agentpool/models/acp_agents/base.py +6 -24
- agentpool/models/acp_agents/mcp_capable.py +126 -157
- agentpool/models/acp_agents/non_mcp.py +129 -95
- agentpool/models/agents.py +98 -76
- agentpool/models/agui_agents.py +1 -1
- agentpool/models/claude_code_agents.py +144 -19
- agentpool/models/file_parsing.py +0 -1
- agentpool/models/manifest.py +113 -50
- agentpool/prompts/conversion_manager.py +1 -1
- agentpool/prompts/prompts.py +5 -2
- agentpool/repomap.py +1 -1
- agentpool/resource_providers/__init__.py +11 -1
- agentpool/resource_providers/aggregating.py +56 -5
- agentpool/resource_providers/base.py +70 -4
- agentpool/resource_providers/codemode/code_executor.py +72 -5
- agentpool/resource_providers/codemode/helpers.py +2 -2
- agentpool/resource_providers/codemode/provider.py +64 -12
- agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
- agentpool/resource_providers/codemode/remote_provider.py +9 -12
- agentpool/resource_providers/filtering.py +3 -1
- agentpool/resource_providers/mcp_provider.py +89 -12
- agentpool/resource_providers/plan_provider.py +228 -46
- agentpool/resource_providers/pool.py +7 -3
- agentpool/resource_providers/resource_info.py +111 -0
- agentpool/resource_providers/static.py +4 -2
- agentpool/sessions/__init__.py +4 -1
- agentpool/sessions/manager.py +33 -5
- agentpool/sessions/models.py +59 -6
- agentpool/sessions/protocol.py +28 -0
- agentpool/sessions/session.py +11 -55
- agentpool/skills/registry.py +13 -8
- agentpool/storage/manager.py +572 -49
- agentpool/talk/registry.py +4 -4
- agentpool/talk/talk.py +9 -10
- agentpool/testing.py +538 -20
- agentpool/tool_impls/__init__.py +6 -0
- agentpool/tool_impls/agent_cli/__init__.py +42 -0
- agentpool/tool_impls/agent_cli/tool.py +95 -0
- agentpool/tool_impls/bash/__init__.py +64 -0
- agentpool/tool_impls/bash/helpers.py +35 -0
- agentpool/tool_impls/bash/tool.py +171 -0
- agentpool/tool_impls/delete_path/__init__.py +70 -0
- agentpool/tool_impls/delete_path/tool.py +142 -0
- agentpool/tool_impls/download_file/__init__.py +80 -0
- agentpool/tool_impls/download_file/tool.py +183 -0
- agentpool/tool_impls/execute_code/__init__.py +55 -0
- agentpool/tool_impls/execute_code/tool.py +163 -0
- agentpool/tool_impls/grep/__init__.py +80 -0
- agentpool/tool_impls/grep/tool.py +200 -0
- agentpool/tool_impls/list_directory/__init__.py +73 -0
- agentpool/tool_impls/list_directory/tool.py +197 -0
- agentpool/tool_impls/question/__init__.py +42 -0
- agentpool/tool_impls/question/tool.py +127 -0
- agentpool/tool_impls/read/__init__.py +104 -0
- agentpool/tool_impls/read/tool.py +305 -0
- agentpool/tools/__init__.py +2 -1
- agentpool/tools/base.py +114 -34
- agentpool/tools/manager.py +57 -1
- agentpool/ui/base.py +2 -2
- agentpool/ui/mock_provider.py +2 -2
- agentpool/ui/stdlib_provider.py +2 -2
- agentpool/utils/file_watcher.py +269 -0
- agentpool/utils/identifiers.py +121 -0
- agentpool/utils/pydantic_ai_helpers.py +46 -0
- agentpool/utils/streams.py +616 -2
- agentpool/utils/subprocess_utils.py +155 -0
- agentpool/utils/token_breakdown.py +461 -0
- agentpool/vfs_registry.py +7 -2
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
- agentpool-2.5.0.dist-info/RECORD +579 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
- agentpool_cli/__main__.py +24 -0
- agentpool_cli/create.py +1 -1
- agentpool_cli/serve_acp.py +100 -21
- agentpool_cli/serve_agui.py +87 -0
- agentpool_cli/serve_opencode.py +119 -0
- agentpool_cli/ui.py +557 -0
- agentpool_commands/__init__.py +42 -5
- agentpool_commands/agents.py +75 -2
- agentpool_commands/history.py +62 -0
- agentpool_commands/mcp.py +176 -0
- agentpool_commands/models.py +56 -3
- agentpool_commands/pool.py +260 -0
- agentpool_commands/session.py +1 -1
- agentpool_commands/text_sharing/__init__.py +119 -0
- agentpool_commands/text_sharing/base.py +123 -0
- agentpool_commands/text_sharing/github_gist.py +80 -0
- agentpool_commands/text_sharing/opencode.py +462 -0
- agentpool_commands/text_sharing/paste_rs.py +59 -0
- agentpool_commands/text_sharing/pastebin.py +116 -0
- agentpool_commands/text_sharing/shittycodingagent.py +112 -0
- agentpool_commands/tools.py +57 -0
- agentpool_commands/utils.py +80 -30
- agentpool_config/__init__.py +30 -2
- agentpool_config/agentpool_tools.py +498 -0
- agentpool_config/builtin_tools.py +77 -22
- agentpool_config/commands.py +24 -1
- agentpool_config/compaction.py +258 -0
- agentpool_config/converters.py +1 -1
- agentpool_config/event_handlers.py +42 -0
- agentpool_config/events.py +1 -1
- agentpool_config/forward_targets.py +1 -4
- agentpool_config/jinja.py +3 -3
- agentpool_config/mcp_server.py +132 -6
- agentpool_config/nodes.py +1 -1
- agentpool_config/observability.py +44 -0
- agentpool_config/session.py +0 -3
- agentpool_config/storage.py +82 -38
- agentpool_config/task.py +3 -3
- agentpool_config/tools.py +11 -22
- agentpool_config/toolsets.py +109 -233
- agentpool_server/a2a_server/agent_worker.py +307 -0
- agentpool_server/a2a_server/server.py +23 -18
- agentpool_server/acp_server/acp_agent.py +234 -181
- agentpool_server/acp_server/commands/acp_commands.py +151 -156
- agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
- agentpool_server/acp_server/event_converter.py +651 -0
- agentpool_server/acp_server/input_provider.py +53 -10
- agentpool_server/acp_server/server.py +24 -90
- agentpool_server/acp_server/session.py +173 -331
- agentpool_server/acp_server/session_manager.py +8 -34
- agentpool_server/agui_server/server.py +3 -1
- agentpool_server/mcp_server/server.py +5 -2
- agentpool_server/opencode_server/.rules +95 -0
- agentpool_server/opencode_server/ENDPOINTS.md +401 -0
- agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
- agentpool_server/opencode_server/__init__.py +19 -0
- agentpool_server/opencode_server/command_validation.py +172 -0
- agentpool_server/opencode_server/converters.py +975 -0
- agentpool_server/opencode_server/dependencies.py +24 -0
- agentpool_server/opencode_server/input_provider.py +421 -0
- agentpool_server/opencode_server/models/__init__.py +250 -0
- agentpool_server/opencode_server/models/agent.py +53 -0
- agentpool_server/opencode_server/models/app.py +72 -0
- agentpool_server/opencode_server/models/base.py +26 -0
- agentpool_server/opencode_server/models/common.py +23 -0
- agentpool_server/opencode_server/models/config.py +37 -0
- agentpool_server/opencode_server/models/events.py +821 -0
- agentpool_server/opencode_server/models/file.py +88 -0
- agentpool_server/opencode_server/models/mcp.py +44 -0
- agentpool_server/opencode_server/models/message.py +179 -0
- agentpool_server/opencode_server/models/parts.py +323 -0
- agentpool_server/opencode_server/models/provider.py +81 -0
- agentpool_server/opencode_server/models/pty.py +43 -0
- agentpool_server/opencode_server/models/question.py +56 -0
- agentpool_server/opencode_server/models/session.py +111 -0
- agentpool_server/opencode_server/routes/__init__.py +29 -0
- agentpool_server/opencode_server/routes/agent_routes.py +473 -0
- agentpool_server/opencode_server/routes/app_routes.py +202 -0
- agentpool_server/opencode_server/routes/config_routes.py +302 -0
- agentpool_server/opencode_server/routes/file_routes.py +571 -0
- agentpool_server/opencode_server/routes/global_routes.py +94 -0
- agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
- agentpool_server/opencode_server/routes/message_routes.py +761 -0
- agentpool_server/opencode_server/routes/permission_routes.py +63 -0
- agentpool_server/opencode_server/routes/pty_routes.py +300 -0
- agentpool_server/opencode_server/routes/question_routes.py +128 -0
- agentpool_server/opencode_server/routes/session_routes.py +1276 -0
- agentpool_server/opencode_server/routes/tui_routes.py +139 -0
- agentpool_server/opencode_server/server.py +475 -0
- agentpool_server/opencode_server/state.py +151 -0
- agentpool_server/opencode_server/time_utils.py +8 -0
- agentpool_storage/__init__.py +12 -0
- agentpool_storage/base.py +184 -2
- agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
- agentpool_storage/claude_provider/__init__.py +42 -0
- agentpool_storage/claude_provider/provider.py +1089 -0
- agentpool_storage/file_provider.py +278 -15
- agentpool_storage/memory_provider.py +193 -12
- agentpool_storage/models.py +3 -0
- agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
- agentpool_storage/opencode_provider/__init__.py +16 -0
- agentpool_storage/opencode_provider/helpers.py +414 -0
- agentpool_storage/opencode_provider/provider.py +895 -0
- agentpool_storage/project_store.py +325 -0
- agentpool_storage/session_store.py +26 -6
- agentpool_storage/sql_provider/__init__.py +4 -2
- agentpool_storage/sql_provider/models.py +48 -0
- agentpool_storage/sql_provider/sql_provider.py +269 -3
- agentpool_storage/sql_provider/utils.py +12 -13
- agentpool_storage/zed_provider/__init__.py +16 -0
- agentpool_storage/zed_provider/helpers.py +281 -0
- agentpool_storage/zed_provider/models.py +130 -0
- agentpool_storage/zed_provider/provider.py +442 -0
- agentpool_storage/zed_provider.py +803 -0
- agentpool_toolsets/__init__.py +0 -2
- agentpool_toolsets/builtin/__init__.py +2 -12
- agentpool_toolsets/builtin/code.py +96 -57
- agentpool_toolsets/builtin/debug.py +118 -48
- agentpool_toolsets/builtin/execution_environment.py +115 -230
- agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
- agentpool_toolsets/builtin/skills.py +9 -4
- agentpool_toolsets/builtin/subagent_tools.py +64 -51
- agentpool_toolsets/builtin/workers.py +4 -2
- agentpool_toolsets/composio_toolset.py +2 -2
- agentpool_toolsets/entry_points.py +3 -1
- agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
- agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
- agentpool_toolsets/fsspec_toolset/grep.py +99 -7
- agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
- agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
- agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
- agentpool_toolsets/mcp_discovery/__init__.py +5 -0
- agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
- agentpool_toolsets/mcp_discovery/toolset.py +511 -0
- agentpool_toolsets/mcp_run_toolset.py +87 -12
- agentpool_toolsets/notifications.py +33 -33
- agentpool_toolsets/openapi.py +3 -1
- agentpool_toolsets/search_toolset.py +3 -1
- agentpool-2.1.9.dist-info/RECORD +0 -474
- agentpool_config/resources.py +0 -33
- agentpool_server/acp_server/acp_tools.py +0 -43
- agentpool_server/acp_server/commands/spawn.py +0 -210
- agentpool_storage/text_log_provider.py +0 -275
- agentpool_toolsets/builtin/agent_management.py +0 -239
- agentpool_toolsets/builtin/chain.py +0 -288
- agentpool_toolsets/builtin/history.py +0 -36
- agentpool_toolsets/builtin/integration.py +0 -85
- agentpool_toolsets/builtin/tool_management.py +0 -90
- agentpool_toolsets/builtin/user_interaction.py +0 -52
- agentpool_toolsets/semantic_memory_toolset.py +0 -536
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
- {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
Binary file
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
# mypy: disable-error-code="import-not-found,unused-ignore"
|
|
2
|
+
"""MCP Discovery Toolset - dynamic MCP server exploration and tool execution.
|
|
3
|
+
|
|
4
|
+
This toolset provides a stable interface for agents to discover and use MCP servers
|
|
5
|
+
on-demand without needing to preload all tools upfront. This preserves prompt cache
|
|
6
|
+
stability while enabling access to the entire MCP ecosystem.
|
|
7
|
+
|
|
8
|
+
The toolset exposes three main capabilities:
|
|
9
|
+
1. search_mcp_servers - Semantic search over 1000+ servers (uses pre-built index)
|
|
10
|
+
2. list_mcp_tools - Get tools from a specific server (connects on-demand)
|
|
11
|
+
3. call_mcp_tool - Execute a tool on any server (reuses connections)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from pydantic import HttpUrl
|
|
20
|
+
from pydantic_ai import RunContext # noqa: TC002
|
|
21
|
+
|
|
22
|
+
from agentpool.agents.context import AgentContext # noqa: TC001
|
|
23
|
+
from agentpool.log import get_logger
|
|
24
|
+
from agentpool.mcp_server.client import MCPClient
|
|
25
|
+
from agentpool.mcp_server.registries.official_registry_client import (
|
|
26
|
+
MCPRegistryClient,
|
|
27
|
+
MCPRegistryError,
|
|
28
|
+
)
|
|
29
|
+
from agentpool.resource_providers import ResourceProvider
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from collections.abc import Sequence
|
|
34
|
+
|
|
35
|
+
from fastmcp.client.sampling import SamplingHandler
|
|
36
|
+
|
|
37
|
+
from agentpool.mcp_server.registries.official_registry_client import RegistryServer
|
|
38
|
+
from agentpool.tools.base import Tool
|
|
39
|
+
from agentpool_config.mcp_server import MCPServerConfig
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
logger = get_logger(__name__)
|
|
43
|
+
|
|
44
|
+
# Path to the pre-built semantic search index (parquet file loaded into LanceDB at runtime)
|
|
45
|
+
PARQUET_PATH = Path(__file__).parent / "data" / "mcp_servers.parquet"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MCPDiscoveryToolset(ResourceProvider):
|
|
49
|
+
"""Toolset for dynamic MCP server discovery and tool execution.
|
|
50
|
+
|
|
51
|
+
This toolset allows agents to:
|
|
52
|
+
- Search the MCP registry for servers by keyword
|
|
53
|
+
- List tools available on a specific server
|
|
54
|
+
- Call tools on any server without preloading
|
|
55
|
+
|
|
56
|
+
Connections are managed lazily - servers are only connected when needed,
|
|
57
|
+
and connections are kept alive for the session duration.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
name: str = "mcp_discovery",
|
|
63
|
+
registry_url: str = "https://registry.modelcontextprotocol.io",
|
|
64
|
+
allowed_servers: list[str] | None = None,
|
|
65
|
+
blocked_servers: list[str] | None = None,
|
|
66
|
+
sampling_callback: SamplingHandler[Any, Any] | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Initialize the MCP Discovery toolset.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
name: Name for this toolset provider
|
|
72
|
+
registry_url: Base URL for the MCP registry API
|
|
73
|
+
allowed_servers: If set, only these server names can be used
|
|
74
|
+
blocked_servers: Server names that cannot be used
|
|
75
|
+
sampling_callback: Callback for MCP sampling requests
|
|
76
|
+
"""
|
|
77
|
+
super().__init__(name=name)
|
|
78
|
+
self._registry_url = registry_url
|
|
79
|
+
self._registry: MCPRegistryClient | None = None
|
|
80
|
+
self._connections: dict[str, MCPClient] = {}
|
|
81
|
+
self._server_cache: dict[str, RegistryServer] = {}
|
|
82
|
+
self._tools_cache: dict[str, list[dict[str, Any]]] = {}
|
|
83
|
+
self._allowed_servers = set(allowed_servers) if allowed_servers else None
|
|
84
|
+
self._blocked_servers = set(blocked_servers) if blocked_servers else set()
|
|
85
|
+
self._sampling_callback = sampling_callback
|
|
86
|
+
self._tools: list[Tool] | None = None
|
|
87
|
+
# Lazy-loaded semantic search components
|
|
88
|
+
self._db: Any = None
|
|
89
|
+
self._table: Any = None
|
|
90
|
+
self._embed_model: Any = None
|
|
91
|
+
self._tmpdir: str | None = None
|
|
92
|
+
|
|
93
|
+
def _get_registry(self) -> MCPRegistryClient:
|
|
94
|
+
"""Get or create the registry client."""
|
|
95
|
+
if self._registry is None:
|
|
96
|
+
self._registry = MCPRegistryClient(base_url=self._registry_url)
|
|
97
|
+
return self._registry
|
|
98
|
+
|
|
99
|
+
def _get_search_index(self) -> Any:
|
|
100
|
+
"""Get or create the LanceDB search index from parquet file."""
|
|
101
|
+
if self._table is not None:
|
|
102
|
+
return self._table
|
|
103
|
+
|
|
104
|
+
import tempfile
|
|
105
|
+
|
|
106
|
+
import lancedb # type: ignore[import-untyped]
|
|
107
|
+
import pyarrow as pa # type: ignore[import-untyped]
|
|
108
|
+
import pyarrow.parquet as pq # type: ignore[import-untyped]
|
|
109
|
+
|
|
110
|
+
if not PARQUET_PATH.exists():
|
|
111
|
+
msg = f"MCP registry index not found at {PARQUET_PATH}. Run build_mcp_registry_index.py"
|
|
112
|
+
raise FileNotFoundError(msg)
|
|
113
|
+
|
|
114
|
+
# Load parquet
|
|
115
|
+
arrow_table = pq.read_table(PARQUET_PATH)
|
|
116
|
+
|
|
117
|
+
# Convert vector column to fixed-size list for LanceDB vector search
|
|
118
|
+
# LanceDB requires fixed-size vectors, not variable-length lists
|
|
119
|
+
vectors = arrow_table.column("vector").to_pylist()
|
|
120
|
+
if vectors:
|
|
121
|
+
vec_dim = len(vectors[0])
|
|
122
|
+
fixed_vectors = pa.FixedSizeListArray.from_arrays(
|
|
123
|
+
pa.array([v for vec in vectors for v in vec], type=pa.float32()),
|
|
124
|
+
list_size=vec_dim,
|
|
125
|
+
)
|
|
126
|
+
# Replace the vector column
|
|
127
|
+
col_idx = arrow_table.schema.get_field_index("vector")
|
|
128
|
+
arrow_table = arrow_table.set_column(col_idx, "vector", fixed_vectors)
|
|
129
|
+
|
|
130
|
+
# Use a temp directory for LanceDB (it needs a path but we're loading from parquet)
|
|
131
|
+
if self._db is None:
|
|
132
|
+
self._tmpdir = tempfile.mkdtemp(prefix="mcp_discovery_")
|
|
133
|
+
self._db = lancedb.connect(self._tmpdir)
|
|
134
|
+
|
|
135
|
+
self._table = self._db.create_table("servers", arrow_table, mode="overwrite")
|
|
136
|
+
return self._table
|
|
137
|
+
|
|
138
|
+
def _get_embed_model(self) -> Any:
|
|
139
|
+
"""Get or create the FastEmbed model for query embedding."""
|
|
140
|
+
if self._embed_model is not None:
|
|
141
|
+
return self._embed_model
|
|
142
|
+
|
|
143
|
+
from fastembed import TextEmbedding
|
|
144
|
+
|
|
145
|
+
self._embed_model = TextEmbedding("BAAI/bge-small-en-v1.5")
|
|
146
|
+
return self._embed_model
|
|
147
|
+
|
|
148
|
+
def _is_server_allowed(self, server_name: str) -> bool:
|
|
149
|
+
"""Check if a server is allowed to be used."""
|
|
150
|
+
if server_name in self._blocked_servers:
|
|
151
|
+
return False
|
|
152
|
+
if self._allowed_servers is not None:
|
|
153
|
+
return server_name in self._allowed_servers
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
async def _get_server_config(self, server_name: str) -> MCPServerConfig:
|
|
157
|
+
"""Get connection config for a server from the registry."""
|
|
158
|
+
from agentpool_config.mcp_server import (
|
|
159
|
+
SSEMCPServerConfig,
|
|
160
|
+
StreamableHTTPMCPServerConfig,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Check cache first
|
|
164
|
+
server: RegistryServer
|
|
165
|
+
if server_name in self._server_cache:
|
|
166
|
+
server = self._server_cache[server_name]
|
|
167
|
+
else:
|
|
168
|
+
# Use list_servers and find by name - more reliable than get_server
|
|
169
|
+
registry = self._get_registry()
|
|
170
|
+
all_servers = await registry.list_servers()
|
|
171
|
+
found_server: RegistryServer | None = None
|
|
172
|
+
for s in all_servers:
|
|
173
|
+
if s.name == server_name:
|
|
174
|
+
found_server = s
|
|
175
|
+
break
|
|
176
|
+
if found_server is None:
|
|
177
|
+
msg = f"Server {server_name!r} not found in registry"
|
|
178
|
+
raise MCPRegistryError(msg)
|
|
179
|
+
server = found_server
|
|
180
|
+
self._server_cache[server_name] = server
|
|
181
|
+
|
|
182
|
+
# Find a usable remote endpoint
|
|
183
|
+
for remote in server.remotes:
|
|
184
|
+
if remote.type == "sse":
|
|
185
|
+
return SSEMCPServerConfig(url=HttpUrl(remote.url))
|
|
186
|
+
if remote.type in ("streamable-http", "http"):
|
|
187
|
+
return StreamableHTTPMCPServerConfig(url=HttpUrl(remote.url))
|
|
188
|
+
|
|
189
|
+
msg = f"No supported remote transport for server {server_name!r}"
|
|
190
|
+
raise MCPRegistryError(msg)
|
|
191
|
+
|
|
192
|
+
async def _get_connection(self, server_name: str) -> MCPClient:
|
|
193
|
+
"""Get or create a connection to a server."""
|
|
194
|
+
if server_name in self._connections:
|
|
195
|
+
client = self._connections[server_name]
|
|
196
|
+
if client.connected:
|
|
197
|
+
return client
|
|
198
|
+
# Connection dropped, remove and reconnect
|
|
199
|
+
del self._connections[server_name]
|
|
200
|
+
|
|
201
|
+
config = await self._get_server_config(server_name)
|
|
202
|
+
client = MCPClient(config=config, sampling_callback=self._sampling_callback)
|
|
203
|
+
await client.__aenter__()
|
|
204
|
+
self._connections[server_name] = client
|
|
205
|
+
logger.info("Connected to MCP server", server=server_name)
|
|
206
|
+
return client
|
|
207
|
+
|
|
208
|
+
async def _close_connections(self) -> None:
|
|
209
|
+
"""Close all active connections."""
|
|
210
|
+
for name, client in list(self._connections.items()):
|
|
211
|
+
try:
|
|
212
|
+
await client.__aexit__(None, None, None)
|
|
213
|
+
logger.debug("Closed connection", server=name)
|
|
214
|
+
except Exception as e: # noqa: BLE001
|
|
215
|
+
logger.warning("Error closing connection", server=name, error=e)
|
|
216
|
+
self._connections.clear()
|
|
217
|
+
|
|
218
|
+
async def get_tools(self) -> Sequence[Tool]:
|
|
219
|
+
"""Get the discovery tools."""
|
|
220
|
+
if self._tools is not None:
|
|
221
|
+
return self._tools
|
|
222
|
+
|
|
223
|
+
self._tools = [
|
|
224
|
+
self.create_tool(
|
|
225
|
+
self.search_mcp_servers,
|
|
226
|
+
category="search",
|
|
227
|
+
read_only=True,
|
|
228
|
+
idempotent=True,
|
|
229
|
+
),
|
|
230
|
+
self.create_tool(
|
|
231
|
+
self.list_mcp_tools,
|
|
232
|
+
category="search",
|
|
233
|
+
read_only=True,
|
|
234
|
+
idempotent=True,
|
|
235
|
+
),
|
|
236
|
+
self.create_tool(
|
|
237
|
+
self.call_mcp_tool,
|
|
238
|
+
category="execute",
|
|
239
|
+
open_world=True,
|
|
240
|
+
),
|
|
241
|
+
]
|
|
242
|
+
return self._tools
|
|
243
|
+
|
|
244
|
+
async def search_mcp_servers( # noqa: D417
|
|
245
|
+
self,
|
|
246
|
+
agent_ctx: AgentContext,
|
|
247
|
+
query: str,
|
|
248
|
+
max_results: int = 10,
|
|
249
|
+
) -> str:
|
|
250
|
+
"""Search the MCP registry for servers matching a query.
|
|
251
|
+
|
|
252
|
+
Uses semantic search over 1000+ indexed MCP servers. The search understands
|
|
253
|
+
meaning, not just keywords - e.g., "web scraping" finds crawlers too.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
query: Search term (e.g., "github issues", "database sql", "file system")
|
|
257
|
+
max_results: Maximum number of results to return
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
List of matching servers with names and descriptions
|
|
261
|
+
"""
|
|
262
|
+
await agent_ctx.events.tool_call_start(
|
|
263
|
+
title=f"Searching MCP servers: {query}",
|
|
264
|
+
kind="search",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
# Get embedding for query
|
|
269
|
+
model = self._get_embed_model()
|
|
270
|
+
query_embedding = next(iter(model.embed([query]))).tolist()
|
|
271
|
+
|
|
272
|
+
# Search the index
|
|
273
|
+
table = self._get_search_index()
|
|
274
|
+
results = table.search(query_embedding).limit(max_results * 2).to_arrow()
|
|
275
|
+
|
|
276
|
+
if len(results) == 0:
|
|
277
|
+
return f"No MCP servers found matching '{query}'"
|
|
278
|
+
|
|
279
|
+
# Format results, filtering by allowed/blocked
|
|
280
|
+
lines = [f"Found MCP servers matching '{query}':\n"]
|
|
281
|
+
count = 0
|
|
282
|
+
for i in range(len(results)):
|
|
283
|
+
name = results["name"][i].as_py()
|
|
284
|
+
|
|
285
|
+
# Filter by allowed/blocked
|
|
286
|
+
if not self._is_server_allowed(name):
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
desc = results["description"][i].as_py()
|
|
290
|
+
version = results["version"][i].as_py()
|
|
291
|
+
has_remote = results["has_remote"][i].as_py()
|
|
292
|
+
remote_types = results["remote_types"][i].as_py()
|
|
293
|
+
|
|
294
|
+
lines.append(f"**{name}** (v{version})")
|
|
295
|
+
lines.append(f" {desc}")
|
|
296
|
+
if has_remote and remote_types:
|
|
297
|
+
lines.append(f" Transports: {remote_types}")
|
|
298
|
+
lines.append("")
|
|
299
|
+
|
|
300
|
+
count += 1
|
|
301
|
+
if count >= max_results:
|
|
302
|
+
break
|
|
303
|
+
|
|
304
|
+
if count == 0:
|
|
305
|
+
return f"No MCP servers found matching '{query}'"
|
|
306
|
+
|
|
307
|
+
lines[0] = f"Found {count} MCP servers matching '{query}':\n"
|
|
308
|
+
return "\n".join(lines)
|
|
309
|
+
|
|
310
|
+
except FileNotFoundError as e:
|
|
311
|
+
return f"Error: {e}"
|
|
312
|
+
except Exception as e:
|
|
313
|
+
logger.exception("Error searching MCP servers")
|
|
314
|
+
return f"Error searching MCP servers: {e}"
|
|
315
|
+
|
|
316
|
+
async def list_mcp_tools( # noqa: D417
|
|
317
|
+
self,
|
|
318
|
+
agent_ctx: AgentContext,
|
|
319
|
+
server_name: str,
|
|
320
|
+
) -> str:
|
|
321
|
+
"""List all tools available on a specific MCP server.
|
|
322
|
+
|
|
323
|
+
This connects to the server (if not already connected) and retrieves
|
|
324
|
+
the list of available tools with their descriptions and parameters.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
server_name: Name of the MCP server (e.g., "com.github/github")
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
List of tools with names, descriptions, and parameter schemas
|
|
331
|
+
"""
|
|
332
|
+
await agent_ctx.events.tool_call_start(
|
|
333
|
+
title=f"Listing tools from: {server_name}",
|
|
334
|
+
kind="search",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if not self._is_server_allowed(server_name):
|
|
338
|
+
return f"Error: Server '{server_name}' is not allowed"
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
# Check tools cache first
|
|
342
|
+
if server_name in self._tools_cache:
|
|
343
|
+
tools_data = self._tools_cache[server_name]
|
|
344
|
+
else:
|
|
345
|
+
client = await self._get_connection(server_name)
|
|
346
|
+
mcp_tools = await client.list_tools()
|
|
347
|
+
|
|
348
|
+
# Convert to serializable format and cache
|
|
349
|
+
tools_data = []
|
|
350
|
+
for tool in mcp_tools:
|
|
351
|
+
tool_info: dict[str, Any] = {
|
|
352
|
+
"name": tool.name,
|
|
353
|
+
"description": tool.description or "No description",
|
|
354
|
+
}
|
|
355
|
+
# Include parameter info
|
|
356
|
+
if tool.inputSchema:
|
|
357
|
+
props = tool.inputSchema.get("properties", {})
|
|
358
|
+
required = set(tool.inputSchema.get("required", []))
|
|
359
|
+
params = []
|
|
360
|
+
for pname, pschema in props.items():
|
|
361
|
+
param_str = pname
|
|
362
|
+
if pname in required:
|
|
363
|
+
param_str += " (required)"
|
|
364
|
+
if "type" in pschema:
|
|
365
|
+
param_str += f": {pschema['type']}"
|
|
366
|
+
if "description" in pschema:
|
|
367
|
+
param_str += f" - {pschema['description']}"
|
|
368
|
+
params.append(param_str)
|
|
369
|
+
if params:
|
|
370
|
+
tool_info["parameters"] = params
|
|
371
|
+
tools_data.append(tool_info)
|
|
372
|
+
|
|
373
|
+
self._tools_cache[server_name] = tools_data
|
|
374
|
+
|
|
375
|
+
if not tools_data:
|
|
376
|
+
return f"No tools found on server '{server_name}'"
|
|
377
|
+
|
|
378
|
+
# Format output
|
|
379
|
+
lines = [f"Tools available on **{server_name}** ({len(tools_data)} tools):\n"]
|
|
380
|
+
for tool_info in tools_data:
|
|
381
|
+
lines.append(f"### {tool_info['name']}")
|
|
382
|
+
lines.append(f"{tool_info['description']}")
|
|
383
|
+
if "parameters" in tool_info:
|
|
384
|
+
lines.append("Parameters:")
|
|
385
|
+
lines.extend(f" - {param}" for param in tool_info["parameters"])
|
|
386
|
+
lines.append("")
|
|
387
|
+
|
|
388
|
+
return "\n".join(lines)
|
|
389
|
+
|
|
390
|
+
except MCPRegistryError as e:
|
|
391
|
+
return f"Error: {e}"
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.exception("Error listing MCP tools", server=server_name)
|
|
394
|
+
return f"Error listing tools from '{server_name}': {e}"
|
|
395
|
+
|
|
396
|
+
async def call_mcp_tool( # noqa: D417
|
|
397
|
+
self,
|
|
398
|
+
ctx: RunContext,
|
|
399
|
+
agent_ctx: AgentContext,
|
|
400
|
+
server_name: str,
|
|
401
|
+
tool_name: str,
|
|
402
|
+
arguments: dict[str, Any] | None = None,
|
|
403
|
+
) -> str | Any:
|
|
404
|
+
"""Call a tool on an MCP server.
|
|
405
|
+
|
|
406
|
+
Use this to execute a specific tool on an MCP server. The server
|
|
407
|
+
connection is reused if already established.
|
|
408
|
+
|
|
409
|
+
This properly supports progress reporting, elicitation, and sampling
|
|
410
|
+
through the AgentContext integration.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
server_name: Name of the MCP server (e.g., "com.github/github")
|
|
414
|
+
tool_name: Name of the tool to call
|
|
415
|
+
arguments: Arguments to pass to the tool
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
The result from the tool execution
|
|
419
|
+
"""
|
|
420
|
+
await agent_ctx.events.tool_call_start(
|
|
421
|
+
title=f"Calling {tool_name} on {server_name}",
|
|
422
|
+
kind="execute",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if not self._is_server_allowed(server_name):
|
|
426
|
+
return f"Error: Server '{server_name}' is not allowed"
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
client = await self._get_connection(server_name)
|
|
430
|
+
|
|
431
|
+
# Use MCPClient.call_tool which handles progress, elicitation, and sampling
|
|
432
|
+
return await client.call_tool(
|
|
433
|
+
name=tool_name,
|
|
434
|
+
run_context=ctx,
|
|
435
|
+
arguments=arguments or {},
|
|
436
|
+
agent_ctx=agent_ctx,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Result is already processed by MCPClient (ToolReturn, str, or structured data)
|
|
440
|
+
|
|
441
|
+
except MCPRegistryError as e:
|
|
442
|
+
return f"Error: {e}"
|
|
443
|
+
except Exception as e:
|
|
444
|
+
logger.exception("Error calling MCP tool", server=server_name, tool=tool_name)
|
|
445
|
+
return f"Error calling '{tool_name}' on '{server_name}': {e}"
|
|
446
|
+
|
|
447
|
+
async def cleanup(self) -> None:
|
|
448
|
+
"""Clean up resources."""
|
|
449
|
+
await self._close_connections()
|
|
450
|
+
if self._registry:
|
|
451
|
+
await self._registry.close()
|
|
452
|
+
self._registry = None
|
|
453
|
+
# Clean up temp directory used for LanceDB
|
|
454
|
+
if self._tmpdir:
|
|
455
|
+
import shutil
|
|
456
|
+
|
|
457
|
+
shutil.rmtree(self._tmpdir, ignore_errors=True)
|
|
458
|
+
self._tmpdir = None
|
|
459
|
+
self._db = None
|
|
460
|
+
self._table = None
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
if __name__ == "__main__":
|
|
464
|
+
import asyncio
|
|
465
|
+
|
|
466
|
+
from agentpool.agents.agent import Agent
|
|
467
|
+
|
|
468
|
+
async def main() -> None:
|
|
469
|
+
"""End-to-end example: Add MCP discovery toolset to an agent and call a tool."""
|
|
470
|
+
# Create the discovery toolset
|
|
471
|
+
toolset = MCPDiscoveryToolset(
|
|
472
|
+
name="mcp_discovery",
|
|
473
|
+
# Optionally restrict to specific servers for safety
|
|
474
|
+
# allowed_servers=["@modelcontextprotocol/server-everything"],
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Create an AgentPool agent with the toolset
|
|
478
|
+
agent = Agent(
|
|
479
|
+
model="openai:gpt-4o",
|
|
480
|
+
system_prompt="""
|
|
481
|
+
You are a helpful assistant with access to MCP servers.
|
|
482
|
+
Use the MCP discovery tools to search for and use tools from the MCP ecosystem.
|
|
483
|
+
""",
|
|
484
|
+
toolsets=[toolset],
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
async with agent:
|
|
488
|
+
# Example 1: Search for servers
|
|
489
|
+
print("\n=== Example 1: Search for file system servers ===")
|
|
490
|
+
result = await agent.run(
|
|
491
|
+
"Search for MCP servers related to file systems and show me the top 3 results"
|
|
492
|
+
)
|
|
493
|
+
print(result.data)
|
|
494
|
+
|
|
495
|
+
# Example 2: List tools from a specific server
|
|
496
|
+
print("\n=== Example 2: List tools from a server ===")
|
|
497
|
+
result = await agent.run(
|
|
498
|
+
"List the tools available on the '@modelcontextprotocol/server-everything' server"
|
|
499
|
+
)
|
|
500
|
+
print(result.data)
|
|
501
|
+
|
|
502
|
+
# Example 3: Call a tool (using a safe, read-only tool for demo)
|
|
503
|
+
print("\n=== Example 3: Call a tool ===")
|
|
504
|
+
result = await agent.run(
|
|
505
|
+
"""Use the MCP discovery to call the 'echo' tool from
|
|
506
|
+
'@modelcontextprotocol/server-everything' with the argument
|
|
507
|
+
message='Hello from MCP Discovery!'"""
|
|
508
|
+
)
|
|
509
|
+
print(result.data)
|
|
510
|
+
|
|
511
|
+
asyncio.run(main())
|
|
@@ -3,21 +3,39 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
-
from typing import TYPE_CHECKING, Any, cast
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal, Self, cast
|
|
7
7
|
|
|
8
8
|
from schemez import OpenAIFunctionDefinition
|
|
9
9
|
|
|
10
|
+
from agentpool.log import get_logger
|
|
10
11
|
from agentpool.resource_providers import ResourceProvider
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from contextlib import AbstractAsyncContextManager
|
|
17
|
+
from types import TracebackType
|
|
18
|
+
|
|
19
|
+
from mcp import ClientSession
|
|
14
20
|
from mcp.types import CallToolResult
|
|
15
21
|
|
|
16
22
|
from agentpool.tools.base import Tool
|
|
17
23
|
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
18
26
|
|
|
19
27
|
class McpRunTools(ResourceProvider):
|
|
20
|
-
"""Provider for MCP.run tools.
|
|
28
|
+
"""Provider for MCP.run tools.
|
|
29
|
+
|
|
30
|
+
Maintains a persistent SSE connection to MCP.run to receive tool change
|
|
31
|
+
notifications. Use as an async context manager to ensure proper cleanup.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
async with McpRunTools("default") as provider:
|
|
35
|
+
tools = await provider.get_tools()
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
kind: Literal["mcp_run"] = "mcp_run"
|
|
21
39
|
|
|
22
40
|
def __init__(self, entity_id: str, session_id: str | None = None) -> None:
|
|
23
41
|
from mcp_run import Client, ClientConfig # type: ignore[import-untyped]
|
|
@@ -27,8 +45,51 @@ class McpRunTools(ResourceProvider):
|
|
|
27
45
|
config = ClientConfig()
|
|
28
46
|
self.client = Client(session_id=id_, config=config)
|
|
29
47
|
self._tools: list[Tool] | None = None
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
self._session: ClientSession | None = None
|
|
49
|
+
# Context manager for persistent connection
|
|
50
|
+
self._mcp_client_ctx: AbstractAsyncContextManager[ClientSession] | None = None
|
|
51
|
+
|
|
52
|
+
async def __aenter__(self) -> Self:
|
|
53
|
+
"""Start persistent SSE connection."""
|
|
54
|
+
mcp_client = self.client.mcp_sse()
|
|
55
|
+
self._mcp_client_ctx = mcp_client.connect()
|
|
56
|
+
self._session = await self._mcp_client_ctx.__aenter__()
|
|
57
|
+
# Set up notification handler for tool changes
|
|
58
|
+
# The MCP ClientSession dispatches notifications via _received_notification
|
|
59
|
+
# We monkey-patch it to intercept ToolListChangedNotification
|
|
60
|
+
original_handler = self._session._received_notification
|
|
61
|
+
|
|
62
|
+
async def notification_handler(notification: Any) -> None:
|
|
63
|
+
from mcp.types import ToolListChangedNotification
|
|
64
|
+
|
|
65
|
+
if isinstance(notification.root, ToolListChangedNotification):
|
|
66
|
+
logger.info("MCP.run tool list changed notification received")
|
|
67
|
+
await self._on_tools_changed()
|
|
68
|
+
await original_handler(notification)
|
|
69
|
+
|
|
70
|
+
self._session._received_notification = notification_handler # type: ignore[method-assign]
|
|
71
|
+
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
async def __aexit__(
|
|
75
|
+
self,
|
|
76
|
+
exc_type: type[BaseException] | None,
|
|
77
|
+
exc_val: BaseException | None,
|
|
78
|
+
exc_tb: TracebackType | None,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Close persistent SSE connection."""
|
|
81
|
+
if self._mcp_client_ctx:
|
|
82
|
+
await self._mcp_client_ctx.__aexit__(exc_type, exc_val, exc_tb)
|
|
83
|
+
self._mcp_client_ctx = None
|
|
84
|
+
self._session = None
|
|
85
|
+
|
|
86
|
+
async def _on_tools_changed(self) -> None:
|
|
87
|
+
"""Handle tool list change notification."""
|
|
88
|
+
logger.info("MCP.run tools changed, refreshing cache")
|
|
89
|
+
self._tools = None
|
|
90
|
+
await self.tools_changed.emit(self.create_change_event("tools"))
|
|
91
|
+
|
|
92
|
+
async def get_tools(self) -> Sequence[Tool]:
|
|
32
93
|
"""Get tools from MCP.run."""
|
|
33
94
|
# Return cached tools if available
|
|
34
95
|
if self._tools is not None:
|
|
@@ -36,19 +97,33 @@ class McpRunTools(ResourceProvider):
|
|
|
36
97
|
|
|
37
98
|
self._tools = []
|
|
38
99
|
for name, tool in self.client.tools.items():
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
100
|
+
session = self._session # Capture session for use in tool calls
|
|
101
|
+
|
|
102
|
+
async def run(
|
|
103
|
+
tool_name: str = name,
|
|
104
|
+
_session: ClientSession | None = session,
|
|
105
|
+
**input_dict: Any,
|
|
106
|
+
) -> CallToolResult:
|
|
107
|
+
if _session is not None:
|
|
108
|
+
# Use persistent session if available
|
|
109
|
+
return await _session.call_tool(tool_name, arguments=input_dict)
|
|
110
|
+
# Fallback to creating a new connection (when not using context manager)
|
|
111
|
+
async with self.client.mcp_sse().connect() as new_session:
|
|
112
|
+
return await new_session.call_tool(tool_name, arguments=input_dict) # type: ignore[no-any-return]
|
|
43
113
|
|
|
44
114
|
run.__name__ = name
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
115
|
+
schema = cast(OpenAIFunctionDefinition, tool.input_schema)
|
|
116
|
+
wrapped_tool = self.create_tool(run, schema_override=schema)
|
|
48
117
|
self._tools.append(wrapped_tool)
|
|
49
|
-
|
|
50
118
|
return self._tools
|
|
51
119
|
|
|
120
|
+
async def refresh_tools(self) -> None:
|
|
121
|
+
"""Manually refresh tools from MCP.run and emit change event."""
|
|
122
|
+
logger.info("Manually refreshing MCP.run tools")
|
|
123
|
+
self._tools = None
|
|
124
|
+
await self.get_tools()
|
|
125
|
+
await self.tools_changed.emit(self.create_change_event("tools"))
|
|
126
|
+
|
|
52
127
|
|
|
53
128
|
if __name__ == "__main__":
|
|
54
129
|
import anyio
|