Files
apple-mcp/apps/contacts.py
T
Eric Jungbauer 7e619d0454 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>
2026-04-15 15:32:24 -06:00

281 lines
8.8 KiB
Python

"""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}