dtlpymcp 0.1.10__tar.gz → 0.1.12__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.
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/PKG-INFO +1 -1
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp/__init__.py +1 -1
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp/min_proxy.py +1 -1
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp/proxy.py +1 -1
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp/utils/dtlpy_context.py +69 -9
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp.egg-info/PKG-INFO +1 -1
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/pyproject.toml +1 -1
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/tests/test_list_platform_tools.py +3 -3
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/tests/test_run.py +1 -1
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/MANIFEST.in +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/README.md +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp/__main__.py +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp.egg-info/SOURCES.txt +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp.egg-info/dependency_links.txt +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp.egg-info/entry_points.txt +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp.egg-info/requires.txt +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/dtlpymcp.egg-info/top_level.txt +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/setup.cfg +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/tests/test_context.py +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/tests/test_custom_sources_file.py +0 -0
- {dtlpymcp-0.1.10 → dtlpymcp-0.1.12}/tests/test_proxy.py +0 -0
|
@@ -28,7 +28,7 @@ def create_dataloop_mcp_server() -> FastMCP:
|
|
|
28
28
|
log_level="DEBUG",
|
|
29
29
|
)
|
|
30
30
|
tool_name = "test"
|
|
31
|
-
input_schema = {"properties": {"ping": {"type": "string", "default": "pong"}}, "required": ["ping"]}
|
|
31
|
+
input_schema = {"type": "object", "properties": {"ping": {"type": "string", "default": "pong"}}, "required": ["ping"]}
|
|
32
32
|
# Create Dataloop context
|
|
33
33
|
dynamic_pydantic_model_params = DataloopContext.build_pydantic_fields_from_schema(input_schema)
|
|
34
34
|
arguments_model = create_model(f"{tool_name}Arguments", **dynamic_pydantic_model_params, __base__=ArgModelBase)
|
|
@@ -118,7 +118,7 @@ def create_dataloop_mcp_server(sources_file: Optional[str] = None, init_timeout:
|
|
|
118
118
|
return result
|
|
119
119
|
|
|
120
120
|
tool_name = "test"
|
|
121
|
-
input_schema = {"properties": {"ping": {"type": "string"}}, "required": ["ping"]}
|
|
121
|
+
input_schema = {"type": "object", "properties": {"ping": {"type": "string"}}, "required": ["ping"]}
|
|
122
122
|
dynamic_pydantic_model_params = DataloopContext.build_pydantic_fields_from_schema(input_schema)
|
|
123
123
|
arguments_model = create_model(f"{tool_name}Arguments", **dynamic_pydantic_model_params, __base__=ArgModelBase)
|
|
124
124
|
resp = FuncMetadata(arg_model=arguments_model)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from imp import source_from_cache
|
|
1
2
|
from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase, FuncMetadata
|
|
2
3
|
from mcp.client.streamable_http import streamablehttp_client
|
|
3
4
|
from typing import List, Tuple, Callable, Optional
|
|
@@ -20,6 +21,8 @@ logger = logging.getLogger("dtlpymcp")
|
|
|
20
21
|
class MCPSource(BaseModel):
|
|
21
22
|
dpk_name: Optional[str] = None
|
|
22
23
|
app_url: Optional[str] = None
|
|
24
|
+
dpk_version: Optional[str] = None
|
|
25
|
+
app_trusted: Optional[bool] = None
|
|
23
26
|
server_url: Optional[str] = None
|
|
24
27
|
app_jwt: Optional[str] = None
|
|
25
28
|
tools: Optional[List[Tool]] = []
|
|
@@ -60,6 +63,7 @@ class DataloopContext:
|
|
|
60
63
|
sources.append(
|
|
61
64
|
{
|
|
62
65
|
"dpk_name": app.dpk_name,
|
|
66
|
+
"dpk_version": app.dpk_version,
|
|
63
67
|
"app_url": next(iter(app.routes.values())),
|
|
64
68
|
"server_url": None,
|
|
65
69
|
"app_jwt": None,
|
|
@@ -95,6 +99,10 @@ class DataloopContext:
|
|
|
95
99
|
ns_tool_name = f"{server_name}.{tool_name}"
|
|
96
100
|
description = tool.description
|
|
97
101
|
input_schema = tool.inputSchema
|
|
102
|
+
|
|
103
|
+
# Normalize input schema to ensure it has "type": "object" at root level
|
|
104
|
+
# This is required by the MCP specification
|
|
105
|
+
input_schema = self.normalize_input_schema(input_schema)
|
|
98
106
|
|
|
99
107
|
def build_handler(tool_name):
|
|
100
108
|
async def inner(**kwargs):
|
|
@@ -150,6 +158,8 @@ class DataloopContext:
|
|
|
150
158
|
app = apps[0]
|
|
151
159
|
logger.info(f"App: {app.name}")
|
|
152
160
|
source.app_url = next(iter(app.routes.values()))
|
|
161
|
+
dpk = dl.dpks.get(dpk_name=source.dpk_name, dpk_version=source.dpk_version).to_json()
|
|
162
|
+
source.app_trusted = dpk.get('trusted', False)
|
|
153
163
|
session = requests.Session()
|
|
154
164
|
response = session.get(source.app_url, headers=dl.client_api.auth)
|
|
155
165
|
logger.info(f"App route URL: {response.url}")
|
|
@@ -164,14 +174,21 @@ class DataloopContext:
|
|
|
164
174
|
def is_expired(app_jwt: str) -> bool:
|
|
165
175
|
"""
|
|
166
176
|
Check if the APP_JWT is expired.
|
|
177
|
+
|
|
178
|
+
Note: Verification is intentionally skipped - this is only used for
|
|
179
|
+
client-side expiration checking. The server validates the JWT.
|
|
167
180
|
"""
|
|
168
181
|
try:
|
|
169
|
-
decoded = jwt.decode(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
182
|
+
decoded = jwt.decode(
|
|
183
|
+
app_jwt,
|
|
184
|
+
options={
|
|
185
|
+
"verify_signature": False,
|
|
186
|
+
"verify_exp": False,
|
|
187
|
+
"verify_aud": False,
|
|
188
|
+
"verify_iss": False,
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
return decoded.get("exp", 0) < time.time()
|
|
175
192
|
except Exception as e:
|
|
176
193
|
logger.error(f"Error decoding JWT: {e}")
|
|
177
194
|
return True
|
|
@@ -199,9 +216,19 @@ class DataloopContext:
|
|
|
199
216
|
def user_info(token: str) -> dict:
|
|
200
217
|
"""
|
|
201
218
|
Decode a JWT token and return user info.
|
|
219
|
+
|
|
220
|
+
Note: Verification is intentionally skipped - this is only used for
|
|
221
|
+
reading claims client-side. The server validates the JWT.
|
|
202
222
|
"""
|
|
203
|
-
|
|
204
|
-
|
|
223
|
+
return jwt.decode(
|
|
224
|
+
token,
|
|
225
|
+
options={
|
|
226
|
+
"verify_signature": False,
|
|
227
|
+
"verify_exp": False,
|
|
228
|
+
"verify_aud": False,
|
|
229
|
+
"verify_iss": False,
|
|
230
|
+
},
|
|
231
|
+
)
|
|
205
232
|
|
|
206
233
|
async def list_source_tools(self, source: MCPSource) -> Tuple[str, List[dict], Callable]:
|
|
207
234
|
"""
|
|
@@ -210,7 +237,10 @@ class DataloopContext:
|
|
|
210
237
|
if source.server_url is None:
|
|
211
238
|
logger.error("DataloopContext required for DPK servers")
|
|
212
239
|
raise ValueError("DataloopContext required for DPK servers")
|
|
213
|
-
|
|
240
|
+
if source.app_trusted:
|
|
241
|
+
headers = {"authorization": f"Bearer {self.token}", "x-dl-info": f"{self.token}"}
|
|
242
|
+
else:
|
|
243
|
+
headers = {"Cookie": f"JWT-APP={source.app_jwt}", "x-dl-info": f"{self.token}"}
|
|
214
244
|
async with streamablehttp_client(source.server_url, headers=headers) as (read, write, _):
|
|
215
245
|
async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as session:
|
|
216
246
|
await session.initialize()
|
|
@@ -230,6 +260,36 @@ class DataloopContext:
|
|
|
230
260
|
logger.info(f"Discovered {len(tools.tools)} tools for source {source.dpk_name}")
|
|
231
261
|
return (source.dpk_name, tools, call_fn)
|
|
232
262
|
|
|
263
|
+
@staticmethod
|
|
264
|
+
def normalize_input_schema(input_schema: dict) -> dict:
|
|
265
|
+
"""
|
|
266
|
+
Normalize input schema to ensure it conforms to MCP specification.
|
|
267
|
+
The schema must have "type": "object" at the root level.
|
|
268
|
+
"""
|
|
269
|
+
if not isinstance(input_schema, dict):
|
|
270
|
+
return {"type": "object", "properties": {}, "required": []}
|
|
271
|
+
|
|
272
|
+
# Create a copy to avoid mutating the original
|
|
273
|
+
normalized = input_schema.copy()
|
|
274
|
+
|
|
275
|
+
# Ensure type is "object" at root level
|
|
276
|
+
if "type" not in normalized:
|
|
277
|
+
normalized["type"] = "object"
|
|
278
|
+
elif normalized.get("type") != "object":
|
|
279
|
+
# If type exists but isn't "object", log a warning and fix it
|
|
280
|
+
logger.warning(f"Input schema has type '{normalized.get('type')}' instead of 'object', fixing...")
|
|
281
|
+
normalized["type"] = "object"
|
|
282
|
+
|
|
283
|
+
# Ensure properties exist
|
|
284
|
+
if "properties" not in normalized:
|
|
285
|
+
normalized["properties"] = {}
|
|
286
|
+
|
|
287
|
+
# Ensure required exists (can be empty list)
|
|
288
|
+
if "required" not in normalized:
|
|
289
|
+
normalized["required"] = []
|
|
290
|
+
|
|
291
|
+
return normalized
|
|
292
|
+
|
|
233
293
|
@staticmethod
|
|
234
294
|
def openapi_type_to_python(type_str):
|
|
235
295
|
return {"string": str, "integer": int, "number": float, "boolean": bool, "array": list, "object": dict}.get(
|
|
@@ -7,13 +7,13 @@ from mcp.client.stdio import stdio_client
|
|
|
7
7
|
from mcp import ClientSession, StdioServerParameters
|
|
8
8
|
import dtlpy as dl
|
|
9
9
|
|
|
10
|
-
dl.setenv('
|
|
10
|
+
dl.setenv('prod')
|
|
11
11
|
if dl.token_expired():
|
|
12
12
|
dl.login()
|
|
13
13
|
# Create server parameters for stdio connection
|
|
14
14
|
server_params = StdioServerParameters(
|
|
15
|
-
command="
|
|
16
|
-
args=["start"], # Command line arguments
|
|
15
|
+
command="uvx", # Executable
|
|
16
|
+
args=["dtlpymcp", "start"], # Command line arguments
|
|
17
17
|
env={"DATALOOP_API_KEY": dl.token()}, # Optional environment variables
|
|
18
18
|
cwd=os.getcwd()
|
|
19
19
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|