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>
147 lines
4.8 KiB
Python
147 lines
4.8 KiB
Python
"""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,
|
|
}
|