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:
@@ -0,0 +1,280 @@
|
||||
"""Apple Contacts tools."""
|
||||
|
||||
from helpers import run_applescript, safe_applescript_string
|
||||
|
||||
|
||||
def search_contacts(query: str) -> list[dict]:
|
||||
"""Search contacts by name, email, or phone."""
|
||||
safe_query = safe_applescript_string(query)
|
||||
script = f'''
|
||||
tell application "Contacts"
|
||||
set output to ""
|
||||
set results to (every person whose name contains "{safe_query}")
|
||||
repeat with p in results
|
||||
set pName to name of p
|
||||
set pId to id of p
|
||||
set pEmail to ""
|
||||
try
|
||||
set pEmail to value of first email of p
|
||||
end try
|
||||
set pPhone to ""
|
||||
try
|
||||
set pPhone to value of first phone of p
|
||||
end try
|
||||
set pOrg to ""
|
||||
try
|
||||
set pOrg to organization of p
|
||||
end try
|
||||
set output to output & pName & "|||" & pEmail & "|||" & pPhone & "|||" & pOrg & "|||" & pId & linefeed
|
||||
end repeat
|
||||
|
||||
-- Also search by email
|
||||
set emailResults to (every person whose value of emails contains "{safe_query}")
|
||||
repeat with p in emailResults
|
||||
set pName to name of p
|
||||
set pId to id of p
|
||||
if output does not contain pId then
|
||||
set pEmail to ""
|
||||
try
|
||||
set pEmail to value of first email of p
|
||||
end try
|
||||
set pPhone to ""
|
||||
try
|
||||
set pPhone to value of first phone of p
|
||||
end try
|
||||
set pOrg to ""
|
||||
try
|
||||
set pOrg to organization of p
|
||||
end try
|
||||
set output to output & pName & "|||" & pEmail & "|||" & pPhone & "|||" & pOrg & "|||" & pId & linefeed
|
||||
end if
|
||||
end repeat
|
||||
|
||||
return output
|
||||
end tell'''
|
||||
raw = run_applescript(script, timeout=30)
|
||||
results = []
|
||||
seen_ids = set()
|
||||
for line in raw.strip().split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split("|||")
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
cid = parts[4].strip()
|
||||
if cid in seen_ids:
|
||||
continue
|
||||
seen_ids.add(cid)
|
||||
results.append({
|
||||
"name": parts[0].strip(),
|
||||
"email": parts[1].strip() or None,
|
||||
"phone": parts[2].strip() or None,
|
||||
"organization": parts[3].strip() or None,
|
||||
"id": cid,
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def get_contact(name: str) -> dict:
|
||||
"""Get detailed info for a contact by name."""
|
||||
safe_name = safe_applescript_string(name)
|
||||
script = f'''
|
||||
tell application "Contacts"
|
||||
set results to (every person whose name is "{safe_name}")
|
||||
if (count of results) = 0 then
|
||||
set results to (every person whose name contains "{safe_name}")
|
||||
end if
|
||||
if (count of results) = 0 then
|
||||
return "NOT_FOUND"
|
||||
end if
|
||||
set p to item 1 of results
|
||||
|
||||
set pName to name of p
|
||||
set pFirst to first name of p
|
||||
set pLast to last name of p
|
||||
set pId to id of p
|
||||
|
||||
set pOrg to ""
|
||||
try
|
||||
set pOrg to organization of p
|
||||
end try
|
||||
|
||||
set pTitle to ""
|
||||
try
|
||||
set pTitle to job title of p
|
||||
end try
|
||||
|
||||
set pNote to ""
|
||||
try
|
||||
set pNote to note of p
|
||||
end try
|
||||
|
||||
-- Emails
|
||||
set emailList to ""
|
||||
repeat with e in emails of p
|
||||
set emailList to emailList & (label of e) & ":" & (value of e) & ","
|
||||
end repeat
|
||||
|
||||
-- Phones
|
||||
set phoneList to ""
|
||||
repeat with ph in phones of p
|
||||
set phoneList to phoneList & (label of ph) & ":" & (value of ph) & ","
|
||||
end repeat
|
||||
|
||||
-- Addresses
|
||||
set addrList to ""
|
||||
repeat with a in addresses of p
|
||||
set addrList to addrList & (label of a) & ":" & (formatted address of a) & ";;;"
|
||||
end repeat
|
||||
|
||||
return pName & "|||" & pFirst & "|||" & pLast & "|||" & pOrg & "|||" & pTitle & "|||" & pNote & "|||" & emailList & "|||" & phoneList & "|||" & addrList & "|||" & pId
|
||||
end tell'''
|
||||
raw = run_applescript(script, timeout=30)
|
||||
if raw == "NOT_FOUND":
|
||||
raise RuntimeError(f"Contact '{name}' not found")
|
||||
|
||||
parts = raw.split("|||")
|
||||
if len(parts) < 10:
|
||||
raise RuntimeError("Failed to parse contact details")
|
||||
|
||||
emails = []
|
||||
for e in parts[6].strip().rstrip(",").split(","):
|
||||
if ":" in e:
|
||||
label, value = e.split(":", 1)
|
||||
emails.append({"label": label.strip(), "value": value.strip()})
|
||||
|
||||
phones = []
|
||||
for ph in parts[7].strip().rstrip(",").split(","):
|
||||
if ":" in ph:
|
||||
label, value = ph.split(":", 1)
|
||||
phones.append({"label": label.strip(), "value": value.strip()})
|
||||
|
||||
addresses = []
|
||||
for a in parts[8].strip().split(";;;"):
|
||||
if ":" in a:
|
||||
label, value = a.split(":", 1)
|
||||
addresses.append({"label": label.strip(), "address": value.strip()})
|
||||
|
||||
return {
|
||||
"name": parts[0].strip(),
|
||||
"first_name": parts[1].strip() or None,
|
||||
"last_name": parts[2].strip() or None,
|
||||
"organization": parts[3].strip() or None,
|
||||
"job_title": parts[4].strip() or None,
|
||||
"note": parts[5].strip() or None,
|
||||
"emails": emails,
|
||||
"phones": phones,
|
||||
"addresses": addresses,
|
||||
"id": parts[9].strip(),
|
||||
}
|
||||
|
||||
|
||||
def list_groups() -> list[dict]:
|
||||
"""List all contact groups."""
|
||||
script = '''
|
||||
tell application "Contacts"
|
||||
set output to ""
|
||||
repeat with g in groups
|
||||
set gName to name of g
|
||||
set gId to id of g
|
||||
set gCount to count of people of g
|
||||
set output to output & gName & "|||" & gId & "|||" & (gCount as string) & linefeed
|
||||
end repeat
|
||||
return output
|
||||
end tell'''
|
||||
raw = run_applescript(script)
|
||||
results = []
|
||||
for line in raw.strip().split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split("|||")
|
||||
if len(parts) >= 3:
|
||||
results.append({
|
||||
"name": parts[0].strip(),
|
||||
"id": parts[1].strip(),
|
||||
"member_count": int(parts[2].strip()) if parts[2].strip().isdigit() else 0,
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
def create_contact(first_name: str, last_name: str | None = None,
|
||||
email: str | None = None, phone: str | None = None,
|
||||
organization: str | None = None, job_title: str | None = None,
|
||||
note: str | None = None) -> dict:
|
||||
"""Create a new contact."""
|
||||
safe_first = safe_applescript_string(first_name)
|
||||
props = [f'first name:"{safe_first}"']
|
||||
|
||||
if last_name:
|
||||
props.append(f'last name:"{safe_applescript_string(last_name)}"')
|
||||
if organization:
|
||||
props.append(f'organization:"{safe_applescript_string(organization)}"')
|
||||
if job_title:
|
||||
props.append(f'job title:"{safe_applescript_string(job_title)}"')
|
||||
if note:
|
||||
props.append(f'note:"{safe_applescript_string(note)}"')
|
||||
|
||||
props_str = ", ".join(props)
|
||||
|
||||
email_line = ""
|
||||
if email:
|
||||
email_line = f'make new email at end of emails of newPerson with properties {{label:"work", value:"{safe_applescript_string(email)}"}}'
|
||||
|
||||
phone_line = ""
|
||||
if phone:
|
||||
phone_line = f'make new phone at end of phones of newPerson with properties {{label:"mobile", value:"{safe_applescript_string(phone)}"}}'
|
||||
|
||||
script = f'''
|
||||
tell application "Contacts"
|
||||
set newPerson to make new person with properties {{{props_str}}}
|
||||
{email_line}
|
||||
{phone_line}
|
||||
save
|
||||
return id of newPerson
|
||||
end tell'''
|
||||
contact_id = run_applescript(script)
|
||||
return {
|
||||
"id": contact_id.strip(),
|
||||
"first_name": first_name,
|
||||
"last_name": last_name,
|
||||
"created": True,
|
||||
}
|
||||
|
||||
|
||||
def update_contact(name: str, email: str | None = None, phone: str | None = None,
|
||||
organization: str | None = None, job_title: str | None = None,
|
||||
note: str | None = None) -> dict:
|
||||
"""Update an existing contact by name."""
|
||||
safe_name = safe_applescript_string(name)
|
||||
updates = []
|
||||
if organization is not None:
|
||||
updates.append(f'set organization of p to "{safe_applescript_string(organization)}"')
|
||||
if job_title is not None:
|
||||
updates.append(f'set job title of p to "{safe_applescript_string(job_title)}"')
|
||||
if note is not None:
|
||||
updates.append(f'set note of p to "{safe_applescript_string(note)}"')
|
||||
if email:
|
||||
updates.append(f'make new email at end of emails of p with properties {{label:"work", value:"{safe_applescript_string(email)}"}}')
|
||||
if phone:
|
||||
updates.append(f'make new phone at end of phones of p with properties {{label:"mobile", value:"{safe_applescript_string(phone)}"}}')
|
||||
|
||||
if not updates:
|
||||
raise RuntimeError("No updates specified")
|
||||
|
||||
updates_str = "\n ".join(updates)
|
||||
|
||||
script = f'''
|
||||
tell application "Contacts"
|
||||
set results to (every person whose name contains "{safe_name}")
|
||||
if (count of results) = 0 then
|
||||
return "NOT_FOUND"
|
||||
end if
|
||||
set p to item 1 of results
|
||||
{updates_str}
|
||||
save
|
||||
return "updated"
|
||||
end tell'''
|
||||
result = run_applescript(script)
|
||||
if result == "NOT_FOUND":
|
||||
raise RuntimeError(f"Contact '{name}' not found")
|
||||
return {"name": name, "updated": True}
|
||||
Reference in New Issue
Block a user