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,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
@@ -0,0 +1,350 @@
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 glob
17
+ import importlib
18
+ import logging
19
+ import os
20
+ from collections.abc import Callable
21
+ from typing import Any
22
+
23
+ from fastmcp import FastMCP
24
+ from starlette.middleware import Middleware
25
+
26
+ from .auth import initialize_oauth_middleware
27
+ from .config import get_config
28
+ from .credentials import get_credentials
29
+ from .dr_mcp_server_logo import log_server_custom_banner
30
+ from .dynamic_prompts.register import register_prompts_from_datarobot_prompt_management
31
+ from .dynamic_tools.deployment.register import register_tools_of_datarobot_deployments
32
+ from .logging import MCPLogging
33
+ from .mcp_instance import mcp
34
+ from .memory_management.manager import MemoryManager
35
+ from .routes import register_routes
36
+ from .routes_utils import prefix_mount_path
37
+ from .server_life_cycle import BaseServerLifecycle
38
+ from .telemetry import OtelASGIMiddleware
39
+ from .telemetry import initialize_telemetry
40
+ from .tool_config import TOOL_CONFIGS
41
+ from .tool_config import is_tool_enabled
42
+
43
+
44
+ def _import_modules_from_dir(
45
+ directory: str, package_prefix: str, module_name: str | None = None
46
+ ) -> None:
47
+ """Dynamically import all modules from a directory."""
48
+ if not os.path.exists(directory):
49
+ return
50
+ if module_name:
51
+ module_name = f"{package_prefix}.{module_name}"
52
+ try:
53
+ importlib.import_module(module_name)
54
+ except ImportError as e:
55
+ logging.warning(f"Failed to import module {module_name}: {e}")
56
+ else:
57
+ for file in glob.glob(os.path.join(directory, "*.py")):
58
+ if os.path.basename(file) != "__init__.py":
59
+ module_name = f"{package_prefix}.{os.path.splitext(os.path.basename(file))[0]}"
60
+ try:
61
+ importlib.import_module(module_name)
62
+ except ImportError as e:
63
+ logging.warning(f"Failed to import module {module_name}: {e}")
64
+
65
+
66
+ class DataRobotMCPServer:
67
+ """
68
+ DataRobot MCP server implementation using FastMCP framework.
69
+
70
+ This server can be extended by providing custom configuration, credentials,
71
+ and lifecycle handlers.
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ mcp: FastMCP,
77
+ transport: str = "streamable-http",
78
+ config_factory: Callable[[], Any] | None = None,
79
+ credentials_factory: Callable[[], Any] | None = None,
80
+ lifecycle: BaseServerLifecycle | None = None,
81
+ additional_module_paths: list[tuple[str, str]] | None = None,
82
+ ):
83
+ """
84
+ Initialize the server.
85
+
86
+ Args:
87
+ mcp: FastMCP instance
88
+ transport: Transport type ("streamable-http" or "stdio")
89
+ config_factory: Optional factory function for user config
90
+ credentials_factory: Optional factory function for user credentials
91
+ lifecycle: Optional lifecycle handler (defaults to BaseServerLifecycle())
92
+ additional_module_paths: Optional list of (directory, package_prefix) tuples for
93
+ loading additional modules
94
+ """
95
+ # Initialize config and logging
96
+ self._config = get_config()
97
+ MCPLogging(self._config.app_log_level)
98
+ self._logger = logging.getLogger(self.__class__.__name__)
99
+ self._logger.info(f"Config initialized: {self._config}")
100
+
101
+ # Initialize credentials
102
+ self._credentials = get_credentials()
103
+ self._logger.info("Credentials initialized")
104
+
105
+ self._user_config = config_factory() if config_factory else None
106
+ self._logger.info(f"User config initialized: {self._user_config}")
107
+ self._user_credentials = credentials_factory() if credentials_factory else None
108
+ self._logger.info("User credentials initialized")
109
+
110
+ # Initialize lifecycle
111
+ self._lifecycle = lifecycle if lifecycle else BaseServerLifecycle()
112
+ self._logger.info("Lifecycle initialized")
113
+
114
+ self._mcp = mcp
115
+ self._mcp_transport = transport
116
+
117
+ # Configure MCP server capabilities
118
+ self._configure_mcp_capabilities()
119
+
120
+ # Initialize telemetry
121
+ initialize_telemetry(mcp)
122
+
123
+ # Initialize OAuth middleware
124
+ initialize_oauth_middleware(mcp)
125
+
126
+ # Initialize memory manager if AWS credentials are available
127
+ self._memory_manager: MemoryManager | None = None
128
+ if self._config.enable_memory_management:
129
+ if self._credentials.has_aws_credentials():
130
+ self._logger.info("Initializing memory manager")
131
+ try:
132
+ self._memory_manager = MemoryManager.get_instance()
133
+ except Exception as e:
134
+ self._logger.error(f"Error initializing memory manager: {e}")
135
+ self._logger.info("Skipping memory manager initialization")
136
+ self._memory_manager = None
137
+ else:
138
+ self._logger.info(
139
+ "No AWS credentials found, skipping memory manager initialization"
140
+ )
141
+
142
+ # Load static tools modules
143
+ base_dir = os.path.dirname(os.path.dirname(__file__))
144
+ for tool_type, tool_config in TOOL_CONFIGS.items():
145
+ if is_tool_enabled(tool_type, self._config):
146
+ _import_modules_from_dir(
147
+ os.path.join(base_dir, "tools", tool_config["directory"]),
148
+ tool_config["package_prefix"],
149
+ )
150
+
151
+ # Load memory management tools if available
152
+ if self._memory_manager:
153
+ _import_modules_from_dir(
154
+ directory=os.path.join(base_dir, "core", "memory_management"),
155
+ package_prefix="datarobot_genai.drmcp.core.memory_management",
156
+ module_name="memory_tools",
157
+ )
158
+
159
+ # Load additional recipe user modules if provided
160
+ if additional_module_paths:
161
+ for directory, package_prefix in additional_module_paths:
162
+ self._logger.info(f"Loading additional modules from {directory}")
163
+ _import_modules_from_dir(directory, package_prefix)
164
+
165
+ # Register HTTP routes if using streamable-http transport
166
+ if transport == "streamable-http":
167
+ register_routes(self._mcp)
168
+
169
+ def _configure_mcp_capabilities(self) -> None:
170
+ """Configure MCP capabilities that FastMCP doesn't expose directly.
171
+
172
+ See: https://github.com/modelcontextprotocol/python-sdk/issues/1126
173
+ """
174
+ server = self._mcp._mcp_server
175
+
176
+ # Declare prompts_changed capability (capabilities.prompts.listChanged: true)
177
+ server.notification_options.prompts_changed = True
178
+
179
+ # Declare experimental capabilities ( experimental.dynamic_prompts: true)
180
+ server.experimental_capabilities = {"dynamic_prompts": {"enabled": True}}
181
+
182
+ # Patch to include experimental_capabilities (FastMCP doesn't expose this)
183
+ original = server.create_initialization_options
184
+
185
+ def patched(
186
+ notification_options: Any = None,
187
+ experimental_capabilities: dict[str, dict[str, Any]] | None = None,
188
+ **kwargs: Any,
189
+ ) -> Any:
190
+ if experimental_capabilities is None:
191
+ experimental_capabilities = getattr(server, "experimental_capabilities", None)
192
+ return original(
193
+ notification_options=notification_options,
194
+ experimental_capabilities=experimental_capabilities,
195
+ **kwargs,
196
+ )
197
+
198
+ server.create_initialization_options = patched
199
+
200
+ def run(self, show_banner: bool = False) -> None:
201
+ """Run the DataRobot MCP server synchronously."""
202
+ try:
203
+ # Validate configuration
204
+ if not self._credentials.has_datarobot_credentials():
205
+ self._logger.error("DataRobot credentials not configured")
206
+ raise ValueError("Missing required DataRobot credentials")
207
+
208
+ if self._config.mcp_server_register_dynamic_tools_on_startup:
209
+ self._logger.info("Registering dynamic tools from deployments...")
210
+ asyncio.run(register_tools_of_datarobot_deployments())
211
+
212
+ if self._config.mcp_server_register_dynamic_prompts_on_startup:
213
+ self._logger.info("Registering dynamic prompts from prompt management...")
214
+ asyncio.run(register_prompts_from_datarobot_prompt_management())
215
+
216
+ # Execute pre-server start actions
217
+ asyncio.run(self._lifecycle.pre_server_start(self._mcp))
218
+
219
+ # List registered tools, prompts, and resources before starting server
220
+ tools = asyncio.run(self._mcp._list_tools_mcp())
221
+ prompts = asyncio.run(self._mcp._list_prompts_mcp())
222
+ resources = asyncio.run(self._mcp._list_resources_mcp())
223
+
224
+ tools_count = len(tools)
225
+ prompts_count = len(prompts)
226
+ resources_count = len(resources)
227
+
228
+ self._logger.info(f"Registered tools: {tools_count}")
229
+ for tool in tools:
230
+ self._logger.info(f" > {tool.name}")
231
+ self._logger.info(f"Registered prompts: {prompts_count}")
232
+ for prompt in prompts:
233
+ self._logger.info(f" > {prompt.name}")
234
+ self._logger.info(f"Registered resources: {resources_count}")
235
+ for resource in resources:
236
+ self._logger.info(f" > {resource.name}")
237
+
238
+ # Create event loop for async operations
239
+ loop = asyncio.new_event_loop()
240
+ asyncio.set_event_loop(loop)
241
+
242
+ async def run_server(show_banner: bool = show_banner) -> None:
243
+ # Start server in background based on transport type
244
+
245
+ if show_banner:
246
+ log_server_custom_banner(
247
+ self._mcp,
248
+ self._mcp_transport,
249
+ port=self._config.mcp_server_port,
250
+ tools_count=tools_count,
251
+ prompts_count=prompts_count,
252
+ resources_count=resources_count,
253
+ )
254
+
255
+ if self._mcp_transport == "stdio":
256
+ server_task = asyncio.create_task(self._mcp.run_stdio_async(show_banner=False))
257
+ elif self._mcp_transport == "streamable-http":
258
+ server_task = asyncio.create_task(
259
+ self._mcp.run_http_async(
260
+ transport="http",
261
+ middleware=[Middleware(OtelASGIMiddleware)],
262
+ show_banner=False,
263
+ port=self._config.mcp_server_port,
264
+ log_level=self._config.mcp_server_log_level,
265
+ host=self._config.mcp_server_host,
266
+ stateless_http=True,
267
+ path=prefix_mount_path("/mcp"),
268
+ )
269
+ )
270
+ else:
271
+ raise ValueError(f"Unsupported transport: {self._mcp_transport}")
272
+
273
+ # Give the server a moment to initialize
274
+ await asyncio.sleep(1)
275
+
276
+ # Execute post-server start actions
277
+ await self._lifecycle.post_server_start(self._mcp)
278
+
279
+ # Wait for server to complete
280
+ await server_task
281
+
282
+ # Start the server
283
+ self._logger.info("Starting MCP server...")
284
+ try:
285
+ loop.run_until_complete(run_server(show_banner=show_banner))
286
+ except KeyboardInterrupt:
287
+ self._logger.info("Server interrupted by user")
288
+ finally:
289
+ # Execute pre-shutdown actions
290
+ self._logger.info("Shutting down server...")
291
+ loop.run_until_complete(self._lifecycle.pre_server_shutdown(self._mcp))
292
+ loop.close()
293
+
294
+ except Exception as e:
295
+ self._logger.error(f"Server error: {e}")
296
+ raise
297
+
298
+
299
+ def create_mcp_server(
300
+ config_factory: Callable[[], Any] | None = None,
301
+ credentials_factory: Callable[[], Any] | None = None,
302
+ lifecycle: BaseServerLifecycle | None = None,
303
+ additional_module_paths: list[tuple[str, str]] | None = None,
304
+ transport: str = "streamable-http",
305
+ ) -> DataRobotMCPServer:
306
+ """
307
+ Create a DataRobot MCP server.
308
+
309
+ Args:
310
+ config_factory: Optional factory function for user config
311
+ credentials_factory: Optional factory function for user credentials
312
+ lifecycle: Optional lifecycle handler
313
+ additional_module_paths: Optional list of (directory, package_prefix) tuples
314
+ transport: Transport type ("streamable-http" or "stdio")
315
+
316
+ Returns
317
+ -------
318
+ Configured DataRobotMCPServer instance
319
+
320
+ Example:
321
+ ```python
322
+ # Basic usage with defaults
323
+ server = create_mcp_server()
324
+ server.run()
325
+
326
+ # With custom configuration
327
+ from myapp.config import get_my_config
328
+ from myapp.lifecycle import MyLifecycle
329
+
330
+ server = create_mcp_server(
331
+ config_factory=get_my_config,
332
+ lifecycle=MyLifecycle(),
333
+ additional_module_paths=[
334
+ ("/path/to/my/tools", "myapp.tools"),
335
+ ("/path/to/my/prompts", "myapp.prompts"),
336
+ ]
337
+ )
338
+ server.run()
339
+ ```
340
+ """
341
+ # Use the global mcp instance that tools are registered with
342
+
343
+ return DataRobotMCPServer(
344
+ mcp=mcp,
345
+ transport=transport,
346
+ config_factory=config_factory,
347
+ credentials_factory=credentials_factory,
348
+ lifecycle=lifecycle,
349
+ additional_module_paths=additional_module_paths,
350
+ )
@@ -0,0 +1,136 @@
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
+ from typing import Any
17
+
18
+ from fastmcp import FastMCP
19
+ from rich.align import Align
20
+ from rich.console import Console
21
+ from rich.console import Group
22
+ from rich.panel import Panel
23
+ from rich.table import Table
24
+ from rich.text import Text
25
+
26
+ from datarobot_genai import __version__ as datarobot_genai_version
27
+
28
+
29
+ # Green color #81FBA5 = RGB(129, 251, 165)
30
+ def _apply_green(text: str) -> str:
31
+ """Apply green color #81FBA5 to all characters in the text."""
32
+ # Apply ANSI escape code for RGB color #81FBA5 (129, 251, 165)
33
+ green_start = "\x1b[38;2;129;251;165m"
34
+ green_end = "\x1b[39m"
35
+ # Wrap the entire text (except trailing newline) with green color codes
36
+ lines = text.split("\n")
37
+ colored_lines = [green_start + line + green_end if line else "" for line in lines]
38
+ return "\n".join(colored_lines)
39
+
40
+
41
+ DR_LOGO_ASCII = _apply_green(r"""
42
+ ____ _ ____ _ _
43
+ | _ \ __ _| |_ __ _| _ \ ___ | |__ ___ | |_
44
+ | | | |/ _` | __/ _` | |_) / _ \| '_ \ / _ \| __|
45
+ | |_| | (_| | || (_| | _ < (_) | |_) | (_) | |_
46
+ |____/ \__,_|\__\__,_|_| \_\___/|_.__/ \___/ \__|
47
+ """)
48
+
49
+
50
+ def log_server_custom_banner(
51
+ server: FastMCP[Any],
52
+ transport: str,
53
+ *,
54
+ host: str | None = None,
55
+ port: int | None = None,
56
+ path: str | None = None,
57
+ tools_count: int | None = None,
58
+ prompts_count: int | None = None,
59
+ resources_count: int | None = None,
60
+ ) -> None:
61
+ """
62
+ Create and log a formatted banner with server information and logo.
63
+
64
+ Args:
65
+ transport: The transport protocol being used
66
+ server_name: Optional server name to display
67
+ host: Host address (for HTTP transports)
68
+ port: Port number (for HTTP transports)
69
+ path: Server path (for HTTP transports)
70
+ tools_count: Number of tools registered
71
+ prompts_count: Number of prompts registered
72
+ resources_count: Number of resources registered
73
+ """
74
+ # Create the logo text
75
+ # Use Text with no_wrap and markup disabled to preserve ANSI escape codes
76
+ logo_text = Text.from_ansi(DR_LOGO_ASCII, no_wrap=True)
77
+
78
+ # Create the main title
79
+ title_text = Text(f"DataRobot MCP Server {datarobot_genai_version}", style="dim green")
80
+ stats_text = Text(
81
+ f"{tools_count} tools, {prompts_count} prompts, {resources_count} resources",
82
+ style="bold green",
83
+ )
84
+
85
+ # Create the information table
86
+ info_table = Table.grid(padding=(0, 1))
87
+ info_table.add_column(style="bold", justify="center") # Emoji column
88
+ info_table.add_column(style="cyan", justify="left") # Label column
89
+ info_table.add_column(style="dim", justify="left") # Value column
90
+
91
+ match transport:
92
+ case "http" | "streamable-http":
93
+ display_transport = "HTTP"
94
+ case "sse":
95
+ display_transport = "SSE"
96
+ case "stdio":
97
+ display_transport = "STDIO"
98
+
99
+ info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
100
+ info_table.add_row("📦", "Transport:", display_transport)
101
+ info_table.add_row("🌐", "MCP port:", str(port))
102
+
103
+ # Show connection info based on transport
104
+ if transport in ("http", "streamable-http", "sse") and host and port:
105
+ server_url = f"http://{host}:{port}"
106
+ if path:
107
+ server_url += f"/{path.lstrip('/')}"
108
+ info_table.add_row("🔗", "Server URL:", server_url)
109
+
110
+ # Add documentation link
111
+ info_table.add_row("", "", "")
112
+ info_table.add_row("📚", "Docs:", "https://docs.datarobot.com")
113
+ info_table.add_row("🚀", "Hosting:", "https://datarobot.com")
114
+
115
+ # Create panel with logo, title, and information using Group
116
+ panel_content = Group(
117
+ Align.center(logo_text),
118
+ "",
119
+ Align.center(title_text),
120
+ Align.center(stats_text),
121
+ "",
122
+ "",
123
+ Align.center(info_table),
124
+ )
125
+
126
+ panel = Panel(
127
+ panel_content,
128
+ border_style="dim",
129
+ padding=(1, 4),
130
+ # expand=False,
131
+ width=80, # Set max width for the pane
132
+ )
133
+
134
+ console = Console(stderr=True)
135
+ # Center the panel itself
136
+ console.print(Group("\n", Align.center(panel), "\n"))
@@ -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.