feat: v0.3.2 — delete_project tool
Closes the project lifecycle (create → start/stop/redeploy → delete). The tool calls SYNO.Docker.Project/delete with the UUID JSON-encoded as the `id` parameter (per DSM convention) and removes only the Container Manager registration — the project folder and compose file remain on the NAS. This mirrors DSM's own "Delete project" behaviour, not a bug; the success message states the folder was preserved so the user is not surprised. Safety: - Project-name validation runs before any I/O. - A `_find_project` pre-flight returns "not found" with a clear message rather than letting DSM reject an unknown UUID. - No automatic stop. If the project is RUNNING and DSM rejects the delete, the response tells the user to `stop_project` first rather than silently halting containers under the guise of a "delete" call. - Requires confirmed=True; preview shows name, UUID, status, full path, and share path so the user can verify before deleting. Tests cover preview-only, not-found, invalid-name, happy path (verifies the UUID is JSON-encoded in the delete call), and the running-project rejection path that surfaces the stop_project hint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -372,6 +372,61 @@ def register_projects(mcp: FastMCP, config: AppConfig, client: DsmClient) -> Non
|
||||
|
||||
return "\n".join(results)
|
||||
|
||||
@mcp.tool()
|
||||
async def delete_project(project_name: str, confirmed: bool = False):
|
||||
"""Remove a project registration from Container Manager. Requires confirmed=True."""
|
||||
# Lazy import: see create_project for why this avoids a circular dep.
|
||||
from mcp_synology_container.dsm_client import SynologyError
|
||||
from mcp_synology_container.modules.compose import _validate_project_name
|
||||
|
||||
if (err := _validate_project_name(project_name)) is not None:
|
||||
return err
|
||||
|
||||
project = await _find_project(client, project_name)
|
||||
if project is None:
|
||||
return f"Project '{project_name}' not found."
|
||||
|
||||
project_id = project.get("id", "")
|
||||
status = (project.get("status") or "?").upper()
|
||||
path = project.get("path", "?")
|
||||
share_path = project.get("share_path", "?")
|
||||
|
||||
if not confirmed:
|
||||
return (
|
||||
f"About to delete project registration '{project_name}':\n"
|
||||
f" UUID: {project_id}\n"
|
||||
f" Status: {status}\n"
|
||||
f" Path: {path}\n"
|
||||
f" Share path: {share_path}\n\n"
|
||||
f"Note: only the Container Manager registration is removed — "
|
||||
f"the folder and compose file will remain on the NAS.\n\n"
|
||||
f"Call this tool again with confirmed=True to proceed."
|
||||
)
|
||||
|
||||
try:
|
||||
await client.request(
|
||||
"SYNO.Docker.Project",
|
||||
"delete",
|
||||
version=1,
|
||||
params={"id": json.dumps(project_id)},
|
||||
)
|
||||
except SynologyError as e:
|
||||
# DSM refuses to delete a running project. We deliberately do NOT
|
||||
# auto-stop — that would be too destructive for a delete tool —
|
||||
# but we tell the user how to proceed.
|
||||
if status == "RUNNING":
|
||||
return (
|
||||
f"Cannot delete project '{project_name}' while it is running ({e}).\n"
|
||||
f"Stop the project first with stop_project."
|
||||
)
|
||||
return f"Error deleting project '{project_name}': {e}"
|
||||
|
||||
return (
|
||||
f"Project '{project_name}' deleted (registration removed).\n"
|
||||
f"Note: the project folder {share_path} was NOT deleted — "
|
||||
f"its files remain on the NAS."
|
||||
)
|
||||
|
||||
|
||||
async def _find_project(client: DsmClient, name: str) -> dict[str, Any] | None:
|
||||
"""Find a project by name from the list.
|
||||
|
||||
Reference in New Issue
Block a user