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.

@@ -0,0 +1,787 @@
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 requests
9
+ from typing import List, Optional, Any, Dict, Literal, ClassVar
10
+
11
+ from langchain_core.tools import BaseToolkit, BaseTool
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+ from ..tools.mcp_server_tool import McpServerTool
15
+ from ..tools.mcp_inspect_tool import McpInspectTool
16
+ from ...tools.utils import TOOLKIT_SPLITTER, clean_string
17
+ from ..models.mcp_models import McpConnectionConfig
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ name = "mcp"
22
+
23
+
24
+ class McpToolkit(BaseToolkit):
25
+ """
26
+ MCP Toolkit for connecting to a single remote MCP server and exposing its tools.
27
+ Each toolkit instance represents one MCP server connection.
28
+ """
29
+
30
+ tools: List[BaseTool] = []
31
+ toolkit_name: Optional[str] = None
32
+
33
+ # Class variable (not Pydantic field) for tool name length limit
34
+ toolkit_max_length: ClassVar[int] = 0 # No limit for MCP tool names
35
+
36
+ def __getstate__(self):
37
+ """Custom serialization for pickle compatibility."""
38
+ state = self.__dict__.copy()
39
+ # The tools list should already be pickle-safe due to individual tool fixes
40
+ # Just return the state as-is since tools handle their own serialization
41
+ return state
42
+
43
+ def __setstate__(self, state):
44
+ """Custom deserialization for pickle compatibility."""
45
+ # Initialize Pydantic internal attributes if needed
46
+ if '__pydantic_fields_set__' not in state:
47
+ state['__pydantic_fields_set__'] = set(state.keys())
48
+ if '__pydantic_extra__' not in state:
49
+ state['__pydantic_extra__'] = None
50
+ if '__pydantic_private__' not in state:
51
+ state['__pydantic_private__'] = None
52
+
53
+ # Update object state
54
+ self.__dict__.update(state)
55
+
56
+ @staticmethod
57
+ def toolkit_config_schema() -> BaseModel:
58
+ """
59
+ Generate the configuration schema for MCP toolkit.
60
+ Following MCP specification for connection parameters.
61
+ """
62
+ from pydantic import create_model
63
+
64
+ return create_model(
65
+ 'mcp',
66
+ url=(
67
+ str,
68
+ Field(
69
+ description="MCP server HTTP URL",
70
+ json_schema_extra={
71
+ 'tooltip': 'HTTP URL for the MCP server (http:// or https://)',
72
+ 'example': 'https://your-mcp-server.com/mcp'
73
+ }
74
+ )
75
+ ),
76
+ headers=(
77
+ Optional[Dict[str, str]],
78
+ Field(
79
+ default=None,
80
+ description="HTTP headers for authentication and configuration",
81
+ json_schema_extra={
82
+ 'tooltip': 'HTTP headers to send with requests (e.g. Authorization)',
83
+ 'example': {'Authorization': 'Bearer your-api-token'}
84
+ }
85
+ )
86
+ ),
87
+ timeout=(
88
+ int,
89
+ Field(
90
+ default=60,
91
+ ge=1, le=300,
92
+ description="Request timeout in seconds"
93
+ )
94
+ ),
95
+ discovery_mode=(
96
+ Literal['static', 'dynamic', 'hybrid'],
97
+ Field(
98
+ default="dynamic",
99
+ description="Discovery mode",
100
+ json_schema_extra={
101
+ 'tooltip': 'static: use registry, dynamic: live discovery, hybrid: try dynamic first'
102
+ }
103
+ )
104
+ ),
105
+ discovery_interval=(
106
+ int,
107
+ Field(
108
+ default=300,
109
+ ge=60, le=3600,
110
+ description="Discovery interval in seconds (for periodic discovery)"
111
+ )
112
+ ),
113
+ selected_tools=(
114
+ List[str],
115
+ Field(
116
+ default=[],
117
+ description="Specific tools to enable (empty = all tools)",
118
+ json_schema_extra={
119
+ 'tooltip': 'Leave empty to enable all tools from the MCP server'
120
+ }
121
+ )
122
+ ),
123
+ enable_caching=(
124
+ bool,
125
+ Field(
126
+ default=True,
127
+ description="Enable caching of tool schemas and responses"
128
+ )
129
+ ),
130
+ cache_ttl=(
131
+ int,
132
+ Field(
133
+ default=300,
134
+ ge=60, le=3600,
135
+ description="Cache TTL in seconds"
136
+ )
137
+ ),
138
+ __config__=ConfigDict(
139
+ json_schema_extra={
140
+ 'metadata': {
141
+ "label": "Remove MCP",
142
+ "icon_url": None,
143
+ "categories": ["other"],
144
+ "extra_categories": ["remote tools", "sse", "http"],
145
+ "description": "Connect to a remote Model Context Protocol (MCP) server via HTTP to access tools"
146
+ }
147
+ }
148
+ )
149
+ )
150
+
151
+ @classmethod
152
+ def get_toolkit(
153
+ cls,
154
+ url: str,
155
+ headers: Optional[Dict[str, str]] = None,
156
+ timeout: int = 60,
157
+ discovery_mode: str = "hybrid",
158
+ discovery_interval: int = 300,
159
+ selected_tools: List[str] = None,
160
+ enable_caching: bool = True,
161
+ cache_ttl: int = 300,
162
+ toolkit_name: str = None,
163
+ client = None,
164
+ **kwargs
165
+ ) -> 'McpToolkit':
166
+ """
167
+ Create an MCP toolkit instance for a single MCP server.
168
+
169
+ When valid connection configuration (url + headers) is provided, the toolkit will:
170
+ 1. Immediately perform live discovery from the MCP server
171
+ 2. Create BaseTool instances for all discovered tools with complete schemas
172
+ 3. Include an inspection tool for server exploration
173
+ 4. Return all tools via get_tools() method
174
+
175
+ Args:
176
+ url: MCP server HTTP URL
177
+ headers: HTTP headers for authentication
178
+ timeout: Request timeout in seconds
179
+ discovery_mode: Discovery mode ('static', 'dynamic', 'hybrid')
180
+ discovery_interval: Discovery interval in seconds (for periodic discovery)
181
+ selected_tools: List of specific tools to enable (empty = all tools)
182
+ enable_caching: Whether to enable caching
183
+ cache_ttl: Cache TTL in seconds
184
+ toolkit_name: Toolkit name/identifier and prefix for tools
185
+ client: Alita client for MCP communication
186
+ **kwargs: Additional configuration options
187
+
188
+ Returns:
189
+ Configured McpToolkit instance with all available tools discovered
190
+ """
191
+ if selected_tools is None:
192
+ selected_tools = []
193
+
194
+ if not toolkit_name:
195
+ raise ValueError("toolkit_name is required")
196
+
197
+ logger.info(f"Creating MCP toolkit: {toolkit_name}")
198
+
199
+ # Parse headers if they're provided as a JSON string
200
+ parsed_headers = headers
201
+ if isinstance(headers, str) and headers.strip():
202
+ try:
203
+ import json
204
+ logger.debug(f"Raw headers string length: {len(headers)} chars")
205
+ logger.debug(f"Raw headers string (first 100 chars): {headers[:100]}")
206
+ logger.debug(f"Raw headers string (last 100 chars): {headers[-100:]}")
207
+ parsed_headers = json.loads(headers)
208
+ logger.info(f"Parsed headers from JSON string successfully")
209
+ logger.debug(f"Parsed headers: {parsed_headers}")
210
+ except json.JSONDecodeError as e:
211
+ logger.error(f"Failed to parse headers JSON: {e}")
212
+ logger.error(f"Headers string length: {len(headers)}")
213
+ logger.error(f"Headers string content: {repr(headers)}")
214
+ raise ValueError(f"Invalid headers JSON format: {e}")
215
+ elif headers is not None and not isinstance(headers, dict):
216
+ logger.error(f"Headers must be a dictionary or JSON string, got: {type(headers)}")
217
+ raise ValueError(f"Headers must be a dictionary or JSON string, got: {type(headers)}")
218
+
219
+ # Create MCP connection configuration
220
+ try:
221
+ connection_config = McpConnectionConfig(url=url, headers=parsed_headers)
222
+ except Exception as e:
223
+ logger.error(f"Invalid MCP connection configuration: {e}")
224
+ raise ValueError(f"Invalid MCP connection configuration: {e}")
225
+
226
+ # Create toolkit instance
227
+ toolkit = cls(toolkit_name=toolkit_name)
228
+
229
+ # Generate tools from the MCP server
230
+ toolkit.tools = cls._create_tools_from_server(
231
+ toolkit_name=toolkit_name,
232
+ connection_config=connection_config,
233
+ timeout=timeout,
234
+ selected_tools=selected_tools,
235
+ client=client,
236
+ discovery_mode=discovery_mode
237
+ )
238
+
239
+ return toolkit
240
+
241
+ @classmethod
242
+ def _create_tools_from_server(
243
+ cls,
244
+ toolkit_name: str,
245
+ connection_config: McpConnectionConfig,
246
+ timeout: int,
247
+ selected_tools: List[str],
248
+ client,
249
+ discovery_mode: str = "dynamic"
250
+ ) -> List[BaseTool]:
251
+ """
252
+ Create tools from a single MCP server. Always performs live discovery when connection config is provided.
253
+ """
254
+ tools = []
255
+
256
+ # First, try direct HTTP discovery since we have valid connection config
257
+ try:
258
+ logger.info(f"Discovering tools from MCP toolkit '{toolkit_name}' at {connection_config.url}")
259
+
260
+ # Use synchronous HTTP discovery for toolkit initialization
261
+ tool_metadata_list = cls._discover_tools_sync(
262
+ toolkit_name=toolkit_name,
263
+ connection_config=connection_config,
264
+ timeout=timeout
265
+ )
266
+
267
+ # Filter tools if specific ones are selected
268
+ selected_tools_lower = [tool.lower() for tool in selected_tools] if selected_tools else []
269
+ if selected_tools_lower:
270
+ tool_metadata_list = [
271
+ tool for tool in tool_metadata_list
272
+ if tool.get('name', '').lower() in selected_tools_lower
273
+ ]
274
+
275
+ # Create BaseTool instances from discovered metadata
276
+ for tool_metadata in tool_metadata_list:
277
+ server_tool = cls._create_tool_from_dict(
278
+ tool_dict=tool_metadata,
279
+ toolkit_name=toolkit_name,
280
+ timeout=timeout,
281
+ client=client
282
+ )
283
+
284
+ if server_tool:
285
+ tools.append(server_tool)
286
+
287
+ logger.info(f"Successfully created {len(tools)} MCP tools from toolkit '{toolkit_name}' via direct discovery")
288
+
289
+ except Exception as e:
290
+ logger.error(f"Direct discovery failed for MCP toolkit '{toolkit_name}': {e}", exc_info=True)
291
+ logger.error(f"Discovery error details - URL: {connection_config.url}, Timeout: {timeout}s")
292
+
293
+ # Fallback to static mode if available and not already static
294
+ if client and discovery_mode != "static":
295
+ logger.info(f"Falling back to static discovery for toolkit '{toolkit_name}'")
296
+ tools = cls._create_tools_static(toolkit_name, selected_tools, timeout, client)
297
+ else:
298
+ logger.warning(f"No fallback available for toolkit '{toolkit_name}' - returning empty tools list")
299
+
300
+ # Don't add inspection tool to agent - it's only for internal use by toolkit
301
+ # inspection_tool = cls._create_inspection_tool(
302
+ # toolkit_name=toolkit_name,
303
+ # connection_config=connection_config
304
+ # )
305
+ # if inspection_tool:
306
+ # tools.append(inspection_tool)
307
+ # logger.info(f"Added MCP inspection tool for toolkit '{toolkit_name}'")
308
+
309
+ # Log final tool count before returning
310
+ logger.info(f"MCP toolkit '{toolkit_name}' will provide {len(tools)} tools to agent")
311
+ if len(tools) == 0:
312
+ logger.warning(f"MCP toolkit '{toolkit_name}' has no tools - discovery may have failed")
313
+
314
+ return tools
315
+
316
+ @classmethod
317
+ def _discover_tools_sync(
318
+ cls,
319
+ toolkit_name: str,
320
+ connection_config: McpConnectionConfig,
321
+ timeout: int
322
+ ) -> List[Dict[str, Any]]:
323
+ """
324
+ Synchronously discover tools and prompts from MCP server using HTTP requests.
325
+ Returns list of tool/prompt dictionaries with name, description, and inputSchema.
326
+ Prompts are converted to tools that can be invoked.
327
+ """
328
+ all_tools = []
329
+
330
+ # Discover regular tools
331
+ tools_data = cls._discover_mcp_endpoint(
332
+ endpoint="tools/list",
333
+ toolkit_name=toolkit_name,
334
+ connection_config=connection_config,
335
+ timeout=timeout
336
+ )
337
+ all_tools.extend(tools_data)
338
+ logger.info(f"Discovered {len(tools_data)} tools from MCP toolkit '{toolkit_name}'")
339
+
340
+ # Discover prompts and convert them to tools
341
+ try:
342
+ prompts_data = cls._discover_mcp_endpoint(
343
+ endpoint="prompts/list",
344
+ toolkit_name=toolkit_name,
345
+ connection_config=connection_config,
346
+ timeout=timeout
347
+ )
348
+ # Convert prompts to tool format
349
+ for prompt in prompts_data:
350
+ prompt_tool = {
351
+ "name": f"prompt_{prompt.get('name', 'unnamed')}",
352
+ "description": prompt.get('description', f"Execute prompt: {prompt.get('name')}"),
353
+ "inputSchema": {
354
+ "type": "object",
355
+ "properties": {
356
+ "arguments": {
357
+ "type": "object",
358
+ "description": "Arguments for the prompt template",
359
+ "properties": {
360
+ arg.get("name"): {
361
+ "type": "string",
362
+ "description": arg.get("description", ""),
363
+ "required": arg.get("required", False)
364
+ }
365
+ for arg in prompt.get("arguments", [])
366
+ }
367
+ }
368
+ }
369
+ },
370
+ "_mcp_type": "prompt",
371
+ "_mcp_prompt_name": prompt.get('name')
372
+ }
373
+ all_tools.append(prompt_tool)
374
+ logger.info(f"Discovered {len(prompts_data)} prompts from MCP toolkit '{toolkit_name}'")
375
+ except Exception as e:
376
+ logger.warning(f"Failed to discover prompts from MCP toolkit '{toolkit_name}': {e}")
377
+
378
+ logger.info(f"Total discovered {len(all_tools)} tools+prompts from MCP toolkit '{toolkit_name}'")
379
+ return all_tools
380
+
381
+ @classmethod
382
+ def _discover_mcp_endpoint(
383
+ cls,
384
+ endpoint: str,
385
+ toolkit_name: str,
386
+ connection_config: McpConnectionConfig,
387
+ timeout: int
388
+ ) -> List[Dict[str, Any]]:
389
+ """
390
+ Discover items from a specific MCP endpoint (tools/list or prompts/list).
391
+ Returns list of dictionaries.
392
+ """
393
+ import time
394
+
395
+ # MCP protocol request
396
+ mcp_request = {
397
+ "jsonrpc": "2.0",
398
+ "id": f"discover_{endpoint.replace('/', '_')}_{int(time.time())}",
399
+ "method": endpoint,
400
+ "params": {}
401
+ }
402
+
403
+ headers = {
404
+ "Content-Type": "application/json",
405
+ "Accept": "application/json, text/event-stream"
406
+ }
407
+ if connection_config.headers:
408
+ headers.update(connection_config.headers)
409
+
410
+ try:
411
+ logger.debug(f"Sending MCP {endpoint} request to {connection_config.url}")
412
+ response = requests.post(
413
+ connection_config.url,
414
+ json=mcp_request,
415
+ headers=headers,
416
+ timeout=timeout
417
+ )
418
+
419
+ if response.status_code != 200:
420
+ logger.error(f"MCP server returned non-200 status: {response.status_code}")
421
+ raise Exception(f"HTTP {response.status_code}: {response.text}")
422
+
423
+ # Check content type and parse accordingly
424
+ content_type = response.headers.get('Content-Type', '')
425
+
426
+ if 'text/event-stream' in content_type:
427
+ # Parse SSE (Server-Sent Events) format
428
+ data = cls._parse_sse_response(response.text)
429
+ elif 'application/json' in content_type:
430
+ # Parse regular JSON
431
+ try:
432
+ data = response.json()
433
+ except ValueError as json_err:
434
+ raise Exception(f"Invalid JSON response: {json_err}. Response text: {response.text[:200]}")
435
+ else:
436
+ raise Exception(f"Unexpected Content-Type: {content_type}. Response: {response.text[:200]}")
437
+
438
+ if "error" in data:
439
+ raise Exception(f"MCP Error: {data['error']}")
440
+
441
+ # Parse MCP response - different endpoints return different keys
442
+ result = data.get("result", {})
443
+ if endpoint == "tools/list":
444
+ return result.get("tools", [])
445
+ elif endpoint == "prompts/list":
446
+ return result.get("prompts", [])
447
+ else:
448
+ return result.get("items", [])
449
+
450
+ except Exception as e:
451
+ logger.error(f"Failed to discover from {endpoint} on MCP toolkit '{toolkit_name}': {e}")
452
+ raise
453
+
454
+ @staticmethod
455
+ def _parse_sse_response(sse_text: str) -> Dict[str, Any]:
456
+ """
457
+ Parse Server-Sent Events (SSE) format response.
458
+ SSE format: event: message\ndata: {json}\n\n
459
+ """
460
+ import json
461
+
462
+ lines = sse_text.strip().split('\n')
463
+ data_line = None
464
+
465
+ for line in lines:
466
+ if line.startswith('data:'):
467
+ data_line = line[5:].strip() # Remove 'data:' prefix
468
+ break
469
+
470
+ if not data_line:
471
+ raise Exception(f"No data found in SSE response: {sse_text[:200]}")
472
+
473
+ try:
474
+ return json.loads(data_line)
475
+ except json.JSONDecodeError as e:
476
+ raise Exception(f"Failed to parse SSE data as JSON: {e}. Data: {data_line[:200]}")
477
+
478
+ @classmethod
479
+ def _create_tool_from_dict(
480
+ cls,
481
+ tool_dict: Dict[str, Any],
482
+ toolkit_name: str,
483
+ timeout: int,
484
+ client
485
+ ) -> Optional[BaseTool]:
486
+ """Create a BaseTool from a tool/prompt dictionary (from direct HTTP discovery)."""
487
+ try:
488
+ # Store toolkit_max_length in local variable to avoid contextual access issues
489
+ max_length_value = cls.toolkit_max_length
490
+
491
+ # Clean toolkit name for prefixing
492
+ clean_prefix = clean_string(toolkit_name, max_length_value)
493
+
494
+ full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}{tool_dict.get("name", "unknown")}'
495
+
496
+ # Check if this is a prompt (converted to tool)
497
+ is_prompt = tool_dict.get("_mcp_type") == "prompt"
498
+ item_type = "prompt" if is_prompt else "tool"
499
+
500
+ return McpServerTool(
501
+ name=full_tool_name,
502
+ description=f"MCP {item_type} '{tool_dict.get('name')}' from toolkit '{toolkit_name}': {tool_dict.get('description', '')}",
503
+ args_schema=McpServerTool.create_pydantic_model_from_schema(
504
+ tool_dict.get("inputSchema", {})
505
+ ),
506
+ client=client,
507
+ server=toolkit_name,
508
+ tool_timeout_sec=timeout,
509
+ is_prompt=is_prompt,
510
+ prompt_name=tool_dict.get("_mcp_prompt_name") if is_prompt else None
511
+ )
512
+ except Exception as e:
513
+ logger.error(f"Failed to create MCP tool '{tool_dict.get('name')}' from toolkit '{toolkit_name}': {e}")
514
+ return None
515
+
516
+ @classmethod
517
+ def _create_tools_static(
518
+ cls,
519
+ toolkit_name: str,
520
+ selected_tools: List[str],
521
+ timeout: int,
522
+ client
523
+ ) -> List[BaseTool]:
524
+ """Fallback static tool creation using the original method."""
525
+ tools = []
526
+
527
+ if not client or not hasattr(client, 'get_mcp_toolkits'):
528
+ logger.warning("Alita client does not support MCP toolkit discovery")
529
+ return tools
530
+
531
+ try:
532
+ all_toolkits = client.get_mcp_toolkits()
533
+ server_toolkit = next((tk for tk in all_toolkits if tk.get('name') == toolkit_name), None)
534
+
535
+ if not server_toolkit:
536
+ logger.warning(f"MCP toolkit '{toolkit_name}' not found in available toolkits")
537
+ return tools
538
+
539
+ # Extract tools from the toolkit
540
+ available_tools = server_toolkit.get('tools', [])
541
+ selected_tools_lower = [tool.lower() for tool in selected_tools] if selected_tools else []
542
+
543
+ for available_tool in available_tools:
544
+ tool_name = available_tool.get("name", "").lower()
545
+
546
+ # Filter tools if specific tools are selected
547
+ if selected_tools_lower and tool_name not in selected_tools_lower:
548
+ continue
549
+
550
+ # Create the tool
551
+ server_tool = cls._create_single_tool(
552
+ toolkit_name=toolkit_name,
553
+ available_tool=available_tool,
554
+ timeout=timeout,
555
+ client=client
556
+ )
557
+
558
+ if server_tool:
559
+ tools.append(server_tool)
560
+
561
+ logger.info(f"Successfully created {len(tools)} MCP tools from toolkit '{toolkit_name}' using static mode")
562
+
563
+ except Exception as e:
564
+ logger.error(f"Error in static tool creation: {e}")
565
+
566
+ # Always add the inspection tool (not subject to selected_tools filtering)
567
+ # For static mode, we need to create a basic connection config from the server info
568
+ try:
569
+ # We don't have full connection config in static mode, so create a basic one
570
+ # The inspection tool will work as long as the server is accessible
571
+ inspection_tool = McpInspectTool(
572
+ name=f"{clean_string(toolkit_name, 50)}{TOOLKIT_SPLITTER}mcp_inspect",
573
+ server_name=toolkit_name,
574
+ server_url="", # Will be populated by the client if available
575
+ description=f"Inspect available tools, prompts, and resources from MCP toolkit '{toolkit_name}'"
576
+ )
577
+ tools.append(inspection_tool)
578
+ logger.info(f"Added MCP inspection tool for toolkit '{toolkit_name}' (static mode)")
579
+ except Exception as e:
580
+ logger.warning(f"Failed to create inspection tool for {toolkit_name}: {e}")
581
+
582
+ return tools
583
+
584
+ @classmethod
585
+ def _create_tool_from_metadata(
586
+ cls,
587
+ tool_metadata,
588
+ toolkit_name: str,
589
+ timeout: int,
590
+ client
591
+ ) -> Optional[BaseTool]:
592
+ """Create a BaseTool from discovered metadata."""
593
+ try:
594
+ # Store toolkit_max_length in local variable to avoid contextual access issues
595
+ max_length_value = cls.toolkit_max_length
596
+
597
+ # Clean server name for prefixing (use tool_metadata.server instead of toolkit_name)
598
+ clean_prefix = clean_string(tool_metadata.server, max_length_value)
599
+ full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}{tool_metadata.name}'
600
+
601
+ return McpServerTool(
602
+ name=full_tool_name,
603
+ description=f"MCP tool '{tool_metadata.name}' from server '{tool_metadata.server}': {tool_metadata.description}",
604
+ args_schema=McpServerTool.create_pydantic_model_from_schema(tool_metadata.input_schema),
605
+ client=client,
606
+ server=tool_metadata.server,
607
+ tool_timeout_sec=timeout
608
+ )
609
+ except Exception as e:
610
+ logger.error(f"Failed to create MCP tool '{tool_metadata.name}' from server '{tool_metadata.server}': {e}")
611
+ return None
612
+
613
+ @classmethod
614
+ def _create_single_tool(
615
+ cls,
616
+ toolkit_name: str,
617
+ available_tool: Dict[str, Any],
618
+ timeout: int,
619
+ client
620
+ ) -> Optional[BaseTool]:
621
+ """Create a single MCP tool."""
622
+ try:
623
+ # Store toolkit_max_length in local variable to avoid contextual access issues
624
+ max_length_value = cls.toolkit_max_length
625
+
626
+ # Clean toolkit name for prefixing
627
+ clean_prefix = clean_string(toolkit_name, max_length_value)
628
+
629
+ full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}{available_tool["name"]}'
630
+
631
+ return McpServerTool(
632
+ name=full_tool_name,
633
+ description=f"MCP tool '{available_tool['name']}' from toolkit '{toolkit_name}': {available_tool.get('description', '')}",
634
+ args_schema=McpServerTool.create_pydantic_model_from_schema(
635
+ available_tool.get("inputSchema", {})
636
+ ),
637
+ client=client,
638
+ server=toolkit_name,
639
+ tool_timeout_sec=timeout
640
+ )
641
+ except Exception as e:
642
+ logger.error(f"Failed to create MCP tool '{available_tool.get('name')}' from toolkit '{toolkit_name}': {e}")
643
+ return None
644
+
645
+ @classmethod
646
+ def _create_inspection_tool(
647
+ cls,
648
+ toolkit_name: str,
649
+ connection_config: McpConnectionConfig
650
+ ) -> Optional[BaseTool]:
651
+ """Create the inspection tool for the MCP toolkit."""
652
+ try:
653
+ # Store toolkit_max_length in local variable to avoid contextual access issues
654
+ max_length_value = cls.toolkit_max_length
655
+
656
+ # Clean toolkit name for prefixing
657
+ clean_prefix = clean_string(toolkit_name, max_length_value)
658
+
659
+ full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}mcp_inspect'
660
+
661
+ return McpInspectTool(
662
+ name=full_tool_name,
663
+ server_name=toolkit_name,
664
+ server_url=connection_config.url,
665
+ server_headers=connection_config.headers,
666
+ description=f"Inspect available tools, prompts, and resources from MCP toolkit '{toolkit_name}'"
667
+ )
668
+ except Exception as e:
669
+ logger.error(f"Failed to create MCP inspection tool for toolkit '{toolkit_name}': {e}")
670
+ return None
671
+
672
+ def get_tools(self) -> List[BaseTool]:
673
+ """Get the list of tools provided by this toolkit."""
674
+ logger.info(f"MCP toolkit '{self.toolkit_name}' returning {len(self.tools)} tools")
675
+ if len(self.tools) > 0:
676
+ tool_names = [t.name if hasattr(t, 'name') else str(t) for t in self.tools]
677
+ logger.info(f"MCP toolkit '{self.toolkit_name}' tools: {tool_names}")
678
+ return self.tools
679
+
680
+ async def refresh_tools(self):
681
+ """Manually refresh tools from the MCP toolkit."""
682
+ if not self.toolkit_name:
683
+ logger.warning("Cannot refresh tools: toolkit_name not set")
684
+ return
685
+
686
+ try:
687
+ from ..clients.mcp_manager import get_mcp_manager
688
+ manager = get_mcp_manager()
689
+ await manager.refresh_server(self.toolkit_name)
690
+ logger.info(f"Successfully refreshed tools for toolkit {self.toolkit_name}")
691
+ except Exception as e:
692
+ logger.error(f"Failed to refresh tools for toolkit {self.toolkit_name}: {e}")
693
+
694
+ async def get_server_health(self) -> Dict[str, Any]:
695
+ """Get health status of the configured MCP toolkit."""
696
+ if not self.toolkit_name:
697
+ return {"status": "not_configured"}
698
+
699
+ try:
700
+ from ..clients.mcp_manager import get_mcp_manager
701
+ manager = get_mcp_manager()
702
+ health_info = await manager.get_server_health(self.toolkit_name)
703
+ return health_info
704
+ except Exception as e:
705
+ logger.error(f"Failed to get server health for {self.toolkit_name}: {e}")
706
+ return {"status": "error", "error": str(e)}
707
+
708
+
709
+ def get_tools(tool_config: dict, alita_client, llm=None, memory_store=None) -> List[BaseTool]:
710
+ """
711
+ Create MCP tools from configuration.
712
+ This function is called by the main tool loading system.
713
+
714
+ Args:
715
+ tool_config: Tool configuration dictionary
716
+ alita_client: Alita client instance
717
+ llm: Language model (not used by MCP tools)
718
+ memory_store: Memory store (not used by MCP tools)
719
+
720
+ Returns:
721
+ List of configured MCP tools
722
+ """
723
+ settings = tool_config.get('settings', {})
724
+ toolkit_name = tool_config.get('toolkit_name')
725
+
726
+ # Extract required fields
727
+ url = settings.get('url')
728
+ headers = settings.get('headers')
729
+
730
+ if not toolkit_name:
731
+ logger.error("MCP toolkit configuration missing required 'toolkit_name'")
732
+ return []
733
+
734
+ if not url:
735
+ logger.error("MCP toolkit configuration missing required 'url'")
736
+ return []
737
+
738
+ # Type conversion for numeric settings that may come as strings from config
739
+ def safe_int(value, default):
740
+ """Convert value to int, handling string inputs."""
741
+ if value is None:
742
+ return default
743
+ try:
744
+ return int(value)
745
+ except (ValueError, TypeError):
746
+ logger.warning(f"Invalid integer value '{value}', using default {default}")
747
+ return default
748
+
749
+ return McpToolkit.get_toolkit(
750
+ url=url,
751
+ headers=headers,
752
+ timeout=safe_int(settings.get('timeout'), 60),
753
+ discovery_mode=settings.get('discovery_mode', 'dynamic'),
754
+ discovery_interval=safe_int(settings.get('discovery_interval'), 300),
755
+ selected_tools=settings.get('selected_tools', []),
756
+ enable_caching=settings.get('enable_caching', True),
757
+ cache_ttl=safe_int(settings.get('cache_ttl'), 300),
758
+ toolkit_name=toolkit_name,
759
+ client=alita_client
760
+ ).get_tools()
761
+
762
+
763
+ # Utility functions for managing MCP discovery
764
+ async def start_global_discovery():
765
+ """Start the global MCP discovery service."""
766
+ from ..clients.mcp_discovery import init_discovery_service
767
+ await init_discovery_service()
768
+
769
+
770
+ async def stop_global_discovery():
771
+ """Stop the global MCP discovery service."""
772
+ from ..clients.mcp_discovery import shutdown_discovery_service
773
+ await shutdown_discovery_service()
774
+
775
+
776
+ async def register_mcp_server_for_discovery(toolkit_name: str, connection_config):
777
+ """Register an MCP server for global discovery."""
778
+ from ..clients.mcp_discovery import get_discovery_service
779
+ service = get_discovery_service()
780
+ await service.register_server(toolkit_name, connection_config)
781
+
782
+
783
+ def get_all_discovered_servers():
784
+ """Get status of all discovered servers."""
785
+ from ..clients.mcp_discovery import get_discovery_service
786
+ service = get_discovery_service()
787
+ return service.get_server_health()