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,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")
|