Initial commit — Apple Apps MCP server with native macOS config app
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>
This commit is contained in:
+95
@@ -0,0 +1,95 @@
|
||||
"""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('"', '\\"')
|
||||
Reference in New Issue
Block a user