alita-sdk 0.3.435__py3-none-any.whl → 0.3.457__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 (54) hide show
  1. alita_sdk/runtime/clients/client.py +39 -7
  2. alita_sdk/runtime/langchain/assistant.py +10 -2
  3. alita_sdk/runtime/langchain/langraph_agent.py +57 -15
  4. alita_sdk/runtime/langchain/utils.py +19 -3
  5. alita_sdk/runtime/models/mcp_models.py +4 -0
  6. alita_sdk/runtime/toolkits/artifact.py +5 -6
  7. alita_sdk/runtime/toolkits/mcp.py +258 -150
  8. alita_sdk/runtime/toolkits/tools.py +44 -2
  9. alita_sdk/runtime/tools/function.py +2 -1
  10. alita_sdk/runtime/tools/mcp_remote_tool.py +166 -0
  11. alita_sdk/runtime/tools/mcp_server_tool.py +9 -76
  12. alita_sdk/runtime/tools/vectorstore_base.py +17 -2
  13. alita_sdk/runtime/utils/mcp_oauth.py +164 -0
  14. alita_sdk/runtime/utils/mcp_sse_client.py +405 -0
  15. alita_sdk/runtime/utils/toolkit_utils.py +9 -2
  16. alita_sdk/tools/ado/repos/__init__.py +1 -0
  17. alita_sdk/tools/ado/test_plan/__init__.py +1 -1
  18. alita_sdk/tools/ado/wiki/__init__.py +1 -5
  19. alita_sdk/tools/ado/work_item/__init__.py +1 -5
  20. alita_sdk/tools/base_indexer_toolkit.py +10 -6
  21. alita_sdk/tools/bitbucket/__init__.py +1 -0
  22. alita_sdk/tools/code/sonar/__init__.py +1 -1
  23. alita_sdk/tools/confluence/__init__.py +2 -2
  24. alita_sdk/tools/github/__init__.py +2 -2
  25. alita_sdk/tools/gitlab/__init__.py +2 -1
  26. alita_sdk/tools/gitlab_org/__init__.py +1 -2
  27. alita_sdk/tools/google_places/__init__.py +2 -1
  28. alita_sdk/tools/jira/__init__.py +1 -0
  29. alita_sdk/tools/memory/__init__.py +1 -1
  30. alita_sdk/tools/pandas/__init__.py +1 -1
  31. alita_sdk/tools/postman/__init__.py +2 -1
  32. alita_sdk/tools/pptx/__init__.py +2 -2
  33. alita_sdk/tools/qtest/__init__.py +3 -3
  34. alita_sdk/tools/qtest/api_wrapper.py +374 -29
  35. alita_sdk/tools/rally/__init__.py +1 -2
  36. alita_sdk/tools/report_portal/__init__.py +1 -0
  37. alita_sdk/tools/salesforce/__init__.py +1 -0
  38. alita_sdk/tools/servicenow/__init__.py +2 -3
  39. alita_sdk/tools/sharepoint/__init__.py +1 -0
  40. alita_sdk/tools/slack/__init__.py +1 -0
  41. alita_sdk/tools/sql/__init__.py +2 -1
  42. alita_sdk/tools/testio/__init__.py +1 -0
  43. alita_sdk/tools/testrail/__init__.py +1 -3
  44. alita_sdk/tools/xray/__init__.py +2 -1
  45. alita_sdk/tools/zephyr/__init__.py +2 -1
  46. alita_sdk/tools/zephyr_enterprise/__init__.py +1 -0
  47. alita_sdk/tools/zephyr_essential/__init__.py +1 -0
  48. alita_sdk/tools/zephyr_scale/__init__.py +1 -0
  49. alita_sdk/tools/zephyr_squad/__init__.py +1 -0
  50. {alita_sdk-0.3.435.dist-info → alita_sdk-0.3.457.dist-info}/METADATA +2 -1
  51. {alita_sdk-0.3.435.dist-info → alita_sdk-0.3.457.dist-info}/RECORD +54 -51
  52. {alita_sdk-0.3.435.dist-info → alita_sdk-0.3.457.dist-info}/WHEEL +0 -0
  53. {alita_sdk-0.3.435.dist-info → alita_sdk-0.3.457.dist-info}/licenses/LICENSE +0 -0
  54. {alita_sdk-0.3.435.dist-info → alita_sdk-0.3.457.dist-info}/top_level.txt +0 -0
@@ -5,21 +5,145 @@ Following MCP specification: https://modelcontextprotocol.io/specification/2025-
5
5
  """
6
6
 
7
7
  import logging
8
+ import re
8
9
  import requests
9
- from typing import List, Optional, Any, Dict, Literal, ClassVar
10
+ import asyncio
11
+ from typing import List, Optional, Any, Dict, Literal, ClassVar, Union
10
12
 
11
13
  from langchain_core.tools import BaseToolkit, BaseTool
12
14
  from pydantic import BaseModel, ConfigDict, Field
13
15
 
14
16
  from ..tools.mcp_server_tool import McpServerTool
17
+ from ..tools.mcp_remote_tool import McpRemoteTool
15
18
  from ..tools.mcp_inspect_tool import McpInspectTool
16
19
  from ...tools.utils import TOOLKIT_SPLITTER, clean_string
17
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
+ )
18
29
 
19
30
  logger = logging.getLogger(__name__)
20
31
 
21
32
  name = "mcp"
22
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
23
147
 
24
148
  class McpToolkit(BaseToolkit):
25
149
  """
@@ -85,11 +209,10 @@ class McpToolkit(BaseToolkit):
85
209
  )
86
210
  ),
87
211
  timeout=(
88
- int,
212
+ Union[int, str], # TODO: remove one I will figure out why UI sends str
89
213
  Field(
90
- default=60,
91
- ge=1, le=300,
92
- description="Request timeout in seconds"
214
+ default=300,
215
+ description="Request timeout in seconds (1-3600)"
93
216
  )
94
217
  ),
95
218
  discovery_mode=(
@@ -103,11 +226,10 @@ class McpToolkit(BaseToolkit):
103
226
  )
104
227
  ),
105
228
  discovery_interval=(
106
- int,
229
+ Union[int, str],
107
230
  Field(
108
231
  default=300,
109
- ge=60, le=3600,
110
- description="Discovery interval in seconds (for periodic discovery)"
232
+ description="Discovery interval in seconds (60-3600, for periodic discovery)"
111
233
  )
112
234
  ),
113
235
  selected_tools=(
@@ -128,11 +250,10 @@ class McpToolkit(BaseToolkit):
128
250
  )
129
251
  ),
130
252
  cache_ttl=(
131
- int,
253
+ Union[int, str],
132
254
  Field(
133
255
  default=300,
134
- ge=60, le=3600,
135
- description="Cache TTL in seconds"
256
+ description="Cache TTL in seconds (60-3600)"
136
257
  )
137
258
  ),
138
259
  __config__=ConfigDict(
@@ -194,6 +315,11 @@ class McpToolkit(BaseToolkit):
194
315
  if not toolkit_name:
195
316
  raise ValueError("toolkit_name is required")
196
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
+
197
323
  logger.info(f"Creating MCP toolkit: {toolkit_name}")
198
324
 
199
325
  # Parse headers if they're provided as a JSON string
@@ -216,9 +342,14 @@ class McpToolkit(BaseToolkit):
216
342
  logger.error(f"Headers must be a dictionary or JSON string, got: {type(headers)}")
217
343
  raise ValueError(f"Headers must be a dictionary or JSON string, got: {type(headers)}")
218
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
+
219
350
  # Create MCP connection configuration
220
351
  try:
221
- connection_config = McpConnectionConfig(url=url, headers=parsed_headers)
352
+ connection_config = McpConnectionConfig(url=url, headers=parsed_headers, session_id=session_id)
222
353
  except Exception as e:
223
354
  logger.error(f"Invalid MCP connection configuration: {e}")
224
355
  raise ValueError(f"Invalid MCP connection configuration: {e}")
@@ -258,7 +389,7 @@ class McpToolkit(BaseToolkit):
258
389
  logger.info(f"Discovering tools from MCP toolkit '{toolkit_name}' at {connection_config.url}")
259
390
 
260
391
  # Use synchronous HTTP discovery for toolkit initialization
261
- tool_metadata_list = cls._discover_tools_sync(
392
+ tool_metadata_list, session_id = cls._discover_tools_sync(
262
393
  toolkit_name=toolkit_name,
263
394
  connection_config=connection_config,
264
395
  timeout=timeout
@@ -273,12 +404,18 @@ class McpToolkit(BaseToolkit):
273
404
  ]
274
405
 
275
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
+
276
411
  for tool_metadata in tool_metadata_list:
277
412
  server_tool = cls._create_tool_from_dict(
278
413
  tool_dict=tool_metadata,
279
414
  toolkit_name=toolkit_name,
415
+ connection_config=connection_config,
280
416
  timeout=timeout,
281
- client=client
417
+ client=client,
418
+ session_id=session_id # Use session from discovery
282
419
  )
283
420
 
284
421
  if server_tool:
@@ -291,6 +428,9 @@ class McpToolkit(BaseToolkit):
291
428
  logger.error(f"Discovery error details - URL: {connection_config.url}, Timeout: {timeout}s")
292
429
 
293
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
294
434
  if client and discovery_mode != "static":
295
435
  logger.info(f"Falling back to static discovery for toolkit '{toolkit_name}'")
296
436
  tools = cls._create_tools_static(toolkit_name, selected_tools, timeout, client)
@@ -321,32 +461,79 @@ class McpToolkit(BaseToolkit):
321
461
  timeout: int
322
462
  ) -> List[Dict[str, Any]]:
323
463
  """
324
- Synchronously discover tools and prompts from MCP server using HTTP requests.
464
+ Discover tools and prompts from MCP server using SSE client.
325
465
  Returns list of tool/prompt dictionaries with name, description, and inputSchema.
326
466
  Prompts are converted to tools that can be invoked.
327
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
+ """
328
498
  all_tools = []
499
+ session_id = connection_config.session_id
329
500
 
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,
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,
335
520
  timeout=timeout
336
521
  )
337
- all_tools.extend(tools_data)
338
- logger.info(f"Discovered {len(tools_data)} tools from MCP toolkit '{toolkit_name}'")
339
522
 
340
- # Discover prompts and convert them to tools
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
341
533
  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
- )
534
+ prompts = await client.list_prompts()
348
535
  # Convert prompts to tool format
349
- for prompt in prompts_data:
536
+ for prompt in prompts:
350
537
  prompt_tool = {
351
538
  "name": f"prompt_{prompt.get('name', 'unnamed')}",
352
539
  "description": prompt.get('description', f"Execute prompt: {prompt.get('name')}"),
@@ -371,117 +558,22 @@ class McpToolkit(BaseToolkit):
371
558
  "_mcp_prompt_name": prompt.get('name')
372
559
  }
373
560
  all_tools.append(prompt_tool)
374
- logger.info(f"Discovered {len(prompts_data)} prompts from MCP toolkit '{toolkit_name}'")
561
+ logger.info(f"[MCP SSE] Discovered {len(prompts)} prompts from '{toolkit_name}'")
375
562
  except Exception as e:
376
- logger.warning(f"Failed to discover prompts from MCP toolkit '{toolkit_name}': {e}")
563
+ logger.warning(f"[MCP SSE] Failed to discover prompts: {e}")
377
564
 
378
- logger.info(f"Total discovered {len(all_tools)} tools+prompts from MCP toolkit '{toolkit_name}'")
565
+ logger.info(f"[MCP SSE] Total discovered {len(all_tools)} items from '{toolkit_name}'")
379
566
  return all_tools
380
567
 
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
568
  @classmethod
479
569
  def _create_tool_from_dict(
480
570
  cls,
481
571
  tool_dict: Dict[str, Any],
482
572
  toolkit_name: str,
573
+ connection_config: McpConnectionConfig,
483
574
  timeout: int,
484
- client
575
+ client,
576
+ session_id: Optional[str] = None
485
577
  ) -> Optional[BaseTool]:
486
578
  """Create a BaseTool from a tool/prompt dictionary (from direct HTTP discovery)."""
487
579
  try:
@@ -491,23 +583,35 @@ class McpToolkit(BaseToolkit):
491
583
  # Clean toolkit name for prefixing
492
584
  clean_prefix = clean_string(toolkit_name, max_length_value)
493
585
 
494
- full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}{tool_dict.get("name", "unknown")}'
586
+ # Optimize tool name to fit within 64 character limit
587
+ full_tool_name = optimize_tool_name(clean_prefix, tool_dict.get("name", "unknown"))
495
588
 
496
589
  # Check if this is a prompt (converted to tool)
497
590
  is_prompt = tool_dict.get("_mcp_type") == "prompt"
498
591
  item_type = "prompt" if is_prompt else "tool"
499
592
 
500
- return McpServerTool(
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(
501
601
  name=full_tool_name,
502
- description=f"MCP {item_type} '{tool_dict.get('name')}' from toolkit '{toolkit_name}': {tool_dict.get('description', '')}",
602
+ description=description,
503
603
  args_schema=McpServerTool.create_pydantic_model_from_schema(
504
604
  tool_dict.get("inputSchema", {})
505
605
  ),
506
606
  client=client,
507
607
  server=toolkit_name,
608
+ server_url=connection_config.url,
609
+ server_headers=connection_config.headers,
508
610
  tool_timeout_sec=timeout,
509
611
  is_prompt=is_prompt,
510
- prompt_name=tool_dict.get("_mcp_prompt_name") if is_prompt else None
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
511
615
  )
512
616
  except Exception as e:
513
617
  logger.error(f"Failed to create MCP tool '{tool_dict.get('name')}' from toolkit '{toolkit_name}': {e}")
@@ -596,11 +700,18 @@ class McpToolkit(BaseToolkit):
596
700
 
597
701
  # Clean server name for prefixing (use tool_metadata.server instead of toolkit_name)
598
702
  clean_prefix = clean_string(tool_metadata.server, max_length_value)
599
- full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}{tool_metadata.name}'
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")
600
711
 
601
712
  return McpServerTool(
602
713
  name=full_tool_name,
603
- description=f"MCP tool '{tool_metadata.name}' from server '{tool_metadata.server}': {tool_metadata.description}",
714
+ description=description,
604
715
  args_schema=McpServerTool.create_pydantic_model_from_schema(tool_metadata.input_schema),
605
716
  client=client,
606
717
  server=tool_metadata.server,
@@ -626,11 +737,18 @@ class McpToolkit(BaseToolkit):
626
737
  # Clean toolkit name for prefixing
627
738
  clean_prefix = clean_string(toolkit_name, max_length_value)
628
739
 
629
- full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}{available_tool["name"]}'
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")
630
748
 
631
749
  return McpServerTool(
632
750
  name=full_tool_name,
633
- description=f"MCP tool '{available_tool['name']}' from toolkit '{toolkit_name}': {available_tool.get('description', '')}",
751
+ description=description,
634
752
  args_schema=McpServerTool.create_pydantic_model_from_schema(
635
753
  available_tool.get("inputSchema", {})
636
754
  ),
@@ -736,16 +854,6 @@ def get_tools(tool_config: dict, alita_client, llm=None, memory_store=None) -> L
736
854
  return []
737
855
 
738
856
  # 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
857
  return McpToolkit.get_toolkit(
750
858
  url=url,
751
859
  headers=headers,
@@ -784,4 +892,4 @@ def get_all_discovered_servers():
784
892
  """Get status of all discovered servers."""
785
893
  from ..clients.mcp_discovery import get_discovery_service
786
894
  service = get_discovery_service()
787
- return service.get_server_health()
895
+ return service.get_server_health()
@@ -19,6 +19,7 @@ from ..tools.image_generation import ImageGenerationToolkit
19
19
  # Import community tools
20
20
  from ...community import get_toolkits as community_toolkits, get_tools as community_tools
21
21
  from ...tools.memory import MemoryToolkit
22
+ from ..utils.mcp_oauth import canonical_resource, McpAuthorizationRequired
22
23
  from ...tools.utils import TOOLKIT_SPLITTER
23
24
 
24
25
  logger = logging.getLogger(__name__)
@@ -37,7 +38,7 @@ def get_toolkits():
37
38
  return core_toolkits + community_toolkits() + alita_toolkits()
38
39
 
39
40
 
40
- def get_tools(tools_list: list, alita_client, llm, memory_store: BaseStore = None, debug_mode: Optional[bool] = False) -> list:
41
+ def get_tools(tools_list: list, alita_client, llm, memory_store: BaseStore = None, debug_mode: Optional[bool] = False, mcp_tokens: Optional[dict] = None) -> list:
41
42
  prompts = []
42
43
  tools = []
43
44
 
@@ -109,11 +110,52 @@ def get_tools(tools_list: list, alita_client, llm, memory_store: BaseStore = Non
109
110
  toolkit_name=tool.get('toolkit_name', ''),
110
111
  **tool['settings']).get_tools())
111
112
  elif tool['type'] == 'mcp':
113
+ # remote mcp tool initialization with token injection
114
+ settings = dict(tool['settings'])
115
+ url = settings.get('url')
116
+ headers = settings.get('headers')
117
+ token_data = None
118
+ session_id = None
119
+ if mcp_tokens and url:
120
+ canonical_url = canonical_resource(url)
121
+ logger.info(f"[MCP Auth] Looking for token for URL: {url}")
122
+ logger.info(f"[MCP Auth] Canonical URL: {canonical_url}")
123
+ logger.info(f"[MCP Auth] Available tokens: {list(mcp_tokens.keys())}")
124
+ token_data = mcp_tokens.get(canonical_url)
125
+ if token_data:
126
+ logger.info(f"[MCP Auth] Found token data for {canonical_url}")
127
+ # Handle both old format (string) and new format (dict with access_token and session_id)
128
+ if isinstance(token_data, dict):
129
+ access_token = token_data.get('access_token')
130
+ session_id = token_data.get('session_id')
131
+ logger.info(f"[MCP Auth] Token data: access_token={'present' if access_token else 'missing'}, session_id={session_id or 'none'}")
132
+ else:
133
+ # Backward compatibility: treat as plain token string
134
+ access_token = token_data
135
+ logger.info(f"[MCP Auth] Using legacy token format (string)")
136
+ else:
137
+ access_token = None
138
+ logger.warning(f"[MCP Auth] No token found for {canonical_url}")
139
+ else:
140
+ access_token = None
141
+
142
+ if access_token:
143
+ merged_headers = dict(headers) if headers else {}
144
+ merged_headers.setdefault('Authorization', f'Bearer {access_token}')
145
+ settings['headers'] = merged_headers
146
+ logger.info(f"[MCP Auth] Added Authorization header for {url}")
147
+
148
+ # Pass session_id to MCP toolkit if available
149
+ if session_id:
150
+ settings['session_id'] = session_id
151
+ logger.info(f"[MCP Auth] Passing session_id to toolkit: {session_id}")
112
152
  tools.extend(McpToolkit.get_toolkit(
113
153
  toolkit_name=tool.get('toolkit_name', ''),
114
154
  client=alita_client,
115
- **tool['settings']).get_tools())
155
+ **settings).get_tools())
116
156
  except Exception as e:
157
+ if isinstance(e, McpAuthorizationRequired):
158
+ raise
117
159
  logger.error(f"Error initializing toolkit for tool '{tool.get('name', 'unknown')}': {e}", exc_info=True)
118
160
  if debug_mode:
119
161
  logger.info("Skipping tool initialization error due to debug mode.")
@@ -120,7 +120,8 @@ class FunctionTool(BaseTool):
120
120
  messages_dict = {
121
121
  "messages": [{
122
122
  "role": "assistant",
123
- "content": dumps(tool_result) if not isinstance(tool_result, ToolException)
123
+ "content": dumps(tool_result)
124
+ if not isinstance(tool_result, ToolException) and not isinstance(tool_result, str)
124
125
  else str(tool_result)
125
126
  }]
126
127
  }