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

Getting started

Install

The SIP stack (baresip, libre, OpenSSL) is built in — no separate baresip install needed.

Homebrew (macOS / Linux):

brew install davidborzek/tap/ringo-mcp

Arch Linux (AUR) — prebuilt, via any AUR helper:

yay -S ringo-mcp-bin   # or: paru -S ringo-mcp-bin

Nix (flake):

nix profile install github:davidborzek/ringo#ringo-mcp

Enable the binary cache first (cachix use ringo) so this is a download instead of a long build; see ringo-phone’s install notes for NixOS settings and pinning a released version (github:davidborzek/ringo/ringo-mcp-v<version>#ringo-mcp).

Pre-built binaries for Linux and macOS (x86_64 + arm64) are on the releases page — download, extract and put ringo-mcp on your $PATH.

With cargo-binstall — the same prebuilt binary, no compile:

cargo binstall ringo-mcp

From crates.io (compiles, needs a C toolchain and CMake):

cargo install ringo-mcp

From GitHub (no clone needed):

cargo install --git https://github.com/davidborzek/ringo ringo-mcp

From a workspace checkout (no install):

cargo run -p ringo-mcp

The pre-built binaries, cargo binstall and cargo install link the opus and spandsp libraries at runtime — install them first (libopus0 libspandsp2 on Debian/Ubuntu, opus spandsp with Homebrew or pacman). Homebrew, the AUR package and Nix pull them in for you.

Homebrew 6.0+ requires third-party taps to be trusted before use. If brew install prompts you to trust the tap, accept it — or trust it up front:

brew tap davidborzek/tap
brew trust --formula davidborzek/tap/ringo-mcp

Configure your agents

Create ~/.config/ringo-mcp/config.toml (or pass --config <path>, or set RINGO_MCP_CONFIG). One [[agent]] per SIP identity:

[[agent]]
name = "support"                 # the name every tool addresses
username = "1001"
domain = "pbx.example.com"
password = "secret"
# password_file = "~/secrets/support"    # or: password_cmd = "pass show sip/support"

[[agent]]
name = "customer"
username = "1002"
domain = "pbx.example.com"
password = "other-secret"

At startup the config is validated (names, required fields, passwords resolved) — a broken file fails fast before any MCP traffic. Nothing else runs at startup: agents are spawned lazily by the first tool call that uses them, and a crashed worker is respawned the same way.

See Configuration for all fields.

Trim the tool surface (optional)

Every tool definition costs context in the LLM client — and some setups want a locked-down surface. Both flags take group names (discovery, call-control, audio, headers, events, recording, lifecycle, trace) or individual tool names:

ringo-mcp --disable call-control            # observe only, no dialing
ringo-mcp --enabled-tools discovery,events  # allowlist: just these
ringo-mcp --disable headers --disable add_header   # groups and tools mix

--enabled-tools is an allowlist (everything else is disabled); --disable subtracts on top of it. Unknown names fail at startup with the valid list.

Restrict what may be dialed

The agent shouldn’t be able to dial freely — expensive destinations, premium numbers. Two regex flags cover every dial/transfer target, globally for all agents (repeatable flags; each regex is matched against the dialed number and the full resolved URI):

ringo-mcp --dial-deny '^00' --dial-deny '^0900' --dial-allow '^\d{2,5}$'

--dial-deny always wins over --dial-allow; an empty allow list means unrestricted. A denied call is a tool error naming the matched rule, so the LLM can adjust. Invalid regexes fail at startup.

Wire it into an MCP client

ringo-mcp speaks MCP over stdio. Point your client at the binary:

Claude Code (claude mcp add):

claude mcp add ringo -- ringo-mcp

Claude Desktop / Cursor / generic (mcpServers in the client’s config):

{
  "mcpServers": {
    "ringo": {
      "command": "ringo-mcp",
      "args": ["--config", "/absolute/path/to/config.toml"]
    }
  }
}

Your first call

Ask your agent to make a call, and it will chain the tools like this:

ToolResult
list_agentswho is configured (running: false = not started yet)
dialplaces the call from an agent (spawns it if needed)
wait_eventblocks until call_established (or call_closed with a reason)
send_dtmf / play / transfer / hold …act on the call
hangupend it

The equivalent of a hello-world, by hand:

// dial
{ "agent": "support", "target": "0800123456" }

// wait for the outcome (blocking, up to 120s)
{ "agent": "support", "timeout_ms": 30000 }
// → { "event": "call_established", "call_id": "…" }

// done
{ "agent": "support" }

Dial targets accept full URIs (sip:1002@pbx.example.com), user@host, or a bare number/extension (resolved to sip:<target>@<agent's domain>). Numbers are cleaned up like in ringo-phone: tel:/callto: and separators (+49 89 123-456 → +4989123456) are dropped before the dial policy checks them.

Diagnostics

The MCP stream stays clean; everything else goes to stderr (most clients show it as server logs). Workers are silent unless you set RINGO_AGENT_LOG (- for stderr, a path for per-agent files) in the server’s environment; it is inherited by the worker processes.

SIP traces are on by default: the workers write them as text to a private temp dir ($XDG_RUNTIME_DIR/ringo-mcp-*, removed when the server exits), and the LLM reads them with sip_trace. To keep them elsewhere, set RINGO_AGENT_SIPTRACE yourself: a .pcap path writes a capture for sngrep/Wireshark, - writes to stderr. sip_trace only reads text files, so it reports the trace as unavailable in those cases.

To check a setup without an MCP client at all, drive the server by hand:

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_agents","arguments":{}}}' \
  | ringo-mcp