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.
- alita_sdk/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +155 -0
- alita_sdk/cli/agent_loader.py +215 -0
- alita_sdk/cli/agent_ui.py +228 -0
- alita_sdk/cli/agents.py +3601 -0
- alita_sdk/cli/callbacks.py +647 -0
- alita_sdk/cli/cli.py +168 -0
- alita_sdk/cli/config.py +306 -0
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/formatting.py +182 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/toolkit.py +327 -0
- alita_sdk/cli/toolkit_loader.py +85 -0
- alita_sdk/cli/tools/__init__.py +43 -0
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +1751 -0
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +11 -0
- alita_sdk/configurations/ado.py +148 -2
- alita_sdk/configurations/azure_search.py +1 -1
- alita_sdk/configurations/bigquery.py +1 -1
- alita_sdk/configurations/bitbucket.py +94 -2
- alita_sdk/configurations/browser.py +18 -0
- alita_sdk/configurations/carrier.py +19 -0
- alita_sdk/configurations/confluence.py +130 -1
- alita_sdk/configurations/delta_lake.py +1 -1
- alita_sdk/configurations/figma.py +76 -5
- alita_sdk/configurations/github.py +65 -1
- alita_sdk/configurations/gitlab.py +81 -0
- alita_sdk/configurations/google_places.py +17 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/openapi.py +111 -0
- alita_sdk/configurations/postman.py +1 -1
- alita_sdk/configurations/qtest.py +72 -3
- alita_sdk/configurations/report_portal.py +115 -0
- alita_sdk/configurations/salesforce.py +19 -0
- alita_sdk/configurations/service_now.py +1 -12
- alita_sdk/configurations/sharepoint.py +167 -0
- alita_sdk/configurations/sonar.py +18 -0
- alita_sdk/configurations/sql.py +20 -0
- alita_sdk/configurations/testio.py +101 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +94 -1
- alita_sdk/configurations/zephyr_enterprise.py +94 -1
- alita_sdk/configurations/zephyr_essential.py +95 -0
- alita_sdk/runtime/clients/artifact.py +21 -4
- alita_sdk/runtime/clients/client.py +458 -67
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/clients/sandbox_client.py +352 -0
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +183 -43
- alita_sdk/runtime/langchain/constants.py +647 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
- alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
- alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
- alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
- alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
- alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
- alita_sdk/runtime/langchain/langraph_agent.py +407 -92
- alita_sdk/runtime/langchain/utils.py +102 -8
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/models/mcp_models.py +61 -0
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +28 -0
- alita_sdk/runtime/toolkits/application.py +14 -4
- alita_sdk/runtime/toolkits/artifact.py +24 -9
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +780 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +11 -6
- alita_sdk/runtime/toolkits/tools.py +314 -70
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +24 -0
- alita_sdk/runtime/tools/application.py +16 -4
- alita_sdk/runtime/tools/artifact.py +367 -33
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +100 -4
- alita_sdk/runtime/tools/graph.py +81 -0
- alita_sdk/runtime/tools/image_generation.py +218 -0
- alita_sdk/runtime/tools/llm.py +1013 -177
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -1
- alita_sdk/runtime/tools/sandbox.py +375 -0
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +69 -65
- alita_sdk/runtime/tools/vectorstore_base.py +163 -90
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +361 -0
- alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/streamlit.py +41 -14
- alita_sdk/runtime/utils/toolkit_utils.py +28 -9
- alita_sdk/runtime/utils/utils.py +48 -0
- alita_sdk/tools/__init__.py +135 -37
- alita_sdk/tools/ado/__init__.py +2 -2
- alita_sdk/tools/ado/repos/__init__.py +15 -19
- alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
- alita_sdk/tools/ado/test_plan/__init__.py +26 -8
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
- alita_sdk/tools/ado/wiki/__init__.py +27 -12
- alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
- alita_sdk/tools/ado/work_item/__init__.py +27 -12
- alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
- alita_sdk/tools/advanced_jira_mining/__init__.py +12 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +14 -11
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +13 -8
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +454 -110
- alita_sdk/tools/bitbucket/__init__.py +27 -19
- alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
- alita_sdk/tools/browser/__init__.py +41 -16
- alita_sdk/tools/browser/crawler.py +3 -1
- alita_sdk/tools/browser/utils.py +15 -6
- alita_sdk/tools/carrier/__init__.py +18 -17
- alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
- alita_sdk/tools/carrier/excel_reporter.py +8 -4
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/codeparser.py +1 -1
- alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +11 -7
- alita_sdk/tools/cloud/azure/__init__.py +11 -7
- alita_sdk/tools/cloud/gcp/__init__.py +11 -7
- alita_sdk/tools/cloud/k8s/__init__.py +11 -7
- alita_sdk/tools/code/linter/__init__.py +9 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +20 -13
- alita_sdk/tools/code_indexer_toolkit.py +199 -0
- alita_sdk/tools/confluence/__init__.py +21 -14
- alita_sdk/tools/confluence/api_wrapper.py +197 -58
- alita_sdk/tools/confluence/loader.py +14 -2
- alita_sdk/tools/custom_open_api/__init__.py +11 -5
- alita_sdk/tools/elastic/__init__.py +10 -8
- alita_sdk/tools/elitea_base.py +546 -64
- alita_sdk/tools/figma/__init__.py +11 -8
- alita_sdk/tools/figma/api_wrapper.py +352 -153
- alita_sdk/tools/github/__init__.py +17 -17
- alita_sdk/tools/github/api_wrapper.py +9 -26
- alita_sdk/tools/github/github_client.py +81 -12
- alita_sdk/tools/github/schemas.py +2 -1
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/gitlab/__init__.py +18 -13
- alita_sdk/tools/gitlab/api_wrapper.py +224 -80
- alita_sdk/tools/gitlab_org/__init__.py +13 -10
- alita_sdk/tools/google/bigquery/__init__.py +13 -13
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +20 -11
- alita_sdk/tools/jira/__init__.py +21 -11
- alita_sdk/tools/jira/api_wrapper.py +315 -168
- alita_sdk/tools/keycloak/__init__.py +10 -8
- alita_sdk/tools/localgit/__init__.py +8 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +38 -14
- alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
- alita_sdk/tools/ocr/__init__.py +10 -8
- alita_sdk/tools/openapi/__init__.py +281 -108
- alita_sdk/tools/openapi/api_wrapper.py +883 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +18 -11
- alita_sdk/tools/pandas/api_wrapper.py +40 -45
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +10 -11
- alita_sdk/tools/postman/api_wrapper.py +19 -8
- alita_sdk/tools/postman/postman_analysis.py +8 -1
- alita_sdk/tools/pptx/__init__.py +10 -10
- alita_sdk/tools/qtest/__init__.py +21 -14
- alita_sdk/tools/qtest/api_wrapper.py +1784 -88
- alita_sdk/tools/rally/__init__.py +12 -10
- alita_sdk/tools/report_portal/__init__.py +22 -16
- alita_sdk/tools/salesforce/__init__.py +21 -16
- alita_sdk/tools/servicenow/__init__.py +20 -16
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +16 -14
- alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
- alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/slack/__init__.py +11 -7
- alita_sdk/tools/sql/__init__.py +21 -19
- alita_sdk/tools/sql/api_wrapper.py +71 -23
- alita_sdk/tools/testio/__init__.py +20 -13
- alita_sdk/tools/testrail/__init__.py +12 -11
- alita_sdk/tools/testrail/api_wrapper.py +214 -46
- alita_sdk/tools/utils/__init__.py +28 -4
- alita_sdk/tools/utils/content_parser.py +182 -62
- alita_sdk/tools/utils/text_operations.py +254 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
- alita_sdk/tools/xray/__init__.py +17 -14
- alita_sdk/tools/xray/api_wrapper.py +58 -113
- alita_sdk/tools/yagmail/__init__.py +8 -3
- alita_sdk/tools/zephyr/__init__.py +11 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +15 -9
- alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
- alita_sdk/tools/zephyr_essential/__init__.py +15 -10
- alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
- alita_sdk/tools/zephyr_essential/client.py +6 -4
- alita_sdk/tools/zephyr_scale/__init__.py +12 -8
- alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
- alita_sdk/tools/zephyr_squad/__init__.py +11 -7
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/METADATA +184 -37
- alita_sdk-0.3.562.dist-info/RECORD +450 -0
- alita_sdk-0.3.562.dist-info/entry_points.txt +2 -0
- alita_sdk/tools/bitbucket/tools.py +0 -304
- alita_sdk-0.3.257.dist-info/RECORD +0 -343
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.562.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
|
+
|