datarobot-genai 0.2.0__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 +250 -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 +316 -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 +128 -0
- datarobot_genai/drmcp/core/dynamic_prompts/register.py +206 -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 +542 -0
- datarobot_genai/drmcp/core/mcp_server_tools.py +129 -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 +436 -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_filter.py +108 -0
- datarobot_genai/drmcp/core/utils.py +131 -0
- datarobot_genai/drmcp/server.py +19 -0
- datarobot_genai/drmcp/test_utils/__init__.py +13 -0
- datarobot_genai/drmcp/test_utils/integration_mcp_server.py +102 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_ete.py +96 -0
- datarobot_genai/drmcp/test_utils/mcp_utils_integration.py +94 -0
- datarobot_genai/drmcp/test_utils/openai_llm_mcp_client.py +234 -0
- datarobot_genai/drmcp/test_utils/tool_base_ete.py +151 -0
- datarobot_genai/drmcp/test_utils/utils.py +91 -0
- datarobot_genai/drmcp/tools/__init__.py +14 -0
- datarobot_genai/drmcp/tools/predictive/__init__.py +27 -0
- datarobot_genai/drmcp/tools/predictive/data.py +97 -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 +72 -0
- datarobot_genai/drmcp/tools/predictive/training.py +651 -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 +258 -0
- datarobot_genai/nat/datarobot_llm_clients.py +249 -0
- datarobot_genai/nat/datarobot_llm_providers.py +130 -0
- datarobot_genai/py.typed +0 -0
- datarobot_genai-0.2.0.dist-info/METADATA +139 -0
- datarobot_genai-0.2.0.dist-info/RECORD +101 -0
- datarobot_genai-0.2.0.dist-info/WHEEL +4 -0
- datarobot_genai-0.2.0.dist-info/entry_points.txt +3 -0
- datarobot_genai-0.2.0.dist-info/licenses/AUTHORS +2 -0
- datarobot_genai-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,87 @@
|
|
|
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 logging
|
|
15
|
+
|
|
16
|
+
import datarobot as dr
|
|
17
|
+
from fastmcp.tools.tool import Tool
|
|
18
|
+
|
|
19
|
+
from datarobot_genai.drmcp.core.clients import get_api_client
|
|
20
|
+
from datarobot_genai.drmcp.core.dynamic_tools.deployment.config import create_deployment_tool_config
|
|
21
|
+
from datarobot_genai.drmcp.core.dynamic_tools.register import register_external_tool
|
|
22
|
+
from datarobot_genai.drmcp.core.exceptions import DynamicToolRegistrationError
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def register_tools_of_datarobot_deployments() -> None:
|
|
28
|
+
"""Register tools for all deployments tagged as 'tool' in DataRobot."""
|
|
29
|
+
deployment_ids = get_datarobot_tool_deployments()
|
|
30
|
+
logger.info(f"Found {len(deployment_ids)} tool deployments.")
|
|
31
|
+
|
|
32
|
+
# Try to register each tool deployment, continue on failure
|
|
33
|
+
for deployment_id in deployment_ids:
|
|
34
|
+
try:
|
|
35
|
+
deployment = dr.Deployment.get(deployment_id)
|
|
36
|
+
await register_tool_of_datarobot_deployment(deployment)
|
|
37
|
+
except DynamicToolRegistrationError:
|
|
38
|
+
pass
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
logger.error(f"Unexpected error for deployment {deployment_id}: {exc}")
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def register_tool_of_datarobot_deployment(
|
|
45
|
+
deployment: dr.Deployment,
|
|
46
|
+
) -> Tool:
|
|
47
|
+
"""Register a single tool for a given deployment.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
deployment: The tool deployment within DataRobot.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
DynamicToolRegistrationError: If registration fails at any step.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
The registered Tool instance.
|
|
59
|
+
"""
|
|
60
|
+
logger.info(f"Found tool: id: {deployment.id}, label: {deployment.label}")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Create the configuration object with all necessary information
|
|
64
|
+
# This includes fetching metadata and assembling all deployment-specific data
|
|
65
|
+
config = create_deployment_tool_config(deployment)
|
|
66
|
+
|
|
67
|
+
# Register using generic external tool registration with the config
|
|
68
|
+
tool = await register_external_tool(config, deployment_id=deployment.id)
|
|
69
|
+
|
|
70
|
+
return tool
|
|
71
|
+
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
logger.error(f"Skipping deployment {deployment.id}. Registration failed: {exc}")
|
|
74
|
+
raise DynamicToolRegistrationError("Registration failed. Could not create tool.") from exc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_datarobot_tool_deployments() -> list[str]:
|
|
78
|
+
"""Fetch deployments from DataRobot that are tagged as 'tool'."""
|
|
79
|
+
# Replace this with dr.Deployment.list when the 3.9.0 version
|
|
80
|
+
# of datarobot python SDK is released.
|
|
81
|
+
deployments_data = dr.utils.pagination.unpaginate(
|
|
82
|
+
initial_url="deployments/",
|
|
83
|
+
initial_params={"tag_values": "tool", "tag_keys": "tool"},
|
|
84
|
+
client=get_api_client(),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return [deployment["id"] for deployment in deployments_data]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"json": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"model": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"default": "unknown",
|
|
10
|
+
"description": "The LLM model to use. If no explicit instructions are provided leave default."
|
|
11
|
+
},
|
|
12
|
+
"messages": {
|
|
13
|
+
"type": "array",
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"properties": {
|
|
17
|
+
"role": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"enum": ["system", "user"],
|
|
20
|
+
"description": "The role of the message author. 'system' for instructions, 'user' for user messages."
|
|
21
|
+
},
|
|
22
|
+
"content": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "The content of the message"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"required": ["role", "content"]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": ["messages"],
|
|
32
|
+
"description": "Chat completions request body. When sending a request please focus on the user messages only. If there are no explicit instructions regarding `model` set it to `unknown`"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": ["json"]
|
|
36
|
+
}
|
datarobot_genai/drmcp/core/dynamic_tools/deployment/schemas/drum_prediction_fallback_schema.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"data": {
|
|
5
|
+
"type": "string",
|
|
6
|
+
"description": "CSV-formatted data for prediction. Each row represents one prediction request. Column names must match the feature names from get_deployment_features tool."
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"required": ["data"]
|
|
10
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
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 logging
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from collections.abc import Coroutine
|
|
19
|
+
from collections.abc import MutableMapping
|
|
20
|
+
from typing import Any
|
|
21
|
+
from typing import Literal
|
|
22
|
+
from urllib.parse import urljoin
|
|
23
|
+
|
|
24
|
+
import aiohttp
|
|
25
|
+
from aiohttp import ClientTimeout
|
|
26
|
+
from aiohttp_retry import ExponentialRetry
|
|
27
|
+
from aiohttp_retry import RetryClient
|
|
28
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
29
|
+
from fastmcp.tools.tool import Tool
|
|
30
|
+
from fastmcp.tools.tool import ToolResult
|
|
31
|
+
from pydantic import BaseModel
|
|
32
|
+
from pydantic import Field
|
|
33
|
+
from requests.structures import CaseInsensitiveDict # type: ignore[import-untyped]
|
|
34
|
+
|
|
35
|
+
from datarobot_genai.drmcp.core.config import get_config
|
|
36
|
+
from datarobot_genai.drmcp.core.dynamic_tools.schema import create_input_schema_pydantic_model
|
|
37
|
+
from datarobot_genai.drmcp.core.mcp_instance import register_tools
|
|
38
|
+
from datarobot_genai.drmcp.core.utils import format_response_as_tool_result
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# HTTP request retry configuration
|
|
44
|
+
REQUEST_RETRY_SLEEP = 0.1
|
|
45
|
+
REQUEST_MAX_RETRY = 5
|
|
46
|
+
REQUEST_RETRYABLE_STATUS_CODES = {429, 570, 502, 503, 504}
|
|
47
|
+
|
|
48
|
+
# HTTP connection timeouts in seconds
|
|
49
|
+
REQUEST_CONNECT_TIMEOUT = 30
|
|
50
|
+
REQUEST_TOTAL_TIMEOUT = 600
|
|
51
|
+
|
|
52
|
+
# Headers that should be forwarded from incoming requests.
|
|
53
|
+
# Keep this lower-cased for case-insensitive comparisons.
|
|
54
|
+
REQUEST_FORWARDED_HEADERS: set[str] = {
|
|
55
|
+
"x-agent-id",
|
|
56
|
+
"x-datarobot-authorization-context",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ExternalToolRegistrationConfig(BaseModel):
|
|
61
|
+
"""Configuration for registering an external HTTP API endpoint as an MCP tool.
|
|
62
|
+
|
|
63
|
+
This specification defines how to register a generic external HTTP API as a tool
|
|
64
|
+
that can be called by LLM agents through the MCP (Model Context Protocol) server.
|
|
65
|
+
The tool acts as a bridge between the LLM and any external HTTP API, automatically
|
|
66
|
+
handling request construction, retry logic, and response formatting.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
name: str = Field(..., description="Name of the tool.")
|
|
70
|
+
title: str | None = Field(None, description="Title for LLMs and users.")
|
|
71
|
+
description: str | None = Field(None, description="Description for LLMs and users.")
|
|
72
|
+
method: Literal["GET", "POST", "PATCH", "PUT", "DELETE"] = Field(
|
|
73
|
+
..., description="HTTP method to use."
|
|
74
|
+
)
|
|
75
|
+
base_url: str = Field(..., description="Base URL of the external API.")
|
|
76
|
+
endpoint: str = Field(
|
|
77
|
+
...,
|
|
78
|
+
description="URL endpoint/route for the external API, may include path params.",
|
|
79
|
+
)
|
|
80
|
+
headers: dict[str, str] | None = Field(
|
|
81
|
+
None, description="Optional static headers to include in requests."
|
|
82
|
+
)
|
|
83
|
+
input_schema: dict[str, Any] = Field(
|
|
84
|
+
..., description="Pydantic schema defining the tool's input schema."
|
|
85
|
+
)
|
|
86
|
+
tags: set[str] | None = Field(
|
|
87
|
+
None, description="Optional tags for tool categorization and filtering."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _external_tool_callable_factory(
|
|
92
|
+
spec: ExternalToolRegistrationConfig,
|
|
93
|
+
) -> Callable[[Any], Coroutine[Any, Any, ToolResult]]:
|
|
94
|
+
"""Dynamically creates an async callable that makes HTTP requests
|
|
95
|
+
based on the given spec. This callable is the execution logic of the
|
|
96
|
+
tool making the external API call.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
spec: Configuration specifying how to make the HTTP request.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
An async callable that takes validated inputs and returns a ToolResult.
|
|
104
|
+
"""
|
|
105
|
+
config = get_config()
|
|
106
|
+
|
|
107
|
+
input_model = create_input_schema_pydantic_model(
|
|
108
|
+
input_schema=spec.input_schema,
|
|
109
|
+
allow_empty=config.tool_registration_allow_empty_schema,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def call_external_tool(inputs: input_model) -> ToolResult: # type: ignore[valid-type]
|
|
113
|
+
request_input = inputs.model_dump() # type: ignore[attr-defined]
|
|
114
|
+
|
|
115
|
+
# Extract request parameters
|
|
116
|
+
path_params = request_input.get("path_params", {})
|
|
117
|
+
params = request_input.get("query_params")
|
|
118
|
+
data = request_input.get("data")
|
|
119
|
+
json = request_input.get("json")
|
|
120
|
+
headers = await get_outbound_headers(spec)
|
|
121
|
+
|
|
122
|
+
# Build full URL with path params
|
|
123
|
+
url = urljoin(spec.base_url, spec.endpoint.format(**path_params))
|
|
124
|
+
|
|
125
|
+
# Configure timeouts
|
|
126
|
+
client_timeout = ClientTimeout(
|
|
127
|
+
total=REQUEST_TOTAL_TIMEOUT,
|
|
128
|
+
connect=REQUEST_CONNECT_TIMEOUT,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Configure retry strategy with exponential backoff
|
|
132
|
+
retry_options = ExponentialRetry(
|
|
133
|
+
attempts=REQUEST_MAX_RETRY,
|
|
134
|
+
start_timeout=REQUEST_RETRY_SLEEP,
|
|
135
|
+
statuses=REQUEST_RETRYABLE_STATUS_CODES,
|
|
136
|
+
exceptions={
|
|
137
|
+
aiohttp.ClientError,
|
|
138
|
+
aiohttp.ServerTimeoutError,
|
|
139
|
+
asyncio.TimeoutError,
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Execute request with retry logic
|
|
144
|
+
async with aiohttp.ClientSession(timeout=client_timeout) as session:
|
|
145
|
+
retry_client = RetryClient(
|
|
146
|
+
client_session=session,
|
|
147
|
+
retry_options=retry_options,
|
|
148
|
+
logger=logger,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async with retry_client.request(
|
|
152
|
+
method=spec.method.upper(),
|
|
153
|
+
url=url,
|
|
154
|
+
params=params,
|
|
155
|
+
data=data,
|
|
156
|
+
json=json,
|
|
157
|
+
headers=headers,
|
|
158
|
+
) as response:
|
|
159
|
+
content = await response.read()
|
|
160
|
+
|
|
161
|
+
return format_response_as_tool_result(
|
|
162
|
+
data=content,
|
|
163
|
+
content_type=response.content_type,
|
|
164
|
+
charset=response.charset or "utf-8",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return call_external_tool
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def register_external_tool(config: ExternalToolRegistrationConfig, **kwargs: Any) -> Tool:
|
|
171
|
+
"""Create and register a generic HTTP tool in the MCP server.
|
|
172
|
+
|
|
173
|
+
This function creates a dynamic tool that makes HTTP requests to external APIs
|
|
174
|
+
and registers it with the MCP server for use by LLM agents.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
config: ExternalToolRegistrationConfig object containing all tool parameters.
|
|
178
|
+
**kwargs: Additional keyword arguments to pass to tools registration.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
The registered Tool instance with full MCP integration.
|
|
183
|
+
|
|
184
|
+
Raises
|
|
185
|
+
------
|
|
186
|
+
ValueError: If required path parameters are missing from input_schema.
|
|
187
|
+
aiohttp.ClientError: If the HTTP request fails during tool execution.
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
>>> config = ExternalToolRegistrationConfig(
|
|
191
|
+
... name="get_user",
|
|
192
|
+
... description="Fetch user by ID",
|
|
193
|
+
... base_url="https://api.example.com/v1",
|
|
194
|
+
... endpoint="users/{user_id}",
|
|
195
|
+
... method="GET",
|
|
196
|
+
... input_schema={
|
|
197
|
+
... "type": "object",
|
|
198
|
+
... "properties": {
|
|
199
|
+
... "path_params": {
|
|
200
|
+
... "type": "object",
|
|
201
|
+
... "properties": {"user_id": {"type": "string"}},
|
|
202
|
+
... "required": ["user_id"]
|
|
203
|
+
... }
|
|
204
|
+
... }
|
|
205
|
+
... },
|
|
206
|
+
... tags={"example", "user"}
|
|
207
|
+
... )
|
|
208
|
+
>>> tool = await register_external_tool(config=config)
|
|
209
|
+
|
|
210
|
+
Note:
|
|
211
|
+
The tool remains registered until explicitly removed or the server restarts.
|
|
212
|
+
"""
|
|
213
|
+
external_tool_callable = _external_tool_callable_factory(config)
|
|
214
|
+
|
|
215
|
+
registered_tool = await register_tools(
|
|
216
|
+
fn=external_tool_callable,
|
|
217
|
+
name=config.name,
|
|
218
|
+
title=config.title,
|
|
219
|
+
description=config.description,
|
|
220
|
+
tags=config.tags,
|
|
221
|
+
**kwargs,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return registered_tool
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def get_outbound_headers(spec: ExternalToolRegistrationConfig) -> dict[str, str]:
|
|
228
|
+
"""Retrieve headers to send to the external tool.
|
|
229
|
+
|
|
230
|
+
The method forwards whitelisted headers from current FastMCP
|
|
231
|
+
HTTP request, with tool specific headers and user overrides.
|
|
232
|
+
"""
|
|
233
|
+
headers = get_http_headers()
|
|
234
|
+
|
|
235
|
+
# Headers from the incoming request to be forwarded (case-insensitive, but preserve
|
|
236
|
+
# original casing)
|
|
237
|
+
forwarded_headers: dict[str, str] = {
|
|
238
|
+
key: value for key, value in headers.items() if key.lower() in REQUEST_FORWARDED_HEADERS
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Tool-specific static headers with user-overrides
|
|
242
|
+
spec_headers = spec.headers or {}
|
|
243
|
+
|
|
244
|
+
# Use CaseInsensitiveDict for merging forwarded and spec_headers,
|
|
245
|
+
# to prevent duplicates differing only by case.
|
|
246
|
+
out_headers: MutableMapping[str, str] = CaseInsensitiveDict()
|
|
247
|
+
|
|
248
|
+
# Insert spec headers first so their casing is preserved if overridden.
|
|
249
|
+
out_headers.update(spec_headers)
|
|
250
|
+
for key, value in forwarded_headers.items():
|
|
251
|
+
if key not in out_headers:
|
|
252
|
+
out_headers[key] = value
|
|
253
|
+
|
|
254
|
+
return dict(out_headers)
|