datarobot-genai 0.2.31__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.
- datarobot_genai/__init__.py +19 -0
- datarobot_genai/core/__init__.py +0 -0
- datarobot_genai/core/agents/__init__.py +43 -0
- datarobot_genai/core/agents/base.py +195 -0
- datarobot_genai/core/chat/__init__.py +19 -0
- datarobot_genai/core/chat/auth.py +146 -0
- datarobot_genai/core/chat/client.py +178 -0
- datarobot_genai/core/chat/responses.py +297 -0
- datarobot_genai/core/cli/__init__.py +18 -0
- datarobot_genai/core/cli/agent_environment.py +47 -0
- datarobot_genai/core/cli/agent_kernel.py +211 -0
- datarobot_genai/core/custom_model.py +141 -0
- datarobot_genai/core/mcp/__init__.py +0 -0
- datarobot_genai/core/mcp/common.py +218 -0
- datarobot_genai/core/telemetry_agent.py +126 -0
- datarobot_genai/core/utils/__init__.py +3 -0
- datarobot_genai/core/utils/auth.py +234 -0
- datarobot_genai/core/utils/urls.py +64 -0
- datarobot_genai/crewai/__init__.py +24 -0
- datarobot_genai/crewai/agent.py +42 -0
- datarobot_genai/crewai/base.py +159 -0
- datarobot_genai/crewai/events.py +117 -0
- datarobot_genai/crewai/mcp.py +59 -0
- datarobot_genai/drmcp/__init__.py +78 -0
- datarobot_genai/drmcp/core/__init__.py +13 -0
- datarobot_genai/drmcp/core/auth.py +165 -0
- datarobot_genai/drmcp/core/clients.py +180 -0
- datarobot_genai/drmcp/core/config.py +364 -0
- datarobot_genai/drmcp/core/config_utils.py +174 -0
- datarobot_genai/drmcp/core/constants.py +18 -0
- datarobot_genai/drmcp/core/credentials.py +190 -0
- datarobot_genai/drmcp/core/dr_mcp_server.py +350 -0
- datarobot_genai/drmcp/core/dr_mcp_server_logo.py +136 -0
- datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +13 -0
- datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +130 -0
- datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +70 -0
- datarobot_genai/drmcp/core/dynamic_prompts/register.py +205 -0
- datarobot_genai/drmcp/core/dynamic_prompts/utils.py +33 -0
- datarobot_genai/drmcp/core/dynamic_tools/__init__.py +14 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +14 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +72 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +82 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +238 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +228 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +63 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +162 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +87 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +36 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +10 -0
- datarobot_genai/drmcp/core/dynamic_tools/register.py +254 -0
- datarobot_genai/drmcp/core/dynamic_tools/schema.py +532 -0
- datarobot_genai/drmcp/core/exceptions.py +25 -0
- datarobot_genai/drmcp/core/logging.py +98 -0
- datarobot_genai/drmcp/core/mcp_instance.py +515 -0
- datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
- datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
- datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
- datarobot_genai/drmcp/core/routes.py +439 -0
- datarobot_genai/drmcp/core/routes_utils.py +30 -0
- datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
- datarobot_genai/drmcp/core/telemetry.py +424 -0
- datarobot_genai/drmcp/core/tool_config.py +111 -0
- datarobot_genai/drmcp/core/tool_filter.py +117 -0
- datarobot_genai/drmcp/core/utils.py +138 -0
- datarobot_genai/drmcp/server.py +19 -0
- datarobot_genai/drmcp/test_utils/__init__.py +13 -0
- datarobot_genai/drmcp/test_utils/clients/__init__.py +0 -0
- datarobot_genai/drmcp/test_utils/clients/anthropic.py +68 -0
- datarobot_genai/drmcp/test_utils/clients/base.py +300 -0
- datarobot_genai/drmcp/test_utils/clients/dr_gateway.py +58 -0
- datarobot_genai/drmcp/test_utils/clients/openai.py +68 -0
- datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
- datarobot_genai/drmcp/test_utils/integration_mcp_server.py +109 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +133 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +107 -0
- datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +220 -0
- datarobot_genai/drmcp/test_utils/utils.py +91 -0
- datarobot_genai/drmcp/tools/__init__.py +14 -0
- datarobot_genai/drmcp/tools/clients/__init__.py +14 -0
- datarobot_genai/drmcp/tools/clients/atlassian.py +188 -0
- datarobot_genai/drmcp/tools/clients/confluence.py +584 -0
- datarobot_genai/drmcp/tools/clients/gdrive.py +832 -0
- datarobot_genai/drmcp/tools/clients/jira.py +334 -0
- datarobot_genai/drmcp/tools/clients/microsoft_graph.py +479 -0
- datarobot_genai/drmcp/tools/clients/s3.py +28 -0
- datarobot_genai/drmcp/tools/confluence/__init__.py +14 -0
- datarobot_genai/drmcp/tools/confluence/tools.py +321 -0
- datarobot_genai/drmcp/tools/gdrive/__init__.py +0 -0
- datarobot_genai/drmcp/tools/gdrive/tools.py +347 -0
- datarobot_genai/drmcp/tools/jira/__init__.py +14 -0
- datarobot_genai/drmcp/tools/jira/tools.py +243 -0
- datarobot_genai/drmcp/tools/microsoft_graph/__init__.py +13 -0
- datarobot_genai/drmcp/tools/microsoft_graph/tools.py +198 -0
- datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
- datarobot_genai/drmcp/tools/predictive/data.py +133 -0
- datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
- datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
- datarobot_genai/drmcp/tools/predictive/model.py +148 -0
- datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
- datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
- datarobot_genai/drmcp/tools/predictive/project.py +90 -0
- datarobot_genai/drmcp/tools/predictive/training.py +661 -0
- datarobot_genai/langgraph/__init__.py +0 -0
- datarobot_genai/langgraph/agent.py +341 -0
- datarobot_genai/langgraph/mcp.py +73 -0
- datarobot_genai/llama_index/__init__.py +16 -0
- datarobot_genai/llama_index/agent.py +50 -0
- datarobot_genai/llama_index/base.py +299 -0
- datarobot_genai/llama_index/mcp.py +79 -0
- datarobot_genai/nat/__init__.py +0 -0
- datarobot_genai/nat/agent.py +275 -0
- datarobot_genai/nat/datarobot_auth_provider.py +110 -0
- datarobot_genai/nat/datarobot_llm_clients.py +318 -0
- datarobot_genai/nat/datarobot_llm_providers.py +130 -0
- datarobot_genai/nat/datarobot_mcp_client.py +266 -0
- datarobot_genai/nat/helpers.py +87 -0
- datarobot_genai/py.typed +0 -0
- datarobot_genai-0.2.31.dist-info/METADATA +145 -0
- datarobot_genai-0.2.31.dist-info/RECORD +125 -0
- datarobot_genai-0.2.31.dist-info/WHEEL +4 -0
- datarobot_genai-0.2.31.dist-info/entry_points.txt +5 -0
- datarobot_genai-0.2.31.dist-info/licenses/AUTHORS +2 -0
- datarobot_genai-0.2.31.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Copyright 2025 DataRobot, Inc. and its affiliates.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
MCP integration for CrewAI using MCPServerAdapter.
|
|
17
|
+
|
|
18
|
+
This module provides MCP server connection management for CrewAI agents.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from collections.abc import Generator
|
|
22
|
+
from contextlib import contextmanager
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from crewai_tools import MCPServerAdapter
|
|
26
|
+
|
|
27
|
+
from datarobot_genai.core.mcp.common import MCPConfig
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@contextmanager
|
|
31
|
+
def mcp_tools_context(
|
|
32
|
+
authorization_context: dict[str, Any] | None = None,
|
|
33
|
+
forwarded_headers: dict[str, str] | None = None,
|
|
34
|
+
) -> Generator[list[Any], None, None]:
|
|
35
|
+
"""Context manager for MCP tools that handles connection lifecycle."""
|
|
36
|
+
config = MCPConfig(
|
|
37
|
+
authorization_context=authorization_context,
|
|
38
|
+
forwarded_headers=forwarded_headers,
|
|
39
|
+
)
|
|
40
|
+
# If no MCP server configured, return empty tools list
|
|
41
|
+
if not config.server_config:
|
|
42
|
+
print("No MCP server configured, using empty tools list", flush=True)
|
|
43
|
+
yield []
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
print(f"Connecting to MCP server: {config.server_config['url']}", flush=True)
|
|
47
|
+
|
|
48
|
+
# Use MCPServerAdapter as context manager with the server config
|
|
49
|
+
try:
|
|
50
|
+
with MCPServerAdapter(config.server_config) as tools:
|
|
51
|
+
print(
|
|
52
|
+
f"Successfully connected to MCP server, got {len(tools)} tools",
|
|
53
|
+
flush=True,
|
|
54
|
+
)
|
|
55
|
+
yield tools
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
# Gracefully degrade when connection fails or adapter initialization raises
|
|
58
|
+
print(f"Failed to connect to MCP server: {exc}", flush=True)
|
|
59
|
+
yield []
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Copyright 2025 DataRobot, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
DataRobot MCP Server Library.
|
|
17
|
+
|
|
18
|
+
A reusable library for building Model Context Protocol (MCP) servers with DataRobot integration.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# Export main server components
|
|
22
|
+
from datarobot_genai.drmcp.test_utils.clients.openai import OpenAILLMMCPClient
|
|
23
|
+
from datarobot_genai.drmcp.test_utils.mcp_utils_ete import ete_test_mcp_session
|
|
24
|
+
from datarobot_genai.drmcp.test_utils.mcp_utils_ete import get_dr_mcp_server_url
|
|
25
|
+
from datarobot_genai.drmcp.test_utils.mcp_utils_ete import get_headers
|
|
26
|
+
from datarobot_genai.drmcp.test_utils.mcp_utils_integration import integration_test_mcp_session
|
|
27
|
+
from datarobot_genai.drmcp.test_utils.tool_base_ete import ETETestExpectations
|
|
28
|
+
from datarobot_genai.drmcp.test_utils.tool_base_ete import ToolBaseE2E
|
|
29
|
+
from datarobot_genai.drmcp.test_utils.tool_base_ete import ToolCallTestExpectations
|
|
30
|
+
|
|
31
|
+
from .core.clients import get_sdk_client
|
|
32
|
+
from .core.config import MCPServerConfig
|
|
33
|
+
from .core.config import get_config
|
|
34
|
+
from .core.config_utils import extract_datarobot_credential_runtime_param_payload
|
|
35
|
+
from .core.config_utils import extract_datarobot_dict_runtime_param_payload
|
|
36
|
+
from .core.config_utils import extract_datarobot_runtime_param_payload
|
|
37
|
+
from .core.constants import RUNTIME_PARAM_ENV_VAR_NAME_PREFIX
|
|
38
|
+
from .core.credentials import MCPServerCredentials
|
|
39
|
+
from .core.credentials import get_credentials
|
|
40
|
+
from .core.dr_mcp_server import BaseServerLifecycle
|
|
41
|
+
from .core.dr_mcp_server import DataRobotMCPServer
|
|
42
|
+
from .core.dr_mcp_server import create_mcp_server
|
|
43
|
+
from .core.logging import MCPLogging
|
|
44
|
+
from .core.mcp_instance import dr_mcp_tool
|
|
45
|
+
from .core.mcp_instance import register_tools
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Main server
|
|
49
|
+
"DataRobotMCPServer",
|
|
50
|
+
"create_mcp_server",
|
|
51
|
+
"BaseServerLifecycle",
|
|
52
|
+
# Configuration
|
|
53
|
+
"get_config",
|
|
54
|
+
"MCPServerConfig",
|
|
55
|
+
# Credentials
|
|
56
|
+
"get_credentials",
|
|
57
|
+
"MCPServerCredentials",
|
|
58
|
+
# Constants
|
|
59
|
+
"RUNTIME_PARAM_ENV_VAR_NAME_PREFIX",
|
|
60
|
+
# User extensibility
|
|
61
|
+
"get_sdk_client",
|
|
62
|
+
"dr_mcp_tool",
|
|
63
|
+
"register_tools",
|
|
64
|
+
# Utilities
|
|
65
|
+
"MCPLogging",
|
|
66
|
+
"extract_datarobot_runtime_param_payload",
|
|
67
|
+
"extract_datarobot_dict_runtime_param_payload",
|
|
68
|
+
"extract_datarobot_credential_runtime_param_payload",
|
|
69
|
+
# Test utilities
|
|
70
|
+
"get_dr_mcp_server_url",
|
|
71
|
+
"get_headers",
|
|
72
|
+
"ete_test_mcp_session",
|
|
73
|
+
"OpenAILLMMCPClient",
|
|
74
|
+
"ETETestExpectations",
|
|
75
|
+
"ToolBaseE2E",
|
|
76
|
+
"ToolCallTestExpectations",
|
|
77
|
+
"integration_test_mcp_session",
|
|
78
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright 2025 DataRobot, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Copyright 2025 DataRobot, Inc. and its affiliates.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from datarobot.auth.session import AuthCtx
|
|
21
|
+
from datarobot.models.genai.agent.auth import ToolAuth
|
|
22
|
+
from fastmcp.server.dependencies import get_context
|
|
23
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
24
|
+
from fastmcp.server.middleware import CallNext
|
|
25
|
+
from fastmcp.server.middleware import Middleware
|
|
26
|
+
from fastmcp.server.middleware import MiddlewareContext
|
|
27
|
+
from fastmcp.tools.tool import ToolResult
|
|
28
|
+
|
|
29
|
+
from datarobot_genai.core.utils.auth import AsyncOAuthTokenProvider
|
|
30
|
+
from datarobot_genai.core.utils.auth import AuthContextHeaderHandler
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
AUTH_CTX_KEY = "authorization_context"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OAuthMiddleWare(Middleware):
|
|
39
|
+
"""Middleware that parses `x-datarobot-authorization-context` for tool calls.
|
|
40
|
+
|
|
41
|
+
The header is expected to be a JWT-encoded token representing an authentication
|
|
42
|
+
context compatible with :class:`datarobot.auth.session.AuthCtx`.
|
|
43
|
+
|
|
44
|
+
Attributes
|
|
45
|
+
----------
|
|
46
|
+
auth_handler : AuthContextHeaderHandler
|
|
47
|
+
Handler for encoding/decoding JWT tokens containing auth context.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, auth_handler: AuthContextHeaderHandler | None = None) -> None:
|
|
51
|
+
self.auth_handler = auth_handler or AuthContextHeaderHandler()
|
|
52
|
+
|
|
53
|
+
async def on_call_tool(
|
|
54
|
+
self, context: MiddlewareContext, call_next: CallNext[Any, ToolResult]
|
|
55
|
+
) -> ToolResult:
|
|
56
|
+
"""Parse header and attach an AuthCtx to the context before running the tool.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
context : MiddlewareContext
|
|
61
|
+
The middleware context that will be passed to the tool.
|
|
62
|
+
call_next : CallNext[Any, ToolResult]
|
|
63
|
+
The next handler in the middleware chain.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
ToolResult
|
|
68
|
+
The result from the tool execution.
|
|
69
|
+
"""
|
|
70
|
+
auth_context = self._extract_auth_context()
|
|
71
|
+
if not auth_context:
|
|
72
|
+
logger.debug("No valid authorization context extracted from request headers.")
|
|
73
|
+
|
|
74
|
+
if context.fastmcp_context is not None:
|
|
75
|
+
context.fastmcp_context.set_state(AUTH_CTX_KEY, auth_context)
|
|
76
|
+
logger.debug("Authorization context attached to state.")
|
|
77
|
+
|
|
78
|
+
return await call_next(context)
|
|
79
|
+
|
|
80
|
+
def _extract_auth_context(self) -> AuthCtx | None:
|
|
81
|
+
"""Extract and validate authentication context from request headers.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
Optional[AuthCtx]
|
|
86
|
+
The validated authentication context, or None if extraction fails.
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
headers = get_http_headers()
|
|
90
|
+
return self.auth_handler.get_context(headers)
|
|
91
|
+
except (ValueError, KeyError, TypeError) as exc:
|
|
92
|
+
logger.warning("Failed to extract auth context from headers: %s", exc, exc_info=True)
|
|
93
|
+
return None
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
logger.error("Unexpected error extracting auth context: %s", exc, exc_info=True)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def must_get_auth_context() -> AuthCtx:
|
|
100
|
+
"""Retrieve the AuthCtx from the current request context or raise error.
|
|
101
|
+
|
|
102
|
+
Raises
|
|
103
|
+
------
|
|
104
|
+
RuntimeError
|
|
105
|
+
If no authorization context is found in the request.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
AuthCtx
|
|
110
|
+
The authorization context associated with the current request.
|
|
111
|
+
"""
|
|
112
|
+
context = get_context()
|
|
113
|
+
|
|
114
|
+
auth_ctx = context.get_state(AUTH_CTX_KEY)
|
|
115
|
+
if not auth_ctx:
|
|
116
|
+
raise RuntimeError("Could not retrieve authorization context from FastMCP context state.")
|
|
117
|
+
|
|
118
|
+
return auth_ctx
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def get_access_token(provider_type: str | None = None) -> str:
|
|
122
|
+
"""Retrieve access token from the DataRobot OAuth Provider Service.
|
|
123
|
+
|
|
124
|
+
OAuth access tokens can be retrieved only for providers where the user completed
|
|
125
|
+
the OAuth flow and granted consent.
|
|
126
|
+
|
|
127
|
+
Note:
|
|
128
|
+
* Currently, only On-Behalf-Of (OBO) tokens are supported, which allow tools to
|
|
129
|
+
act on behalf of the authenticated user, after the user has granted his consent.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
provider_type : str, optional
|
|
134
|
+
The name of the OAuth provider. It should match the name of the provider configured
|
|
135
|
+
during provider setup. If no value is provided and only one OAuth provider exists, that
|
|
136
|
+
provider will be used. If multiple providers exist and none is specified, an error will be
|
|
137
|
+
raised.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
The oauth access token.
|
|
142
|
+
"""
|
|
143
|
+
auth_ctx = await must_get_auth_context()
|
|
144
|
+
logger.debug("Retrieved authorization context")
|
|
145
|
+
|
|
146
|
+
oauth_token_provider = AsyncOAuthTokenProvider(auth_ctx)
|
|
147
|
+
oauth_access_token = await oauth_token_provider.get_token(
|
|
148
|
+
auth_type=ToolAuth.OBO,
|
|
149
|
+
provider_type=provider_type,
|
|
150
|
+
)
|
|
151
|
+
return oauth_access_token
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def initialize_oauth_middleware(mcp: Any) -> None:
|
|
155
|
+
"""Initialize and register OAuth middleware with the MCP server.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
mcp : FastMCP
|
|
160
|
+
The FastMCP server instance to register the middleware with.
|
|
161
|
+
secret_key : Optional[str]
|
|
162
|
+
Secret key for JWT validation. If None, uses the value from config.
|
|
163
|
+
"""
|
|
164
|
+
mcp.add_middleware(OAuthMiddleWare())
|
|
165
|
+
logger.info("OAuth middleware registered successfully")
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Copyright 2025 DataRobot, Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any
|
|
17
|
+
from typing import cast
|
|
18
|
+
|
|
19
|
+
import datarobot as dr
|
|
20
|
+
from datarobot.context import Context as DRContext
|
|
21
|
+
from datarobot.rest import RESTClientObject
|
|
22
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
23
|
+
|
|
24
|
+
from datarobot_genai.core.utils.auth import AuthContextHeaderHandler
|
|
25
|
+
from datarobot_genai.core.utils.auth import DRAppCtx
|
|
26
|
+
|
|
27
|
+
from .credentials import get_credentials
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Header names to check for authorization tokens (in order of preference)
|
|
32
|
+
HEADER_TOKEN_CANDIDATE_NAMES = [
|
|
33
|
+
"authorization",
|
|
34
|
+
"x-datarobot-api-token",
|
|
35
|
+
"x-datarobot-api-key",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _extract_token_from_headers(headers: dict[str, str]) -> str | None:
|
|
40
|
+
"""
|
|
41
|
+
Extract a Bearer token from headers by checking multiple header name candidates.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
headers: Dictionary of headers (keys should be lowercase)
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
The extracted token string, or None if not found
|
|
49
|
+
"""
|
|
50
|
+
for candidate_name in HEADER_TOKEN_CANDIDATE_NAMES:
|
|
51
|
+
auth_header = headers.get(candidate_name)
|
|
52
|
+
if not auth_header:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
if not isinstance(auth_header, str):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Handle Bearer token format
|
|
59
|
+
bearer_prefix = "bearer "
|
|
60
|
+
if auth_header.lower().startswith(bearer_prefix):
|
|
61
|
+
token = auth_header[len(bearer_prefix) :].strip()
|
|
62
|
+
else:
|
|
63
|
+
# Assume it's a plain token
|
|
64
|
+
token = auth_header.strip()
|
|
65
|
+
|
|
66
|
+
if token:
|
|
67
|
+
return token
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _extract_token_from_auth_context(headers: dict[str, str]) -> str | None:
|
|
73
|
+
"""
|
|
74
|
+
Extract API token from authorization context metadata as a fallback.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
headers: Dictionary of headers (keys should be lowercase)
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
The extracted API key from auth context metadata, or None if not found
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
auth_handler = AuthContextHeaderHandler()
|
|
85
|
+
|
|
86
|
+
auth_ctx = auth_handler.get_context(headers)
|
|
87
|
+
if not auth_ctx or not auth_ctx.metadata:
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
metadata = auth_ctx.metadata
|
|
91
|
+
if not isinstance(metadata, dict):
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
dr_ctx: DRAppCtx = DRAppCtx(**metadata.get("dr_ctx", {}))
|
|
95
|
+
if dr_ctx.api_key:
|
|
96
|
+
logger.debug("Extracted token from auth context")
|
|
97
|
+
return dr_ctx.api_key
|
|
98
|
+
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.debug(f"Failed to get token from auth context: {e}")
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def extract_token_from_headers(headers: dict[str, str]) -> str | None:
|
|
107
|
+
"""
|
|
108
|
+
Extract a token from headers with multiple fallback strategies.
|
|
109
|
+
|
|
110
|
+
This function attempts to extract a token in the following order:
|
|
111
|
+
1. From standard authorization headers (Bearer token, x-datarobot-api-token, etc.)
|
|
112
|
+
2. From authorization context metadata (dr_ctx.api_key)
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
headers: Dictionary of headers (keys should be lowercase)
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
The extracted token string, or None if not found
|
|
120
|
+
"""
|
|
121
|
+
if token := _extract_token_from_headers(headers):
|
|
122
|
+
return token
|
|
123
|
+
|
|
124
|
+
if token := _extract_token_from_auth_context(headers):
|
|
125
|
+
return token
|
|
126
|
+
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_sdk_client() -> Any:
|
|
131
|
+
"""
|
|
132
|
+
Get a DataRobot SDK client, using the user's Bearer token from the request.
|
|
133
|
+
|
|
134
|
+
This function attempts to extract the Bearer token from the HTTP request headers
|
|
135
|
+
with fallback strategies:
|
|
136
|
+
1. Standard authorization headers (Bearer token, x-datarobot-api-token, etc.)
|
|
137
|
+
2. Authorization context metadata (dr_ctx.api_key)
|
|
138
|
+
3. Application credentials as final fallback
|
|
139
|
+
"""
|
|
140
|
+
token = None
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
headers = get_http_headers()
|
|
144
|
+
if headers:
|
|
145
|
+
token = extract_token_from_headers(headers)
|
|
146
|
+
if token:
|
|
147
|
+
logger.debug("Using API token found in HTTP headers")
|
|
148
|
+
except Exception:
|
|
149
|
+
# No HTTP context e.g. stdio transport
|
|
150
|
+
logger.warning(
|
|
151
|
+
"Could not get HTTP headers, falling back to application credentials", exc_info=True
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
credentials = get_credentials()
|
|
155
|
+
|
|
156
|
+
# Fallback: Use application token
|
|
157
|
+
if not token:
|
|
158
|
+
token = credentials.datarobot.application_api_token
|
|
159
|
+
logger.debug("Using application API token from credentials")
|
|
160
|
+
|
|
161
|
+
dr.Client(token=token, endpoint=credentials.datarobot.endpoint)
|
|
162
|
+
# The trafaret setting up a use case in the context, seem to mess up the tool calls
|
|
163
|
+
DRContext.use_case = None
|
|
164
|
+
return dr
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_api_client() -> RESTClientObject:
|
|
168
|
+
"""Get a DataRobot SDK api client using application credentials."""
|
|
169
|
+
dr = get_sdk_client()
|
|
170
|
+
|
|
171
|
+
return cast(RESTClientObject, dr.client.get_client())
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_s3_bucket_info() -> dict[str, str]:
|
|
175
|
+
"""Get S3 bucket configuration."""
|
|
176
|
+
credentials = get_credentials()
|
|
177
|
+
return {
|
|
178
|
+
"bucket": credentials.aws_predictions_s3_bucket,
|
|
179
|
+
"prefix": credentials.aws_predictions_s3_prefix,
|
|
180
|
+
}
|