Skip to main content
Glama
aramvr
by aramvr

yandex-music-mcp

An MCP server that turns Yandex Music into tools an AI agent can call directly — search, queue, play, build playlists — with no browser involved.

Audio comes out of your Mac's speakers via a small local player daemon. Claude queues a whole DJ set in one call and the daemon advances through it on its own. On macOS it also ships a native player: a real Now Playing entry in Control Center, and a window with the full queue and per-track like/dislike.

Built with Claude Code. Design, code, debugging and this README were produced in conversation with Claude. Read it before you run it — it talks to your music account and writes a token to disk.

Unofficial. This uses Yandex Music's private API. It is not affiliated with, endorsed by, or supported by Yandex. Endpoints can change without warning.


Who this is for

Developers who want to clone a repo, build it, and mess about.

There is deliberately no installer, no Docker image, no one-click setup, and that is a design decision rather than a to-do:

  • Audio cannot be containerized on macOS. Docker Desktop runs containers in a Linux VM with no access to CoreAudio. A containerized player would run happily and produce silence. Routing audio to a network PulseAudio sink is possible but needs a server on the host and buys nothing on one machine.

  • The native UI is a compiled Swift .app that has to be built and registered on the machine that will display it. Nothing to ship in an image.

  • The MCP server needs nothing kept running — it is stdio, launched on demand. The only long-lived piece is the player daemon, and launchd is the native answer to that (ym install-agent).

So: clone it, build it, play with it. It is a couple of minutes.


Related MCP server: ytm-mcp

Why it exists

Driving music through a browser was miserable:

Browser

This

Login

separate session per browser profile, lost on tab close

one token, stored once

Autoplay

Chrome needs a real click before audio starts

n/a

Track changes

the agent wakes up between every track

daemon auto-advances

Token cost

~3 tool calls per track

1 call per set

Survives closing the tab

no

yes


Requirements

  • macOS for playback and the native player. The API surface (search, playlists, resolving streams) is platform-agnostic; audio and UI are not.

  • Node 20+

  • mpvbrew install mpv. Without it the daemon falls back to afplay, which downloads each track before playing it.

  • Xcode command line tools for the Swift helper — xcode-select --install.

  • A Yandex Music account with Plus. Without Plus the API only returns 30-second previews.

Setup

git clone https://github.com/aramvr/yandex-music-mcp.git
cd yandex-music-mcp
pnpm install          # npm install works too
pnpm build
node dist/cli.js auth

Then point your agent at it:

claude mcp add yandex-music -- node "$PWD/dist/index.js"

Sanity check without an agent:

node dist/cli.js whoami
node dist/cli.js set "Rammstein - Sonne" "SOAD - Chop Suey" "Muse - Uprising"
node dist/cli.js player     # opens the window

Logging in

ym auth opens Yandex's own authorize page, you approve, and you paste back what lands in the address bar. That is the whole flow. The token is verified against the music API before being written to ~/.yandex-music-mcp/.env with 0600 permissions, and a rejected token can never overwrite a working one.

Your password never goes near this code. Yandex issues the token to you in the browser; the only thing that crosses is the token itself.

Why there is no automatic login

Because it cannot work. Yandex publishes no Yandex Music scope for third-party OAuth apps. Register your own app, wire up a perfectly correct loopback flow, and the token you get back carries only landing-play, feed-play, mix-play — the music API treats it as an anonymous session with no uid and no login.

The only client id that can actually stream is Yandex's own music client, and that one does not whitelist a localhost redirect. So the token has to travel by clipboard. This was built the automatic way first; it does not work, and the paste flow is what remains.

If a token ever does come back anonymous, getAccount() throws with the permission list rather than silently pretending it worked.

Tokens are long-lived but not eternal. Run ym auth again when you see a 401; ym whoami shows the expiry when it is known.


Audio quality

Defaults to the best the account is entitled to. Two places quality can leak, both closed.

1. What gets requested. YM_QUALITY=lossless (the default) tries the get-file-info endpoint for FLAC first, then falls back to the highest-bitrate MP3 rather than failing the track. YM_QUALITY=high skips straight to MP3.

In practice you will get MP3 320 CBR — see below.

2. What the player does with it. mpv runs with every processing stage off:

Flag

Why

--replaygain=no

no loudness normalisation

--audio-normalize-downmix=no

no downmix gain

--audio-samplerate=0

no resampling; the device follows the file

--af=

empty filter chain

--volume-max=100

soft volume above unity clips, so it is capped

--cache=yes --demuxer-max-bytes=64MiB

network FLAC stutters without buffer

Volume is therefore 0–100, not 0–130. For more output, use the system volume.

Verify rather than trust — this reports the codec actually served:

node dist/cli.js quality "Rammstein Sonne"

ym_now_playing also shows a Quality: line, and every track logs its codec to ~/.yandex-music-mcp/daemon.log.

Why not lossless

The web player's "superb" setting calls a different endpoint:

GET api.music.yandex.com/get-file-info/batch
      ?trackIds=…&quality=lossless
      &codecs=flac,aac,he-aac,mp3,flac-mp4,aac-mp4,he-aac-mp4
      &transports=encraw&sign=…

Two blockers, both intentional on Yandex's side:

  1. sign is an HMAC whose key lives in the obfuscated web bundle. Recovering it means lifting a secret out of their client.

  2. transports=encraw — the enc is encrypted. Even with a valid signature the stream needs decrypting.

Together that is DRM circumvention, so this project stops at the unencrypted path. That is a deliberate boundary, not a missing feature. Don't "fix" it.

MP3 320 CBR is what the open path serves. If lossless matters to you, the official clients do it properly.


The macOS player

pnpm build:native compiles native/NowPlaying.swift into bin/Yandex Music.app — a real .app, not a bare binary, and that detail matters. Clicking the Now Playing widget asks LaunchServices to open the app that owns the media session; a bare executable has no bundle to open, so macOS walks up to the responsible process and opens Terminal instead.

The app is an LSUIElement agent — no Dock tile, no app-switcher entry — and it does two jobs.

It owns the Now Playing session. mpv can publish one itself, but only from the media file's own tags, and Yandex streams arrive with no ID3 at all, so artist, album and artwork would simply be blank. The daemon runs mpv with --input-media-keys=no so there is exactly one session, and pushes real metadata into this helper instead.

Control Center's layout is fixed: artwork, title, artist, album, scrubber, transport. It has no concept of a track list, and it does not draw like/dislike even when a player registers MPFeedbackCommand. Which is the reason for the second job.

It draws the player window — everything Control Center won't show. The full queue with the current row marked, per-row like and dislike, double-click any row to jump, a volume slider. Closing the window does not quit: the process still owns the media session and the daemon's pipe.

Wire protocol is one JSON object per line over stdio, both directions:

daemon -> app   {"type":"track", title, artist, album, artworkUrl, durationSec,
                 elapsedSec, isPlaying, queueIndex, liked, disliked, volume}
                {"type":"queue", items:[{title, artist, durationSec, feedback}], queueIndex}
                clear | show
app -> daemon   {"command":"play"|"pause"|"toggle"|"next"|"prev"|"stop"}
                {"command":"seek","positionSec":N}
                {"command":"jump","index":N}
                {"command":"like"|"dislike","index":N?}   // index omitted = current
                {"command":"volume","percent":N}

Track updates fire every few seconds; the queue is pushed only when it changes. Likes and dislikes go straight to the Yandex account and are remembered per track id in the daemon's state file, so the hearts survive a restart.

Everything degrades to a no-op off macOS or when the app hasn't been built — playback never depends on the UI being there.


Tools

Tool

What it does

ym_auth_status

verify token, report account + Plus state

ym_auth_login

explain how to log in (there is no automatic flow)

ym_search

search the catalogue

ym_play_set

the main one — queue a list of tracks and play through them

ym_queue_add

append without interrupting

ym_play_now

jump the queue with one track

ym_control

pause / resume / next / prev / stop

ym_now_playing

current track + position

ym_open_player

open the macOS player window

ym_queue_show

list the queue

ym_volume

0–100

ym_playlist_create

create a real playlist on the account

ym_playlists

list your playlists

ym_like

like the current or a named track

ym_history

what has been played

ym_play_set takes free-text "Artist - Title" strings and resolves each by search, preferring plausible-length originals over snippets and karaoke covers. It also accepts startAtIso to begin a set later.

CLI

Everything is reachable without an agent, which makes debugging easy:

ym search "rammstein sonne"
ym set "SOAD - Chop Suey" "Muse - Hysteria" "Rammstein - Sonne"
ym status
ym next
ym pause
ym player

Architecture

Agent ──stdio──> MCP server (dist/index.js)     ephemeral, spawned per session
                       │
                       │ unix socket ~/.yandex-music-mcp/daemon.sock
                       ▼
                 Player daemon (dist/daemon.js)  long-lived, detached
                       │
                       ├── Yandex API (search, download-info → signed mp3 URL)
                       ├── mpv (IPC) or afplay (download-then-play fallback)
                       └── stdio ──> Yandex Music.app (Now Playing + window)

The split matters: the MCP server dies with the agent session, the daemon does not. Queue state persists to ~/.yandex-music-mcp/state.json, so a fresh agent session sees whatever is already playing.

Auto-advance is driven by the backend's end-of-track event, not by polling — so a 20-track set costs exactly one tool call.

One deliberate oddity: src/http.ts uses node:https rather than global fetch. Node's fetch (undici) auto-injects browser-only headers — notably Sec-Fetch-Mode: cors — which makes a server-side request look like a malformed browser one, and Yandex's edge answers that with a 403 anti-bot page instead of JSON. The identical request from curl or node:https succeeds.

Auto-start at login

node dist/cli.js install-agent

Installs a launchd LaunchAgent (dev.yandex-music-mcp.daemon) that starts the player daemon at login and restarts it if it dies. Remove with ym uninstall-agent.

Optional — the MCP server spawns the daemon on demand if it isn't running. Install the agent if you want music to survive restarts without a first call.


Development

pnpm dev       # tsc --watch
pnpm test      # compiles, then node:test

Tests cover the two pure pieces worth locking down: the free-text track resolver's ranking, and the token parser that decides whether a paste is usable. Everything else needs a live account.

Layout:

src/
  index.ts       MCP server — tool definitions
  daemon.ts      player daemon — queue, auto-advance, state
  player.ts      mpv / afplay backends
  api.ts         Yandex API client
  http.ts        node:https wrapper (see above)
  auth.ts        token paste flow
  ipc.ts         unix-socket protocol + daemon autospawn
  config.ts      paths, env, token storage
  nowplaying.ts  Node side of the macOS bridge
native/
  NowPlaying.swift    the .app: Now Playing session + player window
scripts/
  build-native.sh     assembles the .app bundle
  make-icon.swift     draws the icon

Caveats

  • The private API can change without warning. If playback breaks, that is the first suspect — check ~/.yandex-music-mcp/daemon.log.

  • The MCP server is stdio-only and unauthenticated by design; it assumes it is launched by a local agent. Don't expose it.

  • Your token grants full access to your Yandex account's music profile. It sits in ~/.yandex-music-mcp/.env at mode 0600. Treat it like a password.

  • Personal listening on an account you pay for. Not for redistributing audio.

License

MIT.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

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/aramvr/yandex-music-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server