dtlpymcp 0.1.7__tar.gz → 0.1.9__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.7
3
+ Version: 0.1.9
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
@@ -30,7 +30,14 @@ pip install git+<repository-url>
30
30
  You can run the proxy server via CLI:
31
31
 
32
32
  ```shell
33
+ # Basic usage
33
34
  dtlpymcp start
35
+
36
+ # With custom sources file
37
+ dtlpymcp start --sources-file /path/to/sources.json
38
+
39
+ # With custom initialization timeout (default: 30 seconds)
40
+ dtlpymcp start --init-timeout 60.0
34
41
  ```
35
42
 
36
43
  Or using Python module syntax:
@@ -45,6 +52,14 @@ python -m dtlpymcp start
45
52
  - Install dependencies with `pip install -e .`
46
53
  - Run tests with `pytest`
47
54
 
55
+ ## Architecture
56
+
57
+ The server uses a modular architecture with utilities for safe async initialization:
58
+
59
+ - `dtlpymcp/proxy.py` - Main server implementation using FastMCP
60
+ - `dtlpymcp/utils/server_utils.py` - Safe async initialization utilities
61
+ - `dtlpymcp/utils/dtlpy_context.py` - Dataloop context management
62
+
48
63
  ## Cursor MCP Integration
49
64
 
50
65
  To add this MCP to Cursor, add the following to your configuration:
@@ -13,7 +13,14 @@ pip install git+<repository-url>
13
13
  You can run the proxy server via CLI:
14
14
 
15
15
  ```shell
16
+ # Basic usage
16
17
  dtlpymcp start
18
+
19
+ # With custom sources file
20
+ dtlpymcp start --sources-file /path/to/sources.json
21
+
22
+ # With custom initialization timeout (default: 30 seconds)
23
+ dtlpymcp start --init-timeout 60.0
17
24
  ```
18
25
 
19
26
  Or using Python module syntax:
@@ -28,6 +35,14 @@ python -m dtlpymcp start
28
35
  - Install dependencies with `pip install -e .`
29
36
  - Run tests with `pytest`
30
37
 
38
+ ## Architecture
39
+
40
+ The server uses a modular architecture with utilities for safe async initialization:
41
+
42
+ - `dtlpymcp/proxy.py` - Main server implementation using FastMCP
43
+ - `dtlpymcp/utils/server_utils.py` - Safe async initialization utilities
44
+ - `dtlpymcp/utils/dtlpy_context.py` - Dataloop context management
45
+
31
46
  ## Cursor MCP Integration
32
47
 
33
48
  To add this MCP to Cursor, add the following to your configuration:
@@ -0,0 +1,34 @@
1
+ from .utils.dtlpy_context import DataloopContext, MCPSource
2
+ import logging
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+
6
+ __version__ = "0.1.9"
7
+
8
+
9
+ # Setup logging directory and file
10
+ log_dir = Path.home() / ".dataloop" / "mcplogs"
11
+ log_dir.mkdir(parents=True, exist_ok=True)
12
+ log_file = log_dir / f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log"
13
+
14
+ # Remove any existing handlers
15
+ for handler in logging.root.handlers[:]:
16
+ logging.root.removeHandler(handler)
17
+
18
+ # File handler with timestamp
19
+ file_handler = logging.FileHandler(log_file, mode="a", encoding="utf-8")
20
+ file_handler.setFormatter(
21
+ logging.Formatter(fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
22
+ )
23
+
24
+ # Console handler (default format)
25
+ console_handler = logging.StreamHandler()
26
+ console_handler.setFormatter(logging.Formatter(fmt="[%(levelname)s] %(name)s: %(message)s"))
27
+
28
+ # Configure root logger
29
+ logging.basicConfig(level=logging.DEBUG, handlers=[file_handler, console_handler])
30
+
31
+ # Get the main logger
32
+ logger = logging.getLogger("dtlpymcp")
33
+ logger.info(f"Logging configured with level: DEBUG")
34
+ logger.info(f"Log file: {log_file}")
@@ -2,33 +2,37 @@
2
2
  CLI entry point for dtlpymcp.
3
3
  Reads from STDIN and writes to STDOUT.
4
4
  """
5
+
5
6
  import sys
6
7
  import argparse
7
8
  from dtlpymcp.proxy import main as proxy_main
8
9
 
10
+
9
11
  def main():
10
- parser = argparse.ArgumentParser(
11
- description="Dataloop MCP Proxy Server CLI"
12
- )
12
+ parser = argparse.ArgumentParser(description="Dataloop MCP Proxy Server CLI")
13
13
  subparsers = parser.add_subparsers(dest="command", required=False)
14
14
 
15
15
  # 'start' subcommand
16
16
  start_parser = subparsers.add_parser("start", help="Start the MCP proxy server (STDIO mode)")
17
17
  start_parser.add_argument(
18
- "--sources-file",
19
- "-s",
20
- type=str,
21
- default=None,
22
- help="Path to a JSON file with MCP sources to load"
18
+ "--sources-file", "-s", type=str, default=None, help="Path to a JSON file with MCP sources to load"
19
+ )
20
+ start_parser.add_argument(
21
+ "--init-timeout",
22
+ "-t",
23
+ type=float,
24
+ default=30.0,
25
+ help="Timeout in seconds for Dataloop context initialization (default: 30.0)",
23
26
  )
24
27
 
25
28
  args = parser.parse_args()
26
29
 
27
30
  if args.command == "start":
28
- sys.exit(proxy_main(sources_file=args.sources_file))
31
+ sys.exit(proxy_main(sources_file=args.sources_file, init_timeout=args.init_timeout))
29
32
  else:
30
33
  parser.print_help()
31
34
  return 1
32
35
 
36
+
33
37
  if __name__ == "__main__":
34
- main()
38
+ main()
@@ -0,0 +1,75 @@
1
+ from mcp.server.fastmcp import FastMCP, Context
2
+ from typing import Any
3
+ import traceback
4
+ import os
5
+ import logging
6
+ from mcp.server.fastmcp.tools.base import Tool, FuncMetadata
7
+ from pydantic import create_model
8
+ from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase
9
+ from dtlpymcp.utils.dtlpy_context import DataloopContext
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def create_dataloop_mcp_server() -> FastMCP:
15
+ """Create a FastMCP server for Dataloop with Bearer token authentication."""
16
+
17
+ async def test(ctx: Context, ping: Any = None) -> dict[str, Any]:
18
+ """Health check tool. Returns status ok and echoes ping if provided."""
19
+ result = {"status": "ok"}
20
+ if ping is not None:
21
+ result["ping"] = ping
22
+ return result
23
+
24
+ app = FastMCP(
25
+ name="Dataloop MCP Server",
26
+ instructions="A multi-tenant MCP server for Dataloop with authentication",
27
+ debug=True,
28
+ log_level="DEBUG",
29
+ )
30
+ tool_name = "test"
31
+ input_schema = {"properties": {"ping": {"type": "string", "default": "pong"}}, "required": ["ping"]}
32
+ # Create Dataloop context
33
+ dynamic_pydantic_model_params = DataloopContext.build_pydantic_fields_from_schema(input_schema)
34
+ arguments_model = create_model(f"{tool_name}Arguments", **dynamic_pydantic_model_params, __base__=ArgModelBase)
35
+ resp = FuncMetadata(arg_model=arguments_model)
36
+
37
+ app._tool_manager._tools[tool_name] = Tool(
38
+ fn=test,
39
+ name=tool_name,
40
+ description="Test tool for health checks",
41
+ parameters=input_schema,
42
+ is_async=True,
43
+ context_kwarg="ctx",
44
+ fn_metadata=resp,
45
+ annotations=None,
46
+ )
47
+
48
+ return app
49
+
50
+
51
+ def main() -> int:
52
+ logger.info("Starting Dataloop MCP server in stdio mode")
53
+
54
+ # Validate environment variables
55
+ if not os.environ.get('DATALOOP_API_KEY'):
56
+ logger.error("DATALOOP_API_KEY environment variable is required")
57
+ return 1
58
+
59
+ try:
60
+ mcp_server = create_dataloop_mcp_server()
61
+ logger.info("Dataloop MCP server created successfully")
62
+ logger.info("Starting server in stdio mode...")
63
+ mcp_server.run(transport="stdio")
64
+ return 0
65
+ except KeyboardInterrupt:
66
+ logger.info("Server stopped by user")
67
+ return 0
68
+ except Exception as e:
69
+ logger.error(f"Failed to start MCP server: {e}")
70
+ logger.error(f"Traceback: {traceback.format_exc()}")
71
+ return 1
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
@@ -0,0 +1,129 @@
1
+ from mcp.server.fastmcp import FastMCP, Context
2
+ from mcp.server.fastmcp.tools.base import Tool, FuncMetadata
3
+ from typing import Any, Optional, List
4
+ import traceback
5
+ import os
6
+ import asyncio
7
+ import logging
8
+ from pydantic import create_model
9
+ from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase
10
+
11
+ from dtlpymcp.utils.dtlpy_context import DataloopContext
12
+
13
+ logger = logging.getLogger("dtlpymcp")
14
+
15
+
16
+ def run_async(coro):
17
+ try:
18
+ loop = asyncio.get_running_loop()
19
+ except RuntimeError:
20
+ # No event loop running
21
+ return asyncio.run(coro)
22
+ else:
23
+ # Already running event loop
24
+ return loop.create_task(coro)
25
+
26
+
27
+ async def initialize_dataloop_context(sources_file: Optional[str] = None, init_timeout: float = 30.0) -> List[Tool]:
28
+ """
29
+ Initialize Dataloop context with timeout protection.
30
+
31
+ Args:
32
+ dl_context: The DataloopContext instance to initialize
33
+ app: The FastMCP app instance to register tools with
34
+ init_timeout: Timeout in seconds for initialization
35
+
36
+ Returns:
37
+ List[Tool]: List of tools
38
+ """
39
+ try:
40
+ tools = []
41
+ dl_context = DataloopContext(
42
+ token=os.environ.get('DATALOOP_API_KEY'),
43
+ env=os.environ.get('DATALOOP_ENV', 'prod'),
44
+ sources_file=sources_file,
45
+ )
46
+ logger.info("Initializing Dataloop context...")
47
+ await dl_context.initialize()
48
+ logger.info("Dataloop context initialized successfully")
49
+
50
+ logger.info(f"Adding tools from {len(dl_context.mcp_sources)} sources")
51
+ for source in dl_context.mcp_sources:
52
+ logger.info(f"Adding tools from source: {source.dpk_name}")
53
+ for tool in source.tools:
54
+ tools.append(tool)
55
+ logger.info(f"Registered tool: {tool.name}")
56
+
57
+ return tools
58
+
59
+ except asyncio.TimeoutError:
60
+ logger.error("Timeout during Dataloop context initialization")
61
+ return []
62
+ except Exception as e:
63
+ logger.error(f"Failed to initialize Dataloop context: {e}")
64
+ return []
65
+
66
+
67
+ def create_dataloop_mcp_server(sources_file: Optional[str] = None, init_timeout: float = 30.0) -> FastMCP:
68
+ """Create a FastMCP server for Dataloop with Bearer token authentication."""
69
+
70
+ async def test(ctx: Context, ping: Any = None) -> dict[str, Any]:
71
+ """Health check tool. Returns status ok and echoes ping if provided."""
72
+ result = {"status": "ok"}
73
+ if ping is not None:
74
+ result["ping"] = ping
75
+ return result
76
+
77
+ tool_name = "test"
78
+ input_schema = {"properties": {"ping": {"type": "string"}}, "required": ["ping"]}
79
+ dynamic_pydantic_model_params = DataloopContext.build_pydantic_fields_from_schema(input_schema)
80
+ arguments_model = create_model(f"{tool_name}Arguments", **dynamic_pydantic_model_params, __base__=ArgModelBase)
81
+ resp = FuncMetadata(arg_model=arguments_model)
82
+ t = Tool(
83
+ fn=test,
84
+ name=tool_name,
85
+ description="Test tool for health checks",
86
+ parameters=input_schema,
87
+ is_async=True,
88
+ context_kwarg="ctx",
89
+ fn_metadata=resp,
90
+ annotations=None,
91
+ )
92
+ tools = [t]
93
+ tools.extend(run_async(initialize_dataloop_context(sources_file=sources_file, init_timeout=init_timeout)))
94
+ app = FastMCP(
95
+ name="Dataloop MCP Server",
96
+ instructions="A multi-tenant MCP server for Dataloop with authentication",
97
+ debug=True,
98
+ tools=tools,
99
+ )
100
+
101
+ return app
102
+
103
+
104
+ def main(sources_file: Optional[str] = None, init_timeout: float = 30.0) -> int:
105
+
106
+ logger.info("Starting Dataloop MCP server in stdio mode")
107
+
108
+ # Validate environment variables
109
+ if not os.environ.get('DATALOOP_API_KEY'):
110
+ logger.error("DATALOOP_API_KEY environment variable is required")
111
+ return 1
112
+
113
+ try:
114
+ mcp_server = create_dataloop_mcp_server(sources_file=sources_file, init_timeout=init_timeout)
115
+ logger.info("Dataloop MCP server created successfully")
116
+ logger.info("Starting server in stdio mode...")
117
+ mcp_server.run(transport="stdio")
118
+ return 0
119
+ except KeyboardInterrupt:
120
+ logger.info("Server stopped by user")
121
+ return 0
122
+ except Exception as e:
123
+ logger.error(f"Failed to start MCP server: {e}")
124
+ logger.error(f"Traceback: {traceback.format_exc()}")
125
+ return 1
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
@@ -1,8 +1,7 @@
1
1
  from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase, FuncMetadata
2
2
  from mcp.client.streamable_http import streamablehttp_client
3
- from typing import Any, List, Tuple, Callable, Optional
3
+ from typing import List, Tuple, Callable, Optional
4
4
  from mcp.server.fastmcp.tools.base import Tool
5
- from mcp.server.fastmcp import FastMCP
6
5
  from pydantic import BaseModel, Field
7
6
  from pydantic import create_model
8
7
  from datetime import timedelta
@@ -11,27 +10,11 @@ import dtlpy as dl
11
10
  import traceback
12
11
  import requests
13
12
  import logging
14
- import asyncio
15
13
  import time
16
14
  import jwt
17
15
  import json
18
- from dtlpymcp import SOURCES_FILEPATH
19
16
 
20
- # Utility to run async code from sync or async context
21
- # If called from a running event loop, returns a Task (caller must handle it)
22
- # If called from sync, blocks and returns result
23
-
24
- def run_async(coro):
25
- try:
26
- loop = asyncio.get_running_loop()
27
- except RuntimeError:
28
- # No event loop running
29
- return asyncio.run(coro)
30
- else:
31
- # Already running event loop
32
- return loop.create_task(coro)
33
-
34
- logger = logging.getLogger(__name__)
17
+ logger = logging.getLogger("dtlpymcp")
35
18
 
36
19
 
37
20
  class MCPSource(BaseModel):
@@ -53,22 +36,41 @@ class DataloopContext:
53
36
  self.env = env
54
37
  self.mcp_sources: List[MCPSource] = []
55
38
  logger.info("DataloopContext initialized.")
56
- if sources_file is None:
57
- sources_file = SOURCES_FILEPATH
58
39
  self.sources_file = sources_file
59
40
  self.initialized = False
60
-
61
-
41
+
62
42
  async def initialize(self, force: bool = False):
63
- if not self.initialized or force:
43
+ if not self.initialized or force:
64
44
  await self.register_sources(self.sources_file)
65
45
  self.initialized = True
66
46
 
67
- async def register_sources(self, sources_file: str):
68
- with open(sources_file, "r") as f:
69
- data = json.load(f)
70
- logger.info(f"Loading MCP sources from {sources_file}")
71
- for entry in data:
47
+ async def register_sources(self, sources_file: str = None):
48
+ if sources_file is None:
49
+ logger.info("Loading MCP sources from all system apps")
50
+ # load all system apps
51
+ filters = dl.Filters(resource='apps')
52
+ filters.add(field="dpkName", values="dataloop-mcp*")
53
+ filters.add(field="scope", values="system")
54
+ # IMPORTANT: Listing with `all()` cause everything to get stuck. getting only first page using `items` for now
55
+ apps = dl.apps.list(filters=filters).items
56
+ if len(apps) == 0:
57
+ raise ValueError(f"No app found for DPK name: dataloop-mcp*")
58
+ sources = []
59
+ for app in apps:
60
+ sources.append(
61
+ {
62
+ "dpk_name": app.dpk_name,
63
+ "app_url": next(iter(app.routes.values())),
64
+ "server_url": None,
65
+ "app_jwt": None,
66
+ }
67
+ )
68
+ else:
69
+ logger.info(f"Loading MCP sources from {sources_file}")
70
+
71
+ with open(sources_file, "r") as f:
72
+ sources = json.load(f)
73
+ for entry in sources:
72
74
  try:
73
75
  if not isinstance(entry, dict):
74
76
  raise ValueError(f"Invalid source entry: {entry}")
@@ -78,9 +80,12 @@ class DataloopContext:
78
80
  logger.error(f"Failed to add MCP source: {entry}\n{traceback.format_exc()}")
79
81
 
80
82
  async def add_mcp_source(self, mcp_source: MCPSource):
81
- self.mcp_sources.append(mcp_source)
83
+
82
84
  if mcp_source.server_url is None:
83
- self.load_app_info(mcp_source)
85
+ success = self.load_app_info(mcp_source)
86
+ if not success:
87
+ logger.error(f"Failed to load app info for source {mcp_source.dpk_name}")
88
+ return
84
89
  result = await self.list_source_tools(mcp_source)
85
90
  if result is None:
86
91
  raise ValueError(f"Failed to discover tools for source {mcp_source.dpk_name}")
@@ -114,6 +119,7 @@ class DataloopContext:
114
119
  annotations=None,
115
120
  )
116
121
  mcp_source.tools.append(t)
122
+ self.mcp_sources.append(mcp_source)
117
123
  tool_str = ", ".join([tool.name for tool in mcp_source.tools])
118
124
  logger.info(f"Added MCP source: {mcp_source.dpk_name}, Available tools: {tool_str}")
119
125
 
@@ -125,32 +131,34 @@ class DataloopContext:
125
131
  def token(self, token: str):
126
132
  self._token = token
127
133
 
128
- def load_app_info(self, source: MCPSource) -> None:
134
+ def load_app_info(self, source: MCPSource) -> bool:
129
135
  """
130
136
  Get the source URL and app JWT for a given DPK name using Dataloop SDK.
131
137
  """
132
138
  try:
133
- dl.setenv(self.env)
134
- dl.client_api.token = self.token
135
- filters = dl.Filters(resource='apps')
136
- filters.add(field="dpkName", values=source.dpk_name)
137
- filters.add(field="scope", values="system")
138
- apps = list(dl.apps.list(filters=filters).all())
139
- if len(apps) == 0:
140
- raise ValueError(f"No app found for DPK name: {source.dpk_name}")
141
- if len(apps) > 1:
142
- logger.warning(f"Multiple apps found for DPK name: {source.dpk_name}, using first one")
143
- app = apps[0]
144
- logger.info(f"App: {app.name}")
145
- source.app_url = next(iter(app.routes.values()))
139
+ if source.app_url is None:
140
+ dl.setenv(self.env)
141
+ dl.client_api.token = self.token
142
+ filters = dl.Filters(resource='apps')
143
+ filters.add(field="dpkName", values=source.dpk_name)
144
+ filters.add(field="scope", values="system")
145
+ apps = list(dl.apps.list(filters=filters).all())
146
+ if len(apps) == 0:
147
+ raise ValueError(f"No app found for DPK name: {source.dpk_name}")
148
+ if len(apps) > 1:
149
+ logger.warning(f"Multiple apps found for DPK name: {source.dpk_name}, using first one")
150
+ app = apps[0]
151
+ logger.info(f"App: {app.name}")
152
+ source.app_url = next(iter(app.routes.values()))
146
153
  session = requests.Session()
147
154
  response = session.get(source.app_url, headers=dl.client_api.auth)
148
155
  logger.info(f"App route URL: {response.url}")
149
156
  source.server_url = response.url
150
157
  source.app_jwt = session.cookies.get("JWT-APP")
151
- except Exception as e:
158
+ except Exception:
152
159
  logger.error(f"Failed getting app info: {traceback.format_exc()}")
153
- raise Exception(f"Failed getting app info: {traceback.format_exc()}") from e
160
+ return False
161
+ return True
154
162
 
155
163
  @staticmethod
156
164
  def is_expired(app_jwt: str) -> bool:
@@ -172,18 +180,16 @@ class DataloopContext:
172
180
  """
173
181
  Get the APP_JWT from the request headers or refresh if expired.
174
182
  """
183
+ if source.app_url is None:
184
+ raise ValueError("App URL is missing. Please set the app URL.")
175
185
  if source.app_jwt is None or self.is_expired(source.app_jwt):
176
186
  try:
177
187
  session = requests.Session()
178
188
  response = session.get(source.app_url, headers={'authorization': 'Bearer ' + token})
179
189
  source.app_jwt = session.cookies.get("JWT-APP")
180
190
  except Exception:
181
- logger.error(f"Failed getting app JWT from cookies\n{traceback.format_exc()}")
182
191
  raise Exception(f"Failed getting app JWT from cookies\n{traceback.format_exc()}") from None
183
192
  if not source.app_jwt:
184
- logger.error(
185
- "APP_JWT is missing. Please set the APP_JWT environment variable or ensure authentication is working."
186
- )
187
193
  raise ValueError(
188
194
  "APP_JWT is missing. Please set the APP_JWT environment variable or ensure authentication is working."
189
195
  )
@@ -204,8 +210,7 @@ class DataloopContext:
204
210
  if source.server_url is None:
205
211
  logger.error("DataloopContext required for DPK servers")
206
212
  raise ValueError("DataloopContext required for DPK servers")
207
- headers = {"Cookie": f"JWT-APP={source.app_jwt}",
208
- "x-dl-info": f"{self.token}"}
213
+ headers = {"Cookie": f"JWT-APP={source.app_jwt}", "x-dl-info": f"{self.token}"}
209
214
  async with streamablehttp_client(source.server_url, headers=headers) as (read, write, _):
210
215
  async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as session:
211
216
  await session.initialize()
@@ -225,17 +230,19 @@ class DataloopContext:
225
230
  logger.info(f"Discovered {len(tools.tools)} tools for source {source.dpk_name}")
226
231
  return (source.dpk_name, tools, call_fn)
227
232
 
228
- def openapi_type_to_python(self, type_str):
233
+ @staticmethod
234
+ def openapi_type_to_python(type_str):
229
235
  return {"string": str, "integer": int, "number": float, "boolean": bool, "array": list, "object": dict}.get(
230
236
  type_str, str
231
237
  )
232
238
 
233
- def build_pydantic_fields_from_schema(self, input_schema):
239
+ @staticmethod
240
+ def build_pydantic_fields_from_schema(input_schema):
234
241
  required = set(input_schema.get("required", []))
235
242
  properties = input_schema.get("properties", {})
236
243
  fields = {}
237
244
  for name, prop in properties.items():
238
- py_type = self.openapi_type_to_python(prop.get("type", "string"))
245
+ py_type = DataloopContext.openapi_type_to_python(prop.get("type", "string"))
239
246
  if name in required:
240
247
  fields[name] = (py_type, Field(...))
241
248
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dtlpymcp
3
- Version: 0.1.7
3
+ Version: 0.1.9
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
@@ -30,7 +30,14 @@ pip install git+<repository-url>
30
30
  You can run the proxy server via CLI:
31
31
 
32
32
  ```shell
33
+ # Basic usage
33
34
  dtlpymcp start
35
+
36
+ # With custom sources file
37
+ dtlpymcp start --sources-file /path/to/sources.json
38
+
39
+ # With custom initialization timeout (default: 30 seconds)
40
+ dtlpymcp start --init-timeout 60.0
34
41
  ```
35
42
 
36
43
  Or using Python module syntax:
@@ -45,6 +52,14 @@ python -m dtlpymcp start
45
52
  - Install dependencies with `pip install -e .`
46
53
  - Run tests with `pytest`
47
54
 
55
+ ## Architecture
56
+
57
+ The server uses a modular architecture with utilities for safe async initialization:
58
+
59
+ - `dtlpymcp/proxy.py` - Main server implementation using FastMCP
60
+ - `dtlpymcp/utils/server_utils.py` - Safe async initialization utilities
61
+ - `dtlpymcp/utils/dtlpy_context.py` - Dataloop context management
62
+
48
63
  ## Cursor MCP Integration
49
64
 
50
65
  To add this MCP to Cursor, add the following to your configuration:
@@ -3,7 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  dtlpymcp/__init__.py
5
5
  dtlpymcp/__main__.py
6
- dtlpymcp/default_sources.json
6
+ dtlpymcp/min_proxy.py
7
7
  dtlpymcp/proxy.py
8
8
  dtlpymcp.egg-info/PKG-INFO
9
9
  dtlpymcp.egg-info/SOURCES.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dtlpymcp"
7
- version = "0.1.7"
7
+ version = "0.1.9"
8
8
  description = "STDIO MCP proxy server for Dataloop platform."
9
9
  authors = [
10
10
  { name = "Your Name", email = "your.email@example.com" }
@@ -1,32 +1,34 @@
1
1
  import asyncio
2
2
  import random
3
3
  import json
4
+ import sys
4
5
  import os
5
6
  from datetime import timedelta
6
7
  from mcp.client.stdio import stdio_client
7
8
  from mcp import ClientSession, StdioServerParameters
8
9
  import dtlpy as dl
9
10
 
10
- dl.setenv('rc')
11
+ dl.setenv('prod')
11
12
  if dl.token_expired():
12
13
  dl.login()
13
14
  token = dl.token()
14
15
  env = {"DATALOOP_API_KEY": str(token)} if token else None
15
16
  # Create server parameters for stdio connection
16
17
  server_params = StdioServerParameters(
17
- command="dtlpymcp", # Executable
18
- args=["start"], # Command line arguments
18
+ command=sys.executable, # Use current Python interpreter
19
+ args=["-m", "dtlpymcp.min_proxy"], # Run as module with start command
19
20
  env=env, # Optional environment variables
20
21
  cwd=os.getcwd(),
21
22
  )
22
23
 
23
24
 
25
+
24
26
  async def test_health_check():
25
27
  print("[TEST CLIENT] Connecting to MCP server and calling test tool...")
26
28
  async with stdio_client(server=server_params) as (read, write):
27
29
  async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as session:
28
30
  await session.initialize()
29
- num = random.randint(1, 1000000)
31
+ num = str(random.randint(1, 1000000))
30
32
  tool_result = await session.call_tool("test", {"ping": num})
31
33
  print("[RESULT]", tool_result)
32
34
  assert json.loads(tool_result.content[0].text).get("status") == "ok", "Health check failed!"
@@ -34,6 +36,6 @@ async def test_health_check():
34
36
 
35
37
 
36
38
  if __name__ == "__main__":
39
+
37
40
 
38
41
  asyncio.run(test_health_check())
39
- # asyncio.run(test_ask_dataloop())
@@ -1,7 +0,0 @@
1
- import os
2
- SOURCES_FILEPATH = os.path.join(os.path.dirname(__file__), "default_sources.json")
3
-
4
- from .utils.dtlpy_context import DataloopContext, MCPSource
5
-
6
- __version__ = "0.1.7"
7
-
@@ -1,32 +0,0 @@
1
- [
2
- {
3
- "dpk_name": "dataloop-mcp-donna",
4
- "app_url": null,
5
- "server_url": null,
6
- "app_jwt": null
7
- },
8
- {
9
- "dpk_name": "dataloop-mcp-apis",
10
- "app_url": null,
11
- "server_url": null,
12
- "app_jwt": null
13
- },
14
- {
15
- "dpk_name": "dataloop-mcp-debug",
16
- "app_url": null,
17
- "server_url": null,
18
- "app_jwt": null
19
- },
20
- {
21
- "dpk_name": "dataloop-mcp-data",
22
- "app_url": null,
23
- "server_url": null,
24
- "app_jwt": null
25
- },
26
- {
27
- "dpk_name": "dataloop-mcp-dtlpy-sandbox",
28
- "app_url": null,
29
- "server_url": null,
30
- "app_jwt": null
31
- }
32
- ]
@@ -1,101 +0,0 @@
1
- from pydantic_settings import BaseSettings, SettingsConfigDict
2
- from mcp.server.fastmcp import FastMCP, Context
3
- from typing import Any, Optional
4
- from datetime import datetime
5
- import traceback
6
- import logging
7
- import asyncio
8
- import os
9
- from pathlib import Path
10
-
11
- from dtlpymcp.utils.dtlpy_context import DataloopContext, SOURCES_FILEPATH
12
-
13
- # Setup logging to both console and file with timestamp
14
- log_dir = Path.home() / ".dataloop" / "mcplogs"
15
- log_dir.mkdir(parents=True, exist_ok=True)
16
- log_file = log_dir / f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log"
17
-
18
- # Remove any existing handlers
19
- for handler in logging.root.handlers[:]:
20
- logging.root.removeHandler(handler)
21
-
22
- # File handler with timestamp
23
- file_handler = logging.FileHandler(log_file, mode="a", encoding="utf-8")
24
- file_handler.setFormatter(
25
- logging.Formatter(fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
26
- )
27
-
28
- # Console handler (default format)
29
- console_handler = logging.StreamHandler()
30
- console_handler.setFormatter(logging.Formatter(fmt="[%(levelname)s] %(name)s: %(message)s"))
31
-
32
- logging.basicConfig(level=logging.INFO, handlers=[file_handler, console_handler])
33
- logger = logging.getLogger("[DATALOOP-MCP]")
34
-
35
-
36
- class ServerSettings(BaseSettings):
37
- """Settings for the Dataloop MCP server."""
38
-
39
- model_config = SettingsConfigDict(env_prefix="MCP_DATALOOP_")
40
-
41
- def __init__(self, **data):
42
- super().__init__(**data)
43
-
44
-
45
- def create_dataloop_mcp_server(settings: ServerSettings, sources_file: str) -> FastMCP:
46
- """Create a FastMCP server for Dataloop with Bearer token authentication."""
47
- app = FastMCP(
48
- name="Dataloop MCP Server",
49
- instructions="A multi-tenant MCP server for Dataloop with authentication",
50
- stateless_http=True,
51
- debug=True,
52
- )
53
- dl_context = DataloopContext(token=os.environ.get('DATALOOP_API_KEY'),
54
- env=os.environ.get('DATALOOP_ENV', 'prod'),
55
- sources_file=sources_file)
56
-
57
- # Initialize the Dataloop context
58
- asyncio.run(dl_context.initialize())
59
-
60
- @app.tool(description="Test tool for health checks")
61
- async def test(ctx: Context, ping: Any = None) -> dict[str, Any]:
62
- """Health check tool. Returns status ok and echoes ping if provided."""
63
- result = {"status": "ok"}
64
- if ping is not None:
65
- result["ping"] = ping
66
- return result
67
-
68
- logger.info(f"Adding tools from {len(dl_context.mcp_sources)} sources")
69
-
70
- for source in dl_context.mcp_sources:
71
- logger.info(f"Adding tools from source: {source.dpk_name}")
72
- for tool in source.tools:
73
- app._tool_manager._tools[tool.name] = tool
74
- logger.info(f"Registered tool: {tool.name}")
75
-
76
- return app
77
-
78
-
79
- def main(sources_file: Optional[str] = None) -> int:
80
- logger.info("Starting Dataloop MCP server in stdio mode")
81
- try:
82
- settings = ServerSettings()
83
- logger.info("Successfully configured Dataloop MCP server")
84
- except Exception as e:
85
- logger.error(f"Unexpected error during startup:\n{e}")
86
- return 1
87
- try:
88
- if sources_file is None:
89
- sources_file = SOURCES_FILEPATH
90
- logger.info(f"Using sources file: {sources_file}")
91
- mcp_server = create_dataloop_mcp_server(settings, sources_file)
92
- logger.info("Starting Dataloop MCP server in stdio mode")
93
- mcp_server.run(transport="stdio")
94
- return 0
95
- except Exception:
96
- logger.error(f"Failed to start MCP server: {traceback.format_exc()}")
97
- return 1
98
-
99
-
100
- if __name__ == "__main__":
101
- main()
File without changes
File without changes
File without changes
File without changes