datarobot-genai 0.2.0__py3-none-any.whl → 0.2.5__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 (25) hide show
  1. datarobot_genai/drmcp/core/config.py +52 -0
  2. datarobot_genai/drmcp/core/dr_mcp_server.py +45 -8
  3. datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +22 -80
  4. datarobot_genai/drmcp/core/dynamic_prompts/register.py +4 -5
  5. datarobot_genai/drmcp/core/mcp_instance.py +41 -2
  6. datarobot_genai/drmcp/core/routes.py +4 -1
  7. datarobot_genai/drmcp/core/tool_config.py +95 -0
  8. datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +29 -0
  9. datarobot_genai/drmcp/tools/clients/__init__.py +14 -0
  10. datarobot_genai/drmcp/tools/clients/atlassian.py +188 -0
  11. datarobot_genai/drmcp/tools/clients/confluence.py +14 -0
  12. datarobot_genai/drmcp/tools/clients/jira.py +102 -0
  13. datarobot_genai/drmcp/tools/clients/s3.py +28 -0
  14. datarobot_genai/drmcp/tools/confluence/__init__.py +14 -0
  15. datarobot_genai/drmcp/tools/confluence/tools.py +13 -0
  16. datarobot_genai/drmcp/tools/jira/__init__.py +14 -0
  17. datarobot_genai/drmcp/tools/jira/tools.py +58 -0
  18. datarobot_genai/drmcp/tools/predictive/predict.py +1 -1
  19. datarobot_genai/drmcp/tools/predictive/predict_realtime.py +1 -1
  20. {datarobot_genai-0.2.0.dist-info → datarobot_genai-0.2.5.dist-info}/METADATA +5 -2
  21. {datarobot_genai-0.2.0.dist-info → datarobot_genai-0.2.5.dist-info}/RECORD +25 -15
  22. {datarobot_genai-0.2.0.dist-info → datarobot_genai-0.2.5.dist-info}/WHEEL +0 -0
  23. {datarobot_genai-0.2.0.dist-info → datarobot_genai-0.2.5.dist-info}/entry_points.txt +0 -0
  24. {datarobot_genai-0.2.0.dist-info → datarobot_genai-0.2.5.dist-info}/licenses/AUTHORS +0 -0
  25. {datarobot_genai-0.2.0.dist-info → datarobot_genai-0.2.5.dist-info}/licenses/LICENSE +0 -0
@@ -197,6 +197,54 @@ class MCPServerConfig(BaseSettings):
197
197
  description="Enable/disable predictive tools",
198
198
  )
199
199
 
200
+ # Jira tools
201
+ enable_jira_tools: bool = Field(
202
+ default=False,
203
+ validation_alias=AliasChoices(
204
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "ENABLE_JIRA_TOOLS",
205
+ "ENABLE_JIRA_TOOLS",
206
+ ),
207
+ description="Enable/disable Jira tools",
208
+ )
209
+ is_jira_oauth_provider_configured: bool = Field(
210
+ default=False,
211
+ validation_alias=AliasChoices(
212
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "IS_JIRA_OAUTH_PROVIDER_CONFIGURED",
213
+ "IS_JIRA_OAUTH_PROVIDER_CONFIGURED",
214
+ ),
215
+ description="Whether Jira OAuth provider is configured for Jira integration",
216
+ )
217
+
218
+ @property
219
+ def is_jira_oauth_configured(self) -> bool:
220
+ return self.is_jira_oauth_provider_configured or bool(
221
+ os.getenv("JIRA_CLIENT_ID") and os.getenv("JIRA_CLIENT_SECRET")
222
+ )
223
+
224
+ # Confluence tools
225
+ enable_confluence_tools: bool = Field(
226
+ default=False,
227
+ validation_alias=AliasChoices(
228
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "ENABLE_CONFLUENCE_TOOLS",
229
+ "ENABLE_CONFLUENCE_TOOLS",
230
+ ),
231
+ description="Enable/disable Confluence tools",
232
+ )
233
+ is_confluence_oauth_provider_configured: bool = Field(
234
+ default=False,
235
+ validation_alias=AliasChoices(
236
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "IS_CONFLUENCE_OAUTH_PROVIDER_CONFIGURED",
237
+ "IS_CONFLUENCE_OAUTH_PROVIDER_CONFIGURED",
238
+ ),
239
+ description="Whether Confluence OAuth provider is configured for Confluence integration",
240
+ )
241
+
242
+ @property
243
+ def is_confluence_oauth_configured(self) -> bool:
244
+ return self.is_confluence_oauth_provider_configured or bool(
245
+ os.getenv("CONFLUENCE_CLIENT_ID") and os.getenv("CONFLUENCE_CLIENT_SECRET")
246
+ )
247
+
200
248
  @field_validator(
201
249
  "otel_attributes",
202
250
  mode="before",
@@ -220,6 +268,10 @@ class MCPServerConfig(BaseSettings):
220
268
  "tool_registration_duplicate_behavior",
221
269
  "mcp_server_register_dynamic_prompts_on_startup",
222
270
  "enable_predictive_tools",
271
+ "enable_jira_tools",
272
+ "is_jira_oauth_provider_configured",
273
+ "enable_confluence_tools",
274
+ "is_confluence_oauth_provider_configured",
223
275
  mode="before",
224
276
  )
225
277
  @classmethod
@@ -40,6 +40,8 @@ from .routes_utils import prefix_mount_path
40
40
  from .server_life_cycle import BaseServerLifecycle
41
41
  from .telemetry import OtelASGIMiddleware
42
42
  from .telemetry import initialize_telemetry
43
+ from .tool_config import TOOL_CONFIGS
44
+ from .tool_config import is_tool_enabled
43
45
 
44
46
 
45
47
  def _import_modules_from_dir(
@@ -115,6 +117,9 @@ class DataRobotMCPServer:
115
117
  self._mcp = mcp
116
118
  self._mcp_transport = transport
117
119
 
120
+ # Configure MCP server capabilities
121
+ self._configure_mcp_capabilities()
122
+
118
123
  # Initialize telemetry
119
124
  initialize_telemetry(mcp)
120
125
 
@@ -139,11 +144,12 @@ class DataRobotMCPServer:
139
144
 
140
145
  # Load static tools modules
141
146
  base_dir = os.path.dirname(os.path.dirname(__file__))
142
- if self._config.enable_predictive_tools:
143
- _import_modules_from_dir(
144
- os.path.join(base_dir, "tools", "predictive"),
145
- "datarobot_genai.drmcp.tools.predictive",
146
- )
147
+ for tool_type, tool_config in TOOL_CONFIGS.items():
148
+ if is_tool_enabled(tool_type, self._config):
149
+ _import_modules_from_dir(
150
+ os.path.join(base_dir, "tools", tool_config["directory"]),
151
+ tool_config["package_prefix"],
152
+ )
147
153
 
148
154
  # Load memory management tools if available
149
155
  if self._memory_manager:
@@ -163,6 +169,37 @@ class DataRobotMCPServer:
163
169
  if transport == "streamable-http":
164
170
  register_routes(self._mcp)
165
171
 
172
+ def _configure_mcp_capabilities(self) -> None:
173
+ """Configure MCP capabilities that FastMCP doesn't expose directly.
174
+
175
+ See: https://github.com/modelcontextprotocol/python-sdk/issues/1126
176
+ """
177
+ server = self._mcp._mcp_server
178
+
179
+ # Declare prompts_changed capability (capabilities.prompts.listChanged: true)
180
+ server.notification_options.prompts_changed = True
181
+
182
+ # Declare experimental capabilities ( experimental.dynamic_prompts: true)
183
+ server.experimental_capabilities = {"dynamic_prompts": {"enabled": True}}
184
+
185
+ # Patch to include experimental_capabilities (FastMCP doesn't expose this)
186
+ original = server.create_initialization_options
187
+
188
+ def patched(
189
+ notification_options: Any = None,
190
+ experimental_capabilities: dict[str, dict[str, Any]] | None = None,
191
+ **kwargs: Any,
192
+ ) -> Any:
193
+ if experimental_capabilities is None:
194
+ experimental_capabilities = getattr(server, "experimental_capabilities", None)
195
+ return original(
196
+ notification_options=notification_options,
197
+ experimental_capabilities=experimental_capabilities,
198
+ **kwargs,
199
+ )
200
+
201
+ server.create_initialization_options = patched
202
+
166
203
  def run(self, show_banner: bool = False) -> None:
167
204
  """Run the DataRobot MCP server synchronously."""
168
205
  try:
@@ -179,6 +216,9 @@ class DataRobotMCPServer:
179
216
  self._logger.info("Registering dynamic prompts from prompt management...")
180
217
  asyncio.run(register_prompts_from_datarobot_prompt_management())
181
218
 
219
+ # Execute pre-server start actions
220
+ asyncio.run(self._lifecycle.pre_server_start(self._mcp))
221
+
182
222
  # List registered tools, prompts, and resources before starting server
183
223
  tools = asyncio.run(self._mcp._list_tools_mcp())
184
224
  prompts = asyncio.run(self._mcp._list_prompts_mcp())
@@ -198,9 +238,6 @@ class DataRobotMCPServer:
198
238
  for resource in resources:
199
239
  self._logger.info(f" > {resource.name}")
200
240
 
201
- # Execute pre-server start actions
202
- asyncio.run(self._lifecycle.pre_server_start(self._mcp))
203
-
204
241
  # Create event loop for async operations
205
242
  loop = asyncio.new_event_loop()
206
243
  asyncio.set_event_loop(loop)
@@ -12,78 +12,23 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  from collections import defaultdict
15
- from dataclasses import dataclass
16
15
 
17
16
  import datarobot as dr
18
17
 
19
18
  from datarobot_genai.drmcp.core.clients import get_api_client
20
19
 
21
- # Needed SDK version (3.10.0) is not published yet. We'll reimplement simplified version of it.
22
- # get_datarobot_prompt_templates = dr.genai.PromptTemplate.list()
23
- # DrPrompt = dr.genai.PromptTemplate
24
- # DrPromptVersion = dr.genai.PromptTemplateVersion
25
- # DrVariable = dr.genai.Variable
26
20
 
27
-
28
- @dataclass
29
- class DrVariable:
30
- name: str
31
- description: str
32
-
33
-
34
- @dataclass
35
- class DrPromptVersion:
36
- id: str
37
- prompt_template_id: str
38
- version: int
39
- prompt_text: str
40
- variables: list[DrVariable]
41
-
42
- @classmethod
43
- def from_dict(cls, d: dict) -> "DrPromptVersion":
44
- variables = [
45
- DrVariable(name=v["name"], description=v["description"]) for v in d["variables"]
46
- ]
47
- return cls(
48
- id=d["id"],
49
- prompt_template_id=d["promptTemplateId"],
50
- version=d["version"],
51
- prompt_text=d["promptText"],
52
- variables=variables,
53
- )
54
-
55
-
56
- @dataclass
57
- class DrPrompt:
58
- id: str
59
- name: str
60
- description: str
61
-
62
- def get_latest_version(self) -> DrPromptVersion | None:
63
- all_prompt_template_versions = get_datarobot_prompt_template_versions([self.id])
64
- prompt_template_versions = all_prompt_template_versions.get(self.id)
65
-
66
- if not prompt_template_versions:
67
- return None
68
- latest_version = max(prompt_template_versions, key=lambda v: v.version)
69
- return latest_version
70
-
71
- @classmethod
72
- def from_dict(cls, d: dict) -> "DrPrompt":
73
- return cls(id=d["id"], name=d["name"], description=d["description"])
74
-
75
-
76
- def get_datarobot_prompt_templates() -> list[DrPrompt]:
77
- prompt_templates_data = dr.utils.pagination.unpaginate(
78
- initial_url="genai/promptTemplates/", initial_params={}, client=get_api_client()
79
- )
80
-
81
- return [DrPrompt.from_dict(prompt_template) for prompt_template in prompt_templates_data]
21
+ def get_datarobot_prompt_templates() -> list[dr.genai.PromptTemplate]:
22
+ try:
23
+ return dr.genai.PromptTemplate.list()
24
+ except Exception:
25
+ return []
82
26
 
83
27
 
84
28
  def get_datarobot_prompt_template_versions(
85
29
  prompt_template_ids: list[str],
86
- ) -> dict[str, list[DrPromptVersion]]:
30
+ ) -> dict[str, list[dr.genai.PromptTemplateVersion]]:
31
+ # Still missing in SDK
87
32
  prompt_template_versions_data = dr.utils.pagination.unpaginate(
88
33
  initial_url="genai/promptTemplates/versions/",
89
34
  initial_params={
@@ -94,35 +39,32 @@ def get_datarobot_prompt_template_versions(
94
39
  prompt_template_versions = defaultdict(list)
95
40
  for prompt_template_version in prompt_template_versions_data:
96
41
  prompt_template_versions[prompt_template_version["promptTemplateId"]].append(
97
- DrPromptVersion.from_dict(prompt_template_version)
42
+ dr.genai.PromptTemplateVersion(
43
+ id=prompt_template_version["id"],
44
+ prompt_template_id=prompt_template_version["promptTemplateId"],
45
+ prompt_text=prompt_template_version["promptText"],
46
+ commit_comment=prompt_template_version["commitComment"],
47
+ version=prompt_template_version["version"],
48
+ variables=prompt_template_version["variables"],
49
+ creation_date=prompt_template_version["creationDate"],
50
+ creation_user_id=prompt_template_version["creationUserId"],
51
+ user_name=prompt_template_version["userName"],
52
+ )
98
53
  )
99
54
  return prompt_template_versions
100
55
 
101
56
 
102
- def get_datarobot_prompt_template(prompt_template_id: str) -> DrPrompt | None:
103
- api_client = get_api_client()
57
+ def get_datarobot_prompt_template(prompt_template_id: str) -> dr.genai.PromptTemplate | None:
104
58
  try:
105
- prompt_template_response = api_client.get(
106
- f"genai/promptTemplates/{prompt_template_id}/", join_endpoint=True
107
- )
108
- prompt_template_json = prompt_template_response.json()
59
+ return dr.genai.PromptTemplate.get(prompt_template_id)
109
60
  except Exception:
110
61
  return None
111
62
 
112
- return DrPrompt.from_dict(prompt_template_json)
113
-
114
63
 
115
64
  def get_datarobot_prompt_template_version(
116
65
  prompt_template_id: str, prompt_template_version_id: str
117
- ) -> DrPromptVersion | None:
118
- api_client = get_api_client()
66
+ ) -> dr.genai.PromptTemplateVersion | None:
119
67
  try:
120
- prompt_template_version_response = api_client.get(
121
- f"genai/promptTemplates/{prompt_template_id}/versions/{prompt_template_version_id}/",
122
- join_endpoint=True,
123
- )
124
- prompt_template_version_json = prompt_template_version_response.json()
68
+ return dr.genai.PromptTemplateVersion.get(prompt_template_id, prompt_template_version_id)
125
69
  except Exception:
126
70
  return None
127
-
128
- return DrPromptVersion.from_dict(prompt_template_version_json)
@@ -18,15 +18,13 @@ from collections.abc import Callable
18
18
  from inspect import Parameter
19
19
  from inspect import Signature
20
20
 
21
+ import datarobot as dr
21
22
  from fastmcp.prompts.prompt import Prompt
22
23
  from pydantic import Field
23
24
 
24
25
  from datarobot_genai.drmcp.core.exceptions import DynamicPromptRegistrationError
25
26
  from datarobot_genai.drmcp.core.mcp_instance import register_prompt
26
27
 
27
- from .dr_lib import DrPrompt
28
- from .dr_lib import DrPromptVersion
29
- from .dr_lib import DrVariable
30
28
  from .dr_lib import get_datarobot_prompt_template_versions
31
29
  from .dr_lib import get_datarobot_prompt_templates
32
30
 
@@ -57,7 +55,8 @@ async def register_prompts_from_datarobot_prompt_management() -> None:
57
55
 
58
56
 
59
57
  async def register_prompt_from_datarobot_prompt_management(
60
- prompt_template: DrPrompt, prompt_template_version: DrPromptVersion | None = None
58
+ prompt_template: dr.genai.PromptTemplate,
59
+ prompt_template_version: dr.genai.PromptTemplateVersion | None = None,
61
60
  ) -> Prompt:
62
61
  """Register a single prompt.
63
62
 
@@ -173,7 +172,7 @@ def to_valid_mcp_prompt_name(s: str) -> str:
173
172
 
174
173
 
175
174
  def make_prompt_function(
176
- name: str, description: str, prompt_text: str, variables: list[DrVariable]
175
+ name: str, description: str, prompt_text: str, variables: list[dr.genai.Variable]
177
176
  ) -> Callable:
178
177
  params = []
179
178
  for v in variables:
@@ -22,6 +22,7 @@ from fastmcp import Context
22
22
  from fastmcp import FastMCP
23
23
  from fastmcp.exceptions import NotFoundError
24
24
  from fastmcp.prompts.prompt import Prompt
25
+ from fastmcp.server.dependencies import get_context
25
26
  from fastmcp.tools import FunctionTool
26
27
  from fastmcp.tools import Tool
27
28
  from fastmcp.utilities.types import NotSet
@@ -91,6 +92,34 @@ class TaggedFastMCP(FastMCP):
91
92
  self._deployments_map: dict[str, str] = {}
92
93
  self._prompts_map: dict[str, tuple[str, str]] = {}
93
94
 
95
+ async def notify_prompts_changed(self) -> None:
96
+ """
97
+ Notify connected clients that the prompt list has changed.
98
+
99
+ This method attempts to send a prompts/list_changed notification to inform
100
+ clients that they should refresh their prompt list.
101
+
102
+ Note: In stateless HTTP mode (default for this server), notifications may not
103
+ reach clients since each request is independent. This method still logs the
104
+ change for auditing purposes and will work if the server is configured for
105
+ stateful connections.
106
+
107
+ See: https://github.com/modelcontextprotocol/python-sdk/issues/710
108
+ """
109
+ logger.info("Prompt list changed - attempting to notify connected clients")
110
+
111
+ # Try to use FastMCP's built-in notification mechanism if in an MCP context
112
+ try:
113
+ context = get_context()
114
+ context._queue_prompt_list_changed()
115
+ logger.debug("Queued prompts_changed notification via MCP context")
116
+ except RuntimeError:
117
+ # No active MCP context - this is expected when called from REST API
118
+ logger.debug(
119
+ "No active MCP context for notification. "
120
+ "In stateless mode, clients will see changes on next request."
121
+ )
122
+
94
123
  @overload
95
124
  def tool(
96
125
  self,
@@ -286,6 +315,9 @@ class TaggedFastMCP(FastMCP):
286
315
  f"already mapped to {existing_prompt_template_version_id}. "
287
316
  f"Updating to version id = {prompt_template_version_id} and name = {prompt_name}"
288
317
  )
318
+ await self.remove_prompt_mapping(
319
+ prompt_template_id, existing_prompt_template_version_id
320
+ )
289
321
 
290
322
  self._prompts_map[prompt_template_id] = (prompt_template_version_id, prompt_name)
291
323
 
@@ -308,7 +340,7 @@ class TaggedFastMCP(FastMCP):
308
340
  f"skipping removal."
309
341
  )
310
342
  else:
311
- prompts_d = await mcp.get_prompts()
343
+ prompts_d = await self.get_prompts()
312
344
  for prompt in prompts_d.values():
313
345
  if (
314
346
  prompt.meta is not None
@@ -319,6 +351,9 @@ class TaggedFastMCP(FastMCP):
319
351
  prompt.disable()
320
352
 
321
353
  self._prompts_map.pop(prompt_template_id, None)
354
+
355
+ # Notify clients that the prompt list has changed
356
+ await self.notify_prompts_changed()
322
357
  else:
323
358
  logger.debug(
324
359
  f"Do not found prompt template with id = {prompt_template_id} in registry, "
@@ -526,17 +561,21 @@ async def register_prompt(
526
561
  )
527
562
 
528
563
  # Register the prompt
529
- registered_prompt = mcp.add_prompt(prompt)
530
564
  if prompt_template:
531
565
  prompt_template_id, prompt_template_version_id = prompt_template
532
566
  await mcp.set_prompt_mapping(
533
567
  prompt_template_id, prompt_template_version_id, prompt_name_no_duplicate
534
568
  )
535
569
 
570
+ registered_prompt = mcp.add_prompt(prompt)
571
+
536
572
  # Verify prompt is registered
537
573
  prompts = await mcp.get_prompts()
538
574
  if not any(prompt.name == prompt_name_no_duplicate for prompt in prompts.values()):
539
575
  raise RuntimeError(f"Prompt {prompt_name_no_duplicate} was not registered successfully")
540
576
  logger.info(f"Registered prompts: {len(prompts)}")
541
577
 
578
+ # Notify clients that the prompt list has changed
579
+ await mcp.notify_prompts_changed()
580
+
542
581
  return registered_prompt
@@ -428,7 +428,10 @@ def register_routes(mcp: TaggedFastMCP) -> None:
428
428
  """Refresh prompt templates."""
429
429
  try:
430
430
  await refresh_registered_prompt_template()
431
- return JSONResponse(status_code=HTTPStatus.NO_CONTENT, content=None)
431
+ return JSONResponse(
432
+ status_code=HTTPStatus.OK,
433
+ content={"message": "Prompts refreshed successfully"},
434
+ )
432
435
  except Exception as e:
433
436
  return JSONResponse(
434
437
  status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
@@ -0,0 +1,95 @@
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
+ """Tool configuration and enablement logic."""
16
+
17
+ from collections.abc import Callable
18
+ from enum import Enum
19
+ from typing import TYPE_CHECKING
20
+ from typing import TypedDict
21
+
22
+ if TYPE_CHECKING:
23
+ from .config import MCPServerConfig
24
+
25
+
26
+ class ToolType(str, Enum):
27
+ """Enumeration of available tool types."""
28
+
29
+ PREDICTIVE = "predictive"
30
+ JIRA = "jira"
31
+ CONFLUENCE = "confluence"
32
+
33
+
34
+ class ToolConfig(TypedDict):
35
+ """Configuration for a tool type."""
36
+
37
+ name: str
38
+ oauth_check: Callable[["MCPServerConfig"], bool] | None
39
+ directory: str
40
+ package_prefix: str
41
+ config_field_name: str # Name of the config field (e.g., "enable_predictive_tools")
42
+
43
+
44
+ # Tool configuration registry
45
+ TOOL_CONFIGS: dict[ToolType, ToolConfig] = {
46
+ ToolType.PREDICTIVE: ToolConfig(
47
+ name="predictive",
48
+ oauth_check=None,
49
+ directory="predictive",
50
+ package_prefix="datarobot_genai.drmcp.tools.predictive",
51
+ config_field_name="enable_predictive_tools",
52
+ ),
53
+ ToolType.JIRA: ToolConfig(
54
+ name="jira",
55
+ oauth_check=lambda config: config.is_jira_oauth_configured,
56
+ directory="jira",
57
+ package_prefix="datarobot_genai.drmcp.tools.jira",
58
+ config_field_name="enable_jira_tools",
59
+ ),
60
+ ToolType.CONFLUENCE: ToolConfig(
61
+ name="confluence",
62
+ oauth_check=lambda config: config.is_confluence_oauth_configured,
63
+ directory="confluence",
64
+ package_prefix="datarobot_genai.drmcp.tools.confluence",
65
+ config_field_name="enable_confluence_tools",
66
+ ),
67
+ }
68
+
69
+
70
+ def get_tool_enable_config_name(tool_type: ToolType) -> str:
71
+ """Get the configuration field name for enabling a tool."""
72
+ return TOOL_CONFIGS[tool_type]["config_field_name"]
73
+
74
+
75
+ def is_tool_enabled(tool_type: ToolType, config: "MCPServerConfig") -> bool:
76
+ """
77
+ Check if a tool is enabled based on configuration.
78
+
79
+ Args:
80
+ tool_type: The type of tool to check
81
+ config: The server configuration
82
+
83
+ Returns
84
+ -------
85
+ True if the tool is enabled, False otherwise
86
+ """
87
+ tool_config = TOOL_CONFIGS[tool_type]
88
+ enable_config_name = tool_config["config_field_name"]
89
+ is_enabled = getattr(config, enable_config_name)
90
+
91
+ # If tool is enabled, check OAuth requirements if needed
92
+ if is_enabled and tool_config["oauth_check"] is not None:
93
+ return tool_config["oauth_check"](config)
94
+
95
+ return is_enabled
@@ -16,6 +16,8 @@ import os
16
16
  from collections.abc import AsyncGenerator
17
17
  from contextlib import asynccontextmanager
18
18
 
19
+ import aiohttp
20
+ from aiohttp import ClientSession as HttpClientSession
19
21
  from mcp import ClientSession
20
22
  from mcp.client.streamable_http import streamablehttp_client
21
23
 
@@ -29,6 +31,11 @@ def get_dr_mcp_server_url() -> str | None:
29
31
  return os.environ.get("DR_MCP_SERVER_URL")
30
32
 
31
33
 
34
+ def get_dr_mcp_server_http_url() -> str | None:
35
+ """Get DataRobot MCP server http URL."""
36
+ return os.environ.get("DR_MCP_SERVER_HTTP_URL")
37
+
38
+
32
39
  def get_openai_llm_client_config() -> dict[str, str]:
33
40
  """Get OpenAI LLM client configuration."""
34
41
  openai_api_key = os.environ.get("OPENAI_API_KEY")
@@ -94,3 +101,25 @@ async def ete_test_mcp_session(
94
101
  yield session
95
102
  except asyncio.TimeoutError:
96
103
  raise TimeoutError(f"Check if the MCP server is running at {get_dr_mcp_server_url()}")
104
+
105
+
106
+ @asynccontextmanager
107
+ async def ete_test_http_session(
108
+ additional_headers: dict[str, str] | None = None,
109
+ ) -> AsyncGenerator[HttpClientSession, None]:
110
+ """Create an HTTP session for each test that can connect to MCP custom http routes.
111
+
112
+ Parameters
113
+ ----------
114
+ additional_headers : dict[str, str], optional
115
+ Additional headers to include in the HTTP session (e.g., auth headers for testing).
116
+ """
117
+ headers = get_headers()
118
+ if additional_headers:
119
+ headers.update(additional_headers)
120
+
121
+ async with ete_test_mcp_session(additional_headers=additional_headers):
122
+ async with aiohttp.ClientSession(
123
+ base_url=get_dr_mcp_server_http_url(), headers=headers
124
+ ) as client:
125
+ yield client
@@ -0,0 +1,14 @@
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
+
@@ -0,0 +1,188 @@
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
+ """Atlassian API client utilities for OAuth and cloud ID management."""
16
+
17
+ import logging
18
+ from typing import Any
19
+ from typing import Literal
20
+
21
+ import httpx
22
+ from datarobot.auth.datarobot.exceptions import OAuthServiceClientErr
23
+ from fastmcp.exceptions import ToolError
24
+
25
+ from datarobot_genai.drmcp.core.auth import get_access_token
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # Atlassian Cloud API base URL
30
+ ATLASSIAN_API_BASE = "https://api.atlassian.com"
31
+
32
+ # API endpoint paths
33
+ OAUTH_ACCESSIBLE_RESOURCES_PATH = "/oauth/token/accessible-resources"
34
+
35
+ # Supported Atlassian service types
36
+ AtlassianServiceType = Literal["jira", "confluence"]
37
+
38
+
39
+ async def get_atlassian_access_token() -> str | ToolError:
40
+ """
41
+ Get Atlassian OAuth access token with error handling.
42
+
43
+ Returns
44
+ -------
45
+ Access token string on success, ToolError on failure
46
+
47
+ Example:
48
+ ```python
49
+ token = await get_atlassian_access_token()
50
+ if isinstance(token, ToolError):
51
+ # Handle error
52
+ return token
53
+ # Use token
54
+ ```
55
+ """
56
+ try:
57
+ access_token = await get_access_token("atlassian")
58
+ if not access_token:
59
+ logger.warning("Empty access token received")
60
+ return ToolError("Received empty access token. Please complete the OAuth flow.")
61
+ return access_token
62
+ except OAuthServiceClientErr as e:
63
+ logger.error(f"OAuth client error: {e}", exc_info=True)
64
+ return ToolError(
65
+ "Could not obtain access token for Atlassian. Make sure the OAuth "
66
+ "permission was granted for the application to act on your behalf."
67
+ )
68
+ except Exception as e:
69
+ logger.error(f"Unexpected error obtaining access token: {e}", exc_info=True)
70
+ return ToolError("An unexpected error occurred while obtaining access token for Atlassian.")
71
+
72
+
73
+ def _find_resource_by_service(
74
+ resources: list[dict[str, Any]], service_type: str
75
+ ) -> dict[str, Any] | None:
76
+ """
77
+ Find a resource that matches the specified service type.
78
+
79
+ Args:
80
+ resources: List of accessible resources from Atlassian API
81
+ service_type: Service type to filter by (e.g., "jira", "confluence")
82
+
83
+ Returns
84
+ -------
85
+ Resource dictionary if found, None otherwise
86
+ """
87
+ service_lower = service_type.lower()
88
+ for resource in resources:
89
+ if not resource.get("id"):
90
+ continue
91
+ scopes = resource.get("scopes", [])
92
+ if any(service_lower in scope.lower() for scope in scopes):
93
+ return resource
94
+ return None
95
+
96
+
97
+ def _find_first_resource_with_id(resources: list[dict[str, Any]]) -> dict[str, Any] | None:
98
+ """
99
+ Find the first resource that has an ID.
100
+
101
+ Args:
102
+ resources: List of accessible resources from Atlassian API
103
+
104
+ Returns
105
+ -------
106
+ Resource dictionary if found, None otherwise
107
+ """
108
+ for resource in resources:
109
+ if resource.get("id"):
110
+ return resource
111
+ return None
112
+
113
+
114
+ async def get_atlassian_cloud_id(
115
+ client: httpx.AsyncClient,
116
+ service_type: AtlassianServiceType | None = None,
117
+ ) -> str:
118
+ """
119
+ Get the cloud ID for the authenticated Atlassian instance.
120
+
121
+ According to Atlassian OAuth 2.0 documentation, API calls should use:
122
+ https://api.atlassian.com/ex/{service}/{cloudId}/rest/api/3/...
123
+
124
+ Args:
125
+ client: HTTP client with authentication headers configured
126
+ service_type: Optional service type to filter by (e.g., "jira", "confluence").
127
+ If provided, will prioritize resources matching this service type.
128
+
129
+ Returns
130
+ -------
131
+ Cloud ID string for the Atlassian instance
132
+
133
+ Raises
134
+ ------
135
+ ValueError: If cloud ID cannot be retrieved due to:
136
+ - No accessible resources found
137
+ - No cloud ID found in resources
138
+ - Authentication failure (401)
139
+ - HTTP request failure
140
+
141
+ Example:
142
+ ```python
143
+ client = httpx.AsyncClient(headers={"Authorization": f"Bearer {token}"})
144
+ cloud_id = await get_atlassian_cloud_id(client, service_type="jira")
145
+ ```
146
+ """
147
+ url = f"{ATLASSIAN_API_BASE}{OAUTH_ACCESSIBLE_RESOURCES_PATH}"
148
+
149
+ try:
150
+ response = await client.get(url)
151
+ response.raise_for_status()
152
+ resources = response.json()
153
+
154
+ if not resources:
155
+ raise ValueError(
156
+ "No accessible resources found. Ensure OAuth token has required scopes."
157
+ )
158
+
159
+ # If service_type is specified, try to find matching resource
160
+ if service_type:
161
+ resource = _find_resource_by_service(resources, service_type)
162
+ if resource:
163
+ cloud_id = resource["id"]
164
+ logger.debug(f"Using {service_type} cloud ID: {cloud_id}")
165
+ return cloud_id
166
+ logger.warning(
167
+ f"No {service_type} resource found, falling back to first available resource"
168
+ )
169
+
170
+ # Fallback: use the first resource with an ID
171
+ resource = _find_first_resource_with_id(resources)
172
+ if resource:
173
+ cloud_id = resource["id"]
174
+ logger.debug(f"Using cloud ID (fallback): {cloud_id}")
175
+ return cloud_id
176
+
177
+ raise ValueError("No cloud ID found in accessible resources")
178
+ except httpx.HTTPStatusError as e:
179
+ if e.response.status_code == 401:
180
+ raise ValueError(
181
+ "Authentication failed. Token may be expired. "
182
+ "Complete OAuth flow again: GET /oauth/atlassian/authorize"
183
+ ) from e
184
+ logger.error(f"HTTP error getting cloud ID: {e.response.status_code}")
185
+ raise ValueError(f"Failed to get Atlassian cloud ID: HTTP {e.response.status_code}") from e
186
+ except httpx.RequestError as e:
187
+ logger.error(f"Request error getting cloud ID: {e}")
188
+ raise ValueError("Failed to get Atlassian cloud ID: Network error") from e
@@ -0,0 +1,14 @@
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
+
@@ -0,0 +1,102 @@
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
+ import httpx
19
+
20
+ from .atlassian import ATLASSIAN_API_BASE
21
+ from .atlassian import get_atlassian_cloud_id
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class JiraClient:
27
+ """Client for interacting with Jira API using OAuth access token."""
28
+
29
+ def __init__(self, access_token: str):
30
+ """
31
+ Initialize Jira client with access token.
32
+
33
+ Args:
34
+ access_token: OAuth access token for Atlassian API
35
+ """
36
+ self.access_token = access_token
37
+ self._client = httpx.AsyncClient(
38
+ headers={
39
+ "Authorization": f"Bearer {access_token}",
40
+ "Accept": "application/json",
41
+ "Content-Type": "application/json",
42
+ },
43
+ timeout=30.0,
44
+ )
45
+ self._cloud_id: str | None = None
46
+
47
+ async def _get_cloud_id(self) -> str:
48
+ """
49
+ Get the cloud ID for the authenticated Atlassian Jira instance.
50
+
51
+ According to Atlassian OAuth 2.0 documentation, API calls should use:
52
+ https://api.atlassian.com/ex/jira/{cloudId}/rest/api/3/...
53
+
54
+ Returns
55
+ -------
56
+ Cloud ID string
57
+
58
+ Raises
59
+ ------
60
+ ValueError: If cloud ID cannot be retrieved
61
+ """
62
+ if self._cloud_id:
63
+ return self._cloud_id
64
+
65
+ self._cloud_id = await get_atlassian_cloud_id(self._client, service_type="jira")
66
+ return self._cloud_id
67
+
68
+ async def get_jira_issue(self, issue_key: str) -> dict[str, Any]:
69
+ """
70
+ Get a Jira issue by its key.
71
+
72
+ Args:
73
+ issue_key: The key (ID) of the Jira issue, e.g., 'PROJ-123'
74
+
75
+ Returns
76
+ -------
77
+ Dictionary containing the issue data
78
+
79
+ Raises
80
+ ------
81
+ httpx.HTTPStatusError: If the API request fails
82
+ """
83
+ cloud_id = await self._get_cloud_id()
84
+ url = f"{ATLASSIAN_API_BASE}/ex/jira/{cloud_id}/rest/api/3/issue/{issue_key}"
85
+
86
+ response = await self._client.get(url)
87
+ response.raise_for_status()
88
+ return response.json()
89
+
90
+ async def close(self) -> None:
91
+ """Close the HTTP client."""
92
+ await self._client.aclose()
93
+
94
+ async def __aenter__(self) -> "JiraClient":
95
+ """Async context manager entry."""
96
+ return self
97
+
98
+ async def __aexit__(
99
+ self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any
100
+ ) -> None:
101
+ """Async context manager exit."""
102
+ await self.close()
@@ -0,0 +1,28 @@
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
+
17
+ from datarobot_genai.drmcp.core.credentials import get_credentials
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def get_s3_bucket_info() -> dict[str, str]:
23
+ """Get S3 bucket configuration."""
24
+ credentials = get_credentials()
25
+ return {
26
+ "bucket": credentials.aws_predictions_s3_bucket,
27
+ "prefix": credentials.aws_predictions_s3_prefix,
28
+ }
@@ -0,0 +1,14 @@
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
+
@@ -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,14 @@
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
+
@@ -0,0 +1,58 @@
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 Annotated
17
+
18
+ from fastmcp.exceptions import ToolError
19
+ from fastmcp.tools.tool import ToolResult
20
+
21
+ from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
22
+ from datarobot_genai.drmcp.tools.clients.atlassian import get_atlassian_access_token
23
+ from datarobot_genai.drmcp.tools.clients.jira import JiraClient
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ @dr_mcp_tool(tags={"jira", "read", "get", "issue"})
29
+ async def jira_get_issue(
30
+ *, issue_key: Annotated[str, "The key (ID) of the Jira issue to retrieve, e.g., 'PROJ-123'."]
31
+ ) -> ToolResult:
32
+ """Retrieve all fields and details for a single Jira issue by its key."""
33
+ if not issue_key:
34
+ raise ToolError("Argument validation error: 'issue_key' cannot be empty.")
35
+
36
+ access_token = await get_atlassian_access_token()
37
+ if isinstance(access_token, ToolError):
38
+ return access_token
39
+
40
+ try:
41
+ client = JiraClient(
42
+ access_token,
43
+ )
44
+ issue = await client.get_jira_issue(issue_key)
45
+ except Exception as e:
46
+ logger.error(f"Unexpected error getting Jira issue: {e}")
47
+ return ToolError(
48
+ f"An unexpected error occurred while getting Jira issue '{issue_key}': {str(e)}"
49
+ )
50
+
51
+ return ToolResult(
52
+ content=f"Successfully retrieved details for issue '{issue_key}'.",
53
+ # TODO: Add more fields to the structured content, note fields here are just examples
54
+ structured_content={
55
+ "key": issue.get("key", issue_key),
56
+ "status": issue.get("fields", {}).get("status", {}).get("name", "Unknown"),
57
+ },
58
+ )
@@ -22,10 +22,10 @@ from fastmcp.resources import HttpResource
22
22
  from fastmcp.resources import ResourceManager
23
23
 
24
24
  from datarobot_genai.drmcp.core.clients import get_credentials
25
- from datarobot_genai.drmcp.core.clients import get_s3_bucket_info
26
25
  from datarobot_genai.drmcp.core.clients import get_sdk_client
27
26
  from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
28
27
  from datarobot_genai.drmcp.core.utils import generate_presigned_url
28
+ from datarobot_genai.drmcp.tools.clients.s3 import get_s3_bucket_info
29
29
 
30
30
  logger = logging.getLogger(__name__)
31
31
 
@@ -23,11 +23,11 @@ from datarobot_predict import TimeSeriesType
23
23
  from datarobot_predict.deployment import predict as dr_predict
24
24
  from pydantic import BaseModel
25
25
 
26
- from datarobot_genai.drmcp.core.clients import get_s3_bucket_info
27
26
  from datarobot_genai.drmcp.core.clients import get_sdk_client
28
27
  from datarobot_genai.drmcp.core.mcp_instance import dr_mcp_tool
29
28
  from datarobot_genai.drmcp.core.utils import PredictionResponse
30
29
  from datarobot_genai.drmcp.core.utils import predictions_result_response
30
+ from datarobot_genai.drmcp.tools.clients.s3 import get_s3_bucket_info
31
31
 
32
32
  logger = logging.getLogger(__name__)
33
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datarobot-genai
3
- Version: 0.2.0
3
+ Version: 0.2.5
4
4
  Summary: Generic helpers for GenAI
5
5
  Project-URL: Homepage, https://github.com/datarobot-oss/datarobot-genai
6
6
  Author: DataRobot, Inc.
@@ -11,13 +11,15 @@ Requires-Python: <3.13,>=3.10
11
11
  Requires-Dist: ag-ui-protocol<0.2.0,>=0.1.9
12
12
  Requires-Dist: datarobot-drum<2.0.0,>=1.17.5
13
13
  Requires-Dist: datarobot-predict<2.0.0,>=1.13.2
14
- Requires-Dist: datarobot<4.0.0,>=3.9.1
14
+ Requires-Dist: datarobot<4.0.0,>=3.10.0
15
15
  Requires-Dist: openai<2.0.0,>=1.76.2
16
16
  Requires-Dist: opentelemetry-instrumentation-aiohttp-client<1.0.0,>=0.43b0
17
17
  Requires-Dist: opentelemetry-instrumentation-httpx<1.0.0,>=0.43b0
18
18
  Requires-Dist: opentelemetry-instrumentation-openai<1.0.0,>=0.40.5
19
19
  Requires-Dist: opentelemetry-instrumentation-requests<1.0.0,>=0.43b0
20
+ Requires-Dist: opentelemetry-instrumentation-threading<1.0.0,>=0.43b0
20
21
  Requires-Dist: pandas<3.0.0,>=2.2.3
22
+ Requires-Dist: pyarrow==20.0.0
21
23
  Requires-Dist: pyjwt<3.0.0,>=2.10.1
22
24
  Requires-Dist: pypdf<7.0.0,>=6.1.3
23
25
  Requires-Dist: ragas<0.4.0,>=0.3.8
@@ -57,6 +59,7 @@ Requires-Dist: llama-index<0.14.0,>=0.13.6; extra == 'llamaindex'
57
59
  Requires-Dist: opentelemetry-instrumentation-llamaindex<1.0.0,>=0.40.5; extra == 'llamaindex'
58
60
  Requires-Dist: pypdf<7.0.0,>=6.0.0; extra == 'llamaindex'
59
61
  Provides-Extra: nat
62
+ Requires-Dist: anyio==4.11.0; extra == 'nat'
60
63
  Requires-Dist: crewai>=1.1.0; (python_version >= '3.11') and extra == 'nat'
61
64
  Requires-Dist: llama-index-llms-litellm<0.7.0,>=0.4.1; extra == 'nat'
62
65
  Requires-Dist: nvidia-nat-langchain==1.3.0; (python_version >= '3.11') and extra == 'nat'
@@ -27,26 +27,27 @@ datarobot_genai/drmcp/server.py,sha256=KE4kjS5f9bfdYftG14HBHrfvxDfCD4pwCXePfvl1O
27
27
  datarobot_genai/drmcp/core/__init__.py,sha256=y4yapzp3KnFMzSR6HlNDS4uSuyNT7I1iPBvaCLsS0sU,577
28
28
  datarobot_genai/drmcp/core/auth.py,sha256=E-5wrGbBFEBlD5377g6Exddrc7HsazamwX8tWr2RLXY,5815
29
29
  datarobot_genai/drmcp/core/clients.py,sha256=y-yG8617LbmiZ_L7FWfMrk4WjIekyr76u_Q80aLqGpI,5524
30
- datarobot_genai/drmcp/core/config.py,sha256=D7bSi40Yc5J71_JxmpfppG83snbIJW9iz1J7qbiJrRs,9855
30
+ datarobot_genai/drmcp/core/config.py,sha256=0Y0OjbLq-buP66U9tiXMyqrX9wWIn557vKvkBvjM_cM,11797
31
31
  datarobot_genai/drmcp/core/config_utils.py,sha256=U-aieWw7MyP03cGDFIp97JH99ZUfr3vD9uuTzBzxn7w,6428
32
32
  datarobot_genai/drmcp/core/constants.py,sha256=lUwoW_PTrbaBGqRJifKqCn3EoFacoEgdO-CpoFVrUoU,739
33
33
  datarobot_genai/drmcp/core/credentials.py,sha256=PYEUDNMVw1BoMzZKLkPVTypNkVevEPtmk3scKnE-zYg,6706
34
- datarobot_genai/drmcp/core/dr_mcp_server.py,sha256=7mu5UXHQmKNbIpNoQE0lPJaUI7AZa03avfHZRRtpjNI,12841
34
+ datarobot_genai/drmcp/core/dr_mcp_server.py,sha256=yKaIe7Qq23Zgny7Q1dc48iEcpfM8z2Ne6iouGYL58AE,14392
35
35
  datarobot_genai/drmcp/core/dr_mcp_server_logo.py,sha256=hib-nfR1SNTW6CnpFsFCkL9H_OMwa4YYyinV7VNOuLk,4708
36
36
  datarobot_genai/drmcp/core/exceptions.py,sha256=eqsGI-lxybgvWL5w4BFhbm3XzH1eU5tetwjnhJxelpc,905
37
37
  datarobot_genai/drmcp/core/logging.py,sha256=Y_hig4eBWiXGaVV7B_3wBcaYVRNH4ydptbEQhrP9-mY,3414
38
- datarobot_genai/drmcp/core/mcp_instance.py,sha256=wMsP39xqTmNBYqd49olEQb5UHTSsxj6BOIoIElorRB0,19235
38
+ datarobot_genai/drmcp/core/mcp_instance.py,sha256=hArS-BIdsIdRyRA21a4_ILgqqzmuRxZts-Ewgtf1H60,20917
39
39
  datarobot_genai/drmcp/core/mcp_server_tools.py,sha256=odNZKozfx0VV38SLZHw9lY0C0JM_JnRI06W3BBXnyE4,4278
40
- datarobot_genai/drmcp/core/routes.py,sha256=nrgzYkiWWcwSy0BUJ-k5AuXdUHNrnGRudCo4AhtYvlY,17856
40
+ datarobot_genai/drmcp/core/routes.py,sha256=dqE2M0UzAyyN9vQjlyTjYW4rpju3LT039po5weuO__I,17936
41
41
  datarobot_genai/drmcp/core/routes_utils.py,sha256=vSseXWlplMSnRgoJgtP_rHxWSAVYcx_tpTv4lyTpQoc,944
42
42
  datarobot_genai/drmcp/core/server_life_cycle.py,sha256=WKGJWGxalvqxupzJ2y67Kklc_9PgpZT0uyjlv_sr5wc,3419
43
43
  datarobot_genai/drmcp/core/telemetry.py,sha256=NEkSTC1w6uQgtukLHI-sWvR4EMgInysgATcvfQ5CplM,15378
44
+ datarobot_genai/drmcp/core/tool_config.py,sha256=5OkC3e-vekZtdqg-DbwcyadGSrDxmZqSDey2YyGVn1M,2978
44
45
  datarobot_genai/drmcp/core/tool_filter.py,sha256=tLOcG50QBvS48cOVHM6OqoODYiiS6KeM_F-2diaHkW0,2858
45
46
  datarobot_genai/drmcp/core/utils.py,sha256=dSjrayWVcnC5GxQcvOIOSHaoEymPIVtG_s2ZBMlmSOw,4336
46
47
  datarobot_genai/drmcp/core/dynamic_prompts/__init__.py,sha256=y4yapzp3KnFMzSR6HlNDS4uSuyNT7I1iPBvaCLsS0sU,577
47
48
  datarobot_genai/drmcp/core/dynamic_prompts/controllers.py,sha256=AGJlKqgHRO0Kd7Gl-Ulw9KYBgzjTTFXWBvOUF-SuKUI,5454
48
- datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py,sha256=IEdD2Gqm4SfUdiXJB99RiWxkN6frGaxJ2SfATetMM3c,4243
49
- datarobot_genai/drmcp/core/dynamic_prompts/register.py,sha256=5AEh1m8GX-gPZHUdiE1VATt7IKJQk-eThcxh01sWn0I,7204
49
+ datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py,sha256=4j33AKmq7kQX_EE2_RWAbP8-K5KPVEvpUginTWn_MHs,2701
50
+ datarobot_genai/drmcp/core/dynamic_prompts/register.py,sha256=2c-vBaTfu3mq_8tSFfDAzG5hG06uS9CghIC1sJxHRNw,7173
50
51
  datarobot_genai/drmcp/core/dynamic_prompts/utils.py,sha256=BZ3792AgfvYlwL0_J0MzQfGecyEA5_OKUMynEZYzCds,1136
51
52
  datarobot_genai/drmcp/core/dynamic_tools/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
52
53
  datarobot_genai/drmcp/core/dynamic_tools/register.py,sha256=3M5-F0mhUYTZJWmFDmqzsj3QAd7ut7b0kPv-JZyaTzg,9204
@@ -67,19 +68,28 @@ datarobot_genai/drmcp/core/memory_management/manager.py,sha256=gmc_SQs12YQFMWl2U
67
68
  datarobot_genai/drmcp/core/memory_management/memory_tools.py,sha256=AxzpwOlldmhhDfKZcAxaGs7Xih2SCe0XbQuXX5nQczI,6397
68
69
  datarobot_genai/drmcp/test_utils/__init__.py,sha256=y4yapzp3KnFMzSR6HlNDS4uSuyNT7I1iPBvaCLsS0sU,577
69
70
  datarobot_genai/drmcp/test_utils/integration_mcp_server.py,sha256=MdoR7r3m9uT7crodyhY69yhkrM7Thpe__BBD9lB_2oA,3328
70
- datarobot_genai/drmcp/test_utils/mcp_utils_ete.py,sha256=rvLOePXF9epIEksJzTOqdL9-fGn21bRLbK3yzpvi-6E,3430
71
+ datarobot_genai/drmcp/test_utils/mcp_utils_ete.py,sha256=rgZkPF26YCHX2FGppWE4v22l_NQ3kLSPSUimO0tD4nM,4402
71
72
  datarobot_genai/drmcp/test_utils/mcp_utils_integration.py,sha256=0sU29Khal0CelnHBDInyTRiuPKrFFbTbIomOoUbyMhs,3271
72
73
  datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py,sha256=Va3_5c2ToZyfIsEjK2ef5d3z-FA5SE51voikvjKPt8Q,8837
73
74
  datarobot_genai/drmcp/test_utils/tool_base_ete.py,sha256=-mKHBkGkyOKQCVS2LHFhSnRofIqJBbeAPRkwizBDtTg,6104
74
75
  datarobot_genai/drmcp/test_utils/utils.py,sha256=esGKFv8aO31-Qg3owayeWp32BYe1CdYOEutjjdbweCw,3048
75
76
  datarobot_genai/drmcp/tools/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
77
+ datarobot_genai/drmcp/tools/clients/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
78
+ datarobot_genai/drmcp/tools/clients/atlassian.py,sha256=__M_uz7FrcbKCYRzeMn24DCEYD6OmFx_LuywHCxgXsA,6472
79
+ datarobot_genai/drmcp/tools/clients/confluence.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
80
+ datarobot_genai/drmcp/tools/clients/jira.py,sha256=JjvssdMAWgZ3HWZkQg0a3HjpE7yz7jfRtzO4LOp47Uw,3080
81
+ datarobot_genai/drmcp/tools/clients/s3.py,sha256=GmwzvurFdNfvxOooA8g5S4osRysHYU0S9ypg_177Glg,953
82
+ datarobot_genai/drmcp/tools/confluence/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
83
+ datarobot_genai/drmcp/tools/confluence/tools.py,sha256=y4yapzp3KnFMzSR6HlNDS4uSuyNT7I1iPBvaCLsS0sU,577
84
+ datarobot_genai/drmcp/tools/jira/__init__.py,sha256=0kq9vMkF7EBsS6lkEdiLibmUrghTQqosHbZ5k-V9a5g,578
85
+ datarobot_genai/drmcp/tools/jira/tools.py,sha256=EBf8T_VwKXNticorRN8ut9ktEMS0XFeb0Uj_6TKwMio,2191
76
86
  datarobot_genai/drmcp/tools/predictive/__init__.py,sha256=WuOHlNNEpEmcF7gVnhckruJRKU2qtmJLE3E7zoCGLDo,1030
77
87
  datarobot_genai/drmcp/tools/predictive/data.py,sha256=k4EJxJrl8DYVGVfJ0DM4YTfnZlC_K3OUHZ0eRUzfluI,3165
78
88
  datarobot_genai/drmcp/tools/predictive/deployment.py,sha256=lm02Ayuo11L1hP41fgi3QpR1Eyty-Wc16rM0c8SgliM,3277
79
89
  datarobot_genai/drmcp/tools/predictive/deployment_info.py,sha256=BGEF_dmbxOBJR0n1Tt9TO2-iNTQSBTr-oQUyaxLZ0ZI,15297
80
90
  datarobot_genai/drmcp/tools/predictive/model.py,sha256=Yih5-KedJ-1yupPLXCJsCXOdyWWi9pRvgapXDlgXWJA,4891
81
- datarobot_genai/drmcp/tools/predictive/predict.py,sha256=7h73VidPJ4nlxtdFWRQ_S1epDvYuXCypKT8DXzDl4-g,9366
82
- datarobot_genai/drmcp/tools/predictive/predict_realtime.py,sha256=t7f28y_ealZoA6itrCzlJbTc3KqEU0H-a41ah-7U0XI,13468
91
+ datarobot_genai/drmcp/tools/predictive/predict.py,sha256=Qoob2_t2crfWtyPzkXMRz2ITZumnczU6Dq4C7q9RBMI,9370
92
+ datarobot_genai/drmcp/tools/predictive/predict_realtime.py,sha256=urq6rPyZFsAP-bPyclSNzrkvb6FTamdlFau8q0IWWJ0,13472
83
93
  datarobot_genai/drmcp/tools/predictive/project.py,sha256=KaMDAvJY4s12j_4ybA7-KcCS1yMOj-KPIKNBgCSE2iM,2536
84
94
  datarobot_genai/drmcp/tools/predictive/training.py,sha256=kxeDVLqUh9ajDk8wK7CZRRydDK8UNuTVZCB3huUihF8,23660
85
95
  datarobot_genai/langgraph/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -93,9 +103,9 @@ datarobot_genai/nat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
93
103
  datarobot_genai/nat/agent.py,sha256=siBLDWAff2-JwZ8Q3iNpM_e4_IoSwG9IvY0hyEjNenw,10292
94
104
  datarobot_genai/nat/datarobot_llm_clients.py,sha256=STzAZ4OF8U-Y_cUTywxmKBGVotwsnbGP6vTojnu6q0g,9921
95
105
  datarobot_genai/nat/datarobot_llm_providers.py,sha256=aDoQcTeGI-odqydPXEX9OGGNFbzAtpqzTvHHEkmJuEQ,4963
96
- datarobot_genai-0.2.0.dist-info/METADATA,sha256=Qfh60pUsGehZwD7PkQNDLxItoJIkH12bZBZx95F_Ujw,5941
97
- datarobot_genai-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
98
- datarobot_genai-0.2.0.dist-info/entry_points.txt,sha256=CZhmZcSyt_RBltgLN_b9xasJD6J5SaDc_z7K0wuOY9Y,150
99
- datarobot_genai-0.2.0.dist-info/licenses/AUTHORS,sha256=isJGUXdjq1U7XZ_B_9AH8Qf0u4eX0XyQifJZ_Sxm4sA,80
100
- datarobot_genai-0.2.0.dist-info/licenses/LICENSE,sha256=U2_VkLIktQoa60Nf6Tbt7E4RMlfhFSjWjcJJfVC-YCE,11341
101
- datarobot_genai-0.2.0.dist-info/RECORD,,
106
+ datarobot_genai-0.2.5.dist-info/METADATA,sha256=ekA7W-PGh36MkW_Whpb4o7SP2ALvXk9-jzibzcT_Z-8,6088
107
+ datarobot_genai-0.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
108
+ datarobot_genai-0.2.5.dist-info/entry_points.txt,sha256=CZhmZcSyt_RBltgLN_b9xasJD6J5SaDc_z7K0wuOY9Y,150
109
+ datarobot_genai-0.2.5.dist-info/licenses/AUTHORS,sha256=isJGUXdjq1U7XZ_B_9AH8Qf0u4eX0XyQifJZ_Sxm4sA,80
110
+ datarobot_genai-0.2.5.dist-info/licenses/LICENSE,sha256=U2_VkLIktQoa60Nf6Tbt7E4RMlfhFSjWjcJJfVC-YCE,11341
111
+ datarobot_genai-0.2.5.dist-info/RECORD,,