Audiobook Curator
A complete plugin rather than a demonstration of one surface: a real media-management application assembled from React Server Component route modules, request context, durable state, MCP routes, and CLI routes. One build produces a generated stdio MCP server, an installable CLI, one Skill, and native Claude Code and Codex plugin artifacts. It is the reference consumer of the framework-owned package build.
- Run from the repository root:
pnpm example:audiobook - Package:
@agent-bundle-example/audiobook-curator - Public dependencies:
agent-bundle(workspace:*),@agent-bundle/runtime(workspace:*),@modelcontextprotocol/server,react,zod - Targets:
claude,codex, withmarketplace: true - Requires: Node.js 22.19 or later, plus
ffprobeandffmpegonPATH - Source:
examples/audiobook-curator
Optional features call the foreign tools that supply their evidence: Audiobook Forge, Audiolocate
in a selected Python environment, and whisper-cli with a selected model. Skipping them costs
those features, not the build. This example has no hooks.
What it proves
- The route tree is the application.
agent-bundle.config.tsdeclares identity, the Node runtime, and the two targets. File conventions discover the rest: 16 tool routes, one catalog resource, and one curation prompt undersrc/mcp/curator/. There is no route registry, no handwrittensrc/mcp/curator.ts, no per-operation server selector, and noroutessetting. Every tool usesdefineToolto keep its metadata, schemas, and handler in one definition. - One operation, one command. Every tool has a colocated
<tool>.cli.tsprojection,search_audible.cli.tsspells the tool asaudible-search,--duration, and a comma-separated or repeatable--regions;prepare_audiobook.cli.tsspellsoutputRootas--output; andconvert_audiobook.cli.tskeeps its nested conversion contract whole behind--input, so the compiled graph carries exactly 16 CLI commands, each with the tool's own identity (tool:curator/<tool>). There is nosrc/cli/directory and noaudiobook-curator curator <tool> --inputtwin: the projection is the command. Every projection declaresconfirm: falsebecause the plan-first commands gate their mutation on--apply, never on--yes. - Presentation is shared, not duplicated.
src/components/is one report library, and each tool module renders once for both surfaces: the MCP text content and the piped CLI Markdown are the same document, so a tool and its command cannot drift into two presenters. - Markdown blocks are JSX. The library's
DataListandFileListprimitives and the duplicate analysis's measured-files table author their Markdown through the runtime'sMarkdownContentcomponent,<ul>/<li>,<strong>, and a GFM<table>, instead of hand-concatenated strings, so escaping belongs to the renderer and piped CLI Markdown and MCP text content come from one output. - Request context is observed, not assumed. The conventional
src/providers/library.tsprobesffmpeg -versionandffprobe -versionper request and publishes tool availability with the probe time. The catalog resource reads it throughawait agent()and renders either the live context or an explicit unavailable state. - Durable state has a mounted identity.
src/state.tsmounts the workspace-durableaudiobook-curator/shelfstate with three events. If state is not mounted, the read-onlyreview_curation_shelftool,shelfon the command line, returns an empty structured shelf and an explicit unavailable notice instead of failing. - Suspense becomes MCP progress.
audit_libraryplaces its asynchronous analysis behind ReactSuspensewith anAgent.Progressfallback, and that fallback alone is the progress surface: the generated projector sends it asnotifications/progressto a client that passed a progress token, then replaces it with the completed analysis without changing the final structured receipt. Noprogress.report()call repeats the message. - The framework owns the package build. The config declares no
binorscriptsfield. The projected commands compile intodist/bin/audiobook-curator.mjsforpackage.jsonbin, anddist/index.jsplus declarations serveexports. See CLI and library package entries. package.jsonis the single version source. The resolved version flows into project context, artifact manifests,inspectoutput, dev status, and theagent-bundle/metaconstant this plugin imports.
Every command renders its tool's document
Each command runs its tool's component. An interactive terminal draws the streamed
Agent.Progress fallback in place; piped output is one final Markdown document, the same
headline and report the MCP client receives as text content. --json selects machine output and
emits one result-schema-validated JSON value followed by a newline: the canonical final
Agent.Result value, never the Markdown presentation and never an intermediate Suspense fallback,
and byte for byte the structuredContent of the tool call. Named --report and --receipt
options remain optional where a command projects fields as flags; convert carries its optional
receipt inside the JSON conversion object. A command that gets one still writes the receipt
file when it succeeds. This is a behavior change from the retired src/cli/ tree, where
every command that took a report or receipt path, thirteen of the sixteen, inventory --report
and convert --receipt among them, required it: those commands now run without the path and
write no report or receipt file, and their exit codes, --apply gating, and error output are
unchanged either way. inspect, prepare, and shelf never took one.
Working in the workspace
pnpm check inside the package runs validate, build, typecheck, and both test pools. One
agent-bundle build --output artifact produces everything: one composite plugin root at
artifact/ carrying the Claude and Codex projections, each host's manifest in its own dotfolder
over one shared Skill, bundled CLI script, and lifecycle-wrapped MCP server, plus the npm
package beneath dist/.
Start pnpm example:audiobook from the repository root for the visual development loop:
- Expand Application → MCP → curator → Tools and select
search_audible. - Enter a title such as
Dunein the generated input editor, then select Run. - Inspect Rendered first: it shows the actual Agent Document from the route's production RSC execution. Structured data, raw document, MCP/CLI projections, and Trace remain available as secondary tabs.
- Open Trace to see the invocation start and completion correlated under the run, then use Open route to return to this recorded result.
- Edit
src/mcp/curator/tools/search_audible.tsxor a component it renders. After the rebuild reaches Idle, rerun the saved input and inspect the updated rendered result.
The route is directly addressable at
/routes/mcp/curator/tool/search_audible; append ?invocation=<id> to reopen one of this
session's recorded results.
To exercise the built CLI without packing a tarball, link the built bin from any writable
directory already on PATH:
Running the server on stdio
The command resolves the generated entry from the Claude target's MCP manifest, building a
temporary artifact first; pass --artifact artifact to reuse the pnpm build output instead.
Closing stdin exits 0 and Ctrl-C exits 130, and per-server state persists under
.agent-bundle/mcp-run/claude/curator.
Safety boundaries
Sources are immutable and planning never mutates media. Conversion publishes to a separate destination; metadata and chapter replacement require an explicit apply, stage into a same-directory file, verify the staged result, and then rename atomically. JSON receipts refuse audio suffixes and collisions with media or evidence inputs. Network bodies, process output, traversal, and concurrency are bounded, and network work uses bounded attempts plus the caller's cancellation signal rather than hidden deadlines. Local media processes have no wall-clock deadline; caller cancellation and bounded stdout and stderr stay enforced.
The curation Skill directs the agent to perform only the requested operations:
inventory or audit alone does not require conversion or edition matching. For
a mutation, it prepares the exact plan and evidence before seeking missing
approval, and reuses approval only while those inputs remain unchanged. Every
mutating tool call still requires apply: true; source immutability and
human-reviewed edition selection remain intact.