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:
Eric Jungbauer
2026-04-15 15:32:24 -06:00
commit 7e619d0454
16 changed files with 2783 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
"""Apple Maps tools."""
import subprocess
import urllib.parse
from helpers import run_applescript, run_jxa, safe_applescript_string
def search_locations(query: str) -> list[dict]:
"""Search for locations using Maps."""
safe_query = safe_applescript_string(query)
# Use JXA with MapKit-like search via Maps app
script = f'''
var Maps = Application("Maps");
Maps.activate();
// Use URL scheme to search
var app = Application.currentApplication();
app.includeStandardAdditions = true;
app.openLocation("maps://?q=" + encodeURIComponent("{safe_query}"));
// Return the query - Maps will show results in the app
"{safe_query}";
'''
# Maps doesn't expose search results via AppleScript, so we open the search
# and also try to geocode using CoreLocation via JXA
geocode_script = f'''
ObjC.import("CoreLocation");
ObjC.import("Foundation");
var geocoder = $.CLGeocoder.alloc.init;
var query = "{safe_query}";
var results = [];
var done = false;
geocoder.geocodeAddressStringCompletionHandler(query, function(placemarks, error) {{
if (placemarks && placemarks.count > 0) {{
for (var i = 0; i < Math.min(placemarks.count, 5); i++) {{
var pm = placemarks.objectAtIndex(i);
var loc = pm.location;
var name = pm.name ? pm.name.js : query;
var locality = pm.locality ? pm.locality.js : "";
var admin = pm.administrativeArea ? pm.administrativeArea.js : "";
var country = pm.country ? pm.country.js : "";
var postal = pm.postalCode ? pm.postalCode.js : "";
results.push({{
name: name,
latitude: loc.coordinate.latitude,
longitude: loc.coordinate.longitude,
locality: locality,
state: admin,
country: country,
postal_code: postal
}});
}}
}}
done = true;
}});
// Wait for geocoding (up to 10 seconds)
var startTime = new Date().getTime();
while (!done && (new Date().getTime() - startTime) < 10000) {{
$.NSRunLoop.currentRunLoop.runUntilDate($.NSDate.dateWithTimeIntervalSinceNow(0.1));
}}
JSON.stringify(results);
'''
try:
raw = run_jxa(geocode_script, timeout=15)
import json
locations = json.loads(raw)
return locations
except Exception:
# Fallback: just open in Maps
encoded = urllib.parse.quote(query)
subprocess.run(["open", f"maps://?q={encoded}"], capture_output=True)
return [{"query": query, "opened_in_maps": True, "note": "Search opened in Maps app. CoreLocation geocoding unavailable."}]
def get_directions(from_address: str | None = None, to_address: str = "",
mode: str = "driving") -> dict:
"""Get directions between two locations.
Args:
from_address: Starting address. None = current location.
to_address: Destination address.
mode: Travel mode - "driving", "walking", "transit".
"""
mode_map = {"driving": "d", "walking": "w", "transit": "r"}
mode_char = mode_map.get(mode, "d")
params = {"daddr": to_address, "dirflg": mode_char}
if from_address:
params["saddr"] = from_address
url = "maps://?" + urllib.parse.urlencode(params)
subprocess.run(["open", url], capture_output=True)
return {
"from": from_address or "Current Location",
"to": to_address,
"mode": mode,
"opened_in_maps": True,
}
def open_location(address: str | None = None, latitude: float | None = None,
longitude: float | None = None, label: str | None = None) -> dict:
"""Open a location in Apple Maps.
Provide either an address string or lat/lon coordinates.
"""
if latitude is not None and longitude is not None:
params = {"ll": f"{latitude},{longitude}"}
if label:
params["q"] = label
url = "maps://?" + urllib.parse.urlencode(params)
elif address:
params = {"address": address}
if label:
params["q"] = label
url = "maps://?" + urllib.parse.urlencode(params)
else:
raise RuntimeError("Provide either address or latitude/longitude")
subprocess.run(["open", url], capture_output=True)
return {
"address": address,
"latitude": latitude,
"longitude": longitude,
"label": label,
"opened_in_maps": True,
}
def drop_pin(latitude: float, longitude: float, label: str = "Pin") -> dict:
"""Drop a pin at specific coordinates in Maps."""
params = {"ll": f"{latitude},{longitude}", "q": label}
url = "maps://?" + urllib.parse.urlencode(params)
subprocess.run(["open", url], capture_output=True)
return {
"latitude": latitude,
"longitude": longitude,
"label": label,
"dropped_pin": True,
}