main.py•12.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