datarobot-genai 0.2.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datarobot_genai/__init__.py +19 -0
- datarobot_genai/core/__init__.py +0 -0
- datarobot_genai/core/agents/__init__.py +43 -0
- datarobot_genai/core/agents/base.py +195 -0
- datarobot_genai/core/chat/__init__.py +19 -0
- datarobot_genai/core/chat/auth.py +146 -0
- datarobot_genai/core/chat/client.py +178 -0
- datarobot_genai/core/chat/responses.py +297 -0
- datarobot_genai/core/cli/__init__.py +18 -0
- datarobot_genai/core/cli/agent_environment.py +47 -0
- datarobot_genai/core/cli/agent_kernel.py +211 -0
- datarobot_genai/core/custom_model.py +141 -0
- datarobot_genai/core/mcp/__init__.py +0 -0
- datarobot_genai/core/mcp/common.py +218 -0
- datarobot_genai/core/telemetry_agent.py +126 -0
- datarobot_genai/core/utils/__init__.py +3 -0
- datarobot_genai/core/utils/auth.py +234 -0
- datarobot_genai/core/utils/urls.py +64 -0
- datarobot_genai/crewai/__init__.py +24 -0
- datarobot_genai/crewai/agent.py +42 -0
- datarobot_genai/crewai/base.py +159 -0
- datarobot_genai/crewai/events.py +117 -0
- datarobot_genai/crewai/mcp.py +59 -0
- datarobot_genai/drmcp/__init__.py +78 -0
- datarobot_genai/drmcp/core/__init__.py +13 -0
- datarobot_genai/drmcp/core/auth.py +165 -0
- datarobot_genai/drmcp/core/clients.py +180 -0
- datarobot_genai/drmcp/core/config.py +364 -0
- datarobot_genai/drmcp/core/config_utils.py +174 -0
- datarobot_genai/drmcp/core/constants.py +18 -0
- datarobot_genai/drmcp/core/credentials.py +190 -0
- datarobot_genai/drmcp/core/dr_mcp_server.py +350 -0
- datarobot_genai/drmcp/core/dr_mcp_server_logo.py +136 -0
- datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +13 -0
- datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +130 -0
- datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +70 -0
- datarobot_genai/drmcp/core/dynamic_prompts/register.py +205 -0
- datarobot_genai/drmcp/core/dynamic_prompts/utils.py +33 -0
- datarobot_genai/drmcp/core/dynamic_tools/__init__.py +14 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +14 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +72 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +82 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +238 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +228 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +63 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +162 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +87 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +36 -0
- datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +10 -0
- datarobot_genai/drmcp/core/dynamic_tools/register.py +254 -0
- datarobot_genai/drmcp/core/dynamic_tools/schema.py +532 -0
- datarobot_genai/drmcp/core/exceptions.py +25 -0
- datarobot_genai/drmcp/core/logging.py +98 -0
- datarobot_genai/drmcp/core/mcp_instance.py +515 -0
- datarobot_genai/drmcp/core/memory_management/__init__.py +13 -0
- datarobot_genai/drmcp/core/memory_management/manager.py +820 -0
- datarobot_genai/drmcp/core/memory_management/memory_tools.py +201 -0
- datarobot_genai/drmcp/core/routes.py +439 -0
- datarobot_genai/drmcp/core/routes_utils.py +30 -0
- datarobot_genai/drmcp/core/server_life_cycle.py +107 -0
- datarobot_genai/drmcp/core/telemetry.py +424 -0
- datarobot_genai/drmcp/core/tool_config.py +111 -0
- datarobot_genai/drmcp/core/tool_filter.py +117 -0
- datarobot_genai/drmcp/core/utils.py +138 -0
- datarobot_genai/drmcp/server.py +19 -0
- datarobot_genai/drmcp/test_utils/__init__.py +13 -0
- datarobot_genai/drmcp/test_utils/clients/__init__.py +0 -0
- datarobot_genai/drmcp/test_utils/clients/anthropic.py +68 -0
- datarobot_genai/drmcp/test_utils/clients/base.py +300 -0
- datarobot_genai/drmcp/test_utils/clients/dr_gateway.py +58 -0
- datarobot_genai/drmcp/test_utils/clients/openai.py +68 -0
- datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
- datarobot_genai/drmcp/test_utils/integration_mcp_server.py +109 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +133 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +107 -0
- datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +220 -0
- datarobot_genai/drmcp/test_utils/utils.py +91 -0
- datarobot_genai/drmcp/tools/__init__.py +14 -0
- datarobot_genai/drmcp/tools/clients/__init__.py +14 -0
- datarobot_genai/drmcp/tools/clients/atlassian.py +188 -0
- datarobot_genai/drmcp/tools/clients/confluence.py +584 -0
- datarobot_genai/drmcp/tools/clients/gdrive.py +832 -0
- datarobot_genai/drmcp/tools/clients/jira.py +334 -0
- datarobot_genai/drmcp/tools/clients/microsoft_graph.py +479 -0
- datarobot_genai/drmcp/tools/clients/s3.py +28 -0
- datarobot_genai/drmcp/tools/confluence/__init__.py +14 -0
- datarobot_genai/drmcp/tools/confluence/tools.py +321 -0
- datarobot_genai/drmcp/tools/gdrive/__init__.py +0 -0
- datarobot_genai/drmcp/tools/gdrive/tools.py +347 -0
- datarobot_genai/drmcp/tools/jira/__init__.py +14 -0
- datarobot_genai/drmcp/tools/jira/tools.py +243 -0
- datarobot_genai/drmcp/tools/microsoft_graph/__init__.py +13 -0
- datarobot_genai/drmcp/tools/microsoft_graph/tools.py +198 -0
- datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
- datarobot_genai/drmcp/tools/predictive/data.py +133 -0
- datarobot_genai/drmcp/tools/predictive/deployment.py +91 -0
- datarobot_genai/drmcp/tools/predictive/deployment_info.py +392 -0
- datarobot_genai/drmcp/tools/predictive/model.py +148 -0
- datarobot_genai/drmcp/tools/predictive/predict.py +254 -0
- datarobot_genai/drmcp/tools/predictive/predict_realtime.py +307 -0
- datarobot_genai/drmcp/tools/predictive/project.py +90 -0
- datarobot_genai/drmcp/tools/predictive/training.py +661 -0
- datarobot_genai/langgraph/__init__.py +0 -0
- datarobot_genai/langgraph/agent.py +341 -0
- datarobot_genai/langgraph/mcp.py +73 -0
- datarobot_genai/llama_index/__init__.py +16 -0
- datarobot_genai/llama_index/agent.py +50 -0
- datarobot_genai/llama_index/base.py +299 -0
- datarobot_genai/llama_index/mcp.py +79 -0
- datarobot_genai/nat/__init__.py +0 -0
- datarobot_genai/nat/agent.py +275 -0
- datarobot_genai/nat/datarobot_auth_provider.py +110 -0
- datarobot_genai/nat/datarobot_llm_clients.py +318 -0
- datarobot_genai/nat/datarobot_llm_providers.py +130 -0
- datarobot_genai/nat/datarobot_mcp_client.py +266 -0
- datarobot_genai/nat/helpers.py +87 -0
- datarobot_genai/py.typed +0 -0
- datarobot_genai-0.2.31.dist-info/METADATA +145 -0
- datarobot_genai-0.2.31.dist-info/RECORD +125 -0
- datarobot_genai-0.2.31.dist-info/WHEEL +4 -0
- datarobot_genai-0.2.31.dist-info/entry_points.txt +5 -0
- datarobot_genai-0.2.31.dist-info/licenses/AUTHORS +2 -0
- datarobot_genai-0.2.31.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,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.
|