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

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

Input Schema (JSON Schema)

{ "properties": { "name": { "description": "Name to search for (optional - if not provided, returns all contacts). Can be partial name to search.", "type": "string" } }, "type": "object" }

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 []; } }

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