dtlpymcp 0.1.8__tar.gz → 0.1.10__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.
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/PKG-INFO +1 -1
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/dtlpymcp/__init__.py +1 -2
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/dtlpymcp/__main__.py +9 -12
- dtlpymcp-0.1.8/dtlpymcp/proxy.py → dtlpymcp-0.1.10/dtlpymcp/min_proxy.py +36 -35
- dtlpymcp-0.1.10/dtlpymcp/proxy.py +174 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/dtlpymcp/utils/dtlpy_context.py +9 -7
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/dtlpymcp.egg-info/PKG-INFO +1 -1
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/dtlpymcp.egg-info/SOURCES.txt +1 -2
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/pyproject.toml +1 -1
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/tests/test_proxy.py +7 -5
- dtlpymcp-0.1.8/dtlpymcp/utils/logging_config.py +0 -61
- dtlpymcp-0.1.8/dtlpymcp/utils/server_utils.py +0 -91
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/MANIFEST.in +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/README.md +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/dtlpymcp.egg-info/dependency_links.txt +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/dtlpymcp.egg-info/entry_points.txt +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/dtlpymcp.egg-info/requires.txt +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/dtlpymcp.egg-info/top_level.txt +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/setup.cfg +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/tests/test_context.py +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/tests/test_custom_sources_file.py +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/tests/test_list_platform_tools.py +0 -0
- {dtlpymcp-0.1.8 → dtlpymcp-0.1.10}/tests/test_run.py +0 -0
|
@@ -2,39 +2,35 @@
|
|
|
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"
|
|
23
19
|
)
|
|
24
20
|
start_parser.add_argument(
|
|
25
21
|
"--init-timeout",
|
|
26
22
|
"-t",
|
|
27
23
|
type=float,
|
|
28
24
|
default=30.0,
|
|
29
|
-
help="Timeout in seconds for Dataloop context initialization (default: 30.0)"
|
|
25
|
+
help="Timeout in seconds for Dataloop context initialization (default: 30.0)",
|
|
30
26
|
)
|
|
31
27
|
start_parser.add_argument(
|
|
32
28
|
"--log-level",
|
|
33
29
|
"-l",
|
|
34
30
|
type=str,
|
|
31
|
+
default="INFO",
|
|
35
32
|
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
36
|
-
|
|
37
|
-
help="Logging verbosity level (default: DEBUG)"
|
|
33
|
+
help="Logging level (default: INFO)",
|
|
38
34
|
)
|
|
39
35
|
|
|
40
36
|
args = parser.parse_args()
|
|
@@ -45,5 +41,6 @@ def main():
|
|
|
45
41
|
parser.print_help()
|
|
46
42
|
return 1
|
|
47
43
|
|
|
44
|
+
|
|
48
45
|
if __name__ == "__main__":
|
|
49
|
-
main()
|
|
46
|
+
main()
|
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
|
|
2
1
|
from mcp.server.fastmcp import FastMCP, Context
|
|
3
|
-
from typing import Any
|
|
2
|
+
from typing import Any
|
|
4
3
|
import traceback
|
|
5
4
|
import os
|
|
6
|
-
|
|
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
|
|
7
9
|
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
10
|
|
|
11
|
-
|
|
12
|
-
logger = get_logger()
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
def create_dataloop_mcp_server(
|
|
14
|
+
def create_dataloop_mcp_server() -> FastMCP:
|
|
16
15
|
"""Create a FastMCP server for Dataloop with Bearer token authentication."""
|
|
17
|
-
|
|
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")
|
|
16
|
+
|
|
30
17
|
async def test(ctx: Context, ping: Any = None) -> dict[str, Any]:
|
|
31
18
|
"""Health check tool. Returns status ok and echoes ping if provided."""
|
|
32
19
|
result = {"status": "ok"}
|
|
@@ -34,29 +21,43 @@ def create_dataloop_mcp_server(sources_file: Optional[str] = None, init_timeout:
|
|
|
34
21
|
result["ping"] = ping
|
|
35
22
|
return result
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
|
|
44
48
|
return app
|
|
45
49
|
|
|
46
50
|
|
|
47
|
-
def main(
|
|
48
|
-
# Setup logging with the specified level
|
|
49
|
-
setup_logging(log_level)
|
|
50
|
-
|
|
51
|
+
def main() -> int:
|
|
51
52
|
logger.info("Starting Dataloop MCP server in stdio mode")
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
# Validate environment variables
|
|
54
55
|
if not os.environ.get('DATALOOP_API_KEY'):
|
|
55
56
|
logger.error("DATALOOP_API_KEY environment variable is required")
|
|
56
57
|
return 1
|
|
57
|
-
|
|
58
|
+
|
|
58
59
|
try:
|
|
59
|
-
mcp_server = create_dataloop_mcp_server(
|
|
60
|
+
mcp_server = create_dataloop_mcp_server()
|
|
60
61
|
logger.info("Dataloop MCP server created successfully")
|
|
61
62
|
logger.info("Starting server in stdio mode...")
|
|
62
63
|
mcp_server.run(transport="stdio")
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import os
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
from pydantic import create_model
|
|
11
|
+
from mcp.server.fastmcp.utilities.func_metadata import ArgModelBase
|
|
12
|
+
|
|
13
|
+
from dtlpymcp.utils.dtlpy_context import DataloopContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def setup_logging(log_level: str = "INFO") -> logging.Logger:
|
|
17
|
+
"""
|
|
18
|
+
Setup logging configuration with the specified log level.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
log_level: Logging level as string (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
logging.Logger: Configured logger instance
|
|
25
|
+
"""
|
|
26
|
+
# Setup logging directory and file
|
|
27
|
+
log_dir = Path.home() / ".dataloop" / "mcplogs"
|
|
28
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
log_file = log_dir / f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log"
|
|
30
|
+
|
|
31
|
+
# Remove any existing handlers
|
|
32
|
+
for handler in logging.root.handlers[:]:
|
|
33
|
+
logging.root.removeHandler(handler)
|
|
34
|
+
|
|
35
|
+
# File handler with timestamp
|
|
36
|
+
file_handler = logging.FileHandler(log_file, mode="a", encoding="utf-8")
|
|
37
|
+
file_handler.setFormatter(
|
|
38
|
+
logging.Formatter(fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Console handler (default format)
|
|
42
|
+
console_handler = logging.StreamHandler()
|
|
43
|
+
console_handler.setFormatter(logging.Formatter(fmt="[%(levelname)s] %(name)s: %(message)s"))
|
|
44
|
+
|
|
45
|
+
# Convert string log level to logging constant
|
|
46
|
+
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
|
|
47
|
+
|
|
48
|
+
# Configure root logger
|
|
49
|
+
logging.basicConfig(level=numeric_level, handlers=[file_handler, console_handler])
|
|
50
|
+
|
|
51
|
+
# Get the main logger
|
|
52
|
+
logger = logging.getLogger("dtlpymcp")
|
|
53
|
+
logger.info(f"Logging configured with level: {log_level.upper()}")
|
|
54
|
+
logger.info(f"Log file: {log_file}")
|
|
55
|
+
|
|
56
|
+
return logger
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run_async(coro):
|
|
60
|
+
try:
|
|
61
|
+
loop = asyncio.get_running_loop()
|
|
62
|
+
except RuntimeError:
|
|
63
|
+
# No event loop running
|
|
64
|
+
return asyncio.run(coro)
|
|
65
|
+
else:
|
|
66
|
+
# Already running event loop
|
|
67
|
+
return loop.create_task(coro)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def initialize_dataloop_context(sources_file: Optional[str] = None, init_timeout: float = 30.0) -> List[Tool]:
|
|
71
|
+
"""
|
|
72
|
+
Initialize Dataloop context with timeout protection.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
sources_file: Path to sources file
|
|
76
|
+
init_timeout: Timeout in seconds for initialization
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
List[Tool]: List of tools
|
|
80
|
+
"""
|
|
81
|
+
logger = logging.getLogger("dtlpymcp")
|
|
82
|
+
try:
|
|
83
|
+
tools = []
|
|
84
|
+
dl_context = DataloopContext(
|
|
85
|
+
token=os.environ.get('DATALOOP_API_KEY'),
|
|
86
|
+
env=os.environ.get('DATALOOP_ENV', 'prod'),
|
|
87
|
+
sources_file=sources_file,
|
|
88
|
+
)
|
|
89
|
+
logger.info("Initializing Dataloop context...")
|
|
90
|
+
await dl_context.initialize()
|
|
91
|
+
logger.info("Dataloop context initialized successfully")
|
|
92
|
+
|
|
93
|
+
logger.info(f"Adding tools from {len(dl_context.mcp_sources)} sources")
|
|
94
|
+
for source in dl_context.mcp_sources:
|
|
95
|
+
logger.info(f"Adding tools from source: {source.dpk_name}")
|
|
96
|
+
for tool in source.tools:
|
|
97
|
+
tools.append(tool)
|
|
98
|
+
logger.info(f"Registered tool: {tool.name}")
|
|
99
|
+
|
|
100
|
+
return tools
|
|
101
|
+
|
|
102
|
+
except asyncio.TimeoutError:
|
|
103
|
+
logger.error("Timeout during Dataloop context initialization")
|
|
104
|
+
return []
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(f"Failed to initialize Dataloop context: {e}")
|
|
107
|
+
return []
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def create_dataloop_mcp_server(sources_file: Optional[str] = None, init_timeout: float = 30.0) -> FastMCP:
|
|
111
|
+
"""Create a FastMCP server for Dataloop with Bearer token authentication."""
|
|
112
|
+
|
|
113
|
+
async def test(ctx: Context, ping: Any = None) -> dict[str, Any]:
|
|
114
|
+
"""Health check tool. Returns status ok and echoes ping if provided."""
|
|
115
|
+
result = {"status": "ok"}
|
|
116
|
+
if ping is not None:
|
|
117
|
+
result["ping"] = ping
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
tool_name = "test"
|
|
121
|
+
input_schema = {"properties": {"ping": {"type": "string"}}, "required": ["ping"]}
|
|
122
|
+
dynamic_pydantic_model_params = DataloopContext.build_pydantic_fields_from_schema(input_schema)
|
|
123
|
+
arguments_model = create_model(f"{tool_name}Arguments", **dynamic_pydantic_model_params, __base__=ArgModelBase)
|
|
124
|
+
resp = FuncMetadata(arg_model=arguments_model)
|
|
125
|
+
t = Tool(
|
|
126
|
+
fn=test,
|
|
127
|
+
name=tool_name,
|
|
128
|
+
description="Test tool for health checks",
|
|
129
|
+
parameters=input_schema,
|
|
130
|
+
is_async=True,
|
|
131
|
+
context_kwarg="ctx",
|
|
132
|
+
fn_metadata=resp,
|
|
133
|
+
annotations=None,
|
|
134
|
+
)
|
|
135
|
+
tools = [t]
|
|
136
|
+
tools.extend(run_async(initialize_dataloop_context(sources_file=sources_file, init_timeout=init_timeout)))
|
|
137
|
+
app = FastMCP(
|
|
138
|
+
name="Dataloop MCP Server",
|
|
139
|
+
instructions="A multi-tenant MCP server for Dataloop with authentication",
|
|
140
|
+
debug=True,
|
|
141
|
+
tools=tools,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return app
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main(sources_file: Optional[str] = None, init_timeout: float = 30.0, log_level: str = "INFO") -> int:
|
|
148
|
+
# Setup logging with the specified level
|
|
149
|
+
logger = setup_logging(log_level)
|
|
150
|
+
|
|
151
|
+
logger.info("Starting Dataloop MCP server in stdio mode")
|
|
152
|
+
|
|
153
|
+
# Validate environment variables
|
|
154
|
+
if not os.environ.get('DATALOOP_API_KEY'):
|
|
155
|
+
logger.error("DATALOOP_API_KEY environment variable is required")
|
|
156
|
+
return 1
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
mcp_server = create_dataloop_mcp_server(sources_file=sources_file, init_timeout=init_timeout)
|
|
160
|
+
logger.info("Dataloop MCP server created successfully")
|
|
161
|
+
logger.info("Starting server in stdio mode...")
|
|
162
|
+
mcp_server.run(transport="stdio")
|
|
163
|
+
return 0
|
|
164
|
+
except KeyboardInterrupt:
|
|
165
|
+
logger.info("Server stopped by user")
|
|
166
|
+
return 0
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Failed to start MCP server: {e}")
|
|
169
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
170
|
+
return 1
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
main()
|
|
@@ -9,13 +9,12 @@ from mcp import ClientSession
|
|
|
9
9
|
import dtlpy as dl
|
|
10
10
|
import traceback
|
|
11
11
|
import requests
|
|
12
|
+
import logging
|
|
12
13
|
import time
|
|
13
14
|
import jwt
|
|
14
15
|
import json
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
logger = get_logger("dtlpy_context")
|
|
17
|
+
logger = logging.getLogger("dtlpymcp")
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
class MCPSource(BaseModel):
|
|
@@ -52,7 +51,8 @@ class DataloopContext:
|
|
|
52
51
|
filters = dl.Filters(resource='apps')
|
|
53
52
|
filters.add(field="dpkName", values="dataloop-mcp*")
|
|
54
53
|
filters.add(field="scope", values="system")
|
|
55
|
-
|
|
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
56
|
if len(apps) == 0:
|
|
57
57
|
raise ValueError(f"No app found for DPK name: dataloop-mcp*")
|
|
58
58
|
sources = []
|
|
@@ -230,17 +230,19 @@ class DataloopContext:
|
|
|
230
230
|
logger.info(f"Discovered {len(tools.tools)} tools for source {source.dpk_name}")
|
|
231
231
|
return (source.dpk_name, tools, call_fn)
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
@staticmethod
|
|
234
|
+
def openapi_type_to_python(type_str):
|
|
234
235
|
return {"string": str, "integer": int, "number": float, "boolean": bool, "array": list, "object": dict}.get(
|
|
235
236
|
type_str, str
|
|
236
237
|
)
|
|
237
238
|
|
|
238
|
-
|
|
239
|
+
@staticmethod
|
|
240
|
+
def build_pydantic_fields_from_schema(input_schema):
|
|
239
241
|
required = set(input_schema.get("required", []))
|
|
240
242
|
properties = input_schema.get("properties", {})
|
|
241
243
|
fields = {}
|
|
242
244
|
for name, prop in properties.items():
|
|
243
|
-
py_type =
|
|
245
|
+
py_type = DataloopContext.openapi_type_to_python(prop.get("type", "string"))
|
|
244
246
|
if name in required:
|
|
245
247
|
fields[name] = (py_type, Field(...))
|
|
246
248
|
else:
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
dtlpymcp/__init__.py
|
|
5
5
|
dtlpymcp/__main__.py
|
|
6
|
+
dtlpymcp/min_proxy.py
|
|
6
7
|
dtlpymcp/proxy.py
|
|
7
8
|
dtlpymcp.egg-info/PKG-INFO
|
|
8
9
|
dtlpymcp.egg-info/SOURCES.txt
|
|
@@ -11,8 +12,6 @@ dtlpymcp.egg-info/entry_points.txt
|
|
|
11
12
|
dtlpymcp.egg-info/requires.txt
|
|
12
13
|
dtlpymcp.egg-info/top_level.txt
|
|
13
14
|
dtlpymcp/utils/dtlpy_context.py
|
|
14
|
-
dtlpymcp/utils/logging_config.py
|
|
15
|
-
dtlpymcp/utils/server_utils.py
|
|
16
15
|
tests/test_context.py
|
|
17
16
|
tests/test_custom_sources_file.py
|
|
18
17
|
tests/test_list_platform_tools.py
|
|
@@ -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('
|
|
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=
|
|
18
|
-
args=["
|
|
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,61 +0,0 @@
|
|
|
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}")
|
|
@@ -1,91 +0,0 @@
|
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|