Skip to content

Add Visual Voicemail service - #253

Merged
mattt merged 7 commits into
mattt:mainfrom
bradhallett:voicemail-service
Oct 12, 2026
Merged

mattt merged 7 commits into
mattt:mainfrom
bradhallett:voicemail-service

Conversation

@bradhallett

@bradhallett bradhallett commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Closes #252

What

Adds a Voicemail service that reads the voicemails the Phone app on macOS 26 and later syncs to the Mac — both carrier voicemails and FaceTime audio and video messages.

Three read-only tools:

  • voicemails_fetch — list messages (sender, date, duration, read state, type, transcript availability), with filters for sender, date range, and type
  • voicemail_transcript_fetch — flattened transcript text plus per-segment text and confidence
  • voicemail_audio_fetch — the message audio as audio/amr (legacy carrier voicemail) or audio/mp4 (FaceTime audio message and current carrier voicemail)

How

The facetimemessagestored daemon mirrors voicemails into a Core Data SQLite store inside the FaceTime group container (.../Data Store/FaceTimeMessageStore-local.sqlitedb), with message audio in an Assets tree beside the database. The schema is private to the daemon, so VoicemailStore reads only the columns it needs, through the same pattern the Phone service uses for call history: read-only SQLite with an immutable-URI fallback, and a folder grant on Data Store when the default path is not readable (the write-ahead log and the Assets tree sit beside the database, so the grant has to cover the folder).

Transcripts are inline NSKeyedArchiver blobs in two shapes — a transcript object (transcriptionString, confidence, segments) for carrier voicemails and a bare array of segment dictionaries for FaceTime messages; both are decoded.

Since the iCloud voicemail era, the daemon writes carrier voicemails as audio-only QuickTime .MOV files — the same file type FaceTime video messages carry — so .MOV rows are classified by inspecting the asset's tracks: a sound track with no video track is a carrier voicemail (returned as audio/mp4); a video track is a FaceTime video message, listed for completeness but not returned as audio. Assets that are missing or don't parse keep the conservative video-message default, and hasAudio reflects audio the tool can return. Type filters resolve .MOV rows by asset, with the result limit applied after classification.

The service registers beside PhoneService, gated on macOS 26 availability (VoicemailService.isSupported), so Macs without the Phone app never see it; the store itself is validated during activation, when the folder grant runs.

Testing

  • VoicemailStoreTests build a synthesized SQLite store from scratch — no fixture is copied from a real one — and cover row mapping, filters, deleted-message exclusion, both transcript shapes, audio resolution, 64-bit identifiers, and .MOV track-based classification (audio-only, video, unparseable, extended-size atoms, and malformed-size rejection without overflow)
  • swift format lint --strict --recursive . passes
  • xcodebuild -scheme iMCP -configuration Debug -destination 'platform=macOS' build succeeds
  • xcodebuild -scheme imcp-serverTests -destination 'platform=macOS' test passes (44 tests)
  • Read-only checks against a live store on a Mac running the Phone app: message listing, both transcript shapes, playable audio (including iCloud-era .MOV carrier voicemails), and type filters all resolve correctly

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Sandboxed availability detection can hide the service entirely, and activation does not establish its required folder grant.

Review effort: Balanced
Findings: 3 Medium severity

Open (3)
What changed in this PR

Adds a macOS 26+ Visual Voicemail service for listing messages and retrieving transcripts or audio.

Changes:

  • Adds three read-only voicemail tools.
  • Implements SQLite, transcript, audio, and permission handling.
  • Adds tests, service registration, documentation, and artwork.
File Description
README.md Documents voicemail support and database access.
iMCP.xcodeproj/​project.pbxproj Adds store implementation and tests to test target.
CLITests/​VoicemailStoreTests.swift Tests mapping, filtering, transcripts, and audio.
Assets/​voicemail.svg Adds voicemail artwork.
App/​Services/​VoicemailStore.swift Reads and decodes voicemail store data.
App/​Services/​Voicemail.swift Defines tools and access-grant flow.
App/​Controllers/​ServerController.swift Registers and configures the service.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread App/Services/Voicemail.swift
Comment thread App/Services/Voicemail.swift Outdated
Comment thread App/Services/VoicemailStore.swift
* Add VoicemailService with voicemails_fetch, voicemail_transcript_fetch,
  and voicemail_audio_fetch tools

* Read the store the Phone app syncs on macOS 26 and later: ZSTOREDMESSAGE
  rows, inline NSKeyedArchiver transcripts, and audio in the Assets tree

* Decode both transcript shapes: transcript objects with flattened text and
  segments, and bare segment arrays

* Gate service registration on the store's existence, so Macs without the
  Phone app are unaffected

* Request access to the Data Store folder, since the write-ahead log and
  the Assets tree sit beside the database

* List FaceTime video messages but return audio only for carrier
  voicemails and FaceTime audio messages

* Add VoicemailStoreTests against a synthesized store; no fixture is copied
  from a real one
- Register the service on macOS 26 and later instead of probing the
  store path, which the app sandbox hides before any grant is given;
  activation validates the store and requests the Data Store folder
- Implement Service activation: isActivated reports real access state
  and activate() runs the folder-grant flow when the service is toggled
  on, matching MessageService
- Escape SQL LIKE wildcards in the sender filter, so a literal backslash,
  percent, or underscore in an address matches only itself
- Add a regression test for wildcard senders

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It relies on private macOS data formats and security-scoped access behavior that require final validation on supported hardware.

Review effort: Balanced
Findings: None

Resolved since last review (3)

@mattt

mattt commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Hi @bradhallett. Thank you for all your work on this! I appreciate the detailed write-up in #252, the tests against a synthesized store, and how you worked through Copilot's feedback (and thanks for signing your commits, too). I'm excited to try it out today.

@bradhallett

Copy link
Copy Markdown
Contributor Author

Happy to help! Any feedback after trying it out?

@mattt

mattt commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Hi @bradhallett. Sorry, I haven't had a chance to try it yet. I'll follow up here as soon as I do.

…es (0D-10) to clear collision with MCPInitializeCompatibility
Since the iCloud voicemail era, the daemon stores carrier voicemails as
audio-only QuickTime .MOV files — the same ZFILETYPE FaceTime video
messages use — so every recent voicemail listed as a faceTimeVideo with
hasAudio false, and voicemail_audio_fetch rejected them.

The store now inspects each .MOV asset's tracks: a sound track with no
video track is a carrier voicemail, served as audio/mp4; a video track is
a FaceTime video message. Rows whose asset is missing or unparseable keep
the conservative faceTimeVideo default. Kind filters for the two .MOV
kinds select candidates by ZFILETYPE and resolve them by asset, with the
limit applied after classification.
A client hitting the missing-transcript error can fall back to fetching
the audio; the error message now says so.
@bradhallett

Copy link
Copy Markdown
Contributor Author

Two findings from testing against a live store on macOS 27.2, worth knowing before you try it:

Carrier voicemails are audio-only .MOV files now. Since the iCloud voicemail era (this Mac flipped over in July, on the 27 beta), facetimemessagestored writes every new carrier voicemail as a QuickTime .MOV with a single AAC track — the same ZFILETYPE FaceTime video messages carry. On the previous revision that typed every post-July voicemail as faceTimeVideo with hasAudio: false, and voicemail_audio_fetch rejected them. I pinned the taxonomy down against CallHistoryDB: the post-July .MOV rows correlate with cellular calls, .amr is the legacy carrier format, and .m4a rows are FaceTime audio messages.

af3e511 fixes the classification by inspecting each .MOV asset's tracks: a sound track with no video track is a carrier voicemail (served as audio/mp4), a video track is a FaceTime video message, and assets that are missing or don't parse keep the conservative default. The type filters resolve .MOV rows by asset, with the limit applied after classification. The suite is at 40 tests now; four new ones cover the classification and the limit ordering. 0d3ab92 also points the no-transcript error at voicemail_audio_fetch, since that audio is still available.

On untranscribed messages: 19 on this Mac, and most are hang-ups — 12 are 1–9 seconds long with tiny assets, nothing to transcribe. The other 7 have real speech (15–204s), including four max-length 203s carrier voicemails where the system engine marked transcription as failed. So a SpeechTranscriber fallback would only ever help that second group — feels like a follow-up at most, not part of this PR.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Bookmark persistence, 64-bit atom parsing, identifier binding, and large-video probing need correction.

4 open findings

🧠 Review effort: Balanced

Comment thread App/Services/Voicemail.swift Outdated
Comment thread App/Services/VoicemailStore.swift Outdated
Comment thread App/Services/VoicemailStore.swift
Comment thread App/Services/VoicemailStore.swift Outdated
- Create the Data Store bookmark with .withSecurityScope alongside
  .securityScopeAllowOnlyReadAccess, so it resolves as read-only
  security-scoped rather than as an ordinary bookmark.
- Probe .MOV assets through a memory-mapped read, so classification
  faults only the header pages it inspects instead of loading whole
  video assets.
- Parse extended-size QuickTime atoms: a size of 1 marks a 16-byte
  header whose 64-bit size follows the type, and traversal enters or
  skips such atoms correctly.
- Bind integer parameters with sqlite3_bind_int64 so Z_PK values above
  Int32.max keep resolving to their transcripts and audio.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Malformed movie metadata can crash listing, and one transcript error provides incorrect recovery guidance.

1 open finding
4 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Avoid directing clients to audio tools when audio is unavailable

App/​Services/​Voicemail.swift:390

This path does not establish that audio is available: it also handles video messages and voicemails whose asset is missing (hasAudio == false). The error therefore directs clients to another tool that is guaranteed to fail for some valid messages; either condition the advice on hasAudio or omit it.

🧠 Review effort: Balanced

Comment thread App/Services/VoicemailStore.swift Outdated
A malformed extended-size atom can declare a size up to Int.max;
computing offset + size then traps instead of breaking out of the
atom walk. Compare against the bytes remaining in the container so
damaged assets keep the documented conservative classification.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Filtered fetches perform unnecessary asset probes, and transcript failures can incorrectly claim audio is available.

0 open findings

1 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Avoid claiming audio is available when no transcript exists

App/​Services/​Voicemail.swift:390

This error always claims audio is available, but transcript-less FaceTime video messages and rows with missing assets explicitly have hasAudio == false. That sends clients to a tool guaranteed to fail; report only that no transcript exists (or condition the audio guidance on the resolved message's hasAudio).

Medium severity Stop probing assets after reaching the requested match limit

App/​Services/​VoicemailStore.swift:33

For carrier/video filters this loop probes every candidate .MOV asset before filtering and applying limit. Because the SQL result is already newest-first, classification can stop as soon as limit matching rows have been found; otherwise a request such as limit: 1 still opens every movie in a large store.

🧠 Review effort: Balanced

@mattt
mattt merged commit 42e63aa into mattt:main Oct 12, 2026
1 check passed
@mattt

mattt commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Thanks, @bradhallett. I really appreciate you building this out and working through every round of review.

While trying it with my own voicemail, I found a few things, which I fixed in #270:

  • voicemails_fetch now reads the inbox by default. The store keeps blocked and deleted messages in the same table (ZMAILBOXTYPE 1 and 2), so they were mixed into the results. A new mailbox parameter asks for them explicitly.
  • The message type comes from ZPROVIDER and ZMESSAGETYPE instead of the file extension. Some carrier voicemails are stored as m4a, and FaceTime video messages can be lowercase mov.
  • When ZDURATION is 0, the duration comes from the file's movie header.
  • Phone numbers in the sender filter match by digits, so (503) 555-0123 finds +15035550123.
  • The service is green now, to match its icon.

I also took the store tests out of the CLI test target. I'd like to move iMCP's database access into its own package, and test it thoroughly there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: Visual Voicemail service (list, transcripts, and audio for carrier and FaceTime voicemails)

3 participants