Unofficial Apple Music MCP v0.1.0
Added — update checking, with a security-advisory escalation
update-checkcommand and a once-a-day background check. Asks GitHub for
the latest release and for published security advisories, compares against the
installed version, and reports. It never downloads, installs, or opens a URL:
this process holds the Automation grant, so applying an update automatically
would turn a compromised release channel into code execution with Music
control.- Advisories are not suppressible. A routine update notice is shown once per
version so a daily check can't become a daily nag; an advisory whose affected
range covers the running version re-notifies until the install is out of range.
Ranges that can't be parsed fail closed — reported as "not affected", because a
false alarm trains people to ignore the real one. - Everything the API returns is treated as hostile. Tags are validated against
a version shape (with length-capped components) rather than escaped, URLs must
live under this repo, and free text is stripped of control characters and capped
before reaching a terminal or a notification. - Runs in the helper, forked so a slow lookup never stalls
accept(), and
triggered by a client connecting rather than by a timer of its own. Notifications
go through the single existingosascriptcall site, keeping the
process-execution surface at one module. - Opt out with
APPLEMUSIC_MCP_NO_UPDATE_CHECK=1.statusalways reports the
last result, including one already dismissed. Documented in SECURITY.md.
Added — standalone app and scoped permissions
AppleMusicMCP.app— a self-contained bundle with its own vendored Python
(tools/build-app.sh,make app). No Python, Homebrew, or command line
needed: unzip, drag to /Applications, double-click once. Relocatable by
construction — no venv and no absolute paths are baked in.- First-run setup asks before each step and each is skippable: install the
background helper, add the Claude Desktop entry, request the Music permission.
The Claude config is merged (other servers preserved), backed up first,
written atomically, and its mode preserved; an unparseable config is left
untouched rather than overwritten. A dialog that fails to display reads as
"skip", never as consent. - Scoped permission transport —
shim(what the client spawns; no
permissions, no Apple Events) talking over a0600unix socket tohelper
(started by launchd from the bundle; owns the Automation grant). This is what
stops the grant landing on your terminal. See
docs/PERMISSIONS.md. install.shfor source installs (private0700virtualenv,--scoped,
--uninstall), andmake releaseproducing wheel, sdist, signed.app, zip
andSHA256SUMS.txt.tools/make-signing-cert.sh— creates a self-signed code-signing certificate
in its own keychain, so builds are signed non-interactively. macOS keys the
Automation grant on the signing identity, so a stable certificate is what
stops the permission being re-prompted after every rebuild. (Self-signed is
not notarized: someone else downloading the app still has to allow it in
System Settings → Privacy & Security.)- docs/COMPARISON.md — an honest comparison against the
three most-starred Apple Music MCP servers, including where each is the
better choice than this one.
Forked from epheterson/applemusic-mcp
at 0acf697 (v0.18.5+). The goal of the fork is a smaller capability surface,
not more features: everything that let the server reach beyond Apple Music has
been removed, and the removals are enforced by tests rather than by convention.
Removed — capabilities
- Accessibility / UI automation. All 31 functions that drove Music.app
throughSystem Events(synthetic keystrokes, AX-tree walks, menu clicks) and
the four JXA helpers that posted synthetic mouse events viaCGEventPostto
the HID event tap. Accessibility is system-wide synthetic input and cannot be
scoped to one app; it was the broadest permission the project required. - Browser automation.
browser.py(Playwright/Chrome), launched with real
Keychain access and extensions enabled so Touch ID sign-in worked — a full
browser-automation handle held in-process. - Safari Apple-Events JavaScript.
safari.pyandsafari_player.py, which
readdocument.cookiefrom the user's signed-in Safari to harvest the
media-user-token. This required "Allow JavaScript from Apple Events", which
is not scoped to music.apple.com — it grants JS execution in every Safari
tab, making it broader than Accessibility for data theft. - The unofficial amp-api rail.
amp_api.pytalked to
amp-api.music.apple.comusing theAMPWebPlaytoken scraped out of Apple's
web-player JS bundle plus the harvested cookie. - URL handoff to the OS. No code path passes a URL to
openor
webbrowser.playback(url=)now parses a URL for its catalog id and never
fetches, opens, or navigates to it. - The
queuetool. Up Next is state inside the web player's MusicKit
instance; it cannot exist without the web player. keyring,playwright,pyobjc-framework-Quartzdependencies. Runtime
dependencies drop from 7 to 4 (mcp,pyjwt,requests,cryptography).
Fixed — inherited security issues
- Apple Music URL validation was bypassable.
open_catalog_songused
startswith("https://music.apple.com"), which accepts
https://music.apple.com.attacker.tld/…and
https://music.apple.com@attacker.tld/…, then passed the URL toopen.
Replaced with stricturlparsehostname matching, https-only, parse-only. exports://path-traversal guard was inert.is_relative_toon an
unresolved path returns True forcache/../../x, and the check ran after
.exists(). Now resolves both sides and rejects absolute or..components.- Destructive operations acted on a substring guess.
delete_playlist,
rename_playlist, andremove_from_libraryusedname containsand took the
first match, so removing "Love" could permanently delete an unrelated track.
They now refuse when the name matches more than one item and list the
candidates. (Upstream's web rail already did this; the native rail never did,
so keeping only the native rail would have kept only the unsafe behaviour.) clean_onlysilently lost its verification signal in CSV and exported
JSON. The note is suppressed for those formats on the grounds that each row
carriesexplicit, but the CSV field list and the JSON export key set both
dropped the column — so a clean-filtered export read as vetted while
containing unverified tracks.explicitis now carried in both.- State directories outside the config dir were world-readable. The cache
root holds the audit log, library snapshots, and library exports, created
0755/0644. All state roots are now 0700. storefrontwas unvalidated and interpolated into API URL paths; now
constrained to a two-letter country code.
Added
tests/test_capability_invariants.py— 50 tests asserting the removals hold:
no forbidden capability appears in code (comments and docstrings may still
name one to explain the removal),subprocessis confined to one module and
one binary (osascript, argv list, always with a timeout), the removed
modules stay unimportable, the MCP tool inventory is locked to six tools,
hostile URLs are rejected, the export reader cannot escape its directory, and
state dirs are 0700.rate_limit.py— the credential-free remnant ofamp_api: the sticky-429
marker that lets an empty result be reported as "rate limited" rather than
"no such song".- Tokenless catalog search via Apple's public iTunes Search API, replacing the
UI-scraping fallback. No credential, no Accessibility, and it carries an
explicit flag soclean_onlyis verified rather than assumed.
Changed
- macOS-only. Off-macOS code paths return a clear error instead of routing to a
removed rail. - One playback engine (
native). A request for a removed engine
(safari/chrome/web/api) is refused, never silently downgraded — a
caller must never believe it is driving an isolated browser session when it is
driving the real library. - Default install stores no credentials at all. Library, playlists, ratings
and playback need none. A developer token (login --dev) is opt-in and only
required to add a catalog track you don't already own. login --devprints the authorization URL instead of opening a browser.