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