有声书策展器
这不是对某一个表面的演示,而是一个完整的插件:一个真实的媒体管理应用,由 React Server Component 路由 模块、请求上下文、持久状态、MCP 路由与 CLI 路由组装而成。一次构建产出一个生成的 stdio MCP 服务器、 一个可安装的 CLI、一个 Skill,以及原生的 Claude Code 与 Codex 插件产物。它是框架自有包构建的参考 消费者。
- 在仓库根目录运行:
pnpm example:audiobook - 包名:
@agent-bundle-example/audiobook-curator - 公开依赖:
agent-bundle(workspace:*)、@agent-bundle/runtime(workspace:*)、@modelcontextprotocol/server、react、zod - Target:
claude、codex,且marketplace: true - 需要: Node.js 22.19 或更高版本,以及
PATH上的ffprobe与ffmpeg - 源码:
examples/audiobook-curator
可选功能会调用为其提供证据的外部工具:Audiobook Forge、位于所选 Python 环境中的 Audiolocate,以及带
指定模型的 whisper-cli。跳过它们只会失去那些功能,而不影响构建。这个示例没有钩子。
它证明什么
- 路由树就是应用。
agent-bundle.config.ts声明标识、Node 运行时与两个 target。其余由文件约定 发现:src/mcp/curator/下的 16 个工具路由、1 个目录资源与 1 个策展提示词。这里没有路由注册表, 没有手写的src/mcp/curator.ts,没有逐操作的服务器选择器,也没有routes设置。每个工具都使用defineTool,把元数据、schema 与处理器放在同一个定义中。 - 一个操作,一条命令。 每个工具都有一个同目录的
<tool>.cli.ts投影模块,search_audible.cli.ts把工具拼写为audible-search、--duration,以及可逗号分隔或重复的--regions;prepare_audiobook.cli.ts把outputRoot拼写为--output;convert_audiobook.cli.ts则通过--input保留完整的嵌套转换契约,因此编译后的图恰好有 16 条 CLI 命令,每条都带着工具自己的身份(tool:curator/<tool>)。没有src/cli/目录,也没有audiobook-curator curator <tool> --input这样的孪生命令:投影就是命令。每个投影都声明confirm: false,因为这些先计划再执行的命令用--apply而不是--yes把关变更。 - 表现层是共享的,不是复制的。
src/components/是一份报告组件库,每个工具模块只渲染一次并服务 两种表面:MCP 文本内容与管道输出的 CLI Markdown 是同一份文档,因此一个工具和它的命令不可能漂移成 两套表现层。 - Markdown 块就是 JSX。 组件库中的
DataList、FileList原语以及重复分析中的已测量文件表格,都通过 运行时的MarkdownContent组件编写 Markdown,<ul>/<li>、<strong>和一个 GFM<table>,而不是手工拼接字符串,因此转义归渲染器 负责,管道输出的 CLI Markdown 与 MCP 文本内容出自同一份输出。 - 请求上下文是观察出来的,不是假定的。 约定式的
src/providers/library.ts在每个请求中探测ffmpeg -version与ffprobe -version,并连同探测时间一起发布工具可用性。目录资源通过await agent()读取它,并渲染实时上下文或一个显式的不可用状态。 - 持久状态有被挂载的身份。
src/state.ts挂载工作区级持久状态audiobook-curator/shelf与三个 事件。如果状态未挂载,只读的review_curation_shelf工具,命令行上是shelf,会返回一个空的 结构化书架和一条显式的不可用提示,而不是失败。 - Suspense 变成 MCP 进度。
audit_library把它的异步分析放到 ReactSuspense之后,回退内容是一个Agent.Progress节点,而这个回退本身就是全部的进度表面:生成的投影器会把它作为notifications/progress发送给传入了 progress token 的客户端,再用完成后的分析替换它,而最终的结构化收据不变。没有任何progress.report()调用重复这条消息。 - 包构建归框架所有。 配置没有声明
bin,也没有声明scripts。投影出的命令编译为dist/bin/audiobook-curator.mjs以服务package.json的bin,而dist/index.js加声明文件服务exports。见 CLI 与库包入口。 package.json是唯一的版本来源。 解析出的版本会流入项目上下文、 产物清单、inspect输出、dev 状态,以及这个插件导入的agent-bundle/meta常量。
每条命令都渲染其工具的文档
每条命令运行的就是其工具的组件。交互式终端会就地绘制流式的 Agent.Progress 回退;管道输出则是一份
最终的 Markdown 文档,与 MCP 客户端作为文本内容收到的是同一份标题与报告。--json 选择机器输出,
并输出一个经结果 schema 校验的 JSON 值加一个换行:这是最终的规范 Agent.Result 值,绝不是 Markdown
表现,也绝不是中间的 Suspense 回退,并且与工具调用的 structuredContent 逐字节一致。把字段投影为
flag 的命令仍提供可选的 --report 与 --receipt;convert 则把可选的 receipt 放在 JSON
conversion 对象中。传入了的命令在成功时仍会写出收据文件。这相对于已移除的
src/cli/ 树是一处行为变化:那里每条接受报告或收据路径的命令(十六条中的十三条,包括 inventory --report
与 convert --receipt)都把它设为必填;现在这些命令不带该路径也能运行且不写报告或收据文件,而退出码、
--apply 把关与错误输出在两种情况下都不变。inspect、prepare 与 shelf 从来不接受这个参数。
在工作区中开发
包内的 pnpm check 会运行 validate、build、typecheck 与两个测试池。一次
agent-bundle build --output artifact 产出全部内容:位于 artifact/ 的一个复合插件根目录,承载 Claude
与 Codex 两个投影,各宿主的清单位于自己的点目录中,共用同一份 Skill、打包后的 CLI 脚本与被生命周期
包装的 MCP 服务器,以及 dist/ 下的 npm 包。
从仓库根目录启动 pnpm example:audiobook,即可进入可视化开发循环:
- 展开 Application → MCP → curator → Tools 并选中
search_audible。 - 在生成的输入编辑器中输入诸如
Dune这样的标题,然后选择 Run。 - 首先检视 Rendered:它展示该路由生产 RSC 执行得到的真实 Agent Document。结构化数据、原始文档、 MCP/CLI 投影与 Trace 仍作为次要标签可用。
- 打开 Trace,查看归入此次运行的调用开始与完成条目,然后使用 Open route 返回这份已记录结果。
- 编辑
src/mcp/curator/tools/search_audible.tsx或它所渲染的某个组件。重建到达 Idle 之后, 重新运行已保存的输入,并检视更新后的渲染结果。
该路由可直接寻址
/routes/mcp/curator/tool/search_audible;追加 ?invocation=<id> 即可重新打开本次会话中一条已记录的
结果。
想在不打包 tarball 的情况下试用构建好的 CLI,可以从任意已在 PATH 上的可写目录链接构建产物中的 bin:
在 stdio 上运行服务器
该命令会从 Claude target 的 MCP 清单解析出生成的入口,并先构建一份临时产物;传入
--artifact artifact 可改为复用 pnpm build 的输出。关闭 stdin 以 0 退出,Ctrl-C 以 130 退出,
每个服务器的状态持久化在 .agent-bundle/mcp-run/claude/curator 下。
安全边界
源文件不可变,规划阶段绝不改动媒体。转换会发布到独立的目标位置;元数据与章节替换必须显式 apply, 先写入同目录的暂存文件,校验暂存结果,然后原子重命名。JSON 收据拒绝音频后缀,也拒绝与媒体或证据输入 发生冲突。网络响应体、进程输出、目录遍历与并发都是有界的,网络工作使用有界重试加调用方的取消信号, 而不是隐藏的超时。本地媒体进程没有挂钟期限;调用方取消与有界的 stdout、stderr 依旧强制生效。
整理 Skill 指示代理只执行请求范围内的操作:单纯盘点或审计不需要转换或版本匹配。需要修改时,先准备
准确的计划和证据,再请求缺少的批准;只有在输入未变时才复用已有批准。每次修改工具调用仍必须传入
apply: true,同时保留源文件不可变和人工审核版本选择的边界。