dtlpymcp 0.1.4__py3-none-any.whl → 0.1.8__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 -4
- dtlpymcp/__main__.py +16 -1
- dtlpymcp/proxy.py +74 -101
- dtlpymcp/utils/dtlpy_context.py +64 -50
- dtlpymcp/utils/logging_config.py +61 -0
- dtlpymcp/utils/server_utils.py +91 -0
- {dtlpymcp-0.1.4.dist-info → dtlpymcp-0.1.8.dist-info}/METADATA +16 -1
- dtlpymcp-0.1.8.dist-info/RECORD +11 -0
- dtlpymcp/default_sources.json +0 -20
- dtlpymcp-0.1.4.dist-info/RECORD +0 -10
- {dtlpymcp-0.1.4.dist-info → dtlpymcp-0.1.8.dist-info}/WHEEL +0 -0
- {dtlpymcp-0.1.4.dist-info → dtlpymcp-0.1.8.dist-info}/entry_points.txt +0 -0
- {dtlpymcp-0.1.4.dist-info → dtlpymcp-0.1.8.dist-info}/top_level.txt +0 -0
dtlpymcp/__init__.py
CHANGED
dtlpymcp/__main__.py
CHANGED
|
@@ -21,11 +21,26 @@ def main():
|
|
|
21
21
|
default=None,
|
|
22
22
|
help="Path to a JSON file with MCP sources to load"
|
|
23
23
|
)
|
|
24
|
+
start_parser.add_argument(
|
|
25
|
+
"--init-timeout",
|
|
26
|
+
"-t",
|
|
27
|
+
type=float,
|
|
28
|
+
default=30.0,
|
|
29
|
+
help="Timeout in seconds for Dataloop context initialization (default: 30.0)"
|
|
30
|
+
)
|
|
31
|
+
start_parser.add_argument(
|
|
32
|
+
"--log-level",
|
|
33
|
+
"-l",
|
|
34
|
+
type=str,
|
|
35
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
36
|
+
default="DEBUG",
|
|
37
|
+
help="Logging verbosity level (default: DEBUG)"
|
|
38
|
+
)
|
|
24
39
|
|
|
25
40
|
args = parser.parse_args()
|
|
26
41
|
|
|
27
42
|
if args.command == "start":
|
|
28
|
-
sys.exit(proxy_main(sources_file=args.sources_file))
|
|
43
|
+
sys.exit(proxy_main(sources_file=args.sources_file, init_timeout=args.init_timeout, log_level=args.log_level))
|
|
29
44
|
else:
|
|
30
45
|
parser.print_help()
|
|
31
46
|
return 1
|
dtlpymcp/proxy.py
CHANGED
|
@@ -1,101 +1,74 @@
|
|
|
1
|
-
|
|
2
|
-
from mcp.server.fastmcp import FastMCP, Context
|
|
3
|
-
from typing import Any, Optional
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
for
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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()
|
|
1
|
+
|
|
2
|
+
from mcp.server.fastmcp import FastMCP, Context
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
import traceback
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from dtlpymcp.utils.dtlpy_context import DataloopContext
|
|
8
|
+
from dtlpymcp.utils.server_utils import safe_initialize_dataloop_context
|
|
9
|
+
from dtlpymcp.utils.logging_config import setup_logging, get_logger
|
|
10
|
+
|
|
11
|
+
# Get the main logger
|
|
12
|
+
logger = get_logger()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_dataloop_mcp_server(sources_file: Optional[str] = None, init_timeout: float = 30.0) -> FastMCP:
|
|
16
|
+
"""Create a FastMCP server for Dataloop with Bearer token authentication."""
|
|
17
|
+
app = FastMCP(
|
|
18
|
+
name="Dataloop MCP Server",
|
|
19
|
+
instructions="A multi-tenant MCP server for Dataloop with authentication",
|
|
20
|
+
stateless_http=True,
|
|
21
|
+
debug=True,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Create Dataloop context
|
|
25
|
+
dl_context = DataloopContext(token=os.environ.get('DATALOOP_API_KEY'),
|
|
26
|
+
env=os.environ.get('DATALOOP_ENV', 'prod'),
|
|
27
|
+
sources_file=sources_file)
|
|
28
|
+
|
|
29
|
+
@app.tool(description="Test tool for health checks")
|
|
30
|
+
async def test(ctx: Context, ping: Any = None) -> dict[str, Any]:
|
|
31
|
+
"""Health check tool. Returns status ok and echoes ping if provided."""
|
|
32
|
+
result = {"status": "ok"}
|
|
33
|
+
if ping is not None:
|
|
34
|
+
result["ping"] = ping
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
# Initialize context using the safe utility function
|
|
38
|
+
initialization_success = safe_initialize_dataloop_context(dl_context, app, init_timeout)
|
|
39
|
+
|
|
40
|
+
if not initialization_success:
|
|
41
|
+
logger.info("Server will start without Dataloop tools - they will be available on next restart")
|
|
42
|
+
# Continue without initialization - the server will still work with the test tool
|
|
43
|
+
|
|
44
|
+
return app
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def main(sources_file: Optional[str] = None, init_timeout: float = 30.0, log_level: str = "DEBUG") -> int:
|
|
48
|
+
# Setup logging with the specified level
|
|
49
|
+
setup_logging(log_level)
|
|
50
|
+
|
|
51
|
+
logger.info("Starting Dataloop MCP server in stdio mode")
|
|
52
|
+
|
|
53
|
+
# Validate environment variables
|
|
54
|
+
if not os.environ.get('DATALOOP_API_KEY'):
|
|
55
|
+
logger.error("DATALOOP_API_KEY environment variable is required")
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
mcp_server = create_dataloop_mcp_server(sources_file=sources_file, init_timeout=init_timeout)
|
|
60
|
+
logger.info("Dataloop MCP server created successfully")
|
|
61
|
+
logger.info("Starting server in stdio mode...")
|
|
62
|
+
mcp_server.run(transport="stdio")
|
|
63
|
+
return 0
|
|
64
|
+
except KeyboardInterrupt:
|
|
65
|
+
logger.info("Server stopped by user")
|
|
66
|
+
return 0
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to start MCP server: {e}")
|
|
69
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
70
|
+
return 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
dtlpymcp/utils/dtlpy_context.py
CHANGED
|
@@ -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
|
|
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
|
|
@@ -10,28 +9,13 @@ from mcp import ClientSession
|
|
|
10
9
|
import dtlpy as dl
|
|
11
10
|
import traceback
|
|
12
11
|
import requests
|
|
13
|
-
import logging
|
|
14
|
-
import asyncio
|
|
15
12
|
import time
|
|
16
13
|
import jwt
|
|
17
14
|
import json
|
|
18
|
-
from dtlpymcp import SOURCES_FILEPATH
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
# If called from a running event loop, returns a Task (caller must handle it)
|
|
22
|
-
# If called from sync, blocks and returns result
|
|
16
|
+
from dtlpymcp.utils.logging_config import get_logger
|
|
23
17
|
|
|
24
|
-
|
|
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__)
|
|
18
|
+
logger = get_logger("dtlpy_context")
|
|
35
19
|
|
|
36
20
|
|
|
37
21
|
class MCPSource(BaseModel):
|
|
@@ -53,31 +37,55 @@ class DataloopContext:
|
|
|
53
37
|
self.env = env
|
|
54
38
|
self.mcp_sources: List[MCPSource] = []
|
|
55
39
|
logger.info("DataloopContext initialized.")
|
|
56
|
-
if sources_file is None:
|
|
57
|
-
sources_file = SOURCES_FILEPATH
|
|
58
40
|
self.sources_file = sources_file
|
|
59
41
|
self.initialized = False
|
|
60
|
-
|
|
61
|
-
|
|
42
|
+
|
|
62
43
|
async def initialize(self, force: bool = False):
|
|
63
|
-
if not self.initialized or force:
|
|
44
|
+
if not self.initialized or force:
|
|
64
45
|
await self.register_sources(self.sources_file)
|
|
65
46
|
self.initialized = True
|
|
66
47
|
|
|
67
|
-
async def register_sources(self, sources_file: str):
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
48
|
+
async def register_sources(self, sources_file: str = None):
|
|
49
|
+
if sources_file is None:
|
|
50
|
+
logger.info("Loading MCP sources from all system apps")
|
|
51
|
+
# load all system apps
|
|
52
|
+
filters = dl.Filters(resource='apps')
|
|
53
|
+
filters.add(field="dpkName", values="dataloop-mcp*")
|
|
54
|
+
filters.add(field="scope", values="system")
|
|
55
|
+
apps = list(dl.apps.list(filters=filters).all())
|
|
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:
|
|
74
|
+
try:
|
|
75
|
+
if not isinstance(entry, dict):
|
|
76
|
+
raise ValueError(f"Invalid source entry: {entry}")
|
|
77
|
+
logger.info(f"Adding MCP source: {entry.get('dpk_name')}, url: {entry.get('server_url')}")
|
|
78
|
+
await self.add_mcp_source(MCPSource(**entry))
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Failed to add MCP source: {entry}\n{traceback.format_exc()}")
|
|
76
81
|
|
|
77
82
|
async def add_mcp_source(self, mcp_source: MCPSource):
|
|
78
|
-
|
|
83
|
+
|
|
79
84
|
if mcp_source.server_url is None:
|
|
80
|
-
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
|
|
81
89
|
result = await self.list_source_tools(mcp_source)
|
|
82
90
|
if result is None:
|
|
83
91
|
raise ValueError(f"Failed to discover tools for source {mcp_source.dpk_name}")
|
|
@@ -111,6 +119,7 @@ class DataloopContext:
|
|
|
111
119
|
annotations=None,
|
|
112
120
|
)
|
|
113
121
|
mcp_source.tools.append(t)
|
|
122
|
+
self.mcp_sources.append(mcp_source)
|
|
114
123
|
tool_str = ", ".join([tool.name for tool in mcp_source.tools])
|
|
115
124
|
logger.info(f"Added MCP source: {mcp_source.dpk_name}, Available tools: {tool_str}")
|
|
116
125
|
|
|
@@ -122,18 +131,25 @@ class DataloopContext:
|
|
|
122
131
|
def token(self, token: str):
|
|
123
132
|
self._token = token
|
|
124
133
|
|
|
125
|
-
def load_app_info(self, source: MCPSource) ->
|
|
134
|
+
def load_app_info(self, source: MCPSource) -> bool:
|
|
126
135
|
"""
|
|
127
136
|
Get the source URL and app JWT for a given DPK name using Dataloop SDK.
|
|
128
137
|
"""
|
|
129
138
|
try:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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()))
|
|
137
153
|
session = requests.Session()
|
|
138
154
|
response = session.get(source.app_url, headers=dl.client_api.auth)
|
|
139
155
|
logger.info(f"App route URL: {response.url}")
|
|
@@ -141,7 +157,8 @@ class DataloopContext:
|
|
|
141
157
|
source.app_jwt = session.cookies.get("JWT-APP")
|
|
142
158
|
except Exception:
|
|
143
159
|
logger.error(f"Failed getting app info: {traceback.format_exc()}")
|
|
144
|
-
|
|
160
|
+
return False
|
|
161
|
+
return True
|
|
145
162
|
|
|
146
163
|
@staticmethod
|
|
147
164
|
def is_expired(app_jwt: str) -> bool:
|
|
@@ -163,18 +180,16 @@ class DataloopContext:
|
|
|
163
180
|
"""
|
|
164
181
|
Get the APP_JWT from the request headers or refresh if expired.
|
|
165
182
|
"""
|
|
183
|
+
if source.app_url is None:
|
|
184
|
+
raise ValueError("App URL is missing. Please set the app URL.")
|
|
166
185
|
if source.app_jwt is None or self.is_expired(source.app_jwt):
|
|
167
186
|
try:
|
|
168
187
|
session = requests.Session()
|
|
169
188
|
response = session.get(source.app_url, headers={'authorization': 'Bearer ' + token})
|
|
170
189
|
source.app_jwt = session.cookies.get("JWT-APP")
|
|
171
190
|
except Exception:
|
|
172
|
-
logger.error(f"Failed getting app JWT from cookies\n{traceback.format_exc()}")
|
|
173
191
|
raise Exception(f"Failed getting app JWT from cookies\n{traceback.format_exc()}") from None
|
|
174
192
|
if not source.app_jwt:
|
|
175
|
-
logger.error(
|
|
176
|
-
"APP_JWT is missing. Please set the APP_JWT environment variable or ensure authentication is working."
|
|
177
|
-
)
|
|
178
193
|
raise ValueError(
|
|
179
194
|
"APP_JWT is missing. Please set the APP_JWT environment variable or ensure authentication is working."
|
|
180
195
|
)
|
|
@@ -195,8 +210,7 @@ class DataloopContext:
|
|
|
195
210
|
if source.server_url is None:
|
|
196
211
|
logger.error("DataloopContext required for DPK servers")
|
|
197
212
|
raise ValueError("DataloopContext required for DPK servers")
|
|
198
|
-
headers = {"Cookie": f"JWT-APP={source.app_jwt}",
|
|
199
|
-
"x-dl-info": f"{self.token}"}
|
|
213
|
+
headers = {"Cookie": f"JWT-APP={source.app_jwt}", "x-dl-info": f"{self.token}"}
|
|
200
214
|
async with streamablehttp_client(source.server_url, headers=headers) as (read, write, _):
|
|
201
215
|
async with ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as session:
|
|
202
216
|
await session.initialize()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized logging configuration for dtlpymcp.
|
|
3
|
+
Handles file and console logging with configurable verbosity levels.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_logging(log_level: str = "DEBUG") -> None:
|
|
12
|
+
"""
|
|
13
|
+
Set up logging configuration for the entire dtlpymcp application.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
17
|
+
"""
|
|
18
|
+
# Convert string level to logging constant
|
|
19
|
+
level = getattr(logging, log_level.upper(), logging.DEBUG)
|
|
20
|
+
|
|
21
|
+
# Setup logging directory and file
|
|
22
|
+
log_dir = Path.home() / ".dataloop" / "mcplogs"
|
|
23
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
log_file = log_dir / f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log"
|
|
25
|
+
|
|
26
|
+
# Remove any existing handlers
|
|
27
|
+
for handler in logging.root.handlers[:]:
|
|
28
|
+
logging.root.removeHandler(handler)
|
|
29
|
+
|
|
30
|
+
# File handler with timestamp
|
|
31
|
+
file_handler = logging.FileHandler(log_file, mode="a", encoding="utf-8")
|
|
32
|
+
file_handler.setFormatter(
|
|
33
|
+
logging.Formatter(fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Console handler (default format)
|
|
37
|
+
console_handler = logging.StreamHandler()
|
|
38
|
+
console_handler.setFormatter(logging.Formatter(fmt="[%(levelname)s] %(name)s: %(message)s"))
|
|
39
|
+
|
|
40
|
+
# Configure root logger
|
|
41
|
+
logging.basicConfig(level=level, handlers=[file_handler, console_handler])
|
|
42
|
+
|
|
43
|
+
# Get the main logger
|
|
44
|
+
logger = logging.getLogger("dtlpymcp")
|
|
45
|
+
logger.info(f"Logging configured with level: {log_level.upper()}")
|
|
46
|
+
logger.info(f"Log file: {log_file}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_logger(name: str = None) -> logging.Logger:
|
|
50
|
+
"""
|
|
51
|
+
Get a logger instance with the specified name.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
name: Logger name (optional, defaults to "dtlpymcp")
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
logging.Logger: Configured logger instance
|
|
58
|
+
"""
|
|
59
|
+
if name is None:
|
|
60
|
+
return logging.getLogger("dtlpymcp")
|
|
61
|
+
return logging.getLogger(f"dtlpymcp.{name}")
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for Dataloop MCP server initialization and management.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
from dtlpymcp.utils.dtlpy_context import DataloopContext
|
|
8
|
+
from dtlpymcp.utils.logging_config import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger("server_utils")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_async(coro):
|
|
14
|
+
try:
|
|
15
|
+
loop = asyncio.get_running_loop()
|
|
16
|
+
except RuntimeError:
|
|
17
|
+
# No event loop running
|
|
18
|
+
return asyncio.run(coro)
|
|
19
|
+
else:
|
|
20
|
+
# Already running event loop
|
|
21
|
+
return loop.create_task(coro)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def initialize_dataloop_context(dl_context: DataloopContext, app: FastMCP, init_timeout: float = 30.0) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Initialize Dataloop context with timeout protection.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
dl_context: The DataloopContext instance to initialize
|
|
30
|
+
app: The FastMCP app instance to register tools with
|
|
31
|
+
init_timeout: Timeout in seconds for initialization
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
bool: True if initialization succeeded, False otherwise
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
logger.info("Initializing Dataloop context...")
|
|
38
|
+
# Use asyncio.wait_for for timeout protection
|
|
39
|
+
await asyncio.wait_for(dl_context.initialize(), timeout=init_timeout)
|
|
40
|
+
logger.info("Dataloop context initialized successfully")
|
|
41
|
+
|
|
42
|
+
# Register tools after initialization
|
|
43
|
+
logger.info(f"Adding tools from {len(dl_context.mcp_sources)} sources")
|
|
44
|
+
for source in dl_context.mcp_sources:
|
|
45
|
+
logger.info(f"Adding tools from source: {source.dpk_name}")
|
|
46
|
+
for tool in source.tools:
|
|
47
|
+
app._tool_manager._tools[tool.name] = tool
|
|
48
|
+
logger.info(f"Registered tool: {tool.name}")
|
|
49
|
+
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
except asyncio.TimeoutError:
|
|
53
|
+
logger.error("Timeout during Dataloop context initialization")
|
|
54
|
+
return False
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"Failed to initialize Dataloop context: {e}")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def safe_initialize_dataloop_context(dl_context: DataloopContext, app: FastMCP, init_timeout: float = 30.0) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Safely initialize Dataloop context using run_async utility.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
dl_context: The DataloopContext instance to initialize
|
|
66
|
+
app: The FastMCP app instance to register tools with
|
|
67
|
+
init_timeout: Timeout in seconds for initialization
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
bool: True if initialization succeeded, False otherwise
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
# Use the run_async utility to handle async execution properly
|
|
74
|
+
result = run_async(initialize_dataloop_context(dl_context, app, init_timeout))
|
|
75
|
+
|
|
76
|
+
# Handle different return types from run_async
|
|
77
|
+
if hasattr(result, 'done'):
|
|
78
|
+
# It's a task, wait for it to complete
|
|
79
|
+
while not result.done():
|
|
80
|
+
pass
|
|
81
|
+
return result.result()
|
|
82
|
+
elif asyncio.iscoroutine(result):
|
|
83
|
+
# It's a coroutine, run it
|
|
84
|
+
return asyncio.run(result)
|
|
85
|
+
else:
|
|
86
|
+
# It's already the result (bool)
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
logger.error(f"Failed to initialize during server creation: {e}")
|
|
91
|
+
return False
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dtlpymcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
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:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
dtlpymcp/__init__.py,sha256=6ARRnx35x-dKohwr72GdxSPqO6JfXU5rDFhM1h-GYDY,88
|
|
2
|
+
dtlpymcp/__main__.py,sha256=BRLHkOrX5Ayea3mMHo7pkY8URnVHzIJfKgWnWr6SdXo,1402
|
|
3
|
+
dtlpymcp/proxy.py,sha256=yL1pa-yDG6snwxsT7PzMIzjRbeV2bEgayJhXLRYzbK0,2815
|
|
4
|
+
dtlpymcp/utils/dtlpy_context.py,sha256=Y4o40ekeZmp63pJ1LwHy5hholkTmpbcg3LoQXd05xac,10553
|
|
5
|
+
dtlpymcp/utils/logging_config.py,sha256=R9eUByUdr3n7Tv9YrCQxH-LyspEjaa5kzR1p8F13q08,2082
|
|
6
|
+
dtlpymcp/utils/server_utils.py,sha256=M-OxGqFqAiGC3xAcNw0Ifkyno7pLInRHA_9LJJI7eQs,3220
|
|
7
|
+
dtlpymcp-0.1.8.dist-info/METADATA,sha256=W10TSsIYzXmapg9dkPk-2dB3mP--_admyvewYU1DdHw,2189
|
|
8
|
+
dtlpymcp-0.1.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
dtlpymcp-0.1.8.dist-info/entry_points.txt,sha256=6hRVZNTjQevj7erwt9dAOURtPVrSrYu6uHXhAlhTaXQ,52
|
|
10
|
+
dtlpymcp-0.1.8.dist-info/top_level.txt,sha256=z85v20pIEnY3cBaWgwhU3EZS4WAZRywejhIutwd-iHk,9
|
|
11
|
+
dtlpymcp-0.1.8.dist-info/RECORD,,
|
dtlpymcp/default_sources.json
DELETED
|
@@ -1,20 +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
|
-
]
|
dtlpymcp-0.1.4.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
dtlpymcp/__init__.py,sha256=jz6jKLs8djD6u5xy04_NQO7hgKyFea_oPa-yaiQQq0M,185
|
|
2
|
-
dtlpymcp/__main__.py,sha256=1lo4qoZAoUHl9rkt1YGB2kCpKg5cIrTSBSrznxyk6F4,884
|
|
3
|
-
dtlpymcp/default_sources.json,sha256=Es3XZcdMpe6o5FyOoqW9FG_oUjC5gkNbBC6N9eBqpQs,418
|
|
4
|
-
dtlpymcp/proxy.py,sha256=pE-NAJI0A9wKcBJJt18_aSy1BskZKoxaK3f6VW7u44A,3608
|
|
5
|
-
dtlpymcp/utils/dtlpy_context.py,sha256=0NW5FKFpzUl6gTuJGPPPMqklnNuecgBdwLdW-68YUXE,9752
|
|
6
|
-
dtlpymcp-0.1.4.dist-info/METADATA,sha256=_CYzt19o00e_UIw9SeiQXW7lKp5q1Bq2xSSbyF7W3Yc,1677
|
|
7
|
-
dtlpymcp-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
dtlpymcp-0.1.4.dist-info/entry_points.txt,sha256=6hRVZNTjQevj7erwt9dAOURtPVrSrYu6uHXhAlhTaXQ,52
|
|
9
|
-
dtlpymcp-0.1.4.dist-info/top_level.txt,sha256=z85v20pIEnY3cBaWgwhU3EZS4WAZRywejhIutwd-iHk,9
|
|
10
|
-
dtlpymcp-0.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|