内容
# Music Assistant MCP
> 和你的家庭音频系统像对话一样,不再是 API。
| 你说 | 会发生什么 |
|---|---|
| "播放一些放松的音乐" | 搜索流媒体播放列表,选择最佳匹配,在默认播放器上播放 - 无需播放器 ID,无需选择来源 |
| "为浪漫晚餐播放一些爵士音乐" | 情绪/场合查询 → 自动找到合适的播放列表,就像人类浏览 Spotify 一样 |
| "以广播模式播放 The Prodigy" | 解析艺术家,检查实际提供者能否生成混音,然后再开始,确保不会静默失败 |
| "从我的本地文件播放 Coldplay" | 将搜索范围限定在本地/NAS 库中,完全跳过流媒体 |
| "惊喜我" | 直接从库中构建临时混音 - 无需搜索查询 |
| "加入 LedFX 播放器,然后将其取回" | 按需分组/取消分组播放器 |
一行自然语言输入,正确的音乐播放出来 - 上述每个场景都在真正的 Music Assistant 服务器上测试过,而不仅仅是听起来合理的文档(参见下面的[验证对抗真实服务器](#verified-against-a-real-server-schema-version-31))。
一个从头开始的 MCP 服务器,用于 [Music Assistant](https://music-assistant.io/),设计使其几乎不需要系统提示就能驱动。 与原始的低级 MCP 工具集不同,通常需要代理执行的编排(解析播放器,记住它,搜索后播放,应用提供者 tiebreak 等)已内置到工具本身:
- **`play`** — 搜索 + 解析播放器 + 提供者 tiebreak + 播放,一次调用完成。 支持 `scope="online"|"local"|"all"` 以限制到流媒体提供者(Spotify/Tidal/Apple - 丰富的主题/氛围搜索)或本地文件提供者(SMB/WebDAV/本地目录 - 精确匹配),或 `source="tidal"`/`"my-nas-share"` 以强制使用特定提供者。省略 `scope` 默认值为 "online"(如果那里没有结果,则扩大到所有内容),`media_types` 默认值为 `["playlist"]` - 因此 "播放一些放松的音乐"/"播放爵士乐" 自然会优先搜索流媒体播放列表。 `radio_mode=true` 启动一个连续的广播混音,从匹配结果中播种,而不是播放一次。 如果顶级匹配结果无法播放(例如空播放列表),它会自动尝试下一个候选项,然后再放弃。 `shuffle=true|false` 将队列的随机播放模式设置为调用的一部分(例如 "随机播放此播放列表")- 省略则随机播放模式不变。
- **`play_random`** — 直接从库中构建并播放随机混音,无需搜索查询 - 适用于 "播放一些音乐"/"惊喜我"/"来自我本地文件的随机混音" 请求。 与 `play` 相同的 `scope`/`source`/`shuffle` 参数(此处默认 `scope="all"`,因为没有查询要搜索 "在线")。
- **`control`** — 播放/暂停/停止/切换/下一曲/上一曲/跳转,以及只读的 `status`/`get`/`now_playing` 查询,获取当前播放信息(从不发送播放命令)。
- **`volume`** — 音量 / 相对调整 / 静音,可选适用于整个群组。
- **`queue`** — 获取/随机播放/重复/清除/移动/删除,以单个播放器为索引。 `get` 返回精简的摘要(当前/下一曲,待播放列表),而非 MA 的原始队列项目有效载荷。
- **`transfer`** — 将播放从一个播放器转移到另一个。
- **`browse`** — 浏览提供者的库层次结构。
- **`group`** — 加入/离开播放器群组(例如同步 LedFX 可视化器)。
- **`search`** — 原始查找,无需播放(与 `play` 相同的 `scope`/`source` 参数),适用于 "你有 X 吗" 类型的问题。
- **`list_players`** / **`list_providers`** — 仅用于诊断/备用;其他工具自行解析播放器和提供者。
通过 **Streamable HTTP** 提供服务,带有静态的 bearer token,因此可以直接集成到 MCP Client HTTP 节点(例如 n8n)中,就像之前的 stdio-only 社区服务器需要桥接到那里一样。
## 安装
```bash
cp .env.example .env # 填写 MA_SERVER_URL、MCP_BEARER_TOKEN、DEFAULT_PLAYER_NAME 等
uv sync # 或:pip install -e ".[dev]"
uv run music-assistant-mcp
```
`DEFAULT_PLAYER_NAME` 在实践中不是可选的 - 留空并在任何未显式指定播放器的工具调用中失败,并显示 `No default player configured and none could be resolved`(确认有效)。将其设置为 `list_players` 输出的名称(例如 `"Living Room"`),而不是播放器 ID。
服务器监听 `MCP_HOST:MCP_PORT`(默认为 `0.0.0.0:8005`)并在 `/mcp` 处暴露 MCP 端点,需要 `Authorization: Bearer <MCP_BEARER_TOKEN>`。
### 配置(`.env`)
| 变量 | 用途 |
|---|---|
| `MA_SERVER_URL` | Music Assistant 服务器,例如 `http://192.168.1.50:8095` |
| `MA_TOKEN` | Music Assistant 认证令牌(schema ≥ 28 服务器需要) |
| `MCP_HOST` / `MCP_PORT` | 服务器自己的绑定地址 |
| `MCP_BEARER_TOKEN` | 客户端必须发送的共享密钥作为 `Authorization: Bearer <token>` |
| `DEFAULT_PLAYER_NAME` | 用于工具调用时未指定播放器 |
| `SOURCE_PRIORITY` | 以逗号分隔的提供者 tiebreak 顺序,例如 `tidal,spotify,apple_music` |
| `MCP_TRANSPORT` | `streamable-http`(默认为 n8n 等)或 `stdio`(适用于将其作为本地子进程启动的主机 - 见下文);`MCP_HOST`/`MCP_PORT`/`MCP_BEARER_TOKEN` 在 `stdio` 模式下被忽略 |
### 添加到 MCP 客户端配置
与旧社区服务器(仅 stdio,通过 `uvx` 每个连接启动)不同,该服务器是一个独立的、长时间运行的 HTTP 服务 - 客户端通过网络连接到它,而不是启动它。 对于任何支持远程/HTTP 服务器条目的 MCP 主机配置(Claude Desktop、Cursor 等),如下所示:
```json
{
"mcpServers": {
"music-assistant": {
"enabled": true,
"timeout": 60,
"transportType": "http",
"url": "http://<MCP_HOST>:<MCP_PORT>/mcp",
"headers": {
"Authorization": "Bearer <MCP_BEARER_TOKEN>"
}
}
}
}
```
用您运行此服务器的位置替换 `<MCP_HOST>:<MCP_PORT>`(例如 `192.168.1.50:8005`),并用您的 `.env` 中的值替换 `<MCP_BEARER_TOKEN>`。 这需要服务器已经运行(`uv run music-assistant-mcp`,或在下面的 Docker 中)- 与下面的 `command`/`args`/`env` 样式不同,这里不会启动任何内容。
### 直接从 GitHub 使用 uvx(stdio)
如果您的 MCP 主机仅支持 `command`/`args`/`env` 样式配置(旧社区服务器使用的相同形状),请设置 `MCP_TRANSPORT=stdio`,它将以相同的方式工作 - 不需要 bearer token,因为主机直接拥有进程的 stdio 管道:
```json
{
"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` 在这里比 n8n 更重要:如果没有它,每个 `play`/`control`/`volume`/等调用,如果没有显式指定播放器,都会失败,并显示 `No default player configured and none could be resolved`(确认有效)- 工具自行解析播放器的目的就落空了,如果没有 fallback 的话。 将其设置为 `list_players` 返回的名称之一(例如 `"Living Room"`,不是播放器 ID)。 `SOURCE_PRIORITY` 是可选的,但建议显式设置,因为其内置默认值(`tidal,spotify,apple_music`)可能与您实际配置的提供者不匹配。
在本地确认有效:通过 `MCP_TRANSPORT=stdio uv run music-assistant-mcp` 传输真正的 `initialize` 请求,返回一个干净的 JSON-RPC 响应在 stdout 上,其他内容不会混入(日志记录到 stderr,因此不会破坏协议流)。
### 在 Docker 中运行
```bash
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-mcp
```
在存储库验证期间构建并运行测试(`docker build` + `docker run` + bearer-authenticated MCP `initialize` 调用)。
### 预构建的多架构镜像
`.github/workflows/docker-publish.yml` 为 `linux/amd64` 和 `linux/arm64` 构建并推送镜像到 GitHub 容器注册表 - 在 `main` 分支的每次推送和版本标签(`v*.*.*`)上验证本地 Dockerfile 构建干净,适用于两种架构。 发布者不需要设置额外的注册表凭证 - 它使用存储库内置的 `GITHUB_TOKEN` 进行身份验证。 软件包可能需要标记为公共存储库的 Packages 设置中第一次(GitHub 默认新建软件包为私有)。
```bash
docker pull ghcr.io/shuricksumy/music-assistant-mcp:main
```
### 使用 docker-compose 运行
`docker-compose.yml` 拉取上述预构建镜像(因此工作流至少运行一次,软件包需要公开),并从同一目录中的 `.env` 读取其配置:
```bash
cp .env.example .env # 填写它,参见上面的配置
docker compose up -d
```
### 使用 mcp-proxy(适用于仅支持 stdio 启动的服务器)
一些 MCP 主机仅支持 `command`/`args`/`env` 样式配置(旧社区服务器的 `uvx --from git+...` 条目),无法直接指向远程 HTTP URL。对于这些,请使用 [`mcp-proxy`](https://github.com/sparfenyuk/mcp-proxy) 作为本地 stdio↔HTTP 桥接器,在此服务器以容器形式运行时:
```json
{
"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"
}
}
}
```
容器(或裸金属上的 `uv run music-assistant-mcp`)仍然需要在 `<MCP_HOST>:<MCP_PORT>` 处运行并可访问 - `mcp-proxy` 仅桥接主机的 stdio 期望值,而不是启动服务器本身。
## 连接到 n8n
将现有的 "Music MCP Client" 节点的端点指向此服务器的 `/mcp` URL,并将 bearer 凭证设置为 `MCP_BEARER_TOKEN`。 由于播放器解析和提供者 tiebreak 现在位于工具中,因此 AI 代理的系统提示可以大大简化 - 不需要逐步的顺序,播放器 ID 跟踪或源 tiebreak 规则。 请参阅 [PROMPT.md](PROMPT.md) 以获取建议的替换(角色 + 策略,确认后再清除,工具不强制执行)。
### 内置指令(适用于较弱/本地模型)
服务器还声明了一个 `instructions` 字符串作为 MCP `initialize` 响应的一部分(`FastMCP("music-assistant", instructions=...)` 在 `server.py` 中)- 大多数 MCP 客户端(确认 stdio 传输通过检查原始 `initialize` 响应)自动将此显示给模型,独立于主机设置的系统提示。 此存在是因为较小的本地模型(针对 9B 模型在 LM Studio 中运行测试)不会仅凭指令可靠地调用 `play` - 需要明确指示“立即行动,不要请求许可,直接调用工具”,而不仅仅是模型应该推理出来的内容。 如果您运行的是前沿模型,此指令文本是冗余的,与 PROMPT.md/common 感觉相同;如果您运行的是较小/本地模型,它实际上承担了重量,您可能根本不需要 PROMPT.md,除了它在上面添加的确认-清除策略。
## 开发
```bash
uv run pytest
```
测试涵盖了纯逻辑(播放器/提供者解析,源/范围 tiebreak,队列操作调度)与假的 Music Assistant 客户端对抗。
### 验证对抗真实服务器(schema 版本 31)
下面所有四个端到端场景都在实时服务器上运行并修复,直到绿色 - 过程中发现的错误值得了解,如果您进一步扩展:
- **`play(query="Relaxing music", player="Living Room")`** — 有效。
- **`play(query="The Prodigy", player="Living Room", media_types=["artist"], radio_mode=True)`** — 有效。 发现并修复:提供者可以返回一个无关紧要的顶级“匹配”用于身份查找(Tidal 返回“Fatboy Slim”用于“The Prodigy”)- `pick_best()` 现在需要一个与查询的名称匹配来应用源优先级;播放列表/广播仍然仅限优先级,因为主题查询(例如“爵士氛围”)不会与提供者自己的精选名称匹配。
- **`play(query="Coldplay", player="Living Room", media_types=["artist"], scope="local")`** — 有效。 发现并修复:`music/search` 的 `providers=` 限制在此服务器上不可靠(重复相同的请求有时返回每个提供者的结果,而不考虑过滤器)- `filter_by_providers()` 现在在客户端重新检查,包括通过其 `provider_mappings` 匹配合并的“库”项目(`provider="library"`)。
- **`group(action="join", players=["LedFX"], target_player="Living Room")` 然后 `group(action="leave", players=["LedFX"])`** — 有效。 发现并修复:`players/cmd/group_many` 使用 `child_player_ids` 而不是 `player_ids`。
通过直接交叉检查 `music-assistant-client` 的源代码(而不仅仅是实时调用)发现并修复的其他错误:`music/search` 在 **复数** 键下对结果进行分组(`playlists`/`tracks`/`albums`/`artists`,`radio` 保持单数)与单个 `media_types` 请求值不匹配;搜索结果的 `provider` 字段是一个 **实例 ID**,如 `spotify--9hcJiXgW`,而非纯域名,因此现在通过 `provider.split("--", 1)[0]` 派生域名;`player_queues/delete_item` 使用 `item_id_or_index` 而不是 `queue_item_id`;没有 `player_queues/get` 命令(`queue` 的 `"get"` 操作现在获取 `player_queues/all` 并过滤);`music/browse` 只取 `path` 而不取 `limit`/`offset`(从 `browse` 工具中删除);`MusicAssistantClient(...)` 需要 `aiohttp_session` 作为位置参数(传递 `None`);后台 `start_listening()` 读取循环与直接读取身份验证握手之间的启动竞争导致 `ConnectionClosed` - `ma_client.py` 现在等待 `start_listening(init_ready=...)` 信号就绪后再认为客户端已连接。
`radio_mode=True` 直接作为 `player_queues/play_media` 标志传递(不包装成 `radio_playlist://` URI)- 确认此服务器的 schema(31)早于客户端库进行的 schema-34 `radio_playlist` 翻译,因此标志是实际有效的。如果您在 schema ≥ 34 上,则标志可能不再被遵守,直接插入 `radio_playlist://playlist/<uri>`(按 `music_assistant_client.player_queues.radio_playlist_uri`)尝试。
### 宽容的默认值(容忍代理错误)
根据一个请求,确保一个被遗忘或错误的參數永远不会变成一个严重的“错误请求” - 对此进行了审计,并修复了所有工具的漏洞:
- `play`/`search`:无法识别的 `media_types` 条目(复数、“song”、拼写错误)被映射到正确的值或被删除,如果没有有效的条目,则默认回落到 `["playlist"]`;如果 `option` 不是四个有效值之一,则默认回落到 `"play"`。
- `control`:`command` 不区分大小写,并接受一些同义词(`resume`、`skip`、`prev`等);任何无法识别的命令都会回落到 `"play"`(附带一个注释),而不是报错。`seek_seconds` 被限制在 ≥ 0。`get`/`status`/`now_playing`/`info`/`state`/`current` 被识别为只读状态查询,并在回落前短路 - 这很重要,因为一个本地模型询问“正在播放什么?”会自己尝试 `control(command="get")`,如果没有这个功能,它会默默地默认到 `"play"`(即在收到问题时恢复/重新开始播放)。
- `volume`:调用时如果没有设置 `level`/`adjust`/`mute`,现在只会报告当前音量级别,而不是报错;`level` 被限制在 0-100;`adjust` 不区分大小写,并接受同义词(`louder`、`quieter`等);`group=true` 和 `mute` 不再报错 - 它只是静音单个播放器并解释原因。
- `queue`:无法识别的 `action` 会回落到 `"get"`(附带一个注释),而不是报错;`action="shuffle"` 如果没有 `shuffle` 布尔值,则默认为 `True`;`action="repeat"` 如果没有有效的 `repeat` 值,则默认为 `"all"`;`action` 不区分大小写。
- `group`:`action` 不区分大小写。
仍然作为硬错误(真正模糊,没有安全的默认值):一次调用 `volume` 时提供多个 `level`/`adjust`/`mute`;`queue` 的 `move_up`/`move_down`/`move_next`/`remove` 如果没有 `item_id`(无法安全猜测是哪个项目);`group` 的 `action` 如果不是 `join` 或 `leave`(相反的操作,不应猜测)。
### 队列增长错误(`option="play"` 没有清除队列)
实际发现:一个播放器的队列在正常使用过程中默默地增长到 3000+ 个项目。根本原因是 `play()`/`play_random()` 自身,而不是用户行为 - `music_assistant_models.enums.QueueOption` 将 `PLAY` 定义为“在当前位置插入新项目(s)并开始播放”,将 `REPLACE` 定义为“替换整个队列内容并从索引 0 开始播放” - 即 MA 自身的 `"play"` 选项 **不** 清除队列,只有 `"replace"` 才这样做。我们工具的文档/预期默认值(`option="play"` 意味着“清除队列并立即播放”)被发送到 `player_queues/play_media` 作为字面字符串 `"play"`,因此每个播放请求都默默地插入到一个不断增长的队列中,而不是从头开始。修复的方法是将我们的 `"play"`(和 `"replace"`)选项值映射到 MA 的 `"replace"`,而 `"next"`/`"add"` 仍然保持不变。
相关的队列管理覆盖,均为一次调用:添加到当前队列而不清除它(`play(query="...", option="add")` 或 `option="next"` 在当前曲目后插入);清除它(`queue(action="clear")` - 代理确认后按系统提示);打乱当前队列(`queue(action="shuffle", shuffle=true|false)`),或在开始新播放时打乱(`play(query="...", shuffle=true)` / `play_random(shuffle=true)` - 增加这样“播放 X 打乱”不需要额外的后续调用)。
### 精简的正在播放/队列状态
实际发现:MA 的原始 `player_queues/all` + `player_queues/items` 响应嵌套了每个曲目的完整媒体项目 - 图片、DSP 过滤器链、每个提供者的映射、外部 ID、流详细信息等。对于一个有几千个项目的队列,即使只获取当前和少数几个即将播放的项目,其响应也达到了数万个字符,这对于一个小型本地模型来说是无法有效使用的(并且会消耗任何模型的上下文/令牌)。`queue(action="get")` 和 `control` 的新状态命令现在返回一个预测的摘要 - `state`、`shuffle`、`repeat`、`queue_length`、`current`/`next`(仅限名称/艺术家/专辑/时长)和最多 10 个即将播放的曲目,形状相同。
### 人类请求模拟(默认范围 + 质量修复)
在真实服务器上模拟了现实生活中的请求 - 情绪/场合查询(“放松”、“浪漫的晚上”、“晚餐氛围”、“爵士乐”)、特定艺术家/曲目/专辑查找、显式 `source`/`scope` 和无查询的“惊喜我” - 检查工具如何实际为人服务,而不仅仅是检查它们是否运行而不会报错。发现并修复:
- **默认范围是“all”(在线 + 本地混合)**,这与人类期望的“播放一些东西”/“播放 `<artist>`”的行为不符(首先使用流媒体服务)。将默认值更改为 **“online”**,如果没有找到合适的在线内容,则自动扩展到所有内容(`scope="all"` 语义)- 因此,本地匹配仍然可以找到,而不需要显式指定本地范围,但默认情况下不会优先选择家庭 NAS 上文件名奇怪的文件而不是 Spotify/Tidal。
- **无意义的查询仍然可能“自信地”播放无关的内容** - 例如,搜索字面上的胡言乱语 `"asdkfjhqwoeiuasdf"` 作为艺术家返回了一个无关的真实艺术家,因为 Tidal/Spotify/Apple 优先级在 **等未匹配候选项** 中始终选择其中之一。`pick_best()` 现在在艺术家/曲目/专辑查找中,如果没有真正匹配查询的名称,则返回没有匹配(而不仅仅是错误的匹配),因此 `play()` 正确地报告“没有结果”,而不是播放错误的内容。
- **空/空白 `query` 返回垃圾**(本地播放列表的原始文件列表,而不是真正的搜索结果),而不是清晰地失败。`play`/`search` 现在直接拒绝空查询,`play` 的错误消息指向 `play_random` 以实现无查询的“惊喜我”案例。
- **匹配的项目可能仍然真正无法播放**(确认实时:一个同名的本地“爵士乐”播放列表,结果发现是空的)- `player_queues/play_media` 引发的 `MediaNotFoundError`/`UnplayableMediaError` 以前会使整个调用失败。`play()` 现在会自动尝试下一个候选项(最多 3 个),然后再放弃,并在一个注释中报告哪些被跳过。
- **新的 `play_random` 工具**,由 `music/tracks/library_items` 支持,带有 `order_by="random"`(一个真正的服务器端功能,已确认 - 而不是客户端的搜索结果打乱)- 允许“播放我本地库中的一些东西”/“惊喜我”在没有任何搜索查询的情况下构建临时混音。
### 仍未验证
- `transfer` (`player_queues/transfer`) 在上述实时场景中没有被触发。
- 本地/离线范围仅针对 `webdav` 提供者(此服务器的唯一非流媒体提供者)进行了测试 - `filesystem_local`/`filesystem_smb`/`filesystem_nfs` 共享应该分类为相同类型(`is_streaming_provider=False`),但未直接尝试。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器