Skip to main content
Glama

icalPal MCP Server

by wsargent
main.py12.6 kB
#!/usr/bin/env python3 """ icalPal FastMCP Server This server provides access to macOS Calendar and Reminders data via icalPal gem. """ from os import getenv, path import subprocess import sys from typing import List, Optional from fastmcp import Context, FastMCP # Create the FastMCP server instance mcp = FastMCP(name="icalPal MCP Server") def get_database_args(): """Get database arguments for icalPal based on environment (Docker vs native)""" args = [] # Check if we're running in Docker by looking for mounted database paths if path.exists("/calendar/Calendar.sqlitedb"): # Running in Docker - use mounted Calendar database args.extend(["--db", "/calendar/Calendar.sqlitedb"]) elif path.exists("/legacy_calendars/Calendar.sqlitedb"): # Use legacy calendar location args.extend(["--db", "/legacy_calendars/Calendar.sqlitedb"]) return args def get_tasks_database_args(): """Get database arguments for tasks/reminders commands""" args = [] # For tasks, icalPal expects a directory containing .sqlite files # This matches how icalPal's defaults.rb defines the Reminders DB_PATH if path.exists("/reminders"): args.extend(["--db", "/reminders"]) return args @mcp.tool def get_events( days: Optional[int] = None, from_date: Optional[str] = None, to_date: Optional[str] = None, output_format: str = "json", calendars_include: Optional[str] = None, calendars_exclude: Optional[str] = None, all_day_only: bool = False, exclude_all_day: bool = False ) -> str: """ Get calendar events using icalPal. Args: days: Number of days of events to show from_date: Start date (e.g., 'today', 'tomorrow', '2024-01-01') to_date: End date output_format: Output format (json, csv, default, etc.) calendars_include: Comma-separated list of calendars to include calendars_exclude: Comma-separated list of calendars to exclude all_day_only: Include only all-day events exclude_all_day: Exclude all-day events Returns: Events data in the specified format """ cmd = ["icalPal", "events", f"--output={output_format}"] # Add database arguments if running in Docker cmd.extend(get_database_args()) if days is not None: cmd.extend([f"--days={days}"]) if from_date: cmd.extend([f"--from={from_date}"]) if to_date: cmd.extend([f"--to={to_date}"]) if calendars_include: cmd.extend([f"--ic={calendars_include}"]) if calendars_exclude: cmd.extend([f"--ec={calendars_exclude}"]) if all_day_only: cmd.append("--ia") if exclude_all_day: cmd.append("--ea") try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout except subprocess.CalledProcessError as e: error_msg = f"Error running icalPal: {e.stderr}" # Check if it's a permission issue and provide helpful guidance if "Full Disk Access" in error_msg or "Could not open database" in error_msg: error_msg += "\n\nTo fix this, you need to grant Full Disk Access permission to your terminal/application in System Settings > Privacy & Security > Full Disk Access" return error_msg @mcp.tool def get_tasks( output_format: str = "json", include_completed: bool = False, exclude_uncompleted: bool = False, lists_include: Optional[str] = None, lists_exclude: Optional[str] = None, from_date: Optional[str] = None, to_date: Optional[str] = None, ) -> str: """ Get tasks/reminders using icalPal. Args: output_format: Output format (json, csv, default, etc.) include_completed: Include completed reminders exclude_uncompleted: Exclude uncompleted reminders lists_include: Comma-separated list of reminder lists to include lists_exclude: Comma-separated list of reminder lists to exclude from_date: Start date for tasks to_date: End date for tasks Returns: Tasks data in the specified format """ cmd = ["icalPal", "tasks", f"--output={output_format}"] # Add database arguments if running in Docker cmd.extend(get_tasks_database_args()) if include_completed: cmd.append("--id") if exclude_uncompleted: cmd.append("--ed") if lists_include: cmd.extend([f"--il={lists_include}"]) if lists_exclude: cmd.extend([f"--el={lists_exclude}"]) if from_date: cmd.extend([f"--from={from_date}"]) if to_date: cmd.extend([f"--to={to_date}"]) # Debug: log the command being executed for get_tasks print(f"DEBUG get_tasks: Executing command: {' '.join(cmd)}", file=sys.stderr) try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout except subprocess.CalledProcessError as e: error_msg = f"Error running icalPal: {e.stderr}" # Debug: log the command that failed print(f"DEBUG get_tasks: Failed command: {' '.join(cmd)}", file=sys.stderr) print(f"DEBUG get_tasks: stderr: {e.stderr}", file=sys.stderr) print(f"DEBUG get_tasks: stdout: {e.stdout}", file=sys.stderr) # Check if it's a permission issue and provide helpful guidance if "Full Disk Access" in error_msg or "Could not open database" in error_msg: error_msg += "\n\nTo fix this, you need to grant Full Disk Access permission to your terminal/application in System Settings > Privacy & Security > Full Disk Access" elif "syntax error" in error_msg: error_msg += "\n\nNote: There appears to be a compatibility issue between icalPal and the Reminders database schema in this Docker environment. Calendar events work fine, but tasks/reminders may require running icalPal natively on macOS." return error_msg @mcp.tool def get_dated_tasks( output_format: str = "json", sort_by_due_date: bool = False ) -> str: """ Get tasks with due dates using icalPal. Args: output_format: Output format (json, csv, default, etc.) sort_by_due_date: Sort tasks by due date Returns: Dated tasks data in the specified format """ cmd = ["icalPal", "datedTasks", f"--output={output_format}"] # Add database arguments if running in Docker cmd.extend(get_tasks_database_args()) if sort_by_due_date: cmd.append("--std") try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout except subprocess.CalledProcessError as e: error_msg = f"Error running icalPal: {e.stderr}" # Check if it's a permission issue and provide helpful guidance if "Full Disk Access" in error_msg or "Could not open database" in error_msg: error_msg += "\n\nTo fix this, you need to grant Full Disk Access permission to your terminal/application in System Settings > Privacy & Security > Full Disk Access" return error_msg @mcp.tool def get_undated_tasks( output_format: str = "json" ) -> str: """ Get tasks without due dates using icalPal. Args: output_format: Output format (json, csv, default, etc.) Returns: Undated tasks data in the specified format """ cmd = ["icalPal", "undatedTasks", f"--output={output_format}"] # Add database arguments if running in Docker cmd.extend(get_tasks_database_args()) try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout except subprocess.CalledProcessError as e: error_msg = f"Error running icalPal: {e.stderr}" # Check if it's a permission issue and provide helpful guidance if "Full Disk Access" in error_msg or "Could not open database" in error_msg: error_msg += "\n\nTo fix this, you need to grant Full Disk Access permission to your terminal/application in System Settings > Privacy & Security > Full Disk Access" return error_msg @mcp.tool def get_calendars(output_format: str = "json") -> str: """ Get list of calendars using icalPal. Args: output_format: Output format (json, csv, default, etc.) Returns: Calendars data in the specified format """ cmd = ["icalPal", "calendars", f"--output={output_format}"] # Add database arguments if running in Docker cmd.extend(get_database_args()) try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout except subprocess.CalledProcessError as e: error_msg = f"Error running icalPal: {e.stderr}" # Check if it's a permission issue and provide helpful guidance if "Full Disk Access" in error_msg or "Could not open database" in error_msg: error_msg += "\n\nTo fix this, you need to grant Full Disk Access permission to your terminal/application in System Settings > Privacy & Security > Full Disk Access" return error_msg @mcp.tool def get_accounts(output_format: str = "json") -> str: """ Get list of accounts using icalPal. Args: output_format: Output format (json, csv, default, etc.) Returns: Accounts data in the specified format """ cmd = ["icalPal", "accounts", f"--output={output_format}"] # Add database arguments if running in Docker cmd.extend(get_database_args()) try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout except subprocess.CalledProcessError as e: error_msg = f"Error running icalPal: {e.stderr}" # Check if it's a permission issue and provide helpful guidance if "Full Disk Access" in error_msg or "Could not open database" in error_msg: error_msg += "\n\nTo fix this, you need to grant Full Disk Access permission to your terminal/application in System Settings > Privacy & Security > Full Disk Access" return error_msg @mcp.tool def get_events_today(output_format: str = "json") -> str: """ Get events occurring today using icalPal. Args: output_format: Output format (json, csv, default, etc.) Returns: Today's events data in the specified format """ # Use the events command with from=today and to=today as a more reliable alternative cmd = ["icalPal", "events", "--from=today", "--to=today", f"--output={output_format}"] # Add database arguments if running in Docker cmd.extend(get_database_args()) try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout except subprocess.CalledProcessError as e: error_msg = f"Error running icalPal: {e.stderr}" # Check if it's a permission issue and provide helpful guidance if "Full Disk Access" in error_msg: error_msg += "\n\nTo fix this, you need to grant Full Disk Access permission to your terminal/application in System Settings > Privacy & Security > Full Disk Access" return error_msg @mcp.tool def get_events_now(output_format: str = "json") -> str: """ Get events occurring right now using icalPal. Args: output_format: Output format (json, csv, default, etc.) Returns: Current events data in the specified format """ cmd = ["icalPal", "eventsNow", f"--output={output_format}"] # Add database arguments if running in Docker cmd.extend(get_database_args()) try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout except subprocess.CalledProcessError as e: error_msg = f"Error running icalPal: {e.stderr}" # Check if it's a permission issue and provide helpful guidance if "Full Disk Access" in error_msg or "Could not open database" in error_msg: error_msg += "\n\nTo fix this, you need to grant Full Disk Access permission to your terminal/application in System Settings > Privacy & Security > Full Disk Access" return error_msg if __name__ == "__main__": port = int(getenv("MCP_PORT", "8000")) transport = getenv("MCP_TRANSPORT", "sse") mcp.run(transport=transport, host="0.0.0.0", port=port) # type: ignore

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/wsargent/icalpal-mcp'

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