alita-sdk 0.3.423__py3-none-any.whl → 0.3.435__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.
Potentially problematic release.
This version of alita-sdk might be problematic. Click here for more details.
- alita_sdk/runtime/clients/client.py +6 -2
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/langchain/constants.py +1 -1
- alita_sdk/runtime/langchain/langraph_agent.py +4 -1
- alita_sdk/runtime/models/mcp_models.py +57 -0
- alita_sdk/runtime/toolkits/__init__.py +24 -0
- alita_sdk/runtime/toolkits/mcp.py +787 -0
- alita_sdk/runtime/toolkits/tools.py +19 -2
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +79 -10
- alita_sdk/runtime/utils/streamlit.py +34 -3
- alita_sdk/runtime/utils/toolkit_utils.py +5 -2
- alita_sdk/tools/__init__.py +5 -0
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/gitlab/api_wrapper.py +5 -0
- alita_sdk/tools/qtest/api_wrapper.py +240 -39
- {alita_sdk-0.3.423.dist-info → alita_sdk-0.3.435.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.423.dist-info → alita_sdk-0.3.435.dist-info}/RECORD +22 -17
- {alita_sdk-0.3.423.dist-info → alita_sdk-0.3.435.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.423.dist-info → alita_sdk-0.3.435.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.423.dist-info → alita_sdk-0.3.435.dist-info}/top_level.txt +0 -0
|
@@ -575,7 +575,9 @@ class AlitaClient:
|
|
|
575
575
|
Args:
|
|
576
576
|
llm: The LLM to use
|
|
577
577
|
instructions: System instructions for the agent
|
|
578
|
-
tools: Optional list of
|
|
578
|
+
tools: Optional list of tool configurations (not tool instances) to provide to the agent.
|
|
579
|
+
Tool configs will be processed through get_tools() to create tool instances.
|
|
580
|
+
Each tool config should have 'type', 'settings', etc.
|
|
579
581
|
chat_history: Optional chat history
|
|
580
582
|
memory: Optional memory/checkpointer
|
|
581
583
|
runtime: Runtime type (default: 'langchain')
|
|
@@ -595,9 +597,11 @@ class AlitaClient:
|
|
|
595
597
|
|
|
596
598
|
# Create a minimal data structure for predict agent
|
|
597
599
|
# All LLM settings are taken from the passed client instance
|
|
600
|
+
# Note: 'tools' here are tool CONFIGURATIONS, not tool instances
|
|
601
|
+
# They will be converted to tool instances by LangChainAssistant via get_tools()
|
|
598
602
|
agent_data = {
|
|
599
603
|
'instructions': instructions,
|
|
600
|
-
'tools': tools, #
|
|
604
|
+
'tools': tools, # Tool configs that will be processed by get_tools()
|
|
601
605
|
'variables': variables
|
|
602
606
|
}
|
|
603
607
|
return LangChainAssistant(self, agent_data, llm,
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic MCP Server Discovery Client.
|
|
3
|
+
Implements the MCP protocol for discovering tools from remote servers.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Dict, List, Optional, Any, Set
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
import aiohttp
|
|
14
|
+
from datetime import datetime, timedelta
|
|
15
|
+
|
|
16
|
+
from ..models.mcp_models import McpConnectionConfig, McpToolMetadata
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class McpServerInfo:
|
|
23
|
+
"""Information about an MCP server."""
|
|
24
|
+
name: str
|
|
25
|
+
url: str
|
|
26
|
+
headers: Optional[Dict[str, str]] = None
|
|
27
|
+
last_discovery: Optional[datetime] = None
|
|
28
|
+
tools: List[McpToolMetadata] = field(default_factory=list)
|
|
29
|
+
status: str = "unknown" # unknown, online, offline, error
|
|
30
|
+
error: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class McpDiscoveryClient:
|
|
34
|
+
"""
|
|
35
|
+
Client for dynamically discovering tools from MCP servers.
|
|
36
|
+
Implements the MCP protocol for tool discovery.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
discovery_interval: int = 300, # 5 minutes
|
|
42
|
+
request_timeout: int = 30,
|
|
43
|
+
max_retries: int = 3,
|
|
44
|
+
cache_ttl: int = 600 # 10 minutes
|
|
45
|
+
):
|
|
46
|
+
self.discovery_interval = discovery_interval
|
|
47
|
+
self.request_timeout = request_timeout
|
|
48
|
+
self.max_retries = max_retries
|
|
49
|
+
self.cache_ttl = cache_ttl
|
|
50
|
+
|
|
51
|
+
# Server registry
|
|
52
|
+
self.servers: Dict[str, McpServerInfo] = {}
|
|
53
|
+
self.session: Optional[aiohttp.ClientSession] = None
|
|
54
|
+
|
|
55
|
+
# Discovery state
|
|
56
|
+
self._discovery_task: Optional[asyncio.Task] = None
|
|
57
|
+
self._running = False
|
|
58
|
+
|
|
59
|
+
async def __aenter__(self):
|
|
60
|
+
"""Async context manager entry."""
|
|
61
|
+
await self.start()
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
65
|
+
"""Async context manager exit."""
|
|
66
|
+
await self.stop()
|
|
67
|
+
|
|
68
|
+
async def start(self):
|
|
69
|
+
"""Start the discovery client."""
|
|
70
|
+
if self._running:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
self.session = aiohttp.ClientSession(
|
|
74
|
+
timeout=aiohttp.ClientTimeout(total=self.request_timeout)
|
|
75
|
+
)
|
|
76
|
+
self._running = True
|
|
77
|
+
|
|
78
|
+
# Start background discovery task
|
|
79
|
+
self._discovery_task = asyncio.create_task(self._discovery_loop())
|
|
80
|
+
logger.info("MCP Discovery Client started")
|
|
81
|
+
|
|
82
|
+
async def stop(self):
|
|
83
|
+
"""Stop the discovery client."""
|
|
84
|
+
if not self._running:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
self._running = False
|
|
88
|
+
|
|
89
|
+
if self._discovery_task:
|
|
90
|
+
self._discovery_task.cancel()
|
|
91
|
+
try:
|
|
92
|
+
await self._discovery_task
|
|
93
|
+
except asyncio.CancelledError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
if self.session:
|
|
97
|
+
await self.session.close()
|
|
98
|
+
|
|
99
|
+
logger.info("MCP Discovery Client stopped")
|
|
100
|
+
|
|
101
|
+
def add_server(self, server_name: str, connection_config: McpConnectionConfig):
|
|
102
|
+
"""Add an MCP server to discovery."""
|
|
103
|
+
server_info = McpServerInfo(
|
|
104
|
+
name=server_name,
|
|
105
|
+
url=connection_config.url,
|
|
106
|
+
headers=connection_config.headers
|
|
107
|
+
)
|
|
108
|
+
self.servers[server_name] = server_info
|
|
109
|
+
logger.info(f"Added MCP server for discovery: {server_name} at {connection_config.url}")
|
|
110
|
+
|
|
111
|
+
def remove_server(self, server_name: str):
|
|
112
|
+
"""Remove an MCP server from discovery."""
|
|
113
|
+
if server_name in self.servers:
|
|
114
|
+
del self.servers[server_name]
|
|
115
|
+
logger.info(f"Removed MCP server from discovery: {server_name}")
|
|
116
|
+
|
|
117
|
+
async def discover_server_tools(self, server_name: str, force: bool = False) -> List[McpToolMetadata]:
|
|
118
|
+
"""Discover tools from a specific MCP server."""
|
|
119
|
+
if server_name not in self.servers:
|
|
120
|
+
raise ValueError(f"Server {server_name} not registered for discovery")
|
|
121
|
+
|
|
122
|
+
server_info = self.servers[server_name]
|
|
123
|
+
|
|
124
|
+
# Check cache unless force refresh
|
|
125
|
+
if not force and self._is_cache_valid(server_info):
|
|
126
|
+
logger.debug(f"Using cached tools for server {server_name}")
|
|
127
|
+
return server_info.tools
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
tools = await self._fetch_server_tools(server_info)
|
|
131
|
+
server_info.tools = tools
|
|
132
|
+
server_info.last_discovery = datetime.now()
|
|
133
|
+
server_info.status = "online"
|
|
134
|
+
server_info.error = None
|
|
135
|
+
|
|
136
|
+
logger.info(f"Discovered {len(tools)} tools from server {server_name}")
|
|
137
|
+
return tools
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
error_msg = f"Failed to discover tools from {server_name}: {e}"
|
|
141
|
+
logger.error(error_msg)
|
|
142
|
+
server_info.status = "error"
|
|
143
|
+
server_info.error = str(e)
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
async def get_all_tools(self) -> Dict[str, List[McpToolMetadata]]:
|
|
147
|
+
"""Get all discovered tools from all servers."""
|
|
148
|
+
all_tools = {}
|
|
149
|
+
|
|
150
|
+
for server_name in self.servers:
|
|
151
|
+
tools = await self.discover_server_tools(server_name)
|
|
152
|
+
all_tools[server_name] = tools
|
|
153
|
+
|
|
154
|
+
return all_tools
|
|
155
|
+
|
|
156
|
+
def get_server_status(self, server_name: str) -> Optional[McpServerInfo]:
|
|
157
|
+
"""Get status information for a server."""
|
|
158
|
+
return self.servers.get(server_name)
|
|
159
|
+
|
|
160
|
+
def get_all_server_status(self) -> Dict[str, McpServerInfo]:
|
|
161
|
+
"""Get status information for all servers."""
|
|
162
|
+
return self.servers.copy()
|
|
163
|
+
|
|
164
|
+
async def _discovery_loop(self):
|
|
165
|
+
"""Background task for periodic tool discovery."""
|
|
166
|
+
while self._running:
|
|
167
|
+
try:
|
|
168
|
+
await self._perform_discovery()
|
|
169
|
+
await asyncio.sleep(self.discovery_interval)
|
|
170
|
+
except asyncio.CancelledError:
|
|
171
|
+
break
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Error in discovery loop: {e}")
|
|
174
|
+
await asyncio.sleep(60) # Wait before retrying
|
|
175
|
+
|
|
176
|
+
async def _perform_discovery(self):
|
|
177
|
+
"""Perform discovery on all registered servers."""
|
|
178
|
+
if not self.servers:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
discovery_tasks = [
|
|
182
|
+
self.discover_server_tools(server_name)
|
|
183
|
+
for server_name in self.servers
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
results = await asyncio.gather(*discovery_tasks, return_exceptions=True)
|
|
187
|
+
|
|
188
|
+
# Log any errors
|
|
189
|
+
for i, result in enumerate(results):
|
|
190
|
+
if isinstance(result, Exception):
|
|
191
|
+
server_name = list(self.servers.keys())[i]
|
|
192
|
+
logger.error(f"Discovery failed for server {server_name}: {result}")
|
|
193
|
+
|
|
194
|
+
async def _fetch_server_tools(self, server_info: McpServerInfo) -> List[McpToolMetadata]:
|
|
195
|
+
"""Fetch tools from an MCP server using HTTP requests."""
|
|
196
|
+
if not self.session:
|
|
197
|
+
raise RuntimeError("Discovery client not started")
|
|
198
|
+
|
|
199
|
+
# MCP protocol: list_tools request
|
|
200
|
+
mcp_request = {
|
|
201
|
+
"jsonrpc": "2.0",
|
|
202
|
+
"id": f"discover_{int(time.time())}",
|
|
203
|
+
"method": "tools/list",
|
|
204
|
+
"params": {}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
headers = {"Content-Type": "application/json"}
|
|
208
|
+
if server_info.headers:
|
|
209
|
+
headers.update(server_info.headers)
|
|
210
|
+
|
|
211
|
+
async with self.session.post(
|
|
212
|
+
server_info.url,
|
|
213
|
+
json=mcp_request,
|
|
214
|
+
headers=headers
|
|
215
|
+
) as response:
|
|
216
|
+
|
|
217
|
+
if response.status != 200:
|
|
218
|
+
raise Exception(f"HTTP {response.status}: {await response.text()}")
|
|
219
|
+
|
|
220
|
+
data = await response.json()
|
|
221
|
+
|
|
222
|
+
if "error" in data:
|
|
223
|
+
raise Exception(f"MCP Error: {data['error']}")
|
|
224
|
+
|
|
225
|
+
# Parse MCP response
|
|
226
|
+
tools_data = data.get("result", {}).get("tools", [])
|
|
227
|
+
tools = []
|
|
228
|
+
|
|
229
|
+
for tool_data in tools_data:
|
|
230
|
+
try:
|
|
231
|
+
tool_metadata = McpToolMetadata(
|
|
232
|
+
name=tool_data.get("name", ""),
|
|
233
|
+
description=tool_data.get("description", ""),
|
|
234
|
+
server=server_info.name,
|
|
235
|
+
input_schema=tool_data.get("inputSchema", {}),
|
|
236
|
+
enabled=True
|
|
237
|
+
)
|
|
238
|
+
tools.append(tool_metadata)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.warning(f"Failed to parse tool from {server_info.name}: {e}")
|
|
241
|
+
|
|
242
|
+
return tools
|
|
243
|
+
|
|
244
|
+
def _is_cache_valid(self, server_info: McpServerInfo) -> bool:
|
|
245
|
+
"""Check if cached tools are still valid."""
|
|
246
|
+
if not server_info.last_discovery:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
cache_age = datetime.now() - server_info.last_discovery
|
|
250
|
+
return cache_age.total_seconds() < self.cache_ttl
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class McpDiscoveryService:
|
|
254
|
+
"""
|
|
255
|
+
High-level service for managing MCP server discovery.
|
|
256
|
+
Integrates with the existing toolkit system.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
def __init__(self, discovery_client: Optional[McpDiscoveryClient] = None):
|
|
260
|
+
self.client = discovery_client or McpDiscoveryClient()
|
|
261
|
+
self._started = False
|
|
262
|
+
|
|
263
|
+
async def start(self):
|
|
264
|
+
"""Start the discovery service."""
|
|
265
|
+
if not self._started:
|
|
266
|
+
await self.client.start()
|
|
267
|
+
self._started = True
|
|
268
|
+
|
|
269
|
+
async def stop(self):
|
|
270
|
+
"""Stop the discovery service."""
|
|
271
|
+
if self._started:
|
|
272
|
+
await self.client.stop()
|
|
273
|
+
self._started = False
|
|
274
|
+
|
|
275
|
+
async def register_server(self, server_name: str, connection_config: McpConnectionConfig):
|
|
276
|
+
"""Register an MCP server for discovery."""
|
|
277
|
+
self.client.add_server(server_name, connection_config)
|
|
278
|
+
|
|
279
|
+
# Perform immediate discovery
|
|
280
|
+
await self.client.discover_server_tools(server_name, force=True)
|
|
281
|
+
|
|
282
|
+
def unregister_server(self, server_name: str):
|
|
283
|
+
"""Unregister an MCP server."""
|
|
284
|
+
self.client.remove_server(server_name)
|
|
285
|
+
|
|
286
|
+
async def get_server_tools(self, server_name: str) -> List[McpToolMetadata]:
|
|
287
|
+
"""Get tools from a specific server."""
|
|
288
|
+
return await self.client.discover_server_tools(server_name)
|
|
289
|
+
|
|
290
|
+
async def get_all_available_tools(self) -> Dict[str, List[McpToolMetadata]]:
|
|
291
|
+
"""Get all available tools from all registered servers."""
|
|
292
|
+
return await self.client.get_all_tools()
|
|
293
|
+
|
|
294
|
+
def get_server_health(self) -> Dict[str, Dict[str, Any]]:
|
|
295
|
+
"""Get health status of all servers."""
|
|
296
|
+
status_info = {}
|
|
297
|
+
|
|
298
|
+
for name, server_info in self.client.get_all_server_status().items():
|
|
299
|
+
status_info[name] = {
|
|
300
|
+
"status": server_info.status,
|
|
301
|
+
"url": server_info.url,
|
|
302
|
+
"last_discovery": server_info.last_discovery.isoformat() if server_info.last_discovery else None,
|
|
303
|
+
"tool_count": len(server_info.tools),
|
|
304
|
+
"error": server_info.error
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return status_info
|
|
308
|
+
|
|
309
|
+
async def refresh_server(self, server_name: str):
|
|
310
|
+
"""Force refresh tools from a specific server."""
|
|
311
|
+
await self.client.discover_server_tools(server_name, force=True)
|
|
312
|
+
|
|
313
|
+
async def refresh_all_servers(self):
|
|
314
|
+
"""Force refresh tools from all servers."""
|
|
315
|
+
for server_name in self.client.servers:
|
|
316
|
+
await self.client.discover_server_tools(server_name, force=True)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# Global discovery service instance
|
|
320
|
+
_discovery_service: Optional[McpDiscoveryService] = None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def get_discovery_service() -> McpDiscoveryService:
|
|
324
|
+
"""Get the global MCP discovery service instance."""
|
|
325
|
+
global _discovery_service
|
|
326
|
+
if _discovery_service is None:
|
|
327
|
+
_discovery_service = McpDiscoveryService()
|
|
328
|
+
return _discovery_service
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
async def init_discovery_service():
|
|
332
|
+
"""Initialize the global discovery service."""
|
|
333
|
+
service = get_discovery_service()
|
|
334
|
+
await service.start()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
async def shutdown_discovery_service():
|
|
338
|
+
"""Shutdown the global discovery service."""
|
|
339
|
+
global _discovery_service
|
|
340
|
+
if _discovery_service:
|
|
341
|
+
await _discovery_service.stop()
|
|
342
|
+
_discovery_service = None
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Manager - Unified interface for both static and dynamic MCP tool discovery.
|
|
3
|
+
Provides a single API that can work with both registry-based and live discovery.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, List, Optional, Any, Union
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
from ..models.mcp_models import McpConnectionConfig, McpToolMetadata
|
|
12
|
+
from .mcp_discovery import McpDiscoveryService, get_discovery_service
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DiscoveryMode(Enum):
|
|
18
|
+
"""MCP discovery modes."""
|
|
19
|
+
STATIC = "static" # Use alita.get_mcp_toolkits() registry
|
|
20
|
+
DYNAMIC = "dynamic" # Live discovery from MCP servers
|
|
21
|
+
HYBRID = "hybrid" # Try dynamic first, fallback to static
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class McpManager:
|
|
25
|
+
"""
|
|
26
|
+
Unified manager for MCP tool discovery supporting multiple modes.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
default_mode: DiscoveryMode = DiscoveryMode.DYNAMIC,
|
|
32
|
+
discovery_service: Optional[McpDiscoveryService] = None
|
|
33
|
+
):
|
|
34
|
+
self.default_mode = default_mode
|
|
35
|
+
self.discovery_service = discovery_service or get_discovery_service()
|
|
36
|
+
self._static_fallback_enabled = True
|
|
37
|
+
|
|
38
|
+
async def discover_server_tools(
|
|
39
|
+
self,
|
|
40
|
+
server_name: str,
|
|
41
|
+
connection_config: Optional[McpConnectionConfig] = None,
|
|
42
|
+
alita_client=None,
|
|
43
|
+
mode: Optional[DiscoveryMode] = None,
|
|
44
|
+
**kwargs
|
|
45
|
+
) -> List[McpToolMetadata]:
|
|
46
|
+
"""
|
|
47
|
+
Discover tools from an MCP server using the specified mode.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
server_name: Name of the MCP server
|
|
51
|
+
connection_config: Connection configuration (required for dynamic mode)
|
|
52
|
+
alita_client: Alita client (required for static mode)
|
|
53
|
+
mode: Discovery mode to use (defaults to manager's default)
|
|
54
|
+
**kwargs: Additional options
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of discovered tool metadata
|
|
58
|
+
"""
|
|
59
|
+
discovery_mode = mode or self.default_mode
|
|
60
|
+
|
|
61
|
+
if discovery_mode == DiscoveryMode.DYNAMIC:
|
|
62
|
+
return await self._discover_dynamic(server_name, connection_config)
|
|
63
|
+
|
|
64
|
+
elif discovery_mode == DiscoveryMode.STATIC:
|
|
65
|
+
return await self._discover_static(server_name, alita_client)
|
|
66
|
+
|
|
67
|
+
elif discovery_mode == DiscoveryMode.HYBRID:
|
|
68
|
+
return await self._discover_hybrid(server_name, connection_config, alita_client)
|
|
69
|
+
|
|
70
|
+
else:
|
|
71
|
+
raise ValueError(f"Unknown discovery mode: {discovery_mode}")
|
|
72
|
+
|
|
73
|
+
async def _discover_dynamic(
|
|
74
|
+
self,
|
|
75
|
+
server_name: str,
|
|
76
|
+
connection_config: Optional[McpConnectionConfig]
|
|
77
|
+
) -> List[McpToolMetadata]:
|
|
78
|
+
"""Discover tools using dynamic MCP protocol."""
|
|
79
|
+
if not connection_config:
|
|
80
|
+
raise ValueError("Connection configuration required for dynamic discovery")
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
# Ensure discovery service is started
|
|
84
|
+
await self.discovery_service.start()
|
|
85
|
+
|
|
86
|
+
# Register and discover
|
|
87
|
+
await self.discovery_service.register_server(server_name, connection_config)
|
|
88
|
+
tools = await self.discovery_service.get_server_tools(server_name)
|
|
89
|
+
|
|
90
|
+
logger.info(f"Dynamic discovery found {len(tools)} tools from {server_name}")
|
|
91
|
+
return tools
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Dynamic discovery failed for {server_name}: {e}")
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
async def _discover_static(
|
|
98
|
+
self,
|
|
99
|
+
server_name: str,
|
|
100
|
+
alita_client
|
|
101
|
+
) -> List[McpToolMetadata]:
|
|
102
|
+
"""Discover tools using static registry."""
|
|
103
|
+
if not alita_client or not hasattr(alita_client, 'get_mcp_toolkits'):
|
|
104
|
+
raise ValueError("Alita client with get_mcp_toolkits() required for static discovery")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
# Use existing registry approach
|
|
108
|
+
all_toolkits = alita_client.get_mcp_toolkits()
|
|
109
|
+
server_toolkit = next((tk for tk in all_toolkits if tk.get('name') == server_name), None)
|
|
110
|
+
|
|
111
|
+
if not server_toolkit:
|
|
112
|
+
logger.warning(f"Static registry: Server {server_name} not found")
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
# Convert to metadata format
|
|
116
|
+
tools = []
|
|
117
|
+
for tool_info in server_toolkit.get('tools', []):
|
|
118
|
+
metadata = McpToolMetadata(
|
|
119
|
+
name=tool_info.get('name', ''),
|
|
120
|
+
description=tool_info.get('description', ''),
|
|
121
|
+
server=server_name,
|
|
122
|
+
input_schema=tool_info.get('inputSchema', {}),
|
|
123
|
+
enabled=True
|
|
124
|
+
)
|
|
125
|
+
tools.append(metadata)
|
|
126
|
+
|
|
127
|
+
logger.info(f"Static discovery found {len(tools)} tools from {server_name}")
|
|
128
|
+
return tools
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(f"Static discovery failed for {server_name}: {e}")
|
|
132
|
+
raise
|
|
133
|
+
|
|
134
|
+
async def _discover_hybrid(
|
|
135
|
+
self,
|
|
136
|
+
server_name: str,
|
|
137
|
+
connection_config: Optional[McpConnectionConfig],
|
|
138
|
+
alita_client
|
|
139
|
+
) -> List[McpToolMetadata]:
|
|
140
|
+
"""Discover tools using hybrid approach (dynamic first, static fallback)."""
|
|
141
|
+
|
|
142
|
+
# Try dynamic discovery first
|
|
143
|
+
if connection_config:
|
|
144
|
+
try:
|
|
145
|
+
return await self._discover_dynamic(server_name, connection_config)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.warning(f"Dynamic discovery failed for {server_name}, trying static: {e}")
|
|
148
|
+
|
|
149
|
+
# Fallback to static discovery
|
|
150
|
+
if self._static_fallback_enabled and alita_client:
|
|
151
|
+
try:
|
|
152
|
+
return await self._discover_static(server_name, alita_client)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(f"Static fallback also failed for {server_name}: {e}")
|
|
155
|
+
|
|
156
|
+
logger.error(f"All discovery methods failed for {server_name}")
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
async def get_server_health(
|
|
160
|
+
self,
|
|
161
|
+
server_name: Optional[str] = None
|
|
162
|
+
) -> Dict[str, Any]:
|
|
163
|
+
"""Get health information for servers."""
|
|
164
|
+
try:
|
|
165
|
+
if server_name:
|
|
166
|
+
# Get specific server health from discovery service
|
|
167
|
+
all_health = self.discovery_service.get_server_health()
|
|
168
|
+
return all_health.get(server_name, {"status": "unknown"})
|
|
169
|
+
else:
|
|
170
|
+
# Get all server health
|
|
171
|
+
return self.discovery_service.get_server_health()
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Failed to get server health: {e}")
|
|
174
|
+
return {"status": "error", "error": str(e)}
|
|
175
|
+
|
|
176
|
+
async def refresh_server(self, server_name: str):
|
|
177
|
+
"""Force refresh a specific server's tools."""
|
|
178
|
+
try:
|
|
179
|
+
await self.discovery_service.refresh_server(server_name)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Failed to refresh server {server_name}: {e}")
|
|
182
|
+
|
|
183
|
+
async def start(self):
|
|
184
|
+
"""Start the MCP manager."""
|
|
185
|
+
await self.discovery_service.start()
|
|
186
|
+
|
|
187
|
+
async def stop(self):
|
|
188
|
+
"""Stop the MCP manager."""
|
|
189
|
+
await self.discovery_service.stop()
|
|
190
|
+
|
|
191
|
+
def set_static_fallback(self, enabled: bool):
|
|
192
|
+
"""Enable or disable static fallback in hybrid mode."""
|
|
193
|
+
self._static_fallback_enabled = enabled
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Global manager instance
|
|
197
|
+
_mcp_manager: Optional[McpManager] = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def get_mcp_manager(mode: DiscoveryMode = DiscoveryMode.HYBRID) -> McpManager:
|
|
201
|
+
"""Get the global MCP manager instance."""
|
|
202
|
+
global _mcp_manager
|
|
203
|
+
if _mcp_manager is None:
|
|
204
|
+
_mcp_manager = McpManager(default_mode=mode)
|
|
205
|
+
return _mcp_manager
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def discover_mcp_tools(
|
|
209
|
+
server_name: str,
|
|
210
|
+
connection_config: Optional[McpConnectionConfig] = None,
|
|
211
|
+
alita_client=None,
|
|
212
|
+
mode: Optional[DiscoveryMode] = None
|
|
213
|
+
) -> List[McpToolMetadata]:
|
|
214
|
+
"""
|
|
215
|
+
Convenience function for discovering MCP tools.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
server_name: Name of the MCP server
|
|
219
|
+
connection_config: Connection config (for dynamic discovery)
|
|
220
|
+
alita_client: Alita client (for static discovery)
|
|
221
|
+
mode: Discovery mode (defaults to HYBRID)
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of discovered tool metadata
|
|
225
|
+
"""
|
|
226
|
+
manager = get_mcp_manager()
|
|
227
|
+
return await manager.discover_server_tools(
|
|
228
|
+
server_name=server_name,
|
|
229
|
+
connection_config=connection_config,
|
|
230
|
+
alita_client=alita_client,
|
|
231
|
+
mode=mode or DiscoveryMode.HYBRID
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
async def init_mcp_manager(mode: DiscoveryMode = DiscoveryMode.HYBRID):
|
|
236
|
+
"""Initialize the global MCP manager."""
|
|
237
|
+
manager = get_mcp_manager(mode)
|
|
238
|
+
await manager.start()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def shutdown_mcp_manager():
|
|
242
|
+
"""Shutdown the global MCP manager."""
|
|
243
|
+
global _mcp_manager
|
|
244
|
+
if _mcp_manager:
|
|
245
|
+
await _mcp_manager.stop()
|
|
246
|
+
_mcp_manager = None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# Configuration helpers
|
|
250
|
+
def create_discovery_config(
|
|
251
|
+
mode: str = "hybrid",
|
|
252
|
+
discovery_interval: int = 300,
|
|
253
|
+
enable_static_fallback: bool = True,
|
|
254
|
+
**kwargs
|
|
255
|
+
) -> Dict[str, Any]:
|
|
256
|
+
"""Create a discovery configuration dictionary."""
|
|
257
|
+
return {
|
|
258
|
+
"discovery_mode": mode,
|
|
259
|
+
"discovery_interval": discovery_interval,
|
|
260
|
+
"enable_static_fallback": enable_static_fallback,
|
|
261
|
+
**kwargs
|
|
262
|
+
}
|
|
@@ -27,7 +27,7 @@ Use this if you want to respond directly to the human. Markdown code snippet for
|
|
|
27
27
|
```json
|
|
28
28
|
{
|
|
29
29
|
"action": "Final Answer",
|
|
30
|
-
"action_input": string
|
|
30
|
+
"action_input": string // You should put what you want to return to use here
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -244,9 +244,12 @@ class PrinterNode(Runnable):
|
|
|
244
244
|
result = {}
|
|
245
245
|
logger.debug(f"Initial text pattern: {self.input_mapping}")
|
|
246
246
|
mapping = propagate_the_input_mapping(self.input_mapping, [], state)
|
|
247
|
-
if
|
|
247
|
+
if mapping.get(PRINTER) is None:
|
|
248
248
|
raise ToolException(f"PrinterNode requires '{PRINTER}' field in input mapping")
|
|
249
249
|
formatted_output = mapping[PRINTER]
|
|
250
|
+
# add info label to the printer's output
|
|
251
|
+
if formatted_output:
|
|
252
|
+
formatted_output += f"\n\n-----\n*How to proceed?*\n* *to resume the pipeline - type anything...*"
|
|
250
253
|
logger.debug(f"Formatted output: {formatted_output}")
|
|
251
254
|
result[PRINTER_NODE_RS] = formatted_output
|
|
252
255
|
return result
|