roon-mcp
Use it to discover music in Roon and control playback across zones via MCP.\n\n- List Roon zones/outputs with IDs, names, state, and output IDs.\n- Search Roon for artists, albums, tracks, genres, playlists, radio, and optionally streaming genre tracks.\n- Expand artists/albums/genres/playlists into concrete playable tracks.\n- Play a selected item immediately in a zone, optionally shuffled.\n- Build an ordered ad-hoc queue from curated item keys and start it, optionally shuffled.\n- Read the current track/state, title, artist, album, and seek position for a zone.\n- Pause, resume, skip forward/back, or stop playback.\n- Set a zone’s volume by percent.\n- Mute or unmute every output in a zone.\n- Resolve zones via ROON_DEFAULT_ZONE or fallbacks and report ambiguity for clarification.\n\nREADME also documents additional tools not present in this schema: seek, loop mode, pause-all, mute-all, auto-radio, and library export.
Click on "Deploy 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., "@roon-mcp search for Miles Davis and play his album Kind of Blue"
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.
roon-mcp
An MCP server to control the Roon music
player, so an AI agent can find music and start playback. Built on the official
Roon Extension API (node-roon-api + node-roon-api-transport /
node-roon-api-browse).
"Play a female vocal trance mix on Roon (Zone: This Computer) — 2000s Dutch trance vibes, the kind of set you'd hear at Amnesia in its prime. Start with something like 4 Strings' Take Me Away."
Claude searches across your library and streaming, hand-picks the tracks, and queues them — all from a single natural-language prompt.
Requirements
Node.js 20+
A running Roon Core on the local network to pair with
git on the install host — the
node-roon-api*dependencies are published on GitHub (not npm) and are fetched via git URLs during install
Related MCP server: lms-mcp
MCP Client Configuration
Add this to your MCP client config. npx fetches the package on first run:
{
"mcpServers": {
"roon": {
"command": "npx",
"args": ["-y", "roon-mcp"],
"env": { "ROON_DEFAULT_ZONE": "Office" }
}
}
}On first launch, open Roon → Settings → Extensions and enable Roon MCP
to pair. Pairing status is logged to stderr; stdout is reserved for the MCP
protocol. The pairing token is persisted to ~/.config/roon-mcp/config.json
(override with ROON_MCP_CONFIG), so the extension stays authorized across
restarts rather than appearing as a new "Discovered" entry each time.
Global install (optional)
npm install -g roon-mcp{
"mcpServers": {
"roon": {
"command": "roon-mcp",
"env": { "ROON_DEFAULT_ZONE": "Office" }
}
}
}Configuration
Env var | Purpose |
| Optional fallback target for |
| Optional direct-connection host for the Core's API, bypassing SOOD multicast discovery — for VLAN-segmented networks, VPNs, or containers where discovery doesn't survive. Unset (default) uses discovery as before. |
| WebSocket port of the Core's API when |
| Optional path for the persisted pairing token. A value ending in |
Tools
Tool | Purpose |
| List playable zones/outputs (id, name, state, output ids). |
| Resolve a text query into ranked browse candidates (opaque, session-scoped item keys). |
| Expand an artist/album/genre/playlist candidate into concrete playable tracks. |
| Play or queue one search candidate. By default replaces the queue and starts immediately. Pass |
| Build an ad-hoc queue from curated item keys and start it (replaces the zone's queue); reports queued/skipped. |
| Snapshot of the zone's current track — state, title, artist, album, seek position. |
| Run a transport verb: |
| Seek within the current track. |
| Set the loop/repeat mode: |
| Set the zone's volume to |
| Mute ( |
| Pause every zone on the Core in one call. Resume per zone with |
| Mute ( |
| Turn Roon Radio on/off for a zone: |
| Walk Library → Albums and write the album catalog to |
Streaming search (genre and artist)
Genre search fuzzy-matches what you type — "psychedelic trance" will find the right genre even if the name isn't exact. By default results come from your library.
includeStreaming: true(ontype:"genre"ortype:"artist") extends the search to your streaming service, so you can play artists or genres that aren't in your local collection.limitcaps the combined result. Library results come first; streaming tracks fill the remaining slots. When library results alone would fill the limit, up to half of it is reserved for streaming (trimming the lowest-ranked library tail, noted inmessage) so an explicitincludeStreamingalways surfaces streaming content when it exists.
Notes
After starting playback,
now_playingreflects the track that just started, not whatever was playing before.Volume is set as a percentage (0–100) and works correctly across grouped zones.
Concurrent instances
Roon pairs only one running instance of an extension at a time. Since every roon-mcp
process shares the same identity and persisted pairing token, starting a second instance
(a second MCP client session, or an orphaned process from a closed terminal) no longer
fights the first one for the pairing slot — instead, whichever instance starts first
claims a lock file (instance.lock, next to config.json) and every other instance
fails fast with a CORE_PAIRING_HELD error naming the pid that holds it, instead of
flapping between paired/unpaired.
A lock whose owning process is no longer alive is treated as stale and taken over —
both at startup and by an already-running blocked instance, which re-checks the lock
on every call. So when the holder exits (cleanly or via kill -9), the next call to
any other instance acquires the lock, starts discovery, and pairs; nothing needs
restarting.
MCP clients don't always reap server processes when a session ends, so orphaned
roon-mcp instances can pile up and hold the lock long after their terminal is gone. If
you hit CORE_PAIRING_HELD unexpectedly, clear them out:
pkill -f roon-mcpBlocked instances that are still running recover on their own next call, and the next instance you start acquires the lock normally.
Assumptions
Core language: English. Category/action label matching (
Artists,Play Now,Top Tracks, …) assumes an English Core.Sources: local library + your configured streaming service. The server goes through Roon's Browse API, which routes to whatever streaming service you have set up in Roon (Tidal, Qobuz, etc.). Developed and tested against Tidal — other services should work but are untested. Results differ on a local-only Core.
Queue: replace.
enqueue_and_playstarts a fresh queue rather than adding to whatever is already playing.Curation is agent-side. Dedupe / cap-per-artist / ordering / trimming stay in the agent; the server has no curation logic.
There is no save_playlist tool: Roon exposes no official playlist-write service, so
durable playlists are out of scope. Curated playback is delivered by enqueue_and_play
(an ad-hoc, in-the-moment queue).
Logging
Every Roon API call (browse, load, get_zones, control, change_volume, mute,
seek, change_settings, pause_all, mute_all, …) emits one structured line to
stderr (stdout stays reserved for MCP JSON-RPC):
[roon-call] {"t":"2026-06-19T18:00:00.000Z","lvl":"info","op":"browse","ms":12,"params":{"hierarchy":"search","item_key":"…"},"result":{"action":"list","count":7}}Failures log lvl:"error" with the mapped error code, and retries surface as repeated
lines for the same op. Grep stderr for [roon-call] to trace a flow.
Development
npm install # pulls node-roon-api packages from RoonLabs' GitHub
npm run build
npm test # builds, then runs node:test
npm run dev # tsc --watch
npm run typecheckRun the built server directly:
npm startIntegration smoke test
scripts/integration.mjs spawns the built server over stdio (like a real MCP client)
and runs list → search → expand against your Core. Audible steps are opt-in so it never
blasts music by accident:
npm run build
node scripts/integration.mjs # list_zones + search only (read-only)
ROON_PLAY=1 node scripts/integration.mjs # also play_now a shuffled genre
ROON_ENQUEUE=1 node scripts/integration.mjs # also enqueue a few curated tracks
# overrides: ROON_ZONE="Office" ROON_QUERY="Tycho"Enable Roon MCP under Roon → Settings → Extensions on the first run (the script
retries list_zones for ~45s while it waits to pair).
Releasing
npm run release -- <patch|minor|major>Requires a clean worktree on main and a matching ## [<version>] entry in
CHANGELOG.md. The script runs the tests, bumps the version, tags the
commit, and verifies the package with npm pack. It then prints the push and
npm publish commands to run.
License
Available Tools
9 toolscontrol_playbackRun a Roon transport verb (pause/resume/next/previous/stop)A
Use this for the common transport verbs — pause, resume, skip, next track, previous track, stop (e.g. "pause", "pause the kitchen", "skip", "next track", "next song", "play the next song", "resume", "resume in the office", "stop", "stop the music"). Runs one transport verb against the resolved zone. For "louder"/"softer" without a number, call now_playing first to read the current state, then set_volume with a target percent — this tool is verbs only. zoneId is optional and resolves like now_playing. Returns the resolved zoneId, the action taken, and the resulting playback state.
Inputs: action (required), zoneId.
enqueue_and_playBuild and start a curated Roon queueA
Use this when the user wants a custom lineup — a mix of artists, a shuffled selection across albums, a hand-picked set of tracks, or any time "queue", "setlist", "mix of", or "play these in order" comes up (e.g. "queue up five Tycho tracks then some Boards of Canada", "shuffle 10 ambient tracks", "build a set: artist A, then B, then C"). Builds an ad-hoc queue from an ordered list of curated item keys and starts playback in the target zone. This replaces the zone's current queue: the first playable item starts immediately (Play Now), the rest are appended in order. Pass itemKeys from recent get_tracks_for / search_music results (use them promptly — they are session-scoped). zoneId is optional (omit to use the default zone; see play_now). Optionally shuffle. Returns a PlaybackResult with queued/skipped counts so you can backfill skipped items.
Inputs: itemKeys (required), zoneId, shuffle.
get_tracks_forExpand a Roon item into tracksA
Use this after search_music when the user wants a concrete list of songs — to preview tracks, build a queue, or pick one to start with (e.g. "what tracks are on this album?", "give me 5 tracks of Dark Ambient", "what's on this playlist?"). Expands an artist, album, genre, or playlist candidate into concrete playable tracks. Pass an itemKey from a recent search_music result. Returns track candidates with session-scoped item keys (use them promptly with enqueue_and_play). Non-expandable items return empty tracks with a skipped reason rather than an error.
Inputs: itemKey (required), limit.
list_zonesList Roon zonesA
Use this when the user asks which rooms, speakers, or outputs Roon can play to, or before starting playback when the target zone is unclear (e.g. "what zones are on?", "play in the kitchen", "which speaker is in the office?"). Lists every zone/output the paired Core exposes with its id, display name, current playback state, and output ids. Call this first if no zone is obvious and ROON_DEFAULT_ZONE is not set.
Inputs: no named parameters.
muteMute or unmute a Roon zoneA
Use this when the user wants to mute or unmute a zone (e.g. "mute", "mute the kitchen", "unmute", "silence the office", "stop the noise"). Mutes (or unmutes) every output in the resolved zone. zoneId is optional and resolves like now_playing. muted: true mutes, muted: false unmutes.
Inputs: muted (required), zoneId.
now_playingGet the currently-playing track in a Roon zoneA
Use this when the user asks what is currently playing, what song is on, who's singing, what just started, or wants a snapshot before skipping or pausing (e.g. "what's playing?", "what's playing in the kitchen?", "who's this?", "what's the current track?", "what song is this?"). Returns a structured snapshot: zone id and name, playback state (playing/paused/loading/stopped), title, artist, album, and the current seek position when available. title/artist/album are undefined when nothing is playing. Call this before pause/skip/volume changes when the user hasn't named a zone — it confirms where to act and what the state is. zoneId is optional: omit to use ROON_DEFAULT_ZONE, or fall back to the only zone / an "Office" zone / the currently-playing zone; if it still can't decide it returns ZONE_AMBIGUOUS so the agent can ask the user or call list_zones.
Inputs: zoneId.
play_nowPlay an item now in a Roon zoneA
Use this when the user wants one specific thing playing right now — an album, artist, playlist, genre mix, or single track (e.g. "play Tycho", "put on In Rainbows", "start some Psytrance in the office", "play that track"). Immediately plays a single search candidate in the target zone and replaces whatever was queued. Pass an itemKey from a recent search_music (or get_tracks_for) result — item keys are session-scoped, so use a fresh one. zoneId is optional: omit it to use ROON_DEFAULT_ZONE, or fall back to the only zone / an "Office" zone / the currently-playing zone; if it still can't decide it returns ZONE_AMBIGUOUS so the agent can ask the user or call list_zones. Optionally shuffle. Returns a PlaybackResult.
Inputs: itemKey (required), zoneId, shuffle.
search_musicSearch Roon musicA
Use this when the user names music to find — an artist, album, track, playlist, radio station, or genre (e.g. "find Tycho", "look up the album In Rainbows", "play some Psytrance", "anything by Ryuichi Sakamoto?"). Resolves a free-text query into ranked Roon browse candidates. Optionally restrict to one item type (artist, album, track, genre, playlist, radio); for non-genre types, an empty typed search broadens to all categories. type:"genre" is special — genres don't appear in Roon's flat search, so the server walks the dedicated Genres tree and returns the nearest-match genre nodes (with parent path in the subtitle) without broadening; e.g. "Psychedelic Trance" yields "Psytrance"/"Trance". Set includeStreaming:true (only meaningful for type:"genre") to also pull a track mix from streaming services (e.g. TIDAL): the server takes the genre-relevant albums and samples tracks across them, so library genre nodes come first and ready-to-play streaming tracks are appended after. Returns opaque, session-scoped item keys for use by the playback tools — pair with get_tracks_for to expand, then play_now or enqueue_and_play.
Inputs: query (required), type, limit, includeStreaming.
set_volumeSet a Roon zone's volume to a target percentA
Use this when the user wants to change how loud a zone is — turn it up, turn it down, set the volume, or pick a level for a specific room (e.g. "turn it up to 60", "volume to 30 in the office", "set the kitchen to 50", "louder" — see note). Sets the zone's volume to level percent (0 = silent, 100 = max). The server rescales to each output's native range, so a single value works across mixed devices in a grouped zone. Incremental outputs (IR blasters with no numeric range) are reported as skipped rather than guessed at. zoneId is optional and resolves like now_playing. NOTE on relative changes ("louder" / "softer" without a number): this tool is absolute — read the current state with now_playing isn't enough on its own (volume isn't exposed there), so for relative changes, ask the user for a target percent or apply a reasonable default delta.
Inputs: level (required), zoneId.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 9 tools
Each tool has a clearly distinct purpose: control_playback handles transport commands, play_now starts a single item, enqueue_and_play builds a queue, search_music finds candidates, get_tracks_for expands them, etc. No two tools overlap in functionality.
Tool names follow a mostly consistent snake_case pattern with verbs leading (list_zones, set_volume, get_tracks_for). However, 'now_playing' and 'play_now' are verb-phrase based, and 'mute' is a single verb, introducing minor inconsistency.
With 9 tools, the server is well-scoped for a Roon music control system. Each tool handles a essential aspect (search, playback, queue, volume, zone management) without being bloated or underpowered.
Core workflows are covered: search, browse, play, control, volume, and state. Minor gaps exist, such as no playlist management or fine-grained queue manipulation (e.g., insert, remove), but these are not critical for basic operation.
Maintenance
Related MCP Connectors
MCP server for Producer/Riffusion AI music generation
Hosted MCP server connecting AI assistants to 9,000+ apps and 40,000+ actions via Zapier.
Hosted MCP server for live public-data APIs and Skills for AI agents.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceMCP server for Apple Music that lets AI assistants manage playlists, control playback, and browse the music library.832 PyPI132MIT
- AlicenseBqualityDmaintenanceMCP server that provides LLM tools to interact with Lyrion Music Server (LMS), enabling player control, playback management, playlist operations, and music library search.5521 npmMIT
- AlicenseAqualityDmaintenanceMCP server to control Spotify conversationally from Claude, Cursor, and other AI clients.11110 npm2MIT
- AlicenseNot gradedqualityDmaintenanceA comprehensive MCP server that enables AI assistants to control music playback, manage users and access, configure providers, and administer a Music Assistant setup through natural language commands.MIT