dtlpymcp 0.1.7__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 +58 -53
- dtlpymcp/utils/logging_config.py +61 -0
- dtlpymcp/utils/server_utils.py +91 -0
- {dtlpymcp-0.1.7.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 -32
- dtlpymcp-0.1.7.dist-info/RECORD +0 -10
- {dtlpymcp-0.1.7.dist-info → dtlpymcp-0.1.8.dist-info}/WHEEL +0 -0
- {dtlpymcp-0.1.7.dist-info → dtlpymcp-0.1.8.dist-info}/entry_points.txt +0 -0
- {dtlpymcp-0.1.7.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,22 +37,40 @@ 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
|
-
|
|
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:
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
158
|
+
except Exception:
|
|
152
159
|
logger.error(f"Failed getting app info: {traceback.format_exc()}")
|
|
153
|
-
|
|
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()
|
|
@@ -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,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
|
-
]
|
dtlpymcp-0.1.7.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
dtlpymcp/__init__.py,sha256=tUwpqP2EpOpWItUNbzkuuC10aidfIn1SVheVwMf61bY,185
|
|
2
|
-
dtlpymcp/__main__.py,sha256=1lo4qoZAoUHl9rkt1YGB2kCpKg5cIrTSBSrznxyk6F4,884
|
|
3
|
-
dtlpymcp/default_sources.json,sha256=Kg6ArvMscwevXZp5zsnrnSSf8ds8M7AZDtKCYLJEly4,701
|
|
4
|
-
dtlpymcp/proxy.py,sha256=pE-NAJI0A9wKcBJJt18_aSy1BskZKoxaK3f6VW7u44A,3608
|
|
5
|
-
dtlpymcp/utils/dtlpy_context.py,sha256=WmLOoIpujudJ7pDvb07X8_jDAM_ndAi-L4Kqu4T3lFI,10231
|
|
6
|
-
dtlpymcp-0.1.7.dist-info/METADATA,sha256=bkJWJnqSKUCJy2yvUaRHz-IL7DWESe2KyUq6yJnsgN8,1677
|
|
7
|
-
dtlpymcp-0.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
dtlpymcp-0.1.7.dist-info/entry_points.txt,sha256=6hRVZNTjQevj7erwt9dAOURtPVrSrYu6uHXhAlhTaXQ,52
|
|
9
|
-
dtlpymcp-0.1.7.dist-info/top_level.txt,sha256=z85v20pIEnY3cBaWgwhU3EZS4WAZRywejhIutwd-iHk,9
|
|
10
|
-
dtlpymcp-0.1.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|