Skyvern/skyvern/cli/mcp_tools/folder.py
2026-03-12 15:12:03 -07:00

212 lines
7.7 KiB
Python

"""Skyvern MCP folder tools — CRUD for organizing workflows."""
from __future__ import annotations
from typing import Annotated, Any
from pydantic import Field
from skyvern.client.errors import NotFoundError
from ._common import ErrorCode, Timer, make_error, make_result
from ._session import get_skyvern
from ._validation import validate_folder_id
def _serialize_folder(folder: Any) -> dict[str, Any]:
return {
"folder_id": folder.folder_id,
"organization_id": folder.organization_id,
"title": folder.title,
"description": folder.description,
"workflow_count": folder.workflow_count,
"created_at": folder.created_at.isoformat() if folder.created_at else None,
"modified_at": folder.modified_at.isoformat() if folder.modified_at else None,
}
def _folder_not_found(action: str, folder_id: str, timer: Timer) -> dict[str, Any]:
return make_result(
action,
ok=False,
timing_ms=timer.timing_ms,
error=make_error(
ErrorCode.INVALID_INPUT,
f"Folder {folder_id!r} not found",
"Use skyvern_folder_list to find valid folder IDs.",
),
)
async def skyvern_folder_list(
page: Annotated[int, Field(description="Page number (1-based)", ge=1)] = 1,
page_size: Annotated[int, Field(description="Results per page", ge=1, le=500)] = 50,
search: Annotated[str | None, "Search folders by title or description"] = None,
) -> dict[str, Any]:
"""List available folders. Use when you need to discover folder IDs before assigning workflows."""
skyvern = get_skyvern()
with Timer() as timer:
try:
folders = await skyvern.get_folders(page=page, page_size=page_size, search=search)
timer.mark("sdk")
except Exception as e:
return make_result(
"skyvern_folder_list",
ok=False,
timing_ms=timer.timing_ms,
error=make_error(ErrorCode.API_ERROR, str(e), "Check your API key and Skyvern connection."),
)
return make_result(
"skyvern_folder_list",
data={
"folders": [_serialize_folder(folder) for folder in folders],
"page": page,
"page_size": page_size,
"count": len(folders),
# Heuristic until the API exposes pagination metadata.
"has_more": len(folders) == page_size,
},
timing_ms=timer.timing_ms,
)
async def skyvern_folder_create(
title: Annotated[str, Field(description="Folder title", min_length=1, max_length=255)],
description: Annotated[str | None, Field(description="Optional folder description")] = None,
) -> dict[str, Any]:
"""Create a new folder for organizing workflows."""
skyvern = get_skyvern()
with Timer() as timer:
try:
folder = await skyvern.create_folder(title=title, description=description)
timer.mark("sdk")
except Exception as e:
return make_result(
"skyvern_folder_create",
ok=False,
timing_ms=timer.timing_ms,
error=make_error(ErrorCode.API_ERROR, str(e), "Check the folder title and your permissions."),
)
data = _serialize_folder(folder)
data["sdk_equivalent"] = f"await skyvern.create_folder(title={title!r}, description={description!r})"
return make_result("skyvern_folder_create", data=data, timing_ms=timer.timing_ms)
async def skyvern_folder_get(
folder_id: Annotated[str, "Folder ID (starts with fld_)"],
) -> dict[str, Any]:
"""Get details for a specific folder."""
if err := validate_folder_id(folder_id, "skyvern_folder_get"):
return err
skyvern = get_skyvern()
with Timer() as timer:
try:
folder = await skyvern.get_folder(folder_id)
timer.mark("sdk")
except NotFoundError:
return _folder_not_found("skyvern_folder_get", folder_id, timer)
except Exception as e:
return make_result(
"skyvern_folder_get",
ok=False,
timing_ms=timer.timing_ms,
error=make_error(ErrorCode.API_ERROR, str(e), "Check your API key and Skyvern connection."),
)
data = _serialize_folder(folder)
data["sdk_equivalent"] = f"await skyvern.get_folder({folder_id!r})"
return make_result("skyvern_folder_get", data=data, timing_ms=timer.timing_ms)
async def skyvern_folder_update(
folder_id: Annotated[str, "Folder ID (starts with fld_)"],
title: Annotated[str | None, Field(description="New folder title", min_length=1, max_length=255)] = None,
description: Annotated[str | None, Field(description="New folder description")] = None,
) -> dict[str, Any]:
"""Update a folder's title or description."""
if err := validate_folder_id(folder_id, "skyvern_folder_update"):
return err
if title is None and description is None:
return make_result(
"skyvern_folder_update",
ok=False,
error=make_error(
ErrorCode.INVALID_INPUT,
"At least one of title or description must be provided",
"Provide title, description, or both.",
),
)
skyvern = get_skyvern()
with Timer() as timer:
try:
folder = await skyvern.update_folder(folder_id, title=title, description=description)
timer.mark("sdk")
except NotFoundError:
return _folder_not_found("skyvern_folder_update", folder_id, timer)
except Exception as e:
return make_result(
"skyvern_folder_update",
ok=False,
timing_ms=timer.timing_ms,
error=make_error(ErrorCode.API_ERROR, str(e), "Check the folder ID and updated values."),
)
data = _serialize_folder(folder)
data["sdk_equivalent"] = f"await skyvern.update_folder({folder_id!r}, title={title!r}, description={description!r})"
return make_result("skyvern_folder_update", data=data, timing_ms=timer.timing_ms)
async def skyvern_folder_delete(
folder_id: Annotated[str, "Folder ID (starts with fld_)"],
delete_workflows: Annotated[
bool,
"Also delete workflows in the folder instead of just removing their folder assignment",
] = False,
force: Annotated[bool, "Must be true to confirm deletion — prevents accidental deletes"] = False,
) -> dict[str, Any]:
"""Delete a folder. By default workflows remain and are removed from the folder."""
if err := validate_folder_id(folder_id, "skyvern_folder_delete"):
return err
if not force:
return make_result(
"skyvern_folder_delete",
ok=False,
error=make_error(
ErrorCode.INVALID_INPUT,
f"Deletion of folder {folder_id!r} requires confirmation",
"Set force=true to confirm deletion.",
),
)
skyvern = get_skyvern()
with Timer() as timer:
try:
result = await skyvern.delete_folder(folder_id, delete_workflows=delete_workflows)
timer.mark("sdk")
except NotFoundError:
return _folder_not_found("skyvern_folder_delete", folder_id, timer)
except Exception as e:
return make_result(
"skyvern_folder_delete",
ok=False,
timing_ms=timer.timing_ms,
error=make_error(ErrorCode.API_ERROR, str(e), "Check the folder ID and your permissions."),
)
data = result if isinstance(result, dict) else {}
data["sdk_equivalent"] = f"await skyvern.delete_folder({folder_id!r}, delete_workflows={delete_workflows!r})"
return make_result("skyvern_folder_delete", data=data, timing_ms=timer.timing_ms)