Skip to main content
Glama
wolfiesch

iMessage MCP Server

by wolfiesch
sync_contacts.py7.76 kB
#!/usr/bin/env python3 """ Sync contacts from macOS Contacts.app to JSON configuration. Sprint 2: Standalone sync without Life Planner DB dependency. Usage: python scripts/sync_contacts.py python scripts/sync_contacts.py --output config/contacts.json """ import argparse import json import logging import sys from pathlib import Path from typing import List, Dict # Add parent directory to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) from src.contacts_sync import ( MacOSContactsReader, MacOSContact, normalize_phone_number ) # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) def convert_macos_contact_to_json(contact: MacOSContact) -> Dict: """ Convert MacOSContact to JSON-serializable dict for config file. Args: contact: MacOSContact object Returns: Dictionary with contact data """ # Get primary phone number (prefer mobile, then first available) primary_phone = None for phone in contact.phone_numbers: label = phone["label"].lower() if "mobile" in label or "iphone" in label: primary_phone = phone["value"] break # If no mobile, use first phone number if not primary_phone and contact.phone_numbers: primary_phone = contact.phone_numbers[0]["value"] # Normalize phone number if primary_phone: primary_phone = normalize_phone_number(primary_phone) # Determine relationship type (default to "other" for now) # [*TO-DO*] - Sprint 4: Use Life Planner contact data to set relationship type relationship_type = "other" return { "name": contact.full_name, "phone": primary_phone or "", "relationship_type": relationship_type, "notes": f"Synced from macOS Contacts (ID: {contact.identifier})", "macos_contact_id": contact.identifier, "all_phones": [ { "label": p["label"], "value": normalize_phone_number(p["value"]) } for p in contact.phone_numbers ], "emails": contact.email_addresses } def sync_contacts( output_path: Path, filter_no_phone: bool = True, merge_existing: bool = True ) -> int: """ Sync contacts from macOS Contacts to JSON file. Args: output_path: Path to output JSON file filter_no_phone: Skip contacts without phone numbers merge_existing: Merge with existing contacts (preserve manual edits) Returns: Number of contacts synced """ logger.info("Starting contact sync from macOS Contacts...") # Read contacts from macOS try: reader = MacOSContactsReader() macos_contacts = reader.fetch_all_contacts() logger.info(f"Fetched {len(macos_contacts)} contacts from macOS Contacts") except Exception as e: logger.error(f"Failed to read macOS Contacts: {e}") logger.error("Make sure you've granted Contacts permission") return 0 # Filter contacts if filter_no_phone: macos_contacts = [c for c in macos_contacts if c.phone_numbers] logger.info( f"Filtered to {len(macos_contacts)} contacts with phone numbers" ) # Convert to JSON format new_contacts = [ convert_macos_contact_to_json(c) for c in macos_contacts ] # Load existing contacts if merging existing_contacts = [] if merge_existing and output_path.exists(): try: with open(output_path) as f: data = json.load(f) existing_contacts = data.get("contacts", []) logger.info(f"Loaded {len(existing_contacts)} existing contacts") except Exception as e: logger.warning(f"Could not load existing contacts: {e}") # Merge contacts # Strategy: Keep existing contacts, add new ones, update changed ones merged_contacts = {} # Add existing contacts (keyed by macOS ID or name) for contact in existing_contacts: key = contact.get("macos_contact_id") or contact["name"] merged_contacts[key] = contact # Update/add new contacts for contact in new_contacts: key = contact.get("macos_contact_id") or contact["name"] if key in merged_contacts: # Update existing contact (preserve manual fields) existing = merged_contacts[key] # Update phone and emails from macOS existing["phone"] = contact["phone"] existing["all_phones"] = contact["all_phones"] existing["emails"] = contact["emails"] # Update name if changed existing["name"] = contact["name"] # Preserve relationship_type and notes if manually set if not existing.get("notes", "").startswith("Synced from"): # Keep existing notes pass else: existing["notes"] = contact["notes"] logger.debug(f"Updated contact: {contact['name']}") else: # New contact merged_contacts[key] = contact logger.info(f"Added new contact: {contact['name']}") # Convert back to list final_contacts = list(merged_contacts.values()) # Sort by name final_contacts.sort(key=lambda c: c["name"]) # Write to file output_path.parent.mkdir(parents=True, exist_ok=True) output_data = { "_comment": "Auto-synced from macOS Contacts (Sprint 2)", "_instructions": "You can manually edit this file. Changes to relationship_type and notes will be preserved on next sync.", "_last_sync": None, # Will be set after successful write "contacts": final_contacts } try: with open(output_path, 'w') as f: json.dump(output_data, f, indent=2) logger.info(f"Synced {len(final_contacts)} contacts to {output_path}") return len(final_contacts) except Exception as e: logger.error(f"Failed to write contacts file: {e}") return 0 def main(): """Main entry point for contact sync script.""" parser = argparse.ArgumentParser( description="Sync contacts from macOS Contacts to JSON config" ) parser.add_argument( "--output", "-o", type=Path, default=Path("config/contacts.json"), help="Output JSON file path (default: config/contacts.json)" ) parser.add_argument( "--include-no-phone", action="store_true", help="Include contacts without phone numbers" ) parser.add_argument( "--no-merge", action="store_true", help="Don't merge with existing contacts (overwrite)" ) parser.add_argument( "--verbose", "-v", action="store_true", help="Verbose logging" ) args = parser.parse_args() # Set log level if args.verbose: logging.getLogger().setLevel(logging.DEBUG) # Sync contacts count = sync_contacts( output_path=args.output, filter_no_phone=not args.include_no_phone, merge_existing=not args.no_merge ) if count > 0: print(f"\n✅ Successfully synced {count} contacts to {args.output}") print(f"\nNext steps:") print(f"1. Review the synced contacts: cat {args.output}") print(f"2. Edit relationship_type and notes as needed") print(f"3. Restart MCP server to load updated contacts") return 0 else: print("\n❌ Contact sync failed. Check logs for details.") return 1 if __name__ == "__main__": sys.exit(main())

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wolfiesch/imessage-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server