@miadi/voice-mcp
v0.4.3
Published
MCP server fronting the Miadi voice layer with episode addressing — play, find, publish and mint episode-bound voice from an agent's tool surface.
Readme
@miadi/voice-mcp
An MCP server that lets an agent play, find and publish voice messages tied to Miadi episodes, and create an episode when none fits.
In the Miadi stack, this is how an agent uses voice from its own tool loop. It is a front over
@miadi/voice-client, so the audio, the ledger and the credentials stay on the Miadi server,
and it reads episode folders from the chronicle. All six tools work.
Tools
| Tool | Purpose | State |
|---|---|---|
| voice_publish_to_episode | Produce audio bound to its episode from the moment it exists | works |
| voice_play_episode | Resolve an episode to its voice audio and return playable references | works |
| voice_list_episode_voices | Enumerate and search an episode's voices | works |
| voice_resolve_episode | Answer which episode an agent is inside — with the reason, the alternatives, and an honest null | works, from what it is given |
| voice_create_episode | Mint a vessel when none fits, reporting all five closing stages | works (0.4.0) |
| voice_episode_closing_status | Re-prove the five closing stages, read-only, naming drift | works (0.4.0) |
Publishing requires a return address
voice_publish_to_episode takes a required origin. A voice nobody can be
steered back through cannot be answered, and that — not the audio — is what the
portal card is for.
"origin": {
"user": "mia", // whoami
"host": "my-host", // hostname -s, not the FQDN
"cwd": "/home/me/project", // pwd
"multiplexer": "tmux", // or "herdr". "none" is refused.
"session": "work", // tmux display-message -p "#{session_name}"
"pane": "%127" // $TMUX_PANE — herdr also needs workspace
}Read these values — do not compose them. Inside herdr they are already in your environment and cost nothing:
| field | where it actually is |
|---|---|
| workspace | $HERDR_WORKSPACE_ID |
| tab | $HERDR_TAB_ID |
| pane | $HERDR_PANE_ID |
| session | default when $HERDR_SOCKET_PATH is ~/.config/herdr/herdr.sock, else the <name> in .../herdr/sessions/<name>/herdr.sock |
Inside tmux: $TMUX_PANE and tmux display-message -p "#{session_name}". Read
them in the same invocation that publishes — a pane id copied from another
message is a real id pointing at somebody else's terminal.
The server is the authority, twice over:
- Shape. An absent, incomplete, or
"none"declaration answers400 { error, problems, help }— every missing field at once, plus the shape to send — and no TTS is spent on it. - Truth. When the declaration names the portal's own host and user, it is probed against the live multiplexer inventory. An address that is not in it is refused, and the refusal prints what the inventory does hold, so an agent cannot invent a pane id that leads nowhere.
A probe that cannot run — another host, no herdr installed — is unprovable, not
a refusal: it publishes, attested stays "self-declared", and the reason is
recorded on the record. An unreadable inventory proves nothing in either
direction.
reach is the server's verdict, never the caller's claim: declaring a different
host or a multiplexer owned by another user publishes fine and renders as
unreachable, because that is a fact about the topology rather than a defect in
the declaration. attested: "server" means the pane was seen, not that this
persona occupies it — the card says pane live rather than verified, and
verified stays unspent until occupancy itself can be proven.
Resolution consults exactly what it is handed: an explicit reference, a
caller-supplied cwd, or an existing record's binding. It does not rank
those against a session origin or search the wheel. When nothing given answers, the tool says
so; episode: null with a reason is a valid answer.
Architecture
A front, not a second implementation.
- Audio bytes, the KV ledger and the Edge-TTS producer stay behind the Miadi
server, which holds the KV credentials.
This process reaches them through
@miadi/voice-client, which owns the wire envelope and the token tier — so the HTTP surface is described once, not once per caller. - Episode folders are read from the chronicle root; the chronicle's medicine wheel is read
at
MIADI_CHRONICLE_MW_URL. VoiceMessagekeeps exactly one definition in this workspace:@miadi/voice's.- The only wheel write in the whole surface is the registration
mkepisodeperforms duringvoice_create_episode. This server never POSTs or PUTs to the wheel itself.
Configuration
| Variable | Default | Meaning |
|---|---|---|
| MIADI_API_URL | http://127.0.0.1:3335 | Miadi server holding the voice routes |
| MIADI_API_TOKEN_WRITER | — | Read and write. Required to publish. |
| MIADI_API_TOKEN_READER | — | Read only; every publish answers 401 |
| MIADI_CHRONICLE_MW_URL | http://127.0.0.1:8040 | Chronicle wheel. Read first; MW_API_URL_OVERRIDE / MW_API_URL are legacy fallbacks. |
| MIADI_CHRONICLE_ROOT | /srv/miadi/episodes/miadi-chronicle | Episode folders |
| MIADI_CHRONICLE_GIT_ROOT | /srv/miadi/episodes | Git root — one level above the chronicle root |
| MIADI_MKEPISODE_PATH | …/mightyeagle/packages/passages/js/mkepisode.js | Vessel creation |
The chronicle wheel is the only correct target for episode work. A retired wheel host is refused outright, because an episode registered there would be lost.
MIADI_CHRONICLE_GIT_ROOT is a separate value rather than a derived one because
a git -C pointed at the chronicle root reports a clean tree for a chronicle it
cannot see.
Creating episodes: created is not closed
voice_create_episode performs stages 1 and 4 of the five-stage gate and returns
every stage with its proof:
| Stage | Performed by |
|---|---|
| created | the tool |
| committed | the human — the tool emits the exact git add of named files |
| pushed | the human |
| registered | the tool |
| receipt-verified | the tool |
mkepisode exits 0 whether registration succeeded, skipped, or failed —
registration is fail-open by design, so the gate belongs to the caller. The tool
never runs git add, commit or push, never reports a later word than it
proved, and reports unperformed stages as owed, which is a state, not an
omission.
Server version
The episode binding is written by the server's publish route. A Miadi server built before that
route accepted episode ignores the field and stores an unbound record, so check the running
build before reading a missing binding as a client bug.
