Skip to main content
Glama

contacts

Search and retrieve contacts from the Apple Contacts app by entering a full or partial name. Use to quickly access specific or complete contact lists directly via the Apple MCP Server.

Instructions

Search and retrieve contacts from Apple Contacts app

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
nameNoName to search for (optional - if not provided, returns all contacts). Can be partial name to search.

Implementation Reference

  • Main execution handler for the 'contacts' tool. Loads the contacts utils module and either searches for a specific contact by name using findNumber or lists all contacts with phone numbers using getAllNumbers. Handles access errors and formats the response.
    case "contacts": {
    	if (!isContactsArgs(args)) {
    		throw new Error("Invalid arguments for contacts tool");
    	}
    
    	try {
    		const contactsModule = await loadModule("contacts");
    
    		if (args.name) {
    			const numbers = await contactsModule.findNumber(args.name);
    			return {
    				content: [
    					{
    						type: "text",
    						text: numbers.length
    							? `${args.name}: ${numbers.join(", ")}`
    							: `No contact found for "${args.name}". Try a different name or use no name parameter to list all contacts.`,
    					},
    				],
    				isError: false,
    			};
    		} else {
    			const allNumbers = await contactsModule.getAllNumbers();
    			const contactCount = Object.keys(allNumbers).length;
    
    			if (contactCount === 0) {
    				return {
    					content: [
    						{
    							type: "text",
    							text: "No contacts found in the address book. Please make sure you have granted access to Contacts.",
    						},
    					],
    					isError: false,
    				};
    			}
    
    			const formattedContacts = Object.entries(allNumbers)
    				.filter(([_, phones]) => phones.length > 0)
    				.map(([name, phones]) => `${name}: ${phones.join(", ")}`);
    
    			return {
    				content: [
    					{
    						type: "text",
    						text:
    							formattedContacts.length > 0
    								? `Found ${contactCount} contacts:\n\n${formattedContacts.join("\n")}`
    								: "Found contacts but none have phone numbers. Try searching by name to see more details.",
    					},
    				],
    				isError: false,
    			};
    		}
    	} catch (error) {
    		const errorMessage = error instanceof Error ? error.message : String(error);
    		return {
    			content: [
    				{
    					type: "text",
    					text: errorMessage.includes("access") ? errorMessage : `Error accessing contacts: ${errorMessage}`,
    				},
    			],
    			isError: true,
    		};
    	}
    }
  • tools.ts:3-15 (schema)
    Input schema for the 'contacts' tool defining an optional 'name' parameter of type string for searching contacts.
    const CONTACTS_TOOL: Tool = {
        name: "contacts",
        description: "Search and retrieve contacts from Apple Contacts app",
        inputSchema: {
          type: "object",
          properties: {
            name: {
              type: "string",
              description: "Name to search for (optional - if not provided, returns all contacts). Can be partial name to search."
            }
          }
        }
      };
  • tools.ts:294-296 (registration)
    Registration of the 'contacts' tool as part of the exported tools array used by the MCP server for ListTools requests.
    const tools = [CONTACTS_TOOL, NOTES_TOOL, MESSAGES_TOOL, MAIL_TOOL, REMINDERS_TOOL, CALENDAR_TOOL, MAPS_TOOL];
    
    export default tools;
  • Helper function getAllNumbers() retrieves all contacts with phone numbers from Apple Contacts app using AppleScript.
    async function getAllNumbers(): Promise<{ [key: string]: string[] }> {
    	try {
    		const accessResult = await requestContactsAccess();
    		if (!accessResult.hasAccess) {
    			throw new Error(accessResult.message);
    		}
    
    		const script = `
    tell application "Contacts"
        set contactList to {}
        set contactCount to 0
    
        -- Get a limited number of people to avoid performance issues
        set allPeople to people
    
        repeat with i from 1 to (count of allPeople)
            if contactCount >= ${CONFIG.MAX_CONTACTS} then exit repeat
    
            try
                set currentPerson to item i of allPeople
                set personName to name of currentPerson
                set personPhones to {}
    
                try
                    set phonesList to phones of currentPerson
                    repeat with phoneItem in phonesList
                        try
                            set phoneValue to value of phoneItem
                            if phoneValue is not "" then
                                set personPhones to personPhones & {phoneValue}
                            end if
                        on error
                            -- Skip problematic phone entries
                        end try
                    end repeat
                on error
                    -- Skip if no phones or phones can't be accessed
                end try
    
                -- Only add contact if they have phones
                if (count of personPhones) > 0 then
                    set contactInfo to {name:personName, phones:personPhones}
                    set contactList to contactList & {contactInfo}
                    set contactCount to contactCount + 1
                end if
            on error
                -- Skip problematic contacts
            end try
        end repeat
    
        return contactList
    end tell`;
    
    		const result = (await runAppleScript(script)) as any;
    
    		// Convert AppleScript result to our format
    		const resultArray = Array.isArray(result) ? result : result ? [result] : [];
    		const phoneNumbers: { [key: string]: string[] } = {};
    
    		for (const contact of resultArray) {
    			if (contact && contact.name && contact.phones) {
    				phoneNumbers[contact.name] = Array.isArray(contact.phones)
    					? contact.phones
    					: [contact.phones];
    			}
    		}
    
    		return phoneNumbers;
    	} catch (error) {
    		console.error(
    			`Error getting all contacts: ${error instanceof Error ? error.message : String(error)}`,
    		);
    		return {};
    	}
    }
  • Helper function findNumber(name) finds phone numbers for a given contact name using AppleScript search and fuzzy fallback matching.
    async function findNumber(name: string): Promise<string[]> {
    	try {
    		const accessResult = await requestContactsAccess();
    		if (!accessResult.hasAccess) {
    			throw new Error(accessResult.message);
    		}
    
    		if (!name || name.trim() === "") {
    			return [];
    		}
    
    		const searchName = name.toLowerCase().trim();
    
    		// First try exact and partial matching with AppleScript
    		const script = `
    tell application "Contacts"
        set matchedPhones to {}
        set searchText to "${searchName}"
    
        -- Get a limited number of people to search through
        set allPeople to people
        set foundExact to false
        set partialMatches to {}
    
        repeat with i from 1 to (count of allPeople)
            if i > ${CONFIG.MAX_CONTACTS} then exit repeat
    
            try
                set currentPerson to item i of allPeople
                set personName to name of currentPerson
                set lowerPersonName to (do shell script "echo " & quoted form of personName & " | tr '[:upper:]' '[:lower:]'")
    
                -- Check for exact match first (highest priority)
                if lowerPersonName is searchText then
                    try
                        set phonesList to phones of currentPerson
                        repeat with phoneItem in phonesList
                            try
                                set phoneValue to value of phoneItem
                                if phoneValue is not "" then
                                    set matchedPhones to matchedPhones & {phoneValue}
                                    set foundExact to true
                                end if
                            on error
                                -- Skip problematic phone entries
                            end try
                        end repeat
                        if foundExact then exit repeat
                    on error
                        -- Skip if no phones
                    end try
                -- Check if search term is contained in name (partial match)
                else if lowerPersonName contains searchText or searchText contains lowerPersonName then
                    try
                        set phonesList to phones of currentPerson
                        repeat with phoneItem in phonesList
                            try
                                set phoneValue to value of phoneItem
                                if phoneValue is not "" then
                                    set partialMatches to partialMatches & {phoneValue}
                                end if
                            on error
                                -- Skip problematic phone entries
                            end try
                        end repeat
                    on error
                        -- Skip if no phones
                    end try
                end if
            on error
                -- Skip problematic contacts
            end try
        end repeat
    
        -- Return exact matches if found, otherwise partial matches
        if foundExact then
            return matchedPhones
        else
            return partialMatches
        end if
    end tell`;
    
    		const result = (await runAppleScript(script)) as any;
    		const resultArray = Array.isArray(result) ? result : result ? [result] : [];
    
    		// If no matches found with AppleScript, try comprehensive fuzzy matching
    		if (resultArray.length === 0) {
    			console.error(
    				`No AppleScript matches for "${name}", trying comprehensive search...`,
    			);
    			const allNumbers = await getAllNumbers();
    
    			// Helper function to clean name for better matching (remove emojis, extra chars)
    			const cleanName = (name: string) => {
    				return (
    					name
    						.toLowerCase()
    						// Remove emojis and special characters
    						.replace(
    							/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu,
    							"",
    						)
    						// Remove hearts and other symbols
    						.replace(/[♥️❤️💙💚💛💜🧡🖤🤍🤎]/g, "")
    						// Remove extra whitespace
    						.replace(/\s+/g, " ")
    						.trim()
    				);
    			};
    
    			// Try multiple fuzzy matching strategies
    			const strategies = [
    				// Exact match (case insensitive)
    				(personName: string) => cleanName(personName) === searchName,
    				// Exact match with cleaned name vs cleaned search
    				(personName: string) => {
    					const cleanedPerson = cleanName(personName);
    					const cleanedSearch = cleanName(name);
    					return cleanedPerson === cleanedSearch;
    				},
    				// Starts with search term (cleaned)
    				(personName: string) => cleanName(personName).startsWith(searchName),
    				// Contains search term (cleaned)
    				(personName: string) => cleanName(personName).includes(searchName),
    				// Search term contains person name (for nicknames, cleaned)
    				(personName: string) => searchName.includes(cleanName(personName)),
    				// First name match (handle variations)
    				(personName: string) => {
    					const cleanedName = cleanName(personName);
    					const firstWord = cleanedName.split(" ")[0];
    					return (
    						firstWord === searchName ||
    						firstWord.startsWith(searchName) ||
    						searchName.startsWith(firstWord) ||
    						// Handle repeated)
    						firstWord.replace(/(.)\1+/g, "$1") === searchName ||
    						searchName.replace(/(.)\1+/g, "$1") === firstWord
    					);
    				},
    				// Last name match
    				(personName: string) => {
    					const cleanedName = cleanName(personName);
    					const nameParts = cleanedName.split(" ");
    					const lastName = nameParts[nameParts.length - 1];
    					return lastName === searchName || lastName.startsWith(searchName);
    				},
    				// Substring match in any word
    				(personName: string) => {
    					const cleanedName = cleanName(personName);
    					const words = cleanedName.split(" ");
    					return words.some(
    						(word) =>
    							word.includes(searchName) ||
    							searchName.includes(word) ||
    							word.replace(/(.)\1+/g, "$1") === searchName,
    					);
    				},
    			];
    
    			// Try each strategy until we find matches
    			for (const strategy of strategies) {
    				const matches = Object.keys(allNumbers).filter(strategy);
    				if (matches.length > 0) {
    					console.error(
    						`Found ${matches.length} matches using fuzzy strategy for "${name}": ${matches.join(", ")}`,
    					);
    					// Return numbers from the first match for consistency
    					return allNumbers[matches[0]] || [];
    				}
    			}
    		}
    
    		return resultArray.filter((phone: any) => phone && phone.trim() !== "");
    	} catch (error) {
    		console.error(
    			`Error finding contact: ${error instanceof Error ? error.message : String(error)}`,
    		);
    		// Final fallback - try simple fuzzy matching
    		try {
    			const allNumbers = await getAllNumbers();
    			const searchName = name.toLowerCase().trim();
    			const closestMatch = Object.keys(allNumbers).find(
    				(personName) =>
    					personName.toLowerCase().includes(searchName) ||
    					searchName.includes(personName.toLowerCase()),
    			);
    			if (closestMatch) {
    				console.error(`Fallback found match for "${name}": ${closestMatch}`);
    				return allNumbers[closestMatch];
    			}
    		} catch (fallbackError) {
    			console.error(`Fallback search also failed: ${fallbackError}`);
    		}
    		return [];
    	}
    }
Install Server

Other Tools

Related Tools

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/supermemoryai/apple-mcp'

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