resolve_contact
Resolve iMessage handles to names or names to handles by checking a local cache and macOS AddressBook.
Instructions
Resolve an iMessage handle to a name, or a name to handles.
Provide either handle (phone like +15551234567 or email) or name.
Checks a local JSON cache first, then falls back to the macOS AddressBook
SQLite. New hits are written back to the cache for future sessions.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | ||
| handle | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/imessage_mcp/server.py:67-81 (handler)The @mcp.tool()-decorated handler that resolves contacts; delegates to contacts.resolve_handle() and contacts.resolve_name().
@mcp.tool() def resolve_contact( name: str | None = None, handle: str | None = None ) -> dict[str, Any]: """Resolve an iMessage handle to a name, or a name to handles. Provide either `handle` (phone like +15551234567 or email) or `name`. Checks a local JSON cache first, then falls back to the macOS AddressBook SQLite. New hits are written back to the cache for future sessions. """ if handle: return contacts.resolve_handle(handle) if name: return {"name": name, "matches": contacts.resolve_name(name)} raise ValueError("Provide 'name' or 'handle'") - src/imessage_mcp/server.py:67-69 (registration)Registration via @mcp.tool() decorator on FastMCP('imessage') instance.
@mcp.tool() def resolve_contact( name: str | None = None, handle: str | None = None - src/imessage_mcp/contacts.py:194-207 (helper)Helper function that resolves a handle (phone/email) to a contact name, checking cache then AddressBook.
def resolve_handle(handle: str) -> dict[str, Any]: """Resolve an iMessage handle (phone/email) to a contact name.""" with _lock: cache = _load_cache() entry = cache.get(handle) if entry and entry.get("name"): return {"handle": handle, "name": entry["name"], "source": entry.get("source", "cache")} name = _ab_lookup_by_handle(handle) if name: cache.setdefault(handle, {}) cache[handle].update({"handle": handle, "name": name, "source": "addressbook.sqlite"}) _save_cache() return {"handle": handle, "name": name, "source": "addressbook.sqlite"} return {"handle": handle, "name": None, "source": None} - src/imessage_mcp/contacts.py:210-248 (helper)Helper function that resolves a contact name to handles, checking cache then AddressBook and normalizing phones to E.164.
def resolve_name(name: str) -> list[dict[str, Any]]: """Resolve a contact name to iMessage handles (phones normalized to E.164).""" with _lock: cache = _load_cache() needle = name.strip().lower() cache_hits = [ {"handle": h, "name": e.get("name"), "source": "cache"} for h, e in cache.items() if e.get("name") and needle in e["name"].lower() ] ab_hits = _ab_lookup_by_name(name) # Flatten AB hits into one entry per phone/email. ab_flat: list[dict[str, Any]] = [] for hit in ab_hits: for p in hit.get("phones", []): normalized = _normalize_phone_to_handle(p) if normalized: ab_flat.append({"handle": normalized, "name": hit["name"], "source": "addressbook.sqlite"}) for e in hit.get("emails", []): if e: ab_flat.append({"handle": e.lower(), "name": hit["name"], "source": "addressbook.sqlite"}) # Merge by handle, preferring cache source attribution. seen: dict[str, dict[str, Any]] = {} for item in cache_hits + ab_flat: seen.setdefault(item["handle"], item) # Write any new AB-only entries back to cache (no stats) so future sessions skip the AB lookup. dirty = False for item in ab_flat: h = item["handle"] if h not in cache or not cache[h].get("name"): cache.setdefault(h, {}) cache[h].update({"handle": h, "name": item["name"], "source": "addressbook.sqlite"}) dirty = True if dirty: _save_cache() return list(seen.values())