Initial implementation
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
"""MCP tools for SYNO.Docker.Project: list, status, start, stop, redeploy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from mcp_synology_container.config import AppConfig
|
||||
from mcp_synology_container.dsm_client import DsmClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> None:
|
||||
"""Register all project management tools with the MCP server."""
|
||||
|
||||
@mcp.tool()
|
||||
async def list_projects() -> str:
|
||||
"""List all Container Manager projects with their current status.
|
||||
|
||||
Returns a formatted table of projects including name, status, path,
|
||||
and container count.
|
||||
"""
|
||||
try:
|
||||
data = await client.request("SYNO.Docker.Project", "list")
|
||||
except Exception as e:
|
||||
return f"Error listing projects: {e}"
|
||||
|
||||
projects: dict[str, Any] = data if isinstance(data, dict) else {}
|
||||
if not projects:
|
||||
return "No projects found."
|
||||
|
||||
lines = ["Projects:", ""]
|
||||
for project_id, proj in sorted(projects.items(), key=lambda x: x[1].get("name", "")):
|
||||
name = proj.get("name", "?")
|
||||
status = proj.get("status", "?")
|
||||
path = proj.get("path", "?")
|
||||
container_count = len(proj.get("containerIds", []))
|
||||
lines.append(f" {name}")
|
||||
lines.append(f" Status: {status}")
|
||||
lines.append(f" Path: {path}")
|
||||
lines.append(f" Containers: {container_count}")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
@mcp.tool()
|
||||
async def get_project_status(project_name: str) -> str:
|
||||
"""Get detailed status of a specific project.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project to inspect.
|
||||
"""
|
||||
project = await _find_project(client, project_name)
|
||||
if project is None:
|
||||
return f"Project '{project_name}' not found."
|
||||
|
||||
return _format_project_detail(project)
|
||||
|
||||
@mcp.tool()
|
||||
async def start_project(project_name: str) -> str:
|
||||
"""Start a Container Manager project.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project to start.
|
||||
"""
|
||||
project = await _find_project(client, project_name)
|
||||
if project is None:
|
||||
return f"Project '{project_name}' not found."
|
||||
|
||||
project_id = project.get("id", "")
|
||||
try:
|
||||
await client.request(
|
||||
"SYNO.Docker.Project",
|
||||
"start",
|
||||
params={"id": project_id},
|
||||
)
|
||||
return f"Project '{project_name}' started successfully."
|
||||
except Exception as e:
|
||||
return f"Error starting project '{project_name}': {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def stop_project(project_name: str, confirmed: bool = False) -> str:
|
||||
"""Stop a running Container Manager project.
|
||||
|
||||
This operation stops all containers in the project.
|
||||
Requires confirmation before executing.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project to stop.
|
||||
confirmed: Must be True to proceed. Set to True to confirm the stop operation.
|
||||
"""
|
||||
if not confirmed:
|
||||
return (
|
||||
f"Stopping project '{project_name}' will halt all its containers.\n"
|
||||
f"Call this tool again with confirmed=True to proceed."
|
||||
)
|
||||
|
||||
project = await _find_project(client, project_name)
|
||||
if project is None:
|
||||
return f"Project '{project_name}' not found."
|
||||
|
||||
project_id = project.get("id", "")
|
||||
try:
|
||||
await client.request(
|
||||
"SYNO.Docker.Project",
|
||||
"stop",
|
||||
params={"id": project_id},
|
||||
)
|
||||
return f"Project '{project_name}' stopped successfully."
|
||||
except Exception as e:
|
||||
return f"Error stopping project '{project_name}': {e}"
|
||||
|
||||
@mcp.tool()
|
||||
async def redeploy_project(project_name: str, confirmed: bool = False) -> str:
|
||||
"""Redeploy a project: pull latest images, stop, and restart.
|
||||
|
||||
This operation will briefly take the project offline.
|
||||
Requires confirmation before executing.
|
||||
|
||||
Args:
|
||||
project_name: Name of the project to redeploy.
|
||||
confirmed: Must be True to proceed. Set to True to confirm the redeploy.
|
||||
"""
|
||||
if not confirmed:
|
||||
return (
|
||||
f"Redeploying project '{project_name}' will:\n"
|
||||
f" 1. Pull latest images\n"
|
||||
f" 2. Stop all containers\n"
|
||||
f" 3. Restart with new images\n\n"
|
||||
f"Call this tool again with confirmed=True to proceed."
|
||||
)
|
||||
|
||||
project = await _find_project(client, project_name)
|
||||
if project is None:
|
||||
return f"Project '{project_name}' not found."
|
||||
|
||||
project_id = project.get("id", "")
|
||||
results = []
|
||||
|
||||
try:
|
||||
# Step 1: Pull latest images via build (triggers compose pull)
|
||||
results.append("Step 1/3: Pulling latest images...")
|
||||
try:
|
||||
await client.request(
|
||||
"SYNO.Docker.Project",
|
||||
"build",
|
||||
params={"id": project_id, "force": "true"},
|
||||
)
|
||||
results.append(" Images pulled.")
|
||||
except Exception as e:
|
||||
results.append(f" Warning: pull step failed ({e}), continuing with restart.")
|
||||
|
||||
# Step 2: Stop the project
|
||||
results.append("Step 2/3: Stopping project...")
|
||||
await client.request(
|
||||
"SYNO.Docker.Project",
|
||||
"stop",
|
||||
params={"id": project_id},
|
||||
)
|
||||
results.append(" Project stopped.")
|
||||
|
||||
# Step 3: Start the project
|
||||
results.append("Step 3/3: Starting project...")
|
||||
await client.request(
|
||||
"SYNO.Docker.Project",
|
||||
"start",
|
||||
params={"id": project_id},
|
||||
)
|
||||
results.append(" Project started.")
|
||||
|
||||
results.append(f"\nProject '{project_name}' redeployed successfully.")
|
||||
|
||||
except Exception as e:
|
||||
results.append(f"Error during redeploy: {e}")
|
||||
|
||||
return "\n".join(results)
|
||||
|
||||
|
||||
async def _find_project(client: DsmClient, name: str) -> dict[str, Any] | None:
|
||||
"""Find a project by name from the list.
|
||||
|
||||
Args:
|
||||
client: DsmClient instance.
|
||||
name: Project name to search for.
|
||||
|
||||
Returns:
|
||||
Project dict if found, None otherwise.
|
||||
"""
|
||||
try:
|
||||
data = await client.request("SYNO.Docker.Project", "list")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
projects: dict[str, Any] = data if isinstance(data, dict) else {}
|
||||
for project in projects.values():
|
||||
if project.get("name") == name:
|
||||
return dict(project)
|
||||
return None
|
||||
|
||||
|
||||
def _format_project_detail(project: dict[str, Any]) -> str:
|
||||
"""Format project details as human-readable text."""
|
||||
lines = [
|
||||
f"Project: {project.get('name', '?')}",
|
||||
f" ID: {project.get('id', '?')}",
|
||||
f" Status: {project.get('status', '?')}",
|
||||
f" Path: {project.get('path', '?')}",
|
||||
f" Share path: {project.get('share_path', '?')}",
|
||||
f" Created: {project.get('created_at', '?')}",
|
||||
f" Updated: {project.get('updated_at', '?')}",
|
||||
]
|
||||
|
||||
container_ids = project.get("containerIds", [])
|
||||
lines.append(f" Containers: {len(container_ids)}")
|
||||
for cid in container_ids:
|
||||
lines.append(f" - {cid[:12]}")
|
||||
|
||||
services = project.get("services") or []
|
||||
if services:
|
||||
lines.append(f" Services: {len(services)}")
|
||||
for svc in services:
|
||||
lines.append(f" - {svc.get('display_name', '?')}")
|
||||
|
||||
return "\n".join(lines)
|
||||
Reference in New Issue
Block a user