alita-sdk 0.3.257__py3-none-any.whl → 0.3.584__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of alita-sdk might be problematic. Click here for more details.

Files changed (281) 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 +3794 -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 +323 -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 +493 -105
  110. alita_sdk/runtime/langchain/utils.py +118 -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 +25 -9
  124. alita_sdk/runtime/toolkits/datasource.py +13 -6
  125. alita_sdk/runtime/toolkits/mcp.py +782 -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 +1032 -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/constants.py +5 -1
  155. alita_sdk/runtime/utils/mcp_client.py +492 -0
  156. alita_sdk/runtime/utils/mcp_oauth.py +361 -0
  157. alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
  158. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  159. alita_sdk/runtime/utils/streamlit.py +41 -14
  160. alita_sdk/runtime/utils/toolkit_utils.py +28 -9
  161. alita_sdk/runtime/utils/utils.py +48 -0
  162. alita_sdk/tools/__init__.py +135 -37
  163. alita_sdk/tools/ado/__init__.py +2 -2
  164. alita_sdk/tools/ado/repos/__init__.py +16 -19
  165. alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
  166. alita_sdk/tools/ado/test_plan/__init__.py +27 -8
  167. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
  168. alita_sdk/tools/ado/wiki/__init__.py +28 -12
  169. alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
  170. alita_sdk/tools/ado/work_item/__init__.py +28 -12
  171. alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
  172. alita_sdk/tools/advanced_jira_mining/__init__.py +13 -8
  173. alita_sdk/tools/aws/delta_lake/__init__.py +15 -11
  174. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  175. alita_sdk/tools/azure_ai/search/__init__.py +14 -8
  176. alita_sdk/tools/base/tool.py +5 -1
  177. alita_sdk/tools/base_indexer_toolkit.py +454 -110
  178. alita_sdk/tools/bitbucket/__init__.py +28 -19
  179. alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
  180. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
  181. alita_sdk/tools/browser/__init__.py +41 -16
  182. alita_sdk/tools/browser/crawler.py +3 -1
  183. alita_sdk/tools/browser/utils.py +15 -6
  184. alita_sdk/tools/carrier/__init__.py +18 -17
  185. alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
  186. alita_sdk/tools/carrier/excel_reporter.py +8 -4
  187. alita_sdk/tools/chunkers/__init__.py +3 -1
  188. alita_sdk/tools/chunkers/code/codeparser.py +1 -1
  189. alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
  190. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
  191. alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
  192. alita_sdk/tools/chunkers/universal_chunker.py +270 -0
  193. alita_sdk/tools/cloud/aws/__init__.py +12 -7
  194. alita_sdk/tools/cloud/azure/__init__.py +12 -7
  195. alita_sdk/tools/cloud/gcp/__init__.py +12 -7
  196. alita_sdk/tools/cloud/k8s/__init__.py +12 -7
  197. alita_sdk/tools/code/linter/__init__.py +10 -8
  198. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  199. alita_sdk/tools/code/sonar/__init__.py +21 -13
  200. alita_sdk/tools/code_indexer_toolkit.py +199 -0
  201. alita_sdk/tools/confluence/__init__.py +22 -14
  202. alita_sdk/tools/confluence/api_wrapper.py +197 -58
  203. alita_sdk/tools/confluence/loader.py +14 -2
  204. alita_sdk/tools/custom_open_api/__init__.py +12 -5
  205. alita_sdk/tools/elastic/__init__.py +11 -8
  206. alita_sdk/tools/elitea_base.py +546 -64
  207. alita_sdk/tools/figma/__init__.py +60 -11
  208. alita_sdk/tools/figma/api_wrapper.py +1400 -167
  209. alita_sdk/tools/figma/figma_client.py +73 -0
  210. alita_sdk/tools/figma/toon_tools.py +2748 -0
  211. alita_sdk/tools/github/__init__.py +18 -17
  212. alita_sdk/tools/github/api_wrapper.py +9 -26
  213. alita_sdk/tools/github/github_client.py +81 -12
  214. alita_sdk/tools/github/schemas.py +2 -1
  215. alita_sdk/tools/github/tool.py +5 -1
  216. alita_sdk/tools/gitlab/__init__.py +19 -13
  217. alita_sdk/tools/gitlab/api_wrapper.py +256 -80
  218. alita_sdk/tools/gitlab_org/__init__.py +14 -10
  219. alita_sdk/tools/google/bigquery/__init__.py +14 -13
  220. alita_sdk/tools/google/bigquery/tool.py +5 -1
  221. alita_sdk/tools/google_places/__init__.py +21 -11
  222. alita_sdk/tools/jira/__init__.py +22 -11
  223. alita_sdk/tools/jira/api_wrapper.py +315 -168
  224. alita_sdk/tools/keycloak/__init__.py +11 -8
  225. alita_sdk/tools/localgit/__init__.py +9 -3
  226. alita_sdk/tools/localgit/local_git.py +62 -54
  227. alita_sdk/tools/localgit/tool.py +5 -1
  228. alita_sdk/tools/memory/__init__.py +38 -14
  229. alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
  230. alita_sdk/tools/ocr/__init__.py +11 -8
  231. alita_sdk/tools/openapi/__init__.py +491 -106
  232. alita_sdk/tools/openapi/api_wrapper.py +1357 -0
  233. alita_sdk/tools/openapi/tool.py +20 -0
  234. alita_sdk/tools/pandas/__init__.py +20 -12
  235. alita_sdk/tools/pandas/api_wrapper.py +40 -45
  236. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  237. alita_sdk/tools/postman/__init__.py +11 -11
  238. alita_sdk/tools/postman/api_wrapper.py +19 -8
  239. alita_sdk/tools/postman/postman_analysis.py +8 -1
  240. alita_sdk/tools/pptx/__init__.py +11 -10
  241. alita_sdk/tools/qtest/__init__.py +22 -14
  242. alita_sdk/tools/qtest/api_wrapper.py +1784 -88
  243. alita_sdk/tools/rally/__init__.py +13 -10
  244. alita_sdk/tools/report_portal/__init__.py +23 -16
  245. alita_sdk/tools/salesforce/__init__.py +22 -16
  246. alita_sdk/tools/servicenow/__init__.py +21 -16
  247. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  248. alita_sdk/tools/sharepoint/__init__.py +17 -14
  249. alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
  250. alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
  251. alita_sdk/tools/sharepoint/utils.py +8 -2
  252. alita_sdk/tools/slack/__init__.py +13 -8
  253. alita_sdk/tools/sql/__init__.py +22 -19
  254. alita_sdk/tools/sql/api_wrapper.py +71 -23
  255. alita_sdk/tools/testio/__init__.py +21 -13
  256. alita_sdk/tools/testrail/__init__.py +13 -11
  257. alita_sdk/tools/testrail/api_wrapper.py +214 -46
  258. alita_sdk/tools/utils/__init__.py +28 -4
  259. alita_sdk/tools/utils/content_parser.py +241 -55
  260. alita_sdk/tools/utils/text_operations.py +254 -0
  261. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
  262. alita_sdk/tools/xray/__init__.py +18 -14
  263. alita_sdk/tools/xray/api_wrapper.py +58 -113
  264. alita_sdk/tools/yagmail/__init__.py +9 -3
  265. alita_sdk/tools/zephyr/__init__.py +12 -7
  266. alita_sdk/tools/zephyr_enterprise/__init__.py +16 -9
  267. alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
  268. alita_sdk/tools/zephyr_essential/__init__.py +16 -10
  269. alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
  270. alita_sdk/tools/zephyr_essential/client.py +6 -4
  271. alita_sdk/tools/zephyr_scale/__init__.py +13 -8
  272. alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
  273. alita_sdk/tools/zephyr_squad/__init__.py +12 -7
  274. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/METADATA +184 -37
  275. alita_sdk-0.3.584.dist-info/RECORD +452 -0
  276. alita_sdk-0.3.584.dist-info/entry_points.txt +2 -0
  277. alita_sdk/tools/bitbucket/tools.py +0 -304
  278. alita_sdk-0.3.257.dist-info/RECORD +0 -343
  279. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/WHEEL +0 -0
  280. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/licenses/LICENSE +0 -0
  281. {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,361 @@
1
+ import json
2
+ import logging
3
+ import re
4
+ from typing import Any, Dict, Optional
5
+ from urllib.parse import urlparse
6
+
7
+ import requests
8
+ from langchain_core.tools import ToolException
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class McpAuthorizationRequired(ToolException):
14
+ """Raised when an MCP server requires OAuth authorization before use."""
15
+
16
+ def __init__(
17
+ self,
18
+ message: str,
19
+ server_url: str,
20
+ resource_metadata_url: Optional[str] = None,
21
+ www_authenticate: Optional[str] = None,
22
+ resource_metadata: Optional[Dict[str, Any]] = None,
23
+ status: Optional[int] = None,
24
+ tool_name: Optional[str] = None,
25
+ ):
26
+ super().__init__(message)
27
+ self.server_url = server_url
28
+ self.resource_metadata_url = resource_metadata_url
29
+ self.www_authenticate = www_authenticate
30
+ self.resource_metadata = resource_metadata
31
+ self.status = status
32
+ self.tool_name = tool_name
33
+
34
+ def to_dict(self) -> Dict[str, Any]:
35
+ return {
36
+ "message": str(self),
37
+ "server_url": self.server_url,
38
+ "resource_metadata_url": self.resource_metadata_url,
39
+ "www_authenticate": self.www_authenticate,
40
+ "resource_metadata": self.resource_metadata,
41
+ "status": self.status,
42
+ "tool_name": self.tool_name,
43
+ }
44
+
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
+
63
+ def extract_resource_metadata_url(www_authenticate: Optional[str], server_url: Optional[str] = None) -> Optional[str]:
64
+ """
65
+ Pull the resource_metadata URL from a WWW-Authenticate header if present.
66
+ If not found and server_url is provided, try to construct resource metadata URLs.
67
+ """
68
+ if not www_authenticate and not server_url:
69
+ return None
70
+
71
+ # RFC9728 returns `resource_metadata="<url>"` inside the header value
72
+ if www_authenticate:
73
+ match = re.search(r'resource_metadata\s*=\s*\"?([^\", ]+)\"?', www_authenticate)
74
+ if match:
75
+ return match.group(1)
76
+
77
+ # For servers that don't provide resource_metadata in WWW-Authenticate,
78
+ # we'll return None and rely on inferring authorization servers from the realm
79
+ # or using well-known OAuth discovery endpoints directly
80
+ return None
81
+
82
+ def fetch_oauth_authorization_server_metadata(url: str, timeout: int = 10) -> Optional[Dict[str, Any]]:
83
+ """
84
+ Fetch OAuth authorization server metadata from well-known 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.
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
106
+ discovery_endpoints = [
107
+ f"{url}/.well-known/oauth-authorization-server",
108
+ f"{url}/.well-known/openid-configuration",
109
+ ]
110
+
111
+ for endpoint in discovery_endpoints:
112
+ try:
113
+ resp = requests.get(endpoint, timeout=timeout)
114
+ if resp.status_code == 200:
115
+ return resp.json()
116
+ except Exception as exc:
117
+ logger.debug(f"Failed to fetch OAuth metadata from {endpoint}: {exc}")
118
+ continue
119
+
120
+ return None
121
+
122
+
123
+ def infer_authorization_servers_from_realm(www_authenticate: Optional[str], server_url: str) -> Optional[list]:
124
+ """
125
+ Infer authorization server URLs from WWW-Authenticate realm or server URL.
126
+ This is used when the server doesn't provide resource_metadata endpoint.
127
+ """
128
+ if not www_authenticate and not server_url:
129
+ return None
130
+
131
+ authorization_servers = []
132
+
133
+ # Try to extract realm from WWW-Authenticate header
134
+ realm = None
135
+ if www_authenticate:
136
+ realm_match = re.search(r'realm\s*=\s*\"([^\"]+)\"', www_authenticate)
137
+ if realm_match:
138
+ realm = realm_match.group(1)
139
+
140
+ # Parse the server URL to get base domain
141
+ parsed = urlparse(server_url)
142
+ base_url = f"{parsed.scheme}://{parsed.netloc}"
143
+
144
+ # Return the base authorization server URL (not the discovery endpoint)
145
+ # The client will append .well-known paths when fetching metadata
146
+ authorization_servers.append(base_url)
147
+
148
+ return authorization_servers if authorization_servers else None
149
+
150
+
151
+ def fetch_resource_metadata(resource_metadata_url: str, timeout: int = 10) -> Optional[Dict[str, Any]]:
152
+ """Fetch and parse the protected resource metadata document."""
153
+ try:
154
+ resp = requests.get(resource_metadata_url, timeout=timeout)
155
+ resp.raise_for_status()
156
+ return resp.json()
157
+ except Exception as exc: # broad catch – we want to surface auth requirement even if this fails
158
+ logger.warning("Failed to fetch resource metadata from %s: %s", resource_metadata_url, exc)
159
+ return None
160
+
161
+
162
+ async def fetch_resource_metadata_async(resource_metadata_url: str, session=None, timeout: int = 10) -> Optional[Dict[str, Any]]:
163
+ """Async variant for fetching protected resource metadata."""
164
+ try:
165
+ import aiohttp
166
+
167
+ client_timeout = aiohttp.ClientTimeout(total=timeout)
168
+ if session:
169
+ async with session.get(resource_metadata_url, timeout=client_timeout) as resp:
170
+ text = await resp.text()
171
+ else:
172
+ async with aiohttp.ClientSession(timeout=client_timeout) as local_session:
173
+ async with local_session.get(resource_metadata_url) as resp:
174
+ text = await resp.text()
175
+
176
+ try:
177
+ return json.loads(text)
178
+ except json.JSONDecodeError:
179
+ logger.warning("Resource metadata at %s is not valid JSON: %s", resource_metadata_url, text[:200])
180
+ return None
181
+ except Exception as exc:
182
+ logger.warning("Failed to fetch resource metadata from %s: %s", resource_metadata_url, exc)
183
+ return None
184
+
185
+
186
+ def canonical_resource(server_url: str) -> str:
187
+ """Produce a canonical resource identifier for the MCP server."""
188
+ parsed = urlparse(server_url)
189
+ # Normalize scheme/host casing per RFC guidance
190
+ normalized = parsed._replace(
191
+ scheme=parsed.scheme.lower(),
192
+ netloc=parsed.netloc.lower(),
193
+ )
194
+ resource = normalized.geturl()
195
+
196
+ # Prefer form without trailing slash unless path is meaningful
197
+ if resource.endswith("/") and parsed.path in ("", "/"):
198
+ resource = resource[:-1]
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
+