file_service.pyโข6.45 kB
"""File management service."""
from typing import Any, Dict, List
from skill_mcp.core.config import MAX_FILE_SIZE, SKILL_METADATA_FILE, SKILLS_DIR
from skill_mcp.core.exceptions import (
FileNotFoundError,
FileTooBigError,
ProtectedFileError,
SkillNotFoundError,
)
from skill_mcp.utils.path_utils import validate_path
class FileService:
"""Service for managing skill files."""
@staticmethod
def list_skill_files(skill_name: str) -> List[Dict[str, Any]]:
"""
List all files in a skill directory recursively.
Args:
skill_name: Name of the skill
Returns:
List of file information dictionaries
Raises:
SkillNotFoundError: If skill doesn't exist
"""
skill_dir = SKILLS_DIR / skill_name
if not skill_dir.exists():
raise SkillNotFoundError(f"Skill '{skill_name}' does not exist")
if not skill_dir.is_dir():
raise SkillNotFoundError(f"'{skill_name}' is not a directory")
files = []
for item in sorted(skill_dir.rglob("*")):
if item.is_file():
rel_path = item.relative_to(skill_dir)
stat = item.stat()
files.append(
{
"path": str(rel_path),
"size": stat.st_size,
"type": "file",
"modified": stat.st_mtime,
}
)
return files
@staticmethod
def read_file(skill_name: str, file_path: str) -> str:
"""
Read content of a skill file.
Args:
skill_name: Name of the skill
file_path: Relative path to the file
Returns:
File content as string
Raises:
InvalidPathError: If path is invalid
FileNotFoundError: If file doesn't exist
FileTooBigError: If file exceeds size limit
"""
full_path = validate_path(skill_name, file_path)
if not full_path.exists():
raise FileNotFoundError(f"File '{file_path}' does not exist in skill '{skill_name}'")
if not full_path.is_file():
raise FileNotFoundError(f"'{file_path}' is not a file")
file_size = full_path.stat().st_size
if file_size > MAX_FILE_SIZE:
raise FileTooBigError(
f"File too large ({file_size / 1024:.1f} KB). "
f"Maximum size is {MAX_FILE_SIZE / 1024:.1f} KB."
)
return full_path.read_text()
@staticmethod
def create_file(skill_name: str, file_path: str, content: str) -> None:
"""
Create a new skill file.
Args:
skill_name: Name of the skill
file_path: Relative path for the new file
content: File content
Raises:
SkillNotFoundError: If skill doesn't exist
InvalidPathError: If path is invalid
FileNotFoundError: If file already exists
"""
# Check if skill exists first
skill_dir = SKILLS_DIR / skill_name
if not skill_dir.exists():
raise SkillNotFoundError(f"Skill '{skill_name}' does not exist")
if not skill_dir.is_dir():
raise SkillNotFoundError(f"'{skill_name}' is not a directory")
full_path = validate_path(skill_name, file_path)
if full_path.exists():
raise FileNotFoundError(
f"File '{file_path}' already exists in skill '{skill_name}'. "
"Use update to modify it."
)
# Create parent directories if needed
full_path.parent.mkdir(parents=True, exist_ok=True)
# Write content
full_path.write_text(content)
@staticmethod
def update_file(skill_name: str, file_path: str, content: str) -> None:
"""
Update an existing skill file.
Args:
skill_name: Name of the skill
file_path: Relative path to the file
content: New file content
Raises:
SkillNotFoundError: If skill doesn't exist
InvalidPathError: If path is invalid
FileNotFoundError: If file doesn't exist
"""
# Check if skill exists first
skill_dir = SKILLS_DIR / skill_name
if not skill_dir.exists():
raise SkillNotFoundError(f"Skill '{skill_name}' does not exist")
if not skill_dir.is_dir():
raise SkillNotFoundError(f"'{skill_name}' is not a directory")
full_path = validate_path(skill_name, file_path)
if not full_path.exists():
raise FileNotFoundError(
f"File '{file_path}' does not exist in skill '{skill_name}'. "
"Use create to create it."
)
if not full_path.is_file():
raise FileNotFoundError(f"'{file_path}' is not a file")
full_path.write_text(content)
@staticmethod
def delete_file(skill_name: str, file_path: str) -> None:
"""
Delete a skill file.
Args:
skill_name: Name of the skill
file_path: Relative path to the file
Raises:
SkillNotFoundError: If skill doesn't exist
InvalidPathError: If path is invalid
FileNotFoundError: If file doesn't exist
ProtectedFileError: If attempting to delete a protected file (SKILL.md)
"""
# Prevent deletion of SKILL.md
if file_path == SKILL_METADATA_FILE or file_path.endswith(f"/{SKILL_METADATA_FILE}"):
raise ProtectedFileError(
f"Cannot delete '{SKILL_METADATA_FILE}'. This file is protected and required for skill metadata."
)
# Check if skill exists first
skill_dir = SKILLS_DIR / skill_name
if not skill_dir.exists():
raise SkillNotFoundError(f"Skill '{skill_name}' does not exist")
if not skill_dir.is_dir():
raise SkillNotFoundError(f"'{skill_name}' is not a directory")
full_path = validate_path(skill_name, file_path)
if not full_path.exists():
raise FileNotFoundError(f"File '{file_path}' does not exist in skill '{skill_name}'")
if not full_path.is_file():
raise FileNotFoundError(f"'{file_path}' is not a file. Cannot delete directories.")
full_path.unlink()