alita-sdk 0.3.448__py3-none-any.whl → 0.3.450__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.
- alita_sdk/runtime/toolkits/mcp.py +66 -167
- alita_sdk/runtime/tools/mcp_remote_tool.py +55 -158
- alita_sdk/runtime/utils/mcp_sse_client.py +405 -0
- {alita_sdk-0.3.448.dist-info → alita_sdk-0.3.450.dist-info}/METADATA +2 -1
- {alita_sdk-0.3.448.dist-info → alita_sdk-0.3.450.dist-info}/RECORD +8 -7
- {alita_sdk-0.3.448.dist-info → alita_sdk-0.3.450.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.448.dist-info → alita_sdk-0.3.450.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.448.dist-info → alita_sdk-0.3.450.dist-info}/top_level.txt +0 -0
|
@@ -7,6 +7,7 @@ Following MCP specification: https://modelcontextprotocol.io/specification/2025-
|
|
|
7
7
|
import logging
|
|
8
8
|
import re
|
|
9
9
|
import requests
|
|
10
|
+
import asyncio
|
|
10
11
|
from typing import List, Optional, Any, Dict, Literal, ClassVar, Union
|
|
11
12
|
|
|
12
13
|
from langchain_core.tools import BaseToolkit, BaseTool
|
|
@@ -17,6 +18,7 @@ from ..tools.mcp_remote_tool import McpRemoteTool
|
|
|
17
18
|
from ..tools.mcp_inspect_tool import McpInspectTool
|
|
18
19
|
from ...tools.utils import TOOLKIT_SPLITTER, clean_string
|
|
19
20
|
from ..models.mcp_models import McpConnectionConfig
|
|
21
|
+
from ..utils.mcp_sse_client import McpSseClient
|
|
20
22
|
from ..utils.mcp_oauth import (
|
|
21
23
|
McpAuthorizationRequired,
|
|
22
24
|
canonical_resource,
|
|
@@ -459,42 +461,79 @@ class McpToolkit(BaseToolkit):
|
|
|
459
461
|
timeout: int
|
|
460
462
|
) -> List[Dict[str, Any]]:
|
|
461
463
|
"""
|
|
462
|
-
|
|
464
|
+
Discover tools and prompts from MCP server using SSE client.
|
|
463
465
|
Returns list of tool/prompt dictionaries with name, description, and inputSchema.
|
|
464
466
|
Prompts are converted to tools that can be invoked.
|
|
465
467
|
"""
|
|
466
|
-
all_tools = []
|
|
467
|
-
|
|
468
|
-
# Use session_id from UI (passed via connection_config)
|
|
469
|
-
# For SSE-based MCP servers, frontend generates a UUID and sends it with the OAuth token
|
|
470
468
|
session_id = connection_config.session_id
|
|
471
469
|
|
|
472
470
|
if not session_id:
|
|
473
471
|
logger.warning(f"[MCP Session] No session_id provided for '{toolkit_name}' - server may require it")
|
|
474
472
|
logger.warning(f"[MCP Session] Frontend should generate a UUID and include it with mcp_tokens")
|
|
475
473
|
|
|
476
|
-
#
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
483
521
|
)
|
|
484
|
-
all_tools.extend(tools_data)
|
|
485
|
-
logger.info(f"Discovered {len(tools_data)} tools from MCP toolkit '{toolkit_name}'")
|
|
486
522
|
|
|
487
|
-
#
|
|
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
|
|
488
533
|
try:
|
|
489
|
-
|
|
490
|
-
endpoint="prompts/list",
|
|
491
|
-
toolkit_name=toolkit_name,
|
|
492
|
-
connection_config=connection_config,
|
|
493
|
-
timeout=timeout,
|
|
494
|
-
session_id=session_id
|
|
495
|
-
)
|
|
534
|
+
prompts = await client.list_prompts()
|
|
496
535
|
# Convert prompts to tool format
|
|
497
|
-
for prompt in
|
|
536
|
+
for prompt in prompts:
|
|
498
537
|
prompt_tool = {
|
|
499
538
|
"name": f"prompt_{prompt.get('name', 'unnamed')}",
|
|
500
539
|
"description": prompt.get('description', f"Execute prompt: {prompt.get('name')}"),
|
|
@@ -519,152 +558,12 @@ class McpToolkit(BaseToolkit):
|
|
|
519
558
|
"_mcp_prompt_name": prompt.get('name')
|
|
520
559
|
}
|
|
521
560
|
all_tools.append(prompt_tool)
|
|
522
|
-
logger.info(f"Discovered {len(
|
|
523
|
-
except Exception as e:
|
|
524
|
-
logger.warning(f"Failed to discover prompts from MCP toolkit '{toolkit_name}': {e}")
|
|
525
|
-
|
|
526
|
-
logger.info(f"Total discovered {len(all_tools)} tools+prompts from MCP toolkit '{toolkit_name}'")
|
|
527
|
-
return all_tools, session_id
|
|
528
|
-
|
|
529
|
-
@classmethod
|
|
530
|
-
def _discover_mcp_endpoint(
|
|
531
|
-
cls,
|
|
532
|
-
endpoint: str,
|
|
533
|
-
toolkit_name: str,
|
|
534
|
-
connection_config: McpConnectionConfig,
|
|
535
|
-
timeout: int,
|
|
536
|
-
session_id: Optional[str] = None
|
|
537
|
-
) -> List[Dict[str, Any]]:
|
|
538
|
-
"""
|
|
539
|
-
Discover items from a specific MCP endpoint (tools/list or prompts/list).
|
|
540
|
-
Returns list of dictionaries.
|
|
541
|
-
"""
|
|
542
|
-
import time
|
|
543
|
-
|
|
544
|
-
# MCP protocol request
|
|
545
|
-
mcp_request = {
|
|
546
|
-
"jsonrpc": "2.0",
|
|
547
|
-
"id": f"discover_{endpoint.replace('/', '_')}_{int(time.time())}",
|
|
548
|
-
"method": endpoint,
|
|
549
|
-
"params": {}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
headers = {
|
|
553
|
-
"Content-Type": "application/json",
|
|
554
|
-
"Accept": "application/json, text/event-stream"
|
|
555
|
-
}
|
|
556
|
-
if connection_config.headers:
|
|
557
|
-
headers.update(connection_config.headers)
|
|
558
|
-
|
|
559
|
-
# Add sessionId to URL if provided (for stateful SSE servers)
|
|
560
|
-
url = connection_config.url
|
|
561
|
-
if session_id:
|
|
562
|
-
separator = '&' if '?' in url else '?'
|
|
563
|
-
url = f"{url}{separator}sessionId={session_id}"
|
|
564
|
-
logger.info(f"[MCP Session] Using session {session_id} for {endpoint} request")
|
|
565
|
-
else:
|
|
566
|
-
logger.debug(f"[MCP Session] No session ID available for {endpoint} request - server may not require sessions")
|
|
567
|
-
|
|
568
|
-
try:
|
|
569
|
-
logger.debug(f"Sending MCP {endpoint} request to {url}")
|
|
570
|
-
response = requests.post(
|
|
571
|
-
url,
|
|
572
|
-
json=mcp_request,
|
|
573
|
-
headers=headers,
|
|
574
|
-
timeout=timeout
|
|
575
|
-
)
|
|
576
|
-
|
|
577
|
-
auth_header = response.headers.get('WWW-Authenticate', '')
|
|
578
|
-
if response.status_code == 401:
|
|
579
|
-
resource_metadata_url = extract_resource_metadata_url(auth_header, connection_config.url)
|
|
580
|
-
metadata = fetch_resource_metadata(resource_metadata_url, timeout=timeout) if resource_metadata_url else None
|
|
581
|
-
|
|
582
|
-
# If we couldn't get metadata from the resource_metadata endpoint,
|
|
583
|
-
# infer authorization servers from the WWW-Authenticate header and server URL
|
|
584
|
-
if not metadata or not metadata.get('authorization_servers'):
|
|
585
|
-
inferred_servers = infer_authorization_servers_from_realm(auth_header, connection_config.url)
|
|
586
|
-
if inferred_servers:
|
|
587
|
-
if not metadata:
|
|
588
|
-
metadata = {}
|
|
589
|
-
metadata['authorization_servers'] = inferred_servers
|
|
590
|
-
logger.info(f"Inferred authorization servers for {connection_config.url}: {inferred_servers}")
|
|
591
|
-
|
|
592
|
-
# Fetch OAuth authorization server metadata from the inferred server
|
|
593
|
-
# This avoids CORS issues in the frontend
|
|
594
|
-
from alita_sdk.runtime.utils.mcp_oauth import fetch_oauth_authorization_server_metadata
|
|
595
|
-
auth_server_metadata = fetch_oauth_authorization_server_metadata(inferred_servers[0], timeout=timeout)
|
|
596
|
-
if auth_server_metadata:
|
|
597
|
-
metadata['oauth_authorization_server'] = auth_server_metadata
|
|
598
|
-
logger.info(f"Fetched OAuth metadata for {inferred_servers[0]}")
|
|
599
|
-
|
|
600
|
-
raise McpAuthorizationRequired(
|
|
601
|
-
message=f"MCP server {connection_config.url} requires OAuth authorization",
|
|
602
|
-
server_url=canonical_resource(connection_config.url),
|
|
603
|
-
resource_metadata_url=resource_metadata_url,
|
|
604
|
-
www_authenticate=auth_header,
|
|
605
|
-
resource_metadata=metadata,
|
|
606
|
-
status=response.status_code,
|
|
607
|
-
tool_name=toolkit_name,
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
if response.status_code != 200:
|
|
611
|
-
logger.error(f"MCP server returned non-200 status: {response.status_code}")
|
|
612
|
-
raise Exception(f"HTTP {response.status_code}: {response.text}")
|
|
613
|
-
|
|
614
|
-
# Check content type and parse accordingly
|
|
615
|
-
content_type = response.headers.get('Content-Type', '')
|
|
616
|
-
|
|
617
|
-
if 'text/event-stream' in content_type:
|
|
618
|
-
# Parse SSE (Server-Sent Events) format
|
|
619
|
-
data = cls._parse_sse_response(response.text)
|
|
620
|
-
elif 'application/json' in content_type:
|
|
621
|
-
# Parse regular JSON
|
|
622
|
-
try:
|
|
623
|
-
data = response.json()
|
|
624
|
-
except ValueError as json_err:
|
|
625
|
-
raise Exception(f"Invalid JSON response: {json_err}. Response text: {response.text[:200]}")
|
|
626
|
-
else:
|
|
627
|
-
raise Exception(f"Unexpected Content-Type: {content_type}. Response: {response.text[:200]}")
|
|
628
|
-
|
|
629
|
-
if "error" in data:
|
|
630
|
-
raise Exception(f"MCP Error: {data['error']}")
|
|
631
|
-
|
|
632
|
-
# Parse MCP response - different endpoints return different keys
|
|
633
|
-
result = data.get("result", {})
|
|
634
|
-
if endpoint == "tools/list":
|
|
635
|
-
return result.get("tools", [])
|
|
636
|
-
elif endpoint == "prompts/list":
|
|
637
|
-
return result.get("prompts", [])
|
|
638
|
-
else:
|
|
639
|
-
return result.get("items", [])
|
|
640
|
-
|
|
561
|
+
logger.info(f"[MCP SSE] Discovered {len(prompts)} prompts from '{toolkit_name}'")
|
|
641
562
|
except Exception as e:
|
|
642
|
-
logger.
|
|
643
|
-
raise
|
|
644
|
-
|
|
645
|
-
@staticmethod
|
|
646
|
-
def _parse_sse_response(sse_text: str) -> Dict[str, Any]:
|
|
647
|
-
"""
|
|
648
|
-
Parse Server-Sent Events (SSE) format response.
|
|
649
|
-
SSE format: event: message\ndata: {json}\n\n
|
|
650
|
-
"""
|
|
651
|
-
import json
|
|
563
|
+
logger.warning(f"[MCP SSE] Failed to discover prompts: {e}")
|
|
652
564
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
for line in lines:
|
|
657
|
-
if line.startswith('data:'):
|
|
658
|
-
data_line = line[5:].strip() # Remove 'data:' prefix
|
|
659
|
-
break
|
|
660
|
-
|
|
661
|
-
if not data_line:
|
|
662
|
-
raise Exception(f"No data found in SSE response: {sse_text[:200]}")
|
|
663
|
-
|
|
664
|
-
try:
|
|
665
|
-
return json.loads(data_line)
|
|
666
|
-
except json.JSONDecodeError as e:
|
|
667
|
-
raise Exception(f"Failed to parse SSE data as JSON: {e}. Data: {data_line[:200]}")
|
|
565
|
+
logger.info(f"[MCP SSE] Total discovered {len(all_tools)} items from '{toolkit_name}'")
|
|
566
|
+
return all_tools
|
|
668
567
|
|
|
669
568
|
@classmethod
|
|
670
569
|
def _create_tool_from_dict(
|
|
@@ -20,6 +20,7 @@ from ..utils.mcp_oauth import (
|
|
|
20
20
|
fetch_resource_metadata_async,
|
|
21
21
|
infer_authorization_servers_from_realm,
|
|
22
22
|
)
|
|
23
|
+
from ..utils.mcp_sse_client import McpSseClient
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
25
26
|
|
|
@@ -83,172 +84,68 @@ class McpRemoteTool(McpServerTool):
|
|
|
83
84
|
return asyncio.run(self._execute_remote_tool(kwargs))
|
|
84
85
|
|
|
85
86
|
async def _execute_remote_tool(self, kwargs: Dict[str, Any]) -> str:
|
|
86
|
-
"""Execute the actual remote MCP tool call."""
|
|
87
|
-
import aiohttp
|
|
87
|
+
"""Execute the actual remote MCP tool call using SSE client."""
|
|
88
88
|
from ...tools.utils import TOOLKIT_SPLITTER
|
|
89
89
|
|
|
90
|
+
# Check for session_id requirement
|
|
91
|
+
if not self.session_id:
|
|
92
|
+
logger.error(f"[MCP Session] Missing session_id for tool '{self.name}'")
|
|
93
|
+
raise Exception("sessionId required. Frontend must generate UUID and send with mcp_tokens.")
|
|
94
|
+
|
|
90
95
|
# Use the original tool name from discovery for MCP server invocation
|
|
91
|
-
# The MCP server doesn't know about our optimized names
|
|
92
96
|
tool_name_for_server = self.original_tool_name
|
|
93
|
-
|
|
94
|
-
# Fallback: extract from optimized name if original not stored (backwards compatibility)
|
|
95
97
|
if not tool_name_for_server:
|
|
96
98
|
tool_name_for_server = self.name.rsplit(TOOLKIT_SPLITTER, 1)[-1] if TOOLKIT_SPLITTER in self.name else self.name
|
|
97
|
-
logger.warning(f"original_tool_name not set for '{self.name}', using extracted
|
|
99
|
+
logger.warning(f"original_tool_name not set for '{self.name}', using extracted: {tool_name_for_server}")
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# Execute the HTTP request
|
|
139
|
-
timeout = aiohttp.ClientTimeout(total=self.tool_timeout_sec)
|
|
140
|
-
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
141
|
-
try:
|
|
142
|
-
logger.debug(f"Calling remote MCP tool '{tool_name_for_server}' (optimized name: '{self.name}') at {url}")
|
|
143
|
-
logger.debug(f"Request: {json.dumps(mcp_request, indent=2)}")
|
|
101
|
+
logger.info(f"[MCP SSE] Executing tool '{tool_name_for_server}' with session {self.session_id}")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Prepare headers
|
|
105
|
+
headers = {}
|
|
106
|
+
if self.server_headers:
|
|
107
|
+
headers.update(self.server_headers)
|
|
108
|
+
|
|
109
|
+
# Create SSE client
|
|
110
|
+
client = McpSseClient(
|
|
111
|
+
url=self.server_url,
|
|
112
|
+
session_id=self.session_id,
|
|
113
|
+
headers=headers,
|
|
114
|
+
timeout=self.tool_timeout_sec
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Execute tool call via SSE
|
|
118
|
+
result = await client.call_tool(tool_name_for_server, kwargs)
|
|
119
|
+
|
|
120
|
+
# Format the result
|
|
121
|
+
if isinstance(result, dict):
|
|
122
|
+
# Check for content array (common in MCP responses)
|
|
123
|
+
if "content" in result:
|
|
124
|
+
content_items = result["content"]
|
|
125
|
+
if isinstance(content_items, list):
|
|
126
|
+
# Extract text from content items
|
|
127
|
+
text_parts = []
|
|
128
|
+
for item in content_items:
|
|
129
|
+
if isinstance(item, dict):
|
|
130
|
+
if item.get("type") == "text" and "text" in item:
|
|
131
|
+
text_parts.append(item["text"])
|
|
132
|
+
elif "text" in item:
|
|
133
|
+
text_parts.append(item["text"])
|
|
134
|
+
else:
|
|
135
|
+
text_parts.append(json.dumps(item))
|
|
136
|
+
else:
|
|
137
|
+
text_parts.append(str(item))
|
|
138
|
+
return "\n".join(text_parts)
|
|
144
139
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
timeout=self.tool_timeout_sec,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
# If we couldn't get metadata from the resource_metadata endpoint,
|
|
158
|
-
# infer authorization servers from the WWW-Authenticate header and server URL
|
|
159
|
-
if not metadata or not metadata.get('authorization_servers'):
|
|
160
|
-
inferred_servers = infer_authorization_servers_from_realm(auth_header, self.server_url)
|
|
161
|
-
if inferred_servers:
|
|
162
|
-
if not metadata:
|
|
163
|
-
metadata = {}
|
|
164
|
-
metadata['authorization_servers'] = inferred_servers
|
|
165
|
-
logger.info(f"Inferred authorization servers for {self.server_url}: {inferred_servers}")
|
|
166
|
-
|
|
167
|
-
# Fetch OAuth authorization server metadata from the inferred server
|
|
168
|
-
# This avoids CORS issues in the frontend
|
|
169
|
-
from alita_sdk.runtime.utils.mcp_oauth import fetch_oauth_authorization_server_metadata
|
|
170
|
-
auth_server_metadata = fetch_oauth_authorization_server_metadata(inferred_servers[0], timeout=self.tool_timeout_sec)
|
|
171
|
-
if auth_server_metadata:
|
|
172
|
-
metadata['oauth_authorization_server'] = auth_server_metadata
|
|
173
|
-
logger.info(f"Fetched OAuth metadata for {inferred_servers[0]}")
|
|
174
|
-
|
|
175
|
-
raise McpAuthorizationRequired(
|
|
176
|
-
message=f"MCP server {self.server_url} requires OAuth authorization",
|
|
177
|
-
server_url=canonical_resource(self.server_url),
|
|
178
|
-
resource_metadata_url=resource_metadata_url,
|
|
179
|
-
www_authenticate=auth_header,
|
|
180
|
-
resource_metadata=metadata,
|
|
181
|
-
status=response.status,
|
|
182
|
-
tool_name=self.name,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
# Check for errors first
|
|
186
|
-
data = None
|
|
187
|
-
if response.status != 200:
|
|
188
|
-
error_text = await response.text()
|
|
189
|
-
|
|
190
|
-
# Check if this is a "Missing sessionId" error
|
|
191
|
-
if response.status == 404 and "sessionId" in error_text:
|
|
192
|
-
logger.error(f"[MCP Session] Server requires session but none provided")
|
|
193
|
-
logger.error(f"[MCP Session] Frontend must generate a UUID and send it with mcp_tokens")
|
|
194
|
-
logger.error(f"[MCP Session] Example: mcp_tokens = {{'server_url': {{'access_token': '...', 'session_id': crypto.randomUUID()}}}}")
|
|
195
|
-
raise Exception(f"HTTP {response.status}: {error_text} - sessionId required but not provided by frontend")
|
|
196
|
-
else:
|
|
197
|
-
raise Exception(f"HTTP {response.status}: {error_text}")
|
|
198
|
-
|
|
199
|
-
# Parse response if not already done in retry logic
|
|
200
|
-
if data is None:
|
|
201
|
-
content_type = response.headers.get('Content-Type', '')
|
|
202
|
-
if 'text/event-stream' in content_type:
|
|
203
|
-
# Parse SSE format
|
|
204
|
-
text = await response.text()
|
|
205
|
-
data = self._parse_sse(text)
|
|
206
|
-
else:
|
|
207
|
-
# Parse regular JSON
|
|
208
|
-
data = await response.json()
|
|
209
|
-
|
|
210
|
-
logger.debug(f"Response: {json.dumps(data, indent=2)}")
|
|
211
|
-
|
|
212
|
-
# Check for MCP error
|
|
213
|
-
if "error" in data:
|
|
214
|
-
error = data["error"]
|
|
215
|
-
error_msg = error.get("message", str(error))
|
|
216
|
-
raise Exception(f"MCP Error: {error_msg}")
|
|
217
|
-
|
|
218
|
-
# Extract result
|
|
219
|
-
result = data.get("result", {})
|
|
220
|
-
|
|
221
|
-
# Format the result based on content type
|
|
222
|
-
if isinstance(result, dict):
|
|
223
|
-
# Check for content array (common in MCP responses)
|
|
224
|
-
if "content" in result:
|
|
225
|
-
content_items = result["content"]
|
|
226
|
-
if isinstance(content_items, list):
|
|
227
|
-
# Extract text from content items
|
|
228
|
-
text_parts = []
|
|
229
|
-
for item in content_items:
|
|
230
|
-
if isinstance(item, dict):
|
|
231
|
-
if item.get("type") == "text" and "text" in item:
|
|
232
|
-
text_parts.append(item["text"])
|
|
233
|
-
elif "text" in item:
|
|
234
|
-
text_parts.append(item["text"])
|
|
235
|
-
else:
|
|
236
|
-
text_parts.append(json.dumps(item))
|
|
237
|
-
else:
|
|
238
|
-
text_parts.append(str(item))
|
|
239
|
-
return "\n".join(text_parts)
|
|
240
|
-
|
|
241
|
-
# Return formatted JSON if no content field
|
|
242
|
-
return json.dumps(result, indent=2)
|
|
243
|
-
|
|
244
|
-
# Return as string for other types
|
|
245
|
-
return str(result)
|
|
246
|
-
|
|
247
|
-
except asyncio.TimeoutError:
|
|
248
|
-
raise Exception(f"Tool execution timed out after {self.tool_timeout_sec}s")
|
|
249
|
-
except Exception as e:
|
|
250
|
-
logger.error(f"Error calling remote MCP tool '{tool_name_for_server}': {e}", exc_info=True)
|
|
251
|
-
raise
|
|
140
|
+
# Return formatted JSON if no content field
|
|
141
|
+
return json.dumps(result, indent=2)
|
|
142
|
+
|
|
143
|
+
# Return as string for other types
|
|
144
|
+
return str(result)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"[MCP SSE] Tool execution failed: {e}", exc_info=True)
|
|
148
|
+
raise
|
|
252
149
|
|
|
253
150
|
def _parse_sse(self, text: str) -> Dict[str, Any]:
|
|
254
151
|
"""Parse Server-Sent Events (SSE) format response."""
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP SSE (Server-Sent Events) Client
|
|
3
|
+
Handles persistent SSE connections for MCP servers like Atlassian
|
|
4
|
+
"""
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, Any, Optional, AsyncIterator
|
|
9
|
+
import aiohttp
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class McpSseClient:
|
|
15
|
+
"""
|
|
16
|
+
Client for MCP servers using SSE (Server-Sent Events) transport.
|
|
17
|
+
|
|
18
|
+
For Atlassian-style SSE (dual-connection model):
|
|
19
|
+
- GET request opens persistent SSE stream for receiving events
|
|
20
|
+
- POST requests send commands (return 202 Accepted immediately)
|
|
21
|
+
- Responses come via the GET stream
|
|
22
|
+
|
|
23
|
+
This client handles:
|
|
24
|
+
- Opening persistent SSE connection via GET
|
|
25
|
+
- Sending JSON-RPC requests via POST
|
|
26
|
+
- Reading SSE event streams
|
|
27
|
+
- Matching responses to requests by ID
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, url: str, session_id: str, headers: Optional[Dict[str, str]] = None, timeout: int = 300):
|
|
31
|
+
"""
|
|
32
|
+
Initialize SSE client.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
url: Base URL of the MCP SSE server
|
|
36
|
+
session_id: Client-generated UUID for session
|
|
37
|
+
headers: Additional headers (e.g., Authorization)
|
|
38
|
+
timeout: Request timeout in seconds
|
|
39
|
+
"""
|
|
40
|
+
self.url = url
|
|
41
|
+
self.session_id = session_id
|
|
42
|
+
self.headers = headers or {}
|
|
43
|
+
self.timeout = timeout
|
|
44
|
+
self.url_with_session = f"{url}?sessionId={session_id}"
|
|
45
|
+
self._stream_task = None
|
|
46
|
+
self._pending_requests = {} # request_id -> asyncio.Future
|
|
47
|
+
self._stream_session = None
|
|
48
|
+
self._stream_response = None
|
|
49
|
+
self._endpoint_ready = asyncio.Event() # Signal when endpoint is received
|
|
50
|
+
|
|
51
|
+
logger.info(f"[MCP SSE Client] Initialized for {url} with session {session_id}")
|
|
52
|
+
|
|
53
|
+
async def _ensure_stream_connected(self):
|
|
54
|
+
"""Ensure the GET stream is connected and reading events."""
|
|
55
|
+
if self._stream_task is None or self._stream_task.done():
|
|
56
|
+
logger.info(f"[MCP SSE Client] Opening persistent SSE stream...")
|
|
57
|
+
self._stream_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=None))
|
|
58
|
+
|
|
59
|
+
headers = {
|
|
60
|
+
"Accept": "text/event-stream",
|
|
61
|
+
**self.headers
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
self._stream_response = await self._stream_session.get(self.url_with_session, headers=headers)
|
|
65
|
+
|
|
66
|
+
logger.info(f"[MCP SSE Client] Stream opened: status={self._stream_response.status}")
|
|
67
|
+
|
|
68
|
+
# Handle 401 Unauthorized - need OAuth
|
|
69
|
+
if self._stream_response.status == 401:
|
|
70
|
+
from ..utils.mcp_oauth import (
|
|
71
|
+
McpAuthorizationRequired,
|
|
72
|
+
canonical_resource,
|
|
73
|
+
extract_resource_metadata_url,
|
|
74
|
+
fetch_resource_metadata_async,
|
|
75
|
+
infer_authorization_servers_from_realm,
|
|
76
|
+
fetch_oauth_authorization_server_metadata
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
auth_header = self._stream_response.headers.get('WWW-Authenticate', '')
|
|
80
|
+
resource_metadata_url = extract_resource_metadata_url(auth_header, self.url)
|
|
81
|
+
|
|
82
|
+
metadata = None
|
|
83
|
+
if resource_metadata_url:
|
|
84
|
+
metadata = await fetch_resource_metadata_async(
|
|
85
|
+
resource_metadata_url,
|
|
86
|
+
session=self._stream_session,
|
|
87
|
+
timeout=30
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Infer authorization servers if not in metadata
|
|
91
|
+
if not metadata or not metadata.get('authorization_servers'):
|
|
92
|
+
inferred_servers = infer_authorization_servers_from_realm(auth_header, self.url)
|
|
93
|
+
if inferred_servers:
|
|
94
|
+
if not metadata:
|
|
95
|
+
metadata = {}
|
|
96
|
+
metadata['authorization_servers'] = inferred_servers
|
|
97
|
+
logger.info(f"[MCP SSE Client] Inferred authorization servers: {inferred_servers}")
|
|
98
|
+
|
|
99
|
+
# Fetch OAuth metadata
|
|
100
|
+
auth_server_metadata = fetch_oauth_authorization_server_metadata(inferred_servers[0], timeout=30)
|
|
101
|
+
if auth_server_metadata:
|
|
102
|
+
metadata['oauth_authorization_server'] = auth_server_metadata
|
|
103
|
+
logger.info(f"[MCP SSE Client] Fetched OAuth metadata")
|
|
104
|
+
|
|
105
|
+
raise McpAuthorizationRequired(
|
|
106
|
+
message=f"MCP server {self.url} requires OAuth authorization",
|
|
107
|
+
server_url=canonical_resource(self.url),
|
|
108
|
+
resource_metadata_url=resource_metadata_url,
|
|
109
|
+
www_authenticate=auth_header,
|
|
110
|
+
resource_metadata=metadata,
|
|
111
|
+
status=self._stream_response.status,
|
|
112
|
+
tool_name=self.url,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if self._stream_response.status != 200:
|
|
116
|
+
error_text = await self._stream_response.text()
|
|
117
|
+
raise Exception(f"Failed to open SSE stream: HTTP {self._stream_response.status}: {error_text}")
|
|
118
|
+
|
|
119
|
+
# Start background task to read stream
|
|
120
|
+
self._stream_task = asyncio.create_task(self._read_stream())
|
|
121
|
+
|
|
122
|
+
async def _read_stream(self):
|
|
123
|
+
"""Background task that continuously reads the SSE stream."""
|
|
124
|
+
logger.info(f"[MCP SSE Client] Starting stream reader...")
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
buffer = ""
|
|
128
|
+
current_event = {}
|
|
129
|
+
|
|
130
|
+
async for chunk in self._stream_response.content.iter_chunked(1024):
|
|
131
|
+
chunk_str = chunk.decode('utf-8')
|
|
132
|
+
buffer += chunk_str
|
|
133
|
+
|
|
134
|
+
# Process complete lines
|
|
135
|
+
while '\n' in buffer:
|
|
136
|
+
line, buffer = buffer.split('\n', 1)
|
|
137
|
+
line_str = line.strip()
|
|
138
|
+
|
|
139
|
+
# Empty line indicates end of event
|
|
140
|
+
if not line_str:
|
|
141
|
+
if current_event and 'data' in current_event:
|
|
142
|
+
self._process_event(current_event)
|
|
143
|
+
current_event = {}
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# Parse SSE fields
|
|
147
|
+
if line_str.startswith('event:'):
|
|
148
|
+
current_event['event'] = line_str[6:].strip()
|
|
149
|
+
elif line_str.startswith('data:'):
|
|
150
|
+
data_str = line_str[5:].strip()
|
|
151
|
+
current_event['data'] = data_str
|
|
152
|
+
elif line_str.startswith('id:'):
|
|
153
|
+
current_event['id'] = line_str[3:].strip()
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.error(f"[MCP SSE Client] Stream reader error: {e}")
|
|
157
|
+
# Fail all pending requests
|
|
158
|
+
for future in self._pending_requests.values():
|
|
159
|
+
if not future.done():
|
|
160
|
+
future.set_exception(e)
|
|
161
|
+
finally:
|
|
162
|
+
logger.info(f"[MCP SSE Client] Stream reader stopped")
|
|
163
|
+
|
|
164
|
+
def _process_event(self, event: Dict[str, str]):
|
|
165
|
+
"""Process a complete SSE event."""
|
|
166
|
+
event_type = event.get('event', 'message')
|
|
167
|
+
data_str = event.get('data', '')
|
|
168
|
+
|
|
169
|
+
# Handle 'endpoint' event - server provides the actual session URL to use
|
|
170
|
+
if event_type == 'endpoint':
|
|
171
|
+
# Extract session ID from endpoint URL
|
|
172
|
+
# Format: /v1/sse?sessionId=<uuid>
|
|
173
|
+
if 'sessionId=' in data_str:
|
|
174
|
+
new_session_id = data_str.split('sessionId=')[1].split('&')[0]
|
|
175
|
+
logger.info(f"[MCP SSE Client] Server provided session ID: {new_session_id}")
|
|
176
|
+
self.session_id = new_session_id
|
|
177
|
+
self.url_with_session = f"{self.url}?sessionId={new_session_id}"
|
|
178
|
+
self._endpoint_ready.set() # Signal that we can now send requests
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Skip other non-message events
|
|
182
|
+
if event_type != 'message' and not data_str.startswith('{'):
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if not data_str:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
data = json.loads(data_str)
|
|
190
|
+
request_id = data.get('id')
|
|
191
|
+
|
|
192
|
+
logger.debug(f"[MCP SSE Client] Received response for request {request_id}")
|
|
193
|
+
|
|
194
|
+
# Resolve pending request
|
|
195
|
+
if request_id and request_id in self._pending_requests:
|
|
196
|
+
future = self._pending_requests.pop(request_id)
|
|
197
|
+
if not future.done():
|
|
198
|
+
future.set_result(data)
|
|
199
|
+
|
|
200
|
+
except json.JSONDecodeError as e:
|
|
201
|
+
logger.warning(f"[MCP SSE Client] Failed to parse SSE data: {e}, data: {repr(data_str)[:200]}")
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.error(f"[MCP SSE Client] Stream reader error: {e}")
|
|
205
|
+
# Fail all pending requests
|
|
206
|
+
for future in self._pending_requests.values():
|
|
207
|
+
if not future.done():
|
|
208
|
+
future.set_exception(e)
|
|
209
|
+
finally:
|
|
210
|
+
logger.info(f"[MCP SSE Client] Stream reader stopped")
|
|
211
|
+
|
|
212
|
+
async def send_request(self, method: str, params: Optional[Dict[str, Any]] = None, request_id: Optional[str] = None) -> Dict[str, Any]:
|
|
213
|
+
"""
|
|
214
|
+
Send a JSON-RPC request and wait for response via SSE stream.
|
|
215
|
+
|
|
216
|
+
Uses dual-connection model:
|
|
217
|
+
1. GET stream is kept open to receive responses
|
|
218
|
+
2. POST request sends the command (returns 202 immediately)
|
|
219
|
+
3. Response comes via the GET stream
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
method: JSON-RPC method name (e.g., "tools/list", "tools/call")
|
|
223
|
+
params: Method parameters
|
|
224
|
+
request_id: Optional request ID (auto-generated if not provided)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Parsed JSON-RPC response
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
Exception: If request fails or times out
|
|
231
|
+
"""
|
|
232
|
+
import time
|
|
233
|
+
if request_id is None:
|
|
234
|
+
request_id = f"{method.replace('/', '_')}_{int(time.time() * 1000)}"
|
|
235
|
+
|
|
236
|
+
request = {
|
|
237
|
+
"jsonrpc": "2.0",
|
|
238
|
+
"id": request_id,
|
|
239
|
+
"method": method,
|
|
240
|
+
"params": params or {}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
logger.debug(f"[MCP SSE Client] Sending request: {method} (id={request_id})")
|
|
244
|
+
|
|
245
|
+
# Ensure stream is connected
|
|
246
|
+
await self._ensure_stream_connected()
|
|
247
|
+
|
|
248
|
+
# Wait for endpoint event (server provides the actual session ID to use)
|
|
249
|
+
await asyncio.wait_for(self._endpoint_ready.wait(), timeout=10)
|
|
250
|
+
|
|
251
|
+
# Create future for this request
|
|
252
|
+
future = asyncio.Future()
|
|
253
|
+
self._pending_requests[request_id] = future
|
|
254
|
+
|
|
255
|
+
# Send POST request
|
|
256
|
+
headers = {
|
|
257
|
+
"Content-Type": "application/json",
|
|
258
|
+
**self.headers
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
265
|
+
async with session.post(self.url_with_session, json=request, headers=headers) as response:
|
|
266
|
+
if response.status == 404:
|
|
267
|
+
error_text = await response.text()
|
|
268
|
+
raise Exception(f"HTTP 404: {error_text}")
|
|
269
|
+
|
|
270
|
+
# 202 is expected - response will come via stream
|
|
271
|
+
if response.status not in [200, 202]:
|
|
272
|
+
error_text = await response.text()
|
|
273
|
+
raise Exception(f"HTTP {response.status}: {error_text}")
|
|
274
|
+
|
|
275
|
+
# Wait for response from stream (with timeout)
|
|
276
|
+
result = await asyncio.wait_for(future, timeout=self.timeout)
|
|
277
|
+
|
|
278
|
+
# Check for JSON-RPC error
|
|
279
|
+
if 'error' in result:
|
|
280
|
+
error = result['error']
|
|
281
|
+
raise Exception(f"MCP Error: {error.get('message', str(error))}")
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
except asyncio.TimeoutError:
|
|
286
|
+
self._pending_requests.pop(request_id, None)
|
|
287
|
+
logger.error(f"[MCP SSE Client] Request timeout after {self.timeout}s")
|
|
288
|
+
raise Exception(f"SSE request timeout after {self.timeout}s")
|
|
289
|
+
except Exception as e:
|
|
290
|
+
self._pending_requests.pop(request_id, None)
|
|
291
|
+
logger.error(f"[MCP SSE Client] Request failed: {e}")
|
|
292
|
+
raise
|
|
293
|
+
|
|
294
|
+
async def close(self):
|
|
295
|
+
"""Close the persistent SSE stream."""
|
|
296
|
+
logger.info(f"[MCP SSE Client] Closing connection...")
|
|
297
|
+
|
|
298
|
+
# Cancel background stream reader task
|
|
299
|
+
if self._stream_task and not self._stream_task.done():
|
|
300
|
+
self._stream_task.cancel()
|
|
301
|
+
try:
|
|
302
|
+
await self._stream_task
|
|
303
|
+
except (asyncio.CancelledError, Exception) as e:
|
|
304
|
+
logger.debug(f"[MCP SSE Client] Stream task cleanup: {e}")
|
|
305
|
+
|
|
306
|
+
# Close response stream
|
|
307
|
+
if self._stream_response and not self._stream_response.closed:
|
|
308
|
+
try:
|
|
309
|
+
self._stream_response.close()
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.debug(f"[MCP SSE Client] Response close error: {e}")
|
|
312
|
+
|
|
313
|
+
# Close session
|
|
314
|
+
if self._stream_session and not self._stream_session.closed:
|
|
315
|
+
try:
|
|
316
|
+
await self._stream_session.close()
|
|
317
|
+
# Give aiohttp time to cleanup
|
|
318
|
+
await asyncio.sleep(0.1)
|
|
319
|
+
except Exception as e:
|
|
320
|
+
logger.debug(f"[MCP SSE Client] Session close error: {e}")
|
|
321
|
+
|
|
322
|
+
logger.info(f"[MCP SSE Client] Connection closed")
|
|
323
|
+
|
|
324
|
+
async def __aenter__(self):
|
|
325
|
+
"""Async context manager entry."""
|
|
326
|
+
return self
|
|
327
|
+
|
|
328
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
329
|
+
"""Async context manager exit."""
|
|
330
|
+
await self.close()
|
|
331
|
+
|
|
332
|
+
async def initialize(self) -> Dict[str, Any]:
|
|
333
|
+
"""
|
|
334
|
+
Send initialize request to establish MCP protocol session.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Server capabilities and info
|
|
338
|
+
"""
|
|
339
|
+
response = await self.send_request(
|
|
340
|
+
method="initialize",
|
|
341
|
+
params={
|
|
342
|
+
"protocolVersion": "2024-11-05",
|
|
343
|
+
"capabilities": {
|
|
344
|
+
"roots": {"listChanged": True},
|
|
345
|
+
"sampling": {}
|
|
346
|
+
},
|
|
347
|
+
"clientInfo": {
|
|
348
|
+
"name": "Alita MCP Client",
|
|
349
|
+
"version": "1.0.0"
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
logger.info(f"[MCP SSE Client] MCP session initialized")
|
|
355
|
+
return response.get('result', {})
|
|
356
|
+
|
|
357
|
+
async def list_tools(self) -> list:
|
|
358
|
+
"""
|
|
359
|
+
Discover available tools from the MCP server.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
List of tool definitions
|
|
363
|
+
"""
|
|
364
|
+
response = await self.send_request(method="tools/list")
|
|
365
|
+
result = response.get('result', {})
|
|
366
|
+
tools = result.get('tools', [])
|
|
367
|
+
|
|
368
|
+
logger.info(f"[MCP SSE Client] Discovered {len(tools)} tools")
|
|
369
|
+
return tools
|
|
370
|
+
|
|
371
|
+
async def list_prompts(self) -> list:
|
|
372
|
+
"""
|
|
373
|
+
Discover available prompts from the MCP server.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
List of prompt definitions
|
|
377
|
+
"""
|
|
378
|
+
response = await self.send_request(method="prompts/list")
|
|
379
|
+
result = response.get('result', {})
|
|
380
|
+
prompts = result.get('prompts', [])
|
|
381
|
+
|
|
382
|
+
logger.debug(f"[MCP SSE Client] Discovered {len(prompts)} prompts")
|
|
383
|
+
return prompts
|
|
384
|
+
|
|
385
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
|
386
|
+
"""
|
|
387
|
+
Execute a tool on the MCP server.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
tool_name: Name of the tool to call
|
|
391
|
+
arguments: Tool arguments
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Tool execution result
|
|
395
|
+
"""
|
|
396
|
+
response = await self.send_request(
|
|
397
|
+
method="tools/call",
|
|
398
|
+
params={
|
|
399
|
+
"name": tool_name,
|
|
400
|
+
"arguments": arguments
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
result = response.get('result', {})
|
|
405
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alita_sdk
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.450
|
|
4
4
|
Summary: SDK for building langchain agents using resources from Alita
|
|
5
5
|
Author-email: Artem Rozumenko <artyom.rozumenko@gmail.com>, Mikalai Biazruchka <mikalai_biazruchka@epam.com>, Roman Mitusov <roman_mitusov@epam.com>, Ivan Krakhmaliuk <lifedj27@gmail.com>, Artem Dubrovskiy <ad13box@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -24,6 +24,7 @@ Requires-Dist: fastapi==0.115.9
|
|
|
24
24
|
Requires-Dist: httpcore==1.0.7
|
|
25
25
|
Requires-Dist: urllib3>=2
|
|
26
26
|
Requires-Dist: certifi==2024.8.30
|
|
27
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
27
28
|
Provides-Extra: runtime
|
|
28
29
|
Requires-Dist: streamlit>=1.28.0; extra == "runtime"
|
|
29
30
|
Requires-Dist: langchain_core<0.4.0,>=0.3.76; extra == "runtime"
|
|
@@ -103,7 +103,7 @@ alita_sdk/runtime/toolkits/application.py,sha256=HHAKgwKOckxc7EQG-AV7rz4POOzQJKF
|
|
|
103
103
|
alita_sdk/runtime/toolkits/artifact.py,sha256=YChNCX4QhVpaQG7Jk4TS-Wl0Aruc4slQ2K21zh9nNO0,3176
|
|
104
104
|
alita_sdk/runtime/toolkits/configurations.py,sha256=kIDAlnryPQfbZyFxV-9SzN2-Vefzx06TX1BBdIIpN90,141
|
|
105
105
|
alita_sdk/runtime/toolkits/datasource.py,sha256=qk78OdPoReYPCWwahfkKLbKc4pfsu-061oXRryFLP6I,2498
|
|
106
|
-
alita_sdk/runtime/toolkits/mcp.py,sha256=
|
|
106
|
+
alita_sdk/runtime/toolkits/mcp.py,sha256=wa-47obeWm8WrIiaUw5BR40dsR5sLdQKZHzG7MCRnRM,37105
|
|
107
107
|
alita_sdk/runtime/toolkits/prompt.py,sha256=WIpTkkVYWqIqOWR_LlSWz3ug8uO9tm5jJ7aZYdiGRn0,1192
|
|
108
108
|
alita_sdk/runtime/toolkits/subgraph.py,sha256=wwUK8JjPXkGzyVZ3tAukmvST6eGbqx_U11rpnmbrvtg,2105
|
|
109
109
|
alita_sdk/runtime/toolkits/tools.py,sha256=74R0SadMiUJFUe3U5_PJcxzw5m1KMMBiVfVeiowZK6I,13434
|
|
@@ -122,7 +122,7 @@ alita_sdk/runtime/tools/llm.py,sha256=iRG_wU4T01LRsjEMPZe5Uah7LiMqDc-vspwkMuQtlt
|
|
|
122
122
|
alita_sdk/runtime/tools/loop.py,sha256=uds0WhZvwMxDVFI6MZHrcmMle637cQfBNg682iLxoJA,8335
|
|
123
123
|
alita_sdk/runtime/tools/loop_output.py,sha256=U4hO9PCQgWlXwOq6jdmCGbegtAxGAPXObSxZQ3z38uk,8069
|
|
124
124
|
alita_sdk/runtime/tools/mcp_inspect_tool.py,sha256=38X8euaxDbEGjcfp6ElvExZalpZun6QEr6ZEW4nU5pQ,11496
|
|
125
|
-
alita_sdk/runtime/tools/mcp_remote_tool.py,sha256=
|
|
125
|
+
alita_sdk/runtime/tools/mcp_remote_tool.py,sha256=Bpgu5r97QzDvNert8Vv424DlolMR0rXsac3Qrg2iAig,7059
|
|
126
126
|
alita_sdk/runtime/tools/mcp_server_tool.py,sha256=-xO3H6BM63KaIV1CdcQKPVE0WPigiqOgFZDX7m2_yGs,4419
|
|
127
127
|
alita_sdk/runtime/tools/pgvector_search.py,sha256=NN2BGAnq4SsDHIhUcFZ8d_dbEOM8QwB0UwpsWCYruXU,11692
|
|
128
128
|
alita_sdk/runtime/tools/prompt.py,sha256=nJafb_e5aOM1Rr3qGFCR-SKziU9uCsiP2okIMs9PppM,741
|
|
@@ -137,6 +137,7 @@ alita_sdk/runtime/utils/constants.py,sha256=Xntx1b_uxUzT4clwqHA_U6K8y5bBqf_4lSQw
|
|
|
137
137
|
alita_sdk/runtime/utils/evaluate.py,sha256=iM1P8gzBLHTuSCe85_Ng_h30m52hFuGuhNXJ7kB1tgI,1872
|
|
138
138
|
alita_sdk/runtime/utils/logging.py,sha256=svPyiW8ztDfhqHFITv5FBCj8UhLxz6hWcqGIY6wpJJE,3331
|
|
139
139
|
alita_sdk/runtime/utils/mcp_oauth.py,sha256=Ynoa_C_G5WXL_tlcdol2wBLgQauyvIPX0isCJKsvWMs,6151
|
|
140
|
+
alita_sdk/runtime/utils/mcp_sse_client.py,sha256=cSOyfnOoxVorfIePQ4k-BmOutBOeL3YDhTNxWtFREA0,16251
|
|
140
141
|
alita_sdk/runtime/utils/save_dataframe.py,sha256=i-E1wp-t4wb17Zq3nA3xYwgSILjoXNizaQAA9opWvxY,1576
|
|
141
142
|
alita_sdk/runtime/utils/streamlit.py,sha256=yIb4YIGH8HRAXZtZlywjxI07Xdcb5eUt7rMA-elFTdc,107261
|
|
142
143
|
alita_sdk/runtime/utils/toolkit_runtime.py,sha256=MU63Fpxj0b5_r1IUUc0Q3-PN9VwL7rUxp2MRR4tmYR8,5136
|
|
@@ -360,8 +361,8 @@ alita_sdk/tools/zephyr_scale/api_wrapper.py,sha256=kT0TbmMvuKhDUZc0i7KO18O38JM9S
|
|
|
360
361
|
alita_sdk/tools/zephyr_squad/__init__.py,sha256=0ne8XLJEQSLOWfzd2HdnqOYmQlUliKHbBED5kW_Vias,2895
|
|
361
362
|
alita_sdk/tools/zephyr_squad/api_wrapper.py,sha256=kmw_xol8YIYFplBLWTqP_VKPRhL_1ItDD0_vXTe_UuI,14906
|
|
362
363
|
alita_sdk/tools/zephyr_squad/zephyr_squad_cloud_client.py,sha256=R371waHsms4sllHCbijKYs90C-9Yu0sSR3N4SUfQOgU,5066
|
|
363
|
-
alita_sdk-0.3.
|
|
364
|
-
alita_sdk-0.3.
|
|
365
|
-
alita_sdk-0.3.
|
|
366
|
-
alita_sdk-0.3.
|
|
367
|
-
alita_sdk-0.3.
|
|
364
|
+
alita_sdk-0.3.450.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
365
|
+
alita_sdk-0.3.450.dist-info/METADATA,sha256=ef9IMngx_rnicjlwr5pbK6rWNCbb8oGxTS4NbiAatuw,19101
|
|
366
|
+
alita_sdk-0.3.450.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
367
|
+
alita_sdk-0.3.450.dist-info/top_level.txt,sha256=0vJYy5p_jK6AwVb1aqXr7Kgqgk3WDtQ6t5C-XI9zkmg,10
|
|
368
|
+
alita_sdk-0.3.450.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|