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:
+134
@@ -0,0 +1,134 @@
|
||||
"""Apple Find My tools — reads cached location data."""
|
||||
|
||||
import json
|
||||
import plistlib
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
FINDMY_CACHE = Path.home() / "Library" / "Caches" / "com.apple.findmy.fmipcore"
|
||||
ITEMS_FILE = FINDMY_CACHE / "Items.data"
|
||||
DEVICES_FILE = FINDMY_CACHE / "Devices.data"
|
||||
|
||||
|
||||
def _load_cache_file(path: Path) -> list[dict]:
|
||||
"""Load a Find My cache file (plist format)."""
|
||||
if not path.exists():
|
||||
raise RuntimeError(
|
||||
f"Find My cache not found at {path}. "
|
||||
"Make sure the Find My app has been opened at least once."
|
||||
)
|
||||
try:
|
||||
f = open(path, "rb")
|
||||
except PermissionError:
|
||||
raise RuntimeError(
|
||||
f"Permission denied reading {path}. "
|
||||
"Grant Full Disk Access to the terminal/app running the MCP server: "
|
||||
"System Settings > Privacy & Security > Full Disk Access."
|
||||
)
|
||||
with f:
|
||||
try:
|
||||
data = plistlib.load(f)
|
||||
except Exception:
|
||||
# Some versions use JSON
|
||||
f.seek(0)
|
||||
try:
|
||||
data = json.load(f)
|
||||
except Exception:
|
||||
raise RuntimeError(f"Could not parse Find My cache at {path}")
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
return []
|
||||
|
||||
|
||||
def _format_location(item: dict) -> dict | None:
|
||||
"""Extract location info from a Find My item/device."""
|
||||
loc = item.get("location")
|
||||
if not loc:
|
||||
return None
|
||||
result = {
|
||||
"latitude": loc.get("latitude"),
|
||||
"longitude": loc.get("longitude"),
|
||||
"altitude": loc.get("altitude"),
|
||||
"horizontal_accuracy": loc.get("horizontalAccuracy"),
|
||||
"is_old": loc.get("isOld", False),
|
||||
"floor_level": loc.get("floorLevel"),
|
||||
}
|
||||
timestamp = loc.get("timeStamp")
|
||||
if timestamp:
|
||||
try:
|
||||
if isinstance(timestamp, (int, float)):
|
||||
# Apple epoch (2001-01-01) or Unix epoch
|
||||
if timestamp > 1e15: # nanoseconds
|
||||
timestamp = timestamp / 1e9
|
||||
if timestamp < 1e9: # Apple epoch
|
||||
timestamp += 978307200 # seconds between 1970 and 2001
|
||||
result["timestamp"] = datetime.fromtimestamp(timestamp).isoformat()
|
||||
else:
|
||||
result["timestamp"] = str(timestamp)
|
||||
except Exception:
|
||||
result["timestamp"] = str(timestamp)
|
||||
return result
|
||||
|
||||
|
||||
def list_devices() -> list[dict]:
|
||||
"""List all Apple devices in Find My."""
|
||||
try:
|
||||
devices = _load_cache_file(DEVICES_FILE)
|
||||
except RuntimeError as e:
|
||||
return [{"error": str(e)}]
|
||||
|
||||
results = []
|
||||
for dev in devices:
|
||||
location = _format_location(dev)
|
||||
results.append({
|
||||
"name": dev.get("name", "Unknown"),
|
||||
"device_model": dev.get("deviceDisplayName", dev.get("deviceModel", "Unknown")),
|
||||
"battery_level": dev.get("batteryLevel"),
|
||||
"battery_status": dev.get("batteryStatus"),
|
||||
"location": location,
|
||||
"id": dev.get("baUUID", dev.get("id", "")),
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def get_device_location(device_name: str) -> dict:
|
||||
"""Get location of a specific Apple device."""
|
||||
try:
|
||||
devices = _load_cache_file(DEVICES_FILE)
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError(str(e))
|
||||
|
||||
device_name_lower = device_name.lower()
|
||||
for dev in devices:
|
||||
name = dev.get("name", "")
|
||||
if device_name_lower in name.lower():
|
||||
location = _format_location(dev)
|
||||
return {
|
||||
"name": name,
|
||||
"device_model": dev.get("deviceDisplayName", dev.get("deviceModel", "Unknown")),
|
||||
"battery_level": dev.get("batteryLevel"),
|
||||
"location": location,
|
||||
}
|
||||
raise RuntimeError(f"Device '{device_name}' not found in Find My")
|
||||
|
||||
|
||||
def list_items() -> list[dict]:
|
||||
"""List all Find My items (AirTags, third-party trackers)."""
|
||||
try:
|
||||
items = _load_cache_file(ITEMS_FILE)
|
||||
except RuntimeError as e:
|
||||
return [{"error": str(e)}]
|
||||
|
||||
results = []
|
||||
for item in items:
|
||||
location = _format_location(item)
|
||||
results.append({
|
||||
"name": item.get("name", "Unknown"),
|
||||
"product_type": item.get("productType", {}).get("type", "Unknown") if isinstance(item.get("productType"), dict) else item.get("productType", "Unknown"),
|
||||
"serial_number": item.get("serialNumber"),
|
||||
"battery_status": item.get("batteryStatus"),
|
||||
"location": location,
|
||||
"id": item.get("identifier", ""),
|
||||
})
|
||||
return results
|
||||
Reference in New Issue
Block a user