
Isimud
from bnomei
Have your agent speak to you (OSX only)
isimud
isimud is a macOS menu bar app and streamable-HTTP MCP server that lets AI agents speak. An agent sends text to an MCP tool; isimud resolves a named voice, synthesizes speech through Apple, OpenAI, or Google, then plays it through a single serialized speech queue.
isimud is the text-to-speech counterpart to muninn, which turns speech into text for agents.
Use isimud when you want:
- A local macOS tray app that pulses while an agent is speaking.
- A headless MCP server for scripted or background use.
- Local-first text-to-speech with optional bring-your-own-key cloud providers.
- Named voices that hide provider-specific voice IDs from agents.
- Queueing, cancellation, status, and speech lifecycle notifications over MCP.
Connect an MCP client
Configure your MCP client with streamable HTTP transport:
URL: http://127.0.0.1:3654/mcpIf you set ISIMUD_AUTH_TOKEN or [server].auth_token, add this request header:
Authorization: Bearer <TOKEN>The server only binds to loopback IP addresses. 127.0.0.1 and ::1 are accepted; hostnames such as localhost and non-loopback addresses such as 0.0.0.0 are rejected at startup.
When auth is configured, requests without the exact bearer token return HTTP 401.
Speak from an agent
Call isimud.speak with text:
{
"text": "Build finished.",
"voice": "default",
"rate": 1.0
}The call returns immediately by default:
{
"job_id": "00000000-0000-0000-0000-000000000000",
"queue_depth": 0
}Set wait to true when the MCP caller should block until the utterance completes, fails, is cancelled, or reaches [tts].wait_timeout_secs:
{
"text": "Deployment complete.",
"wait": true
}With wait=true, the response includes outcome:
{
"job_id": "00000000-0000-0000-0000-000000000000",
"queue_depth": 0,
"outcome": "completed"
}Configure voices and providers
Named voices are the public voice contract for agents. The voice argument to isimud.speak and [tts].default_voice must match a [voices.<name>] key.
[tts]
providers = ["apple", "openai", "google"]
default_voice = "default"
rate = 1.0
max_queue_depth = 64
wait_timeout_secs = 0
[voices.default]
provider = "apple"
voice = "Samantha"
[voices.narrator]
provider = "openai"
voice = "onyx"
[voices.googler]
provider = "google"
voice = "en-US-Neural2-C"
language = "en-US"Important: default_voice is a voice name, not a provider name. To make OpenAI the default, set default_voice = "narrator" or create another [voices.<name>] block whose provider = "openai".
Optional per-voice fields are language, rate, pitch, and volume. A request rate overrides the voice rate, which overrides [tts].rate; voice volume defaults to 1.0.
Provider availability follows [tts].providers. If the requested voice's provider is unavailable, isimud selects the first available fallback provider and drops the provider-specific voice ID for that fallback utterance. If no provider is available, the job fails and emits a failed event.
Provider setup
| Provider | Credentials | Behavior |
|---|---|---|
| Apple | None | Uses the macOS say command, plays inline, and enumerates installed voices with AVSpeechSynthesisVoice. |
| OpenAI | OPENAI_API_KEY or [providers.openai].api_key | Calls POST /v1/audio/speech, requests the configured response format, and plays returned audio through rodio. |
GOOGLE_API_KEY or [providers.google].api_key | Calls Google Cloud Text-to-Speech text:synthesize, decodes audioContent, and plays returned audio through rodio. |
OpenAI and Google send a rate/speed value only when the resolved rate is within 0.25..=4.0. Google sends pitch only when the resolved pitch is within -20..=20.
OpenAI built-in voice IDs exposed by isimud.list_voices:
alloy ash ballad cedar coral echo fable marin nova onyx sage shimmer verseGoogle voice listing is intentionally best-effort and currently returns an empty provider catalog; configured named Google voices still work.
MCP tool reference
| Tool | Parameters | Result |
|---|---|---|
isimud.speak | text required; optional voice, rate, wait | job_id, queue_depth; with wait=true, also outcome and optional error. |
isimud.stop | none | Cancels the active job if present and clears queued jobs. Returns cancelled_job and cleared. |
isimud.list_voices | none | Lists configured named voices and provider voice catalogs. |
isimud.status | none | Returns state, active job_id, voice, provider, queue_depth, and degraded. |
isimud.speak rejects empty text and unknown named voices as invalid parameters. When the queue is full, it returns JSON-RPC error code -32010 with this data payload:
{
"queue_depth": 64,
"capacity": 64
}Connected MCP peers receive isimud/speech_event custom notifications for these lifecycle events:
enqueued started finished failed stopped degradedCustom MCP requests named isimud/quit or isimud/exit trigger graceful shutdown.
Runtime behavior
isimud runs one speech worker. Jobs never overlap; each accepted utterance waits for the current utterance to finish or be cancelled.
The active job does not count toward [tts].max_queue_depth. That setting only limits jobs waiting behind the active utterance.
Saving the config file hot-reloads the speech engine and tray palette. Valid changes update voices, provider credentials, speaking rates, queue settings, and tray colors without a restart. Invalid edits are logged and the previous config stays active.
Server bind settings ([server].host, [server].port, [server].path, and auth) and LaunchAgent autostart sync are applied at startup. Restart isimud after changing those settings.
The macOS tray icon is gray while idle and pulses green while speaking. If the speech worker exits unexpectedly or a job panics, isimud.status reports degraded: true and a degraded event is broadcast.
macOS app features
URL scheme
The packaged app registers the isimud:// URL scheme. Use it to enqueue speech from macOS links or automation:
isimud://speak/Hello%20world
isimud://speak?text=Hello%20world&voice=narrator&rate=1.25The path text takes precedence over the text query parameter when both are present. The URL scheme is available when running the packaged .app that includes the CFBundleURLTypes entry.
Tray click
In menu bar mode, a left click on the tray icon tries to run the optional fortune command and speak its output. If fortune is not installed or returns no text, the click is ignored and a warning is logged.
Autostart
Set [app].autostart = true to sync a per-user LaunchAgent at ~/Library/LaunchAgents/com.bnomei.isimud.plist. The LaunchAgent uses the current executable path and sets ISIMUD_CONFIG to the resolved config path.
When running the packaged .app, prefer macOS Login Items if you want app-style launch behavior.
Packaging
Build a release binary for a target:
TARGET=aarch64-apple-darwin scripts/build-release.shBuild a macOS app bundle and zip archive from an existing release binary:
TARGET=aarch64-apple-darwin scripts/package-macos-app.shIf you built the default host target with cargo build --release --bin isimud, run scripts/package-macos-app.sh without TARGET.
Useful packaging variables:
| Variable | Default | Description |
|---|---|---|
BIN_NAME | isimud | Binary name copied into the app bundle. |
PRODUCT_NAME | Isimud | App bundle product name. |
BUNDLE_IDENTIFIER | com.bnomei.isimud | macOS bundle identifier. |
TARGET | unset | Selects target/<TARGET>/release/isimud as the binary path. |
BIN_PATH | unset | Explicit binary path. Overrides TARGET. |
ICON_PATH | packaging/macos/Isimud.icns | Optional icon copied into the app bundle when present. |
CODESIGN_APP | 1 | Ad-hoc signs with codesign --sign -. Set to 0 if codesign is unavailable. |
ZIP_APP | 1 | Set to 0 to skip zip creation. |
Create a tarball release archive from a built target:
VERSION=$(scripts/resolve-version.sh) TARGET=aarch64-apple-darwin scripts/package-release.shDevelopment
Run the main checks:
cargo test
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warningsThis repository also includes prek hooks:
prek install
prek run --all-filesCurrent limits
- macOS is the only supported platform.
- The MCP server supports streamable HTTP only; there is no stdio transport.
- The server binds only to loopback addresses.
- Apple
sayhonorsratebut does not applyvolumeorpitch. - OpenAI and Google request timeouts are left to the HTTP client and provider/network behavior.
[tts].wait_timeout_secsonly limits how long an MCPwait=truecall waits for the job outcome; it does not cancel synthesis. - isimud validates that
textis non-empty, but it does not enforce a fixed character, byte, token, or provider response-size limit. - Cloud-provider audio is decoded and played in memory, so very long utterances can increase memory use and playback latency.
Source map
- Configuration loading and defaults: src/config.rs
- MCP tools and notification fan-out: src/mcp.rs
- HTTP server and loopback/auth enforcement: src/server.rs
- Named voice resolution: src/voices.rs
- Speech queue and worker: src/worker.rs
- Provider registry and fallback: src/providers/mod.rs
- macOS tray behavior: src/runtime_tray.rs
- URL scheme parsing: src/url_scheme.rs
- macOS app bundle template: packaging/macos/Info.plist.template
Quickstart
Complete this path to run isimud with the local Apple text-to-speech provider. Apple TTS does not require an API key.
Prerequisites
- macOS. The project is macOS-only; the packaged app declares macOS 12.0 or newer.
- Rust 1.89 or newer and Cargo.
- An MCP client that supports streamable HTTP.
Install
Install the published binary crate:
cargo install isimud-text-to-speechVerify that Cargo installed the isimud binary:
isimud --versionOr build from this repository:
cargo build --release --bin isimud
./target/release/isimud --versionConfigure
isimud creates a launchable default config on first run if no config file exists. That generated config contains a single Apple-backed default voice. To start from the full sample config instead, create the config directory and copy the sample:
mkdir -p ~/.config/isimud
cp configs/config.sample.toml ~/.config/isimud/config.tomlConfig path precedence is:
ISIMUD_CONFIG$XDG_CONFIG_HOME/isimud/config.toml~/.config/isimud/config.toml
Run
Start the menu bar app and MCP server:
isimudFor server-only operation:
isimud --headlessCLI options:
--headless Run only the MCP server
-h, --help Print help
-V, --version Print the versionExpected result:
- In menu bar mode, a small isimud indicator appears in the macOS menu bar.
- The MCP server listens on
http://127.0.0.1:3654/mcp. - Calling
isimud.statusfrom your MCP client returnsstate: "idle"when nothing is speaking.
Configuration reference
See configs/config.sample.toml for the full sample.
| Setting | Default | Description |
|---|---|---|
[app].menubar | true | Runs the macOS tray app. --headless disables the tray for that process. |
[app].autostart | false | Syncs a per-user macOS LaunchAgent that starts the current executable at login. |
[server].host | "127.0.0.1" | Loopback bind host. Must parse as an IP address. |
[server].port | 3654 | MCP HTTP port. |
[server].path | "/mcp" | MCP streamable-HTTP path. |
[server].auth_token | unset | Optional bearer token. ISIMUD_AUTH_TOKEN takes precedence. |
[tts].providers | ["apple", "openai", "google"] | Provider fallback order. |
[tts].default_voice | "default" | Name of the default [voices.<name>] entry. |
[tts].rate | 1.0 | Neutral speaking-rate multiplier. |
[tts].max_queue_depth | 64 | Number of jobs allowed behind the active utterance. 0 disables the limit. |
[tts].wait_timeout_secs | 0 | Timeout for wait=true calls. 0 waits forever. |
[indicator.colors.*] | Apple system gray/green palette | Menu bar indicator colors. Values must be #RRGGBB hex strings. |
The TOML schema is strict. Unknown fields fail config parsing; hot reload keeps the previous config when a changed file fails to parse or validate.
Environment variables:
| Variable | Purpose |
|---|---|
ISIMUD_CONFIG | Overrides config path resolution. |
ISIMUD_AUTH_TOKEN | Overrides [server].auth_token. |
ISIMUD_LOAD_DOTENV=0 | Disables startup loading of ./.env from the current working directory. Also accepts false or no. |
OPENAI_API_KEY | Enables the OpenAI provider. |
GOOGLE_API_KEY | Enables the Google provider. |
RUST_LOG | Overrides [logging].level; useful targets are runtime, server, provider, config, and speech. |
Shell environment variables win over .env and config-file secrets.
No common issues documented yet. If you hit a problem, the repository's GitHub Issues page is the best place to look.
Licensed under MITโ you can use, modify, and redistribute it under that license's terms.
License
MIT. See LICENSE.