dtlpymcp 0.1.3__py3-none-any.whl → 0.1.7__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 -1
- dtlpymcp/default_sources.json +12 -0
- dtlpymcp/proxy.py +101 -94
- dtlpymcp/utils/dtlpy_context.py +22 -11
- {dtlpymcp-0.1.3.dist-info → dtlpymcp-0.1.7.dist-info}/METADATA +4 -2
- dtlpymcp-0.1.7.dist-info/RECORD +10 -0
- dtlpymcp-0.1.3.dist-info/RECORD +0 -10
- {dtlpymcp-0.1.3.dist-info → dtlpymcp-0.1.7.dist-info}/WHEEL +0 -0
- {dtlpymcp-0.1.3.dist-info → dtlpymcp-0.1.7.dist-info}/entry_points.txt +0 -0
- {dtlpymcp-0.1.3.dist-info → dtlpymcp-0.1.7.dist-info}/top_level.txt +0 -0
dtlpymcp/__init__.py
CHANGED
dtlpymcp/default_sources.json
CHANGED
|
@@ -16,5 +16,17 @@
|
|
|
16
16
|
"app_url": null,
|
|
17
17
|
"server_url": null,
|
|
18
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
|
|
19
31
|
}
|
|
20
32
|
]
|
dtlpymcp/proxy.py
CHANGED
|
@@ -1,94 +1,101 @@
|
|
|
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
|
|
8
|
-
import os
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from . import SOURCES_FILEPATH
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
log_dir =
|
|
16
|
-
log_dir.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
file_handler
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
console_handler
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
logging.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
sources_file=sources_file)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
logger.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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()
|
dtlpymcp/utils/dtlpy_context.py
CHANGED
|
@@ -15,7 +15,7 @@ import asyncio
|
|
|
15
15
|
import time
|
|
16
16
|
import jwt
|
|
17
17
|
import json
|
|
18
|
-
from
|
|
18
|
+
from dtlpymcp import SOURCES_FILEPATH
|
|
19
19
|
|
|
20
20
|
# Utility to run async code from sync or async context
|
|
21
21
|
# If called from a running event loop, returns a Task (caller must handle it)
|
|
@@ -48,8 +48,9 @@ class DataloopContext:
|
|
|
48
48
|
Handles JWTs, server URLs, and dynamic tool registration for multi-tenant environments.
|
|
49
49
|
"""
|
|
50
50
|
|
|
51
|
-
def __init__(self, token: str = None, sources_file: str = None):
|
|
51
|
+
def __init__(self, token: str = None, sources_file: str = None, env: str = 'prod'):
|
|
52
52
|
self._token = token
|
|
53
|
+
self.env = env
|
|
53
54
|
self.mcp_sources: List[MCPSource] = []
|
|
54
55
|
logger.info("DataloopContext initialized.")
|
|
55
56
|
if sources_file is None:
|
|
@@ -68,10 +69,13 @@ class DataloopContext:
|
|
|
68
69
|
data = json.load(f)
|
|
69
70
|
logger.info(f"Loading MCP sources from {sources_file}")
|
|
70
71
|
for entry in data:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
try:
|
|
73
|
+
if not isinstance(entry, dict):
|
|
74
|
+
raise ValueError(f"Invalid source entry: {entry}")
|
|
75
|
+
logger.info(f"Adding MCP source: {entry.get('dpk_name')}, url: {entry.get('server_url')}")
|
|
76
|
+
await self.add_mcp_source(MCPSource(**entry))
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.error(f"Failed to add MCP source: {entry}\n{traceback.format_exc()}")
|
|
75
79
|
|
|
76
80
|
async def add_mcp_source(self, mcp_source: MCPSource):
|
|
77
81
|
self.mcp_sources.append(mcp_source)
|
|
@@ -126,10 +130,17 @@ class DataloopContext:
|
|
|
126
130
|
Get the source URL and app JWT for a given DPK name using Dataloop SDK.
|
|
127
131
|
"""
|
|
128
132
|
try:
|
|
133
|
+
dl.setenv(self.env)
|
|
129
134
|
dl.client_api.token = self.token
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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]
|
|
133
144
|
logger.info(f"App: {app.name}")
|
|
134
145
|
source.app_url = next(iter(app.routes.values()))
|
|
135
146
|
session = requests.Session()
|
|
@@ -137,9 +148,9 @@ class DataloopContext:
|
|
|
137
148
|
logger.info(f"App route URL: {response.url}")
|
|
138
149
|
source.server_url = response.url
|
|
139
150
|
source.app_jwt = session.cookies.get("JWT-APP")
|
|
140
|
-
except Exception:
|
|
151
|
+
except Exception as e:
|
|
141
152
|
logger.error(f"Failed getting app info: {traceback.format_exc()}")
|
|
142
|
-
raise
|
|
153
|
+
raise Exception(f"Failed getting app info: {traceback.format_exc()}") from e
|
|
143
154
|
|
|
144
155
|
@staticmethod
|
|
145
156
|
def is_expired(app_jwt: str) -> bool:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dtlpymcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
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
|
|
@@ -68,8 +68,10 @@ To add this MCP to Cursor, add the following to your configuration:
|
|
|
68
68
|
{
|
|
69
69
|
"mcpServers": {
|
|
70
70
|
"dataloop-ai-mcp": {
|
|
71
|
-
"command": "
|
|
71
|
+
"command": "uvx",
|
|
72
|
+
"args": ["dtlpymcp", "start"],
|
|
72
73
|
"env": {
|
|
74
|
+
"DATALOOP_ENV": "prod",
|
|
73
75
|
"DATALOOP_API_KEY": "API KEY"
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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,,
|
dtlpymcp-0.1.3.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
dtlpymcp/__init__.py,sha256=ISimynz4Nggmf-PH4y1xR2gddnazpe77QNLxSAWM9IM,185
|
|
2
|
-
dtlpymcp/__main__.py,sha256=1lo4qoZAoUHl9rkt1YGB2kCpKg5cIrTSBSrznxyk6F4,884
|
|
3
|
-
dtlpymcp/default_sources.json,sha256=Es3XZcdMpe6o5FyOoqW9FG_oUjC5gkNbBC6N9eBqpQs,418
|
|
4
|
-
dtlpymcp/proxy.py,sha256=_7BGShm-V5LZCe24vbFsysF-Z_Yc70Uuxilot3P5uP4,3447
|
|
5
|
-
dtlpymcp/utils/dtlpy_context.py,sha256=A8TCGjcvRvUc38etrIY-IZlU-JDOPwVUsUJKweKJ1nk,9670
|
|
6
|
-
dtlpymcp-0.1.3.dist-info/METADATA,sha256=zWm9OaXKvM9oIIphSuPNDgtWbylrLkdFlPMpFag3D28,1617
|
|
7
|
-
dtlpymcp-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
dtlpymcp-0.1.3.dist-info/entry_points.txt,sha256=6hRVZNTjQevj7erwt9dAOURtPVrSrYu6uHXhAlhTaXQ,52
|
|
9
|
-
dtlpymcp-0.1.3.dist-info/top_level.txt,sha256=z85v20pIEnY3cBaWgwhU3EZS4WAZRywejhIutwd-iHk,9
|
|
10
|
-
dtlpymcp-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|