Skip to main content
Glama
Contacts.swift10.7 kB
import Contacts import Foundation import JSONSchema import OSLog import Ontology import OrderedCollections private let log = Logger.service("contacts") private let contactKeys = [ CNContactTypeKey, CNContactGivenNameKey, CNContactFamilyNameKey, CNContactBirthdayKey, CNContactOrganizationNameKey, CNContactJobTitleKey, CNContactPhoneNumbersKey, CNContactEmailAddressesKey, CNContactInstantMessageAddressesKey, CNContactSocialProfilesKey, CNContactUrlAddressesKey, CNContactPostalAddressesKey, CNContactRelationsKey, ] as [CNKeyDescriptor] private let contactProperties: OrderedDictionary<String, JSONSchema> = [ "givenName": .string(), "familyName": .string(), "organizationName": .string(), "jobTitle": .string(), "phoneNumbers": .object( properties: [ "mobile": .string(), "work": .string(), "home": .string(), ], additionalProperties: true ), "emailAddresses": .object( properties: [ "work": .string(), "home": .string(), ], additionalProperties: true ), "postalAddresses": .object( properties: [ "work": .object( properties: [ "street": .string(), "city": .string(), "state": .string(), "postalCode": .string(), "country": .string(), ] ), "home": .object( properties: [ "street": .string(), "city": .string(), "state": .string(), "postalCode": .string(), "country": .string(), ] ), ], additionalProperties: true ), "birthday": .object( properties: [ "day": .integer(minimum: 1, maximum: 31), "month": .integer(minimum: 1, maximum: 12), "year": .integer(), ], required: ["day", "month"] ), ] final class ContactsService: Service { private let contactStore = CNContactStore() static let shared = ContactsService() var isActivated: Bool { get async { let status = CNContactStore.authorizationStatus(for: .contacts) return status == .authorized } } func activate() async throws { log.debug("Activating contacts service") let status = CNContactStore.authorizationStatus(for: .contacts) switch status { case .authorized: log.debug("Contacts access authorized") return case .denied: log.error("Contacts access denied") throw NSError( domain: "ContactsService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Contacts access denied"] ) case .restricted: log.error("Contacts access restricted") throw NSError( domain: "ContactsService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Contacts access restricted"] ) case .notDetermined: log.debug("Requesting contacts access") _ = try await contactStore.requestAccess(for: .contacts) @unknown default: log.error("Unknown contacts authorization status") throw NSError( domain: "ContactsService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown contacts authorization status"] ) } } var tools: [Tool] { Tool( name: "contacts_me", description: "Get contact information about the user, including name, phone number, email, birthday, relations, address, online presence, and occupation. Always run this tool when the user asks a question that requires personal information about themselves.", inputSchema: .object( properties: [:], additionalProperties: false ), annotations: .init( title: "Who Am I?", readOnlyHint: true, openWorldHint: false ) ) { _ in let contact = try self.contactStore.unifiedMeContactWithKeys(toFetch: contactKeys) return Person(contact) } Tool( name: "contacts_search", description: "Search contacts by name, phone number, and/or email", inputSchema: .object( properties: [ "name": .string( description: "Name to search for" ), "phone": .string( description: "Phone number to search for" ), "email": .string( description: "Email address to search for" ), ], additionalProperties: false ), annotations: .init( title: "Search Contacts", readOnlyHint: true, openWorldHint: false ) ) { arguments in var predicates: [NSPredicate] = [] if case let .string(name) = arguments["name"] { let normalizedName = name.trimmingCharacters(in: .whitespaces) if !normalizedName.isEmpty { predicates.append(CNContact.predicateForContacts(matchingName: normalizedName)) } } if case let .string(phone) = arguments["phone"] { let phoneNumber = CNPhoneNumber(stringValue: phone) predicates.append(CNContact.predicateForContacts(matching: phoneNumber)) } if case let .string(email) = arguments["email"] { // Normalize email to lowercase let normalizedEmail = email.trimmingCharacters(in: .whitespaces).lowercased() if !normalizedEmail.isEmpty { predicates.append( CNContact.predicateForContacts(matchingEmailAddress: normalizedEmail)) } } guard !predicates.isEmpty else { throw NSError( domain: "ContactsService", code: 1, userInfo: [ NSLocalizedDescriptionKey: "At least one valid search parameter is required" ] ) } // Combine predicates with AND if multiple criteria are provided let finalPredicate = predicates.count == 1 ? predicates[0] : NSCompoundPredicate(andPredicateWithSubpredicates: predicates) let contacts = try self.contactStore.unifiedContacts( matching: finalPredicate, keysToFetch: contactKeys ) return contacts.compactMap { Person($0) } } Tool( name: "contacts_update", description: "Update an existing contact's information. Only provide values for properties that need to be changed; omit any properties that should remain unchanged.", inputSchema: .object( properties: ([ "identifier": .string( description: "Unique identifier of the contact to update" ) ] as OrderedDictionary).merging( contactProperties, uniquingKeysWith: { new, _ in new }), required: ["identifier"] ), annotations: .init( title: "Update Contact", readOnlyHint: false, destructiveHint: true, openWorldHint: false ) ) { arguments in guard case let .string(identifier) = arguments["identifier"], !identifier.isEmpty else { throw NSError( domain: "ContactsService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Valid contact identifier required"] ) } // Fetch the mutable copy of the contact let predicate = CNContact.predicateForContacts(withIdentifiers: [identifier]) let contact = try self.contactStore.unifiedContacts(matching: predicate, keysToFetch: contactKeys) .first? .mutableCopy() as? CNMutableContact guard let updatedContact = contact else { throw NSError( domain: "ContactsService", code: 2, userInfo: [ NSLocalizedDescriptionKey: "Contact not found with identifier: \(identifier)" ] ) } // Update all properties updatedContact.populate(from: arguments) // Create a save request let saveRequest = CNSaveRequest() saveRequest.update(updatedContact) // Save the changes try self.contactStore.execute(saveRequest) return Person(updatedContact) } Tool( name: "contacts_create", description: "Create a new contact with the specified information.", inputSchema: .object( properties: contactProperties, required: ["givenName"] ), annotations: .init( title: "Create Contact", readOnlyHint: false, openWorldHint: false ) ) { arguments in // Create and populate a new contact let newContact = CNMutableContact() newContact.populate(from: arguments) // Validate that given name is provided and not empty if newContact.givenName.isEmpty { throw NSError( domain: "ContactsService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Given name is required"] ) } // Create a save request let saveRequest = CNSaveRequest() saveRequest.add(newContact, toContainerWithIdentifier: nil) // Execute the save request try self.contactStore.execute(saveRequest) return Person(newContact) } } }

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/mattt/iMCP'

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