alita-sdk 0.3.379__py3-none-any.whl → 0.3.627__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/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +156 -0
- alita_sdk/cli/agent_loader.py +245 -0
- alita_sdk/cli/agent_ui.py +228 -0
- alita_sdk/cli/agents.py +3113 -0
- alita_sdk/cli/callbacks.py +647 -0
- alita_sdk/cli/cli.py +168 -0
- alita_sdk/cli/config.py +306 -0
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/formatting.py +182 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/testcases/__init__.py +94 -0
- alita_sdk/cli/testcases/data_generation.py +119 -0
- alita_sdk/cli/testcases/discovery.py +96 -0
- alita_sdk/cli/testcases/executor.py +84 -0
- alita_sdk/cli/testcases/logger.py +85 -0
- alita_sdk/cli/testcases/parser.py +172 -0
- alita_sdk/cli/testcases/prompts.py +91 -0
- alita_sdk/cli/testcases/reporting.py +125 -0
- alita_sdk/cli/testcases/setup.py +108 -0
- alita_sdk/cli/testcases/test_runner.py +282 -0
- alita_sdk/cli/testcases/utils.py +39 -0
- alita_sdk/cli/testcases/validation.py +90 -0
- alita_sdk/cli/testcases/workflow.py +196 -0
- alita_sdk/cli/toolkit.py +327 -0
- alita_sdk/cli/toolkit_loader.py +85 -0
- alita_sdk/cli/tools/__init__.py +43 -0
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +1751 -0
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +1 -1
- alita_sdk/configurations/ado.py +141 -20
- alita_sdk/configurations/bitbucket.py +94 -2
- alita_sdk/configurations/confluence.py +130 -1
- alita_sdk/configurations/figma.py +76 -0
- alita_sdk/configurations/gitlab.py +91 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/openapi.py +329 -0
- alita_sdk/configurations/qtest.py +72 -1
- alita_sdk/configurations/report_portal.py +96 -0
- alita_sdk/configurations/sharepoint.py +148 -0
- alita_sdk/configurations/testio.py +83 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +93 -0
- alita_sdk/configurations/zephyr_enterprise.py +93 -0
- alita_sdk/configurations/zephyr_essential.py +75 -0
- alita_sdk/runtime/clients/artifact.py +3 -3
- alita_sdk/runtime/clients/client.py +388 -46
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/clients/sandbox_client.py +8 -21
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +157 -39
- alita_sdk/runtime/langchain/constants.py +647 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -4
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
- alita_sdk/runtime/langchain/document_loaders/constants.py +40 -19
- alita_sdk/runtime/langchain/langraph_agent.py +405 -84
- alita_sdk/runtime/langchain/utils.py +106 -7
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/models/mcp_models.py +61 -0
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +31 -0
- alita_sdk/runtime/toolkits/application.py +29 -10
- alita_sdk/runtime/toolkits/artifact.py +20 -11
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +783 -0
- alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +251 -6
- alita_sdk/runtime/toolkits/tools.py +356 -69
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +10 -3
- alita_sdk/runtime/tools/application.py +27 -6
- alita_sdk/runtime/tools/artifact.py +511 -28
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +67 -35
- alita_sdk/runtime/tools/graph.py +10 -4
- alita_sdk/runtime/tools/image_generation.py +148 -46
- alita_sdk/runtime/tools/llm.py +1003 -128
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +8 -5
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -4
- alita_sdk/runtime/tools/sandbox.py +65 -48
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +9 -3
- alita_sdk/runtime/tools/vectorstore_base.py +70 -14
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +361 -0
- alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/serialization.py +155 -0
- alita_sdk/runtime/utils/streamlit.py +40 -13
- alita_sdk/runtime/utils/toolkit_utils.py +30 -9
- alita_sdk/runtime/utils/utils.py +36 -0
- alita_sdk/tools/__init__.py +134 -35
- alita_sdk/tools/ado/repos/__init__.py +51 -32
- alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
- alita_sdk/tools/ado/test_plan/__init__.py +25 -9
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
- alita_sdk/tools/ado/utils.py +1 -18
- alita_sdk/tools/ado/wiki/__init__.py +25 -12
- alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
- alita_sdk/tools/ado/work_item/__init__.py +26 -13
- alita_sdk/tools/ado/work_item/ado_wrapper.py +73 -11
- alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +11 -8
- alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +271 -84
- alita_sdk/tools/bitbucket/__init__.py +17 -11
- alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
- alita_sdk/tools/browser/__init__.py +5 -4
- alita_sdk/tools/carrier/__init__.py +5 -6
- alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
- alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
- alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
- alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +10 -7
- alita_sdk/tools/cloud/azure/__init__.py +10 -7
- alita_sdk/tools/cloud/gcp/__init__.py +10 -7
- alita_sdk/tools/cloud/k8s/__init__.py +10 -7
- alita_sdk/tools/code/linter/__init__.py +10 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +11 -8
- alita_sdk/tools/code_indexer_toolkit.py +82 -22
- alita_sdk/tools/confluence/__init__.py +22 -16
- alita_sdk/tools/confluence/api_wrapper.py +107 -30
- alita_sdk/tools/confluence/loader.py +14 -2
- alita_sdk/tools/custom_open_api/__init__.py +12 -5
- alita_sdk/tools/elastic/__init__.py +11 -8
- alita_sdk/tools/elitea_base.py +493 -30
- alita_sdk/tools/figma/__init__.py +58 -11
- alita_sdk/tools/figma/api_wrapper.py +1235 -143
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +14 -15
- alita_sdk/tools/github/github_client.py +224 -100
- alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
- alita_sdk/tools/github/schemas.py +14 -5
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/github/tool_prompts.py +9 -22
- alita_sdk/tools/gitlab/__init__.py +16 -11
- alita_sdk/tools/gitlab/api_wrapper.py +218 -48
- alita_sdk/tools/gitlab_org/__init__.py +10 -9
- alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
- alita_sdk/tools/google/bigquery/__init__.py +13 -12
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +11 -8
- alita_sdk/tools/google_places/api_wrapper.py +1 -1
- alita_sdk/tools/jira/__init__.py +17 -10
- alita_sdk/tools/jira/api_wrapper.py +92 -41
- alita_sdk/tools/keycloak/__init__.py +11 -8
- alita_sdk/tools/localgit/__init__.py +9 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +12 -4
- alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
- alita_sdk/tools/ocr/__init__.py +11 -8
- alita_sdk/tools/openapi/__init__.py +491 -106
- alita_sdk/tools/openapi/api_wrapper.py +1368 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +20 -12
- alita_sdk/tools/pandas/api_wrapper.py +38 -25
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +10 -9
- alita_sdk/tools/pptx/__init__.py +11 -10
- alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
- alita_sdk/tools/qtest/__init__.py +31 -11
- alita_sdk/tools/qtest/api_wrapper.py +2135 -86
- alita_sdk/tools/rally/__init__.py +10 -9
- alita_sdk/tools/rally/api_wrapper.py +1 -1
- alita_sdk/tools/report_portal/__init__.py +12 -8
- alita_sdk/tools/salesforce/__init__.py +10 -8
- alita_sdk/tools/servicenow/__init__.py +17 -15
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +10 -7
- alita_sdk/tools/sharepoint/api_wrapper.py +129 -38
- alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/slack/__init__.py +10 -7
- alita_sdk/tools/slack/api_wrapper.py +2 -2
- alita_sdk/tools/sql/__init__.py +12 -9
- alita_sdk/tools/testio/__init__.py +10 -7
- alita_sdk/tools/testrail/__init__.py +11 -10
- alita_sdk/tools/testrail/api_wrapper.py +1 -1
- alita_sdk/tools/utils/__init__.py +9 -4
- alita_sdk/tools/utils/content_parser.py +103 -18
- alita_sdk/tools/utils/text_operations.py +410 -0
- alita_sdk/tools/utils/tool_prompts.py +79 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +30 -13
- alita_sdk/tools/xray/__init__.py +13 -9
- alita_sdk/tools/yagmail/__init__.py +9 -3
- alita_sdk/tools/zephyr/__init__.py +10 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +11 -7
- alita_sdk/tools/zephyr_essential/__init__.py +10 -7
- alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
- alita_sdk/tools/zephyr_essential/client.py +2 -2
- alita_sdk/tools/zephyr_scale/__init__.py +11 -8
- alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
- alita_sdk/tools/zephyr_squad/__init__.py +10 -7
- {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +154 -8
- alita_sdk-0.3.627.dist-info/RECORD +468 -0
- alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
- alita_sdk-0.3.379.dist-info/RECORD +0 -360
- {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.379.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
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
|
+
extract_authorization_uri,
|
|
75
|
+
fetch_resource_metadata_async,
|
|
76
|
+
infer_authorization_servers_from_realm,
|
|
77
|
+
fetch_oauth_authorization_server_metadata
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
auth_header = self._stream_response.headers.get('WWW-Authenticate', '')
|
|
81
|
+
resource_metadata_url = extract_resource_metadata_url(auth_header, self.url)
|
|
82
|
+
|
|
83
|
+
# First, try authorization_uri from WWW-Authenticate header (preferred)
|
|
84
|
+
authorization_uri = extract_authorization_uri(auth_header)
|
|
85
|
+
|
|
86
|
+
metadata = None
|
|
87
|
+
if authorization_uri:
|
|
88
|
+
# Fetch OAuth metadata directly from authorization_uri
|
|
89
|
+
auth_server_metadata = fetch_oauth_authorization_server_metadata(authorization_uri, timeout=30)
|
|
90
|
+
if auth_server_metadata:
|
|
91
|
+
# Extract base authorization server URL from the issuer or the well-known URL
|
|
92
|
+
base_auth_server = auth_server_metadata.get('issuer')
|
|
93
|
+
if not base_auth_server and '/.well-known/' in authorization_uri:
|
|
94
|
+
base_auth_server = authorization_uri.split('/.well-known/')[0]
|
|
95
|
+
|
|
96
|
+
metadata = {
|
|
97
|
+
'authorization_servers': [base_auth_server] if base_auth_server else [authorization_uri],
|
|
98
|
+
'oauth_authorization_server': auth_server_metadata
|
|
99
|
+
}
|
|
100
|
+
logger.info(f"[MCP SSE Client] Using authorization_uri: {authorization_uri}, base: {base_auth_server}")
|
|
101
|
+
|
|
102
|
+
# Fall back to resource_metadata if authorization_uri didn't work
|
|
103
|
+
if not metadata:
|
|
104
|
+
if resource_metadata_url:
|
|
105
|
+
metadata = await fetch_resource_metadata_async(
|
|
106
|
+
resource_metadata_url,
|
|
107
|
+
session=self._stream_session,
|
|
108
|
+
timeout=30
|
|
109
|
+
)
|
|
110
|
+
# If we got resource_metadata, also fetch oauth_authorization_server
|
|
111
|
+
if metadata and metadata.get('authorization_servers'):
|
|
112
|
+
auth_server_metadata = fetch_oauth_authorization_server_metadata(
|
|
113
|
+
metadata['authorization_servers'][0], timeout=30
|
|
114
|
+
)
|
|
115
|
+
if auth_server_metadata:
|
|
116
|
+
metadata['oauth_authorization_server'] = auth_server_metadata
|
|
117
|
+
logger.info(f"[MCP SSE Client] Fetched OAuth metadata from resource_metadata")
|
|
118
|
+
|
|
119
|
+
# Infer authorization servers if not in metadata
|
|
120
|
+
if not metadata or not metadata.get('authorization_servers'):
|
|
121
|
+
inferred_servers = infer_authorization_servers_from_realm(auth_header, self.url)
|
|
122
|
+
if inferred_servers:
|
|
123
|
+
if not metadata:
|
|
124
|
+
metadata = {}
|
|
125
|
+
metadata['authorization_servers'] = inferred_servers
|
|
126
|
+
logger.info(f"[MCP SSE Client] Inferred authorization servers: {inferred_servers}")
|
|
127
|
+
|
|
128
|
+
# Fetch OAuth metadata
|
|
129
|
+
auth_server_metadata = fetch_oauth_authorization_server_metadata(inferred_servers[0], timeout=30)
|
|
130
|
+
if auth_server_metadata:
|
|
131
|
+
metadata['oauth_authorization_server'] = auth_server_metadata
|
|
132
|
+
logger.info(f"[MCP SSE Client] Fetched OAuth metadata")
|
|
133
|
+
|
|
134
|
+
raise McpAuthorizationRequired(
|
|
135
|
+
message=f"MCP server {self.url} requires OAuth authorization",
|
|
136
|
+
server_url=canonical_resource(self.url),
|
|
137
|
+
resource_metadata_url=resource_metadata_url,
|
|
138
|
+
www_authenticate=auth_header,
|
|
139
|
+
resource_metadata=metadata,
|
|
140
|
+
status=self._stream_response.status,
|
|
141
|
+
tool_name=self.url,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if self._stream_response.status != 200:
|
|
145
|
+
error_text = await self._stream_response.text()
|
|
146
|
+
raise Exception(f"Failed to open SSE stream: HTTP {self._stream_response.status}: {error_text}")
|
|
147
|
+
|
|
148
|
+
# Start background task to read stream
|
|
149
|
+
self._stream_task = asyncio.create_task(self._read_stream())
|
|
150
|
+
|
|
151
|
+
async def _read_stream(self):
|
|
152
|
+
"""Background task that continuously reads the SSE stream."""
|
|
153
|
+
logger.info(f"[MCP SSE Client] Starting stream reader...")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
buffer = ""
|
|
157
|
+
current_event = {}
|
|
158
|
+
|
|
159
|
+
async for chunk in self._stream_response.content.iter_chunked(1024):
|
|
160
|
+
chunk_str = chunk.decode('utf-8')
|
|
161
|
+
buffer += chunk_str
|
|
162
|
+
|
|
163
|
+
# Process complete lines
|
|
164
|
+
while '\n' in buffer:
|
|
165
|
+
line, buffer = buffer.split('\n', 1)
|
|
166
|
+
line_str = line.strip()
|
|
167
|
+
|
|
168
|
+
# Empty line indicates end of event
|
|
169
|
+
if not line_str:
|
|
170
|
+
if current_event and 'data' in current_event:
|
|
171
|
+
self._process_event(current_event)
|
|
172
|
+
current_event = {}
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Parse SSE fields
|
|
176
|
+
if line_str.startswith('event:'):
|
|
177
|
+
current_event['event'] = line_str[6:].strip()
|
|
178
|
+
elif line_str.startswith('data:'):
|
|
179
|
+
data_str = line_str[5:].strip()
|
|
180
|
+
current_event['data'] = data_str
|
|
181
|
+
elif line_str.startswith('id:'):
|
|
182
|
+
current_event['id'] = line_str[3:].strip()
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"[MCP SSE Client] Stream reader error: {e}")
|
|
186
|
+
# Fail all pending requests
|
|
187
|
+
for future in self._pending_requests.values():
|
|
188
|
+
if not future.done():
|
|
189
|
+
future.set_exception(e)
|
|
190
|
+
finally:
|
|
191
|
+
logger.info(f"[MCP SSE Client] Stream reader stopped")
|
|
192
|
+
|
|
193
|
+
def _process_event(self, event: Dict[str, str]):
|
|
194
|
+
"""Process a complete SSE event."""
|
|
195
|
+
event_type = event.get('event', 'message')
|
|
196
|
+
data_str = event.get('data', '')
|
|
197
|
+
|
|
198
|
+
# Handle 'endpoint' event - server provides the actual session URL to use
|
|
199
|
+
if event_type == 'endpoint':
|
|
200
|
+
# Extract session ID from endpoint URL
|
|
201
|
+
# Format: /v1/sse?sessionId=<uuid>
|
|
202
|
+
if 'sessionId=' in data_str:
|
|
203
|
+
new_session_id = data_str.split('sessionId=')[1].split('&')[0]
|
|
204
|
+
logger.info(f"[MCP SSE Client] Server provided session ID: {new_session_id}")
|
|
205
|
+
self.session_id = new_session_id
|
|
206
|
+
self.url_with_session = f"{self.url}?sessionId={new_session_id}"
|
|
207
|
+
self._endpoint_ready.set() # Signal that we can now send requests
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Skip other non-message events
|
|
211
|
+
if event_type != 'message' and not data_str.startswith('{'):
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if not data_str:
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
data = json.loads(data_str)
|
|
219
|
+
request_id = data.get('id')
|
|
220
|
+
|
|
221
|
+
logger.debug(f"[MCP SSE Client] Received response for request {request_id}")
|
|
222
|
+
|
|
223
|
+
# Resolve pending request
|
|
224
|
+
if request_id and request_id in self._pending_requests:
|
|
225
|
+
future = self._pending_requests.pop(request_id)
|
|
226
|
+
if not future.done():
|
|
227
|
+
future.set_result(data)
|
|
228
|
+
|
|
229
|
+
except json.JSONDecodeError as e:
|
|
230
|
+
logger.warning(f"[MCP SSE Client] Failed to parse SSE data: {e}, data: {repr(data_str)[:200]}")
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"[MCP SSE Client] Stream reader error: {e}")
|
|
234
|
+
# Fail all pending requests
|
|
235
|
+
for future in self._pending_requests.values():
|
|
236
|
+
if not future.done():
|
|
237
|
+
future.set_exception(e)
|
|
238
|
+
finally:
|
|
239
|
+
logger.info(f"[MCP SSE Client] Stream reader stopped")
|
|
240
|
+
|
|
241
|
+
async def send_request(self, method: str, params: Optional[Dict[str, Any]] = None, request_id: Optional[str] = None) -> Dict[str, Any]:
|
|
242
|
+
"""
|
|
243
|
+
Send a JSON-RPC request and wait for response via SSE stream.
|
|
244
|
+
|
|
245
|
+
Uses dual-connection model:
|
|
246
|
+
1. GET stream is kept open to receive responses
|
|
247
|
+
2. POST request sends the command (returns 202 immediately)
|
|
248
|
+
3. Response comes via the GET stream
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
method: JSON-RPC method name (e.g., "tools/list", "tools/call")
|
|
252
|
+
params: Method parameters
|
|
253
|
+
request_id: Optional request ID (auto-generated if not provided)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Parsed JSON-RPC response
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
Exception: If request fails or times out
|
|
260
|
+
"""
|
|
261
|
+
import time
|
|
262
|
+
if request_id is None:
|
|
263
|
+
request_id = f"{method.replace('/', '_')}_{int(time.time() * 1000)}"
|
|
264
|
+
|
|
265
|
+
request = {
|
|
266
|
+
"jsonrpc": "2.0",
|
|
267
|
+
"id": request_id,
|
|
268
|
+
"method": method,
|
|
269
|
+
"params": params or {}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
logger.debug(f"[MCP SSE Client] Sending request: {method} (id={request_id})")
|
|
273
|
+
|
|
274
|
+
# Ensure stream is connected
|
|
275
|
+
await self._ensure_stream_connected()
|
|
276
|
+
|
|
277
|
+
# Wait for endpoint event (server provides the actual session ID to use)
|
|
278
|
+
await asyncio.wait_for(self._endpoint_ready.wait(), timeout=10)
|
|
279
|
+
|
|
280
|
+
# Create future for this request
|
|
281
|
+
future = asyncio.Future()
|
|
282
|
+
self._pending_requests[request_id] = future
|
|
283
|
+
|
|
284
|
+
# Send POST request
|
|
285
|
+
headers = {
|
|
286
|
+
"Content-Type": "application/json",
|
|
287
|
+
**self.headers
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
timeout = aiohttp.ClientTimeout(total=30)
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
294
|
+
async with session.post(self.url_with_session, json=request, headers=headers) as response:
|
|
295
|
+
if response.status == 404:
|
|
296
|
+
error_text = await response.text()
|
|
297
|
+
raise Exception(f"HTTP 404: {error_text}")
|
|
298
|
+
|
|
299
|
+
# 202 is expected - response will come via stream
|
|
300
|
+
if response.status not in [200, 202]:
|
|
301
|
+
error_text = await response.text()
|
|
302
|
+
raise Exception(f"HTTP {response.status}: {error_text}")
|
|
303
|
+
|
|
304
|
+
# Wait for response from stream (with timeout)
|
|
305
|
+
result = await asyncio.wait_for(future, timeout=self.timeout)
|
|
306
|
+
|
|
307
|
+
# Check for JSON-RPC error
|
|
308
|
+
if 'error' in result:
|
|
309
|
+
error = result['error']
|
|
310
|
+
raise Exception(f"MCP Error: {error.get('message', str(error))}")
|
|
311
|
+
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
except asyncio.TimeoutError:
|
|
315
|
+
self._pending_requests.pop(request_id, None)
|
|
316
|
+
logger.error(f"[MCP SSE Client] Request timeout after {self.timeout}s")
|
|
317
|
+
raise Exception(f"SSE request timeout after {self.timeout}s")
|
|
318
|
+
except Exception as e:
|
|
319
|
+
self._pending_requests.pop(request_id, None)
|
|
320
|
+
logger.error(f"[MCP SSE Client] Request failed: {e}")
|
|
321
|
+
raise
|
|
322
|
+
|
|
323
|
+
async def close(self):
|
|
324
|
+
"""Close the persistent SSE stream."""
|
|
325
|
+
logger.info(f"[MCP SSE Client] Closing connection...")
|
|
326
|
+
|
|
327
|
+
# Cancel background stream reader task
|
|
328
|
+
if self._stream_task and not self._stream_task.done():
|
|
329
|
+
self._stream_task.cancel()
|
|
330
|
+
try:
|
|
331
|
+
await self._stream_task
|
|
332
|
+
except (asyncio.CancelledError, Exception) as e:
|
|
333
|
+
logger.debug(f"[MCP SSE Client] Stream task cleanup: {e}")
|
|
334
|
+
|
|
335
|
+
# Close response stream
|
|
336
|
+
if self._stream_response and not self._stream_response.closed:
|
|
337
|
+
try:
|
|
338
|
+
self._stream_response.close()
|
|
339
|
+
except Exception as e:
|
|
340
|
+
logger.debug(f"[MCP SSE Client] Response close error: {e}")
|
|
341
|
+
|
|
342
|
+
# Close session
|
|
343
|
+
if self._stream_session and not self._stream_session.closed:
|
|
344
|
+
try:
|
|
345
|
+
await self._stream_session.close()
|
|
346
|
+
# Give aiohttp time to cleanup
|
|
347
|
+
await asyncio.sleep(0.1)
|
|
348
|
+
except Exception as e:
|
|
349
|
+
logger.debug(f"[MCP SSE Client] Session close error: {e}")
|
|
350
|
+
|
|
351
|
+
logger.info(f"[MCP SSE Client] Connection closed")
|
|
352
|
+
|
|
353
|
+
async def __aenter__(self):
|
|
354
|
+
"""Async context manager entry."""
|
|
355
|
+
return self
|
|
356
|
+
|
|
357
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
358
|
+
"""Async context manager exit."""
|
|
359
|
+
await self.close()
|
|
360
|
+
|
|
361
|
+
async def initialize(self) -> Dict[str, Any]:
|
|
362
|
+
"""
|
|
363
|
+
Send initialize request to establish MCP protocol session.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Server capabilities and info
|
|
367
|
+
"""
|
|
368
|
+
response = await self.send_request(
|
|
369
|
+
method="initialize",
|
|
370
|
+
params={
|
|
371
|
+
"protocolVersion": "2024-11-05",
|
|
372
|
+
"capabilities": {
|
|
373
|
+
"roots": {"listChanged": True},
|
|
374
|
+
"sampling": {}
|
|
375
|
+
},
|
|
376
|
+
"clientInfo": {
|
|
377
|
+
"name": "ELITEA MCP Client",
|
|
378
|
+
"version": "1.0.0"
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
logger.info(f"[MCP SSE Client] MCP session initialized")
|
|
384
|
+
return response.get('result', {})
|
|
385
|
+
|
|
386
|
+
async def list_tools(self) -> list:
|
|
387
|
+
"""
|
|
388
|
+
Discover available tools from the MCP server.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
List of tool definitions
|
|
392
|
+
"""
|
|
393
|
+
response = await self.send_request(method="tools/list")
|
|
394
|
+
result = response.get('result', {})
|
|
395
|
+
tools = result.get('tools', [])
|
|
396
|
+
|
|
397
|
+
logger.info(f"[MCP SSE Client] Discovered {len(tools)} tools")
|
|
398
|
+
return tools
|
|
399
|
+
|
|
400
|
+
async def list_prompts(self) -> list:
|
|
401
|
+
"""
|
|
402
|
+
Discover available prompts from the MCP server.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
List of prompt definitions
|
|
406
|
+
"""
|
|
407
|
+
response = await self.send_request(method="prompts/list")
|
|
408
|
+
result = response.get('result', {})
|
|
409
|
+
prompts = result.get('prompts', [])
|
|
410
|
+
|
|
411
|
+
logger.debug(f"[MCP SSE Client] Discovered {len(prompts)} prompts")
|
|
412
|
+
return prompts
|
|
413
|
+
|
|
414
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
|
415
|
+
"""
|
|
416
|
+
Execute a tool on the MCP server.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
tool_name: Name of the tool to call
|
|
420
|
+
arguments: Tool arguments
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
Tool execution result
|
|
424
|
+
"""
|
|
425
|
+
response = await self.send_request(
|
|
426
|
+
method="tools/call",
|
|
427
|
+
params={
|
|
428
|
+
"name": tool_name,
|
|
429
|
+
"arguments": arguments
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
result = response.get('result', {})
|
|
434
|
+
return result
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Tools Discovery Utility.
|
|
3
|
+
Provides a standalone function to discover tools from remote MCP servers.
|
|
4
|
+
Supports both SSE (Server-Sent Events) and Streamable HTTP transports with auto-detection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from .mcp_oauth import McpAuthorizationRequired
|
|
12
|
+
from .mcp_client import McpClient
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def discover_mcp_tools(
|
|
18
|
+
url: str,
|
|
19
|
+
headers: Optional[Dict[str, str]] = None,
|
|
20
|
+
timeout: int = 60,
|
|
21
|
+
session_id: Optional[str] = None,
|
|
22
|
+
) -> List[Dict[str, Any]]:
|
|
23
|
+
"""
|
|
24
|
+
Discover available tools from a remote MCP server.
|
|
25
|
+
|
|
26
|
+
This function connects to a remote MCP server and retrieves the list of
|
|
27
|
+
available tools using the MCP protocol. Automatically detects and uses
|
|
28
|
+
the appropriate transport (SSE or Streamable HTTP).
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
url: MCP server HTTP URL (http:// or https://)
|
|
32
|
+
headers: Optional HTTP headers for authentication
|
|
33
|
+
timeout: Request timeout in seconds (default: 60)
|
|
34
|
+
session_id: Optional session ID for stateful connections
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of tool definitions, each containing:
|
|
38
|
+
- name: Tool name
|
|
39
|
+
- description: Tool description
|
|
40
|
+
- inputSchema: JSON schema for tool input parameters
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
McpAuthorizationRequired: If the server requires OAuth authorization (401)
|
|
44
|
+
Exception: For other connection or protocol errors
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
>>> tools = discover_mcp_tools(
|
|
48
|
+
... url="https://mcp.example.com/sse",
|
|
49
|
+
... headers={"Authorization": "Bearer token123"}
|
|
50
|
+
... )
|
|
51
|
+
>>> print(f"Found {len(tools)} tools")
|
|
52
|
+
"""
|
|
53
|
+
logger.info(f"[MCP Discovery] Starting tool discovery from {url}")
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# Run the async discovery in a new event loop
|
|
57
|
+
tools_list = asyncio.run(
|
|
58
|
+
_discover_tools_async(url, headers, timeout, session_id)
|
|
59
|
+
)
|
|
60
|
+
logger.info(f"[MCP Discovery] Successfully discovered {len(tools_list)} tools from {url}")
|
|
61
|
+
return tools_list
|
|
62
|
+
|
|
63
|
+
except McpAuthorizationRequired:
|
|
64
|
+
# Re-raise auth exceptions directly
|
|
65
|
+
logger.info(f"[MCP Discovery] Authorization required for {url}")
|
|
66
|
+
raise
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"[MCP Discovery] Failed to discover tools from {url}: {e}")
|
|
70
|
+
raise
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def _discover_tools_async(
|
|
74
|
+
url: str,
|
|
75
|
+
headers: Optional[Dict[str, str]],
|
|
76
|
+
timeout: int,
|
|
77
|
+
session_id: Optional[str],
|
|
78
|
+
) -> List[Dict[str, Any]]:
|
|
79
|
+
"""
|
|
80
|
+
Async implementation of tool discovery using unified MCP client.
|
|
81
|
+
"""
|
|
82
|
+
all_tools = []
|
|
83
|
+
|
|
84
|
+
# Create unified MCP client (auto-detects transport)
|
|
85
|
+
client = McpClient(
|
|
86
|
+
url=url,
|
|
87
|
+
session_id=session_id,
|
|
88
|
+
headers=headers,
|
|
89
|
+
timeout=timeout
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async with client:
|
|
93
|
+
# Initialize MCP session
|
|
94
|
+
await client.initialize()
|
|
95
|
+
logger.debug(f"[MCP Discovery] Session initialized (transport={client.detected_transport})")
|
|
96
|
+
|
|
97
|
+
# Get tools list
|
|
98
|
+
tools = await client.list_tools()
|
|
99
|
+
logger.debug(f"[MCP Discovery] Received {len(tools)} tools")
|
|
100
|
+
|
|
101
|
+
# Convert tools to standard format
|
|
102
|
+
for tool in tools:
|
|
103
|
+
tool_def = {
|
|
104
|
+
'name': tool.get('name'),
|
|
105
|
+
'description': tool.get('description', ''),
|
|
106
|
+
'inputSchema': tool.get('inputSchema', {}),
|
|
107
|
+
}
|
|
108
|
+
all_tools.append(tool_def)
|
|
109
|
+
|
|
110
|
+
return all_tools
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def discover_mcp_tools_async(
|
|
114
|
+
url: str,
|
|
115
|
+
headers: Optional[Dict[str, str]] = None,
|
|
116
|
+
timeout: int = 60,
|
|
117
|
+
session_id: Optional[str] = None,
|
|
118
|
+
) -> List[Dict[str, Any]]:
|
|
119
|
+
"""
|
|
120
|
+
Async version of discover_mcp_tools.
|
|
121
|
+
|
|
122
|
+
See discover_mcp_tools for full documentation.
|
|
123
|
+
"""
|
|
124
|
+
return await _discover_tools_async(url, headers, timeout, session_id)
|