dtlpymcp 0.1.14__py3-none-any.whl → 0.1.15__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.
- dtlpymcp/__init__.py +1 -1
- dtlpymcp/utils/dtlpy_context.py +317 -310
- {dtlpymcp-0.1.14.dist-info → dtlpymcp-0.1.15.dist-info}/METADATA +1 -1
- dtlpymcp-0.1.15.dist-info/RECORD +10 -0
- dtlpymcp-0.1.14.dist-info/RECORD +0 -10
- {dtlpymcp-0.1.14.dist-info → dtlpymcp-0.1.15.dist-info}/WHEEL +0 -0
- {dtlpymcp-0.1.14.dist-info → dtlpymcp-0.1.15.dist-info}/entry_points.txt +0 -0
- {dtlpymcp-0.1.14.dist-info → dtlpymcp-0.1.15.dist-info}/top_level.txt +0 -0
dtlpymcp/__init__.py
CHANGED
dtlpymcp/utils/dtlpy_context.py
CHANGED
|
@@ -1,310 +1,317 @@
|
|
|
1
|
-
from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase, FuncMetadata
|
|
2
|
-
from mcp.client.streamable_http import streamablehttp_client
|
|
3
|
-
from typing import List, Tuple, Callable, Optional
|
|
4
|
-
from mcp.server.fastmcp.tools.base import Tool
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
from pydantic import create_model
|
|
7
|
-
from datetime import timedelta
|
|
8
|
-
from mcp import ClientSession
|
|
9
|
-
import dtlpy as dl
|
|
10
|
-
import traceback
|
|
11
|
-
import requests
|
|
12
|
-
import logging
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.
|
|
39
|
-
self.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
self.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
self.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
filters.
|
|
55
|
-
filters.add(field="
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
logger.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
1
|
+
from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase, FuncMetadata
|
|
2
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
3
|
+
from typing import List, Tuple, Callable, Optional
|
|
4
|
+
from mcp.server.fastmcp.tools.base import Tool
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from pydantic import create_model
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from mcp import ClientSession
|
|
9
|
+
import dtlpy as dl
|
|
10
|
+
import traceback
|
|
11
|
+
import requests
|
|
12
|
+
import logging
|
|
13
|
+
import asyncio
|
|
14
|
+
import time
|
|
15
|
+
import jwt
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("dtlpymcp")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MCPSource(BaseModel):
|
|
22
|
+
dpk_name: Optional[str] = None
|
|
23
|
+
app_url: Optional[str] = None
|
|
24
|
+
dpk_version: Optional[str] = None
|
|
25
|
+
app_trusted: Optional[bool] = None
|
|
26
|
+
server_url: Optional[str] = None
|
|
27
|
+
app_jwt: Optional[str] = None
|
|
28
|
+
tools: Optional[List[Tool]] = []
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DataloopContext:
|
|
32
|
+
"""
|
|
33
|
+
DataloopContext manages authentication, tool discovery, and proxy registration for Dataloop MCP servers.
|
|
34
|
+
Handles JWTs, server URLs, and dynamic tool registration for multi-tenant environments.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, token: str = None, sources_file: str = None, env: str = 'prod'):
|
|
38
|
+
self._token = token
|
|
39
|
+
self.env = env
|
|
40
|
+
self.mcp_sources: List[MCPSource] = []
|
|
41
|
+
logger.info("DataloopContext initialized.")
|
|
42
|
+
self.sources_file = sources_file
|
|
43
|
+
self.initialized = False
|
|
44
|
+
|
|
45
|
+
async def initialize(self, force: bool = False):
|
|
46
|
+
if not self.initialized or force:
|
|
47
|
+
await self.register_sources(self.sources_file)
|
|
48
|
+
self.initialized = True
|
|
49
|
+
|
|
50
|
+
async def register_sources(self, sources_file: str = None):
|
|
51
|
+
if sources_file is None:
|
|
52
|
+
logger.info("Loading MCP sources from all system apps")
|
|
53
|
+
# load all system apps
|
|
54
|
+
filters = dl.Filters(resource='apps')
|
|
55
|
+
filters.add(field="dpkName", values="dataloop-mcp*")
|
|
56
|
+
filters.add(field="scope", values="system")
|
|
57
|
+
# IMPORTANT: Listing with `all()` cause everything to get stuck. getting only first page using `items` for now
|
|
58
|
+
apps = dl.apps.list(filters=filters).items
|
|
59
|
+
if len(apps) == 0:
|
|
60
|
+
raise ValueError(f"No app found for DPK name: dataloop-mcp*")
|
|
61
|
+
sources = []
|
|
62
|
+
for app in apps:
|
|
63
|
+
sources.append(
|
|
64
|
+
{
|
|
65
|
+
"dpk_name": app.dpk_name,
|
|
66
|
+
"dpk_version": app.dpk_version,
|
|
67
|
+
"app_url": next(iter(app.routes.values())),
|
|
68
|
+
"server_url": None,
|
|
69
|
+
"app_jwt": None,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
logger.info(f"Loading MCP sources from {sources_file}")
|
|
74
|
+
|
|
75
|
+
with open(sources_file, "r") as f:
|
|
76
|
+
sources = json.load(f)
|
|
77
|
+
|
|
78
|
+
# Load all sources in parallel for faster initialization
|
|
79
|
+
async def load_source(entry):
|
|
80
|
+
try:
|
|
81
|
+
if not isinstance(entry, dict):
|
|
82
|
+
raise ValueError(f"Invalid source entry: {entry}")
|
|
83
|
+
logger.info(f"Adding MCP source: {entry.get('dpk_name')}, url: {entry.get('server_url')}")
|
|
84
|
+
await self.add_mcp_source(MCPSource(**entry))
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Failed to add MCP source: {entry}\n{traceback.format_exc()}")
|
|
87
|
+
|
|
88
|
+
logger.info(f"Loading {len(sources)} MCP sources in parallel...")
|
|
89
|
+
await asyncio.gather(*[load_source(entry) for entry in sources])
|
|
90
|
+
logger.info(f"Finished loading {len(self.mcp_sources)} MCP sources")
|
|
91
|
+
|
|
92
|
+
async def add_mcp_source(self, mcp_source: MCPSource):
|
|
93
|
+
|
|
94
|
+
if mcp_source.server_url is None:
|
|
95
|
+
success = self.load_app_info(mcp_source)
|
|
96
|
+
if not success:
|
|
97
|
+
logger.error(f"Failed to load app info for source {mcp_source.dpk_name}")
|
|
98
|
+
return
|
|
99
|
+
result = await self.list_source_tools(mcp_source)
|
|
100
|
+
if result is None:
|
|
101
|
+
raise ValueError(f"Failed to discover tools for source {mcp_source.dpk_name}")
|
|
102
|
+
server_name, tools, call_fn = result
|
|
103
|
+
for tool in tools.tools:
|
|
104
|
+
tool_name = tool.name
|
|
105
|
+
ns_tool_name = f"{server_name}__{tool_name}"
|
|
106
|
+
description = tool.description
|
|
107
|
+
input_schema = tool.inputSchema
|
|
108
|
+
|
|
109
|
+
# Normalize input schema to ensure it has "type": "object" at root level
|
|
110
|
+
# This is required by the MCP specification
|
|
111
|
+
input_schema = self.normalize_input_schema(input_schema)
|
|
112
|
+
|
|
113
|
+
def build_handler(tool_name):
|
|
114
|
+
async def inner(**kwargs):
|
|
115
|
+
fn = call_fn(tool_name, kwargs)
|
|
116
|
+
return await fn()
|
|
117
|
+
|
|
118
|
+
return inner
|
|
119
|
+
|
|
120
|
+
dynamic_pydantic_model_params = self.build_pydantic_fields_from_schema(input_schema)
|
|
121
|
+
arguments_model = create_model(
|
|
122
|
+
f"{tool_name}Arguments", **dynamic_pydantic_model_params, __base__=ArgModelBase
|
|
123
|
+
)
|
|
124
|
+
resp = FuncMetadata(arg_model=arguments_model)
|
|
125
|
+
t = Tool(
|
|
126
|
+
fn=build_handler(tool_name),
|
|
127
|
+
name=ns_tool_name,
|
|
128
|
+
description=description,
|
|
129
|
+
parameters=input_schema,
|
|
130
|
+
fn_metadata=resp,
|
|
131
|
+
is_async=True,
|
|
132
|
+
context_kwarg="ctx",
|
|
133
|
+
annotations=None,
|
|
134
|
+
)
|
|
135
|
+
mcp_source.tools.append(t)
|
|
136
|
+
self.mcp_sources.append(mcp_source)
|
|
137
|
+
tool_str = ", ".join([tool.name for tool in mcp_source.tools])
|
|
138
|
+
logger.info(f"Added MCP source: {mcp_source.dpk_name}, Available tools: {tool_str}")
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def token(self) -> str:
|
|
142
|
+
return self._token
|
|
143
|
+
|
|
144
|
+
@token.setter
|
|
145
|
+
def token(self, token: str):
|
|
146
|
+
self._token = token
|
|
147
|
+
|
|
148
|
+
def load_app_info(self, source: MCPSource) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Get the source URL and app JWT for a given DPK name using Dataloop SDK.
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
if source.app_url is None:
|
|
154
|
+
dl.setenv(self.env)
|
|
155
|
+
dl.client_api.token = self.token
|
|
156
|
+
filters = dl.Filters(resource='apps')
|
|
157
|
+
filters.add(field="dpkName", values=source.dpk_name)
|
|
158
|
+
filters.add(field="scope", values="system")
|
|
159
|
+
apps = list(dl.apps.list(filters=filters).all())
|
|
160
|
+
if len(apps) == 0:
|
|
161
|
+
raise ValueError(f"No app found for DPK name: {source.dpk_name}")
|
|
162
|
+
if len(apps) > 1:
|
|
163
|
+
logger.warning(f"Multiple apps found for DPK name: {source.dpk_name}, using first one")
|
|
164
|
+
app = apps[0]
|
|
165
|
+
logger.info(f"App: {app.name}")
|
|
166
|
+
source.app_url = next(iter(app.routes.values()))
|
|
167
|
+
dpk = dl.dpks.get(dpk_name=source.dpk_name, dpk_version=source.dpk_version).to_json()
|
|
168
|
+
source.app_trusted = dpk.get('trusted', False)
|
|
169
|
+
session = requests.Session()
|
|
170
|
+
response = session.get(source.app_url, headers=dl.client_api.auth)
|
|
171
|
+
logger.info(f"App route URL: {response.url}")
|
|
172
|
+
source.server_url = response.url
|
|
173
|
+
source.app_jwt = session.cookies.get("JWT-APP")
|
|
174
|
+
except Exception:
|
|
175
|
+
logger.error(f"Failed getting app info: {traceback.format_exc()}")
|
|
176
|
+
return False
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def is_expired(app_jwt: str) -> bool:
|
|
181
|
+
"""
|
|
182
|
+
Check if the APP_JWT is expired.
|
|
183
|
+
|
|
184
|
+
Note: Verification is intentionally skipped - this is only used for
|
|
185
|
+
client-side expiration checking. The server validates the JWT.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
decoded = jwt.decode(
|
|
189
|
+
app_jwt,
|
|
190
|
+
options={
|
|
191
|
+
"verify_signature": False,
|
|
192
|
+
"verify_exp": False,
|
|
193
|
+
"verify_aud": False,
|
|
194
|
+
"verify_iss": False,
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
return decoded.get("exp", 0) < time.time()
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"Error decoding JWT: {e}")
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
def get_app_jwt(self, source: MCPSource, token: str) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Get the APP_JWT from the request headers or refresh if expired.
|
|
205
|
+
"""
|
|
206
|
+
if source.app_url is None:
|
|
207
|
+
raise ValueError("App URL is missing. Please set the app URL.")
|
|
208
|
+
if source.app_jwt is None or self.is_expired(source.app_jwt):
|
|
209
|
+
try:
|
|
210
|
+
session = requests.Session()
|
|
211
|
+
response = session.get(source.app_url, headers={'authorization': 'Bearer ' + token})
|
|
212
|
+
source.app_jwt = session.cookies.get("JWT-APP")
|
|
213
|
+
except Exception:
|
|
214
|
+
raise Exception(f"Failed getting app JWT from cookies\n{traceback.format_exc()}") from None
|
|
215
|
+
if not source.app_jwt:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
"APP_JWT is missing. Please set the APP_JWT environment variable or ensure authentication is working."
|
|
218
|
+
)
|
|
219
|
+
return source.app_jwt
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def user_info(token: str) -> dict:
|
|
223
|
+
"""
|
|
224
|
+
Decode a JWT token and return user info.
|
|
225
|
+
|
|
226
|
+
Note: Verification is intentionally skipped - this is only used for
|
|
227
|
+
reading claims client-side. The server validates the JWT.
|
|
228
|
+
"""
|
|
229
|
+
return jwt.decode(
|
|
230
|
+
token,
|
|
231
|
+
options={
|
|
232
|
+
"verify_signature": False,
|
|
233
|
+
"verify_exp": False,
|
|
234
|
+
"verify_aud": False,
|
|
235
|
+
"verify_iss": False,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
async def list_source_tools(self, source: MCPSource) -> Tuple[str, List[dict], Callable]:
|
|
240
|
+
"""
|
|
241
|
+
Discover tools for a given source and return (server_name, list_of_tools, call_fn).
|
|
242
|
+
"""
|
|
243
|
+
if source.server_url is None:
|
|
244
|
+
logger.error("DataloopContext required for DPK servers")
|
|
245
|
+
raise ValueError("DataloopContext required for DPK servers")
|
|
246
|
+
if source.app_trusted:
|
|
247
|
+
headers = {"authorization": f"Bearer {self.token}", "x-dl-info": f"{self.token}"}
|
|
248
|
+
else:
|
|
249
|
+
headers = {"Cookie": f"JWT-APP={source.app_jwt}", "x-dl-info": f"{self.token}"}
|
|
250
|
+
async with streamablehttp_client(source.server_url, headers=headers) as (read, write, _):
|
|
251
|
+
async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as session:
|
|
252
|
+
await session.initialize()
|
|
253
|
+
tools = await session.list_tools()
|
|
254
|
+
|
|
255
|
+
def call_fn(tool_name, kwargs):
|
|
256
|
+
async def inner():
|
|
257
|
+
async with streamablehttp_client(source.server_url, headers=headers) as (read, write, _):
|
|
258
|
+
async with ClientSession(
|
|
259
|
+
read, write, read_timeout_seconds=timedelta(seconds=60)
|
|
260
|
+
) as session:
|
|
261
|
+
await session.initialize()
|
|
262
|
+
return await session.call_tool(tool_name, kwargs)
|
|
263
|
+
|
|
264
|
+
return inner
|
|
265
|
+
|
|
266
|
+
logger.info(f"Discovered {len(tools.tools)} tools for source {source.dpk_name}")
|
|
267
|
+
return (source.dpk_name, tools, call_fn)
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def normalize_input_schema(input_schema: dict) -> dict:
|
|
271
|
+
"""
|
|
272
|
+
Normalize input schema to ensure it conforms to MCP specification.
|
|
273
|
+
The schema must have "type": "object" at the root level.
|
|
274
|
+
"""
|
|
275
|
+
if not isinstance(input_schema, dict):
|
|
276
|
+
return {"type": "object", "properties": {}, "required": []}
|
|
277
|
+
|
|
278
|
+
# Create a copy to avoid mutating the original
|
|
279
|
+
normalized = input_schema.copy()
|
|
280
|
+
|
|
281
|
+
# Ensure type is "object" at root level
|
|
282
|
+
if "type" not in normalized:
|
|
283
|
+
normalized["type"] = "object"
|
|
284
|
+
elif normalized.get("type") != "object":
|
|
285
|
+
# If type exists but isn't "object", log a warning and fix it
|
|
286
|
+
logger.warning(f"Input schema has type '{normalized.get('type')}' instead of 'object', fixing...")
|
|
287
|
+
normalized["type"] = "object"
|
|
288
|
+
|
|
289
|
+
# Ensure properties exist
|
|
290
|
+
if "properties" not in normalized:
|
|
291
|
+
normalized["properties"] = {}
|
|
292
|
+
|
|
293
|
+
# Ensure required exists (can be empty list)
|
|
294
|
+
if "required" not in normalized:
|
|
295
|
+
normalized["required"] = []
|
|
296
|
+
|
|
297
|
+
return normalized
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def openapi_type_to_python(type_str):
|
|
301
|
+
return {"string": str, "integer": int, "number": float, "boolean": bool, "array": list, "object": dict}.get(
|
|
302
|
+
type_str, str
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def build_pydantic_fields_from_schema(input_schema):
|
|
307
|
+
required = set(input_schema.get("required", []))
|
|
308
|
+
properties = input_schema.get("properties", {})
|
|
309
|
+
fields = {}
|
|
310
|
+
for name, prop in properties.items():
|
|
311
|
+
py_type = DataloopContext.openapi_type_to_python(prop.get("type", "string"))
|
|
312
|
+
if name in required:
|
|
313
|
+
fields[name] = (py_type, Field(...))
|
|
314
|
+
else:
|
|
315
|
+
default = prop.get("default", None)
|
|
316
|
+
fields[name] = (py_type, Field(default=default))
|
|
317
|
+
return fields
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
dtlpymcp/__init__.py,sha256=Bmm-sygV4K2FTGiK00VKf3Ywgis10Me1EetnyktVRFs,87
|
|
2
|
+
dtlpymcp/__main__.py,sha256=ZsXN8guga8Qo-94bSvgC6u9s5gmmdyppUijb-_bCxCw,1347
|
|
3
|
+
dtlpymcp/min_proxy.py,sha256=QYGoqclGNN7ZMjoLzWBLCxUm4yT2xCoLTbtdFRsqlEk,2572
|
|
4
|
+
dtlpymcp/proxy.py,sha256=IzZrK6s37sXM-AM2L2m_K9AveT7uX_VSxwgrwggJYV4,6213
|
|
5
|
+
dtlpymcp/utils/dtlpy_context.py,sha256=5kk4MXaDyoQRlme-crcq8beZi-leVPrhKLs6IEsc_xM,13130
|
|
6
|
+
dtlpymcp-0.1.15.dist-info/METADATA,sha256=TqcPikUvF-4eO5CPUGOBEEq6VuqTBbLXkLsyz_8Wdbk,2190
|
|
7
|
+
dtlpymcp-0.1.15.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
+
dtlpymcp-0.1.15.dist-info/entry_points.txt,sha256=6hRVZNTjQevj7erwt9dAOURtPVrSrYu6uHXhAlhTaXQ,52
|
|
9
|
+
dtlpymcp-0.1.15.dist-info/top_level.txt,sha256=z85v20pIEnY3cBaWgwhU3EZS4WAZRywejhIutwd-iHk,9
|
|
10
|
+
dtlpymcp-0.1.15.dist-info/RECORD,,
|
dtlpymcp-0.1.14.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
dtlpymcp/__init__.py,sha256=a7kR_WAe-QHnrVm2KbvZGUBK4A9RVQ5t9mOuYc2UxjI,87
|
|
2
|
-
dtlpymcp/__main__.py,sha256=ZsXN8guga8Qo-94bSvgC6u9s5gmmdyppUijb-_bCxCw,1347
|
|
3
|
-
dtlpymcp/min_proxy.py,sha256=QYGoqclGNN7ZMjoLzWBLCxUm4yT2xCoLTbtdFRsqlEk,2572
|
|
4
|
-
dtlpymcp/proxy.py,sha256=IzZrK6s37sXM-AM2L2m_K9AveT7uX_VSxwgrwggJYV4,6213
|
|
5
|
-
dtlpymcp/utils/dtlpy_context.py,sha256=LQ7jbY4UY21RwzVcwSzyhr2WRxu4GN7SSrUdVj2vRNk,13110
|
|
6
|
-
dtlpymcp-0.1.14.dist-info/METADATA,sha256=LnZLbg1eV_fuY-7zKdXS99eKn9jyuupdlnxcAfBTNqo,2190
|
|
7
|
-
dtlpymcp-0.1.14.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
-
dtlpymcp-0.1.14.dist-info/entry_points.txt,sha256=6hRVZNTjQevj7erwt9dAOURtPVrSrYu6uHXhAlhTaXQ,52
|
|
9
|
-
dtlpymcp-0.1.14.dist-info/top_level.txt,sha256=z85v20pIEnY3cBaWgwhU3EZS4WAZRywejhIutwd-iHk,9
|
|
10
|
-
dtlpymcp-0.1.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|