ringo-mcp
An MCP server over stdio that exposes ringo SIP agents to LLM agents for telephony: place calls, accept incoming calls, send DTMF, transfer, observe call events and media quality.
How it works
- Each
[[agent]]in the config gets its ownringo-agentworker process (one SIP user agent each, with its own port and registration), driven over the framed stdio protocol — the same architecture ringo-flow uses. - Agents start lazily: the config is validated at startup (no processes),
and a worker is spawned by the first tool call that touches an agent
(single-flight — concurrent calls share one spawn). A worker that died is
respawned on the next tool call.
list_agentsshows configured-but-not- running agents with"running": false. - The binary re-invokes itself as
ringo-mcp agentfor those workers, so server and workers always share one build. - The MCP server speaks JSON-RPC on stdin/stdout; worker diagnostics go to stderr (MCP clients surface them as logs).
Install into an MCP client
Without --config, the path comes from $RINGO_MCP_CONFIG or defaults to
~/.config/ringo-mcp/config.toml.
Optional flags: --enabled-tools <LIST> / --disable <GROUP|TOOL> trim the
tool surface (groups: discovery, call-control, audio, headers, events,
recording, lifecycle, trace — or individual tool names);
--dial-deny <REGEX> / --dial-allow <REGEX> (repeatable, global) restrict
what dial/transfer may place a call to — deny always wins, e.g.
--dial-deny '^00' --dial-deny '^0900'; --version prints the version.
Config
[[]]
= "alice" # unique label used by every tool
= "1001"
= "pbx.example.com"
= "secret"
# password_file = "~/secrets/alice" # or: password_cmd = "pass show sip/alice"
# display_name, auth_user, transport, outbound, stun_server, media_enc,
# regint, mwi, dtmf_mode, catchall, audio_codecs — see the ringo-core Account.
# Custom headers on outgoing INVITEs (array of pairs, or a table):
# custom_headers = [["X-Session-Tag", "session-${uuid}"]]
# `${uuid}` re-renders on every outgoing call; `$$` is a literal `$`.
[[]]
= "bob"
= "1002"
= "pbx.example.com"
= "other-secret"
= "info" # SIP-INFO DTMF — reliable when the audio source is idle
[]
= "aubridge" # headless default (no sound hardware needed)
# max_calls, local_timeout_s, sip_cafile, sip_capath, user_agent,
# hold_other_calls (default false), record_audio (record every call by default)
Multiple agents = multiple UAs, one process each. Each registers its own AOR;
calls between them (or to/from the outside) are routed by your SIP
infrastructure. catchall defaults to true (safe here because each worker is
the only UA in its process — see the ringo-core Account docs).
Agents can also take their account from ringo-phone profiles — per agent or in bulk — so SIP credentials and custom headers live in one place:
[[]]
= "[prod] Office" # username/domain/credentials/headers from the profile
= "prod-office" # optional — defaults to the profile name's slug
= "info" # explicit fields override the profile
[]
# Or in bulk: every ringo-phone profile whose name matches a regex becomes an
# agent (unanchored, case-sensitive — escape `[` and `]`).
= ['^\[test\]']
list_agents/agent_status show such an agent's profile origin and its
description/metadata (e.g. metadata.number, so the LLM knows which
number reaches which agent). Patterns
select accounts, not just names — a broad regex exposes LIVE trunks to the LLM
too; the configuration docs have the rules and pitfalls.
Tools
| Tool | Description |
|---|---|
list_agents |
All configured agents: name, AOR, running, registration, calls |
agent_status |
One agent: registration, calls, media stats, received DTMF (starts the agent if not yet running) |
dial |
Outgoing call (agent, target: URI, user@host, or bare extension; optional record: true records the call); optional wait_established blocks until the outcome (established / failed with reason / still-ringing with call_id) |
accept / hangup / hangup_all |
Answer (optional record: true) / end calls |
hold / resume / mute |
In-call control. agent_status reflects local holds ("held" phase); peer holds arrive as call_hold/call_resume events |
send_dtmf |
One digit (0-9, *, #, A-F) |
transfer |
Blind transfer of the current call |
play |
What the agent transmits: "silence", "ausine,425", "aufile,<wav>" (resets to silence when the last call ends) |
wait_event |
Block for the agent's next event (JSON), up to 120 s; optional event filter and after_id log cursor (no lost events across tool roundtrips) |
agent_events |
Read the agent's event log with id cursors (catch up after looking away) |
agent_stop |
Stop an agent's worker (deregister + exit; restarts on next use) |
call_headers |
SIP headers of received INVITEs, per Call-ID or all (newest first) |
add_header / rm_header |
Custom headers on outgoing INVITEs (runtime; ${uuid} renders once, immediately) |
record_start / record_stop |
Record an agent's calls (sent + received; record_start starts fresh, record_stop keeps the audio for save_audio) |
save_audio |
Write the recorded audio to WAVs (paths, durations, truncation flag) |
sip_trace |
Recent SIP messages of an agent (plaintext, also TLS), filterable by Call-ID |
Call control is fire-and-forget: observe progress with wait_event
(call_ringing → call_established → call_closed) or by polling
agent_status.
Events
wait_event speaks these event names:
registering, register_ok, register_failed, unregistered,
call_incoming, call_outgoing, call_ringing, call_established,
call_closed (with reason/error), call_deflected, call_hold /
call_resume (the peer held or resumed), call_transfer_failed,
voicemail_status, response, backend_connect_failed. wait_event takes an
optional event filter — a name or an array, e.g.
["call_established", "call_closed"] to await a call's outcome either way.
Logging
Workers are silent unless RINGO_AGENT_LOG is set in the server's environment
(- for stderr, a path for a per-agent file); it is inherited by the worker
processes. SIP traces are on by default (text, in a private $XDG_RUNTIME_DIR/ringo-mcp-*
dir, removed on exit) and readable via sip_trace; set RINGO_AGENT_SIPTRACE to
write them elsewhere (a .pcap path for sngrep/Wireshark, - for stderr) —
sip_trace then only works for a text file.