alita-sdk 0.3.365__py3-none-any.whl → 0.3.462__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.

Files changed (118) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent_executor.py +144 -0
  4. alita_sdk/cli/agent_loader.py +197 -0
  5. alita_sdk/cli/agent_ui.py +166 -0
  6. alita_sdk/cli/agents.py +1069 -0
  7. alita_sdk/cli/callbacks.py +576 -0
  8. alita_sdk/cli/cli.py +159 -0
  9. alita_sdk/cli/config.py +153 -0
  10. alita_sdk/cli/formatting.py +182 -0
  11. alita_sdk/cli/mcp_loader.py +315 -0
  12. alita_sdk/cli/toolkit.py +330 -0
  13. alita_sdk/cli/toolkit_loader.py +55 -0
  14. alita_sdk/cli/tools/__init__.py +9 -0
  15. alita_sdk/cli/tools/filesystem.py +905 -0
  16. alita_sdk/configurations/bitbucket.py +95 -0
  17. alita_sdk/configurations/confluence.py +96 -1
  18. alita_sdk/configurations/gitlab.py +79 -0
  19. alita_sdk/configurations/jira.py +103 -0
  20. alita_sdk/configurations/testrail.py +88 -0
  21. alita_sdk/configurations/xray.py +93 -0
  22. alita_sdk/configurations/zephyr_enterprise.py +93 -0
  23. alita_sdk/configurations/zephyr_essential.py +75 -0
  24. alita_sdk/runtime/clients/artifact.py +1 -1
  25. alita_sdk/runtime/clients/client.py +47 -10
  26. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  27. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  28. alita_sdk/runtime/clients/sandbox_client.py +373 -0
  29. alita_sdk/runtime/langchain/assistant.py +70 -41
  30. alita_sdk/runtime/langchain/constants.py +6 -1
  31. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  32. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -1
  33. alita_sdk/runtime/langchain/document_loaders/constants.py +73 -100
  34. alita_sdk/runtime/langchain/langraph_agent.py +164 -38
  35. alita_sdk/runtime/langchain/utils.py +43 -7
  36. alita_sdk/runtime/models/mcp_models.py +61 -0
  37. alita_sdk/runtime/toolkits/__init__.py +24 -0
  38. alita_sdk/runtime/toolkits/application.py +8 -1
  39. alita_sdk/runtime/toolkits/artifact.py +5 -6
  40. alita_sdk/runtime/toolkits/mcp.py +895 -0
  41. alita_sdk/runtime/toolkits/tools.py +140 -50
  42. alita_sdk/runtime/tools/__init__.py +7 -2
  43. alita_sdk/runtime/tools/application.py +7 -0
  44. alita_sdk/runtime/tools/function.py +94 -5
  45. alita_sdk/runtime/tools/graph.py +10 -4
  46. alita_sdk/runtime/tools/image_generation.py +104 -8
  47. alita_sdk/runtime/tools/llm.py +204 -114
  48. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  49. alita_sdk/runtime/tools/mcp_remote_tool.py +166 -0
  50. alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
  51. alita_sdk/runtime/tools/sandbox.py +180 -79
  52. alita_sdk/runtime/tools/vectorstore.py +22 -21
  53. alita_sdk/runtime/tools/vectorstore_base.py +79 -26
  54. alita_sdk/runtime/utils/mcp_oauth.py +164 -0
  55. alita_sdk/runtime/utils/mcp_sse_client.py +405 -0
  56. alita_sdk/runtime/utils/streamlit.py +34 -3
  57. alita_sdk/runtime/utils/toolkit_utils.py +14 -4
  58. alita_sdk/runtime/utils/utils.py +1 -0
  59. alita_sdk/tools/__init__.py +48 -31
  60. alita_sdk/tools/ado/repos/__init__.py +1 -0
  61. alita_sdk/tools/ado/test_plan/__init__.py +1 -1
  62. alita_sdk/tools/ado/wiki/__init__.py +1 -5
  63. alita_sdk/tools/ado/work_item/__init__.py +1 -5
  64. alita_sdk/tools/ado/work_item/ado_wrapper.py +17 -8
  65. alita_sdk/tools/base_indexer_toolkit.py +194 -112
  66. alita_sdk/tools/bitbucket/__init__.py +1 -0
  67. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  68. alita_sdk/tools/code/sonar/__init__.py +1 -1
  69. alita_sdk/tools/code_indexer_toolkit.py +15 -5
  70. alita_sdk/tools/confluence/__init__.py +2 -2
  71. alita_sdk/tools/confluence/api_wrapper.py +110 -63
  72. alita_sdk/tools/confluence/loader.py +10 -0
  73. alita_sdk/tools/elitea_base.py +22 -22
  74. alita_sdk/tools/github/__init__.py +2 -2
  75. alita_sdk/tools/gitlab/__init__.py +2 -1
  76. alita_sdk/tools/gitlab/api_wrapper.py +11 -7
  77. alita_sdk/tools/gitlab_org/__init__.py +1 -2
  78. alita_sdk/tools/google_places/__init__.py +2 -1
  79. alita_sdk/tools/jira/__init__.py +1 -0
  80. alita_sdk/tools/jira/api_wrapper.py +1 -1
  81. alita_sdk/tools/memory/__init__.py +1 -1
  82. alita_sdk/tools/non_code_indexer_toolkit.py +2 -2
  83. alita_sdk/tools/openapi/__init__.py +10 -1
  84. alita_sdk/tools/pandas/__init__.py +1 -1
  85. alita_sdk/tools/postman/__init__.py +2 -1
  86. alita_sdk/tools/postman/api_wrapper.py +18 -8
  87. alita_sdk/tools/postman/postman_analysis.py +8 -1
  88. alita_sdk/tools/pptx/__init__.py +2 -2
  89. alita_sdk/tools/qtest/__init__.py +3 -3
  90. alita_sdk/tools/qtest/api_wrapper.py +1708 -76
  91. alita_sdk/tools/rally/__init__.py +1 -2
  92. alita_sdk/tools/report_portal/__init__.py +1 -0
  93. alita_sdk/tools/salesforce/__init__.py +1 -0
  94. alita_sdk/tools/servicenow/__init__.py +2 -3
  95. alita_sdk/tools/sharepoint/__init__.py +1 -0
  96. alita_sdk/tools/sharepoint/api_wrapper.py +125 -34
  97. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  98. alita_sdk/tools/sharepoint/utils.py +8 -2
  99. alita_sdk/tools/slack/__init__.py +1 -0
  100. alita_sdk/tools/sql/__init__.py +2 -1
  101. alita_sdk/tools/sql/api_wrapper.py +71 -23
  102. alita_sdk/tools/testio/__init__.py +1 -0
  103. alita_sdk/tools/testrail/__init__.py +1 -3
  104. alita_sdk/tools/utils/__init__.py +17 -0
  105. alita_sdk/tools/utils/content_parser.py +35 -24
  106. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +67 -21
  107. alita_sdk/tools/xray/__init__.py +2 -1
  108. alita_sdk/tools/zephyr/__init__.py +2 -1
  109. alita_sdk/tools/zephyr_enterprise/__init__.py +1 -0
  110. alita_sdk/tools/zephyr_essential/__init__.py +1 -0
  111. alita_sdk/tools/zephyr_scale/__init__.py +1 -0
  112. alita_sdk/tools/zephyr_squad/__init__.py +1 -0
  113. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/METADATA +8 -2
  114. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/RECORD +118 -93
  115. alita_sdk-0.3.462.dist-info/entry_points.txt +2 -0
  116. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/WHEEL +0 -0
  117. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/licenses/LICENSE +0 -0
  118. {alita_sdk-0.3.365.dist-info → alita_sdk-0.3.462.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,895 @@
1
+ """
2
+ MCP (Model Context Protocol) Toolkit for Alita SDK.
3
+ This toolkit enables connection to a single remote MCP server and exposes its tools.
4
+ Following MCP specification: https://modelcontextprotocol.io/specification/2025-06-18
5
+ """
6
+
7
+ import logging
8
+ import re
9
+ import requests
10
+ import asyncio
11
+ from typing import List, Optional, Any, Dict, Literal, ClassVar, Union
12
+
13
+ from langchain_core.tools import BaseToolkit, BaseTool
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+ from ..tools.mcp_server_tool import McpServerTool
17
+ from ..tools.mcp_remote_tool import McpRemoteTool
18
+ from ..tools.mcp_inspect_tool import McpInspectTool
19
+ from ...tools.utils import TOOLKIT_SPLITTER, clean_string
20
+ from ..models.mcp_models import McpConnectionConfig
21
+ from ..utils.mcp_sse_client import McpSseClient
22
+ from ..utils.mcp_oauth import (
23
+ McpAuthorizationRequired,
24
+ canonical_resource,
25
+ extract_resource_metadata_url,
26
+ fetch_resource_metadata,
27
+ infer_authorization_servers_from_realm,
28
+ )
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ name = "mcp"
33
+
34
+ def safe_int(value, default):
35
+ """Convert value to int, handling string inputs."""
36
+ if value is None:
37
+ return default
38
+ try:
39
+ return int(value)
40
+ except (ValueError, TypeError):
41
+ logger.warning(f"Invalid integer value '{value}', using default {default}")
42
+ return default
43
+
44
+ def optimize_tool_name(prefix: str, tool_name: str, max_total_length: int = 64) -> str:
45
+ """
46
+ Optimize tool name to fit within max_total_length while preserving meaning.
47
+
48
+ Args:
49
+ prefix: The toolkit prefix (already cleaned)
50
+ tool_name: The original tool name
51
+ max_total_length: Maximum total length for the full tool name (default: 64)
52
+
53
+ Returns:
54
+ Optimized full tool name in format: prefix___tool_name
55
+ """
56
+ splitter = TOOLKIT_SPLITTER
57
+ splitter_len = len(splitter)
58
+ prefix_len = len(prefix)
59
+
60
+ # Calculate available space for tool name
61
+ available_space = max_total_length - prefix_len - splitter_len
62
+
63
+ if available_space <= 0:
64
+ logger.error(f"Prefix '{prefix}' is too long ({prefix_len} chars), cannot create valid tool name")
65
+ # Fallback: truncate prefix itself
66
+ prefix = prefix[:max_total_length - splitter_len - 10] # Leave 10 chars for tool name
67
+ available_space = max_total_length - len(prefix) - splitter_len
68
+
69
+ # If tool name fits, use it as-is
70
+ if len(tool_name) <= available_space:
71
+ return f'{prefix}{splitter}{tool_name}'
72
+
73
+ # Tool name is too long, need to optimize
74
+ logger.debug(f"Tool name '{tool_name}' is too long ({len(tool_name)} chars), optimizing to fit {available_space} chars")
75
+
76
+ # Split tool name into parts (handle camelCase, snake_case, and mixed)
77
+ # First, split by underscores and hyphens
78
+ parts = re.split(r'[_-]', tool_name)
79
+
80
+ # Further split camelCase within each part
81
+ all_parts = []
82
+ for part in parts:
83
+ # Insert underscore before uppercase letters (camelCase to snake_case)
84
+ snake_case_part = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', part)
85
+ all_parts.extend(snake_case_part.split('_'))
86
+
87
+ # Filter out empty parts
88
+ all_parts = [p for p in all_parts if p]
89
+
90
+ # Remove redundant prefix words (case-insensitive comparison)
91
+ # Only remove if prefix is meaningful (>= 3 chars) to avoid over-filtering
92
+ prefix_lower = prefix.lower()
93
+ filtered_parts = []
94
+ for part in all_parts:
95
+ part_lower = part.lower()
96
+ # Skip if this part contains the prefix or the prefix contains this part
97
+ # But only if both are meaningful (>= 3 chars)
98
+ should_remove = False
99
+ if len(prefix_lower) >= 3 and len(part_lower) >= 3:
100
+ if part_lower in prefix_lower or prefix_lower in part_lower:
101
+ should_remove = True
102
+ logger.debug(f"Removing redundant part '{part}' (matches prefix '{prefix}')")
103
+
104
+ if not should_remove:
105
+ filtered_parts.append(part)
106
+
107
+ # If we removed all parts, keep the original parts
108
+ if not filtered_parts:
109
+ filtered_parts = all_parts
110
+
111
+ # Reconstruct tool name with filtered parts
112
+ optimized_name = '_'.join(filtered_parts)
113
+
114
+ # If still too long, truncate intelligently
115
+ if len(optimized_name) > available_space:
116
+ # Strategy: Keep beginning and end, as they often contain the most important info
117
+ # For example: "projectalita_github_io_list_branches" -> "projectalita_list_branches"
118
+
119
+ # Try removing middle parts first
120
+ if len(filtered_parts) > 2:
121
+ # Keep first and last parts, remove middle
122
+ kept_parts = [filtered_parts[0], filtered_parts[-1]]
123
+ optimized_name = '_'.join(kept_parts)
124
+
125
+ # If still too long, add parts from the end until we run out of space
126
+ if len(optimized_name) <= available_space and len(filtered_parts) > 2:
127
+ for i in range(len(filtered_parts) - 2, 0, -1):
128
+ candidate = '_'.join([filtered_parts[0]] + filtered_parts[i:])
129
+ if len(candidate) <= available_space:
130
+ optimized_name = candidate
131
+ break
132
+
133
+ # If still too long, just truncate
134
+ if len(optimized_name) > available_space:
135
+ # Try to truncate at word boundary
136
+ truncated = optimized_name[:available_space]
137
+ last_underscore = truncated.rfind('_')
138
+ if last_underscore > available_space * 0.7: # Keep if we're not losing too much
139
+ optimized_name = truncated[:last_underscore]
140
+ else:
141
+ optimized_name = truncated
142
+
143
+ full_name = f'{prefix}{splitter}{optimized_name}'
144
+ logger.info(f"Optimized tool name: '{tool_name}' ({len(tool_name)} chars) -> '{optimized_name}' ({len(optimized_name)} chars), full: '{full_name}' ({len(full_name)} chars)")
145
+
146
+ return full_name
147
+
148
+ class McpToolkit(BaseToolkit):
149
+ """
150
+ MCP Toolkit for connecting to a single remote MCP server and exposing its tools.
151
+ Each toolkit instance represents one MCP server connection.
152
+ """
153
+
154
+ tools: List[BaseTool] = []
155
+ toolkit_name: Optional[str] = None
156
+
157
+ # Class variable (not Pydantic field) for tool name length limit
158
+ toolkit_max_length: ClassVar[int] = 0 # No limit for MCP tool names
159
+
160
+ def __getstate__(self):
161
+ """Custom serialization for pickle compatibility."""
162
+ state = self.__dict__.copy()
163
+ # The tools list should already be pickle-safe due to individual tool fixes
164
+ # Just return the state as-is since tools handle their own serialization
165
+ return state
166
+
167
+ def __setstate__(self, state):
168
+ """Custom deserialization for pickle compatibility."""
169
+ # Initialize Pydantic internal attributes if needed
170
+ if '__pydantic_fields_set__' not in state:
171
+ state['__pydantic_fields_set__'] = set(state.keys())
172
+ if '__pydantic_extra__' not in state:
173
+ state['__pydantic_extra__'] = None
174
+ if '__pydantic_private__' not in state:
175
+ state['__pydantic_private__'] = None
176
+
177
+ # Update object state
178
+ self.__dict__.update(state)
179
+
180
+ @staticmethod
181
+ def toolkit_config_schema() -> BaseModel:
182
+ """
183
+ Generate the configuration schema for MCP toolkit.
184
+ Following MCP specification for connection parameters.
185
+ """
186
+ from pydantic import create_model
187
+
188
+ return create_model(
189
+ 'mcp',
190
+ url=(
191
+ str,
192
+ Field(
193
+ description="MCP server HTTP URL",
194
+ json_schema_extra={
195
+ 'tooltip': 'HTTP URL for the MCP server (http:// or https://)',
196
+ 'example': 'https://your-mcp-server.com/mcp'
197
+ }
198
+ )
199
+ ),
200
+ headers=(
201
+ Optional[Dict[str, str]],
202
+ Field(
203
+ default=None,
204
+ description="HTTP headers for authentication and configuration",
205
+ json_schema_extra={
206
+ 'tooltip': 'HTTP headers to send with requests (e.g. Authorization)',
207
+ 'example': {'Authorization': 'Bearer your-api-token'}
208
+ }
209
+ )
210
+ ),
211
+ timeout=(
212
+ Union[int, str], # TODO: remove one I will figure out why UI sends str
213
+ Field(
214
+ default=300,
215
+ description="Request timeout in seconds (1-3600)"
216
+ )
217
+ ),
218
+ discovery_mode=(
219
+ Literal['static', 'dynamic', 'hybrid'],
220
+ Field(
221
+ default="dynamic",
222
+ description="Discovery mode",
223
+ json_schema_extra={
224
+ 'tooltip': 'static: use registry, dynamic: live discovery, hybrid: try dynamic first'
225
+ }
226
+ )
227
+ ),
228
+ discovery_interval=(
229
+ Union[int, str],
230
+ Field(
231
+ default=300,
232
+ description="Discovery interval in seconds (60-3600, for periodic discovery)"
233
+ )
234
+ ),
235
+ selected_tools=(
236
+ List[str],
237
+ Field(
238
+ default=[],
239
+ description="Specific tools to enable (empty = all tools)",
240
+ json_schema_extra={
241
+ 'tooltip': 'Leave empty to enable all tools from the MCP server'
242
+ }
243
+ )
244
+ ),
245
+ enable_caching=(
246
+ bool,
247
+ Field(
248
+ default=True,
249
+ description="Enable caching of tool schemas and responses"
250
+ )
251
+ ),
252
+ cache_ttl=(
253
+ Union[int, str],
254
+ Field(
255
+ default=300,
256
+ description="Cache TTL in seconds (60-3600)"
257
+ )
258
+ ),
259
+ __config__=ConfigDict(
260
+ json_schema_extra={
261
+ 'metadata': {
262
+ "label": "Remove MCP",
263
+ "icon_url": None,
264
+ "categories": ["other"],
265
+ "extra_categories": ["remote tools", "sse", "http"],
266
+ "description": "Connect to a remote Model Context Protocol (MCP) server via HTTP to access tools"
267
+ }
268
+ }
269
+ )
270
+ )
271
+
272
+ @classmethod
273
+ def get_toolkit(
274
+ cls,
275
+ url: str,
276
+ headers: Optional[Dict[str, str]] = None,
277
+ timeout: int = 60,
278
+ discovery_mode: str = "hybrid",
279
+ discovery_interval: int = 300,
280
+ selected_tools: List[str] = None,
281
+ enable_caching: bool = True,
282
+ cache_ttl: int = 300,
283
+ toolkit_name: str = None,
284
+ client = None,
285
+ **kwargs
286
+ ) -> 'McpToolkit':
287
+ """
288
+ Create an MCP toolkit instance for a single MCP server.
289
+
290
+ When valid connection configuration (url + headers) is provided, the toolkit will:
291
+ 1. Immediately perform live discovery from the MCP server
292
+ 2. Create BaseTool instances for all discovered tools with complete schemas
293
+ 3. Include an inspection tool for server exploration
294
+ 4. Return all tools via get_tools() method
295
+
296
+ Args:
297
+ url: MCP server HTTP URL
298
+ headers: HTTP headers for authentication
299
+ timeout: Request timeout in seconds
300
+ discovery_mode: Discovery mode ('static', 'dynamic', 'hybrid')
301
+ discovery_interval: Discovery interval in seconds (for periodic discovery)
302
+ selected_tools: List of specific tools to enable (empty = all tools)
303
+ enable_caching: Whether to enable caching
304
+ cache_ttl: Cache TTL in seconds
305
+ toolkit_name: Toolkit name/identifier and prefix for tools
306
+ client: Alita client for MCP communication
307
+ **kwargs: Additional configuration options
308
+
309
+ Returns:
310
+ Configured McpToolkit instance with all available tools discovered
311
+ """
312
+ if selected_tools is None:
313
+ selected_tools = []
314
+
315
+ if not toolkit_name:
316
+ raise ValueError("toolkit_name is required")
317
+
318
+ # Convert numeric parameters that may come as strings from UI
319
+ timeout = safe_int(timeout, 60)
320
+ discovery_interval = safe_int(discovery_interval, 300)
321
+ cache_ttl = safe_int(cache_ttl, 300)
322
+
323
+ logger.info(f"Creating MCP toolkit: {toolkit_name}")
324
+
325
+ # Parse headers if they're provided as a JSON string
326
+ parsed_headers = headers
327
+ if isinstance(headers, str) and headers.strip():
328
+ try:
329
+ import json
330
+ logger.debug(f"Raw headers string length: {len(headers)} chars")
331
+ logger.debug(f"Raw headers string (first 100 chars): {headers[:100]}")
332
+ logger.debug(f"Raw headers string (last 100 chars): {headers[-100:]}")
333
+ parsed_headers = json.loads(headers)
334
+ logger.info(f"Parsed headers from JSON string successfully")
335
+ logger.debug(f"Parsed headers: {parsed_headers}")
336
+ except json.JSONDecodeError as e:
337
+ logger.error(f"Failed to parse headers JSON: {e}")
338
+ logger.error(f"Headers string length: {len(headers)}")
339
+ logger.error(f"Headers string content: {repr(headers)}")
340
+ raise ValueError(f"Invalid headers JSON format: {e}")
341
+ elif headers is not None and not isinstance(headers, dict):
342
+ logger.error(f"Headers must be a dictionary or JSON string, got: {type(headers)}")
343
+ raise ValueError(f"Headers must be a dictionary or JSON string, got: {type(headers)}")
344
+
345
+ # Extract session_id from kwargs if provided
346
+ session_id = kwargs.get('session_id')
347
+ if session_id:
348
+ logger.info(f"[MCP Session] Using provided session ID for toolkit '{toolkit_name}': {session_id}")
349
+
350
+ # Create MCP connection configuration
351
+ try:
352
+ connection_config = McpConnectionConfig(url=url, headers=parsed_headers, session_id=session_id)
353
+ except Exception as e:
354
+ logger.error(f"Invalid MCP connection configuration: {e}")
355
+ raise ValueError(f"Invalid MCP connection configuration: {e}")
356
+
357
+ # Create toolkit instance
358
+ toolkit = cls(toolkit_name=toolkit_name)
359
+
360
+ # Generate tools from the MCP server
361
+ toolkit.tools = cls._create_tools_from_server(
362
+ toolkit_name=toolkit_name,
363
+ connection_config=connection_config,
364
+ timeout=timeout,
365
+ selected_tools=selected_tools,
366
+ client=client,
367
+ discovery_mode=discovery_mode
368
+ )
369
+
370
+ return toolkit
371
+
372
+ @classmethod
373
+ def _create_tools_from_server(
374
+ cls,
375
+ toolkit_name: str,
376
+ connection_config: McpConnectionConfig,
377
+ timeout: int,
378
+ selected_tools: List[str],
379
+ client,
380
+ discovery_mode: str = "dynamic"
381
+ ) -> List[BaseTool]:
382
+ """
383
+ Create tools from a single MCP server. Always performs live discovery when connection config is provided.
384
+ """
385
+ tools = []
386
+
387
+ # First, try direct HTTP discovery since we have valid connection config
388
+ try:
389
+ logger.info(f"Discovering tools from MCP toolkit '{toolkit_name}' at {connection_config.url}")
390
+
391
+ # Use synchronous HTTP discovery for toolkit initialization
392
+ tool_metadata_list, session_id = cls._discover_tools_sync(
393
+ toolkit_name=toolkit_name,
394
+ connection_config=connection_config,
395
+ timeout=timeout
396
+ )
397
+
398
+ # Filter tools if specific ones are selected
399
+ selected_tools_lower = [tool.lower() for tool in selected_tools] if selected_tools else []
400
+ if selected_tools_lower:
401
+ tool_metadata_list = [
402
+ tool for tool in tool_metadata_list
403
+ if tool.get('name', '').lower() in selected_tools_lower
404
+ ]
405
+
406
+ # Create BaseTool instances from discovered metadata
407
+ # Use session_id from frontend (passed via connection_config)
408
+ if session_id:
409
+ logger.info(f"[MCP Session] Using session_id from frontend: {session_id}")
410
+
411
+ for tool_metadata in tool_metadata_list:
412
+ server_tool = cls._create_tool_from_dict(
413
+ tool_dict=tool_metadata,
414
+ toolkit_name=toolkit_name,
415
+ connection_config=connection_config,
416
+ timeout=timeout,
417
+ client=client,
418
+ session_id=session_id # Use session from discovery
419
+ )
420
+
421
+ if server_tool:
422
+ tools.append(server_tool)
423
+
424
+ logger.info(f"Successfully created {len(tools)} MCP tools from toolkit '{toolkit_name}' via direct discovery")
425
+
426
+ except Exception as e:
427
+ logger.error(f"Direct discovery failed for MCP toolkit '{toolkit_name}': {e}", exc_info=True)
428
+ logger.error(f"Discovery error details - URL: {connection_config.url}, Timeout: {timeout}s")
429
+
430
+ # Fallback to static mode if available and not already static
431
+ if isinstance(e, McpAuthorizationRequired):
432
+ # Authorization is required; surface upstream so the caller can prompt the user
433
+ raise
434
+ if client and discovery_mode != "static":
435
+ logger.info(f"Falling back to static discovery for toolkit '{toolkit_name}'")
436
+ tools = cls._create_tools_static(toolkit_name, selected_tools, timeout, client)
437
+ else:
438
+ logger.warning(f"No fallback available for toolkit '{toolkit_name}' - returning empty tools list")
439
+
440
+ # Don't add inspection tool to agent - it's only for internal use by toolkit
441
+ # inspection_tool = cls._create_inspection_tool(
442
+ # toolkit_name=toolkit_name,
443
+ # connection_config=connection_config
444
+ # )
445
+ # if inspection_tool:
446
+ # tools.append(inspection_tool)
447
+ # logger.info(f"Added MCP inspection tool for toolkit '{toolkit_name}'")
448
+
449
+ # Log final tool count before returning
450
+ logger.info(f"MCP toolkit '{toolkit_name}' will provide {len(tools)} tools to agent")
451
+ if len(tools) == 0:
452
+ logger.warning(f"MCP toolkit '{toolkit_name}' has no tools - discovery may have failed")
453
+
454
+ return tools
455
+
456
+ @classmethod
457
+ def _discover_tools_sync(
458
+ cls,
459
+ toolkit_name: str,
460
+ connection_config: McpConnectionConfig,
461
+ timeout: int
462
+ ) -> List[Dict[str, Any]]:
463
+ """
464
+ Discover tools and prompts from MCP server using SSE client.
465
+ Returns list of tool/prompt dictionaries with name, description, and inputSchema.
466
+ Prompts are converted to tools that can be invoked.
467
+ """
468
+ session_id = connection_config.session_id
469
+
470
+ if not session_id:
471
+ logger.warning(f"[MCP Session] No session_id provided for '{toolkit_name}' - server may require it")
472
+ logger.warning(f"[MCP Session] Frontend should generate a UUID and include it with mcp_tokens")
473
+
474
+ # Run async discovery in sync context
475
+ try:
476
+ all_tools = asyncio.run(
477
+ cls._discover_tools_async(
478
+ toolkit_name=toolkit_name,
479
+ connection_config=connection_config,
480
+ timeout=timeout
481
+ )
482
+ )
483
+ return all_tools, session_id
484
+ except Exception as e:
485
+ logger.error(f"[MCP SSE] Discovery failed for '{toolkit_name}': {e}")
486
+ raise
487
+
488
+ @classmethod
489
+ async def _discover_tools_async(
490
+ cls,
491
+ toolkit_name: str,
492
+ connection_config: McpConnectionConfig,
493
+ timeout: int
494
+ ) -> List[Dict[str, Any]]:
495
+ """
496
+ Async implementation of tool discovery using SSE client.
497
+ """
498
+ all_tools = []
499
+ session_id = connection_config.session_id
500
+
501
+ # Generate temporary session_id if not provided (for OAuth flow)
502
+ # The real session_id should come from frontend after OAuth completes
503
+ if not session_id:
504
+ import uuid
505
+ session_id = str(uuid.uuid4())
506
+ logger.info(f"[MCP SSE] Generated temporary session_id for OAuth: {session_id}")
507
+
508
+ logger.info(f"[MCP SSE] Discovering from {connection_config.url} with session {session_id}")
509
+
510
+ # Prepare headers
511
+ headers = {}
512
+ if connection_config.headers:
513
+ headers.update(connection_config.headers)
514
+
515
+ # Create SSE client
516
+ client = McpSseClient(
517
+ url=connection_config.url,
518
+ session_id=session_id,
519
+ headers=headers,
520
+ timeout=timeout
521
+ )
522
+
523
+ # Initialize MCP session
524
+ await client.initialize()
525
+ logger.info(f"[MCP SSE] Session initialized for '{toolkit_name}'")
526
+
527
+ # Discover tools
528
+ tools = await client.list_tools()
529
+ all_tools.extend(tools)
530
+ logger.info(f"[MCP SSE] Discovered {len(tools)} tools from '{toolkit_name}'")
531
+
532
+ # Discover prompts
533
+ try:
534
+ prompts = await client.list_prompts()
535
+ # Convert prompts to tool format
536
+ for prompt in prompts:
537
+ prompt_tool = {
538
+ "name": f"prompt_{prompt.get('name', 'unnamed')}",
539
+ "description": prompt.get('description', f"Execute prompt: {prompt.get('name')}"),
540
+ "inputSchema": {
541
+ "type": "object",
542
+ "properties": {
543
+ "arguments": {
544
+ "type": "object",
545
+ "description": "Arguments for the prompt template",
546
+ "properties": {
547
+ arg.get("name"): {
548
+ "type": "string",
549
+ "description": arg.get("description", ""),
550
+ "required": arg.get("required", False)
551
+ }
552
+ for arg in prompt.get("arguments", [])
553
+ }
554
+ }
555
+ }
556
+ },
557
+ "_mcp_type": "prompt",
558
+ "_mcp_prompt_name": prompt.get('name')
559
+ }
560
+ all_tools.append(prompt_tool)
561
+ logger.info(f"[MCP SSE] Discovered {len(prompts)} prompts from '{toolkit_name}'")
562
+ except Exception as e:
563
+ logger.warning(f"[MCP SSE] Failed to discover prompts: {e}")
564
+
565
+ logger.info(f"[MCP SSE] Total discovered {len(all_tools)} items from '{toolkit_name}'")
566
+ return all_tools
567
+
568
+ @classmethod
569
+ def _create_tool_from_dict(
570
+ cls,
571
+ tool_dict: Dict[str, Any],
572
+ toolkit_name: str,
573
+ connection_config: McpConnectionConfig,
574
+ timeout: int,
575
+ client,
576
+ session_id: Optional[str] = None
577
+ ) -> Optional[BaseTool]:
578
+ """Create a BaseTool from a tool/prompt dictionary (from direct HTTP discovery)."""
579
+ try:
580
+ # Store toolkit_max_length in local variable to avoid contextual access issues
581
+ max_length_value = cls.toolkit_max_length
582
+
583
+ # Clean toolkit name for prefixing
584
+ clean_prefix = clean_string(toolkit_name, max_length_value)
585
+
586
+ # Optimize tool name to fit within 64 character limit
587
+ full_tool_name = optimize_tool_name(clean_prefix, tool_dict.get("name", "unknown"))
588
+
589
+ # Check if this is a prompt (converted to tool)
590
+ is_prompt = tool_dict.get("_mcp_type") == "prompt"
591
+ item_type = "prompt" if is_prompt else "tool"
592
+
593
+ # Build description and ensure it doesn't exceed 1000 characters
594
+ description = f"MCP {item_type} '{tool_dict.get('name')}' from toolkit '{toolkit_name}': {tool_dict.get('description', '')}"
595
+ if len(description) > 1000:
596
+ description = description[:997] + "..."
597
+ logger.debug(f"Trimmed description for tool '{tool_dict.get('name')}' from {len(description)} to 1000 chars")
598
+
599
+ # Use McpRemoteTool for remote MCP servers (HTTP/SSE)
600
+ return McpRemoteTool(
601
+ name=full_tool_name,
602
+ description=description,
603
+ args_schema=McpServerTool.create_pydantic_model_from_schema(
604
+ tool_dict.get("inputSchema", {})
605
+ ),
606
+ client=client,
607
+ server=toolkit_name,
608
+ server_url=connection_config.url,
609
+ server_headers=connection_config.headers,
610
+ tool_timeout_sec=timeout,
611
+ is_prompt=is_prompt,
612
+ prompt_name=tool_dict.get("_mcp_prompt_name") if is_prompt else None,
613
+ original_tool_name=tool_dict.get('name'), # Store original name for MCP server invocation
614
+ session_id=session_id # Pass session ID for stateful SSE servers
615
+ )
616
+ except Exception as e:
617
+ logger.error(f"Failed to create MCP tool '{tool_dict.get('name')}' from toolkit '{toolkit_name}': {e}")
618
+ return None
619
+
620
+ @classmethod
621
+ def _create_tools_static(
622
+ cls,
623
+ toolkit_name: str,
624
+ selected_tools: List[str],
625
+ timeout: int,
626
+ client
627
+ ) -> List[BaseTool]:
628
+ """Fallback static tool creation using the original method."""
629
+ tools = []
630
+
631
+ if not client or not hasattr(client, 'get_mcp_toolkits'):
632
+ logger.warning("Alita client does not support MCP toolkit discovery")
633
+ return tools
634
+
635
+ try:
636
+ all_toolkits = client.get_mcp_toolkits()
637
+ server_toolkit = next((tk for tk in all_toolkits if tk.get('name') == toolkit_name), None)
638
+
639
+ if not server_toolkit:
640
+ logger.warning(f"MCP toolkit '{toolkit_name}' not found in available toolkits")
641
+ return tools
642
+
643
+ # Extract tools from the toolkit
644
+ available_tools = server_toolkit.get('tools', [])
645
+ selected_tools_lower = [tool.lower() for tool in selected_tools] if selected_tools else []
646
+
647
+ for available_tool in available_tools:
648
+ tool_name = available_tool.get("name", "").lower()
649
+
650
+ # Filter tools if specific tools are selected
651
+ if selected_tools_lower and tool_name not in selected_tools_lower:
652
+ continue
653
+
654
+ # Create the tool
655
+ server_tool = cls._create_single_tool(
656
+ toolkit_name=toolkit_name,
657
+ available_tool=available_tool,
658
+ timeout=timeout,
659
+ client=client
660
+ )
661
+
662
+ if server_tool:
663
+ tools.append(server_tool)
664
+
665
+ logger.info(f"Successfully created {len(tools)} MCP tools from toolkit '{toolkit_name}' using static mode")
666
+
667
+ except Exception as e:
668
+ logger.error(f"Error in static tool creation: {e}")
669
+
670
+ # Always add the inspection tool (not subject to selected_tools filtering)
671
+ # For static mode, we need to create a basic connection config from the server info
672
+ try:
673
+ # We don't have full connection config in static mode, so create a basic one
674
+ # The inspection tool will work as long as the server is accessible
675
+ inspection_tool = McpInspectTool(
676
+ name=f"{clean_string(toolkit_name, 50)}{TOOLKIT_SPLITTER}mcp_inspect",
677
+ server_name=toolkit_name,
678
+ server_url="", # Will be populated by the client if available
679
+ description=f"Inspect available tools, prompts, and resources from MCP toolkit '{toolkit_name}'"
680
+ )
681
+ tools.append(inspection_tool)
682
+ logger.info(f"Added MCP inspection tool for toolkit '{toolkit_name}' (static mode)")
683
+ except Exception as e:
684
+ logger.warning(f"Failed to create inspection tool for {toolkit_name}: {e}")
685
+
686
+ return tools
687
+
688
+ @classmethod
689
+ def _create_tool_from_metadata(
690
+ cls,
691
+ tool_metadata,
692
+ toolkit_name: str,
693
+ timeout: int,
694
+ client
695
+ ) -> Optional[BaseTool]:
696
+ """Create a BaseTool from discovered metadata."""
697
+ try:
698
+ # Store toolkit_max_length in local variable to avoid contextual access issues
699
+ max_length_value = cls.toolkit_max_length
700
+
701
+ # Clean server name for prefixing (use tool_metadata.server instead of toolkit_name)
702
+ clean_prefix = clean_string(tool_metadata.server, max_length_value)
703
+ # Optimize tool name to fit within 64 character limit
704
+ full_tool_name = optimize_tool_name(clean_prefix, tool_metadata.name)
705
+
706
+ # Build description and ensure it doesn't exceed 1000 characters
707
+ description = f"MCP tool '{tool_metadata.name}' from server '{tool_metadata.server}': {tool_metadata.description}"
708
+ if len(description) > 1000:
709
+ description = description[:997] + "..."
710
+ logger.debug(f"Trimmed description for tool '{tool_metadata.name}' from {len(description)} to 1000 chars")
711
+
712
+ return McpServerTool(
713
+ name=full_tool_name,
714
+ description=description,
715
+ args_schema=McpServerTool.create_pydantic_model_from_schema(tool_metadata.input_schema),
716
+ client=client,
717
+ server=tool_metadata.server,
718
+ tool_timeout_sec=timeout
719
+ )
720
+ except Exception as e:
721
+ logger.error(f"Failed to create MCP tool '{tool_metadata.name}' from server '{tool_metadata.server}': {e}")
722
+ return None
723
+
724
+ @classmethod
725
+ def _create_single_tool(
726
+ cls,
727
+ toolkit_name: str,
728
+ available_tool: Dict[str, Any],
729
+ timeout: int,
730
+ client
731
+ ) -> Optional[BaseTool]:
732
+ """Create a single MCP tool."""
733
+ try:
734
+ # Store toolkit_max_length in local variable to avoid contextual access issues
735
+ max_length_value = cls.toolkit_max_length
736
+
737
+ # Clean toolkit name for prefixing
738
+ clean_prefix = clean_string(toolkit_name, max_length_value)
739
+
740
+ # Optimize tool name to fit within 64 character limit
741
+ full_tool_name = optimize_tool_name(clean_prefix, available_tool["name"])
742
+
743
+ # Build description and ensure it doesn't exceed 1000 characters
744
+ description = f"MCP tool '{available_tool['name']}' from toolkit '{toolkit_name}': {available_tool.get('description', '')}"
745
+ if len(description) > 1000:
746
+ description = description[:997] + "..."
747
+ logger.debug(f"Trimmed description for tool '{available_tool['name']}' from {len(description)} to 1000 chars")
748
+
749
+ return McpServerTool(
750
+ name=full_tool_name,
751
+ description=description,
752
+ args_schema=McpServerTool.create_pydantic_model_from_schema(
753
+ available_tool.get("inputSchema", {})
754
+ ),
755
+ client=client,
756
+ server=toolkit_name,
757
+ tool_timeout_sec=timeout
758
+ )
759
+ except Exception as e:
760
+ logger.error(f"Failed to create MCP tool '{available_tool.get('name')}' from toolkit '{toolkit_name}': {e}")
761
+ return None
762
+
763
+ @classmethod
764
+ def _create_inspection_tool(
765
+ cls,
766
+ toolkit_name: str,
767
+ connection_config: McpConnectionConfig
768
+ ) -> Optional[BaseTool]:
769
+ """Create the inspection tool for the MCP toolkit."""
770
+ try:
771
+ # Store toolkit_max_length in local variable to avoid contextual access issues
772
+ max_length_value = cls.toolkit_max_length
773
+
774
+ # Clean toolkit name for prefixing
775
+ clean_prefix = clean_string(toolkit_name, max_length_value)
776
+
777
+ full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}mcp_inspect'
778
+
779
+ return McpInspectTool(
780
+ name=full_tool_name,
781
+ server_name=toolkit_name,
782
+ server_url=connection_config.url,
783
+ server_headers=connection_config.headers,
784
+ description=f"Inspect available tools, prompts, and resources from MCP toolkit '{toolkit_name}'"
785
+ )
786
+ except Exception as e:
787
+ logger.error(f"Failed to create MCP inspection tool for toolkit '{toolkit_name}': {e}")
788
+ return None
789
+
790
+ def get_tools(self) -> List[BaseTool]:
791
+ """Get the list of tools provided by this toolkit."""
792
+ logger.info(f"MCP toolkit '{self.toolkit_name}' returning {len(self.tools)} tools")
793
+ if len(self.tools) > 0:
794
+ tool_names = [t.name if hasattr(t, 'name') else str(t) for t in self.tools]
795
+ logger.info(f"MCP toolkit '{self.toolkit_name}' tools: {tool_names}")
796
+ return self.tools
797
+
798
+ async def refresh_tools(self):
799
+ """Manually refresh tools from the MCP toolkit."""
800
+ if not self.toolkit_name:
801
+ logger.warning("Cannot refresh tools: toolkit_name not set")
802
+ return
803
+
804
+ try:
805
+ from ..clients.mcp_manager import get_mcp_manager
806
+ manager = get_mcp_manager()
807
+ await manager.refresh_server(self.toolkit_name)
808
+ logger.info(f"Successfully refreshed tools for toolkit {self.toolkit_name}")
809
+ except Exception as e:
810
+ logger.error(f"Failed to refresh tools for toolkit {self.toolkit_name}: {e}")
811
+
812
+ async def get_server_health(self) -> Dict[str, Any]:
813
+ """Get health status of the configured MCP toolkit."""
814
+ if not self.toolkit_name:
815
+ return {"status": "not_configured"}
816
+
817
+ try:
818
+ from ..clients.mcp_manager import get_mcp_manager
819
+ manager = get_mcp_manager()
820
+ health_info = await manager.get_server_health(self.toolkit_name)
821
+ return health_info
822
+ except Exception as e:
823
+ logger.error(f"Failed to get server health for {self.toolkit_name}: {e}")
824
+ return {"status": "error", "error": str(e)}
825
+
826
+
827
+ def get_tools(tool_config: dict, alita_client, llm=None, memory_store=None) -> List[BaseTool]:
828
+ """
829
+ Create MCP tools from configuration.
830
+ This function is called by the main tool loading system.
831
+
832
+ Args:
833
+ tool_config: Tool configuration dictionary
834
+ alita_client: Alita client instance
835
+ llm: Language model (not used by MCP tools)
836
+ memory_store: Memory store (not used by MCP tools)
837
+
838
+ Returns:
839
+ List of configured MCP tools
840
+ """
841
+ settings = tool_config.get('settings', {})
842
+ toolkit_name = tool_config.get('toolkit_name')
843
+
844
+ # Extract required fields
845
+ url = settings.get('url')
846
+ headers = settings.get('headers')
847
+
848
+ if not toolkit_name:
849
+ logger.error("MCP toolkit configuration missing required 'toolkit_name'")
850
+ return []
851
+
852
+ if not url:
853
+ logger.error("MCP toolkit configuration missing required 'url'")
854
+ return []
855
+
856
+ # Type conversion for numeric settings that may come as strings from config
857
+ return McpToolkit.get_toolkit(
858
+ url=url,
859
+ headers=headers,
860
+ timeout=safe_int(settings.get('timeout'), 60),
861
+ discovery_mode=settings.get('discovery_mode', 'dynamic'),
862
+ discovery_interval=safe_int(settings.get('discovery_interval'), 300),
863
+ selected_tools=settings.get('selected_tools', []),
864
+ enable_caching=settings.get('enable_caching', True),
865
+ cache_ttl=safe_int(settings.get('cache_ttl'), 300),
866
+ toolkit_name=toolkit_name,
867
+ client=alita_client
868
+ ).get_tools()
869
+
870
+
871
+ # Utility functions for managing MCP discovery
872
+ async def start_global_discovery():
873
+ """Start the global MCP discovery service."""
874
+ from ..clients.mcp_discovery import init_discovery_service
875
+ await init_discovery_service()
876
+
877
+
878
+ async def stop_global_discovery():
879
+ """Stop the global MCP discovery service."""
880
+ from ..clients.mcp_discovery import shutdown_discovery_service
881
+ await shutdown_discovery_service()
882
+
883
+
884
+ async def register_mcp_server_for_discovery(toolkit_name: str, connection_config):
885
+ """Register an MCP server for global discovery."""
886
+ from ..clients.mcp_discovery import get_discovery_service
887
+ service = get_discovery_service()
888
+ await service.register_server(toolkit_name, connection_config)
889
+
890
+
891
+ def get_all_discovered_servers():
892
+ """Get status of all discovered servers."""
893
+ from ..clients.mcp_discovery import get_discovery_service
894
+ service = get_discovery_service()
895
+ return service.get_server_health()