ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

peon-ping MCP Server:让 AI Agent 通过 play_sound 工具自选游戏音效的完整实现解析

peon-ping MCP Server:让 AI Agent 通过 play_sound 工具自选游戏音效的完整实现解析

【免费下载链接】peon-ping

Warcraft III Peon voice notifications (+ more!) for Claude Code, Codex, IDEs, and any AI agent. Stop babysitting your terminal. Employ a Peon today.

项目地址:https://gitcode.com/gh_mirrors/pe/peon-ping
点击查看免费下载

peon-ping 除了经典的 hook 事件触发模式外,还内置了一个基于 Model Context Protocol(MCP)的 Node.js 服务,让任何支持 MCP 的客户端(Claude Desktop、Cursor 等)都能以工具调用的方式播放 75+ 款游戏音效包,无需配置任何 hook。本文以 mcp/README.md 为主体,逐节展开其工具与资源设计、客户端接入配置、环境变量,并结合 mcp/peon-mcp.js 源码深入讲解目录发现、清单解析、音量优先级与各平台播放器调度的底层实现。

它解决什么问题:Agent 自选音效,而非事件固定音效

peon-ping 的默认工作方式是 hook 模式:Agent 会话事件(会话开始、等待输入、完成等)映射到固定音效。而 MCP 模式的核心差异在于:由 Agent 自己决定播什么。如 README.md 中所述,Agent 可以在构建失败时调用play_sound播放duke_nukem/SonOfABitch,在读取文件时播放sc_kerrigan/IReadYou。

整个服务端只暴露一个工具、两个资源:

MCP 特性作用
Tool:play_sound播放一个或多个音效,{ sound: "pack/Name" }或{ sounds: ["a/B", "c/D"] }
Resource:peon-ping://catalog完整目录——所有 pack 和音效,客户端一次读取,模型即有上下文
Resource:peon-ping://pack/{name}按类别列出某个 pack 内的全部音效

目录之所以做成 MCP Resource 而不是工具,是一个明确的工程取舍:客户端把目录预取一次放入模型上下文,避免模型反复发起“浏览工具”来查看有哪些音效可播。

前置要求与安装

运行前提(见 mcp/README.md):

  • Node.js 18+
  • 已安装 peon-ping(macOS、Linux 或 WSL2)

安装步骤:

# 若尚未安装 peon-ping curl -fsSL https://peonping.com/install | bash # 安装 MCP 服务端依赖 cd mcp && npm install

mcp/package.json 显示这是一个"type": "module"的 ES Module 包,唯一运行时依赖是@modelcontextprotocol/sdk(^1.12.1),并声明了bin入口peon-mcp。

主文档 README.md 补充了一个 Homebrew 场景:通过 Homebrew 安装 peon-ping 时,服务端入口位于$(brew --prefix peon-ping)/libexec/mcp/peon-mcp.js。

在 MCP 客户端中接入

Claude Desktop

将配置追加到~/Library/Application Support/Claude/claude_desktop_config.json:

{ "mcpServers": { "peon-ping": { "command": "node", "args": ["/path/to/peon-ping/mcp/peon-mcp.js"] } } }

Cursor

将同样的结构写入项目下的.cursor/mcp.json:

{ "mcpServers": { "peon-ping": { "command": "node", "args": ["/path/to/peon-ping/mcp/peon-mcp.js"] } } }

任意 MCP 客户端

任何支持 stdio 传输的 MCP 客户端都使用如下通用配置,可通过env注入音量等环境变量:

{ "command": "node", "args": ["/path/to/peon-ping/mcp/peon-mcp.js"], "env": { "PEON_VOLUME": "0.5" } }

将/path/to/peon-ping替换为你本地仓库或安装的根目录即可。服务端通过StdioServerTransport连接(mcp/peon-mcp.js 的await server.connect(new StdioServerTransport())),因此所有走 stdio 的 MCP 客户端协议上都开箱即用。

服务端架构:一个工具、两个资源

工具play_sound的输入与边界

源码中play_sound用 zod 定义入参(mcp/peon-mcp.js):

server.tool( "play_sound", 'Play a sound effect. Keys: "pack/sound" (e.g. "duke_nukem/Groovy"). Read the catalog resource for available sounds.', { sound: z.string().optional().describe("Sound key"), sounds: z.array(z.string()).optional().describe("Multiple sound keys"), }, ... );

处理逻辑中有几个值得注意的细节:

  • 键合并:const keys = arr || (sound ? [sound] : []),两个字段都缺省时返回No sound specified.;
  • 单批上限 5 个:keys.slice(0, 5),一次调用最多处理 5 个音效键,防止模型误传超长列表;
  • 逐键解析与分级报错:每个键按key.split("/", 2)拆出pack与sound,依次经过“格式非法(❌ Invalid)→ 目录中不存在该 pack 或音效(❌ Not found)→ 清单有记录但文件缺失(❌ Missing file)→ 成功入队(🔊 key ("label"))四个结果分支,最终以换行拼接的文本一次性返回。这种逐键反馈让模型能根据错误信息自我纠正键名。

音效解析:从pack/sound到真实文件路径

resolveSound(pack, sound)(mcp/peon-mcp.js)是键到路径的映射核心:它遍历该 pack 清单manifest.categories下每个类别的sounds数组,把file字段取 basename 并去掉扩展名后与请求的音效名做精确匹配,命中后返回{ path: <packs_dir>/<pack>/<s.file>, label: s.label || sound }。也就是说音效键是大小写敏感的字符串匹配,目录里到底有哪些可用键,完全由各 pack 的 CESP 清单决定。

队列:串行播放保证音效不重叠

playFile通过spawn启动播放器并等待其close/error事件。所有音效先进入queue,再由drain()逐个串行消费(mcp/peon-mcp.js)。一次play_sound调用传入多个键时,它们按顺序排队播放,互不覆盖;播放失败不会阻塞后续队列(error事件同样触发 resolve)。

两个 Resource 的目录输出格式

peon-ping://catalog(mcp/peon-mcp.js)按 pack 名字典序输出:

N packs. Use play_sound with "pack/SoundName". packA (显示名): packA/Sound1 ("label 1"), packA/Sound2 ("label 2") packB (显示名): ...

peon-ping://pack/{name}(mcp/peon-mcp.js)则按类别分组,每行一个音效键及其 label:

显示名: 类别名: pack/Sound1 — "label 1" pack/Sound2 — "label 2"

两个资源都在模块加载时惰性构建并缓存目录(loadCatalog()首次调用后复用),pack 不存在时返回Pack "<name>" not found.,未安装任何 pack 时 catalog 返回No packs installed.——这些文案都是给模型读的,可直接作为决策依据。

目录发现与 CESP 清单解析

pack 目录的搜索顺序(findPacksDir,mcp/peon-mcp.js):

  1. 环境变量PEON_PACKS_DIR(存在即采用);
  2. ~/.openpeon/packs;
  3. ~/.claude/hooks/peon-ping/packs;
  4. 都没有则返回null,所有资源返回空目录提示。

这与 peon-ping 主程序的用户数据目录策略一致:peon.sh 中同样是~/.claude/hooks/peon-ping与~/.openpeon二选一的发现逻辑,MCP 服务端只是复用了同一约定,保证无论安装方式如何,MCP 都能找到与 hook 模式相同的音效库。

目录加载时(loadCatalog)只认含openpeon.json的子目录,其余条目被跳过,解析失败(JSON 损坏)同样静默跳过。这里的openpeon.json即 CESP 清单,其结构可从 skills/peon-ping-create-pack/SKILL.md 得到印证:cesp_version、name、display_name、version、description,以及categories——每个类别映射到{"sounds": [{"file": "sounds/xxx.wav", "label": "..."}]}。MCP 服务端正是读取display_name与每个s.file/s.label来渲染 catalog 文本的。

音量与配置的解析优先级

源码顶部的配置加载(mcp/peon-mcp.js)定义了明确优先级:

const cfg = readConfig(); const volume = process.env.PEON_VOLUME ? (parseFloat(process.env.PEON_VOLUME) || 0.5) : (typeof cfg.volume === "number" ? cfg.volume : 0.5); const useSoundEffectsDevice = cfg.use_sound_effects_device !== false;
  • 配置文件候选路径依次为~/.claude/hooks/peon-ping/config.json与~/.openpeon/config.json,取第一个可解析的;仓库根目录的 config.json 展示了包含"use_sound_effects_device": true在内的完整字段形态;
  • 音量优先级:PEON_VOLUME环境变量 > 配置文件volume字段 > 默认0.5;
  • use_sound_effects_device缺省视为true,仅在显式配置为false时关闭。

macOS 上这个开关有实际意义:当其为true且~/.claude/hooks/peon-ping/scripts/peon-play存在时,服务端不走afplay,而是调用自研的 scripts/peon-play.swift 编译产物——它是一个将音频路由到系统“Sound Effects”输出设备的afplay替代实现,支持-v <volume>音量参数,避免提示音打断用户正在听的音乐设备。

版本号同样有兜底:先尝试读取仓库根的VERSION文件(当前为 2.37.0),失败则回落到硬编码的2.1.0,注册为new McpServer({ name: "peon-ping", version })。

跨平台播放实现

detectPlatform()(mcp/peon-mcp.js)把运行时映射为mac/wsl/linux/windows四类,其中 Linux 会通过检查/proc/version是否含microsoft字样进一步识别 WSL。各平台的实际播放命令:

平台播放器音量参数
macOSafplay(或peon-play)-v <volume>
WSL2powershell.exe -NoProfile -Command调Media.SoundPlayer不支持,固定音量
Linux按pw-play→paplay→ffplay→mpv→play→aplay顺序探测第一个可用的见下

Linux 各播放器的音量换算在getPlayCommand(mcp/peon-mcp.js)中逐一特化,因为各工具的量纲不同:

if (player === "pw-play") return [player, ["--volume", String(v), filePath]]; if (player === "paplay") return [player, ["--volume", String(Math.round(v * 65536)), filePath]]; if (player === "ffplay") return [player, ["-nodisp", "-autoexit", "-volume", String(Math.round(v * 100)), filePath]]; if (player === "mpv") return [player, ["--no-video", `--volume=${Math.round(v * 100)}`, filePath]]; return [player, [filePath]]; // play / aplay 不传音量

PEON_VOLUME的 0–1 归一化值被换算成 paplay 的 0–65536 线性刻度或 ffplay/mpv 的 0–100 百分比刻度;play(sox)与aplay则直接裸播、无法控音量。WSL2 走 PowerShellSoundPlayer时会将路径斜杠转换为反斜杠(mcp/peon-mcp.js)。与 mcp/README.md 的“Platform support”一节描述完全一致:macOS 用afplay,Linux 按序取第一个可用播放器,WSL2 用SoundPlayer。

环境变量汇总

变量默认值说明
PEON_PACKS_DIR~/.openpeon/packs音效包目录;仅当目录存在时生效,否则回落到内置搜索顺序
PEON_VOLUME0.5播放音量(0–1),优先级高于配置文件中的volume字段

两个变量都可在任意 MCP 客户端配置的env段中注入,无需改动服务端代码。

实际使用方式与适用边界

接入完成后的典型交互是:客户端启动时读取peon-ping://catalog资源(或peon-ping://pack/{name}查看单个 pack),模型据此得知全部可用音效键;随后在合适的时机发起工具调用,例如:

{ "sound": "duke_nukem/Groovy" }

或批量播放:

{ "sounds": ["peon/PeonReady1", "sc_kerrigan/IReadYou"] }

返回文本中逐键的🔊/❌前缀既是给用户的可读反馈,也是给模型的纠错信号——遇到Not found时模型可重新读取 catalog 校正键名。

适用前提与限制需要明确:

  • 该服务端只读本地已安装的 pack 目录,不负责下载或安装音效包;未安装任何 pack 时 catalog 会明确提示No packs installed.;
  • 音效键基于清单精确匹配,且单次调用最多处理 5 个键;
  • 音量控制依赖底层播放器能力,WSL2 的SoundPlayer与 Linux 的play/aplay路径无法应用PEON_VOLUME;
  • 服务端是纯本地 stdio 进程,不监听网络端口,行为完全由本地文件系统与系统播放器决定。

小结

peon-ping 的 MCP 服务端是一个约 200 行、零外部业务依赖的 Node.js 实现,设计上有三个可借鉴的点:目录信息以 Resource 形式一次性交付上下文,避免工具调用风暴;键的解析结果以带状态前缀的文本逐键返回,让模型能自我纠错;平台差异全部收敛在一个getPlayCommand函数内,用统一的 0–1 音量值向各播放器换算量纲。对于任何希望让 AI Agent 具备“发声反馈”能力的本地工具,这套“单工具 + 目录资源 + 本地串行队列”的结构都可直接参照 mcp/peon-mcp.js 的实现来移植。

【免费下载链接】peon-ping

Warcraft III Peon voice notifications (+ more!) for Claude Code, Codex, IDEs, and any AI agent. Stop babysitting your terminal. Employ a Peon today.

项目地址:https://gitcode.com/gh_mirrors/pe/peon-ping
点击查看免费下载
上一篇:如何轻松获取和配置Microsoft Office完整套件:三步完成专业部署
下一篇:Windhawk:免费开源的Windows系统定制与模块化扩展平台完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进