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.
Files changed (311) hide show
  1. acp/__init__.py +13 -4
  2. acp/acp_requests.py +20 -77
  3. acp/agent/connection.py +8 -0
  4. acp/agent/implementations/debug_server/debug_server.py +6 -2
  5. acp/agent/protocol.py +6 -0
  6. acp/bridge/README.md +15 -2
  7. acp/bridge/__init__.py +3 -2
  8. acp/bridge/__main__.py +60 -19
  9. acp/bridge/ws_server.py +173 -0
  10. acp/bridge/ws_server_cli.py +89 -0
  11. acp/client/connection.py +38 -29
  12. acp/client/implementations/default_client.py +3 -2
  13. acp/client/implementations/headless_client.py +2 -2
  14. acp/connection.py +2 -2
  15. acp/notifications.py +20 -50
  16. acp/schema/__init__.py +2 -0
  17. acp/schema/agent_responses.py +21 -0
  18. acp/schema/client_requests.py +3 -3
  19. acp/schema/session_state.py +63 -29
  20. acp/stdio.py +39 -9
  21. acp/task/supervisor.py +2 -2
  22. acp/transports.py +362 -2
  23. acp/utils.py +17 -4
  24. agentpool/__init__.py +6 -1
  25. agentpool/agents/__init__.py +2 -0
  26. agentpool/agents/acp_agent/acp_agent.py +407 -277
  27. agentpool/agents/acp_agent/acp_converters.py +196 -38
  28. agentpool/agents/acp_agent/client_handler.py +191 -26
  29. agentpool/agents/acp_agent/session_state.py +17 -6
  30. agentpool/agents/agent.py +607 -572
  31. agentpool/agents/agui_agent/__init__.py +0 -2
  32. agentpool/agents/agui_agent/agui_agent.py +176 -110
  33. agentpool/agents/agui_agent/agui_converters.py +0 -131
  34. agentpool/agents/agui_agent/helpers.py +3 -4
  35. agentpool/agents/base_agent.py +632 -17
  36. agentpool/agents/claude_code_agent/FORKING.md +191 -0
  37. agentpool/agents/claude_code_agent/__init__.py +13 -1
  38. agentpool/agents/claude_code_agent/claude_code_agent.py +1058 -291
  39. agentpool/agents/claude_code_agent/converters.py +74 -143
  40. agentpool/agents/claude_code_agent/history.py +474 -0
  41. agentpool/agents/claude_code_agent/models.py +77 -0
  42. agentpool/agents/claude_code_agent/static_info.py +100 -0
  43. agentpool/agents/claude_code_agent/usage.py +242 -0
  44. agentpool/agents/context.py +40 -0
  45. agentpool/agents/events/__init__.py +24 -0
  46. agentpool/agents/events/builtin_handlers.py +67 -1
  47. agentpool/agents/events/event_emitter.py +32 -2
  48. agentpool/agents/events/events.py +104 -3
  49. agentpool/agents/events/infer_info.py +145 -0
  50. agentpool/agents/events/processors.py +254 -0
  51. agentpool/agents/interactions.py +41 -6
  52. agentpool/agents/modes.py +67 -0
  53. agentpool/agents/slashed_agent.py +5 -4
  54. agentpool/agents/tool_call_accumulator.py +213 -0
  55. agentpool/agents/tool_wrapping.py +18 -6
  56. agentpool/common_types.py +56 -21
  57. agentpool/config_resources/__init__.py +38 -1
  58. agentpool/config_resources/acp_assistant.yml +2 -2
  59. agentpool/config_resources/agents.yml +3 -0
  60. agentpool/config_resources/agents_template.yml +1 -0
  61. agentpool/config_resources/claude_code_agent.yml +10 -6
  62. agentpool/config_resources/external_acp_agents.yml +2 -1
  63. agentpool/delegation/base_team.py +4 -30
  64. agentpool/delegation/pool.py +136 -289
  65. agentpool/delegation/team.py +58 -57
  66. agentpool/delegation/teamrun.py +51 -55
  67. agentpool/diagnostics/__init__.py +53 -0
  68. agentpool/diagnostics/lsp_manager.py +1593 -0
  69. agentpool/diagnostics/lsp_proxy.py +41 -0
  70. agentpool/diagnostics/lsp_proxy_script.py +229 -0
  71. agentpool/diagnostics/models.py +398 -0
  72. agentpool/functional/run.py +10 -4
  73. agentpool/mcp_server/__init__.py +0 -2
  74. agentpool/mcp_server/client.py +76 -32
  75. agentpool/mcp_server/conversions.py +54 -13
  76. agentpool/mcp_server/manager.py +34 -54
  77. agentpool/mcp_server/registries/official_registry_client.py +35 -1
  78. agentpool/mcp_server/tool_bridge.py +186 -139
  79. agentpool/messaging/__init__.py +0 -2
  80. agentpool/messaging/compaction.py +72 -197
  81. agentpool/messaging/connection_manager.py +11 -10
  82. agentpool/messaging/event_manager.py +5 -5
  83. agentpool/messaging/message_container.py +6 -30
  84. agentpool/messaging/message_history.py +99 -8
  85. agentpool/messaging/messagenode.py +52 -14
  86. agentpool/messaging/messages.py +54 -35
  87. agentpool/messaging/processing.py +12 -22
  88. agentpool/models/__init__.py +1 -1
  89. agentpool/models/acp_agents/base.py +6 -24
  90. agentpool/models/acp_agents/mcp_capable.py +126 -157
  91. agentpool/models/acp_agents/non_mcp.py +129 -95
  92. agentpool/models/agents.py +98 -76
  93. agentpool/models/agui_agents.py +1 -1
  94. agentpool/models/claude_code_agents.py +144 -19
  95. agentpool/models/file_parsing.py +0 -1
  96. agentpool/models/manifest.py +113 -50
  97. agentpool/prompts/conversion_manager.py +1 -1
  98. agentpool/prompts/prompts.py +5 -2
  99. agentpool/repomap.py +1 -1
  100. agentpool/resource_providers/__init__.py +11 -1
  101. agentpool/resource_providers/aggregating.py +56 -5
  102. agentpool/resource_providers/base.py +70 -4
  103. agentpool/resource_providers/codemode/code_executor.py +72 -5
  104. agentpool/resource_providers/codemode/helpers.py +2 -2
  105. agentpool/resource_providers/codemode/provider.py +64 -12
  106. agentpool/resource_providers/codemode/remote_mcp_execution.py +2 -2
  107. agentpool/resource_providers/codemode/remote_provider.py +9 -12
  108. agentpool/resource_providers/filtering.py +3 -1
  109. agentpool/resource_providers/mcp_provider.py +89 -12
  110. agentpool/resource_providers/plan_provider.py +228 -46
  111. agentpool/resource_providers/pool.py +7 -3
  112. agentpool/resource_providers/resource_info.py +111 -0
  113. agentpool/resource_providers/static.py +4 -2
  114. agentpool/sessions/__init__.py +4 -1
  115. agentpool/sessions/manager.py +33 -5
  116. agentpool/sessions/models.py +59 -6
  117. agentpool/sessions/protocol.py +28 -0
  118. agentpool/sessions/session.py +11 -55
  119. agentpool/skills/registry.py +13 -8
  120. agentpool/storage/manager.py +572 -49
  121. agentpool/talk/registry.py +4 -4
  122. agentpool/talk/talk.py +9 -10
  123. agentpool/testing.py +538 -20
  124. agentpool/tool_impls/__init__.py +6 -0
  125. agentpool/tool_impls/agent_cli/__init__.py +42 -0
  126. agentpool/tool_impls/agent_cli/tool.py +95 -0
  127. agentpool/tool_impls/bash/__init__.py +64 -0
  128. agentpool/tool_impls/bash/helpers.py +35 -0
  129. agentpool/tool_impls/bash/tool.py +171 -0
  130. agentpool/tool_impls/delete_path/__init__.py +70 -0
  131. agentpool/tool_impls/delete_path/tool.py +142 -0
  132. agentpool/tool_impls/download_file/__init__.py +80 -0
  133. agentpool/tool_impls/download_file/tool.py +183 -0
  134. agentpool/tool_impls/execute_code/__init__.py +55 -0
  135. agentpool/tool_impls/execute_code/tool.py +163 -0
  136. agentpool/tool_impls/grep/__init__.py +80 -0
  137. agentpool/tool_impls/grep/tool.py +200 -0
  138. agentpool/tool_impls/list_directory/__init__.py +73 -0
  139. agentpool/tool_impls/list_directory/tool.py +197 -0
  140. agentpool/tool_impls/question/__init__.py +42 -0
  141. agentpool/tool_impls/question/tool.py +127 -0
  142. agentpool/tool_impls/read/__init__.py +104 -0
  143. agentpool/tool_impls/read/tool.py +305 -0
  144. agentpool/tools/__init__.py +2 -1
  145. agentpool/tools/base.py +114 -34
  146. agentpool/tools/manager.py +57 -1
  147. agentpool/ui/base.py +2 -2
  148. agentpool/ui/mock_provider.py +2 -2
  149. agentpool/ui/stdlib_provider.py +2 -2
  150. agentpool/utils/file_watcher.py +269 -0
  151. agentpool/utils/identifiers.py +121 -0
  152. agentpool/utils/pydantic_ai_helpers.py +46 -0
  153. agentpool/utils/streams.py +616 -2
  154. agentpool/utils/subprocess_utils.py +155 -0
  155. agentpool/utils/token_breakdown.py +461 -0
  156. agentpool/vfs_registry.py +7 -2
  157. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/METADATA +41 -27
  158. agentpool-2.5.0.dist-info/RECORD +579 -0
  159. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/WHEEL +1 -1
  160. agentpool_cli/__main__.py +24 -0
  161. agentpool_cli/create.py +1 -1
  162. agentpool_cli/serve_acp.py +100 -21
  163. agentpool_cli/serve_agui.py +87 -0
  164. agentpool_cli/serve_opencode.py +119 -0
  165. agentpool_cli/ui.py +557 -0
  166. agentpool_commands/__init__.py +42 -5
  167. agentpool_commands/agents.py +75 -2
  168. agentpool_commands/history.py +62 -0
  169. agentpool_commands/mcp.py +176 -0
  170. agentpool_commands/models.py +56 -3
  171. agentpool_commands/pool.py +260 -0
  172. agentpool_commands/session.py +1 -1
  173. agentpool_commands/text_sharing/__init__.py +119 -0
  174. agentpool_commands/text_sharing/base.py +123 -0
  175. agentpool_commands/text_sharing/github_gist.py +80 -0
  176. agentpool_commands/text_sharing/opencode.py +462 -0
  177. agentpool_commands/text_sharing/paste_rs.py +59 -0
  178. agentpool_commands/text_sharing/pastebin.py +116 -0
  179. agentpool_commands/text_sharing/shittycodingagent.py +112 -0
  180. agentpool_commands/tools.py +57 -0
  181. agentpool_commands/utils.py +80 -30
  182. agentpool_config/__init__.py +30 -2
  183. agentpool_config/agentpool_tools.py +498 -0
  184. agentpool_config/builtin_tools.py +77 -22
  185. agentpool_config/commands.py +24 -1
  186. agentpool_config/compaction.py +258 -0
  187. agentpool_config/converters.py +1 -1
  188. agentpool_config/event_handlers.py +42 -0
  189. agentpool_config/events.py +1 -1
  190. agentpool_config/forward_targets.py +1 -4
  191. agentpool_config/jinja.py +3 -3
  192. agentpool_config/mcp_server.py +132 -6
  193. agentpool_config/nodes.py +1 -1
  194. agentpool_config/observability.py +44 -0
  195. agentpool_config/session.py +0 -3
  196. agentpool_config/storage.py +82 -38
  197. agentpool_config/task.py +3 -3
  198. agentpool_config/tools.py +11 -22
  199. agentpool_config/toolsets.py +109 -233
  200. agentpool_server/a2a_server/agent_worker.py +307 -0
  201. agentpool_server/a2a_server/server.py +23 -18
  202. agentpool_server/acp_server/acp_agent.py +234 -181
  203. agentpool_server/acp_server/commands/acp_commands.py +151 -156
  204. agentpool_server/acp_server/commands/docs_commands/fetch_repo.py +18 -17
  205. agentpool_server/acp_server/event_converter.py +651 -0
  206. agentpool_server/acp_server/input_provider.py +53 -10
  207. agentpool_server/acp_server/server.py +24 -90
  208. agentpool_server/acp_server/session.py +173 -331
  209. agentpool_server/acp_server/session_manager.py +8 -34
  210. agentpool_server/agui_server/server.py +3 -1
  211. agentpool_server/mcp_server/server.py +5 -2
  212. agentpool_server/opencode_server/.rules +95 -0
  213. agentpool_server/opencode_server/ENDPOINTS.md +401 -0
  214. agentpool_server/opencode_server/OPENCODE_UI_TOOLS_COMPLETE.md +202 -0
  215. agentpool_server/opencode_server/__init__.py +19 -0
  216. agentpool_server/opencode_server/command_validation.py +172 -0
  217. agentpool_server/opencode_server/converters.py +975 -0
  218. agentpool_server/opencode_server/dependencies.py +24 -0
  219. agentpool_server/opencode_server/input_provider.py +421 -0
  220. agentpool_server/opencode_server/models/__init__.py +250 -0
  221. agentpool_server/opencode_server/models/agent.py +53 -0
  222. agentpool_server/opencode_server/models/app.py +72 -0
  223. agentpool_server/opencode_server/models/base.py +26 -0
  224. agentpool_server/opencode_server/models/common.py +23 -0
  225. agentpool_server/opencode_server/models/config.py +37 -0
  226. agentpool_server/opencode_server/models/events.py +821 -0
  227. agentpool_server/opencode_server/models/file.py +88 -0
  228. agentpool_server/opencode_server/models/mcp.py +44 -0
  229. agentpool_server/opencode_server/models/message.py +179 -0
  230. agentpool_server/opencode_server/models/parts.py +323 -0
  231. agentpool_server/opencode_server/models/provider.py +81 -0
  232. agentpool_server/opencode_server/models/pty.py +43 -0
  233. agentpool_server/opencode_server/models/question.py +56 -0
  234. agentpool_server/opencode_server/models/session.py +111 -0
  235. agentpool_server/opencode_server/routes/__init__.py +29 -0
  236. agentpool_server/opencode_server/routes/agent_routes.py +473 -0
  237. agentpool_server/opencode_server/routes/app_routes.py +202 -0
  238. agentpool_server/opencode_server/routes/config_routes.py +302 -0
  239. agentpool_server/opencode_server/routes/file_routes.py +571 -0
  240. agentpool_server/opencode_server/routes/global_routes.py +94 -0
  241. agentpool_server/opencode_server/routes/lsp_routes.py +319 -0
  242. agentpool_server/opencode_server/routes/message_routes.py +761 -0
  243. agentpool_server/opencode_server/routes/permission_routes.py +63 -0
  244. agentpool_server/opencode_server/routes/pty_routes.py +300 -0
  245. agentpool_server/opencode_server/routes/question_routes.py +128 -0
  246. agentpool_server/opencode_server/routes/session_routes.py +1276 -0
  247. agentpool_server/opencode_server/routes/tui_routes.py +139 -0
  248. agentpool_server/opencode_server/server.py +475 -0
  249. agentpool_server/opencode_server/state.py +151 -0
  250. agentpool_server/opencode_server/time_utils.py +8 -0
  251. agentpool_storage/__init__.py +12 -0
  252. agentpool_storage/base.py +184 -2
  253. agentpool_storage/claude_provider/ARCHITECTURE.md +433 -0
  254. agentpool_storage/claude_provider/__init__.py +42 -0
  255. agentpool_storage/claude_provider/provider.py +1089 -0
  256. agentpool_storage/file_provider.py +278 -15
  257. agentpool_storage/memory_provider.py +193 -12
  258. agentpool_storage/models.py +3 -0
  259. agentpool_storage/opencode_provider/ARCHITECTURE.md +386 -0
  260. agentpool_storage/opencode_provider/__init__.py +16 -0
  261. agentpool_storage/opencode_provider/helpers.py +414 -0
  262. agentpool_storage/opencode_provider/provider.py +895 -0
  263. agentpool_storage/project_store.py +325 -0
  264. agentpool_storage/session_store.py +26 -6
  265. agentpool_storage/sql_provider/__init__.py +4 -2
  266. agentpool_storage/sql_provider/models.py +48 -0
  267. agentpool_storage/sql_provider/sql_provider.py +269 -3
  268. agentpool_storage/sql_provider/utils.py +12 -13
  269. agentpool_storage/zed_provider/__init__.py +16 -0
  270. agentpool_storage/zed_provider/helpers.py +281 -0
  271. agentpool_storage/zed_provider/models.py +130 -0
  272. agentpool_storage/zed_provider/provider.py +442 -0
  273. agentpool_storage/zed_provider.py +803 -0
  274. agentpool_toolsets/__init__.py +0 -2
  275. agentpool_toolsets/builtin/__init__.py +2 -12
  276. agentpool_toolsets/builtin/code.py +96 -57
  277. agentpool_toolsets/builtin/debug.py +118 -48
  278. agentpool_toolsets/builtin/execution_environment.py +115 -230
  279. agentpool_toolsets/builtin/file_edit/file_edit.py +115 -7
  280. agentpool_toolsets/builtin/skills.py +9 -4
  281. agentpool_toolsets/builtin/subagent_tools.py +64 -51
  282. agentpool_toolsets/builtin/workers.py +4 -2
  283. agentpool_toolsets/composio_toolset.py +2 -2
  284. agentpool_toolsets/entry_points.py +3 -1
  285. agentpool_toolsets/fsspec_toolset/__init__.py +13 -1
  286. agentpool_toolsets/fsspec_toolset/diagnostics.py +860 -73
  287. agentpool_toolsets/fsspec_toolset/grep.py +99 -7
  288. agentpool_toolsets/fsspec_toolset/helpers.py +3 -2
  289. agentpool_toolsets/fsspec_toolset/image_utils.py +161 -0
  290. agentpool_toolsets/fsspec_toolset/toolset.py +500 -95
  291. agentpool_toolsets/mcp_discovery/__init__.py +5 -0
  292. agentpool_toolsets/mcp_discovery/data/mcp_servers.parquet +0 -0
  293. agentpool_toolsets/mcp_discovery/toolset.py +511 -0
  294. agentpool_toolsets/mcp_run_toolset.py +87 -12
  295. agentpool_toolsets/notifications.py +33 -33
  296. agentpool_toolsets/openapi.py +3 -1
  297. agentpool_toolsets/search_toolset.py +3 -1
  298. agentpool-2.1.9.dist-info/RECORD +0 -474
  299. agentpool_config/resources.py +0 -33
  300. agentpool_server/acp_server/acp_tools.py +0 -43
  301. agentpool_server/acp_server/commands/spawn.py +0 -210
  302. agentpool_storage/text_log_provider.py +0 -275
  303. agentpool_toolsets/builtin/agent_management.py +0 -239
  304. agentpool_toolsets/builtin/chain.py +0 -288
  305. agentpool_toolsets/builtin/history.py +0 -36
  306. agentpool_toolsets/builtin/integration.py +0 -85
  307. agentpool_toolsets/builtin/tool_management.py +0 -90
  308. agentpool_toolsets/builtin/user_interaction.py +0 -52
  309. agentpool_toolsets/semantic_memory_toolset.py +0 -536
  310. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/entry_points.txt +0 -0
  311. {agentpool-2.1.9.dist-info → agentpool-2.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,5 @@
1
+ """MCP Discovery Toolset - dynamic MCP server exploration and tool execution."""
2
+
3
+ from agentpool_toolsets.mcp_discovery.toolset import MCPDiscoveryToolset
4
+
5
+ __all__ = ["MCPDiscoveryToolset"]
@@ -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
- async def get_tools(self) -> list[Tool]:
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
- async def run(tool_name: str = name, **input_dict: Any) -> CallToolResult:
41
- async with self.client.mcp_sse().connect() as session:
42
- return await session.call_tool(tool_name, arguments=input_dict) # type: ignore[no-any-return]
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
- wrapped_tool = self.create_tool(
46
- run, schema_override=cast(OpenAIFunctionDefinition, tool.input_schema)
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