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:
+146
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user