7e619d0454
Python MCP server (FastMCP) providing Claude Code/CoWork access to Apple Reminders, Calendar, Mail, Contacts, Find My, and Maps via AppleScript. Includes a native SwiftUI config/installer app and a compiled EventKit helper for fast reminder queries on large databases (8,000+ items). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
96 lines
2.7 KiB
Python
96 lines
2.7 KiB
Python
"""Shared utilities for Apple MCP server."""
|
|
|
|
import json
|
|
import subprocess
|
|
from datetime import datetime, date
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
CONFIG_PATH = Path(__file__).parent / "config.json"
|
|
|
|
|
|
def load_config() -> dict:
|
|
"""Load the app permissions config."""
|
|
with open(CONFIG_PATH) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def is_app_enabled(app_name: str) -> bool:
|
|
"""Check if an app is enabled in config."""
|
|
cfg = load_config()
|
|
app = cfg.get("apps", {}).get(app_name, {})
|
|
return app.get("enabled", False)
|
|
|
|
|
|
def is_write_allowed(app_name: str) -> bool:
|
|
"""Check if write access is allowed for an app."""
|
|
cfg = load_config()
|
|
app = cfg.get("apps", {}).get(app_name, {})
|
|
return app.get("enabled", False) and app.get("mode") == "read-write"
|
|
|
|
|
|
def run_applescript(script: str, timeout: int = 30) -> str:
|
|
"""Run an AppleScript and return stdout.
|
|
|
|
Raises RuntimeError on failure.
|
|
"""
|
|
result = subprocess.run(
|
|
["osascript", "-e", script],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"AppleScript error: {result.stderr.strip()}")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def run_jxa(script: str, timeout: int = 30) -> str:
|
|
"""Run a JXA (JavaScript for Automation) script and return stdout."""
|
|
result = subprocess.run(
|
|
["osascript", "-l", "JavaScript", "-e", script],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"JXA error: {result.stderr.strip()}")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def parse_applescript_date(date_str: str) -> str | None:
|
|
"""Try to parse a macOS AppleScript date string into ISO format."""
|
|
if not date_str or date_str == "missing value":
|
|
return None
|
|
formats = [
|
|
"%A, %B %d, %Y at %I:%M:%S %p",
|
|
"%A, %B %d, %Y at %H:%M:%S",
|
|
"%m/%d/%Y %I:%M:%S %p",
|
|
"%m/%d/%Y %H:%M:%S",
|
|
"%Y-%m-%d %H:%M:%S %z",
|
|
]
|
|
for fmt in formats:
|
|
try:
|
|
dt = datetime.strptime(date_str, fmt)
|
|
return dt.isoformat()
|
|
except ValueError:
|
|
continue
|
|
return date_str
|
|
|
|
|
|
def format_date_for_applescript(iso_date: str) -> str:
|
|
"""Convert ISO date string to AppleScript date format."""
|
|
dt = datetime.fromisoformat(iso_date)
|
|
return dt.strftime("%m/%d/%Y %I:%M:%S %p")
|
|
|
|
|
|
def today_str() -> str:
|
|
"""Return today's date as YYYY-MM-DD."""
|
|
return date.today().isoformat()
|
|
|
|
|
|
def safe_applescript_string(s: str) -> str:
|
|
"""Escape a string for safe embedding in AppleScript."""
|
|
return s.replace("\\", "\\\\").replace('"', '\\"')
|