swanlab-mcp 0.0.1__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.
- swanlab_mcp/__init__.py +12 -0
- swanlab_mcp/__main__.py +4 -0
- swanlab_mcp/_version.py +3 -0
- swanlab_mcp/cli.py +59 -0
- swanlab_mcp/config.py +59 -0
- swanlab_mcp/constants.py +7 -0
- swanlab_mcp/meta/__init__.py +0 -0
- swanlab_mcp/meta/info.py +72 -0
- swanlab_mcp/models.py +26 -0
- swanlab_mcp/server.py +38 -0
- swanlab_mcp/tools/__init__.py +16 -0
- swanlab_mcp/tools/experiment.py +245 -0
- swanlab_mcp/tools/project.py +154 -0
- swanlab_mcp/tools/workspace.py +58 -0
- swanlab_mcp-0.0.1.dist-info/METADATA +155 -0
- swanlab_mcp-0.0.1.dist-info/RECORD +18 -0
- swanlab_mcp-0.0.1.dist-info/WHEEL +4 -0
- swanlab_mcp-0.0.1.dist-info/entry_points.txt +2 -0
swanlab_mcp/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from ._version import __version__
|
|
2
|
+
from .config import SwanLabConfig
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main() -> None:
|
|
6
|
+
"""Serve as the main entry point for SwanLab MCP Server."""
|
|
7
|
+
from .cli import main as cli_main
|
|
8
|
+
|
|
9
|
+
cli_main()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["main", "__version__", "SwanLabConfig"]
|
swanlab_mcp/__main__.py
ADDED
swanlab_mcp/_version.py
ADDED
swanlab_mcp/cli.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Command line interface for SwanLab MCP Server."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .meta.info import get_server_name, get_server_name_with_version, get_server_version
|
|
7
|
+
from .server import create_mcp_server
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
11
|
+
"""Create and configure the argument parser."""
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
description=get_server_name_with_version(),
|
|
14
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
15
|
+
epilog="""
|
|
16
|
+
Examples:
|
|
17
|
+
%(prog)s # Run with stdio transport (default)
|
|
18
|
+
%(prog)s --transport stdio # Run with stdio transport
|
|
19
|
+
""",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--transport",
|
|
24
|
+
choices=["stdio"],
|
|
25
|
+
default="stdio",
|
|
26
|
+
help="Transport type (default: stdio)",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--version",
|
|
31
|
+
action="version",
|
|
32
|
+
version=f"%(prog)s {get_server_version()}",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return parser
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> None:
|
|
39
|
+
"""Handle CLI entry point operations."""
|
|
40
|
+
parser = create_parser()
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
# Create and configure the MCP server
|
|
44
|
+
try:
|
|
45
|
+
mcp = create_mcp_server()
|
|
46
|
+
print(f"MCP server created successfully on transport {args.transport}")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"Error creating MCP server: {e}", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
print(f"Starting MCP server on transport {args.transport}...")
|
|
53
|
+
mcp.run(transport=args.transport)
|
|
54
|
+
except KeyboardInterrupt:
|
|
55
|
+
print(f"\nShutting down {get_server_name()}...", file=sys.stderr)
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f"Error starting server: {e}", file=sys.stderr)
|
|
59
|
+
sys.exit(1)
|
swanlab_mcp/config.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Configuration management for SwanLab MCP Server."""
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
|
|
6
|
+
from .constants import DEFAULT_API_TIMEOUT_SECONDS, DEFAULT_SWANLAB_API_HOST, DEFAULT_SWANLAB_HOST
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SwanLabConfig(BaseSettings):
|
|
10
|
+
"""SwanLab MCP Server configuration settings."""
|
|
11
|
+
|
|
12
|
+
# Authentication settings
|
|
13
|
+
api_key: str | None = Field(
|
|
14
|
+
default=None,
|
|
15
|
+
description="SwanLab API key for authentication",
|
|
16
|
+
validation_alias="SWANLAB_API_KEY",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Domain settings
|
|
20
|
+
main_domain: str = Field(
|
|
21
|
+
default=DEFAULT_SWANLAB_HOST,
|
|
22
|
+
description="SwanLab website domain",
|
|
23
|
+
validation_alias="SWANLAB_HOST",
|
|
24
|
+
)
|
|
25
|
+
api_domain: str = Field(
|
|
26
|
+
default=DEFAULT_SWANLAB_API_HOST,
|
|
27
|
+
description="SwanLab API domain",
|
|
28
|
+
validation_alias="SWANLAB_API_HOST",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Request settings
|
|
32
|
+
timeout: int = Field(
|
|
33
|
+
default=DEFAULT_API_TIMEOUT_SECONDS,
|
|
34
|
+
description="API request timeout in seconds",
|
|
35
|
+
validation_alias="API_TIMEOUT",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
class Config:
|
|
39
|
+
"""Pydantic configuration."""
|
|
40
|
+
|
|
41
|
+
env_file = ".env"
|
|
42
|
+
env_file_encoding = "utf-8"
|
|
43
|
+
extra = "ignore"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_config() -> SwanLabConfig:
|
|
47
|
+
"""
|
|
48
|
+
Get the SwanLab configuration.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
SwanLabConfig instance with settings loaded from environment.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If required settings (like api_key) are not set.
|
|
55
|
+
"""
|
|
56
|
+
config = SwanLabConfig()
|
|
57
|
+
if not config.api_key:
|
|
58
|
+
raise ValueError("SWANLAB_API_KEY environment variable must be set")
|
|
59
|
+
return config
|
swanlab_mcp/constants.py
ADDED
|
File without changes
|
swanlab_mcp/meta/info.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Metadata utilities for version information."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from importlib import metadata
|
|
5
|
+
|
|
6
|
+
from .. import __version__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_server_name() -> str:
|
|
10
|
+
"""Get SwanLab MCP Server name.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
str: Server name without version
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
return "SwanLab MCP Server"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_server_name_with_version() -> str:
|
|
20
|
+
"""Get SwanLab MCP Server name with version.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
str: Server name with version (e.g., "SwanLab MCP Server v1.0.0")
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
return f"{get_server_name()} v{get_server_version()}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_server_version() -> str:
|
|
30
|
+
"""Get SwanLab MCP Server version.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str: Server version string
|
|
34
|
+
|
|
35
|
+
"""
|
|
36
|
+
return __version__
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_fastmcp_version() -> str:
|
|
40
|
+
"""Get FastMCP framework version.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
str: FastMCP version string, or "unknown" if not available
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
return metadata.version("fastmcp")
|
|
48
|
+
except metadata.PackageNotFoundError:
|
|
49
|
+
return "unknown"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_mcp_protocol_version() -> str:
|
|
53
|
+
"""Get MCP protocol version.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
str: MCP protocol version string, or "unknown" if not available
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
return metadata.version("mcp")
|
|
61
|
+
except metadata.PackageNotFoundError:
|
|
62
|
+
return "unknown"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_python_version() -> str:
|
|
66
|
+
"""Get Python runtime version.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
str: Python version in x.y.z format
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
swanlab_mcp/models.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Experiment(BaseModel):
|
|
7
|
+
cuid: str # 实验CUID, 唯一标识符
|
|
8
|
+
name: str # 实验名
|
|
9
|
+
description: str = "" # 实验描述
|
|
10
|
+
state: str # 实验状态, 'FINISHED' 或 'RUNNING'
|
|
11
|
+
show: bool # 显示状态
|
|
12
|
+
createdAt: str # e.g., '2024-11-23T12:28:04.286Z'
|
|
13
|
+
finishedAt: str = "" # e.g., '2024-11-23T12:28:04.286Z'
|
|
14
|
+
user: Dict[str, str] # 实验创建者, 包含 'username' 与 'name'
|
|
15
|
+
profile: Dict # 实验相关配置
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Project(BaseModel):
|
|
19
|
+
cuid: str # 项目CUID, 唯一标识符
|
|
20
|
+
name: str # 项目名
|
|
21
|
+
description: str = "" # 项目描述
|
|
22
|
+
visibility: str # 可见性, 'PUBLIC' 或 'PRIVATE'
|
|
23
|
+
createdAt: str # e.g., '2024-11-23T12:28:04.286Z'
|
|
24
|
+
updatedAt: str # e.g., '2024-11-23T12:28:04.286Z'
|
|
25
|
+
group: Dict[str, str] # 工作空间信息, 包含 'type', 'username', 'name'
|
|
26
|
+
count: Dict[str, int] = {} # 项目的统计信息
|
swanlab_mcp/server.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""SwanLab MCP Server."""
|
|
2
|
+
|
|
3
|
+
from mcp.server.fastmcp import FastMCP
|
|
4
|
+
from swanlab import OpenApi
|
|
5
|
+
|
|
6
|
+
from .config import get_config
|
|
7
|
+
from .meta.info import get_server_name_with_version
|
|
8
|
+
from .tools import register_experiment_tools, register_project_tools, register_workspace_tools
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_mcp_server():
|
|
12
|
+
# Load configuration
|
|
13
|
+
config = get_config()
|
|
14
|
+
|
|
15
|
+
# Initialize SwanLab API
|
|
16
|
+
swanlab_api = OpenApi(api_key=config.api_key or "")
|
|
17
|
+
|
|
18
|
+
# Initialize MCP server
|
|
19
|
+
mcp = FastMCP(
|
|
20
|
+
name=get_server_name_with_version(),
|
|
21
|
+
instructions="""
|
|
22
|
+
A Model Context Protocol (MCP) server for SwanLab - a collaborative machine learning experiment tracking platform.
|
|
23
|
+
|
|
24
|
+
This server provides tools to:
|
|
25
|
+
- List and manage workspaces
|
|
26
|
+
- List, get, and delete projects
|
|
27
|
+
- List, get, and delete experiments
|
|
28
|
+
- Retrieve experiment metrics and summaries
|
|
29
|
+
|
|
30
|
+
All operations require a valid SWANLAB_API_KEY set in the environment.
|
|
31
|
+
""",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Register all tools
|
|
35
|
+
register_workspace_tools(mcp, swanlab_api)
|
|
36
|
+
register_project_tools(mcp, swanlab_api)
|
|
37
|
+
register_experiment_tools(mcp, swanlab_api)
|
|
38
|
+
return mcp
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""SwanLab MCP Tools."""
|
|
2
|
+
|
|
3
|
+
from .experiment import ExperimentTools, register_experiment_tools
|
|
4
|
+
from .project import ProjectTools, register_project_tools
|
|
5
|
+
from .workspace import WorkspaceTools, register_workspace_tools
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
# Tool classes
|
|
9
|
+
"ProjectTools",
|
|
10
|
+
"ExperimentTools",
|
|
11
|
+
"WorkspaceTools",
|
|
12
|
+
# Registration functions
|
|
13
|
+
"register_workspace_tools",
|
|
14
|
+
"register_project_tools",
|
|
15
|
+
"register_experiment_tools",
|
|
16
|
+
]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Experiment related MCP tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
from mcp.types import ToolAnnotations
|
|
7
|
+
from swanlab import OpenApi
|
|
8
|
+
|
|
9
|
+
from ..models import Experiment
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExperimentTools:
|
|
13
|
+
"""SwanLab Experiment management tools."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, api: OpenApi):
|
|
16
|
+
self.api = api
|
|
17
|
+
|
|
18
|
+
async def list_experiments(self, project: str) -> List[Experiment]:
|
|
19
|
+
"""
|
|
20
|
+
List all experiments in a project.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
project: Project name or CUID
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
List of Experiment objects containing:
|
|
27
|
+
- cuid: Unique experiment identifier
|
|
28
|
+
- name: Experiment name
|
|
29
|
+
- description: Experiment description
|
|
30
|
+
- state: RUNNING or FINISHED
|
|
31
|
+
- show: Display status
|
|
32
|
+
- createdAt/finishedAt: Timestamps
|
|
33
|
+
- user: Creator information
|
|
34
|
+
- profile: Experiment configuration
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
response = self.api.list_experiments(project=project)
|
|
38
|
+
experiments = []
|
|
39
|
+
for item in response.data:
|
|
40
|
+
if hasattr(item, "model_dump"):
|
|
41
|
+
experiments.append(Experiment(**item.model_dump()))
|
|
42
|
+
elif hasattr(item, "__dict__"):
|
|
43
|
+
experiments.append(Experiment(**item.__dict__))
|
|
44
|
+
else:
|
|
45
|
+
experiments.append(Experiment(**item))
|
|
46
|
+
return experiments
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise RuntimeError(f"Failed to list experiments for project '{project}': {str(e)}") from e
|
|
49
|
+
|
|
50
|
+
async def get_experiment(self, project: str, exp_id: str) -> Experiment:
|
|
51
|
+
"""
|
|
52
|
+
Get detailed information about a specific experiment.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
project: Project name or CUID
|
|
56
|
+
exp_id: Experiment CUID
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Experiment object with detailed information including profile data
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
response = self.api.get_experiment(project=project, exp_id=exp_id)
|
|
63
|
+
item = response.data
|
|
64
|
+
if hasattr(item, "model_dump"):
|
|
65
|
+
return Experiment(**item.model_dump())
|
|
66
|
+
elif hasattr(item, "__dict__"):
|
|
67
|
+
return Experiment(**item.__dict__)
|
|
68
|
+
else:
|
|
69
|
+
return Experiment(**item)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise RuntimeError(f"Failed to get experiment '{exp_id}' from project '{project}': {str(e)}") from e
|
|
72
|
+
|
|
73
|
+
async def get_summary(self, project: str, exp_id: str) -> Dict[str, Any]:
|
|
74
|
+
"""
|
|
75
|
+
Get summary statistics for an experiment's metrics.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
project: Project name or CUID
|
|
79
|
+
exp_id: Experiment CUID
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dictionary containing metric summaries with step, value, min, and max information.
|
|
83
|
+
Example: {"loss": {"step": 47, "value": 0.19, "min": {...}, "max": {...}}}
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
response = self.api.get_summary(project=project, exp_id=exp_id)
|
|
87
|
+
return response.data
|
|
88
|
+
except Exception as e:
|
|
89
|
+
raise RuntimeError(f"Failed to get summary for experiment '{exp_id}': {str(e)}") from e
|
|
90
|
+
|
|
91
|
+
async def get_metrics(self, exp_id: str, keys: Union[str, List[str]]) -> Optional[Dict[str, Any]]:
|
|
92
|
+
"""
|
|
93
|
+
Get metric data for an experiment.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
exp_id: Experiment CUID
|
|
97
|
+
keys: List of metric names to retrieve (e.g., ["loss", "acc"])
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Dictionary containing metric values indexed by step.
|
|
101
|
+
Includes both values and timestamps for each metric.
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
response = self.api.get_metrics(exp_id=exp_id, keys=keys if isinstance(keys, list) else [keys])
|
|
105
|
+
data = response.data
|
|
106
|
+
if hasattr(data, "to_dict"):
|
|
107
|
+
return data.to_dict()
|
|
108
|
+
return None
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise RuntimeError(f"Failed to get metrics for experiment '{exp_id}': {str(e)}") from e
|
|
111
|
+
|
|
112
|
+
async def delete_experiment(self, project: str, exp_id: str) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Delete an experiment from a project.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
project: Project name or CUID
|
|
118
|
+
exp_id: Experiment CUID to delete
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Success message
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
self.api.delete_experiment(project=project, exp_id=exp_id)
|
|
125
|
+
return f"Successfully deleted experiment '{exp_id}' from project '{project}'"
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise RuntimeError(f"Failed to delete experiment '{exp_id}': {str(e)}") from e
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def register_experiment_tools(mcp: FastMCP, api: OpenApi) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Register experiment-related MCP tools.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
mcp: FastMCP server instance
|
|
136
|
+
api: SwanLab OpenApi instance
|
|
137
|
+
"""
|
|
138
|
+
experiment_tools = ExperimentTools(api)
|
|
139
|
+
|
|
140
|
+
@mcp.tool(
|
|
141
|
+
name="swanlab_list_experiments_in_a_project",
|
|
142
|
+
description="List all experiments in a project.",
|
|
143
|
+
annotations=ToolAnnotations(
|
|
144
|
+
title="List all experiments in a project.",
|
|
145
|
+
readOnlyHint=True,
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
async def list_experiments(project: str) -> List[Dict[str, Any]]:
|
|
149
|
+
"""
|
|
150
|
+
List all experiments in a project.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
project: Project name or CUID
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
List of experiments with their names, states, descriptions, and metadata.
|
|
157
|
+
"""
|
|
158
|
+
experiments = await experiment_tools.list_experiments(project)
|
|
159
|
+
return [exp.model_dump() for exp in experiments]
|
|
160
|
+
|
|
161
|
+
@mcp.tool(
|
|
162
|
+
name="swanlab_get_a_specific_experiment",
|
|
163
|
+
description="Get detailed information about a specific experiment in a project.",
|
|
164
|
+
annotations=ToolAnnotations(
|
|
165
|
+
title="Get detailed information about a specific experiment in a project.",
|
|
166
|
+
readOnlyHint=True,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
async def get_experiment(project: str, exp_id: str) -> Dict[str, Any]:
|
|
170
|
+
"""
|
|
171
|
+
Get detailed information about a specific experiment.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
project: Project name or CUID
|
|
175
|
+
exp_id: Experiment CUID
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Experiment details including profile data, configuration, and metadata.
|
|
179
|
+
"""
|
|
180
|
+
experiment = await experiment_tools.get_experiment(project, exp_id)
|
|
181
|
+
return experiment.model_dump()
|
|
182
|
+
|
|
183
|
+
@mcp.tool(
|
|
184
|
+
name="swanlab_get_summary_for_an_experiment",
|
|
185
|
+
description="Get summary statistics for an experiment's metrics.",
|
|
186
|
+
annotations=ToolAnnotations(
|
|
187
|
+
title="Get summary statistics for an experiment's metrics.",
|
|
188
|
+
readOnlyHint=True,
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
async def get_experiment_summary(project: str, exp_id: str) -> Dict[str, Any]:
|
|
192
|
+
"""
|
|
193
|
+
Get summary statistics for an experiment's metrics.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
project: Project name or CUID
|
|
197
|
+
exp_id: Experiment CUID
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Dictionary containing metric summaries with step, value, min, and max information.
|
|
201
|
+
"""
|
|
202
|
+
return await experiment_tools.get_summary(project, exp_id)
|
|
203
|
+
|
|
204
|
+
@mcp.tool(
|
|
205
|
+
name="swanlab_get_metrics_for_an_experiment",
|
|
206
|
+
description="Get metric data for an experiment.",
|
|
207
|
+
annotations=ToolAnnotations(
|
|
208
|
+
title="Get metric data for an experiment.",
|
|
209
|
+
readOnlyHint=True,
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
async def get_experiment_metrics(exp_id: str, keys: List[str]) -> Optional[Dict[str, Any]]:
|
|
213
|
+
"""
|
|
214
|
+
Get metric data for an experiment.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
exp_id: Experiment CUID
|
|
218
|
+
keys: Optional list of metric names to retrieve (e.g., ["loss", "acc"]).
|
|
219
|
+
If not provided, all metrics will be returned.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Dictionary containing metric values indexed by step, including timestamps.
|
|
223
|
+
"""
|
|
224
|
+
return await experiment_tools.get_metrics(exp_id, keys)
|
|
225
|
+
|
|
226
|
+
@mcp.tool(
|
|
227
|
+
name="swanlab_delete_an_experiment",
|
|
228
|
+
description="Delete an experiment from a project. Warning: This action cannot be undone.",
|
|
229
|
+
annotations=ToolAnnotations(
|
|
230
|
+
title="Delete an experiment from a project. Warning: This action cannot be undone.",
|
|
231
|
+
readOnlyHint=False,
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
async def delete_experiment(project: str, exp_id: str) -> str:
|
|
235
|
+
"""
|
|
236
|
+
Delete an experiment from a project.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
project: Project name or CUID
|
|
240
|
+
exp_id: Experiment CUID to delete
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Success message confirming deletion.
|
|
244
|
+
"""
|
|
245
|
+
return await experiment_tools.delete_experiment(project, exp_id)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Project related MCP tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
from mcp.types import ToolAnnotations
|
|
7
|
+
from swanlab import OpenApi
|
|
8
|
+
|
|
9
|
+
from ..models import Project
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProjectTools:
|
|
13
|
+
"""SwanLab Project management tools."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, api: OpenApi):
|
|
16
|
+
self.api = api
|
|
17
|
+
|
|
18
|
+
async def list_projects(self) -> List[Project]:
|
|
19
|
+
"""
|
|
20
|
+
List all projects in the workspace.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
List of Project objects containing project information including:
|
|
24
|
+
- cuid: Unique project identifier
|
|
25
|
+
- name: Project name
|
|
26
|
+
- description: Project description
|
|
27
|
+
- visibility: PUBLIC or PRIVATE
|
|
28
|
+
- createdAt/updatedAt: Timestamps
|
|
29
|
+
- group: Workspace information
|
|
30
|
+
- count: Statistics (experiments, contributors, etc.)
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
response = self.api.list_projects()
|
|
34
|
+
# SwanLab API returns Project objects, convert to our Pydantic models
|
|
35
|
+
projects = []
|
|
36
|
+
for item in response.data:
|
|
37
|
+
if hasattr(item, "model_dump"):
|
|
38
|
+
# If it's already a Pydantic model
|
|
39
|
+
projects.append(Project(**item.model_dump()))
|
|
40
|
+
elif hasattr(item, "__dict__"):
|
|
41
|
+
# If it's a regular object with attributes
|
|
42
|
+
projects.append(Project(**item.__dict__))
|
|
43
|
+
else:
|
|
44
|
+
# If it's a dict
|
|
45
|
+
projects.append(Project(**item))
|
|
46
|
+
return projects
|
|
47
|
+
except Exception as e:
|
|
48
|
+
raise RuntimeError(f"Failed to list projects: {str(e)}") from e
|
|
49
|
+
|
|
50
|
+
async def get_project(self, project: str) -> Project:
|
|
51
|
+
"""
|
|
52
|
+
Get detailed information about a specific project by filtering from list.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
project: Project name or CUID
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Project object with detailed information
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
# SwanLab API doesn't have get_project, so we filter from list_projects
|
|
62
|
+
projects = await self.list_projects()
|
|
63
|
+
for proj in projects:
|
|
64
|
+
if proj.cuid == project or proj.name == project:
|
|
65
|
+
return proj
|
|
66
|
+
raise ValueError(f"Project '{project}' not found")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise RuntimeError(f"Failed to get project '{project}': {str(e)}") from e
|
|
69
|
+
|
|
70
|
+
async def delete_project(self, project: str) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Delete a project from the workspace.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
project: Project name or CUID to delete
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Success message
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
self.api.delete_project(project=project)
|
|
82
|
+
return f"Successfully deleted project '{project}'"
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise RuntimeError(f"Failed to delete project '{project}': {str(e)}") from e
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def register_project_tools(mcp: FastMCP, api: OpenApi) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Register project-related MCP tools.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
mcp: FastMCP server instance
|
|
93
|
+
api: SwanLab OpenApi instance
|
|
94
|
+
"""
|
|
95
|
+
project_tools = ProjectTools(api)
|
|
96
|
+
|
|
97
|
+
@mcp.tool(
|
|
98
|
+
name="swanlab_list_projects_in_a_workspace",
|
|
99
|
+
description="List all projects in the workspace.",
|
|
100
|
+
annotations=ToolAnnotations(
|
|
101
|
+
title="List all projects in the workspace.",
|
|
102
|
+
readOnlyHint=True,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
async def list_projects() -> List[Dict[str, Any]]:
|
|
106
|
+
"""
|
|
107
|
+
List all projects in the workspace.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of projects with details including name, description, visibility, statistics, etc.
|
|
111
|
+
"""
|
|
112
|
+
projects = await project_tools.list_projects()
|
|
113
|
+
return [project.model_dump() for project in projects]
|
|
114
|
+
|
|
115
|
+
@mcp.tool(
|
|
116
|
+
name="swanlab_get_a_specific_project",
|
|
117
|
+
description="Get detailed information about a specific project.",
|
|
118
|
+
annotations=ToolAnnotations(
|
|
119
|
+
title="Get detailed information about a specific project.",
|
|
120
|
+
readOnlyHint=True,
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
async def get_project(project: str) -> Dict[str, Any]:
|
|
124
|
+
"""
|
|
125
|
+
Get detailed information about a specific project.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
project: Project name or CUID
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Project details including metadata and statistics.
|
|
132
|
+
"""
|
|
133
|
+
project_obj = await project_tools.get_project(project)
|
|
134
|
+
return project_obj.model_dump()
|
|
135
|
+
|
|
136
|
+
@mcp.tool(
|
|
137
|
+
name="swanlab_delete_a_project",
|
|
138
|
+
description="Delete a project from the workspace. Warning: This action cannot be undone.",
|
|
139
|
+
annotations=ToolAnnotations(
|
|
140
|
+
title="Delete a project from the workspace. Warning: This action cannot be undone.",
|
|
141
|
+
readOnlyHint=False,
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
async def delete_project(project: str) -> str:
|
|
145
|
+
"""
|
|
146
|
+
Delete a project from the workspace.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
project: Project name or CUID to delete
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Success message confirming deletion.
|
|
153
|
+
"""
|
|
154
|
+
return await project_tools.delete_project(project)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Workspace related MCP tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
from mcp.types import ToolAnnotations
|
|
7
|
+
from swanlab import OpenApi
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkspaceTools:
|
|
11
|
+
"""SwanLab Workspace management tools."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, api: OpenApi):
|
|
14
|
+
self.api = api
|
|
15
|
+
|
|
16
|
+
async def list_workspaces(self) -> List[Dict[str, Any]]:
|
|
17
|
+
"""
|
|
18
|
+
List all workspaces accessible to the current user.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
List of workspace dictionaries containing:
|
|
22
|
+
- name: Workspace name
|
|
23
|
+
- username: Workspace username/identifier
|
|
24
|
+
- role: User's role in the workspace (OWNER, MEMBER, etc.)
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
response = self.api.list_workspaces()
|
|
28
|
+
return response.data
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise RuntimeError(f"Failed to list workspaces: {str(e)}") from e
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register_workspace_tools(mcp: FastMCP, api: OpenApi) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Register workspace-related MCP tools.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
mcp: FastMCP server instance
|
|
39
|
+
api: SwanLab OpenApi instance
|
|
40
|
+
"""
|
|
41
|
+
workspace_tools = WorkspaceTools(api)
|
|
42
|
+
|
|
43
|
+
@mcp.tool(
|
|
44
|
+
name="swanlab_list_workspaces",
|
|
45
|
+
description="List all workspaces accessible to the current user.",
|
|
46
|
+
annotations=ToolAnnotations(
|
|
47
|
+
title="List all workspaces accessible to the current user.",
|
|
48
|
+
readOnlyHint=True,
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
async def list_workspaces() -> List[Dict[str, Any]]:
|
|
52
|
+
"""
|
|
53
|
+
List all workspaces accessible to the current user.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of workspaces with their names, usernames, and user roles.
|
|
57
|
+
"""
|
|
58
|
+
return await workspace_tools.list_workspaces()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swanlab-mcp
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: MCP (Model Context Protocol) server support for SwanLab
|
|
5
|
+
Author-email: CaddiesNew <nexisato0810@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: fastmcp>=2.14.4
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.0.0
|
|
10
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
11
|
+
Requires-Dist: swanlab
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# SwanLab-MCP-Server
|
|
15
|
+
|
|
16
|
+
> A Model Context Protocol (MCP) server implementation for SwanLab, combining SwanLab-OpenAPI & FastMCP.
|
|
17
|
+
|
|
18
|
+
## ✨ Features
|
|
19
|
+
|
|
20
|
+
### Core Features
|
|
21
|
+
|
|
22
|
+
- **Workspace Management** - List and manage user-accessible workspaces
|
|
23
|
+
- **Project Management** - Create, retrieve, delete projects, and list project information
|
|
24
|
+
- **Experiment Management** - Create, retrieve, delete experiments, and retrieve experiment metrics and summaries
|
|
25
|
+
- **API Integration** - Provide complete platform access through SwanLab OpenAPI
|
|
26
|
+
|
|
27
|
+
### Tech Stack
|
|
28
|
+
|
|
29
|
+
- **Language**: Python 3.12+
|
|
30
|
+
- **Core Framework**: FastMCP (v2.14.4+)
|
|
31
|
+
- **API Client**: SwanLab SDK
|
|
32
|
+
- **Config Management**: Pydantic Settings
|
|
33
|
+
|
|
34
|
+
## 🚀 Quick Start
|
|
35
|
+
|
|
36
|
+
### ❗️Configuration
|
|
37
|
+
|
|
38
|
+
Add the following configuration to your relative mcp config list
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers":
|
|
43
|
+
...
|
|
44
|
+
{
|
|
45
|
+
"swanlab-mcp": {
|
|
46
|
+
"command": "uv",
|
|
47
|
+
"args": ["run", "swanlab_mcp", "--transport", "stdio"],
|
|
48
|
+
"env": {
|
|
49
|
+
"SWANLAB_API_KEY": "your_api_key_here"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For `Claude Code` Users, you can config like this:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
claude mcp add --env SWANLAB_API_KEY=<your_api_key> -- swanlab_mcp uv run swanlab_mcp --transport stdio
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Prerequisites
|
|
63
|
+
|
|
64
|
+
- Python >= 3.12
|
|
65
|
+
- SwanLab API Key (get it from [SwanLab](https://swanlab.cn))
|
|
66
|
+
|
|
67
|
+
### Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Using uv (recommended)
|
|
71
|
+
uv sync
|
|
72
|
+
|
|
73
|
+
# Or using pip
|
|
74
|
+
pip install -e .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Configuration
|
|
78
|
+
|
|
79
|
+
#### Environment Variables
|
|
80
|
+
|
|
81
|
+
Create a `.env` file and configure your API key:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cp .env.template .env
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Edit the `.env` file:
|
|
88
|
+
|
|
89
|
+
```env
|
|
90
|
+
SWANLAB_API_KEY=your_api_key_here
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Running
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Using stdio transport (default)
|
|
97
|
+
python -m swanlab_mcp
|
|
98
|
+
|
|
99
|
+
# Or using CLI
|
|
100
|
+
python -m swanlab_mcp --transport stdio
|
|
101
|
+
|
|
102
|
+
# Check version
|
|
103
|
+
python -m swanlab_mcp --version
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Usage
|
|
107
|
+
|
|
108
|
+
After configuration, restart Claude Desktop to interact with SwanLab via the MCP protocol.
|
|
109
|
+
|
|
110
|
+
Available Tools:
|
|
111
|
+
- `swanlab_list_workspaces` - List workspaces
|
|
112
|
+
- `swanlab_create_project` - Create project
|
|
113
|
+
- `swanlab_list_projects` - List projects
|
|
114
|
+
- `swanlab_create_experiment` - Create experiment
|
|
115
|
+
- `swanlab_list_experiments` - List experiments
|
|
116
|
+
- `swanlab_get_experiment` - Get experiment details
|
|
117
|
+
- `swanlab_delete_experiment` - Delete experiment
|
|
118
|
+
|
|
119
|
+
## 🛠️ Development
|
|
120
|
+
|
|
121
|
+
### Code Formatting
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Using Makefile
|
|
125
|
+
make format
|
|
126
|
+
|
|
127
|
+
# Or manually
|
|
128
|
+
uvx isort . --skip-gitignore
|
|
129
|
+
uvx ruff format . --quiet
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Lint Check
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
uvx ruff check .
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Pre-commit Hooks
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
bash scripts/install-hooks.sh
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 📚 References & Acknowledgements
|
|
145
|
+
|
|
146
|
+
- [SwanLab](https://github.com/SwanHubX/SwanLab)
|
|
147
|
+
- [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)
|
|
148
|
+
- [FastMCP v2](https://github.com/jlowin/fastmcp)
|
|
149
|
+
- [modelscope-mcp-server](https://github.com/modelscope/modelscope-mcp-server)
|
|
150
|
+
- [TrackIO-mcp-server](https://github.com/fcakyon/trackio-mcp)
|
|
151
|
+
- [Simple-Wandb-mcp-server](https://github.com/tsilva/simple-wandb-mcp-server)
|
|
152
|
+
|
|
153
|
+
## 📄 License
|
|
154
|
+
|
|
155
|
+
MIT License
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
swanlab_mcp/__init__.py,sha256=BE5zf9dh3Lx_QvUr_7vI9afv-C_2wBJaWWCiGrSrlz4,261
|
|
2
|
+
swanlab_mcp/__main__.py,sha256=5BjNuyet8AY-POwoF5rGt722rHQ7tJ0Vf0UFUfzzi-I,58
|
|
3
|
+
swanlab_mcp/_version.py,sha256=V9RIhJ1LjlFvbGmWoU5Vy5wjiVD_LKdlNdrh-NQ88Lw,73
|
|
4
|
+
swanlab_mcp/cli.py,sha256=7tJZv8f8ByJ0ME8wDcyTZiTkp2fPYjBk0TSGFRRbZTc,1710
|
|
5
|
+
swanlab_mcp/config.py,sha256=MgqztPVcEfIeRDv4AUmX9tpYTW42DXbVtgY1BW6BXHU,1603
|
|
6
|
+
swanlab_mcp/constants.py,sha256=0aDFnrfgN_DkY8IQY6w-0WV9hDTwvNGbCJH5BD_SXuc,172
|
|
7
|
+
swanlab_mcp/models.py,sha256=PbcCBYjoospoXX0HjaKtcEGq6YHQWO4oMZFrUqbln7c,967
|
|
8
|
+
swanlab_mcp/server.py,sha256=wYWvaezWi0hrYlLuYPRp8kRS1XfnrnXNAMxF0Sz-qDo,1172
|
|
9
|
+
swanlab_mcp/meta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
swanlab_mcp/meta/info.py,sha256=2w6_UhXUWWtrdSSlq_gTPdn6-Bbs7TBN24R_LO2GrEw,1482
|
|
11
|
+
swanlab_mcp/tools/__init__.py,sha256=AgPfbSmuI4SzryUzvqfl_wvByAV-A7zfV9zf3-lnQSU,438
|
|
12
|
+
swanlab_mcp/tools/experiment.py,sha256=jzwmfzwKrZctz-_hN8VCVvQJT6_EzQWoNh0JSlOp7JE,8630
|
|
13
|
+
swanlab_mcp/tools/project.py,sha256=sA_uKbM8M5xB4llIGa7drViLcDZF4t5qWYAK4fGNp4c,5115
|
|
14
|
+
swanlab_mcp/tools/workspace.py,sha256=hXhH6T1EoJkBy1pkbPEMhO_ZSqNYxy_fEoRCj0SGq9w,1726
|
|
15
|
+
swanlab_mcp-0.0.1.dist-info/METADATA,sha256=XZpv8xHFZ5axR7geXPI8Qvx1Ujz5-7_axXzsc9MvCnA,3356
|
|
16
|
+
swanlab_mcp-0.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
swanlab_mcp-0.0.1.dist-info/entry_points.txt,sha256=zKTWAZz7UcDiZdVTNkgtxe7ESf50fvKpBacGJGJrnS0,53
|
|
18
|
+
swanlab_mcp-0.0.1.dist-info/RECORD,,
|