Music Assistant MCP
Allows searching and playing music from Apple Music, including playlists and radio mode, with scope restrictions and provider tiebreak support.
Allows searching and playing music from Spotify, including playlists and radio mode, with scope restrictions and provider tiebreak support.
Allows searching and playing music from Tidal, including playlists and radio mode, with scope restrictions and provider tiebreak support.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Music Assistant MCPPlay something relaxing"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Music Assistant MCP
Talk to your home audio system like a person, not an API.
You say | What happens |
"Play something relaxing" | Searches streaming playlists, picks the best match, plays it on the default player - no player id, no source picking |
"Play some jazz for a romantic dinner" | Mood/occasion query → finds a fitting playlist automatically, same as a human browsing Spotify would |
"Play The Prodigy in radio mode" | Resolves the artist, checks the actual provider can generate a mix before starting one, so it never silently no-ops |
"Play Coldplay from my local files" | Scopes the search to just the local/NAS library, skipping streaming entirely |
"Surprise me" | Builds an ad-hoc mix straight from the library - no search query needed at all |
"Join the LedFX player while this plays, then take it back out" | Groups/ungroups players on request |
One line of natural language in, the right thing playing out - every scenario above is tested against a real Music Assistant server, not just plausible-sounding docs (see Verified against a real server below for the bugs that were actually found and fixed getting there).
A from-scratch MCP server for Music Assistant, designed so an LLM agent needs almost no system prompt to drive it. Unlike raw/low-level MCP tool sets, the orchestration an agent would otherwise have to be told to do every turn (resolve a player, remember it, search before playing, apply a provider tiebreak, etc.) is baked into the tools themselves:
play— search + resolve player + provider tiebreak + play, in one call. Supportsscope="online"|"local"|"all"to restrict to streaming providers (Spotify/Tidal/Applerich thematic/"vibe" search) vs local file providers (SMB/WebDAV/local dir - literal matching), or
source="tidal"/"my-nas-share"to force one specific provider. Omittedscopedefaults to "online" (broadening to everything if nothing turns up there), andmedia_typesdefaults to["playlist"]- so "play something relaxing"/"play jazz" naturally searches streaming playlists first, same as a human would.radio_mode=truestarts a continuous radio mix seeded from the match instead of playing it once. If the top match turns out unplayable (e.g. an empty playlist), it automatically tries the next candidate before giving up.shuffle=true|falsesets the queue's shuffle mode as part of the same call (e.g. "play this playlist shuffled") - omitted leaves shuffle mode untouched.
play_random— build and play a random mix straight from the library, no search query needed - for "play something"/"surprise me"/"random mix from my local files" requests. Samescope/source/shuffleparams asplay(defaultscope="all"here, since there's no query to search "online" against - the point is usually to surface your own library).control— play/pause/stop/toggle/next/previous/seek, plus a read-onlystatus/get/now_playingquery for "what's playing?" (never sends a playback command).volume— level / relative adjust / mute, optionally group-wide.queue— get/shuffle/repeat/clear/move/remove, keyed off one player.getreturns a slimmed-down summary (current/next track, upcoming list), not MA's raw queue-item payload.transfer— move playback from one player to another.browse— walk a provider's library hierarchy.group— join/leave player groups (e.g. syncing a LedFX visualizer).search— raw lookup without playing (samescope/sourceparams asplay), for "what do you have for X" questions.list_players/list_providers— diagnostic/fallback only; other tools resolve players and providers themselves.
Serves over Streamable HTTP with a static bearer token, so it drops straight into an MCP Client HTTP node (e.g. n8n) the same way the previous stdio-only community server had to be bridged to get there.
Setup
cp .env.example .env # fill in MA_SERVER_URL, MCP_BEARER_TOKEN, DEFAULT_PLAYER_NAME, etc.
uv sync # or: pip install -e ".[dev]"
uv run music-assistant-mcpDEFAULT_PLAYER_NAME isn't optional in practice - leave it unset and any tool call that
doesn't explicitly name a player fails with No default player configured and none could be resolved (confirmed live). Set it to a name from list_players' output (e.g.
"Living Room"), not a player id.
The server listens on MCP_HOST:MCP_PORT (default 0.0.0.0:8005) and exposes the MCP
endpoint at /mcp, requiring Authorization: Bearer <MCP_BEARER_TOKEN>.
Config (.env)
Variable | Purpose |
| Music Assistant server, e.g. |
| Music Assistant auth token (required on schema ≥ 28 servers) |
| This server's own bind address |
| Shared secret clients must send as |
| Used when a tool call doesn't name a player |
| Comma-separated provider tiebreak order, e.g. |
|
|
Adding to an MCP client config
Unlike the old community server (stdio-only, spawned per-connection via uvx), this
server is a standalone long-running HTTP service - the client connects to it over the
network instead of launching it. For any MCP host config that supports a remote/HTTP
server entry (Claude Desktop, Cursor, etc.), that looks like:
{
"mcpServers": {
"music-assistant": {
"enabled": true,
"timeout": 60,
"transportType": "http",
"url": "http://<MCP_HOST>:<MCP_PORT>/mcp",
"headers": {
"Authorization": "Bearer <MCP_BEARER_TOKEN>"
}
}
}
}Replace <MCP_HOST>:<MCP_PORT> with wherever you're running this server (e.g.
192.168.1.50:8005) and <MCP_BEARER_TOKEN> with the value from your .env. This
requires the server to already be running (uv run music-assistant-mcp, or in Docker
below) - unlike the command/args/env style below, nothing gets spawned here.
Running via uvx straight from GitHub (stdio)
If your MCP host only supports the command/args/env, stdio-launched style of
config (the same shape the old community server used), set MCP_TRANSPORT=stdio and
it works the same way - no bearer token needed, since the host owns the process's
stdio pipes directly instead of talking to it over the network:
{
"mcpServers": {
"music-assistant": {
"enabled": true,
"timeout": 60,
"command": "uvx",
"args": [
"--from",
"git+https://github.com/shuricksumy/MCP-MusicAssistant",
"music-assistant-mcp"
],
"env": {
"MA_SERVER_URL": "http://<MA_SERVER_IP>:8095",
"MA_TOKEN": "<your-ma-token>",
"MCP_TRANSPORT": "stdio",
"DEFAULT_PLAYER_NAME": "<your-default-player-name>",
"SOURCE_PRIORITY": "tidal,spotify,apple_music"
},
"transportType": "stdio"
}
}
}DEFAULT_PLAYER_NAME matters here more than it does for n8n: without it, every play/
control/volume/etc. call that doesn't explicitly name a player fails with No default player configured and none could be resolved (confirmed live) - the whole
point of the tools resolving players themselves falls apart if there's nothing to fall
back to. Set it to one of the names list_players returns (e.g. "Living Room", not a
player id). SOURCE_PRIORITY is optional but worth setting explicitly since its
built-in default (tidal,spotify,apple_music) may not match the providers you actually
have configured.
Confirmed working locally: piping a real initialize request into
MCP_TRANSPORT=stdio uv run music-assistant-mcp returns a clean JSON-RPC response on
stdout with nothing else mixed in (logging goes to stderr, so it won't corrupt the
protocol stream).
Running in Docker
docker build -t music-assistant-mcp .
docker run -d --name music-assistant-mcp -p 8005:8005 \
-e MA_SERVER_URL=http://192.168.1.50:8095 \
-e MA_TOKEN=<your-ma-token> \
-e MCP_BEARER_TOKEN=<your-mcp-bearer-token> \
-e DEFAULT_PLAYER_NAME=<your-default-player-name> \
music-assistant-mcpBuilt and run-tested (docker build + docker run + a bearer-authed MCP initialize
call) as part of this repo's verification.
Pre-built multi-arch images
.github/workflows/docker-publish.yml builds and pushes linux/amd64 +
linux/arm64 images to GitHub Container Registry on every push to main and on
version tags (v*.*.*) - verified locally that the Dockerfile builds cleanly for both
architectures via docker buildx build --platform linux/amd64,linux/arm64. No extra
registry credentials to set up on the publishing side - it authenticates with the
repo's built-in GITHUB_TOKEN. The package may need to be marked public in the repo's
Packages settings the first time (GitHub defaults new packages to private).
docker pull ghcr.io/shuricksumy/music-assistant-mcp:mainRunning with docker-compose
docker-compose.yml pulls the pre-built image above (so the workflow needs to have run
at least once, and the package needs to be public) and reads its config from .env in
the same directory:
cp .env.example .env # fill it in first, see Config above
docker compose up -dUsing mcp-proxy (for hosts that only support stdio-launched servers)
Some MCP hosts only support the command/args/env, stdio-launch style of config
(like the old community server's uvx --from git+... entry) and can't point at a
remote HTTP URL directly. For those, use mcp-proxy
as a local stdio↔HTTP bridge in front of this server running in its container:
{
"mcpServers": {
"music-assistant": {
"enabled": true,
"timeout": 60,
"command": "uvx",
"args": [
"mcp-proxy",
"--transport",
"streamablehttp",
"--headers",
"Authorization",
"Bearer <MCP_BEARER_TOKEN>",
"http://<MCP_HOST>:<MCP_PORT>/mcp"
],
"transportType": "stdio"
}
}
}The container (or uv run music-assistant-mcp on bare metal) still has to be running
and reachable at <MCP_HOST>:<MCP_PORT> - mcp-proxy only bridges the host's stdio
expectation to it, it doesn't start the server itself.
Wiring into n8n
Point the existing "Music MCP Client" node's endpoint at this server's /mcp URL with
the bearer credential set to MCP_BEARER_TOKEN. Because player resolution and the
provider tiebreak now live in the tools, the AI Agent's system prompt can shrink
drastically - no STEP-by-STEP sequencing, player-id tracking, or source-tiebreak rules
needed. See PROMPT.md for a suggested replacement (persona + the one policy,
confirming before a queue clear, that the tools don't enforce on their own).
Built-in instructions (for weaker/local models)
The server also declares an instructions string as part of the MCP initialize
response (FastMCP("music-assistant", instructions=...) in server.py) - most MCP
clients (confirmed for the stdio transport by inspecting the raw initialize response)
surface this to the model automatically, independent of whatever system prompt the host
does or doesn't set. This exists because a smaller/local model (tested against a 9B
model run through LM Studio) didn't reliably call play on its own without being told
directly - "act immediately, don't ask permission, call the tool" needed to be explicit
rather than something the model was expected to reason its way to. If you're running a
frontier model this instructions text is redundant with PROMPT.md/common sense; if
you're running something smaller/local, it's the part actually carrying the weight, and
you likely don't need PROMPT.md as a system prompt at all beyond the confirm-before-clear
policy it adds on top.
Development
uv run pytestTests cover the pure logic (player/provider resolution, source/scope tiebreak, queue action dispatch) against a fake Music Assistant client.
Verified against a real server (schema version 31)
All four end-to-end scenarios below were run against a live server and fixed until green - the bugs found along the way are worth knowing about if you extend this further:
play(query="Relaxing music", player="Living Room")— works.play(query="The Prodigy", player="Living Room", media_types=["artist"], radio_mode=True)— works. Found and fixed: a provider can return an irrelevant top "match" for an identity lookup (Tidal returned "Fatboy Slim" for "The Prodigy") -pick_best()now requires a name-match to the query forartist/track/albumbefore applying source priority; playlists/radio stay priority-only since themed queries (e.g. "jazz vibes") legitimately won't literally match a provider's own curated name.play(query="Coldplay", player="Living Room", media_types=["artist"], scope="local")— works. Found and fixed:music/search'sproviders=restriction is unreliable on this server (repeat identical requests sometimes returned every provider's results regardless of the filter) -filter_by_providers()now re-checks client-side, including matching a merged "library" item (provider="library") via itsprovider_mappings.group(action="join", players=["LedFX"], target_player="Living Room")thengroup(action="leave", players=["LedFX"])— works. Found and fixed:players/cmd/group_manytakeschild_player_ids, notplayer_ids.
Other bugs found and fixed by cross-checking music-assistant-client's source directly
(not just live-called): music/search groups results under plural keys
(playlists/tracks/albums/artists, radio stays singular) that don't match the
singular media_types request values; a search result's provider field is an
instance id like spotify--9hcJiXgW, not a plain domain, so the domain is now
derived via provider.split("--", 1)[0]; player_queues/delete_item takes
item_id_or_index, not queue_item_id; there's no player_queues/get command (queue's
"get" action now fetches player_queues/all and filters); music/browse only takes
path, no limit/offset (removed from the browse tool); MusicAssistantClient(...)
requires aiohttp_session as a positional arg (pass None); and a startup race between
the background start_listening() read loop and the direct-read auth handshake caused
ConnectionClosed - ma_client.py now waits for start_listening(init_ready=...) to
signal ready before considering the client connected.
radio_mode=True is passed straight through as the player_queues/play_media flag
(not wrapped into a radio_playlist:// URI) - confirmed live that this server's schema
(31) predates the schema-34 radio_playlist translation the client library does
internally, so the flag is what actually works here. If you're on schema ≥ 34, the flag
may no longer be honored and enqueuing radio_playlist://playlist/<uri> directly (per
music_assistant_client.player_queues.radio_playlist_uri) would be the thing to try.
radio_mode capability gating
Checked live against each configured provider's actual supported_features: only
providers reporting similar_tracks/similar_artists (Spotify, Tidal, Apple Music on
this server) can generate an actual radio mix - webdav (the only local/offline
provider here) reports neither. Requesting radio_mode=True for something MA can't
generate a mix for (a local-only pick, or any playlist/album/radio-station media type,
since those have no "similar tracks" concept at all) used to silently send the flag
anyway and just play normally with no indication anything was skipped. play() now
checks providers_logic.supports_radio() before honoring the flag and returns
radio_mode: false plus an explanatory note when it had to fall back, instead of
quietly no-op'ing. Verified live: stays true for a Spotify artist, correctly falls
back with a note for a Tidal playlist and for a local/webdav-backed artist.
Lenient defaults (agent-mistake tolerant)
Per a request to make sure a forgotten/wrong parameter never turns into a hard "bad request" - audited every tool for this and fixed the gaps:
play/search: unrecognizedmedia_typesentries (plurals, "song", typos) are mapped to the right value or dropped, falling back to["playlist"]if nothing valid remains;optionfalls back to"play"if not one of the four valid values.control:commandis case-insensitive and accepts a few synonyms (resume,skip,prev, ...); anything still unrecognized falls back to"play"(with anote) instead of raising.seek_secondsis clamped to ≥ 0.get/status/now_playing/info/state/currentare recognized as read-only status queries and short-circuit before that fallback - important because a local model asked "what's playing?" will reach forcontrol(command="get")on its own, and without this it used to silently default to"play"(i.e. resume/restart playback in response to a question).volume: calling with none oflevel/adjust/muteset now just reports the current level instead of erroring;levelis clamped to 0-100;adjustis case-insensitive and accepts synonyms (louder,quieter, ...);group=truewithmuteno longer errors - it just mutes the single player and notes why.queue: an unrecognizedactionfalls back to"get"(with anote) instead of raising;action="shuffle"without ashufflebool defaults toTrue;action="repeat"without a validrepeatvalue defaults to"all";actionis case-insensitive.group:actionis case-insensitive.
Left as hard errors (genuinely ambiguous, no safe default exists): volume given more
than one of level/adjust/mute at once; queue's move_up/move_down/
move_next/remove without item_id (no safe guess for which item); group's
action if it's neither join nor leave (opposite operations, shouldn't guess).
Queue-growth bug (option="play" wasn't clearing the queue)
Found live: a player's queue had silently grown to 3000+ items over normal use.
Root cause was in play()/play_random() themselves, not user behavior -
music_assistant_models.enums.QueueOption defines PLAY as "insert new item(s) at
the current position and start playing" and REPLACE as "replace entire queue
contents and start playing from index 0" - i.e. MA's own "play" option does not
clear the queue, only "replace" does. Our tools' documented/expected default
(option="play" meaning "clear queue and play now") was being sent to
player_queues/play_media as the literal string "play", so every play request was
quietly inserting into an ever-growing queue instead of starting fresh. Fixed by
mapping our "play" (and "replace") option values to MA's "replace" on the wire,
while "next"/"add" still pass through unchanged.
Related queue-management coverage, all one call each: add to the current queue without
clearing it (play(query="...", option="add") or option="next" to insert right
after the current track), clear it (queue(action="clear") - agent confirms with the
user first per the system prompt), shuffle the current queue
(queue(action="shuffle", shuffle=true|false)), or shuffle as part of starting a new
play (play(query="...", shuffle=true) / play_random(shuffle=true) - added so "play
X shuffled" doesn't need a separate follow-up call).
Slim now-playing/queue status
Found live: MA's raw player_queues/all + player_queues/items response nests the
full media item for every track - images, DSP filter chains, every provider mapping,
external IDs, streamdetails, etc. For a queue with a few thousand items, even fetching
just the current + a handful of upcoming items produced a response tens of thousands of
characters long, which a small local model can't usefully consume (and burns context/
tokens for any model). queue(action="get") and control's new status commands now
return a projected summary instead - state, shuffle, repeat, queue_length,
current/next (name/artist/album/duration only), and up to 10 upcoming tracks in
the same slim shape.
Human-request simulation (default scope + quality fixes)
Simulated realistic requests live against the real server - mood/occasion queries
("relaxing", "romantic evening", "dinner vibe", "jazz"), specific artist/track/album
lookups, explicit source/scope, and a query-less "surprise me" - to check how the
tools actually behave for a human, not just whether they run without erroring. Found
and fixed:
Default scope was "all" (online + local mixed together), which doesn't match how a human expects "play something"/"play
<artist>" to behave (reach for a streaming service first). Changed the default to "online", automatically broadening to everything (scope="all"semantics) if nothing turns up there - so a local-only match still gets found without asking for it by name, but a home NAS full of oddly-named files doesn't get preferred over Spotify/Tidal by default. Explicitscope="all"/"local"/"online"is still respected exactly, no fallback applied.A nonsense query could still "confidently" play something unrelated - e.g. searching the literal gibberish
"asdkfjhqwoeiuasdf"as an artist returned an unrelated real artist, because the Tidal/Spotify/Apple priority tiebreak among equally unmatched candidates always picks one of them.pick_best()now returns no match at all (not just "wrong match") for artist/track/album lookups where nothing genuinely matched the query by name, soplay()correctly reports "no results" instead of playing something wrong.An empty/blank
queryreturned garbage (a local playlist's raw file listing, not a real search result) instead of failing clearly.play/searchnow reject an empty query outright, andplay's error message points atplay_randomfor the query-less "surprise me" case.A matched item could still be genuinely unplayable (confirmed live: a same-named local "Jazz" playlist that turned out to be empty) -
player_queues/play_mediaraisingMediaNotFoundError/UnplayableMediaErrorused to fail the whole call.play()now automatically tries the next candidate (up to 3) before giving up, and reports which ones got skipped in anote.New
play_randomtool, backed bymusic/tracks/library_itemswithorder_by="random"(a real server-side capability, confirmed - not client-side shuffling of a search result) - lets "play something from my local library"/"surprise me" build an ad-hoc mix without needing any search query at all.
Still unverified
transfer(player_queues/transfer) wasn't exercised in the live scenarios above.Local/offline scope was only tested against a
webdavprovider (this server's only non-streaming one) - afilesystem_local/filesystem_smb/filesystem_nfsshare should classify the same way (is_streaming_provider=False) but wasn't tried directly.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/shuricksumy/MCP-MusicAssistant'
If you have feedback or need assistance with the MCP directory API, please join our Discord server