Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

cochlea-mcp

An MCP (Model Context Protocol) stdio server over the cochlea libraries. Any MCP client — Claude Code, another agent, a script — gets render / probe / spectro / lint / digest / diff as tool calls, so it can compose, render, and “listen” to audio through numbers and images without shelling out to the cochlea binary or reading raw PCM.

The protocol is hand-rolled JSON-RPC 2.0 over newline-delimited stdio: one JSON object per line in, at most one JSON object per line out. No async runtime — offline batch tools (render a score, extract a report, write a PNG) need none. Every response is a pure function of its request: no wall clock, no session state, so identical requests produce identical responses.

Tools

Each tool mirrors the matching cochlea CLI subcommand’s semantics exactly (see crates/cli/src/main.rs) over the same library calls — this is a second front end onto the same offline pipeline, not a reimplementation.

ToolArgumentsReturns
render_scorescore_path (string, required), out_path (string, required), stems_dir (string, optional), verify (bool, default false)Text summary: frame count, duration, sample rate, peak dBFS, stems written; if verify is set, the full verify-report JSON is appended and the call reports isError: true on a failed verification.
probe_audioaudio_path (string, required)The full feature report, schema v2 (loudness/LUFS/true peak/LRA, onsets, YIN pitch, chroma/key, tempo + clear_rhythm, stereo image, structural sections, silence, clipping) as pretty JSON. Works on any WAV or FLAC — no score required.
spectrogramaudio_path (string, required), out_path (string, required), sheet (bool, default false), bars_per_tile (integer, default 8)Text: the written PNG path and its pixel dimensions. Plain spectrogram or, with sheet: true, a tiled contact sheet — no score-aware bar markers yet (those need score context via render_score).
lint_scorescore_path (string, required)Text: "ok: no lint findings", or the JSON list of findings. isError: true iff any finding is Severity::Error, matching cochlea lint’s exit-1 threshold.
probe_digestaudio_path (string, required), window_ms (number, default 1000)A ~40-line deterministic text digest (cochlea_features::digest_text) instead of a full JSON report — the token-cheap way to “listen” to a WAV or FLAC. Prefer this over probe_audio unless the caller needs exact numbers to assert against.
audio_diffaudio_path_a (string, required), audio_path_b (string, required), window_ms (number, default 1000), json (bool, default false)Feature-space comparison text (cochlea_features::compare_text): a verdict (byte-identical / tier2-equivalent / different (dimensions...)) plus per-dimension deltas. json: true appends the full CompareReport as pretty JSON. A different verdict is a normal, successful answer — not isError.

Tool-level failures (a bad path, a render error, a failed verify or lint) come back as a normal tools/call success response with isError: true and the reason in the text content — never a JSON-RPC error. JSON-RPC errors (-32700/-32601/-32602) are reserved for protocol problems: malformed JSON, an unknown method, or missing/malformed arguments on a known tool. audio_diff’s different verdict is not one of these failures — see its row above.

Client setup

Claude Code:

claude mcp add cochlea -- cargo run -p cochlea-mcp --release

or, against an already-built binary:

claude mcp add cochlea -- /path/to/target/release/cochlea-mcp

Any other stdio MCP client: launch cochlea-mcp (or cargo run -p cochlea-mcp) as a subprocess and speak JSON-RPC 2.0 over its stdin/stdout, one object per line. stdout carries only protocol responses; all logging goes to stderr, so it’s safe to leave stderr connected to a terminal or log file without corrupting the transport.

Example

Request (tools/call for probe_audio, sent as one line):

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"probe_audio","arguments":{"audio_path":"mix.wav"}}}

Response (one line back; the pretty-printed report is escaped into the text field, shown here unescaped for readability):

{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\n  \"schema_version\": 2,\n  \"source\": {\n    \"sample_rate\": 48000,\n    \"channels\": 2,\n    ...\n  },\n  ...\n}"}],"isError":false}}

Testing this crate

crates/mcp/src/lib.rs exists so the dispatch logic (server::Server) can be driven in-process from integration tests — Server::handle_line(&str) -> Option<String> takes one request line and returns at most one response line, with no stdin/stdout/subprocess involved. crates/mcp/src/main.rs is just the framing loop around it. See crates/mcp/tests/protocol.rs for JSON-RPC conformance and crates/mcp/tests/tools_e2e.rs for a real render → probe → spectrogram round trip against examples/scores/first_light.ron.