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

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It states the tool searches and retrieves contacts, implying a read-only operation, but doesn't specify permissions needed, whether it accesses local or synced data, rate limits, or what the return format looks like (e.g., list of contacts with fields). This leaves significant gaps for a tool interacting with personal data.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence with zero waste. It front-loads the core purpose ('Search and retrieve contacts') and specifies the source ('from Apple Contacts app'), making it easy to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (searching personal contacts), lack of annotations, and no output schema, the description is minimally adequate. It states what the tool does but omits critical context like return format, error handling, or data sensitivity considerations. It relies heavily on the schema for parameter details, leaving behavioral aspects underspecified.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the input schema fully documents the single optional parameter. The description adds no additional parameter semantics beyond what's in the schema (e.g., no examples of search syntax or format). This meets the baseline for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with a specific verb ('Search and retrieve') and resource ('contacts from Apple Contacts app'). It distinguishes itself from siblings like calendar or mail by focusing on contacts, but doesn't explicitly differentiate from potential contact-related tools that might exist in other contexts.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives. It doesn't mention any prerequisites, limitations, or scenarios where other tools might be more appropriate. The agent must infer usage from the purpose alone.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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