[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "secure-applemusic-mcp-for-osx" version = "0.3.1" description = "Control Apple Music on macOS with Claude: play, search, build playlists, add music to your library. No account, no API key, nothing stored." readme = "README.md" license = "MIT" requires-python = ">=3.10" authors = [ { name = "Eric Pheterson" }, # original author of applemusic-mcp { name = "jaminben" }, ] keywords = [ "mcp", "model-context-protocol", "mcp-server", "apple-music", "applemusic", "musickit", "itunes", "macos", "music", "playlist", "playlists", "claude", "anthropic", "ai", "llm", "llm-tools", "agent", ] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Environment :: MacOS X", # End users matter as much as developers here: the whole point of the fork # is that a non-developer can install it. "Intended Audience :: End Users/Desktop", "Intended Audience :: Developers", "Natural Language :: English", "License :: OSI Approved :: MIT License", "Operating System :: MacOS", "Operating System :: MacOS :: MacOS X", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: Players", ] dependencies = [ # Both majors are supported (see the import alias in server.py). The upper # bound stays: mcp 2.0.0 moved `mcp.server.fastmcp` with no deprecation, and an # unbounded requirement is what let that break every fresh install. Raise the # ceiling deliberately after testing, never by leaving it open. "mcp>=1.0.0,<3", "requests>=2.31.0", # Not used directly — `mcp` requires pyjwt[crypto], which pulls it in. The # ceiling exists because cryptography stopped publishing Intel macOS wheels # at 49, so without it an Intel `pip install` tries to compile Rust from # source and fails on any machine without the x86_64-apple-darwin toolchain. # Scoped by marker so Apple Silicon keeps current upstream. # # Known cost, from a security audit: on Intel this caps cryptography for the # WHOLE environment, so a co-installed package that does use x509 path # verification would be held below the 49.0.0 fixes (PYSEC-2026-3553/3554) # and the 50.0.0 PKCS#7 fix. Nothing here can reach those — the only # consumer is PyJWT's ES256 encode over an EC key, and requests uses the # stdlib ssl module — but the environment-wide effect is real. # # It cannot move to the dev-token extra: mcp pulls cryptography regardless, # so a cap that only applies with the extra would not bind. DROP THIS LINE # when cryptography ships Intel wheels again, or when mcp stops requiring # pyjwt[crypto]; check both before assuming it is still needed. "cryptography<49; platform_machine == 'x86_64'", # tomllib landed in 3.11; we still support 3.10, where the Codex/TOML client # config path needs the backport. Not installed on 3.11+. "tomli>=2.0.1; python_version < '3.11'", ] [project.optional-dependencies] # Only needed to SIGN an Apple Developer token from a .p8 — the optional rail # that buys a larger rate limit. `cryptography` dropped Intel macOS wheels at # 49, so keeping it in the base dependency set made every Intel install compile # Rust from source for a feature almost nobody uses. dev-token = ["pyjwt[crypto]>=2.8.0", "cryptography>=41.0.0"] dev = [ "pytest>=7.0.0", "pytest-mock>=3.0.0", "pytest-xdist>=3.0.0", # parallel test execution: pytest -n auto "responses>=0.23.0", "pytest-cov>=4.1.0", "black>=23.0.0", "ruff>=0.1.0", ] [project.scripts] secure-applemusic-mcp = "applemusic_mcp.cli:main" [project.urls] # PyPI renders these as sidebar links, and they are the only navigation the # page has. "Homepage" points at the landing page rather than the repo, because # someone arriving from a PyPI search wants to know what it does before they # want to read the source — the source is one row down. Homepage = "https://jaminben.github.io/secure-applemusic-mcp-for-osx/" Download = "https://github.com/jaminben/secure-applemusic-mcp-for-osx/releases/latest" Changelog = "https://github.com/jaminben/secure-applemusic-mcp-for-osx/blob/main/CHANGELOG.md" Repository = "https://github.com/jaminben/secure-applemusic-mcp-for-osx" Issues = "https://github.com/jaminben/secure-applemusic-mcp-for-osx/issues" "Upstream project" = "https://github.com/epheterson/applemusic-mcp" [tool.hatch.build.targets.wheel] packages = ["src/applemusic_mcp"] # NOTE: the MusicKit helper is force-included by scripts/wheel_tag.py, NOT here. # A static `force-include` fails the build outright when the file is absent # ("Forced include not found"), and the .app is a build artifact that is # gitignored — so a fresh clone, and every CI run, could not even install the # package. The hook adds it only when it has been built. # Not a pure-python wheel: it carries a macOS-only, Developer-ID-signed binary. # Without this the wheel is tagged py3-none-any and pip will happily install it # on Linux and Windows, where the helper is dead weight that cannot run. [tool.hatch.build.targets.wheel.hooks.custom] path = "scripts/wheel_tag.py" [tool.black] line-length = 100 target-version = ["py310"] [tool.ruff] line-length = 100 select = ["E", "F", "I", "W"] [tool.pytest.ini_options] markers = [ "slow: marks tests as slow (live AppleScript / Music.app I/O); deselect with '-m \"not slow\"'", "ui: marks the live API pre-release gate (real account mutations); require TEST_API=1", "ui_live: marks live Music.app UI-automation tests (native playback / catalog deep-link / UI search); require TEST_UI=1 + a signed-in, unlocked Mac with Accessibility granted", ] # Default: skip slow + live gates unless explicitly opted in. Local pre-commit # runs the fast subset; CI and `make test-all` run everything. addopts = "-m 'not slow and not ui and not ui_live'" [tool.coverage.run] source = ["applemusic_mcp"] branch = false [tool.coverage.report] # Lines that are genuinely impossible to exercise in a unit test (live Chrome # owner thread, raw network I/O, process entry points) carry an inline # `# pragma: no cover` and are validated by the live gates instead. exclude_also = [ "if __name__ == .__main__.:", "raise NotImplementedError", "if TYPE_CHECKING:", "@(abc\\.)?abstractmethod", ] # Regression floor, not a vanity number, and measured where the suite can # actually run: MACOS. On the Linux matrix 144 tests skip by design -- helper.py # is a launchd process, shim.py wants a unix socket, app_setup.py is the macOS # installer -- so coverage there measures the platform, not the tests. Gating it # on Linux (which is what this repo did) made a number nobody could ever satisfy. # # The real macOS figure for the mocked suite is 89.4%. The gap to 100 is mostly # helper.py (11%) and shim.py (20%): process entry points whose tests carry the # `slow` marker, because they need a signed-in Mac with Automation granted -- # which no CI runner has. `make test-all` runs those. # # 88 leaves a point of headroom under 89.4 so an unrelated refactor does not turn # CI red, while still catching a real slide. Raise it when the entry points get # reachable coverage; do not raise it by excluding them from the measurement. fail_under = 88 show_missing = true