datarobot-genai 0.2.17__tar.gz → 0.2.25__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 (121) hide show
  1. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/PKG-INFO +1 -1
  2. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/pyproject.toml +4 -1
  3. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/config.py +24 -0
  4. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dr_mcp_server.py +0 -3
  5. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/mcp_instance.py +3 -88
  6. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/tool_config.py +8 -0
  7. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/tool_filter.py +10 -1
  8. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/utils.py +7 -0
  9. datarobot_genai-0.2.25/src/datarobot_genai/drmcp/test_utils/elicitation_test_tool.py +89 -0
  10. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/integration_mcp_server.py +7 -0
  11. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +9 -1
  12. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +17 -4
  13. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +71 -8
  14. datarobot_genai-0.2.25/src/datarobot_genai/drmcp/test_utils/test_interactive.py +205 -0
  15. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/tool_base_ete.py +22 -20
  16. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/confluence.py +192 -1
  17. datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/clients/gdrive.py +610 -0
  18. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/confluence/tools.py +133 -0
  19. datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/gdrive/tools.py +177 -0
  20. datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/predictive/data.py +125 -0
  21. datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/predictive/project.py +90 -0
  22. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/training.py +160 -151
  23. datarobot_genai-0.2.25/src/datarobot_genai/py.typed +0 -0
  24. datarobot_genai-0.2.17/src/datarobot_genai/drmcp/core/mcp_server_tools.py +0 -129
  25. datarobot_genai-0.2.17/src/datarobot_genai/drmcp/tools/predictive/data.py +0 -97
  26. datarobot_genai-0.2.17/src/datarobot_genai/drmcp/tools/predictive/project.py +0 -72
  27. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/.gitignore +0 -0
  28. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/AUTHORS +0 -0
  29. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/LICENSE +0 -0
  30. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/README.md +0 -0
  31. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/__init__.py +0 -0
  32. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/__init__.py +0 -0
  33. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/agents/__init__.py +0 -0
  34. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/agents/base.py +0 -0
  35. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/chat/__init__.py +0 -0
  36. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/chat/auth.py +0 -0
  37. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/chat/client.py +0 -0
  38. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/chat/responses.py +0 -0
  39. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/cli/__init__.py +0 -0
  40. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/cli/agent_environment.py +0 -0
  41. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/cli/agent_kernel.py +0 -0
  42. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/custom_model.py +0 -0
  43. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/mcp/__init__.py +0 -0
  44. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/mcp/common.py +0 -0
  45. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/telemetry_agent.py +0 -0
  46. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/utils/__init__.py +0 -0
  47. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/utils/auth.py +0 -0
  48. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/core/utils/urls.py +0 -0
  49. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/__init__.py +0 -0
  50. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/agent.py +0 -0
  51. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/base.py +0 -0
  52. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/events.py +0 -0
  53. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/crewai/mcp.py +0 -0
  54. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/__init__.py +0 -0
  55. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/__init__.py +0 -0
  56. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/auth.py +0 -0
  57. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/clients.py +0 -0
  58. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/config_utils.py +0 -0
  59. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/constants.py +0 -0
  60. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/credentials.py +0 -0
  61. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dr_mcp_server_logo.py +0 -0
  62. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/__init__.py +0 -0
  63. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/controllers.py +0 -0
  64. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/dr_lib.py +0 -0
  65. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/register.py +0 -0
  66. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_prompts/utils.py +0 -0
  67. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/__init__.py +0 -0
  68. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/__init__.py +0 -0
  69. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/__init__.py +0 -0
  70. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/base.py +0 -0
  71. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/default.py +0 -0
  72. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/adapters/drum.py +0 -0
  73. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/config.py +0 -0
  74. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/controllers.py +0 -0
  75. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/metadata.py +0 -0
  76. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/register.py +0 -0
  77. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_agentic_fallback_schema.json +0 -0
  78. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json +0 -0
  79. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/register.py +0 -0
  80. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/dynamic_tools/schema.py +0 -0
  81. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/exceptions.py +0 -0
  82. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/logging.py +0 -0
  83. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/memory_management/__init__.py +0 -0
  84. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/memory_management/manager.py +0 -0
  85. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/memory_management/memory_tools.py +0 -0
  86. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/routes.py +0 -0
  87. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/routes_utils.py +0 -0
  88. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/server_life_cycle.py +0 -0
  89. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/core/telemetry.py +0 -0
  90. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/server.py +0 -0
  91. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/__init__.py +0 -0
  92. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/test_utils/utils.py +0 -0
  93. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/__init__.py +0 -0
  94. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/__init__.py +0 -0
  95. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/atlassian.py +0 -0
  96. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/jira.py +0 -0
  97. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/clients/s3.py +0 -0
  98. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/confluence/__init__.py +0 -0
  99. {datarobot_genai-0.2.17/src/datarobot_genai/langgraph → datarobot_genai-0.2.25/src/datarobot_genai/drmcp/tools/gdrive}/__init__.py +0 -0
  100. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/jira/__init__.py +0 -0
  101. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/jira/tools.py +0 -0
  102. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/__init__.py +0 -0
  103. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/deployment.py +0 -0
  104. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/deployment_info.py +0 -0
  105. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/model.py +0 -0
  106. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/predict.py +0 -0
  107. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/drmcp/tools/predictive/predict_realtime.py +0 -0
  108. {datarobot_genai-0.2.17/src/datarobot_genai/nat → datarobot_genai-0.2.25/src/datarobot_genai/langgraph}/__init__.py +0 -0
  109. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/langgraph/agent.py +0 -0
  110. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/langgraph/mcp.py +0 -0
  111. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/llama_index/__init__.py +0 -0
  112. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/llama_index/agent.py +0 -0
  113. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/llama_index/base.py +0 -0
  114. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/llama_index/mcp.py +0 -0
  115. /datarobot_genai-0.2.17/src/datarobot_genai/py.typed → /datarobot_genai-0.2.25/src/datarobot_genai/nat/__init__.py +0 -0
  116. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/agent.py +0 -0
  117. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/datarobot_auth_provider.py +0 -0
  118. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/datarobot_llm_clients.py +0 -0
  119. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/datarobot_llm_providers.py +0 -0
  120. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/src/datarobot_genai/nat/datarobot_mcp_client.py +0 -0
  121. {datarobot_genai-0.2.17 → datarobot_genai-0.2.25}/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.25
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.25"
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",
@@ -31,9 +31,6 @@ from .dynamic_prompts.register import register_prompts_from_datarobot_prompt_man
31
31
  from .dynamic_tools.deployment.register import register_tools_of_datarobot_deployments
32
32
  from .logging import MCPLogging
33
33
  from .mcp_instance import mcp
34
- from .mcp_server_tools import get_all_available_tags # noqa # pylint: disable=unused-import
35
- from .mcp_server_tools import get_tool_info_by_name # noqa # pylint: disable=unused-import
36
- from .mcp_server_tools import list_tools_by_tags # noqa # pylint: disable=unused-import
37
34
  from .memory_management.manager import MemoryManager
38
35
  from .routes import register_routes
39
36
  from .routes_utils import prefix_mount_path
@@ -16,17 +16,13 @@ import logging
16
16
  from collections.abc import Callable
17
17
  from functools import wraps
18
18
  from typing import Any
19
- from typing import overload
20
19
 
21
20
  from fastmcp import Context
22
21
  from fastmcp import FastMCP
23
22
  from fastmcp.exceptions import NotFoundError
24
23
  from fastmcp.prompts.prompt import Prompt
25
24
  from fastmcp.server.dependencies import get_context
26
- from fastmcp.tools import FunctionTool
27
25
  from fastmcp.tools import Tool
28
- from fastmcp.utilities.types import NotSet
29
- from fastmcp.utilities.types import NotSetT
30
26
  from mcp.types import AnyFunction
31
27
  from mcp.types import Tool as MCPTool
32
28
  from mcp.types import ToolAnnotations
@@ -120,86 +116,6 @@ class TaggedFastMCP(FastMCP):
120
116
  "In stateless mode, clients will see changes on next request."
121
117
  )
122
118
 
123
- @overload
124
- def tool(
125
- self,
126
- name_or_fn: AnyFunction,
127
- *,
128
- name: str | None = None,
129
- title: str | None = None,
130
- description: str | None = None,
131
- tags: set[str] | None = None,
132
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
133
- annotations: ToolAnnotations | dict[str, Any] | None = None,
134
- exclude_args: list[str] | None = None,
135
- meta: dict[str, Any] | None = None,
136
- enabled: bool | None = None,
137
- ) -> FunctionTool: ...
138
-
139
- @overload
140
- def tool(
141
- self,
142
- name_or_fn: str | None = None,
143
- *,
144
- name: str | None = None,
145
- title: str | None = None,
146
- description: str | None = None,
147
- tags: set[str] | None = None,
148
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
149
- annotations: ToolAnnotations | dict[str, Any] | None = None,
150
- exclude_args: list[str] | None = None,
151
- meta: dict[str, Any] | None = None,
152
- enabled: bool | None = None,
153
- ) -> Callable[[AnyFunction], FunctionTool]: ...
154
-
155
- def tool(
156
- self,
157
- name_or_fn: str | Callable[..., Any] | None = None,
158
- *,
159
- name: str | None = None,
160
- title: str | None = None,
161
- description: str | None = None,
162
- tags: set[str] | None = None,
163
- output_schema: dict[str, Any] | None | NotSetT = NotSet,
164
- annotations: ToolAnnotations | dict[str, Any] | None = None,
165
- exclude_args: list[str] | None = None,
166
- meta: dict[str, Any] | None = None,
167
- enabled: bool | None = None,
168
- **kwargs: Any,
169
- ) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
170
- """
171
- Extend tool decorator that supports tags and other annotations, while remaining
172
- signature-compatible with FastMCP.tool to avoid recursion issues with partials.
173
- """
174
- if isinstance(annotations, dict):
175
- annotations = ToolAnnotations(**annotations)
176
-
177
- # Ensure tags are available both via native fastmcp `tags` and inside annotations
178
- if tags is not None:
179
- tags_ = sorted(tags)
180
- if annotations is None:
181
- annotations = ToolAnnotations() # type: ignore[call-arg]
182
- annotations.tags = tags_ # type: ignore[attr-defined, union-attr]
183
- else:
184
- # At this point, annotations is ToolAnnotations (not dict)
185
- assert isinstance(annotations, ToolAnnotations)
186
- annotations.tags = tags_ # type: ignore[attr-defined]
187
-
188
- return super().tool(
189
- name_or_fn,
190
- name=name,
191
- title=title,
192
- description=description,
193
- tags=tags,
194
- output_schema=output_schema
195
- if output_schema is not None
196
- else kwargs.get("output_schema"),
197
- annotations=annotations,
198
- exclude_args=exclude_args,
199
- meta=meta,
200
- enabled=enabled,
201
- )
202
-
203
119
  async def list_tools(
204
120
  self, tags: list[str] | None = None, match_all: bool = False
205
121
  ) -> list[MCPTool]:
@@ -488,11 +404,10 @@ async def register_tools(
488
404
  # Apply dr_mcp_extras to the memory-aware function
489
405
  wrapped_fn = dr_mcp_extras()(memory_aware_fn)
490
406
 
491
- # Create annotations with tags, deployment_id if provided
492
- annotations = ToolAnnotations() # type: ignore[call-arg]
493
- if tags is not None:
494
- annotations.tags = tags # type: ignore[attr-defined]
407
+ # Create annotations only when additional metadata is required
408
+ annotations: ToolAnnotations | None = None # type: ignore[assignment]
495
409
  if deployment_id is not None:
410
+ annotations = ToolAnnotations() # type: ignore[call-arg]
496
411
  annotations.deployment_id = deployment_id # type: ignore[attr-defined]
497
412
 
498
413
  tool = Tool.from_function(
@@ -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
 
@@ -41,7 +41,7 @@ def filter_tools_by_tags(
41
41
  filtered_tools = []
42
42
 
43
43
  for tool in tools:
44
- tool_tags = getattr(tool.annotations, "tags", []) if tool.annotations else []
44
+ tool_tags = get_tool_tags(tool)
45
45
 
46
46
  if not tool_tags:
47
47
  continue
@@ -68,9 +68,18 @@ def get_tool_tags(tool: Tool | MCPTool) -> list[str]:
68
68
  -------
69
69
  List of tags for the tool
70
70
  """
71
+ # Primary: native FastMCP meta location
72
+ if hasattr(tool, "meta") and getattr(tool, "meta"):
73
+ fastmcp_meta = tool.meta.get("_fastmcp", {})
74
+ meta_tags = fastmcp_meta.get("tags", [])
75
+ if isinstance(meta_tags, list):
76
+ return meta_tags
77
+
78
+ # Fallback: annotations.tags (for compatibility during transition)
71
79
  if tool.annotations and hasattr(tool.annotations, "tags"):
72
80
  tags = getattr(tool.annotations, "tags", [])
73
81
  return tags if isinstance(tags, list) else []
82
+
74
83
  return []
75
84
 
76
85
 
@@ -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},