datarobot-genai 0.2.0__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 (101) hide show
  1. datarobot_genai/__init__.py +19 -0
  2. datarobot_genai/core/__init__.py +0 -0
  3. datarobot_genai/core/agents/__init__.py +43 -0
  4. datarobot_genai/core/agents/base.py +195 -0
  5. datarobot_genai/core/chat/__init__.py +19 -0
  6. datarobot_genai/core/chat/auth.py +146 -0
  7. datarobot_genai/core/chat/client.py +178 -0
  8. datarobot_genai/core/chat/responses.py +297 -0
  9. datarobot_genai/core/cli/__init__.py +18 -0
  10. datarobot_genai/core/cli/agent_environment.py +47 -0
  11. datarobot_genai/core/cli/agent_kernel.py +211 -0
  12. datarobot_genai/core/custom_model.py +141 -0
  13. datarobot_genai/core/mcp/__init__.py +0 -0
  14. datarobot_genai/core/mcp/common.py +218 -0
  15. datarobot_genai/core/telemetry_agent.py +126 -0
  16. datarobot_genai/core/utils/__init__.py +3 -0
  17. datarobot_genai/core/utils/auth.py +234 -0
  18. datarobot_genai/core/utils/urls.py +64 -0
  19. datarobot_genai/crewai/__init__.py +24 -0
  20. datarobot_genai/crewai/agent.py +42 -0
  21. datarobot_genai/crewai/base.py +159 -0
  22. datarobot_genai/crewai/events.py +117 -0
  23. datarobot_genai/crewai/mcp.py +59 -0
  24. datarobot_genai/drmcp/__init__.py +78 -0
  25. datarobot_genai/drmcp/core/__init__.py +13 -0
  26. datarobot_genai/drmcp/core/auth.py +165 -0
  27. datarobot_genai/drmcp/core/clients.py +180 -0
  28. datarobot_genai/drmcp/core/config.py +250 -0
  29. datarobot_genai/drmcp/core/config_utils.py +174 -0
  30. datarobot_genai/drmcp/core/constants.py +18 -0
  31. datarobot_genai/drmcp/core/credentials.py +190 -0
  32. datarobot_genai/drmcp/core/dr_mcp_server.py +316 -0
  33. datarobot_genai/drmcp/core/dr_mcp_server_logo.py +136 -0
  34. datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +13 -0
  35. datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +130 -0
  36. datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +128 -0
  37. datarobot_genai/drmcp/core/dynamic_prompts/register.py +206 -0
  38. datarobot_genai/drmcp/core/dynamic_prompts/utils.py +33 -0
  39. datarobot_genai/drmcp/core/dynamic_tools/__init__.py +14 -0
  40. datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
  41. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +14 -0
  42. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +72 -0
  43. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +82 -0
  44. datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +238 -0
  45. datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +228 -0
  46. datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +63 -0
  47. datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +162 -0
  48. datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +87 -0
  49. datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +36 -0
  50. datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +10 -0
  51. datarobot_genai/drmcp/core/dynamic_tools/register.py +254 -0
  52. datarobot_genai/drmcp/core/dynamic_tools/schema.py +532 -0
  53. datarobot_genai/drmcp/core/exceptions.py +25 -0
  54. datarobot_genai/drmcp/core/logging.py +98 -0
  55. datarobot_genai/drmcp/core/mcp_instance.py +542 -0
  56. datarobot_genai/drmcp/core/mcp_server_tools.py +129 -0
  57. datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
  58. datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
  59. datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
  60. datarobot_genai/drmcp/core/routes.py +436 -0
  61. datarobot_genai/drmcp/core/routes_utils.py +30 -0
  62. datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
  63. datarobot_genai/drmcp/core/telemetry.py +424 -0
  64. datarobot_genai/drmcp/core/tool_filter.py +108 -0
  65. datarobot_genai/drmcp/core/utils.py +131 -0
  66. datarobot_genai/drmcp/server.py +19 -0
  67. datarobot_genai/drmcp/test_utils/__init__.py +13 -0
  68. datarobot_genai/drmcp/test_utils/integration_mcp_server.py +102 -0
  69. datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +96 -0
  70. datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +94 -0
  71. datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +234 -0
  72. datarobot_genai/drmcp/test_utils/tool_base_ete.py +151 -0
  73. datarobot_genai/drmcp/test_utils/utils.py +91 -0
  74. datarobot_genai/drmcp/tools/__init__.py +14 -0
  75. datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
  76. datarobot_genai/drmcp/tools/predictive/data.py +97 -0
  77. datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
  78. datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
  79. datarobot_genai/drmcp/tools/predictive/model.py +148 -0
  80. datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
  81. datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
  82. datarobot_genai/drmcp/tools/predictive/project.py +72 -0
  83. datarobot_genai/drmcp/tools/predictive/training.py +651 -0
  84. datarobot_genai/langgraph/__init__.py +0 -0
  85. datarobot_genai/langgraph/agent.py +341 -0
  86. datarobot_genai/langgraph/mcp.py +73 -0
  87. datarobot_genai/llama_index/__init__.py +16 -0
  88. datarobot_genai/llama_index/agent.py +50 -0
  89. datarobot_genai/llama_index/base.py +299 -0
  90. datarobot_genai/llama_index/mcp.py +79 -0
  91. datarobot_genai/nat/__init__.py +0 -0
  92. datarobot_genai/nat/agent.py +258 -0
  93. datarobot_genai/nat/datarobot_llm_clients.py +249 -0
  94. datarobot_genai/nat/datarobot_llm_providers.py +130 -0
  95. datarobot_genai/py.typed +0 -0
  96. datarobot_genai-0.2.0.dist-info/METADATA +139 -0
  97. datarobot_genai-0.2.0.dist-info/RECORD +101 -0
  98. datarobot_genai-0.2.0.dist-info/WHEEL +4 -0
  99. datarobot_genai-0.2.0.dist-info/entry_points.txt +3 -0
  100. datarobot_genai-0.2.0.dist-info/licenses/AUTHORS +2 -0
  101. datarobot_genai-0.2.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,250 @@
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 os
16
+ from typing import Any
17
+ from typing import Literal
18
+ from urllib.parse import urlparse
19
+ from urllib.parse import urlunparse
20
+
21
+ from fastmcp.settings import DuplicateBehavior
22
+ from pydantic import AliasChoices
23
+ from pydantic import Field
24
+ from pydantic import field_validator
25
+ from pydantic_settings import BaseSettings
26
+ from pydantic_settings import SettingsConfigDict
27
+
28
+ from .config_utils import extract_datarobot_dict_runtime_param_payload
29
+ from .config_utils import extract_datarobot_runtime_param_payload
30
+ from .constants import DEFAULT_DATAROBOT_ENDPOINT
31
+ from .constants import RUNTIME_PARAM_ENV_VAR_NAME_PREFIX
32
+
33
+
34
+ class MCPServerConfig(BaseSettings):
35
+ """MCP Server configuration using pydantic settings."""
36
+
37
+ mcp_server_name: str = Field(
38
+ default="datarobot-mcp-server",
39
+ validation_alias=AliasChoices(
40
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "MCP_SERVER_NAME",
41
+ "MCP_SERVER_NAME",
42
+ ),
43
+ description="Name of the MCP server",
44
+ )
45
+ mcp_server_port: int = Field(
46
+ default=8080,
47
+ validation_alias=AliasChoices(
48
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "MCP_SERVER_PORT",
49
+ "MCP_SERVER_PORT",
50
+ ),
51
+ description="Port number for the MCP server",
52
+ )
53
+ mcp_server_log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
54
+ default="WARNING",
55
+ validation_alias=AliasChoices(
56
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "MCP_SERVER_LOG_LEVEL",
57
+ "MCP_SERVER_LOG_LEVEL",
58
+ ),
59
+ description="Log level for the MCP server",
60
+ )
61
+ mcp_server_host: str = Field(
62
+ default="0.0.0.0",
63
+ validation_alias=AliasChoices(
64
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "MCP_SERVER_HOST",
65
+ "MCP_SERVER_HOST",
66
+ ),
67
+ description="Host address for the MCP server",
68
+ )
69
+ app_log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(
70
+ default="INFO",
71
+ validation_alias=AliasChoices(
72
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "APP_LOG_LEVEL",
73
+ "APP_LOG_LEVEL",
74
+ ),
75
+ description="App log level",
76
+ )
77
+ # When the server is run in a custom model, it is important to mount all routes under the
78
+ # prefix provided in the URL_PREFIX
79
+ mount_path: str = Field(default="/", alias="URL_PREFIX")
80
+
81
+ @staticmethod
82
+ def _get_default_otel_endpoint() -> str:
83
+ """Get the default OpenTelemetry endpoint e.g. https://app.datarobot.com/otel."""
84
+ parsed_url = urlparse(os.environ.get("DATAROBOT_ENDPOINT", DEFAULT_DATAROBOT_ENDPOINT))
85
+ stripped_url = (parsed_url.scheme, parsed_url.netloc, "otel", "", "", "")
86
+ return urlunparse(stripped_url)
87
+
88
+ otel_collector_base_url: str = Field(
89
+ default=_get_default_otel_endpoint(),
90
+ validation_alias=AliasChoices(
91
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "OTEL_COLLECTOR_BASE_URL",
92
+ "OTEL_COLLECTOR_BASE_URL",
93
+ ),
94
+ description="Base URL for the OpenTelemetry collector",
95
+ )
96
+ otel_entity_id: str = Field(
97
+ default="",
98
+ validation_alias=AliasChoices(
99
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "OTEL_ENTITY_ID",
100
+ "OTEL_ENTITY_ID",
101
+ ),
102
+ description="Entity ID for tracing",
103
+ )
104
+ otel_attributes: dict[str, Any] = Field(
105
+ default={},
106
+ validation_alias=AliasChoices(
107
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "OTEL_ATTRIBUTES",
108
+ "OTEL_ATTRIBUTES",
109
+ ),
110
+ description="Attributes for tracing (as JSON string)",
111
+ )
112
+ otel_enabled: bool = Field(
113
+ default=True,
114
+ validation_alias=AliasChoices(
115
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "OTEL_ENABLED",
116
+ "OTEL_ENABLED",
117
+ ),
118
+ description="Enable/disable OpenTelemetry",
119
+ )
120
+ otel_enabled_http_instrumentors: bool = Field(
121
+ default=False,
122
+ validation_alias=AliasChoices(
123
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "OTEL_ENABLED_HTTP_INSTRUMENTORS",
124
+ "OTEL_ENABLED_HTTP_INSTRUMENTORS",
125
+ ),
126
+ description="Enable/disable HTTP instrumentors",
127
+ )
128
+ mcp_server_register_dynamic_tools_on_startup: bool = Field(
129
+ default=False,
130
+ validation_alias=AliasChoices(
131
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "MCP_SERVER_REGISTER_DYNAMIC_TOOLS_ON_STARTUP",
132
+ "MCP_SERVER_REGISTER_DYNAMIC_TOOLS_ON_STARTUP",
133
+ ),
134
+ description="Register dynamic tools on startup. When enabled, the MCP server will "
135
+ "automatically register all DataRobot tool deployments as MCP tools during startup.",
136
+ )
137
+ tool_registration_allow_empty_schema: bool = Field(
138
+ default=False,
139
+ validation_alias=AliasChoices(
140
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "MCP_SERVER_TOOL_REGISTRATION_ALLOW_EMPTY_SCHEMA",
141
+ "MCP_SERVER_TOOL_REGISTRATION_ALLOW_EMPTY_SCHEMA",
142
+ ),
143
+ description="Allow registration of tools with no input parameters. When enabled, "
144
+ "tools can be registered with empty schemas for static endpoints that don't require any "
145
+ "inputs. "
146
+ "Disabled by default, as this is not typical use case and can hide potential issues with "
147
+ "schema.",
148
+ )
149
+ tool_registration_duplicate_behavior: DuplicateBehavior = Field(
150
+ default="warn",
151
+ validation_alias=AliasChoices(
152
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "MCP_SERVER_TOOL_REGISTRATION_DUPLICATE_BEHAVIOR",
153
+ "MCP_SERVER_TOOL_REGISTRATION_DUPLICATE_BEHAVIOR",
154
+ ),
155
+ description="Behavior when a tool with the same name already exists in the MCP server. "
156
+ " - 'warn': will log a warning and replace the existing tool. "
157
+ " - 'replace': will replace the existing tool without a warning. "
158
+ " - 'error': will raise an error and prevent registration. "
159
+ " - 'ignore': will skip registration of the new tool.",
160
+ )
161
+ mcp_server_register_dynamic_prompts_on_startup: bool = Field(
162
+ default=False,
163
+ validation_alias=AliasChoices(
164
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "MCP_SERVER_REGISTER_DYNAMIC_PROMPTS_ON_STARTUP",
165
+ "MCP_SERVER_REGISTER_DYNAMIC_PROMPTS_ON_STARTUP",
166
+ ),
167
+ description="Register dynamic prompts on startup. When enabled, the MCP server will "
168
+ "automatically register all prompts from DataRobot Prompt Management "
169
+ "as MCP prompts during startup.",
170
+ )
171
+ prompt_registration_duplicate_behavior: DuplicateBehavior = Field(
172
+ default="warn",
173
+ validation_alias=AliasChoices(
174
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "MCP_SERVER_PROMPT_REGISTRATION_DUPLICATE_BEHAVIOR",
175
+ "MCP_SERVER_PROMPT_REGISTRATION_DUPLICATE_BEHAVIOR",
176
+ ),
177
+ description="Behavior when a prompt with the same name already exists in the MCP server. "
178
+ " - 'warn': will log a warning and replace the existing tool. "
179
+ " - 'replace': will replace the existing tool without a warning. "
180
+ " - 'error': will raise an error and prevent registration. "
181
+ " - 'ignore': will skip registration of the new tool.",
182
+ )
183
+ enable_memory_management: bool = Field(
184
+ default=False,
185
+ validation_alias=AliasChoices(
186
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "ENABLE_MEMORY_MANAGEMENT",
187
+ "ENABLE_MEMORY_MANAGEMENT",
188
+ ),
189
+ description="Enable/disable memory management",
190
+ )
191
+ enable_predictive_tools: bool = Field(
192
+ default=True,
193
+ validation_alias=AliasChoices(
194
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "ENABLE_PREDICTIVE_TOOLS",
195
+ "ENABLE_PREDICTIVE_TOOLS",
196
+ ),
197
+ description="Enable/disable predictive tools",
198
+ )
199
+
200
+ @field_validator(
201
+ "otel_attributes",
202
+ mode="before",
203
+ )
204
+ @classmethod
205
+ def validate_dict_runtime_params(cls, v: Any) -> Any:
206
+ """Validate dict runtime parameters."""
207
+ return extract_datarobot_dict_runtime_param_payload(v)
208
+
209
+ @field_validator(
210
+ "mcp_server_name",
211
+ "mcp_server_log_level",
212
+ "app_log_level",
213
+ "otel_collector_base_url",
214
+ "otel_entity_id",
215
+ "otel_enabled",
216
+ "otel_enabled_http_instrumentors",
217
+ "enable_memory_management",
218
+ "tool_registration_allow_empty_schema",
219
+ "mcp_server_register_dynamic_tools_on_startup",
220
+ "tool_registration_duplicate_behavior",
221
+ "mcp_server_register_dynamic_prompts_on_startup",
222
+ "enable_predictive_tools",
223
+ mode="before",
224
+ )
225
+ @classmethod
226
+ def validate_runtime_params(cls, v: Any) -> Any:
227
+ """Validate runtime parameters."""
228
+ return extract_datarobot_runtime_param_payload(v)
229
+
230
+ model_config = SettingsConfigDict(
231
+ env_file=".env",
232
+ case_sensitive=False,
233
+ env_file_encoding="utf-8",
234
+ extra="ignore",
235
+ )
236
+
237
+
238
+ # Global configuration instance
239
+ _config: MCPServerConfig | None = None
240
+
241
+
242
+ def get_config() -> MCPServerConfig:
243
+ """Get the global configuration instance."""
244
+ # Use a local variable to avoid global statement warning
245
+ config = _config
246
+ if config is None:
247
+ config = MCPServerConfig()
248
+ # Update the global variable
249
+ globals()["_config"] = config
250
+ return config
@@ -0,0 +1,174 @@
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 json
16
+ from typing import Any
17
+
18
+ from pydantic_core import PydanticUseDefault
19
+
20
+
21
+ def extract_datarobot_runtime_param_payload(v: Any) -> Any:
22
+ """Extract payload from DataRobot runtime parameter JSON format.
23
+
24
+ DataRobot runtime parameters come in the format:
25
+ {"type":"string","payload":"value"} or {"type":"boolean","payload":false}
26
+
27
+ If payload is None, raises PydanticUseDefault so Pydantic uses the field default.
28
+
29
+ This function extracts the payload value for simple types (strings, booleans, numbers).
30
+ For dict types with nested JSON, use extract_datarobot_dict_runtime_param_payload instead.
31
+
32
+ Args:
33
+ v: The input value (may be a raw value, JSON string, or DataRobot runtime param format)
34
+
35
+ Returns
36
+ -------
37
+ The extracted payload value
38
+
39
+ Raises
40
+ ------
41
+ PydanticUseDefault: When payload is None, signaling Pydantic to use field default
42
+ """
43
+ # If it's a string, try to parse as JSON
44
+ if isinstance(v, str):
45
+ # Handle Python-style boolean strings (True/False) by converting to lowercase
46
+ v_normalized = v.lower() if v.lower() in ("true", "false") else v
47
+
48
+ try:
49
+ parsed = json.loads(v_normalized)
50
+ if isinstance(parsed, dict) and "payload" in parsed:
51
+ payload = parsed["payload"]
52
+ if payload is not None:
53
+ return payload
54
+ # If payload is None, use field default
55
+ raise PydanticUseDefault
56
+ # If it's a plain JSON value (boolean, number, etc.), return it
57
+ # This handles cases like "true", "false", "123", etc.
58
+ return parsed
59
+ except (json.JSONDecodeError, ValueError):
60
+ pass
61
+ return v
62
+
63
+
64
+ def extract_datarobot_dict_runtime_param_payload(v: Any) -> Any: # noqa: PLR0911
65
+ r"""Extract and parse dict from DataRobot runtime parameter JSON format.
66
+
67
+ DataRobot runtime parameters for dict fields come in the format:
68
+ {"type":"string","payload":"{\\"key\\":\\"value\\"}"}
69
+
70
+ The payload itself is a JSON string that needs to be parsed into a dict.
71
+ If payload is None, raises PydanticUseDefault so Pydantic uses the field default.
72
+
73
+ Args:
74
+ v: The input value (may be a dict, JSON string, or DataRobot runtime param format)
75
+
76
+ Returns
77
+ -------
78
+ The extracted and parsed dict
79
+
80
+ Raises
81
+ ------
82
+ PydanticUseDefault: When payload is None, signaling Pydantic to use field default
83
+ """
84
+ # If it's already a dict, check if it's in DataRobot format
85
+ if isinstance(v, dict):
86
+ # If it has "payload" key, it's in DataRobot format - extract it
87
+ if "payload" in v:
88
+ payload = v["payload"]
89
+ if payload is None:
90
+ raise PydanticUseDefault
91
+ # If payload is a string, parse it as JSON
92
+ if isinstance(payload, str):
93
+ try:
94
+ return json.loads(payload)
95
+ except (json.JSONDecodeError, ValueError):
96
+ return {}
97
+ return payload
98
+ # Otherwise, return as-is (already a plain dict)
99
+ return v
100
+
101
+ # If it's a string, try to parse as JSON
102
+ if isinstance(v, str):
103
+ try:
104
+ parsed = json.loads(v)
105
+ if isinstance(parsed, dict) and "payload" in parsed:
106
+ payload = parsed["payload"]
107
+ if payload is not None:
108
+ # If payload is a string, parse it as JSON
109
+ if isinstance(payload, str):
110
+ try:
111
+ return json.loads(payload)
112
+ except (json.JSONDecodeError, ValueError):
113
+ return {}
114
+ return payload
115
+ raise PydanticUseDefault
116
+ # If it's already a dict, return it
117
+ if isinstance(parsed, dict):
118
+ return parsed
119
+ except (json.JSONDecodeError, ValueError):
120
+ pass
121
+
122
+ # Fallback: return empty dict
123
+ return {}
124
+
125
+
126
+ def extract_datarobot_credential_runtime_param_payload(v: Any) -> Any:
127
+ """Extract credential payload from DataRobot runtime parameter JSON format.
128
+
129
+ DataRobot credential runtime parameters come in the format:
130
+ {"type":"credential","payload":{...}} where the payload is the full credential object.
131
+
132
+ For credential types, the entire dict is preserved.
133
+ If payload is None, raises PydanticUseDefault so Pydantic uses the field default.
134
+
135
+ Args:
136
+ v: The input value (may be a dict, JSON string, or DataRobot runtime param format)
137
+
138
+ Returns
139
+ -------
140
+ The extracted credential dict
141
+
142
+ Raises
143
+ ------
144
+ PydanticUseDefault: When payload is None, signaling Pydantic to use field default
145
+ """
146
+ # If it's already a dict (credential object or wrapped payload)
147
+ if isinstance(v, dict):
148
+ # If it's a wrapped payload, extract it
149
+ if "payload" in v:
150
+ payload = v["payload"]
151
+ if payload is not None:
152
+ return payload
153
+ # If payload is None, use field default
154
+ raise PydanticUseDefault
155
+ # Otherwise return the dict as-is (it's the credential object)
156
+ return v
157
+
158
+ # If it's a string, try to parse as JSON
159
+ if isinstance(v, str):
160
+ try:
161
+ parsed = json.loads(v)
162
+ if isinstance(parsed, dict):
163
+ # If it's a wrapped payload, extract it
164
+ if "payload" in parsed:
165
+ payload = parsed["payload"]
166
+ if payload is not None:
167
+ return payload
168
+ # If payload is None, use field default
169
+ raise PydanticUseDefault
170
+ # Otherwise return the dict as-is
171
+ return parsed
172
+ except (json.JSONDecodeError, ValueError):
173
+ pass
174
+ return v
@@ -0,0 +1,18 @@
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
+ MAX_INLINE_SIZE = 1024 * 1024 # 1MB
17
+ DEFAULT_DATAROBOT_ENDPOINT = "https://app.datarobot.com/api/v2"
18
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX = "MLOPS_RUNTIME_PARAM_"
@@ -0,0 +1,190 @@
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
+
18
+ from pydantic import AliasChoices
19
+ from pydantic import Field
20
+ from pydantic import field_validator
21
+ from pydantic_settings import BaseSettings
22
+ from pydantic_settings import SettingsConfigDict
23
+
24
+ from .config_utils import extract_datarobot_credential_runtime_param_payload
25
+ from .config_utils import extract_datarobot_runtime_param_payload
26
+ from .constants import DEFAULT_DATAROBOT_ENDPOINT
27
+ from .constants import RUNTIME_PARAM_ENV_VAR_NAME_PREFIX
28
+
29
+
30
+ class DataRobotCredentials(BaseSettings):
31
+ """DataRobot API credentials."""
32
+
33
+ application_api_token: str = Field(
34
+ default="",
35
+ validation_alias=AliasChoices(
36
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "DATAROBOT_API_TOKEN",
37
+ "DATAROBOT_API_TOKEN",
38
+ ),
39
+ description="DataRobot API token",
40
+ )
41
+ endpoint: str = Field(
42
+ default=DEFAULT_DATAROBOT_ENDPOINT,
43
+ validation_alias=AliasChoices(
44
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "DATAROBOT_ENDPOINT",
45
+ "DATAROBOT_ENDPOINT",
46
+ ),
47
+ description="DataRobot API endpoint",
48
+ )
49
+
50
+ @field_validator(
51
+ "application_api_token",
52
+ "endpoint",
53
+ mode="before",
54
+ )
55
+ @classmethod
56
+ def validate_runtime_params(cls, v: Any) -> Any:
57
+ """Validate runtime parameters."""
58
+ return extract_datarobot_runtime_param_payload(v)
59
+
60
+ model_config = SettingsConfigDict(
61
+ env_file=".env",
62
+ case_sensitive=False,
63
+ env_file_encoding="utf-8",
64
+ extra="ignore",
65
+ )
66
+
67
+
68
+ class MCPServerCredentials(BaseSettings):
69
+ """Application credentials combining DataRobot and AWS credentials."""
70
+
71
+ datarobot: DataRobotCredentials = Field(default_factory=DataRobotCredentials)
72
+
73
+ # AWS Credentials - loaded from DataRobot credential object via aws_credential runtime parameter
74
+ aws_credential: dict[str, Any] | None = Field(
75
+ default=None,
76
+ validation_alias=AliasChoices(
77
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "AWS_CREDENTIAL",
78
+ "AWS_CREDENTIAL",
79
+ ),
80
+ description="DataRobot AWS Credential object (contains awsAccessKeyId, "
81
+ "awsSecretAccessKey, awsSessionToken)",
82
+ )
83
+ # AWS credentials are also available as direct environment variables for local development
84
+ aws_access_key_id: str | None = Field(
85
+ default=None,
86
+ alias="AWS_ACCESS_KEY_ID",
87
+ description="AWS Access Key ID (direct, for local use)",
88
+ )
89
+ aws_secret_access_key: str | None = Field(
90
+ default=None,
91
+ alias="AWS_SECRET_ACCESS_KEY",
92
+ description="AWS Secret Access Key (direct, for local use)",
93
+ )
94
+ aws_session_token: str | None = Field(
95
+ default=None,
96
+ alias="AWS_SESSION_TOKEN",
97
+ description="AWS Session Token (direct, for local use)",
98
+ )
99
+
100
+ aws_predictions_s3_bucket: str = Field(
101
+ default="datarobot-rd",
102
+ validation_alias=AliasChoices(
103
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "AWS_PREDICTIONS_S3_BUCKET",
104
+ "AWS_PREDICTIONS_S3_BUCKET",
105
+ ),
106
+ description="S3 bucket name",
107
+ )
108
+ aws_predictions_s3_prefix: str = Field(
109
+ default="dev/mcp-temp-storage/predictions/",
110
+ validation_alias=AliasChoices(
111
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "AWS_PREDICTIONS_S3_PREFIX",
112
+ "AWS_PREDICTIONS_S3_PREFIX",
113
+ ),
114
+ description="S3 key prefix",
115
+ )
116
+
117
+ @field_validator(
118
+ "aws_credential",
119
+ "aws_predictions_s3_bucket",
120
+ "aws_predictions_s3_prefix",
121
+ mode="before",
122
+ )
123
+ @classmethod
124
+ def validate_credential_runtime_params(cls, v: Any) -> Any:
125
+ """Validate credential runtime parameters."""
126
+ return extract_datarobot_credential_runtime_param_payload(v)
127
+
128
+ model_config = SettingsConfigDict(
129
+ env_file=".env",
130
+ case_sensitive=False,
131
+ env_file_encoding="utf-8",
132
+ extra="ignore",
133
+ )
134
+
135
+ def has_aws_credentials(self) -> bool:
136
+ """Check if AWS credentials are configured (either direct or via credential object)."""
137
+ return bool((self.aws_access_key_id and self.aws_secret_access_key) or self.aws_credential)
138
+
139
+ def has_datarobot_credentials(self) -> bool:
140
+ """Check if DataRobot credentials are configured."""
141
+ return bool(self.datarobot.application_api_token)
142
+
143
+ def get_aws_credentials(self) -> tuple[str | None, str | None, str | None]:
144
+ """Get AWS credentials (access_key_id, secret_access_key, session_token).
145
+
146
+ If aws_credential dict is set, extracts credentials from it.
147
+ Otherwise, returns the direct environment variable values.
148
+
149
+ Returns
150
+ -------
151
+ Tuple of (access_key_id, secret_access_key, session_token)
152
+ """
153
+ # If credentials are provided directly (local development), use them
154
+ if self.aws_access_key_id and self.aws_secret_access_key:
155
+ return (
156
+ self.aws_access_key_id,
157
+ self.aws_secret_access_key,
158
+ self.aws_session_token,
159
+ )
160
+
161
+ # If credential object is provided, extract keys from it
162
+ if self.aws_credential:
163
+ try:
164
+ return (
165
+ self.aws_credential.get("awsAccessKeyId"),
166
+ self.aws_credential.get("awsSecretAccessKey"),
167
+ self.aws_credential.get("awsSessionToken"),
168
+ )
169
+ except Exception as e:
170
+ logging.getLogger(__name__).warning(
171
+ f"Failed to extract AWS credentials from credential object: {e}"
172
+ )
173
+ return (None, None, None)
174
+
175
+ return (None, None, None)
176
+
177
+
178
+ # Global credentials instance
179
+ _credentials: MCPServerCredentials | None = None
180
+
181
+
182
+ def get_credentials() -> MCPServerCredentials:
183
+ """Get the global credentials instance."""
184
+ # Use a local variable to avoid global statement warning
185
+ credentials = _credentials
186
+ if credentials is None:
187
+ credentials = MCPServerCredentials()
188
+ # Update the global variable
189
+ globals()["_credentials"] = credentials
190
+ return credentials