""" Decorators for MCP Office Tools. Provides common patterns for error handling and Pydantic field resolution. """ from functools import wraps from typing import Any, Callable, TypeVar from pydantic.fields import FieldInfo from .validation import OfficeFileError T = TypeVar('T') def resolve_field_defaults(**defaults: Any) -> Callable: """ Decorator to resolve Pydantic Field defaults for direct function calls. When MCP tool methods are called directly (outside the MCP framework), Pydantic Field() defaults aren't automatically applied - parameters remain as FieldInfo objects. This decorator converts them to actual values. Usage: @mcp_tool(...) @resolve_field_defaults(sheet_names=[], include_statistics=True) async def analyze_excel_data(self, file_path: str, sheet_names: list = Field(...)): # sheet_names will be [] if called directly without argument ... Args: **defaults: Mapping of parameter names to their default values Returns: Decorated async function with resolved defaults """ import inspect def decorator(func: Callable[..., T]) -> Callable[..., T]: sig = inspect.signature(func) param_names = list(sig.parameters.keys()) @wraps(func) async def wrapper(self, *args, **kwargs): # Build a dict of all parameter values (combining args and kwargs) # Skip 'self' which is the first parameter bound_args = {} for i, arg in enumerate(args): if i + 1 < len(param_names): # +1 to skip 'self' bound_args[param_names[i + 1]] = arg # Merge with kwargs bound_args.update(kwargs) # For parameters not provided, check if default is FieldInfo for param_name, default_value in defaults.items(): if param_name not in bound_args: # Parameter using its default value - set to our resolved default kwargs[param_name] = default_value elif isinstance(bound_args[param_name], FieldInfo): # Explicitly passed FieldInfo - resolve it kwargs[param_name] = default_value return await func(self, *args, **kwargs) return wrapper return decorator def handle_office_errors(operation_name: str) -> Callable: """ Decorator for consistent error handling in Office document operations. Wraps async functions to catch exceptions and re-raise them as OfficeFileError with a descriptive message. Already-raised OfficeFileError exceptions are passed through unchanged. Usage: @mcp_tool(...) @handle_office_errors("Excel analysis") async def analyze_excel_data(self, file_path: str): # Any exception becomes: OfficeFileError("Excel analysis failed: ...") ... Args: operation_name: Human-readable name for the operation (used in error messages) Returns: Decorated async function with error handling """ def decorator(func: Callable[..., T]) -> Callable[..., T]: @wraps(func) async def wrapper(*args, **kwargs): try: return await func(*args, **kwargs) except OfficeFileError: # Re-raise our custom errors unchanged raise except Exception as e: raise OfficeFileError(f"{operation_name} failed: {str(e)}") return wrapper return decorator