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.
Files changed (125) 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 +364 -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 +350 -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 +70 -0
  37. datarobot_genai/drmcp/core/dynamic_prompts/register.py +205 -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 +515 -0
  56. datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
  57. datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
  58. datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
  59. datarobot_genai/drmcp/core/routes.py +439 -0
  60. datarobot_genai/drmcp/core/routes_utils.py +30 -0
  61. datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
  62. datarobot_genai/drmcp/core/telemetry.py +424 -0
  63. datarobot_genai/drmcp/core/tool_config.py +111 -0
  64. datarobot_genai/drmcp/core/tool_filter.py +117 -0
  65. datarobot_genai/drmcp/core/utils.py +138 -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/clients/__init__.py +0 -0
  69. datarobot_genai/drmcp/test_utils/clients/anthropic.py +68 -0
  70. datarobot_genai/drmcp/test_utils/clients/base.py +300 -0
  71. datarobot_genai/drmcp/test_utils/clients/dr_gateway.py +58 -0
  72. datarobot_genai/drmcp/test_utils/clients/openai.py +68 -0
  73. datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
  74. datarobot_genai/drmcp/test_utils/integration_mcp_server.py +109 -0
  75. datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +133 -0
  76. datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +107 -0
  77. datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
  78. datarobot_genai/drmcp/test_utils/tool_base_ete.py +220 -0
  79. datarobot_genai/drmcp/test_utils/utils.py +91 -0
  80. datarobot_genai/drmcp/tools/__init__.py +14 -0
  81. datarobot_genai/drmcp/tools/clients/__init__.py +14 -0
  82. datarobot_genai/drmcp/tools/clients/atlassian.py +188 -0
  83. datarobot_genai/drmcp/tools/clients/confluence.py +584 -0
  84. datarobot_genai/drmcp/tools/clients/gdrive.py +832 -0
  85. datarobot_genai/drmcp/tools/clients/jira.py +334 -0
  86. datarobot_genai/drmcp/tools/clients/microsoft_graph.py +479 -0
  87. datarobot_genai/drmcp/tools/clients/s3.py +28 -0
  88. datarobot_genai/drmcp/tools/confluence/__init__.py +14 -0
  89. datarobot_genai/drmcp/tools/confluence/tools.py +321 -0
  90. datarobot_genai/drmcp/tools/gdrive/__init__.py +0 -0
  91. datarobot_genai/drmcp/tools/gdrive/tools.py +347 -0
  92. datarobot_genai/drmcp/tools/jira/__init__.py +14 -0
  93. datarobot_genai/drmcp/tools/jira/tools.py +243 -0
  94. datarobot_genai/drmcp/tools/microsoft_graph/__init__.py +13 -0
  95. datarobot_genai/drmcp/tools/microsoft_graph/tools.py +198 -0
  96. datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
  97. datarobot_genai/drmcp/tools/predictive/data.py +133 -0
  98. datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
  99. datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
  100. datarobot_genai/drmcp/tools/predictive/model.py +148 -0
  101. datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
  102. datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
  103. datarobot_genai/drmcp/tools/predictive/project.py +90 -0
  104. datarobot_genai/drmcp/tools/predictive/training.py +661 -0
  105. datarobot_genai/langgraph/__init__.py +0 -0
  106. datarobot_genai/langgraph/agent.py +341 -0
  107. datarobot_genai/langgraph/mcp.py +73 -0
  108. datarobot_genai/llama_index/__init__.py +16 -0
  109. datarobot_genai/llama_index/agent.py +50 -0
  110. datarobot_genai/llama_index/base.py +299 -0
  111. datarobot_genai/llama_index/mcp.py +79 -0
  112. datarobot_genai/nat/__init__.py +0 -0
  113. datarobot_genai/nat/agent.py +275 -0
  114. datarobot_genai/nat/datarobot_auth_provider.py +110 -0
  115. datarobot_genai/nat/datarobot_llm_clients.py +318 -0
  116. datarobot_genai/nat/datarobot_llm_providers.py +130 -0
  117. datarobot_genai/nat/datarobot_mcp_client.py +266 -0
  118. datarobot_genai/nat/helpers.py +87 -0
  119. datarobot_genai/py.typed +0 -0
  120. datarobot_genai-0.2.31.dist-info/METADATA +145 -0
  121. datarobot_genai-0.2.31.dist-info/RECORD +125 -0
  122. datarobot_genai-0.2.31.dist-info/WHEEL +4 -0
  123. datarobot_genai-0.2.31.dist-info/entry_points.txt +5 -0
  124. datarobot_genai-0.2.31.dist-info/licenses/AUTHORS +2 -0
  125. datarobot_genai-0.2.31.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,58 @@
1
+ # Copyright 2026 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
+ """DataRobot LLM Gateway MCP Client implementation."""
16
+
17
+ import openai
18
+
19
+ from .base import BaseLLMMCPClient
20
+
21
+
22
+ class DRLLMGatewayMCPClient(BaseLLMMCPClient):
23
+ """
24
+ Client for interacting with LLMs via MCP using DataRobot LLM Gateway.
25
+
26
+ Note: Elicitation is handled at the protocol level by FastMCP's ctx.elicit().
27
+ Tools using FastMCP's built-in elicitation will work automatically.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ config: str | dict,
33
+ ):
34
+ """
35
+ Initialize the LLM MCP client.
36
+
37
+ Args:
38
+ config: Configuration string or dict with:
39
+ - datarobot_api_token: DataRobot API token
40
+ - datarobot_endpoint: DataRobot endpoint URL (default: "https://app.datarobot.com/api/v2")
41
+ - model: Model name (default: "gpt-4o-mini")
42
+ - save_llm_responses: Whether to save responses (default: True)
43
+ """
44
+ super().__init__(config)
45
+
46
+ def _create_llm_client(self, config_dict: dict) -> tuple[openai.OpenAI, str]:
47
+ """Create the LLM client for DataRobot LLM Gateway."""
48
+ datarobot_api_token = config_dict.get("datarobot_api_token")
49
+ datarobot_endpoint = config_dict.get(
50
+ "datarobot_endpoint", "https://app.datarobot.com/api/v2"
51
+ )
52
+ model = config_dict.get("model", "gpt-4o-mini")
53
+
54
+ # Build gateway URL: {endpoint}/genai/llmgw
55
+ gateway_url = datarobot_endpoint.rstrip("/") + "/genai/llmgw"
56
+
57
+ client = openai.OpenAI(api_key=datarobot_api_token, base_url=gateway_url)
58
+ return client, model
@@ -0,0 +1,68 @@
1
+ # Copyright 2026 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
+ """OpenAI LLM MCP Client implementation."""
16
+
17
+ import openai
18
+
19
+ from .base import BaseLLMMCPClient
20
+
21
+
22
+ class OpenAILLMMCPClient(BaseLLMMCPClient):
23
+ """
24
+ Client for interacting with LLMs via MCP using OpenAI or Azure OpenAI.
25
+
26
+ Note: Elicitation is handled at the protocol level by FastMCP's ctx.elicit().
27
+ Tools using FastMCP's built-in elicitation will work automatically.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ config: str | dict,
33
+ ):
34
+ """
35
+ Initialize the LLM MCP client.
36
+
37
+ Args:
38
+ config: Configuration string or dict with:
39
+ - openai_api_key: OpenAI API key
40
+ - openai_api_base: Optional Azure OpenAI endpoint
41
+ - openai_api_deployment_id: Optional Azure deployment ID
42
+ - openai_api_version: Optional Azure API version
43
+ - model: Model name (default: "gpt-3.5-turbo")
44
+ - save_llm_responses: Whether to save responses (default: True)
45
+ """
46
+ super().__init__(config)
47
+
48
+ def _create_llm_client(
49
+ self, config_dict: dict
50
+ ) -> tuple[openai.OpenAI | openai.AzureOpenAI, str]:
51
+ """Create the LLM client for OpenAI or Azure OpenAI."""
52
+ openai_api_key = config_dict.get("openai_api_key")
53
+ openai_api_base = config_dict.get("openai_api_base")
54
+ openai_api_deployment_id = config_dict.get("openai_api_deployment_id")
55
+ model = config_dict.get("model", "gpt-3.5-turbo")
56
+
57
+ if openai_api_base and openai_api_deployment_id:
58
+ # Azure OpenAI
59
+ client = openai.AzureOpenAI(
60
+ api_key=openai_api_key,
61
+ azure_endpoint=openai_api_base,
62
+ api_version=config_dict.get("openai_api_version", "2024-02-15-preview"),
63
+ )
64
+ return client, openai_api_deployment_id
65
+ else:
66
+ # Regular OpenAI
67
+ client = openai.OpenAI(api_key=openai_api_key) # type: ignore[assignment]
68
+ return client, model
@@ -0,0 +1,89 @@
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
+ """Test tool for elicitation testing.
16
+
17
+ This module registers a test tool that can be used to test elicitation support.
18
+ It should be imported in tests that need it.
19
+ """
20
+
21
+ from fastmcp import Context
22
+ from fastmcp.server.context import AcceptedElicitation
23
+ from fastmcp.server.context import CancelledElicitation
24
+ from fastmcp.server.context import DeclinedElicitation
25
+
26
+ from datarobot_genai.drmcp.core.mcp_instance import mcp
27
+
28
+
29
+ @mcp.tool(
30
+ name="get_user_greeting",
31
+ description=(
32
+ "Get a personalized greeting for a user. "
33
+ "Requires a username - if not provided, will request it via elicitation."
34
+ ),
35
+ tags={"test", "elicitation"},
36
+ )
37
+ async def get_user_greeting(ctx: Context, username: str | None = None) -> dict:
38
+ """
39
+ Get a personalized greeting for a user.
40
+
41
+ This tool demonstrates FastMCP's built-in elicitation by requiring a username parameter.
42
+ If username is not provided, it uses ctx.elicit() to request it from the user.
43
+
44
+ Args:
45
+ ctx: FastMCP context (automatically injected)
46
+ username: The username to greet. If None, elicitation will be triggered.
47
+
48
+ Returns
49
+ -------
50
+ Dictionary with greeting message or error if elicitation was declined/cancelled
51
+ """
52
+ if not username:
53
+ # Use elicitation to request username from the client
54
+ try:
55
+ result = await ctx.elicit(
56
+ message="Username is required to generate a personalized greeting",
57
+ response_type=str,
58
+ )
59
+
60
+ if isinstance(result, AcceptedElicitation):
61
+ username = result.data
62
+ elif isinstance(result, DeclinedElicitation):
63
+ return {
64
+ "status": "error",
65
+ "error": "Username declined by user",
66
+ "message": "Cannot generate greeting without username",
67
+ }
68
+ elif isinstance(result, CancelledElicitation):
69
+ return {
70
+ "status": "error",
71
+ "error": "Operation cancelled",
72
+ "message": "Greeting request was cancelled",
73
+ }
74
+ except Exception:
75
+ # Elicitation not supported by client - return graceful skip
76
+ return {
77
+ "status": "skipped",
78
+ "message": (
79
+ "Elicitation not supported by client. "
80
+ "Username parameter is required when client does not support elicitation."
81
+ ),
82
+ "elicitation_supported": False,
83
+ }
84
+
85
+ return {
86
+ "status": "success",
87
+ "message": f"Hello, {username}! Welcome to the DataRobot MCP server.",
88
+ "username": username,
89
+ }
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Copyright 2025 DataRobot, Inc.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ """
18
+ Integration test MCP server.
19
+
20
+ This server works standalone (base tools only) or detects and loads
21
+ user modules if they exist in the project structure.
22
+ """
23
+
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ from datarobot_genai.drmcp import create_mcp_server
28
+
29
+ # Import elicitation test tool to register it with the MCP server
30
+ try:
31
+ from datarobot_genai.drmcp.test_utils import elicitation_test_tool # noqa: F401
32
+ except ImportError:
33
+ # Test utils not available (e.g., running in production)
34
+ pass
35
+
36
+ # Import user components (will be used conditionally)
37
+ try:
38
+ from app.core.server_lifecycle import ServerLifecycle # type: ignore # noqa: F401
39
+ from app.core.user_config import get_user_config # type: ignore # noqa: F401
40
+ from app.core.user_credentials import get_user_credentials # type: ignore # noqa: F401
41
+
42
+ except ImportError:
43
+ # These imports will fail when running from library without user modules
44
+ pass
45
+
46
+
47
+ def detect_user_modules() -> Any:
48
+ """
49
+ Detect if user modules exist in the project.
50
+
51
+ Returns
52
+ -------
53
+ Tuple of (config_factory, credentials_factory, lifecycle, module_paths) or None
54
+ """
55
+ # Try to find app directory
56
+ # When run from library: won't find it
57
+ # When run from project: will find it
58
+ current_dir = Path.cwd()
59
+
60
+ # Look for app in current directory or parent directories
61
+ for search_dir in [current_dir, current_dir.parent, current_dir.parent.parent]:
62
+ app_dir = search_dir / "app"
63
+ app_core_dir = app_dir / "core"
64
+ if app_core_dir.exists():
65
+ # Found user directory - load user modules
66
+ try:
67
+ module_paths = [
68
+ (str(app_dir / "tools"), "app.tools"),
69
+ (str(app_dir / "prompts"), "app.prompts"),
70
+ (str(app_dir / "resources"), "app.resources"),
71
+ ]
72
+
73
+ return (
74
+ get_user_config,
75
+ get_user_credentials,
76
+ ServerLifecycle(),
77
+ module_paths,
78
+ )
79
+ except ImportError:
80
+ # User modules don't exist or can't be imported
81
+ pass
82
+
83
+ return None
84
+
85
+
86
+ def main() -> None:
87
+ """Run the integration test MCP server."""
88
+ # Try to detect and load user modules
89
+ user_components = detect_user_modules()
90
+
91
+ if user_components:
92
+ # User modules found - create server with user extensions
93
+ config_factory, credentials_factory, lifecycle, module_paths = user_components
94
+ server = create_mcp_server(
95
+ config_factory=config_factory,
96
+ credentials_factory=credentials_factory,
97
+ lifecycle=lifecycle,
98
+ additional_module_paths=module_paths,
99
+ transport="stdio",
100
+ )
101
+ else:
102
+ # No user modules - create server with base tools only
103
+ server = create_mcp_server(transport="stdio")
104
+
105
+ server.run()
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -0,0 +1,133 @@
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
+ import asyncio
15
+ import os
16
+ from collections.abc import AsyncGenerator
17
+ from contextlib import asynccontextmanager
18
+ from typing import Any
19
+
20
+ import aiohttp
21
+ from aiohttp import ClientSession as HttpClientSession
22
+ from mcp import ClientSession
23
+ from mcp.client.streamable_http import streamablehttp_client
24
+
25
+ from .utils import load_env
26
+
27
+ load_env()
28
+
29
+
30
+ def get_dr_mcp_server_url() -> str | None:
31
+ """Get DataRobot MCP server URL."""
32
+ return os.environ.get("DR_MCP_SERVER_URL")
33
+
34
+
35
+ def get_dr_mcp_server_http_url() -> str | None:
36
+ """Get DataRobot MCP server http URL."""
37
+ return os.environ.get("DR_MCP_SERVER_HTTP_URL")
38
+
39
+
40
+ def get_openai_llm_client_config() -> dict[str, str]:
41
+ """Get OpenAI LLM client configuration."""
42
+ openai_api_key = os.environ.get("OPENAI_API_KEY")
43
+ openai_api_base = os.environ.get("OPENAI_API_BASE")
44
+ openai_api_deployment_id = os.environ.get("OPENAI_API_DEPLOYMENT_ID")
45
+ openai_api_version = os.environ.get("OPENAI_API_VERSION")
46
+ save_llm_responses = os.environ.get("SAVE_LLM_RESPONSES", "false").lower() == "true"
47
+
48
+ # Check for OpenAI configuration
49
+ if not openai_api_key:
50
+ raise ValueError("Missing required environment variable: OPENAI_API_KEY")
51
+ if (
52
+ openai_api_base and not openai_api_deployment_id
53
+ ): # For Azure OpenAI, we need additional variables
54
+ raise ValueError("Missing required environment variable: OPENAI_API_DEPLOYMENT_ID")
55
+
56
+ config: dict[str, str] = {
57
+ "openai_api_key": openai_api_key,
58
+ }
59
+
60
+ if openai_api_base:
61
+ config["openai_api_base"] = openai_api_base
62
+ if openai_api_deployment_id:
63
+ config["openai_api_deployment_id"] = openai_api_deployment_id
64
+ if openai_api_version:
65
+ config["openai_api_version"] = openai_api_version
66
+ config["save_llm_responses"] = str(save_llm_responses)
67
+
68
+ return config
69
+
70
+
71
+ def get_headers() -> dict[str, str]:
72
+ # When the MCP server is deployed in DataRobot, we have to include the API token in headers for
73
+ # authentication.
74
+ api_token = os.getenv("DATAROBOT_API_TOKEN")
75
+ headers = {"Authorization": f"Bearer {api_token}"}
76
+ return headers
77
+
78
+
79
+ @asynccontextmanager
80
+ async def ete_test_mcp_session(
81
+ additional_headers: dict[str, str] | None = None,
82
+ elicitation_callback: Any | None = None,
83
+ ) -> AsyncGenerator[ClientSession, None]:
84
+ """Create an MCP session for each test.
85
+
86
+ Parameters
87
+ ----------
88
+ additional_headers : dict[str, str], optional
89
+ Additional headers to include in the MCP session (e.g., auth headers for testing).
90
+ elicitation_callback : callable, optional
91
+ Callback function to handle elicitation requests from the server.
92
+ The callback should have signature:
93
+ async def callback(context, params: ElicitRequestParams) -> ElicitResult
94
+ """
95
+ try:
96
+ headers = get_headers()
97
+ if additional_headers:
98
+ headers.update(additional_headers)
99
+
100
+ async with streamablehttp_client(url=get_dr_mcp_server_url(), headers=headers) as (
101
+ read_stream,
102
+ write_stream,
103
+ _,
104
+ ):
105
+ async with ClientSession(
106
+ read_stream, write_stream, elicitation_callback=elicitation_callback
107
+ ) as session:
108
+ await asyncio.wait_for(session.initialize(), timeout=5)
109
+ yield session
110
+ except asyncio.TimeoutError:
111
+ raise TimeoutError(f"Check if the MCP server is running at {get_dr_mcp_server_url()}")
112
+
113
+
114
+ @asynccontextmanager
115
+ async def ete_test_http_session(
116
+ additional_headers: dict[str, str] | None = None,
117
+ ) -> AsyncGenerator[HttpClientSession, None]:
118
+ """Create an HTTP session for each test that can connect to MCP custom http routes.
119
+
120
+ Parameters
121
+ ----------
122
+ additional_headers : dict[str, str], optional
123
+ Additional headers to include in the HTTP session (e.g., auth headers for testing).
124
+ """
125
+ headers = get_headers()
126
+ if additional_headers:
127
+ headers.update(additional_headers)
128
+
129
+ async with ete_test_mcp_session(additional_headers=additional_headers):
130
+ async with aiohttp.ClientSession(
131
+ base_url=get_dr_mcp_server_http_url(), headers=headers
132
+ ) as client:
133
+ yield client
@@ -0,0 +1,107 @@
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 asyncio
16
+ import contextlib
17
+ import os
18
+ from collections.abc import AsyncGenerator
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from mcp import ClientSession
23
+ from mcp.client.stdio import StdioServerParameters
24
+ from mcp.client.stdio import stdio_client
25
+
26
+ from .utils import load_env
27
+
28
+ load_env()
29
+
30
+
31
+ def integration_test_mcp_server_params() -> StdioServerParameters:
32
+ env = {
33
+ "DATAROBOT_API_TOKEN": os.environ.get("DATAROBOT_API_TOKEN") or "test-token",
34
+ "DATAROBOT_ENDPOINT": os.environ.get("DATAROBOT_ENDPOINT")
35
+ or "https://test.datarobot.com/api/v2",
36
+ "MCP_SERVER_LOG_LEVEL": os.environ.get("MCP_SERVER_LOG_LEVEL") or "WARNING",
37
+ "APP_LOG_LEVEL": os.environ.get("APP_LOG_LEVEL") or "WARNING",
38
+ # Disable all OTEL telemetry for integration tests
39
+ "OTEL_ENABLED": "false",
40
+ "OTEL_SDK_DISABLED": "true",
41
+ "OTEL_TRACES_EXPORTER": "none",
42
+ "OTEL_LOGS_EXPORTER": "none",
43
+ "OTEL_METRICS_EXPORTER": "none",
44
+ "MCP_SERVER_REGISTER_DYNAMIC_TOOLS_ON_STARTUP": os.environ.get(
45
+ "MCP_SERVER_REGISTER_DYNAMIC_TOOLS_ON_STARTUP"
46
+ )
47
+ or "false",
48
+ "MCP_SERVER_REGISTER_DYNAMIC_PROMPTS_ON_STARTUP": os.environ.get(
49
+ "MCP_SERVER_REGISTER_DYNAMIC_PROMPTS_ON_STARTUP"
50
+ )
51
+ or "true",
52
+ }
53
+
54
+ script_dir = Path(__file__).resolve().parent
55
+ server_script = str(script_dir / "integration_mcp_server.py")
56
+ # Add src/ directory to Python path so datarobot_genai can be imported
57
+ src_dir = script_dir.parent.parent.parent
58
+
59
+ return StdioServerParameters(
60
+ command="uv",
61
+ args=["run", server_script],
62
+ env={
63
+ "PYTHONPATH": str(src_dir),
64
+ "MCP_SERVER_NAME": "integration",
65
+ "MCP_SERVER_PORT": "8081",
66
+ **env,
67
+ },
68
+ )
69
+
70
+
71
+ @contextlib.asynccontextmanager
72
+ async def integration_test_mcp_session(
73
+ server_params: StdioServerParameters | None = None,
74
+ timeout: int = 30,
75
+ elicitation_callback: Any | None = None,
76
+ ) -> AsyncGenerator[ClientSession, None]:
77
+ """
78
+ Create and connect a client for the MCP server as a context manager.
79
+
80
+ Args:
81
+ server_params: Parameters for configuring the server connection
82
+ timeout: Timeout
83
+ elicitation_callback: Optional callback for handling elicitation requests
84
+
85
+ Yields
86
+ ------
87
+ ClientSession: Connected MCP client session
88
+
89
+ Raises
90
+ ------
91
+ ConnectionError: If session initialization fails
92
+ TimeoutError: If session initialization exceeds timeout
93
+ """
94
+ server_params = server_params or integration_test_mcp_server_params()
95
+
96
+ try:
97
+ async with stdio_client(server_params) as (read_stream, write_stream):
98
+ async with ClientSession(
99
+ read_stream, write_stream, elicitation_callback=elicitation_callback
100
+ ) as session:
101
+ init_result = await asyncio.wait_for(session.initialize(), timeout=timeout)
102
+ # Store the init result on the session for tests that need to inspect capabilities
103
+ session._init_result = init_result # type: ignore[attr-defined]
104
+ yield session
105
+
106
+ except asyncio.TimeoutError:
107
+ raise TimeoutError(f"Session initialization timed out after {timeout} seconds")