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

@@ -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,76 @@ 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
+ if not session_id:
502
+ logger.error(f"[MCP SSE] session_id is required for SSE servers")
503
+ raise ValueError("session_id is required. Frontend must generate UUID.")
504
+
505
+ logger.info(f"[MCP SSE] Discovering from {connection_config.url} with session {session_id}")
506
+
507
+ # Prepare headers
508
+ headers = {}
509
+ if connection_config.headers:
510
+ headers.update(connection_config.headers)
511
+
512
+ # Create SSE client
513
+ client = McpSseClient(
514
+ url=connection_config.url,
515
+ session_id=session_id,
516
+ headers=headers,
335
517
  timeout=timeout
336
518
  )
337
- all_tools.extend(tools_data)
338
- logger.info(f"Discovered {len(tools_data)} tools from MCP toolkit '{toolkit_name}'")
339
519
 
340
- # Discover prompts and convert them to tools
520
+ # Initialize MCP session
521
+ await client.initialize()
522
+ logger.info(f"[MCP SSE] Session initialized for '{toolkit_name}'")
523
+
524
+ # Discover tools
525
+ tools = await client.list_tools()
526
+ all_tools.extend(tools)
527
+ logger.info(f"[MCP SSE] Discovered {len(tools)} tools from '{toolkit_name}'")
528
+
529
+ # Discover prompts
341
530
  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
- )
531
+ prompts = await client.list_prompts()
348
532
  # Convert prompts to tool format
349
- for prompt in prompts_data:
533
+ for prompt in prompts:
350
534
  prompt_tool = {
351
535
  "name": f"prompt_{prompt.get('name', 'unnamed')}",
352
536
  "description": prompt.get('description', f"Execute prompt: {prompt.get('name')}"),
@@ -371,117 +555,22 @@ class McpToolkit(BaseToolkit):
371
555
  "_mcp_prompt_name": prompt.get('name')
372
556
  }
373
557
  all_tools.append(prompt_tool)
374
- logger.info(f"Discovered {len(prompts_data)} prompts from MCP toolkit '{toolkit_name}'")
558
+ logger.info(f"[MCP SSE] Discovered {len(prompts)} prompts from '{toolkit_name}'")
375
559
  except Exception as e:
376
- logger.warning(f"Failed to discover prompts from MCP toolkit '{toolkit_name}': {e}")
560
+ logger.warning(f"[MCP SSE] Failed to discover prompts: {e}")
377
561
 
378
- logger.info(f"Total discovered {len(all_tools)} tools+prompts from MCP toolkit '{toolkit_name}'")
562
+ logger.info(f"[MCP SSE] Total discovered {len(all_tools)} items from '{toolkit_name}'")
379
563
  return all_tools
380
564
 
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
565
  @classmethod
479
566
  def _create_tool_from_dict(
480
567
  cls,
481
568
  tool_dict: Dict[str, Any],
482
569
  toolkit_name: str,
570
+ connection_config: McpConnectionConfig,
483
571
  timeout: int,
484
- client
572
+ client,
573
+ session_id: Optional[str] = None
485
574
  ) -> Optional[BaseTool]:
486
575
  """Create a BaseTool from a tool/prompt dictionary (from direct HTTP discovery)."""
487
576
  try:
@@ -491,23 +580,35 @@ class McpToolkit(BaseToolkit):
491
580
  # Clean toolkit name for prefixing
492
581
  clean_prefix = clean_string(toolkit_name, max_length_value)
493
582
 
494
- full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}{tool_dict.get("name", "unknown")}'
583
+ # Optimize tool name to fit within 64 character limit
584
+ full_tool_name = optimize_tool_name(clean_prefix, tool_dict.get("name", "unknown"))
495
585
 
496
586
  # Check if this is a prompt (converted to tool)
497
587
  is_prompt = tool_dict.get("_mcp_type") == "prompt"
498
588
  item_type = "prompt" if is_prompt else "tool"
499
589
 
500
- return McpServerTool(
590
+ # Build description and ensure it doesn't exceed 1000 characters
591
+ description = f"MCP {item_type} '{tool_dict.get('name')}' from toolkit '{toolkit_name}': {tool_dict.get('description', '')}"
592
+ if len(description) > 1000:
593
+ description = description[:997] + "..."
594
+ logger.debug(f"Trimmed description for tool '{tool_dict.get('name')}' from {len(description)} to 1000 chars")
595
+
596
+ # Use McpRemoteTool for remote MCP servers (HTTP/SSE)
597
+ return McpRemoteTool(
501
598
  name=full_tool_name,
502
- description=f"MCP {item_type} '{tool_dict.get('name')}' from toolkit '{toolkit_name}': {tool_dict.get('description', '')}",
599
+ description=description,
503
600
  args_schema=McpServerTool.create_pydantic_model_from_schema(
504
601
  tool_dict.get("inputSchema", {})
505
602
  ),
506
603
  client=client,
507
604
  server=toolkit_name,
605
+ server_url=connection_config.url,
606
+ server_headers=connection_config.headers,
508
607
  tool_timeout_sec=timeout,
509
608
  is_prompt=is_prompt,
510
- prompt_name=tool_dict.get("_mcp_prompt_name") if is_prompt else None
609
+ prompt_name=tool_dict.get("_mcp_prompt_name") if is_prompt else None,
610
+ original_tool_name=tool_dict.get('name'), # Store original name for MCP server invocation
611
+ session_id=session_id # Pass session ID for stateful SSE servers
511
612
  )
512
613
  except Exception as e:
513
614
  logger.error(f"Failed to create MCP tool '{tool_dict.get('name')}' from toolkit '{toolkit_name}': {e}")
@@ -596,11 +697,18 @@ class McpToolkit(BaseToolkit):
596
697
 
597
698
  # Clean server name for prefixing (use tool_metadata.server instead of toolkit_name)
598
699
  clean_prefix = clean_string(tool_metadata.server, max_length_value)
599
- full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}{tool_metadata.name}'
700
+ # Optimize tool name to fit within 64 character limit
701
+ full_tool_name = optimize_tool_name(clean_prefix, tool_metadata.name)
702
+
703
+ # Build description and ensure it doesn't exceed 1000 characters
704
+ description = f"MCP tool '{tool_metadata.name}' from server '{tool_metadata.server}': {tool_metadata.description}"
705
+ if len(description) > 1000:
706
+ description = description[:997] + "..."
707
+ logger.debug(f"Trimmed description for tool '{tool_metadata.name}' from {len(description)} to 1000 chars")
600
708
 
601
709
  return McpServerTool(
602
710
  name=full_tool_name,
603
- description=f"MCP tool '{tool_metadata.name}' from server '{tool_metadata.server}': {tool_metadata.description}",
711
+ description=description,
604
712
  args_schema=McpServerTool.create_pydantic_model_from_schema(tool_metadata.input_schema),
605
713
  client=client,
606
714
  server=tool_metadata.server,
@@ -626,11 +734,18 @@ class McpToolkit(BaseToolkit):
626
734
  # Clean toolkit name for prefixing
627
735
  clean_prefix = clean_string(toolkit_name, max_length_value)
628
736
 
629
- full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}{available_tool["name"]}'
737
+ # Optimize tool name to fit within 64 character limit
738
+ full_tool_name = optimize_tool_name(clean_prefix, available_tool["name"])
739
+
740
+ # Build description and ensure it doesn't exceed 1000 characters
741
+ description = f"MCP tool '{available_tool['name']}' from toolkit '{toolkit_name}': {available_tool.get('description', '')}"
742
+ if len(description) > 1000:
743
+ description = description[:997] + "..."
744
+ logger.debug(f"Trimmed description for tool '{available_tool['name']}' from {len(description)} to 1000 chars")
630
745
 
631
746
  return McpServerTool(
632
747
  name=full_tool_name,
633
- description=f"MCP tool '{available_tool['name']}' from toolkit '{toolkit_name}': {available_tool.get('description', '')}",
748
+ description=description,
634
749
  args_schema=McpServerTool.create_pydantic_model_from_schema(
635
750
  available_tool.get("inputSchema", {})
636
751
  ),
@@ -736,16 +851,6 @@ def get_tools(tool_config: dict, alita_client, llm=None, memory_store=None) -> L
736
851
  return []
737
852
 
738
853
  # 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
854
  return McpToolkit.get_toolkit(
750
855
  url=url,
751
856
  headers=headers,
@@ -784,4 +889,4 @@ def get_all_discovered_servers():
784
889
  """Get status of all discovered servers."""
785
890
  from ..clients.mcp_discovery import get_discovery_service
786
891
  service = get_discovery_service()
787
- return service.get_server_health()
892
+ 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,51 @@ 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
+ settings = dict(tool['settings'])
114
+ url = settings.get('url')
115
+ headers = settings.get('headers')
116
+ token_data = None
117
+ session_id = None
118
+ if mcp_tokens and url:
119
+ canonical_url = canonical_resource(url)
120
+ logger.info(f"[MCP Auth] Looking for token for URL: {url}")
121
+ logger.info(f"[MCP Auth] Canonical URL: {canonical_url}")
122
+ logger.info(f"[MCP Auth] Available tokens: {list(mcp_tokens.keys())}")
123
+ token_data = mcp_tokens.get(canonical_url)
124
+ if token_data:
125
+ logger.info(f"[MCP Auth] Found token data for {canonical_url}")
126
+ # Handle both old format (string) and new format (dict with access_token and session_id)
127
+ if isinstance(token_data, dict):
128
+ access_token = token_data.get('access_token')
129
+ session_id = token_data.get('session_id')
130
+ logger.info(f"[MCP Auth] Token data: access_token={'present' if access_token else 'missing'}, session_id={session_id or 'none'}")
131
+ else:
132
+ # Backward compatibility: treat as plain token string
133
+ access_token = token_data
134
+ logger.info(f"[MCP Auth] Using legacy token format (string)")
135
+ else:
136
+ access_token = None
137
+ logger.warning(f"[MCP Auth] No token found for {canonical_url}")
138
+ else:
139
+ access_token = None
140
+
141
+ if access_token:
142
+ merged_headers = dict(headers) if headers else {}
143
+ merged_headers.setdefault('Authorization', f'Bearer {access_token}')
144
+ settings['headers'] = merged_headers
145
+ logger.info(f"[MCP Auth] Added Authorization header for {url}")
146
+
147
+ # Pass session_id to MCP toolkit if available
148
+ if session_id:
149
+ settings['session_id'] = session_id
150
+ logger.info(f"[MCP Auth] Passing session_id to toolkit: {session_id}")
112
151
  tools.extend(McpToolkit.get_toolkit(
113
152
  toolkit_name=tool.get('toolkit_name', ''),
114
153
  client=alita_client,
115
- **tool['settings']).get_tools())
154
+ **settings).get_tools())
116
155
  except Exception as e:
156
+ if isinstance(e, McpAuthorizationRequired):
157
+ raise
117
158
  logger.error(f"Error initializing toolkit for tool '{tool.get('name', 'unknown')}': {e}", exc_info=True)
118
159
  if debug_mode:
119
160
  logger.info("Skipping tool initialization error due to debug mode.")