alita-sdk 0.3.257__py3-none-any.whl → 0.3.562__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.
Files changed (278) hide show
  1. alita_sdk/cli/__init__.py +10 -0
  2. alita_sdk/cli/__main__.py +17 -0
  3. alita_sdk/cli/agent/__init__.py +5 -0
  4. alita_sdk/cli/agent/default.py +258 -0
  5. alita_sdk/cli/agent_executor.py +155 -0
  6. alita_sdk/cli/agent_loader.py +215 -0
  7. alita_sdk/cli/agent_ui.py +228 -0
  8. alita_sdk/cli/agents.py +3601 -0
  9. alita_sdk/cli/callbacks.py +647 -0
  10. alita_sdk/cli/cli.py +168 -0
  11. alita_sdk/cli/config.py +306 -0
  12. alita_sdk/cli/context/__init__.py +30 -0
  13. alita_sdk/cli/context/cleanup.py +198 -0
  14. alita_sdk/cli/context/manager.py +731 -0
  15. alita_sdk/cli/context/message.py +285 -0
  16. alita_sdk/cli/context/strategies.py +289 -0
  17. alita_sdk/cli/context/token_estimation.py +127 -0
  18. alita_sdk/cli/formatting.py +182 -0
  19. alita_sdk/cli/input_handler.py +419 -0
  20. alita_sdk/cli/inventory.py +1073 -0
  21. alita_sdk/cli/mcp_loader.py +315 -0
  22. alita_sdk/cli/toolkit.py +327 -0
  23. alita_sdk/cli/toolkit_loader.py +85 -0
  24. alita_sdk/cli/tools/__init__.py +43 -0
  25. alita_sdk/cli/tools/approval.py +224 -0
  26. alita_sdk/cli/tools/filesystem.py +1751 -0
  27. alita_sdk/cli/tools/planning.py +389 -0
  28. alita_sdk/cli/tools/terminal.py +414 -0
  29. alita_sdk/community/__init__.py +72 -12
  30. alita_sdk/community/inventory/__init__.py +236 -0
  31. alita_sdk/community/inventory/config.py +257 -0
  32. alita_sdk/community/inventory/enrichment.py +2137 -0
  33. alita_sdk/community/inventory/extractors.py +1469 -0
  34. alita_sdk/community/inventory/ingestion.py +3172 -0
  35. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  36. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  37. alita_sdk/community/inventory/parsers/base.py +295 -0
  38. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  39. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  40. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  41. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  42. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  43. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  44. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  45. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  46. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  47. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  48. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  49. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  50. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  51. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  52. alita_sdk/community/inventory/patterns/loader.py +348 -0
  53. alita_sdk/community/inventory/patterns/registry.py +198 -0
  54. alita_sdk/community/inventory/presets.py +535 -0
  55. alita_sdk/community/inventory/retrieval.py +1403 -0
  56. alita_sdk/community/inventory/toolkit.py +173 -0
  57. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  58. alita_sdk/community/inventory/visualize.py +1370 -0
  59. alita_sdk/configurations/__init__.py +11 -0
  60. alita_sdk/configurations/ado.py +148 -2
  61. alita_sdk/configurations/azure_search.py +1 -1
  62. alita_sdk/configurations/bigquery.py +1 -1
  63. alita_sdk/configurations/bitbucket.py +94 -2
  64. alita_sdk/configurations/browser.py +18 -0
  65. alita_sdk/configurations/carrier.py +19 -0
  66. alita_sdk/configurations/confluence.py +130 -1
  67. alita_sdk/configurations/delta_lake.py +1 -1
  68. alita_sdk/configurations/figma.py +76 -5
  69. alita_sdk/configurations/github.py +65 -1
  70. alita_sdk/configurations/gitlab.py +81 -0
  71. alita_sdk/configurations/google_places.py +17 -0
  72. alita_sdk/configurations/jira.py +103 -0
  73. alita_sdk/configurations/openapi.py +111 -0
  74. alita_sdk/configurations/postman.py +1 -1
  75. alita_sdk/configurations/qtest.py +72 -3
  76. alita_sdk/configurations/report_portal.py +115 -0
  77. alita_sdk/configurations/salesforce.py +19 -0
  78. alita_sdk/configurations/service_now.py +1 -12
  79. alita_sdk/configurations/sharepoint.py +167 -0
  80. alita_sdk/configurations/sonar.py +18 -0
  81. alita_sdk/configurations/sql.py +20 -0
  82. alita_sdk/configurations/testio.py +101 -0
  83. alita_sdk/configurations/testrail.py +88 -0
  84. alita_sdk/configurations/xray.py +94 -1
  85. alita_sdk/configurations/zephyr_enterprise.py +94 -1
  86. alita_sdk/configurations/zephyr_essential.py +95 -0
  87. alita_sdk/runtime/clients/artifact.py +21 -4
  88. alita_sdk/runtime/clients/client.py +458 -67
  89. alita_sdk/runtime/clients/mcp_discovery.py +342 -0
  90. alita_sdk/runtime/clients/mcp_manager.py +262 -0
  91. alita_sdk/runtime/clients/sandbox_client.py +352 -0
  92. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  93. alita_sdk/runtime/langchain/assistant.py +183 -43
  94. alita_sdk/runtime/langchain/constants.py +647 -1
  95. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  96. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
  97. alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
  98. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  99. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
  100. alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
  101. alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
  102. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
  103. alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
  104. alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
  105. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
  106. alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
  107. alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
  108. alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
  109. alita_sdk/runtime/langchain/langraph_agent.py +407 -92
  110. alita_sdk/runtime/langchain/utils.py +102 -8
  111. alita_sdk/runtime/llms/preloaded.py +2 -6
  112. alita_sdk/runtime/models/mcp_models.py +61 -0
  113. alita_sdk/runtime/skills/__init__.py +91 -0
  114. alita_sdk/runtime/skills/callbacks.py +498 -0
  115. alita_sdk/runtime/skills/discovery.py +540 -0
  116. alita_sdk/runtime/skills/executor.py +610 -0
  117. alita_sdk/runtime/skills/input_builder.py +371 -0
  118. alita_sdk/runtime/skills/models.py +330 -0
  119. alita_sdk/runtime/skills/registry.py +355 -0
  120. alita_sdk/runtime/skills/skill_runner.py +330 -0
  121. alita_sdk/runtime/toolkits/__init__.py +28 -0
  122. alita_sdk/runtime/toolkits/application.py +14 -4
  123. alita_sdk/runtime/toolkits/artifact.py +24 -9
  124. alita_sdk/runtime/toolkits/datasource.py +13 -6
  125. alita_sdk/runtime/toolkits/mcp.py +780 -0
  126. alita_sdk/runtime/toolkits/planning.py +178 -0
  127. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  128. alita_sdk/runtime/toolkits/subgraph.py +11 -6
  129. alita_sdk/runtime/toolkits/tools.py +314 -70
  130. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  131. alita_sdk/runtime/tools/__init__.py +24 -0
  132. alita_sdk/runtime/tools/application.py +16 -4
  133. alita_sdk/runtime/tools/artifact.py +367 -33
  134. alita_sdk/runtime/tools/data_analysis.py +183 -0
  135. alita_sdk/runtime/tools/function.py +100 -4
  136. alita_sdk/runtime/tools/graph.py +81 -0
  137. alita_sdk/runtime/tools/image_generation.py +218 -0
  138. alita_sdk/runtime/tools/llm.py +1013 -177
  139. alita_sdk/runtime/tools/loop.py +3 -1
  140. alita_sdk/runtime/tools/loop_output.py +3 -1
  141. alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
  142. alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
  143. alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
  144. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  145. alita_sdk/runtime/tools/planning/models.py +246 -0
  146. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  147. alita_sdk/runtime/tools/router.py +2 -1
  148. alita_sdk/runtime/tools/sandbox.py +375 -0
  149. alita_sdk/runtime/tools/skill_router.py +776 -0
  150. alita_sdk/runtime/tools/tool.py +3 -1
  151. alita_sdk/runtime/tools/vectorstore.py +69 -65
  152. alita_sdk/runtime/tools/vectorstore_base.py +163 -90
  153. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  154. alita_sdk/runtime/utils/mcp_client.py +492 -0
  155. alita_sdk/runtime/utils/mcp_oauth.py +361 -0
  156. alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
  157. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  158. alita_sdk/runtime/utils/streamlit.py +41 -14
  159. alita_sdk/runtime/utils/toolkit_utils.py +28 -9
  160. alita_sdk/runtime/utils/utils.py +48 -0
  161. alita_sdk/tools/__init__.py +135 -37
  162. alita_sdk/tools/ado/__init__.py +2 -2
  163. alita_sdk/tools/ado/repos/__init__.py +15 -19
  164. alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
  165. alita_sdk/tools/ado/test_plan/__init__.py +26 -8
  166. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
  167. alita_sdk/tools/ado/wiki/__init__.py +27 -12
  168. alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
  169. alita_sdk/tools/ado/work_item/__init__.py +27 -12
  170. alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
  171. alita_sdk/tools/advanced_jira_mining/__init__.py +12 -8
  172. alita_sdk/tools/aws/delta_lake/__init__.py +14 -11
  173. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  174. alita_sdk/tools/azure_ai/search/__init__.py +13 -8
  175. alita_sdk/tools/base/tool.py +5 -1
  176. alita_sdk/tools/base_indexer_toolkit.py +454 -110
  177. alita_sdk/tools/bitbucket/__init__.py +27 -19
  178. alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
  179. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
  180. alita_sdk/tools/browser/__init__.py +41 -16
  181. alita_sdk/tools/browser/crawler.py +3 -1
  182. alita_sdk/tools/browser/utils.py +15 -6
  183. alita_sdk/tools/carrier/__init__.py +18 -17
  184. alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
  185. alita_sdk/tools/carrier/excel_reporter.py +8 -4
  186. alita_sdk/tools/chunkers/__init__.py +3 -1
  187. alita_sdk/tools/chunkers/code/codeparser.py +1 -1
  188. alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
  189. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  190. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  191. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  192. alita_sdk/tools/cloud/aws/__init__.py +11 -7
  193. alita_sdk/tools/cloud/azure/__init__.py +11 -7
  194. alita_sdk/tools/cloud/gcp/__init__.py +11 -7
  195. alita_sdk/tools/cloud/k8s/__init__.py +11 -7
  196. alita_sdk/tools/code/linter/__init__.py +9 -8
  197. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  198. alita_sdk/tools/code/sonar/__init__.py +20 -13
  199. alita_sdk/tools/code_indexer_toolkit.py +199 -0
  200. alita_sdk/tools/confluence/__init__.py +21 -14
  201. alita_sdk/tools/confluence/api_wrapper.py +197 -58
  202. alita_sdk/tools/confluence/loader.py +14 -2
  203. alita_sdk/tools/custom_open_api/__init__.py +11 -5
  204. alita_sdk/tools/elastic/__init__.py +10 -8
  205. alita_sdk/tools/elitea_base.py +546 -64
  206. alita_sdk/tools/figma/__init__.py +11 -8
  207. alita_sdk/tools/figma/api_wrapper.py +352 -153
  208. alita_sdk/tools/github/__init__.py +17 -17
  209. alita_sdk/tools/github/api_wrapper.py +9 -26
  210. alita_sdk/tools/github/github_client.py +81 -12
  211. alita_sdk/tools/github/schemas.py +2 -1
  212. alita_sdk/tools/github/tool.py +5 -1
  213. alita_sdk/tools/gitlab/__init__.py +18 -13
  214. alita_sdk/tools/gitlab/api_wrapper.py +224 -80
  215. alita_sdk/tools/gitlab_org/__init__.py +13 -10
  216. alita_sdk/tools/google/bigquery/__init__.py +13 -13
  217. alita_sdk/tools/google/bigquery/tool.py +5 -1
  218. alita_sdk/tools/google_places/__init__.py +20 -11
  219. alita_sdk/tools/jira/__init__.py +21 -11
  220. alita_sdk/tools/jira/api_wrapper.py +315 -168
  221. alita_sdk/tools/keycloak/__init__.py +10 -8
  222. alita_sdk/tools/localgit/__init__.py +8 -3
  223. alita_sdk/tools/localgit/local_git.py +62 -54
  224. alita_sdk/tools/localgit/tool.py +5 -1
  225. alita_sdk/tools/memory/__init__.py +38 -14
  226. alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
  227. alita_sdk/tools/ocr/__init__.py +10 -8
  228. alita_sdk/tools/openapi/__init__.py +281 -108
  229. alita_sdk/tools/openapi/api_wrapper.py +883 -0
  230. alita_sdk/tools/openapi/tool.py +20 -0
  231. alita_sdk/tools/pandas/__init__.py +18 -11
  232. alita_sdk/tools/pandas/api_wrapper.py +40 -45
  233. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  234. alita_sdk/tools/postman/__init__.py +10 -11
  235. alita_sdk/tools/postman/api_wrapper.py +19 -8
  236. alita_sdk/tools/postman/postman_analysis.py +8 -1
  237. alita_sdk/tools/pptx/__init__.py +10 -10
  238. alita_sdk/tools/qtest/__init__.py +21 -14
  239. alita_sdk/tools/qtest/api_wrapper.py +1784 -88
  240. alita_sdk/tools/rally/__init__.py +12 -10
  241. alita_sdk/tools/report_portal/__init__.py +22 -16
  242. alita_sdk/tools/salesforce/__init__.py +21 -16
  243. alita_sdk/tools/servicenow/__init__.py +20 -16
  244. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  245. alita_sdk/tools/sharepoint/__init__.py +16 -14
  246. alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
  247. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  248. alita_sdk/tools/sharepoint/utils.py +8 -2
  249. alita_sdk/tools/slack/__init__.py +11 -7
  250. alita_sdk/tools/sql/__init__.py +21 -19
  251. alita_sdk/tools/sql/api_wrapper.py +71 -23
  252. alita_sdk/tools/testio/__init__.py +20 -13
  253. alita_sdk/tools/testrail/__init__.py +12 -11
  254. alita_sdk/tools/testrail/api_wrapper.py +214 -46
  255. alita_sdk/tools/utils/__init__.py +28 -4
  256. alita_sdk/tools/utils/content_parser.py +182 -62
  257. alita_sdk/tools/utils/text_operations.py +254 -0
  258. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
  259. alita_sdk/tools/xray/__init__.py +17 -14
  260. alita_sdk/tools/xray/api_wrapper.py +58 -113
  261. alita_sdk/tools/yagmail/__init__.py +8 -3
  262. alita_sdk/tools/zephyr/__init__.py +11 -7
  263. alita_sdk/tools/zephyr_enterprise/__init__.py +15 -9
  264. alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
  265. alita_sdk/tools/zephyr_essential/__init__.py +15 -10
  266. alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
  267. alita_sdk/tools/zephyr_essential/client.py +6 -4
  268. alita_sdk/tools/zephyr_scale/__init__.py +12 -8
  269. alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
  270. alita_sdk/tools/zephyr_squad/__init__.py +11 -7
  271. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/METADATA +184 -37
  272. alita_sdk-0.3.562.dist-info/RECORD +450 -0
  273. alita_sdk-0.3.562.dist-info/entry_points.txt +2 -0
  274. alita_sdk/tools/bitbucket/tools.py +0 -304
  275. alita_sdk-0.3.257.dist-info/RECORD +0 -343
  276. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/WHEEL +0 -0
  277. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/licenses/LICENSE +0 -0
  278. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.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": "Alita 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)
@@ -287,7 +287,6 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
287
287
  model_config={
288
288
  "temperature": 0.1,
289
289
  "max_tokens": 1000,
290
- "top_p": 1.0
291
290
  }
292
291
  )
293
292
  except Exception as e:
@@ -868,10 +867,24 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
868
867
  label = f"{'🔒 ' if is_secret else ''}{'*' if is_required else ''}{field_name.replace('_', ' ').title()}"
869
868
 
870
869
  if field_type == 'string':
871
- if is_secret:
870
+ # Check if this is an enum field
871
+ if field_schema.get('enum'):
872
+ # Dropdown for enum values
873
+ options = field_schema['enum']
874
+ default_index = 0
875
+ if default_value and str(default_value) in options:
876
+ default_index = options.index(str(default_value))
877
+ toolkit_config_values[field_name] = st.selectbox(
878
+ label,
879
+ options=options,
880
+ index=default_index,
881
+ help=field_description,
882
+ key=f"config_{field_name}_{selected_toolkit_idx}"
883
+ )
884
+ elif is_secret:
872
885
  toolkit_config_values[field_name] = st.text_input(
873
886
  label,
874
- value=str(default_value) if default_value else '',
887
+ value=str(default_value) if default_value else '',
875
888
  help=field_description,
876
889
  type="password",
877
890
  key=f"config_{field_name}_{selected_toolkit_idx}"
@@ -879,7 +892,7 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
879
892
  else:
880
893
  toolkit_config_values[field_name] = st.text_input(
881
894
  label,
882
- value=str(default_value) if default_value else '',
895
+ value=str(default_value) if default_value else '',
883
896
  help=field_description,
884
897
  key=f"config_{field_name}_{selected_toolkit_idx}"
885
898
  )
@@ -971,6 +984,23 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
971
984
  key=f"config_{field_name}_{selected_toolkit_idx}"
972
985
  )
973
986
  toolkit_config_values[field_name] = [line.strip() for line in array_input.split('\n') if line.strip()]
987
+ elif field_type == 'object':
988
+ # Handle object/dict types (like headers)
989
+ obj_input = st.text_area(
990
+ f"{label} (JSON object)",
991
+ value=json.dumps(default_value) if isinstance(default_value, dict) else str(default_value) if default_value else '',
992
+ help=f"{field_description} - Enter as JSON object, e.g. {{\"Authorization\": \"Bearer token\"}}",
993
+ placeholder='{"key": "value"}',
994
+ key=f"config_{field_name}_{selected_toolkit_idx}"
995
+ )
996
+ try:
997
+ if obj_input.strip():
998
+ toolkit_config_values[field_name] = json.loads(obj_input)
999
+ else:
1000
+ toolkit_config_values[field_name] = None
1001
+ except json.JSONDecodeError as e:
1002
+ st.error(f"Invalid JSON format for {field_name}: {e}")
1003
+ toolkit_config_values[field_name] = None
974
1004
  else:
975
1005
  st.info("This toolkit doesn't require additional configuration.")
976
1006
 
@@ -1102,7 +1132,7 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
1102
1132
  st_cb = AlitaStreamlitCallback(st)
1103
1133
  logger.info(st.session_state.messages)
1104
1134
  response = st.session_state.agent_executor.invoke(
1105
- {"input": prompt, "chat_history": st.session_state.messages[:-1]},
1135
+ {"input": [prompt], "chat_history": st.session_state.messages[:-1]},
1106
1136
  { 'callbacks': [st_cb], 'configurable': {"thread_id": st.session_state.thread_id}}
1107
1137
  )
1108
1138
  st.write(response["output"])
@@ -1225,7 +1255,6 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
1225
1255
  model_config={
1226
1256
  "temperature": 0.1,
1227
1257
  "max_tokens": 1000,
1228
- "top_p": 1.0
1229
1258
  }
1230
1259
  )
1231
1260
  except Exception as e:
@@ -1356,20 +1385,18 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
1356
1385
  help="Maximum number of tokens in the AI response"
1357
1386
  )
1358
1387
 
1359
- top_p = st.slider(
1360
- "Top-p:",
1361
- min_value=0.1,
1362
- max_value=1.0,
1363
- value=1.0,
1364
- step=0.1,
1365
- help="Controls diversity via nucleus sampling"
1388
+ reasoning_effort = st.selectbox(
1389
+ "Reasoning effort:",
1390
+ options=['null', 'low', 'medium', 'high'],
1391
+ index=0,
1392
+ help="Higher effort better reasoning, slower response"
1366
1393
  )
1367
1394
 
1368
1395
  # Create LLM config
1369
1396
  llm_config = {
1370
1397
  'max_tokens': max_tokens,
1371
1398
  'temperature': temperature,
1372
- 'top_p': top_p
1399
+ 'reasoning_effort': reasoning_effort
1373
1400
  }
1374
1401
 
1375
1402
  col1, col2 = st.columns([3, 1])