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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dtlpymcp
3
- Version: 0.1.10
3
+ Version: 0.1.12
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
@@ -1,3 +1,3 @@
1
1
  from .utils.dtlpy_context import DataloopContext, MCPSource
2
2
 
3
- __version__ = "0.1.10"
3
+ __version__ = "0.1.12"
@@ -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(app_jwt, options={"verify_signature": False})
170
- if decoded.get("exp") < time.time():
171
- return True
172
- return False
173
- except jwt.ExpiredSignatureError:
174
- return True
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
- decoded = jwt.decode(token, options={"verify_signature": False})
204
- return decoded
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
- headers = {"Cookie": f"JWT-APP={source.app_jwt}", "x-dl-info": f"{self.token}"}
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dtlpymcp
3
- Version: 0.1.10
3
+ Version: 0.1.12
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dtlpymcp"
7
- version = "0.1.10"
7
+ version = "0.1.12"
8
8
  description = "STDIO MCP proxy server for Dataloop platform."
9
9
  authors = [
10
10
  { name = "Your Name", email = "your.email@example.com" }
@@ -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('rc')
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="dtlpymcp", # Executable
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
  )
@@ -1,7 +1,7 @@
1
1
  from dtlpymcp.proxy import main
2
2
  import dtlpy as dl
3
3
  import os
4
- dl.setenv('rc')
4
+ dl.setenv('prod')
5
5
  if dl.token_expired():
6
6
  dl.login()
7
7
  os.environ["DATALOOP_API_KEY"] = dl.token()
File without changes
File without changes
File without changes
File without changes