Skill: joe12801/xiaozhi-mcp-music-official (version 1.0.0)

--- name: xiaozhi-mcp-music-official description: 按小智官方 MCP 接入方式，为小智增加在线音乐播放能力。适用于已经有小智 MCP 接入点（wss://api.xiaozhi.me/mcp/?[REDACTED_SECRET] MCP 工具实现搜歌、播放、暂停、继续、停止等在线音乐控制的场景。支持在线音乐 API 搜索、多源 fallback、调用本地播放器播放网络音频链接。 Official XiaoZhi MCP online music bridge for searching and playing online music through local players such as mpv. --- # xiaozhi-mcp-music-official [简体中文](#简体中文) | [English](#english) --- ## 简体中文 ### 作用 这是一个最小可用的 **小智在线音乐 MCP 原型**，按小智官方 MCP 接入方式设计。 ### 架构 ```text 小智 → MCP 接入点 → mcp_pipe.py → music_mcp.py → 在线音乐 API → 本地播放器（mpv） → 返回结果给小智 ``` ### 提供的工具 - `play_music(query)` - `play_music_index(query, n)` - `stop_music()` - `pause_music()` - `resume_music()` - `next_track()` - `set_volume(level)` - `music_info()` ### 当前方案说明 - 当前接入在线点歌 API - 支持多源 fallback（优先 `kuwo`） - 优先尝试从 API 返回中提取可播放直链 - 用 `mpv` 直接播放在线 URL - 如果没有可播放链接，就把歌曲信息返回给小智 ### 启动 ```bash pip install -r requirements.txt cp .env.example .env python3 mcp_pipe.py music_mcp.py ``` ### 环境变量 - `MCP_ENDPOINT`：小智 MCP 接入点 - `MUSIC_API_KEY`：音乐 API key - `MUSIC_SOURCE`：默认优先源，建议 `kuwo` - `PLAYER_CMD`：播放器命令，默认 `mpv` ### 注意事项 - 如果服务器里没有安装 `mpv`，播放会失败，但搜歌和返回信息仍然可用。 - 当前是最小原型，后续还可以升级成播放列表、上一首/下一首、音量精控、多平台音乐源版本。 --- ## English ### Purpose This is a minimal working **XiaoZhi online music MCP prototype**, designed following XiaoZhi's official MCP integration style. ### Architecture ```text XiaoZhi → MCP endpoint → mcp_pipe.py → music_mcp.py → online music API → local player (mpv) → return result to XiaoZhi ``` ### Provided tools - `play_music(query)` - `play_music_index(query, n)` - `stop_music()` - `pause_music()` - `resume_music()` - `next_track()` - `set_volume(level)` - `music_info()` ### Current approach - Uses an online music API - Supports multi-source fallback (prefers `kuwo`) - Tries to extract a playable direct link first - Uses `mpv` to play network audio URLs - If no playable URL is returned, it reports the matched song info back to XiaoZhi ### Start ```bash pip install -r requirements.txt cp .env.example .env python3 mcp_pipe.py music_mcp.py ``` ### Environment variables - `MCP_ENDPOINT`: XiaoZhi MCP endpoint - `MUSIC_API_KEY`: music API key - `MUSIC_SOURCE`: preferred source, recommended `kuwo` - `PLAYER_CMD`: player command, default `mpv` ### Notes - If `mpv` is not installed on the server, playback will fail, but search/info retrieval will still work. - This is a minimal prototype and can later be extended with playlists, previous/next track, fine-grained volume control, and richer music sources.

path: README.md
content: # xiaozhi-mcp-music-official

[简体中文](#简体中文) | [English](#english)

---

## 简体中文

这是一个按小智官方 MCP 接入方式设计的**在线音乐播放 MCP 原型**。

### 主要能力
- 在线搜歌
- 多源 fallback
- 返回歌曲信息
- 如果拿到直链，则调用本地播放器直接播放
- 支持暂停、继续、停止等基本控制

### 依赖
```bash
pip install -r requirements.txt
```

### 配置
复制 `.env.example` 为 `.env`，填写：
- `MCP_ENDPOINT`
- `MUSIC_API_KEY`
- `MUSIC_SOURCE`
- `PLAYER_CMD`

### 启动
```bash
python3 mcp_pipe.py music_mcp.py
```

### 推荐播放器
建议安装：
```bash
apt-get install -y mpv
```

---

## English

This is an **online music playback MCP prototype** designed for XiaoZhi using the official MCP integration style.

### Main capabilities
- Search online music
- Multi-source fallback
- Return track info
- Play direct music URLs through a local player when available
- Support pause / resume / stop basic controls

### Install dependencies
```bash
pip install -r requirements.txt
```

### Configuration
Copy `.env.example` to `.env`, then fill in:
- `MCP_ENDPOINT`
- `MUSIC_API_KEY`
- `MUSIC_SOURCE`
- `PLAYER_CMD`

### Start
```bash
python3 mcp_pipe.py music_mcp.py
```

### Recommended player
Install `mpv` if you want actual playback:
```bash
apt-get install -y mpv
```


path: _meta.json
content: {
  "name": "xiaozhi-mcp-music-official",
  "version": "1.0.0",
  "description": "Official-style XiaoZhi MCP online music bridge with search and playback support.",
  "author": "OpenClaw Assistant",
  "tags": ["xiaozhi", "mcp", "music", "player", "online-music"]
}


path: mcp_pipe.py
content: import asyncio
import websockets
import subprocess
import logging
import os
import signal
import sys
from dotenv import load_dotenv

load_dotenv()
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger('MCP_PIPE')

async def pipe_websocket_to_process(websocket, process):
    while True:
        msg = await websocket.recv()
        if isinstance(msg, bytes):
            msg = msg.decode('utf-8', errors='replace')
        process.stdin.write(msg + '\n')
        process.stdin.flush()

async def pipe_process_to_websocket(process, websocket):
    loop = asyncio.get_event_loop()
    while True:
        line = await loop.run_in_executor(None, process.stdout.readline)
        if not line:
            break
        await websocket.send(line)

async def pipe_stderr(process):
    loop = asyncio.get_event_loop()
    while True:
        line = await loop.run_in_executor(None, process.stderr.readline)
        if not line:
            break
        sys.stderr.write(line)
        sys.stderr.flush()

async def main():
    if len(sys.argv) < 2:
        print('Usage: python3 mcp_pipe.py <mcp_script>')
        raise SystemExit(1)

    uri = os.environ.get('MCP_ENDPOINT')
    if not uri:
        print('Missing MCP_ENDPOINT')
        raise SystemExit(1)

    script_path = sys.argv[1]
    async with websockets.connect(uri) as websocket:
        process = subprocess.Popen(
            ['python3', script_path],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            encoding='utf-8',
            errors='replace'
        )
        try:
            await asyncio.gather(
                pipe_websocket_to_process(websocket, process),
                pipe_process_to_websocket(process, websocket),
                pipe_stderr(process)
            )
        finally:
            process.terminate()
            try:
                process.wait(timeout=3)
            except Exception:
                process.kill()

if __name__ == '__main__':
    signal.signal(signal.SIGINT, lambda s, f: sys.exit(0))
    asyncio.run(main())


path: music_mcp.py
content: from mcp.server.fastmcp import FastMCP
import subprocess
import os
import requests
from dotenv import load_dotenv

load_dotenv()
mcp = FastMCP("龙虾音乐")

[REDACTED_SECRET]'MUSIC_API_KEY', '')
PLAYER_CMD = os.environ.get('PLAYER_CMD', 'mpv')
PREFER_SOURCE = os.environ.get('MUSIC_SOURCE', 'kuwo')  # kuwo / baidu / migu / netease

SOURCES = {
    'kuwo': 'https://api-v2.yuafeng.cn/API/kwmusic.php',
    'netease': 'https://api-v2.yuafeng.cn/API/wymusic.php',
    'migu': 'https://api-v2.yuafeng.cn/API/mgmusic.php',
    'baidu': 'https://api-v2.yuafeng.cn/API/bdmusic.php',
}

LAST_TRACK = {}


def api_search(source: str, query: str, n: int = 1, quality: str = '1'):
    url = SOURCES[source]
    params = {
        'apikey': API_KEY,
        'msg': query,
        'n': n,
        'type': quality,
    }
    r = requests.get(url, params=params, timeout=20)
    r.raise_for_status()
    return r.json()


def find_playable(query: str, n: int = 1, quality: str = '1'):
    order = [PREFER_SOURCE] + [k for k in SOURCES.keys() if k != PREFER_SOURCE]
    for source in order:
        try:
            data = api_search(source, query, n=n, quality=quality)
            item = data.get('data') or {}
            music = item.get('music')
            if isinstance(music, str) and music.startswith('http'):
                item['_source'] = source
                return item
            # 即使没有音乐直链，也先记下最接近的结果
            if item.get('song'):
                item['_source'] = source
                return item
        except Exception:
            continue
    return None


def play_url(url: str):
    # 直接交给本地播放器。mpv 对网络资源支持最好。
    subprocess.Popen([PLAYER_CMD, url])


@mcp.tool()
def play_music(query: str) -> str:
    """
    Play online music by song name / singer / keyword.
    Use when user asks to play a song or music.
    """
    global LAST_TRACK
    if not [REDACTED_SECRET] '音乐 API KEY 未配置。'
    item = find_playable(query, n=1, quality='1')
    if not item:
        return f'没有找到和 {query} 相关的在线音乐。'

    song = item.get('song', query)
    singer = item.get('singer', '未知歌手')
    music = item.get('music')
    source = item.get('_source', 'unknown')
    LAST_TRACK = item

    if isinstance(music, str) and music.startswith('http'):
        try:
            play_url(music)
            return f'正在播放 {song} - {singer}（来源：{source}）。'
        except Exception as e:
            return f'已找到 {song} - {singer}，但播放失败：{str(e)}'

    return f'已找到 {song} - {singer}，但当前接口没有返回可直接播放的音频链接。'


@mcp.tool()
def play_music_index(query: str, n: int) -> str:
    """
    Play the Nth result for a search query.
    Useful when user wants the second/third search result.
    """
    global LAST_TRACK
    if not [REDACTED_SECRET] '音乐 API KEY 未配置。'
    item = find_playable(query, n=n, quality='1')
    if not item:
        return f'没有找到第 {n} 个与 {query} 相关的结果。'

    song = item.get('song', query)
    singer = item.get('singer', '未知歌手')
    music = item.get('music')
    source = item.get('_source', 'unknown')
    LAST_TRACK = item

    if isinstance(music, str) and music.startswith('http'):
        try:
            play_url(music)
            return f'正在播放第 {n} 个结果：{song} - {singer}（来源：{source}）。'
        except Exception as e:
            return f'已找到第 {n} 个结果 {song} - {singer}，但播放失败：{str(e)}'

    return f'第 {n} 个结果是 {song} - {singer}，但当前没有可直接播放的链接。'


@mcp.tool()
def stop_music() -> str:
    try:
        subprocess.run(['pkill', '-f', PLAYER_CMD], check=False)
        return '已经停止播放。'
    except Exception as e:
        return f'停止失败：{str(e)}'


@mcp.tool()
def pause_music() -> str:
    try:
        subprocess.run(['pkill', '-STOP', '-f', PLAYER_CMD], check=False)
        return '音乐已暂停。'
    except Exception as e:
        return f'暂停失败：{str(e)}'


@mcp.tool()
def resume_music() -> str:
    try:
        subprocess.run(['pkill', '-CONT', '-f', PLAYER_CMD], check=False)
        return '继续播放。'
    except Exception as e:
        return f'继续播放失败：{str(e)}'


@mcp.tool()
def next_track() -> str:
    return '下一首功能可后续接入播放列表。当前建议直接说“播放某首歌”。'


@mcp.tool()
def set_volume(level: int) -> str:
    return f'已收到音量设置请求：{level}。如需精确控制，可后续接入播放器命令。'


@mcp.tool()
def music_info() -> str:
    global LAST_TRACK
    if not LAST_TRACK:
        return '当前还没有最近一次点歌信息。'
    return f"最近一次点歌：{LAST_TRACK.get('song', '未知歌曲')} - {LAST_TRACK.get('singer', '未知歌手')}。"


if __name__ == '__main__':
    mcp.run(transport='stdio')


path: requirements.txt
content: fastmcp>=0.1.0
websockets>=12.0
python-dotenv>=1.0.0


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)