Skip to main content
Glama
calendar.py38.9 kB
import datetime as dt import logging from typing import Optional from mcp.server.fastmcp import Context, FastMCP from nextcloud_mcp_server.auth import require_scopes from nextcloud_mcp_server.context import get_client from nextcloud_mcp_server.models.calendar import ( Calendar, ListCalendarsResponse, ListTodosResponse, Todo, ) logger = logging.getLogger(__name__) def configure_calendar_tools(mcp: FastMCP): # Calendar tools @mcp.tool() @require_scopes("calendar:read") async def nc_calendar_list_calendars(ctx: Context) -> ListCalendarsResponse: """List all available calendars for the user""" client = await get_client(ctx) calendars_data = await client.calendar.list_calendars() calendars = [Calendar(**cal_data) for cal_data in calendars_data] return ListCalendarsResponse(calendars=calendars, total_count=len(calendars)) @mcp.tool() @require_scopes("calendar:write") async def nc_calendar_create_event( calendar_name: str, title: str, start_datetime: str, ctx: Context, end_datetime: str = "", all_day: bool = False, description: str = "", location: str = "", categories: str = "", recurring: bool = False, recurrence_rule: str = "", recurrence_end_date: str = "", reminder_minutes: int = 15, reminder_email: bool = False, status: str = "CONFIRMED", priority: int = 5, privacy: str = "PUBLIC", attendees: str = "", url: str = "", color: str = "", ): """Create a comprehensive calendar event with full feature support Args: calendar_name: Name of the calendar to create the event in title: Event title start_datetime: ISO format: "2025-01-15T14:00:00" or "2025-01-15" for all-day ctx: MCP context end_datetime: ISO format end time, empty for all-day events all_day: Whether this is an all-day event description: Event description/details location: Event location categories: Comma-separated categories (e.g., "work,meeting") recurring: Whether this is a recurring event recurrence_rule: RFC5545 RRULE (e.g., "FREQ=WEEKLY;BYDAY=MO,WE,FR") recurrence_end_date: When to stop recurring reminder_minutes: Minutes before event to send reminder reminder_email: Whether to send email notification status: Event status: CONFIRMED, TENTATIVE, or CANCELLED priority: Priority level 1-9 (1=highest, 9=lowest, 5=normal) privacy: Privacy level: PUBLIC, PRIVATE, or CONFIDENTIAL attendees: Comma-separated email addresses url: Related URL for the event color: Event color (hex or name) Returns: Dict with event creation result """ client = await get_client(ctx) event_data = { "title": title, "start_datetime": start_datetime, "end_datetime": end_datetime, "all_day": all_day, "description": description, "location": location, "categories": categories, "recurring": recurring, "recurrence_rule": recurrence_rule, "recurrence_end_date": recurrence_end_date, "reminder_minutes": reminder_minutes, "reminder_email": reminder_email, "status": status, "priority": priority, "privacy": privacy, "attendees": attendees, "url": url, "color": color, } return await client.calendar.create_event(calendar_name, event_data) @mcp.tool() @require_scopes("calendar:read") async def nc_calendar_list_events( calendar_name: str, ctx: Context, start_date: str = "", end_date: str = "", limit: int = 50, min_attendees: Optional[int] = None, min_duration_minutes: Optional[int] = None, categories: Optional[str] = None, status: Optional[str] = None, title_contains: Optional[str] = None, location_contains: Optional[str] = None, search_all_calendars: bool = False, ): """List events in a calendar (or all calendars) within date range with advanced filtering. Args: calendar_name: Name of the calendar to search. Ignored if search_all_calendars=True. ctx: MCP context start_date: Start date for search (YYYY-MM-DD format, e.g., "2025-01-01") end_date: End date for search (YYYY-MM-DD format, e.g., "2025-01-31") limit: Maximum number of events to return min_attendees: Filter events with at least this many attendees min_duration_minutes: Filter events with at least this duration categories: Filter events containing any of these categories (comma-separated, e.g., "work,meeting") status: Filter events by status (CONFIRMED, TENTATIVE, or CANCELLED) title_contains: Filter events where title contains this text location_contains: Filter events where location contains this text search_all_calendars: If True, search across all calendars instead of just one Returns: List of events matching the filters """ client = await get_client(ctx) # Convert YYYY-MM-DD format dates to datetime objects start_datetime = None end_datetime = None if start_date: try: start_datetime = dt.datetime.strptime(start_date, "%Y-%m-%d") except ValueError: # If parsing fails, try to parse as ISO format try: start_datetime = dt.datetime.fromisoformat(start_date) except ValueError: logger.warning(f"Invalid start_date format: {start_date}") if end_date: try: # For end date, set to end of day (23:59:59) end_datetime = dt.datetime.strptime(end_date, "%Y-%m-%d").replace( hour=23, minute=59, second=59 ) except ValueError: # If parsing fails, try to parse as ISO format try: end_datetime = dt.datetime.fromisoformat(end_date) except ValueError: logger.warning(f"Invalid end_date format: {end_date}") # Build filters dictionary filters = {} if min_attendees is not None: filters["min_attendees"] = min_attendees if min_duration_minutes is not None: filters["min_duration_minutes"] = min_duration_minutes if categories is not None: filters["categories"] = [cat.strip() for cat in categories.split(",")] if status is not None: filters["status"] = status if title_contains is not None: filters["title_contains"] = title_contains if location_contains is not None: filters["location_contains"] = location_contains if search_all_calendars: # Search across all calendars with filters events = await client.calendar.search_events_across_calendars( start_datetime=start_datetime, end_datetime=end_datetime, filters=filters if filters else None, ) return events[:limit] else: # Search in specific calendar events = await client.calendar.get_calendar_events( calendar_name=calendar_name, start_datetime=start_datetime, end_datetime=end_datetime, limit=limit, ) # Apply filters if provided if filters: events = client.calendar._apply_event_filters(events, filters) return events @mcp.tool() @require_scopes("calendar:read") async def nc_calendar_get_event( calendar_name: str, event_uid: str, ctx: Context, ): """Get detailed information about a specific event""" client = await get_client(ctx) event_data, etag = await client.calendar.get_event(calendar_name, event_uid) return event_data @mcp.tool() @require_scopes("calendar:write") async def nc_calendar_update_event( calendar_name: str, event_uid: str, ctx: Context, # All the same parameters as create_event but optional title: str | None = None, start_datetime: str | None = None, end_datetime: str | None = None, all_day: bool | None = None, description: str | None = None, location: str | None = None, categories: str | None = None, # Recurrence updates recurring: bool | None = None, recurrence_rule: str | None = None, # Notification updates reminder_minutes: int | None = None, reminder_email: bool | None = None, # Event property updates status: str | None = None, priority: int | None = None, privacy: str | None = None, attendees: str | None = None, url: str | None = None, color: str | None = None, etag: str = "", ): """Update any aspect of an existing event""" client = await get_client(ctx) # Build update data with only non-None values event_data = {} if title is not None: event_data["title"] = title if start_datetime is not None: event_data["start_datetime"] = start_datetime if end_datetime is not None: event_data["end_datetime"] = end_datetime if all_day is not None: event_data["all_day"] = all_day if description is not None: event_data["description"] = description if location is not None: event_data["location"] = location if categories is not None: event_data["categories"] = categories if recurring is not None: event_data["recurring"] = recurring if recurrence_rule is not None: event_data["recurrence_rule"] = recurrence_rule if reminder_minutes is not None: event_data["reminder_minutes"] = reminder_minutes if reminder_email is not None: event_data["reminder_email"] = reminder_email if status is not None: event_data["status"] = status if priority is not None: event_data["priority"] = priority if privacy is not None: event_data["privacy"] = privacy if attendees is not None: event_data["attendees"] = attendees if url is not None: event_data["url"] = url if color is not None: event_data["color"] = color return await client.calendar.update_event( calendar_name, event_uid, event_data, etag ) @mcp.tool() @require_scopes("calendar:write") async def nc_calendar_delete_event( calendar_name: str, event_uid: str, ctx: Context, ): """Delete a calendar event""" client = await get_client(ctx) return await client.calendar.delete_event(calendar_name, event_uid) @mcp.tool() @require_scopes("calendar:write") async def nc_calendar_create_meeting( title: str, date: str, time: str, ctx: Context, duration_minutes: int = 60, calendar_name: str = "personal", attendees: str = "", location: str = "", description: str = "", reminder_minutes: int = 15, ): """Quick meeting creation with smart defaults This is a convenience function for creating events with common meeting defaults. It automatically: - Calculates end time based on duration - Sets status to CONFIRMED - Adds a reminder - Uses simpler date/time inputs instead of full ISO format For full control over all event properties, use nc_calendar_create_event instead. Args: title: Meeting title date: Meeting date (YYYY-MM-DD format, e.g., "2025-01-15") time: Meeting start time (HH:MM format, e.g., "14:00") ctx: MCP context duration_minutes: Meeting duration in minutes (default: 60) calendar_name: Calendar to create the meeting in (default: "personal") attendees: Comma-separated email addresses of attendees location: Meeting location description: Meeting description/agenda reminder_minutes: Minutes before meeting to send reminder (default: 15) Returns: Dict with meeting creation result """ client = await get_client(ctx) # Combine date and time for start_datetime start_datetime = f"{date}T{time}:00" # Calculate end_datetime start_dt = dt.datetime.fromisoformat(start_datetime) end_dt = start_dt + dt.timedelta(minutes=duration_minutes) end_datetime = end_dt.isoformat() event_data = { "title": title, "start_datetime": start_datetime, "end_datetime": end_datetime, "all_day": False, "description": description, "location": location, "attendees": attendees, "reminder_minutes": reminder_minutes, "status": "CONFIRMED", "priority": 5, "privacy": "PUBLIC", } return await client.calendar.create_event(calendar_name, event_data) @mcp.tool() @require_scopes("calendar:read") async def nc_calendar_get_upcoming_events( ctx: Context, calendar_name: str = "", # Empty = all calendars days_ahead: int = 7, limit: int = 10, ): """Get upcoming events in next N days""" client = await get_client(ctx) now = dt.datetime.now() end_datetime = now + dt.timedelta(days=days_ahead) if calendar_name: # Get events from specific calendar return await client.calendar.get_calendar_events( calendar_name=calendar_name, start_datetime=now, end_datetime=end_datetime, limit=limit, ) else: # Get events from all calendars all_calendars = await client.calendar.list_calendars() all_events = [] for calendar in all_calendars: try: events = await client.calendar.get_calendar_events( calendar_name=calendar["name"], start_datetime=now, end_datetime=end_datetime, limit=limit, ) # Add calendar info to each event for event in events: event["calendar_name"] = calendar["name"] event["calendar_display_name"] = calendar["display_name"] all_events.extend(events) except Exception as e: logger.warning( f"Error getting events from calendar {calendar['name']}: {e}" ) continue # Sort by start time and limit all_events.sort(key=lambda x: x.get("start_datetime", "")) return all_events[:limit] @mcp.tool() @require_scopes("calendar:read") async def nc_calendar_find_availability( duration_minutes: int, ctx: Context, attendees: str = "", # Comma-separated email list date_range_start: str = "", # "2025-07-28" date_range_end: str = "", # "2025-08-04" business_hours_only: bool = True, exclude_weekends: bool = True, preferred_times: str = "", # Comma-separated time ranges like "09:00-12:00,14:00-17:00" ): """Find available time slots for scheduling meetings. This tool intelligently analyzes existing calendar events to find free time slots that work for all specified attendees within the given constraints. Args: duration_minutes: Required duration for the meeting in minutes attendees: Comma-separated list of attendee email addresses to check availability for date_range_start: Start date for availability search (YYYY-MM-DD) date_range_end: End date for availability search (YYYY-MM-DD) business_hours_only: Only suggest slots during business hours (9 AM - 5 PM) exclude_weekends: Skip weekends when finding availability preferred_times: Preferred time ranges as "HH:MM-HH:MM" (comma-separated) Returns: List of available time slots with start/end times and duration """ client = await get_client(ctx) # Parse attendees attendee_list = [] if attendees: attendee_list = [ email.strip() for email in attendees.split(",") if email.strip() ] # Parse preferred times preferred_time_list = [] if preferred_times: preferred_time_list = [ time_range.strip() for time_range in preferred_times.split(",") if time_range.strip() ] # Convert date strings to datetime objects start_datetime = None end_datetime = None if date_range_start: try: start_datetime = dt.datetime.strptime(date_range_start, "%Y-%m-%d") except ValueError: logger.warning(f"Invalid date_range_start format: {date_range_start}") if date_range_end: try: end_datetime = dt.datetime.strptime(date_range_end, "%Y-%m-%d").replace( hour=23, minute=59, second=59 ) except ValueError: logger.warning(f"Invalid date_range_end format: {date_range_end}") # Build constraints constraints = { "business_hours_only": business_hours_only, "exclude_weekends": exclude_weekends, "preferred_times": preferred_time_list, } return await client.calendar.find_availability( duration_minutes=duration_minutes, attendees=attendee_list, start_datetime=start_datetime, end_datetime=end_datetime, constraints=constraints, ) @mcp.tool() @require_scopes("calendar:write") async def nc_calendar_bulk_operations( operation: str, # "update", "delete", "move" ctx: Context, title_contains: Optional[str] = None, categories: Optional[str] = None, # Comma-separated calendar_name: Optional[str] = None, start_date: str = "", # "2025-07-01" end_date: str = "", # "2025-07-31" status: Optional[str] = None, location_contains: Optional[str] = None, # Update operation parameters new_title: Optional[str] = None, new_description: Optional[str] = None, new_location: Optional[str] = None, new_categories: Optional[str] = None, new_priority: Optional[int] = None, new_reminder_minutes: Optional[int] = None, # Move operation parameters target_calendar: Optional[str] = None, ): """Perform bulk operations (update/delete) on events matching filter criteria. This tool allows you to efficiently modify or delete multiple events at once by applying filters to find matching events and then performing the specified operation. Args: operation: Type of operation - "update" or "delete" title_contains: Filter events where title contains this text categories: Filter events containing any of these categories (comma-separated) calendar_name: Filter events from this specific calendar start_date: Filter events starting from this date (YYYY-MM-DD) end_date: Filter events ending before this date (YYYY-MM-DD) status: Filter events by status (CONFIRMED, TENTATIVE, CANCELLED) location_contains: Filter events where location contains this text # For update operations: new_title: New title for matching events new_description: New description for matching events new_location: New location for matching events new_categories: New categories for matching events (comma-separated) new_priority: New priority for matching events (1-9, 5=normal) new_reminder_minutes: New reminder time in minutes before event # For move operations: target_calendar: Calendar to move events to (requires operation="move") Returns: Summary of operation results including counts and details """ client = await get_client(ctx) if operation not in ["update", "delete", "move"]: raise ValueError("Operation must be 'update', 'delete', or 'move'") # Convert date strings to datetime objects start_datetime = None end_datetime = None if start_date: try: start_datetime = dt.datetime.strptime(start_date, "%Y-%m-%d") except ValueError: logger.warning(f"Invalid start_date format: {start_date}") if end_date: try: end_datetime = dt.datetime.strptime(end_date, "%Y-%m-%d").replace( hour=23, minute=59, second=59 ) except ValueError: logger.warning(f"Invalid end_date format: {end_date}") # Build filter criteria filter_criteria = {} if title_contains is not None: filter_criteria["title_contains"] = title_contains if categories is not None: filter_criteria["categories"] = [ cat.strip() for cat in categories.split(",") ] if status is not None: filter_criteria["status"] = status if location_contains is not None: filter_criteria["location_contains"] = location_contains # Add datetime strings for client compatibility if start_date: filter_criteria["start_date"] = start_date if end_date: filter_criteria["end_date"] = end_date if operation == "delete": # Find matching events and delete them if calendar_name: events = await client.calendar.get_calendar_events( calendar_name=calendar_name, start_datetime=start_datetime, end_datetime=end_datetime, ) if filter_criteria: events = client.calendar._apply_event_filters( events, filter_criteria ) else: events = await client.calendar.search_events_across_calendars( start_datetime=start_datetime, end_datetime=end_datetime, filters=filter_criteria, ) deleted_count = 0 failed_count = 0 results = [] for event in events: try: await client.calendar.delete_event( event.get("calendar_name", calendar_name), event["uid"] ) deleted_count += 1 results.append( { "uid": event["uid"], "status": "deleted", "title": event.get("title", ""), } ) except Exception as e: failed_count += 1 results.append( { "uid": event["uid"], "status": "failed", "error": str(e), "title": event.get("title", ""), } ) return { "operation": "delete", "total_found": len(events), "deleted_count": deleted_count, "failed_count": failed_count, "results": results, } elif operation == "update": # Build update data update_data = {} if new_title is not None: update_data["title"] = new_title if new_description is not None: update_data["description"] = new_description if new_location is not None: update_data["location"] = new_location if new_categories is not None: update_data["categories"] = new_categories if new_priority is not None: update_data["priority"] = new_priority if new_reminder_minutes is not None: update_data["reminder_minutes"] = new_reminder_minutes if not update_data: raise ValueError("No update data provided for update operation") return await client.calendar.bulk_update_events( filter_criteria, update_data ) elif operation == "move": if not target_calendar: raise ValueError("target_calendar is required for move operation") # Find matching events if calendar_name: events = await client.calendar.get_calendar_events( calendar_name=calendar_name, start_datetime=start_datetime, end_datetime=end_datetime, ) if filter_criteria: events = client.calendar._apply_event_filters( events, filter_criteria ) else: events = await client.calendar.search_events_across_calendars( start_datetime=start_datetime, end_datetime=end_datetime, filters=filter_criteria, ) moved_count = 0 failed_count = 0 results = [] for event in events: try: # Create event in target calendar event_data = { k: v for k, v in event.items() if k not in [ "uid", "href", "etag", "calendar_name", "calendar_display_name", ] } await client.calendar.create_event(target_calendar, event_data) # Delete from source calendar await client.calendar.delete_event( event.get("calendar_name", calendar_name), event["uid"] ) moved_count += 1 results.append( { "uid": event["uid"], "status": "moved", "title": event.get("title", ""), "from_calendar": event.get("calendar_name", calendar_name), "to_calendar": target_calendar, } ) except Exception as e: failed_count += 1 results.append( { "uid": event["uid"], "status": "failed", "error": str(e), "title": event.get("title", ""), } ) return { "operation": "move", "total_found": len(events), "moved_count": moved_count, "failed_count": failed_count, "target_calendar": target_calendar, "results": results, } @mcp.tool() @require_scopes("calendar:write") async def nc_calendar_manage_calendar( action: str, # "create", "delete", "update", "list" ctx: Context, calendar_name: str = "", display_name: str = "", description: str = "", color: str = "#1976D2", # Default blue color ): """Manage calendar creation, deletion, and properties. This tool provides comprehensive calendar management functionality including creating new calendars, deleting existing ones, and updating calendar properties. Args: action: Action to perform - "create", "delete", "update", or "list" calendar_name: Internal name for the calendar (required for create/delete/update) display_name: Human-readable name for the calendar (used for create/update) description: Description for the calendar (used for create/update) color: Hex color code for the calendar (e.g., "#1976D2" for blue) Returns: Result of the calendar management operation """ client = await get_client(ctx) if action == "list": return await client.calendar.list_calendars() elif action == "create": if not calendar_name: raise ValueError("calendar_name is required for create action") return await client.calendar.create_calendar( calendar_name=calendar_name, display_name=display_name or calendar_name, description=description, color=color, ) elif action == "delete": if not calendar_name: raise ValueError("calendar_name is required for delete action") return await client.calendar.delete_calendar(calendar_name) elif action == "update": if not calendar_name: raise ValueError("calendar_name is required for update action") # Note: Calendar property updates require additional CalDAV PROPPATCH implementation # For now, return an informative message return { "status": "not_implemented", "message": "Calendar property updates require PROPPATCH implementation", "calendar_name": calendar_name, "requested_changes": { "display_name": display_name, "description": description, "color": color, }, } else: raise ValueError("Action must be 'create', 'delete', 'update', or 'list'") # ============= Todo/Task Tools ============= @mcp.tool() @require_scopes("todo:read", "calendar:read") async def nc_calendar_list_todos( calendar_name: str, ctx: Context, status: Optional[str] = None, min_priority: Optional[int] = None, categories: Optional[str] = None, summary_contains: Optional[str] = None, ) -> ListTodosResponse: """List todos/tasks in a calendar with optional filtering. Args: calendar_name: Name of the calendar to list todos from ctx: MCP context status: Filter by status (NEEDS-ACTION, IN-PROCESS, COMPLETED, CANCELLED) min_priority: Filter by minimum priority (1=highest, 9=lowest) categories: Filter by categories (comma-separated, e.g., "work,urgent") summary_contains: Filter todos where summary contains this text Returns: List of todos matching the filters """ client = await get_client(ctx) # Build filters dictionary filters = {} if status is not None: filters["status"] = status if min_priority is not None: filters["min_priority"] = min_priority if categories is not None: filters["categories"] = [cat.strip() for cat in categories.split(",")] if summary_contains is not None: filters["summary_contains"] = summary_contains todos_data = await client.calendar.list_todos( calendar_name, filters if filters else None ) todos = [Todo(**todo_data) for todo_data in todos_data] return ListTodosResponse( todos=todos, calendar_name=calendar_name, total_count=len(todos) ) @mcp.tool() @require_scopes("todo:write", "calendar:read") async def nc_calendar_create_todo( calendar_name: str, summary: str, ctx: Context, description: str = "", status: str = "NEEDS-ACTION", priority: int = 0, due: str = "", dtstart: str = "", categories: str = "", ): """Create a new todo/task in a calendar. Args: calendar_name: Name of the calendar to create the todo in summary: Todo title/summary ctx: MCP context description: Detailed description of the todo status: Todo status (NEEDS-ACTION, IN-PROCESS, COMPLETED, CANCELLED) priority: Priority (0=undefined, 1=highest, 9=lowest) due: Due date/time (ISO format, e.g., "2025-01-15T14:00:00") dtstart: Start date/time (ISO format) categories: Comma-separated categories (e.g., "work,urgent") Returns: Dict with todo creation result """ client = await get_client(ctx) todo_data = { "summary": summary, "description": description, "status": status, "priority": priority, "due": due, "dtstart": dtstart, "categories": categories, } return await client.calendar.create_todo(calendar_name, todo_data) @mcp.tool() @require_scopes("todo:write", "calendar:read") async def nc_calendar_update_todo( calendar_name: str, todo_uid: str, ctx: Context, summary: Optional[str] = None, description: Optional[str] = None, status: Optional[str] = None, priority: Optional[int] = None, percent_complete: Optional[int] = None, due: Optional[str] = None, dtstart: Optional[str] = None, completed: Optional[str] = None, categories: Optional[str] = None, ): """Update an existing todo/task. Args: calendar_name: Name of the calendar containing the todo todo_uid: UID of the todo to update ctx: MCP context summary: New summary/title description: New description status: New status (NEEDS-ACTION, IN-PROCESS, COMPLETED, CANCELLED) priority: New priority (0-9) percent_complete: New completion percentage (0-100) due: New due date/time (ISO format) dtstart: New start date/time (ISO format) completed: Completion timestamp (ISO format) categories: New categories (comma-separated) Returns: Dict with todo update result """ client = await get_client(ctx) # Build update data with only non-None values todo_data = {} if summary is not None: todo_data["summary"] = summary if description is not None: todo_data["description"] = description if status is not None: todo_data["status"] = status if priority is not None: todo_data["priority"] = priority if percent_complete is not None: todo_data["percent_complete"] = percent_complete if due is not None: todo_data["due"] = due if dtstart is not None: todo_data["dtstart"] = dtstart if completed is not None: todo_data["completed"] = completed if categories is not None: todo_data["categories"] = categories return await client.calendar.update_todo(calendar_name, todo_uid, todo_data) @mcp.tool() @require_scopes("todo:write", "calendar:read") async def nc_calendar_delete_todo( calendar_name: str, todo_uid: str, ctx: Context, ): """Delete a todo/task from a calendar. Args: calendar_name: Name of the calendar containing the todo todo_uid: UID of the todo to delete ctx: MCP context Returns: Dict with deletion status """ client = await get_client(ctx) return await client.calendar.delete_todo(calendar_name, todo_uid) @mcp.tool() @require_scopes("todo:read", "calendar:read") async def nc_calendar_search_todos( ctx: Context, status: Optional[str] = None, min_priority: Optional[int] = None, categories: Optional[str] = None, summary_contains: Optional[str] = None, ): """Search todos across all calendars with optional filtering. Args: ctx: MCP context status: Filter by status (NEEDS-ACTION, IN-PROCESS, COMPLETED, CANCELLED) min_priority: Filter by minimum priority (1=highest, 9=lowest) categories: Filter by categories (comma-separated, e.g., "work,urgent") summary_contains: Filter todos where summary contains this text Returns: List of todos matching the filters from all calendars """ client = await get_client(ctx) # Build filters dictionary filters = {} if status is not None: filters["status"] = status if min_priority is not None: filters["min_priority"] = min_priority if categories is not None: filters["categories"] = [cat.strip() for cat in categories.split(",")] if summary_contains is not None: filters["summary_contains"] = summary_contains todos_data = await client.calendar.search_todos_across_calendars( filters if filters else None ) todos = [Todo(**todo_data) for todo_data in todos_data] return ListTodosResponse(todos=todos, total_count=len(todos))

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/No-Smoke/nextcloud-mcp-comprehensive'

If you have feedback or need assistance with the MCP directory API, please join our Discord server