datarobot-genai 0.2.17__tar.gz → 0.2.23__tar.gz

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 (120) hide show
  1. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/PKG-INFO +1 -1
  2. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/pyproject.toml +4 -1
  3. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/config.py +24 -0
  4. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/tool_config.py +8 -0
  5. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/utils.py +7 -0
  6. datarobot_genai-0.2.23/src/datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
  7. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/test_utils/integration_mcp_server.py +7 -0
  8. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +9 -1
  9. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +17 -4
  10. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +71 -8
  11. datarobot_genai-0.2.23/src/datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
  12. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/test_utils/tool_base_ete.py +22 -20
  13. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/clients/confluence.py +192 -1
  14. datarobot_genai-0.2.23/src/datarobot_genai/drmcp/tools/clients/gdrive.py +610 -0
  15. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/confluence/tools.py +133 -0
  16. datarobot_genai-0.2.23/src/datarobot_genai/drmcp/tools/gdrive/tools.py +177 -0
  17. datarobot_genai-0.2.23/src/datarobot_genai/drmcp/tools/predictive/data.py +125 -0
  18. datarobot_genai-0.2.23/src/datarobot_genai/py.typed +0 -0
  19. datarobot_genai-0.2.17/src/datarobot_genai/drmcp/tools/predictive/data.py +0 -97
  20. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/.gitignore +0 -0
  21. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/AUTHORS +0 -0
  22. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/LICENSE +0 -0
  23. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/README.md +0 -0
  24. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/__init__.py +0 -0
  25. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/__init__.py +0 -0
  26. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/agents/__init__.py +0 -0
  27. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/agents/base.py +0 -0
  28. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/chat/__init__.py +0 -0
  29. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/chat/auth.py +0 -0
  30. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/chat/client.py +0 -0
  31. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/chat/responses.py +0 -0
  32. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/cli/__init__.py +0 -0
  33. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/cli/agent_environment.py +0 -0
  34. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/cli/agent_kernel.py +0 -0
  35. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/custom_model.py +0 -0
  36. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/mcp/__init__.py +0 -0
  37. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/mcp/common.py +0 -0
  38. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/telemetry_agent.py +0 -0
  39. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/utils/__init__.py +0 -0
  40. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/utils/auth.py +0 -0
  41. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/core/utils/urls.py +0 -0
  42. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/crewai/__init__.py +0 -0
  43. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/crewai/agent.py +0 -0
  44. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/crewai/base.py +0 -0
  45. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/crewai/events.py +0 -0
  46. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/crewai/mcp.py +0 -0
  47. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/__init__.py +0 -0
  48. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/__init__.py +0 -0
  49. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/auth.py +0 -0
  50. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/clients.py +0 -0
  51. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/config_utils.py +0 -0
  52. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/constants.py +0 -0
  53. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/credentials.py +0 -0
  54. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dr_mcp_server.py +0 -0
  55. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dr_mcp_server_logo.py +0 -0
  56. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +0 -0
  57. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +0 -0
  58. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +0 -0
  59. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_prompts/register.py +0 -0
  60. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_prompts/utils.py +0 -0
  61. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/__init__.py +0 -0
  62. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
  63. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +0 -0
  64. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +0 -0
  65. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +0 -0
  66. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +0 -0
  67. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +0 -0
  68. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +0 -0
  69. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +0 -0
  70. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +0 -0
  71. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +0 -0
  72. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +0 -0
  73. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/register.py +0 -0
  74. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/dynamic_tools/schema.py +0 -0
  75. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/exceptions.py +0 -0
  76. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/logging.py +0 -0
  77. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/mcp_instance.py +0 -0
  78. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/mcp_server_tools.py +0 -0
  79. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/memory_management/__init__.py +0 -0
  80. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/memory_management/manager.py +0 -0
  81. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/memory_management/memory_tools.py +0 -0
  82. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/routes.py +0 -0
  83. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/routes_utils.py +0 -0
  84. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/server_life_cycle.py +0 -0
  85. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/telemetry.py +0 -0
  86. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/core/tool_filter.py +0 -0
  87. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/server.py +0 -0
  88. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/test_utils/__init__.py +0 -0
  89. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/test_utils/utils.py +0 -0
  90. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/__init__.py +0 -0
  91. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/clients/__init__.py +0 -0
  92. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/clients/atlassian.py +0 -0
  93. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/clients/jira.py +0 -0
  94. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/clients/s3.py +0 -0
  95. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/confluence/__init__.py +0 -0
  96. {datarobot_genai-0.2.17/src/datarobot_genai/langgraph → datarobot_genai-0.2.23/src/datarobot_genai/drmcp/tools/gdrive}/__init__.py +0 -0
  97. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/jira/__init__.py +0 -0
  98. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/jira/tools.py +0 -0
  99. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/predictive/__init__.py +0 -0
  100. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/predictive/deployment.py +0 -0
  101. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/predictive/deployment_info.py +0 -0
  102. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/predictive/model.py +0 -0
  103. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/predictive/predict.py +0 -0
  104. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/predictive/predict_realtime.py +0 -0
  105. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/predictive/project.py +0 -0
  106. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/drmcp/tools/predictive/training.py +0 -0
  107. {datarobot_genai-0.2.17/src/datarobot_genai/nat → datarobot_genai-0.2.23/src/datarobot_genai/langgraph}/__init__.py +0 -0
  108. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/langgraph/agent.py +0 -0
  109. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/langgraph/mcp.py +0 -0
  110. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/llama_index/__init__.py +0 -0
  111. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/llama_index/agent.py +0 -0
  112. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/llama_index/base.py +0 -0
  113. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/llama_index/mcp.py +0 -0
  114. /datarobot_genai-0.2.17/src/datarobot_genai/py.typed → /datarobot_genai-0.2.23/src/datarobot_genai/nat/__init__.py +0 -0
  115. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/nat/agent.py +0 -0
  116. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/nat/datarobot_auth_provider.py +0 -0
  117. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/nat/datarobot_llm_clients.py +0 -0
  118. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/nat/datarobot_llm_providers.py +0 -0
  119. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/nat/datarobot_mcp_client.py +0 -0
  120. {datarobot_genai-0.2.17 → datarobot_genai-0.2.23}/src/datarobot_genai/nat/helpers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datarobot-genai
3
- Version: 0.2.17
3
+ Version: 0.2.23
4
4
  Summary: Generic helpers for GenAI
5
5
  Project-URL: Homepage, https://github.com/datarobot-oss/datarobot-genai
6
6
  Author: DataRobot, Inc.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "datarobot-genai"
7
- version = "0.2.17"
7
+ version = "0.2.23"
8
8
  description = "Generic helpers for GenAI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10, <3.13"
@@ -205,6 +205,9 @@ asyncio_mode = "auto"
205
205
  source = ["datarobot_genai"]
206
206
  omit = [
207
207
  "*/__init__.py",
208
+ "*/test_utils/*",
209
+ # nat requires Python >=3.11, can't be imported on 3.10 for coverage
210
+ "*/nat/*",
208
211
  ]
209
212
 
210
213
  [tool.coverage.report]
@@ -245,6 +245,30 @@ class MCPServerConfig(BaseSettings):
245
245
  os.getenv("CONFLUENCE_CLIENT_ID") and os.getenv("CONFLUENCE_CLIENT_SECRET")
246
246
  )
247
247
 
248
+ # Gdrive tools
249
+ enable_gdrive_tools: bool = Field(
250
+ default=False,
251
+ validation_alias=AliasChoices(
252
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "ENABLE_GDRIVE_TOOLS",
253
+ "ENABLE_GDRIVE_TOOLS",
254
+ ),
255
+ description="Enable/disable GDrive tools",
256
+ )
257
+ is_gdrive_oauth_provider_configured: bool = Field(
258
+ default=False,
259
+ validation_alias=AliasChoices(
260
+ RUNTIME_PARAM_ENV_VAR_NAME_PREFIX + "IS_GDRIVE_OAUTH_PROVIDER_CONFIGURED",
261
+ "IS_GDRIVE_OAUTH_PROVIDER_CONFIGURED",
262
+ ),
263
+ description="Whether GDrive OAuth provider is configured for GDrive integration",
264
+ )
265
+
266
+ @property
267
+ def is_gdrive_oauth_configured(self) -> bool:
268
+ return self.is_gdrive_oauth_provider_configured or bool(
269
+ os.getenv("GDRIVE_CLIENT_ID") and os.getenv("GDRIVE_CLIENT_SECRET")
270
+ )
271
+
248
272
  @field_validator(
249
273
  "otel_attributes",
250
274
  mode="before",
@@ -29,6 +29,7 @@ class ToolType(str, Enum):
29
29
  PREDICTIVE = "predictive"
30
30
  JIRA = "jira"
31
31
  CONFLUENCE = "confluence"
32
+ GDRIVE = "gdrive"
32
33
 
33
34
 
34
35
  class ToolConfig(TypedDict):
@@ -64,6 +65,13 @@ TOOL_CONFIGS: dict[ToolType, ToolConfig] = {
64
65
  package_prefix="datarobot_genai.drmcp.tools.confluence",
65
66
  config_field_name="enable_confluence_tools",
66
67
  ),
68
+ ToolType.GDRIVE: ToolConfig(
69
+ name="gdrive",
70
+ oauth_check=lambda config: config.is_gdrive_oauth_configured,
71
+ directory="gdrive",
72
+ package_prefix="datarobot_genai.drmcp.tools.gdrive",
73
+ config_field_name="enable_gdrive_tools",
74
+ ),
67
75
  }
68
76
 
69
77
 
@@ -14,6 +14,7 @@
14
14
  import base64
15
15
  import uuid
16
16
  from typing import Any
17
+ from urllib.parse import urlparse
17
18
 
18
19
  import boto3
19
20
  from fastmcp.resources import HttpResource
@@ -129,3 +130,9 @@ def format_response_as_tool_result(data: bytes, content_type: str, charset: str)
129
130
  }
130
131
 
131
132
  return ToolResult(structured_content=payload)
133
+
134
+
135
+ def is_valid_url(url: str) -> bool:
136
+ """Check if a URL is valid."""
137
+ result = urlparse(url)
138
+ return all([result.scheme, result.netloc])
@@ -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
+ }
@@ -26,6 +26,13 @@ from typing import Any
26
26
 
27
27
  from datarobot_genai.drmcp import create_mcp_server
28
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
+
29
36
  # Import user components (will be used conditionally)
30
37
  try:
31
38
  from app.core.server_lifecycle import ServerLifecycle # type: ignore # noqa: F401
@@ -15,6 +15,7 @@ import asyncio
15
15
  import os
16
16
  from collections.abc import AsyncGenerator
17
17
  from contextlib import asynccontextmanager
18
+ from typing import Any
18
19
 
19
20
  import aiohttp
20
21
  from aiohttp import ClientSession as HttpClientSession
@@ -78,6 +79,7 @@ def get_headers() -> dict[str, str]:
78
79
  @asynccontextmanager
79
80
  async def ete_test_mcp_session(
80
81
  additional_headers: dict[str, str] | None = None,
82
+ elicitation_callback: Any | None = None,
81
83
  ) -> AsyncGenerator[ClientSession, None]:
82
84
  """Create an MCP session for each test.
83
85
 
@@ -85,6 +87,10 @@ async def ete_test_mcp_session(
85
87
  ----------
86
88
  additional_headers : dict[str, str], optional
87
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
88
94
  """
89
95
  try:
90
96
  headers = get_headers()
@@ -96,7 +102,9 @@ async def ete_test_mcp_session(
96
102
  write_stream,
97
103
  _,
98
104
  ):
99
- async with ClientSession(read_stream, write_stream) as session:
105
+ async with ClientSession(
106
+ read_stream, write_stream, elicitation_callback=elicitation_callback
107
+ ) as session:
100
108
  await asyncio.wait_for(session.initialize(), timeout=5)
101
109
  yield session
102
110
  except asyncio.TimeoutError:
@@ -17,6 +17,7 @@ import contextlib
17
17
  import os
18
18
  from collections.abc import AsyncGenerator
19
19
  from pathlib import Path
20
+ from typing import Any
20
21
 
21
22
  from mcp import ClientSession
22
23
  from mcp.client.stdio import StdioServerParameters
@@ -34,7 +35,12 @@ def integration_test_mcp_server_params() -> StdioServerParameters:
34
35
  or "https://test.datarobot.com/api/v2",
35
36
  "MCP_SERVER_LOG_LEVEL": os.environ.get("MCP_SERVER_LOG_LEVEL") or "WARNING",
36
37
  "APP_LOG_LEVEL": os.environ.get("APP_LOG_LEVEL") or "WARNING",
37
- "OTEL_ENABLED": os.environ.get("OTEL_ENABLED") or "false",
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",
38
44
  "MCP_SERVER_REGISTER_DYNAMIC_TOOLS_ON_STARTUP": os.environ.get(
39
45
  "MCP_SERVER_REGISTER_DYNAMIC_TOOLS_ON_STARTUP"
40
46
  )
@@ -64,7 +70,9 @@ def integration_test_mcp_server_params() -> StdioServerParameters:
64
70
 
65
71
  @contextlib.asynccontextmanager
66
72
  async def integration_test_mcp_session(
67
- server_params: StdioServerParameters | None = None, timeout: int = 30
73
+ server_params: StdioServerParameters | None = None,
74
+ timeout: int = 30,
75
+ elicitation_callback: Any | None = None,
68
76
  ) -> AsyncGenerator[ClientSession, None]:
69
77
  """
70
78
  Create and connect a client for the MCP server as a context manager.
@@ -72,6 +80,7 @@ async def integration_test_mcp_session(
72
80
  Args:
73
81
  server_params: Parameters for configuring the server connection
74
82
  timeout: Timeout
83
+ elicitation_callback: Optional callback for handling elicitation requests
75
84
 
76
85
  Yields
77
86
  ------
@@ -86,8 +95,12 @@ async def integration_test_mcp_session(
86
95
 
87
96
  try:
88
97
  async with stdio_client(server_params) as (read_stream, write_stream):
89
- async with ClientSession(read_stream, write_stream) as session:
90
- await asyncio.wait_for(session.initialize(), timeout=timeout)
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]
91
104
  yield session
92
105
 
93
106
  except asyncio.TimeoutError:
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import json
16
+ from ast import literal_eval
16
17
  from typing import Any
17
18
 
18
19
  import openai
@@ -44,12 +45,39 @@ class LLMResponse:
44
45
 
45
46
 
46
47
  class LLMMCPClient:
47
- """Client for interacting with LLMs via MCP."""
48
+ """
49
+ Client for interacting with LLMs via MCP.
48
50
 
49
- def __init__(self, config: str):
50
- """Initialize the LLM MCP client."""
51
+ Note: Elicitation is handled at the protocol level by FastMCP's ctx.elicit().
52
+ Tools using FastMCP's built-in elicitation will work automatically.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ config: str,
58
+ ):
59
+ """
60
+ Initialize the LLM MCP client.
61
+
62
+ Args:
63
+ config: Configuration string or dict with:
64
+ - openai_api_key: OpenAI API key
65
+ - openai_api_base: Optional Azure OpenAI endpoint
66
+ - openai_api_deployment_id: Optional Azure deployment ID
67
+ - openai_api_version: Optional Azure API version
68
+ - model: Model name (default: "gpt-3.5-turbo")
69
+ - save_llm_responses: Whether to save responses (default: True)
70
+ """
51
71
  # Parse config string to extract parameters
52
- config_dict = eval(config) if isinstance(config, str) else config
72
+ if isinstance(config, str):
73
+ # Try JSON first (safer), fall back to literal_eval for Python dict strings
74
+ try:
75
+ config_dict = json.loads(config)
76
+ except json.JSONDecodeError:
77
+ # Fall back to literal_eval for Python dict literal strings
78
+ config_dict = literal_eval(config)
79
+ else:
80
+ config_dict = config
53
81
 
54
82
  openai_api_key = config_dict.get("openai_api_key")
55
83
  openai_api_base = config_dict.get("openai_api_base")
@@ -93,7 +121,21 @@ class LLMMCPClient:
93
121
  async def _call_mcp_tool(
94
122
  self, tool_name: str, parameters: dict[str, Any], mcp_session: ClientSession
95
123
  ) -> str:
96
- """Call an MCP tool and return the result as a string."""
124
+ """
125
+ Call an MCP tool and return the result as a string.
126
+
127
+ Note: Elicitation is handled at the protocol level by FastMCP's ctx.elicit().
128
+ Tools using FastMCP's built-in elicitation will work automatically.
129
+
130
+ Args:
131
+ tool_name: Name of the tool to call
132
+ parameters: Parameters to pass to the tool
133
+ mcp_session: MCP client session
134
+
135
+ Returns
136
+ -------
137
+ Result text from the tool call
138
+ """
97
139
  result: CallToolResult = await mcp_session.call_tool(tool_name, parameters)
98
140
  content = (
99
141
  result.content[0].text
@@ -177,7 +219,26 @@ class LLMMCPClient:
177
219
  async def process_prompt_with_mcp_support(
178
220
  self, prompt: str, mcp_session: ClientSession, output_file_name: str = ""
179
221
  ) -> LLMResponse:
180
- """Process a prompt with MCP tool support."""
222
+ """
223
+ Process a prompt with MCP tool support and elicitation handling.
224
+
225
+ This method:
226
+ 1. Adds MCP tools to available tools
227
+ 2. Sends prompt to LLM
228
+ 3. Processes tool calls
229
+ 4. Continues until LLM provides final response
230
+
231
+ Note: Elicitation is handled at the protocol level by FastMCP's ctx.elicit().
232
+
233
+ Args:
234
+ prompt: User prompt
235
+ mcp_session: MCP client session
236
+ output_file_name: Optional file name to save response
237
+
238
+ Returns
239
+ -------
240
+ LLMResponse with content, tool calls, and tool results
241
+ """
181
242
  # Add MCP tools to available tools
182
243
  await self._add_mcp_tool_to_available_tools(mcp_session)
183
244
 
@@ -191,8 +252,10 @@ class LLMMCPClient:
191
252
  "content": (
192
253
  "You are a helpful AI assistant that can use tools to help users. "
193
254
  "If you need more information to provide a complete response, you can make "
194
- "multiple tool calls. When dealing with file paths, use them as raw paths "
195
- "without converting to file:// URLs."
255
+ "multiple tool calls or ask the user for more info, but prefer tool calls "
256
+ "when possible. "
257
+ "When dealing with file paths, use them as raw paths without converting "
258
+ "to file:// URLs."
196
259
  ),
197
260
  },
198
261
  {"role": "user", "content": prompt},
@@ -0,0 +1,205 @@
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
+ """Interactive MCP Client Test Script.
18
+
19
+ This script allows you to test arbitrary commands with the MCP server
20
+ using an LLM agent that can decide which tools to call.
21
+
22
+ Supports elicitation - when tools require user input (like authentication tokens),
23
+ the script will prompt you interactively.
24
+ """
25
+
26
+ import asyncio
27
+ import json
28
+ import os
29
+ import sys
30
+ import traceback
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+ from dotenv import load_dotenv
35
+ from mcp import ClientSession
36
+ from mcp.client.streamable_http import streamablehttp_client
37
+ from mcp.shared.context import RequestContext
38
+ from mcp.types import ElicitRequestParams
39
+ from mcp.types import ElicitResult
40
+
41
+ from datarobot_genai.drmcp import get_dr_mcp_server_url
42
+ from datarobot_genai.drmcp import get_headers
43
+ from datarobot_genai.drmcp.test_utils.openai_llm_mcp_client import LLMMCPClient
44
+ from datarobot_genai.drmcp.test_utils.openai_llm_mcp_client import LLMResponse
45
+ from datarobot_genai.drmcp.test_utils.openai_llm_mcp_client import ToolCall
46
+
47
+ # Re-export for backwards compatibility
48
+ __all__ = ["LLMMCPClient", "LLMResponse", "ToolCall", "test_mcp_interactive"]
49
+
50
+
51
+ async def test_mcp_interactive() -> None:
52
+ """Test the MCP server interactively with LLM agent."""
53
+ # Check for required environment variables
54
+ openai_api_key = os.environ.get("OPENAI_API_KEY")
55
+ if not openai_api_key:
56
+ print("❌ Error: OPENAI_API_KEY environment variable is required")
57
+ print("Please set it in your .env file or export it")
58
+ return
59
+
60
+ # Optional Azure OpenAI settings
61
+ openai_api_base = os.environ.get("OPENAI_API_BASE")
62
+ openai_api_deployment_id = os.environ.get("OPENAI_API_DEPLOYMENT_ID")
63
+ openai_api_version = os.environ.get("OPENAI_API_VERSION")
64
+
65
+ print("🤖 Initializing LLM MCP Client...")
66
+
67
+ # Initialize the LLM client with elicitation handler
68
+ config = {
69
+ "openai_api_key": openai_api_key,
70
+ "openai_api_base": openai_api_base,
71
+ "openai_api_deployment_id": openai_api_deployment_id,
72
+ "openai_api_version": openai_api_version,
73
+ "save_llm_responses": False,
74
+ }
75
+
76
+ llm_client = LLMMCPClient(str(config))
77
+
78
+ # Get MCP server URL
79
+ mcp_server_url = get_dr_mcp_server_url()
80
+ if not mcp_server_url:
81
+ print("❌ Error: MCP server URL is not configured")
82
+ print("Please set DR_MCP_SERVER_URL environment variable or run: task test-interactive")
83
+ return
84
+
85
+ print(f"🔗 Connecting to MCP server at: {mcp_server_url}")
86
+
87
+ # Elicitation handler: prompt user for required values
88
+ async def elicitation_handler(
89
+ context: RequestContext[ClientSession, Any], params: ElicitRequestParams
90
+ ) -> ElicitResult:
91
+ print(f"\n📋 Elicitation Request: {params.message}")
92
+ if params.requestedSchema:
93
+ print(f" Schema: {params.requestedSchema}")
94
+
95
+ while True:
96
+ try:
97
+ response = input(" Enter value (or 'decline'/'cancel'): ").strip()
98
+ except (EOFError, KeyboardInterrupt):
99
+ return ElicitResult(action="cancel")
100
+
101
+ if response.lower() == "decline":
102
+ return ElicitResult(action="decline")
103
+ if response.lower() == "cancel":
104
+ return ElicitResult(action="cancel")
105
+ if response:
106
+ return ElicitResult(action="accept", content={"value": response})
107
+ print(" Please enter a value or 'decline'/'cancel'")
108
+
109
+ try:
110
+ async with streamablehttp_client(
111
+ url=mcp_server_url,
112
+ headers=get_headers(),
113
+ ) as (read_stream, write_stream, _):
114
+ async with ClientSession(
115
+ read_stream,
116
+ write_stream,
117
+ elicitation_callback=elicitation_handler,
118
+ ) as session:
119
+ await session.initialize()
120
+
121
+ print("✅ Connected to MCP server!")
122
+ print("📋 Available tools:")
123
+
124
+ tools_result = await session.list_tools()
125
+ for i, tool in enumerate(tools_result.tools, 1):
126
+ print(f" {i}. {tool.name}: {tool.description}")
127
+
128
+ print("\n" + "=" * 60)
129
+ print("🎯 Interactive Testing Mode")
130
+ print("=" * 60)
131
+ print("Type your questions/commands. The AI will decide which tools to use.")
132
+ print("If a tool requires additional information, you will be prompted.")
133
+ print("Type 'quit' or 'exit' to stop.")
134
+ print()
135
+
136
+ while True:
137
+ try:
138
+ user_input = input("🤔 You: ").strip()
139
+
140
+ if user_input.lower() in ["quit", "exit", "q"]:
141
+ print("👋 Goodbye!")
142
+ break
143
+
144
+ if not user_input:
145
+ continue
146
+ except (EOFError, KeyboardInterrupt):
147
+ print("\n👋 Goodbye!")
148
+ break
149
+
150
+ print("🤖 AI is thinking...")
151
+
152
+ response = await llm_client.process_prompt_with_mcp_support(
153
+ prompt=user_input,
154
+ mcp_session=session,
155
+ )
156
+
157
+ print("\n🤖 AI Response:")
158
+ print("-" * 40)
159
+ print(response.content)
160
+
161
+ if response.tool_calls:
162
+ print("\n🔧 Tools Used:")
163
+ for i, tool_call in enumerate(response.tool_calls, 1):
164
+ print(f" {i}. {tool_call.tool_name}")
165
+ print(f" Parameters: {tool_call.parameters}")
166
+ print(f" Reasoning: {tool_call.reasoning}")
167
+
168
+ if i <= len(response.tool_results):
169
+ result = response.tool_results[i - 1]
170
+ try:
171
+ result_data = json.loads(result)
172
+ if result_data.get("status") == "error":
173
+ error_msg = result_data.get("error", "Unknown error")
174
+ print(f" ❌ Error: {error_msg}")
175
+ elif result_data.get("status") == "success":
176
+ print(" ✅ Success")
177
+ except json.JSONDecodeError:
178
+ if len(result) > 100:
179
+ print(f" Result: {result[:100]}...")
180
+ else:
181
+ print(f" Result: {result}")
182
+
183
+ print("\n" + "=" * 60)
184
+ except Exception as e:
185
+ print(f"❌ Connection Error: {e}")
186
+ print(f" Server URL: {mcp_server_url}")
187
+ traceback.print_exc()
188
+ return
189
+
190
+
191
+ if __name__ == "__main__":
192
+ # Ensure we're in the right directory
193
+ if not Path("src").exists():
194
+ print("❌ Error: Please run this script from the project root")
195
+ sys.exit(1)
196
+
197
+ # Load environment variables from .env file
198
+ print("📄 Loading environment variables...")
199
+ load_dotenv()
200
+
201
+ print("🚀 Starting Interactive MCP Client Test")
202
+ print("Make sure the MCP server is running with: task drmcp-dev")
203
+ print()
204
+
205
+ asyncio.run(test_mcp_interactive())
@@ -116,28 +116,30 @@ class ToolBaseE2E:
116
116
  f"Should have called {test_expectations.tool_calls_expected[i].name} tool, but "
117
117
  f"got: {tool_call.tool_name}"
118
118
  )
119
- assert tool_call.parameters == test_expectations.tool_calls_expected[i].parameters, (
120
- f"Should have called {tool_call.tool_name} tool with the correct parameters, but "
121
- f"got: {tool_call.parameters}"
122
- )
123
- if test_expectations.tool_calls_expected[i].result != SHOULD_NOT_BE_EMPTY:
124
- expected_result = test_expectations.tool_calls_expected[i].result
125
- if isinstance(expected_result, str):
126
- assert expected_result in response.tool_results[i], (
127
- f"Should have called {tool_call.tool_name} tool with the correct result, "
128
- f"but got: {response.tool_results[i]}"
129
- )
119
+ assert (
120
+ tool_call.parameters == test_expectations.tool_calls_expected[i].parameters
121
+ ), (
122
+ f"Should have called {tool_call.tool_name} tool with the correct parameters, "
123
+ f"but got: {tool_call.parameters}"
124
+ )
125
+ if test_expectations.tool_calls_expected[i].result != SHOULD_NOT_BE_EMPTY:
126
+ expected_result = test_expectations.tool_calls_expected[i].result
127
+ if isinstance(expected_result, str):
128
+ assert expected_result in response.tool_results[i], (
129
+ f"Should have called {tool_call.tool_name} tool with the correct "
130
+ f"result, but got: {response.tool_results[i]}"
131
+ )
132
+ else:
133
+ actual_result = json.loads(response.tool_results[i])
134
+ assert _check_dict_has_keys(expected_result, actual_result), (
135
+ f"Should have called {tool_call.tool_name} tool with the correct "
136
+ f"result structure, but got: {response.tool_results[i]}"
137
+ )
130
138
  else:
131
- actual_result = json.loads(response.tool_results[i])
132
- assert _check_dict_has_keys(expected_result, actual_result), (
133
- f"Should have called {tool_call.tool_name} tool with the correct result "
134
- f"structure, but got: {response.tool_results[i]}"
139
+ assert len(response.tool_results[i]) > 0, (
140
+ f"Should have called {tool_call.tool_name} tool with non-empty result, but "
141
+ f"got: {response.tool_results[i]}"
135
142
  )
136
- else:
137
- assert len(response.tool_results[i]) > 0, (
138
- f"Should have called {tool_call.tool_name} tool with non-empty result, but "
139
- f"got: {response.tool_results[i]}"
140
- )
141
143
 
142
144
  # Verify LLM provided comprehensive response
143
145
  assert len(response.content) > 100, "LLM should provide detailed response"