alita-sdk 0.3.462__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.
Files changed (261) hide show
  1. alita_sdk/cli/agent/__init__.py +5 -0
  2. alita_sdk/cli/agent/default.py +258 -0
  3. alita_sdk/cli/agent_executor.py +15 -3
  4. alita_sdk/cli/agent_loader.py +56 -8
  5. alita_sdk/cli/agent_ui.py +93 -31
  6. alita_sdk/cli/agents.py +2274 -230
  7. alita_sdk/cli/callbacks.py +96 -25
  8. alita_sdk/cli/cli.py +10 -1
  9. alita_sdk/cli/config.py +162 -9
  10. alita_sdk/cli/context/__init__.py +30 -0
  11. alita_sdk/cli/context/cleanup.py +198 -0
  12. alita_sdk/cli/context/manager.py +731 -0
  13. alita_sdk/cli/context/message.py +285 -0
  14. alita_sdk/cli/context/strategies.py +289 -0
  15. alita_sdk/cli/context/token_estimation.py +127 -0
  16. alita_sdk/cli/input_handler.py +419 -0
  17. alita_sdk/cli/inventory.py +1073 -0
  18. alita_sdk/cli/testcases/__init__.py +94 -0
  19. alita_sdk/cli/testcases/data_generation.py +119 -0
  20. alita_sdk/cli/testcases/discovery.py +96 -0
  21. alita_sdk/cli/testcases/executor.py +84 -0
  22. alita_sdk/cli/testcases/logger.py +85 -0
  23. alita_sdk/cli/testcases/parser.py +172 -0
  24. alita_sdk/cli/testcases/prompts.py +91 -0
  25. alita_sdk/cli/testcases/reporting.py +125 -0
  26. alita_sdk/cli/testcases/setup.py +108 -0
  27. alita_sdk/cli/testcases/test_runner.py +282 -0
  28. alita_sdk/cli/testcases/utils.py +39 -0
  29. alita_sdk/cli/testcases/validation.py +90 -0
  30. alita_sdk/cli/testcases/workflow.py +196 -0
  31. alita_sdk/cli/toolkit.py +14 -17
  32. alita_sdk/cli/toolkit_loader.py +35 -5
  33. alita_sdk/cli/tools/__init__.py +36 -2
  34. alita_sdk/cli/tools/approval.py +224 -0
  35. alita_sdk/cli/tools/filesystem.py +910 -64
  36. alita_sdk/cli/tools/planning.py +389 -0
  37. alita_sdk/cli/tools/terminal.py +414 -0
  38. alita_sdk/community/__init__.py +72 -12
  39. alita_sdk/community/inventory/__init__.py +236 -0
  40. alita_sdk/community/inventory/config.py +257 -0
  41. alita_sdk/community/inventory/enrichment.py +2137 -0
  42. alita_sdk/community/inventory/extractors.py +1469 -0
  43. alita_sdk/community/inventory/ingestion.py +3172 -0
  44. alita_sdk/community/inventory/knowledge_graph.py +1457 -0
  45. alita_sdk/community/inventory/parsers/__init__.py +218 -0
  46. alita_sdk/community/inventory/parsers/base.py +295 -0
  47. alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
  48. alita_sdk/community/inventory/parsers/go_parser.py +851 -0
  49. alita_sdk/community/inventory/parsers/html_parser.py +389 -0
  50. alita_sdk/community/inventory/parsers/java_parser.py +593 -0
  51. alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
  52. alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
  53. alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
  54. alita_sdk/community/inventory/parsers/python_parser.py +604 -0
  55. alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
  56. alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
  57. alita_sdk/community/inventory/parsers/text_parser.py +322 -0
  58. alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
  59. alita_sdk/community/inventory/patterns/__init__.py +61 -0
  60. alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
  61. alita_sdk/community/inventory/patterns/loader.py +348 -0
  62. alita_sdk/community/inventory/patterns/registry.py +198 -0
  63. alita_sdk/community/inventory/presets.py +535 -0
  64. alita_sdk/community/inventory/retrieval.py +1403 -0
  65. alita_sdk/community/inventory/toolkit.py +173 -0
  66. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  67. alita_sdk/community/inventory/visualize.py +1370 -0
  68. alita_sdk/configurations/__init__.py +1 -1
  69. alita_sdk/configurations/ado.py +141 -20
  70. alita_sdk/configurations/bitbucket.py +0 -3
  71. alita_sdk/configurations/confluence.py +76 -42
  72. alita_sdk/configurations/figma.py +76 -0
  73. alita_sdk/configurations/gitlab.py +17 -5
  74. alita_sdk/configurations/openapi.py +329 -0
  75. alita_sdk/configurations/qtest.py +72 -1
  76. alita_sdk/configurations/report_portal.py +96 -0
  77. alita_sdk/configurations/sharepoint.py +148 -0
  78. alita_sdk/configurations/testio.py +83 -0
  79. alita_sdk/runtime/clients/artifact.py +3 -3
  80. alita_sdk/runtime/clients/client.py +353 -48
  81. alita_sdk/runtime/clients/sandbox_client.py +0 -21
  82. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  83. alita_sdk/runtime/langchain/assistant.py +123 -26
  84. alita_sdk/runtime/langchain/constants.py +642 -1
  85. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
  86. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  87. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +6 -3
  88. alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
  89. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
  90. alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
  91. alita_sdk/runtime/langchain/langraph_agent.py +279 -73
  92. alita_sdk/runtime/langchain/utils.py +82 -15
  93. alita_sdk/runtime/llms/preloaded.py +2 -6
  94. alita_sdk/runtime/skills/__init__.py +91 -0
  95. alita_sdk/runtime/skills/callbacks.py +498 -0
  96. alita_sdk/runtime/skills/discovery.py +540 -0
  97. alita_sdk/runtime/skills/executor.py +610 -0
  98. alita_sdk/runtime/skills/input_builder.py +371 -0
  99. alita_sdk/runtime/skills/models.py +330 -0
  100. alita_sdk/runtime/skills/registry.py +355 -0
  101. alita_sdk/runtime/skills/skill_runner.py +330 -0
  102. alita_sdk/runtime/toolkits/__init__.py +7 -0
  103. alita_sdk/runtime/toolkits/application.py +21 -9
  104. alita_sdk/runtime/toolkits/artifact.py +15 -5
  105. alita_sdk/runtime/toolkits/datasource.py +13 -6
  106. alita_sdk/runtime/toolkits/mcp.py +139 -251
  107. alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
  108. alita_sdk/runtime/toolkits/planning.py +178 -0
  109. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  110. alita_sdk/runtime/toolkits/subgraph.py +251 -6
  111. alita_sdk/runtime/toolkits/tools.py +238 -32
  112. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  113. alita_sdk/runtime/tools/__init__.py +3 -1
  114. alita_sdk/runtime/tools/application.py +20 -6
  115. alita_sdk/runtime/tools/artifact.py +511 -28
  116. alita_sdk/runtime/tools/data_analysis.py +183 -0
  117. alita_sdk/runtime/tools/function.py +43 -15
  118. alita_sdk/runtime/tools/image_generation.py +50 -44
  119. alita_sdk/runtime/tools/llm.py +852 -67
  120. alita_sdk/runtime/tools/loop.py +3 -1
  121. alita_sdk/runtime/tools/loop_output.py +3 -1
  122. alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
  123. alita_sdk/runtime/tools/mcp_server_tool.py +7 -6
  124. alita_sdk/runtime/tools/planning/__init__.py +36 -0
  125. alita_sdk/runtime/tools/planning/models.py +246 -0
  126. alita_sdk/runtime/tools/planning/wrapper.py +607 -0
  127. alita_sdk/runtime/tools/router.py +2 -4
  128. alita_sdk/runtime/tools/sandbox.py +9 -6
  129. alita_sdk/runtime/tools/skill_router.py +776 -0
  130. alita_sdk/runtime/tools/tool.py +3 -1
  131. alita_sdk/runtime/tools/vectorstore.py +7 -2
  132. alita_sdk/runtime/tools/vectorstore_base.py +51 -11
  133. alita_sdk/runtime/utils/AlitaCallback.py +137 -21
  134. alita_sdk/runtime/utils/constants.py +5 -1
  135. alita_sdk/runtime/utils/mcp_client.py +492 -0
  136. alita_sdk/runtime/utils/mcp_oauth.py +202 -5
  137. alita_sdk/runtime/utils/mcp_sse_client.py +36 -7
  138. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  139. alita_sdk/runtime/utils/serialization.py +155 -0
  140. alita_sdk/runtime/utils/streamlit.py +6 -10
  141. alita_sdk/runtime/utils/toolkit_utils.py +16 -5
  142. alita_sdk/runtime/utils/utils.py +36 -0
  143. alita_sdk/tools/__init__.py +113 -29
  144. alita_sdk/tools/ado/repos/__init__.py +51 -33
  145. alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
  146. alita_sdk/tools/ado/test_plan/__init__.py +25 -9
  147. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
  148. alita_sdk/tools/ado/utils.py +1 -18
  149. alita_sdk/tools/ado/wiki/__init__.py +25 -8
  150. alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
  151. alita_sdk/tools/ado/work_item/__init__.py +26 -9
  152. alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
  153. alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
  154. alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
  155. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  156. alita_sdk/tools/azure_ai/search/__init__.py +11 -8
  157. alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
  158. alita_sdk/tools/base/tool.py +5 -1
  159. alita_sdk/tools/base_indexer_toolkit.py +170 -45
  160. alita_sdk/tools/bitbucket/__init__.py +17 -12
  161. alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
  162. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
  163. alita_sdk/tools/browser/__init__.py +5 -4
  164. alita_sdk/tools/carrier/__init__.py +5 -6
  165. alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
  166. alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
  167. alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
  168. alita_sdk/tools/chunkers/__init__.py +3 -1
  169. alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
  170. alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
  171. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  172. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  173. alita_sdk/tools/cloud/aws/__init__.py +10 -7
  174. alita_sdk/tools/cloud/azure/__init__.py +10 -7
  175. alita_sdk/tools/cloud/gcp/__init__.py +10 -7
  176. alita_sdk/tools/cloud/k8s/__init__.py +10 -7
  177. alita_sdk/tools/code/linter/__init__.py +10 -8
  178. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  179. alita_sdk/tools/code/sonar/__init__.py +10 -7
  180. alita_sdk/tools/code_indexer_toolkit.py +73 -23
  181. alita_sdk/tools/confluence/__init__.py +21 -15
  182. alita_sdk/tools/confluence/api_wrapper.py +78 -23
  183. alita_sdk/tools/confluence/loader.py +4 -2
  184. alita_sdk/tools/custom_open_api/__init__.py +12 -5
  185. alita_sdk/tools/elastic/__init__.py +11 -8
  186. alita_sdk/tools/elitea_base.py +493 -30
  187. alita_sdk/tools/figma/__init__.py +58 -11
  188. alita_sdk/tools/figma/api_wrapper.py +1235 -143
  189. alita_sdk/tools/figma/figma_client.py +73 -0
  190. alita_sdk/tools/figma/toon_tools.py +2748 -0
  191. alita_sdk/tools/github/__init__.py +13 -14
  192. alita_sdk/tools/github/github_client.py +224 -100
  193. alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
  194. alita_sdk/tools/github/schemas.py +14 -5
  195. alita_sdk/tools/github/tool.py +5 -1
  196. alita_sdk/tools/github/tool_prompts.py +9 -22
  197. alita_sdk/tools/gitlab/__init__.py +15 -11
  198. alita_sdk/tools/gitlab/api_wrapper.py +207 -41
  199. alita_sdk/tools/gitlab_org/__init__.py +10 -8
  200. alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
  201. alita_sdk/tools/google/bigquery/__init__.py +13 -12
  202. alita_sdk/tools/google/bigquery/tool.py +5 -1
  203. alita_sdk/tools/google_places/__init__.py +10 -8
  204. alita_sdk/tools/google_places/api_wrapper.py +1 -1
  205. alita_sdk/tools/jira/__init__.py +17 -11
  206. alita_sdk/tools/jira/api_wrapper.py +91 -40
  207. alita_sdk/tools/keycloak/__init__.py +11 -8
  208. alita_sdk/tools/localgit/__init__.py +9 -3
  209. alita_sdk/tools/localgit/local_git.py +62 -54
  210. alita_sdk/tools/localgit/tool.py +5 -1
  211. alita_sdk/tools/memory/__init__.py +11 -3
  212. alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
  213. alita_sdk/tools/ocr/__init__.py +11 -8
  214. alita_sdk/tools/openapi/__init__.py +490 -114
  215. alita_sdk/tools/openapi/api_wrapper.py +1368 -0
  216. alita_sdk/tools/openapi/tool.py +20 -0
  217. alita_sdk/tools/pandas/__init__.py +20 -12
  218. alita_sdk/tools/pandas/api_wrapper.py +38 -25
  219. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  220. alita_sdk/tools/postman/__init__.py +11 -11
  221. alita_sdk/tools/pptx/__init__.py +10 -9
  222. alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
  223. alita_sdk/tools/qtest/__init__.py +30 -10
  224. alita_sdk/tools/qtest/api_wrapper.py +430 -13
  225. alita_sdk/tools/rally/__init__.py +10 -8
  226. alita_sdk/tools/rally/api_wrapper.py +1 -1
  227. alita_sdk/tools/report_portal/__init__.py +12 -9
  228. alita_sdk/tools/salesforce/__init__.py +10 -9
  229. alita_sdk/tools/servicenow/__init__.py +17 -14
  230. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  231. alita_sdk/tools/sharepoint/__init__.py +10 -8
  232. alita_sdk/tools/sharepoint/api_wrapper.py +4 -4
  233. alita_sdk/tools/slack/__init__.py +10 -8
  234. alita_sdk/tools/slack/api_wrapper.py +2 -2
  235. alita_sdk/tools/sql/__init__.py +11 -9
  236. alita_sdk/tools/testio/__init__.py +10 -8
  237. alita_sdk/tools/testrail/__init__.py +11 -8
  238. alita_sdk/tools/testrail/api_wrapper.py +1 -1
  239. alita_sdk/tools/utils/__init__.py +9 -4
  240. alita_sdk/tools/utils/content_parser.py +77 -3
  241. alita_sdk/tools/utils/text_operations.py +410 -0
  242. alita_sdk/tools/utils/tool_prompts.py +79 -0
  243. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +17 -13
  244. alita_sdk/tools/xray/__init__.py +12 -9
  245. alita_sdk/tools/yagmail/__init__.py +9 -3
  246. alita_sdk/tools/zephyr/__init__.py +9 -7
  247. alita_sdk/tools/zephyr_enterprise/__init__.py +11 -8
  248. alita_sdk/tools/zephyr_essential/__init__.py +10 -8
  249. alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
  250. alita_sdk/tools/zephyr_essential/client.py +2 -2
  251. alita_sdk/tools/zephyr_scale/__init__.py +11 -9
  252. alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
  253. alita_sdk/tools/zephyr_squad/__init__.py +10 -8
  254. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +147 -7
  255. alita_sdk-0.3.627.dist-info/RECORD +468 -0
  256. alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
  257. alita_sdk-0.3.462.dist-info/RECORD +0 -384
  258. alita_sdk-0.3.462.dist-info/entry_points.txt +0 -2
  259. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
  260. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
  261. {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
@@ -43,6 +43,23 @@ class McpAuthorizationRequired(ToolException):
43
43
  }
44
44
 
45
45
 
46
+ def extract_authorization_uri(www_authenticate: Optional[str]) -> Optional[str]:
47
+ """
48
+ Extract authorization_uri from WWW-Authenticate header.
49
+ This points directly to the OAuth authorization server metadata URL.
50
+ Should be used before falling back to resource_metadata.
51
+ """
52
+ if not www_authenticate:
53
+ return None
54
+
55
+ # Look for authorization_uri="<url>" in the header
56
+ match = re.search(r'authorization_uri\s*=\s*\"?([^\", ]+)\"?', www_authenticate)
57
+ if match:
58
+ return match.group(1)
59
+
60
+ return None
61
+
62
+
46
63
  def extract_resource_metadata_url(www_authenticate: Optional[str], server_url: Optional[str] = None) -> Optional[str]:
47
64
  """
48
65
  Pull the resource_metadata URL from a WWW-Authenticate header if present.
@@ -62,15 +79,33 @@ def extract_resource_metadata_url(www_authenticate: Optional[str], server_url: O
62
79
  # or using well-known OAuth discovery endpoints directly
63
80
  return None
64
81
 
65
-
66
- def fetch_oauth_authorization_server_metadata(base_url: str, timeout: int = 10) -> Optional[Dict[str, Any]]:
82
+ def fetch_oauth_authorization_server_metadata(url: str, timeout: int = 10) -> Optional[Dict[str, Any]]:
67
83
  """
68
84
  Fetch OAuth authorization server metadata from well-known endpoints.
69
- Tries both oauth-authorization-server and openid-configuration discovery endpoints.
85
+
86
+ Args:
87
+ url: Either a full well-known URL (e.g., https://api.figma.com/.well-known/oauth-authorization-server)
88
+ or a base URL (e.g., https://api.figma.com) where we'll try discovery endpoints.
89
+ timeout: Request timeout in seconds.
90
+
91
+ Returns:
92
+ OAuth authorization server metadata dict, or None if not found.
70
93
  """
94
+ # If the URL is already a .well-known endpoint, try it directly first
95
+ if '/.well-known/' in url:
96
+ try:
97
+ resp = requests.get(url, timeout=timeout)
98
+ if resp.status_code == 200:
99
+ return resp.json()
100
+ except Exception as exc:
101
+ logger.debug(f"Failed to fetch OAuth metadata from {url}: {exc}")
102
+ # If direct fetch failed, don't try other endpoints
103
+ return None
104
+
105
+ # Otherwise, try standard discovery endpoints
71
106
  discovery_endpoints = [
72
- f"{base_url}/.well-known/oauth-authorization-server",
73
- f"{base_url}/.well-known/openid-configuration",
107
+ f"{url}/.well-known/oauth-authorization-server",
108
+ f"{url}/.well-known/openid-configuration",
74
109
  ]
75
110
 
76
111
  for endpoint in discovery_endpoints:
@@ -162,3 +197,165 @@ def canonical_resource(server_url: str) -> str:
162
197
  if resource.endswith("/") and parsed.path in ("", "/"):
163
198
  resource = resource[:-1]
164
199
  return resource
200
+
201
+
202
+ def exchange_oauth_token(
203
+ token_endpoint: str,
204
+ code: str,
205
+ redirect_uri: str,
206
+ client_id: Optional[str] = None,
207
+ client_secret: Optional[str] = None,
208
+ code_verifier: Optional[str] = None,
209
+ scope: Optional[str] = None,
210
+ timeout: int = 30,
211
+ ) -> Dict[str, Any]:
212
+ """
213
+ Exchange an OAuth authorization code for access tokens.
214
+
215
+ This function performs the OAuth token exchange on the server side,
216
+ avoiding CORS issues that would occur if done from a browser.
217
+
218
+ Args:
219
+ token_endpoint: OAuth token endpoint URL
220
+ code: Authorization code from OAuth provider
221
+ redirect_uri: Redirect URI used in authorization request
222
+ client_id: OAuth client ID (optional for DCR/public clients)
223
+ client_secret: OAuth client secret (optional for public clients)
224
+ code_verifier: PKCE code verifier (optional)
225
+ scope: OAuth scope (optional)
226
+ timeout: Request timeout in seconds
227
+
228
+ Returns:
229
+ Token response from OAuth provider containing access_token, etc.
230
+
231
+ Raises:
232
+ requests.RequestException: If the HTTP request fails
233
+ ValueError: If the token exchange fails
234
+
235
+ Note:
236
+ client_id may be optional for:
237
+ - Dynamic Client Registration (DCR): client_id may be in the code
238
+ - OIDC public clients: some providers don't require it
239
+ - Some MCP servers handle auth differently
240
+ """
241
+ # Build the token request body
242
+ token_body = {
243
+ "grant_type": "authorization_code",
244
+ "code": code,
245
+ "redirect_uri": redirect_uri,
246
+ }
247
+
248
+ if client_id:
249
+ token_body["client_id"] = client_id
250
+ if client_secret:
251
+ token_body["client_secret"] = client_secret
252
+ if code_verifier:
253
+ token_body["code_verifier"] = code_verifier
254
+ if scope:
255
+ token_body["scope"] = scope
256
+
257
+ logger.info(f"MCP OAuth: exchanging code at {token_endpoint}")
258
+
259
+ # Make the token exchange request
260
+ response = requests.post(
261
+ token_endpoint,
262
+ data=token_body,
263
+ headers={
264
+ "Content-Type": "application/x-www-form-urlencoded",
265
+ "Accept": "application/json",
266
+ },
267
+ timeout=timeout
268
+ )
269
+
270
+ # Try to parse as JSON
271
+ try:
272
+ token_data = response.json()
273
+ except Exception:
274
+ # Some providers return URL-encoded response
275
+ from urllib.parse import parse_qs
276
+ token_data = {k: v[0] if len(v) == 1 else v
277
+ for k, v in parse_qs(response.text).items()}
278
+
279
+ if response.ok:
280
+ logger.info("MCP OAuth: token exchange successful")
281
+ return token_data
282
+ else:
283
+ error_msg = token_data.get("error_description") or token_data.get("error") or response.text
284
+ logger.error(f"MCP OAuth: token exchange failed - {response.status_code}: {error_msg}")
285
+ raise ValueError(f"Token exchange failed: {error_msg}")
286
+
287
+
288
+ def refresh_oauth_token(
289
+ token_endpoint: str,
290
+ refresh_token: str,
291
+ client_id: Optional[str] = None,
292
+ client_secret: Optional[str] = None,
293
+ scope: Optional[str] = None,
294
+ timeout: int = 30,
295
+ ) -> Dict[str, Any]:
296
+ """
297
+ Refresh an OAuth access token using a refresh token.
298
+
299
+ Args:
300
+ token_endpoint: OAuth token endpoint URL
301
+ refresh_token: Refresh token from previous authorization
302
+ client_id: OAuth client ID (optional for DCR/public clients)
303
+ client_secret: OAuth client secret (optional for public clients)
304
+ scope: OAuth scope (optional)
305
+ timeout: Request timeout in seconds
306
+
307
+ Returns:
308
+ Token response from OAuth provider containing access_token, etc.
309
+ May also include a new refresh_token depending on the provider.
310
+
311
+ Raises:
312
+ requests.RequestException: If the HTTP request fails
313
+ ValueError: If the token refresh fails
314
+
315
+ Note:
316
+ client_id may be optional for:
317
+ - Dynamic Client Registration (DCR): client_id embedded in refresh_token
318
+ - OIDC public clients: some providers don't require it
319
+ - Some MCP servers handle auth differently
320
+ """
321
+ token_body = {
322
+ "grant_type": "refresh_token",
323
+ "refresh_token": refresh_token,
324
+ }
325
+
326
+ if client_id:
327
+ token_body["client_id"] = client_id
328
+ if client_secret:
329
+ token_body["client_secret"] = client_secret
330
+ if scope:
331
+ token_body["scope"] = scope
332
+
333
+ logger.info(f"MCP OAuth: refreshing token at {token_endpoint}")
334
+
335
+ response = requests.post(
336
+ token_endpoint,
337
+ data=token_body,
338
+ headers={
339
+ "Content-Type": "application/x-www-form-urlencoded",
340
+ "Accept": "application/json",
341
+ },
342
+ timeout=timeout
343
+ )
344
+
345
+ # Try to parse as JSON
346
+ try:
347
+ token_data = response.json()
348
+ except Exception:
349
+ # Some providers return URL-encoded response
350
+ from urllib.parse import parse_qs
351
+ token_data = {k: v[0] if len(v) == 1 else v
352
+ for k, v in parse_qs(response.text).items()}
353
+
354
+ if response.ok:
355
+ logger.info("MCP OAuth: token refresh successful")
356
+ return token_data
357
+ else:
358
+ error_msg = token_data.get("error_description") or token_data.get("error") or response.text
359
+ logger.error(f"MCP OAuth: token refresh failed - {response.status_code}: {error_msg}")
360
+ raise ValueError(f"Token refresh failed: {error_msg}")
361
+
@@ -71,6 +71,7 @@ class McpSseClient:
71
71
  McpAuthorizationRequired,
72
72
  canonical_resource,
73
73
  extract_resource_metadata_url,
74
+ extract_authorization_uri,
74
75
  fetch_resource_metadata_async,
75
76
  infer_authorization_servers_from_realm,
76
77
  fetch_oauth_authorization_server_metadata
@@ -79,13 +80,41 @@ class McpSseClient:
79
80
  auth_header = self._stream_response.headers.get('WWW-Authenticate', '')
80
81
  resource_metadata_url = extract_resource_metadata_url(auth_header, self.url)
81
82
 
83
+ # First, try authorization_uri from WWW-Authenticate header (preferred)
84
+ authorization_uri = extract_authorization_uri(auth_header)
85
+
82
86
  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
- )
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")
89
118
 
90
119
  # Infer authorization servers if not in metadata
91
120
  if not metadata or not metadata.get('authorization_servers'):
@@ -345,7 +374,7 @@ class McpSseClient:
345
374
  "sampling": {}
346
375
  },
347
376
  "clientInfo": {
348
- "name": "Alita MCP Client",
377
+ "name": "ELITEA MCP Client",
349
378
  "version": "1.0.0"
350
379
  }
351
380
  }
@@ -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)
@@ -0,0 +1,155 @@
1
+ """
2
+ Serialization utilities for safe JSON encoding of complex objects.
3
+
4
+ Handles Pydantic models, LangChain messages, datetime objects, and other
5
+ non-standard types that may appear in state variables.
6
+ """
7
+ import json
8
+ import logging
9
+ from datetime import datetime, date
10
+ from typing import Any
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def _convert_to_serializable(obj: Any, _seen: set = None) -> Any:
16
+ """
17
+ Recursively convert an object to JSON-serializable primitives.
18
+
19
+ Handles nested dicts and lists that may contain non-serializable objects.
20
+ Uses a seen set to prevent infinite recursion with circular references.
21
+
22
+ Args:
23
+ obj: Any object to convert
24
+ _seen: Internal set to track seen object ids (for circular reference detection)
25
+
26
+ Returns:
27
+ JSON-serializable representation of the object
28
+ """
29
+ # Initialize seen set for circular reference detection
30
+ if _seen is None:
31
+ _seen = set()
32
+
33
+ # Check for circular references (only for mutable objects)
34
+ obj_id = id(obj)
35
+ if isinstance(obj, (dict, list, set)) and obj_id in _seen:
36
+ return f"<circular reference: {type(obj).__name__}>"
37
+
38
+ # Primitives - return as-is
39
+ if obj is None or isinstance(obj, (str, int, float, bool)):
40
+ return obj
41
+
42
+ # Add to seen set for mutable containers
43
+ if isinstance(obj, (dict, list, set)):
44
+ _seen = _seen | {obj_id} # Create new set to avoid mutation issues
45
+
46
+ # Dict - recursively process all values
47
+ if isinstance(obj, dict):
48
+ return {
49
+ _convert_to_serializable(k, _seen): _convert_to_serializable(v, _seen)
50
+ for k, v in obj.items()
51
+ }
52
+
53
+ # List/tuple - recursively process all items
54
+ if isinstance(obj, (list, tuple)):
55
+ return [_convert_to_serializable(item, _seen) for item in obj]
56
+
57
+ # Set - convert to list and process
58
+ if isinstance(obj, set):
59
+ return [_convert_to_serializable(item, _seen) for item in obj]
60
+
61
+ # Bytes - decode to string
62
+ if isinstance(obj, bytes):
63
+ try:
64
+ return obj.decode('utf-8')
65
+ except UnicodeDecodeError:
66
+ return obj.decode('utf-8', errors='replace')
67
+
68
+ # Datetime objects
69
+ if isinstance(obj, datetime):
70
+ return obj.isoformat()
71
+ if isinstance(obj, date):
72
+ return obj.isoformat()
73
+
74
+ # Pydantic BaseModel (v2) - check for model_dump method
75
+ if hasattr(obj, 'model_dump') and callable(getattr(obj, 'model_dump')):
76
+ try:
77
+ return _convert_to_serializable(obj.model_dump(), _seen)
78
+ except Exception as e:
79
+ logger.debug(f"Failed to call model_dump on {type(obj).__name__}: {e}")
80
+
81
+ # Pydantic BaseModel (v1) - check for dict method
82
+ if hasattr(obj, 'dict') and callable(getattr(obj, 'dict')) and hasattr(obj, '__fields__'):
83
+ try:
84
+ return _convert_to_serializable(obj.dict(), _seen)
85
+ except Exception as e:
86
+ logger.debug(f"Failed to call dict on {type(obj).__name__}: {e}")
87
+
88
+ # LangChain BaseMessage - extract key fields
89
+ if hasattr(obj, 'type') and hasattr(obj, 'content'):
90
+ try:
91
+ result = {
92
+ "type": obj.type,
93
+ "content": _convert_to_serializable(obj.content, _seen),
94
+ }
95
+ if hasattr(obj, 'additional_kwargs') and obj.additional_kwargs:
96
+ result["additional_kwargs"] = _convert_to_serializable(obj.additional_kwargs, _seen)
97
+ if hasattr(obj, 'name') and obj.name:
98
+ result["name"] = obj.name
99
+ return result
100
+ except Exception as e:
101
+ logger.debug(f"Failed to extract message fields from {type(obj).__name__}: {e}")
102
+
103
+ # Objects with __dict__ attribute (custom classes)
104
+ if hasattr(obj, '__dict__'):
105
+ try:
106
+ return _convert_to_serializable(obj.__dict__, _seen)
107
+ except Exception as e:
108
+ logger.debug(f"Failed to serialize __dict__ of {type(obj).__name__}: {e}")
109
+
110
+ # UUID objects
111
+ if hasattr(obj, 'hex') and hasattr(obj, 'int'):
112
+ return str(obj)
113
+
114
+ # Enum objects
115
+ if hasattr(obj, 'value') and hasattr(obj, 'name') and hasattr(obj.__class__, '__members__'):
116
+ return obj.value
117
+
118
+ # Last resort - convert to string
119
+ try:
120
+ return str(obj)
121
+ except Exception:
122
+ return f"<non-serializable: {type(obj).__name__}>"
123
+
124
+
125
+ def safe_serialize(obj: Any, **kwargs) -> str:
126
+ """
127
+ Safely serialize any object to a JSON string.
128
+
129
+ Pre-processes the entire object tree to convert non-serializable
130
+ objects before passing to json.dumps. This ensures nested dicts
131
+ and lists with non-standard objects are handled correctly.
132
+
133
+ Args:
134
+ obj: Any object to serialize
135
+ **kwargs: Additional arguments passed to json.dumps
136
+ (e.g., indent, sort_keys)
137
+
138
+ Returns:
139
+ JSON string representation of the object
140
+
141
+ Example:
142
+ >>> from pydantic import BaseModel
143
+ >>> class User(BaseModel):
144
+ ... name: str
145
+ >>> state = {"user": User(name="Alice"), "count": 5}
146
+ >>> safe_serialize(state)
147
+ '{"user": {"name": "Alice"}, "count": 5}'
148
+ """
149
+ # Pre-process the entire object tree
150
+ serializable = _convert_to_serializable(obj)
151
+
152
+ # Set defaults
153
+ kwargs.setdefault('ensure_ascii', False)
154
+
155
+ return json.dumps(serializable, **kwargs)
@@ -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:
@@ -1256,7 +1255,6 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
1256
1255
  model_config={
1257
1256
  "temperature": 0.1,
1258
1257
  "max_tokens": 1000,
1259
- "top_p": 1.0
1260
1258
  }
1261
1259
  )
1262
1260
  except Exception as e:
@@ -1387,20 +1385,18 @@ def run_streamlit(st, ai_icon=None, user_icon=None):
1387
1385
  help="Maximum number of tokens in the AI response"
1388
1386
  )
1389
1387
 
1390
- top_p = st.slider(
1391
- "Top-p:",
1392
- min_value=0.1,
1393
- max_value=1.0,
1394
- value=1.0,
1395
- step=0.1,
1396
- 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"
1397
1393
  )
1398
1394
 
1399
1395
  # Create LLM config
1400
1396
  llm_config = {
1401
1397
  'max_tokens': max_tokens,
1402
1398
  'temperature': temperature,
1403
- 'top_p': top_p
1399
+ 'reasoning_effort': reasoning_effort
1404
1400
  }
1405
1401
 
1406
1402
  col1, col2 = st.columns([3, 1])
@@ -12,7 +12,9 @@ logger = logging.getLogger(__name__)
12
12
 
13
13
  def instantiate_toolkit_with_client(toolkit_config: Dict[str, Any],
14
14
  llm_client: Any,
15
- alita_client: Optional[Any] = None) -> List[Any]:
15
+ alita_client: Optional[Any] = None,
16
+ mcp_tokens: Optional[Dict[str, Any]] = None,
17
+ use_prefix: bool = False) -> List[Any]:
16
18
  """
17
19
  Instantiate a toolkit with LLM client support.
18
20
 
@@ -22,7 +24,11 @@ def instantiate_toolkit_with_client(toolkit_config: Dict[str, Any],
22
24
  Args:
23
25
  toolkit_config: Configuration dictionary for the toolkit
24
26
  llm_client: LLM client instance for tools that need LLM capabilities
25
- client: Optional additional client instance
27
+ alita_client: Optional additional client instance
28
+ mcp_tokens: Optional dictionary of MCP OAuth tokens by server URL
29
+ use_prefix: If True, tools get prefixed with toolkit_name to prevent collisions
30
+ (for agent use). If False, tools use base names only (for testing interface).
31
+ Default False for backward compatibility with testing.
26
32
 
27
33
  Returns:
28
34
  List of instantiated tools from the toolkit
@@ -52,16 +58,19 @@ def instantiate_toolkit_with_client(toolkit_config: Dict[str, Any],
52
58
  toolkit_type = toolkit_config.get('type', toolkit_name.lower())
53
59
 
54
60
  # Create a tool configuration dict with required fields
61
+ # Note: MCP toolkit always requires toolkit_name, other toolkits respect use_prefix flag
62
+ # Note: 'name' is always set for provider-based toolkits (used by provider_worker.utils.tools)
55
63
  tool_config = {
56
64
  'id': toolkit_config.get('id', random.randint(1, 1000000)),
57
65
  'type': toolkit_config.get('type', toolkit_type),
58
66
  'settings': settings,
59
- 'toolkit_name': toolkit_name
67
+ 'name': toolkit_name, # Always pass name for provider toolkits
68
+ 'toolkit_name': toolkit_name if (use_prefix or toolkit_type == 'mcp') else None
60
69
  }
61
70
 
62
71
  # Get tools using the toolkit configuration with clients
63
- # Parameter order: get_tools(tools_list, alita_client, llm, memory_store)
64
- tools = get_tools([tool_config], alita_client, llm_client)
72
+ # Parameter order: get_tools(tools_list, alita_client, llm, memory_store, debug_mode, mcp_tokens)
73
+ tools = get_tools([tool_config], alita_client, llm_client, mcp_tokens=mcp_tokens)
65
74
 
66
75
  if not tools:
67
76
  logger.warning(f"No tools returned for toolkit {toolkit_name}")
@@ -73,9 +82,11 @@ def instantiate_toolkit_with_client(toolkit_config: Dict[str, Any],
73
82
  except Exception as e:
74
83
  # Re-raise McpAuthorizationRequired without logging as error
75
84
  from ..utils.mcp_oauth import McpAuthorizationRequired
85
+
76
86
  if isinstance(e, McpAuthorizationRequired):
77
87
  logger.info(f"Toolkit {toolkit_name} requires MCP OAuth authorization")
78
88
  raise
89
+
79
90
  # Log and re-raise other errors
80
91
  logger.error(f"Error instantiating toolkit {toolkit_name} with client: {str(e)}")
81
92
  raise