The call-flow learner finally gets fed: exploration mode records its IVR discoveries on the call (ActiveCall.exploration_steps) instead of throwing them away, persistence stores them in the call record's metadata, and the rebuilt learn_call_flow MCP tool turns a completed exploration call into a stored flow via CallFlowLearner — correct constructor (llm_client from get_llm, heuristic labels when the LLM is unavailable), build for a new number, merge/refine when a flow already exists. save_learned_flow/update_flow_from_model keep the CallFlow↔row mapping in call_persistence. Test gaps closed: tests/test_learner.py (discoveries→linked steps, exploration persistence, learn-then-refine through the in-memory MCP client, no-data and unknown-call answers) and tests/test_websocket.py (4401 without token, trunk-status-then-replay on connect, per-call stream filtering). Docs aligned to code: README (15 tools incl. learn_call_flow, HTTP not SSE, Python 3.12+, PostgreSQL+Alembic — no SQLite fallback, media pipeline marked stub-mode until pjsua2 installed, Alembic and honest /health checked off); docs/mcp-server.md rewritten against the actual tool surface (hangup not end_call, real params, 3 real resources, /mcp/ streamable HTTP + bearer auth); architecture/development/ configuration drift fixed. pyproject: pruned never-imported deps (websockets, librosa, soundfile, python-multipart). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.0 KiB
MCP Server
The MCP (Model Context Protocol) server lets any MCP-compatible AI assistant
control the Hold Slayer gateway. Built with FastMCP,
it is mounted on the FastAPI app at /mcp/ (trailing slash) over
streamable HTTP and authenticates with the same static bearer token as the
REST API and WebSocket.
Overview
An AI assistant connects to the MCP endpoint and gains access to 15 tools and 3 resources for placing calls, checking status, sending DTMF, getting transcripts, and managing call flows. The assistant can orchestrate an entire call through natural language.
make_call places a real PSTN call that may incur charges; emergency
numbers are always refused, and the concurrent-call cap applies.
Tools
make_call
Place an outbound call through the SIP trunk.
| Param | Type | Required | Description |
|---|---|---|---|
number |
string | Yes | Phone number to call (E.164 format) |
mode |
string | No | direct, hold_slayer, or ai_assisted (default: direct) |
intent |
string | No | What you want to accomplish on the call |
call_flow_id |
string | No | ID of a stored call flow to follow |
device |
string | No | Device to transfer to when a human is detected |
Returns: call ID and initial status.
get_call_status
Check the current state of a call — status, duration, hold time, current audio classification, recent transcript.
| Param | Type | Required | Description |
|---|---|---|---|
call_id |
string | Yes | The call to check |
transfer_call
Transfer an active call to a registered device.
| Param | Type | Required | Description |
|---|---|---|---|
call_id |
string | Yes | The call to transfer |
device |
string | Yes | Device ID or type to ring |
hangup
Hang up an active call.
| Param | Type | Required | Description |
|---|---|---|---|
call_id |
string | Yes | The call to hang up |
list_active_calls
List all calls currently in progress. No parameters.
send_dtmf
Send touch-tone digits on an active call (manual IVR navigation).
| Param | Type | Required | Description |
|---|---|---|---|
call_id |
string | Yes | The call to send digits on |
digits |
string | Yes | DTMF digits (e.g., "1", "123#") |
get_call_transcript
Get the full transcript of an active call.
| Param | Type | Required | Description |
|---|---|---|---|
call_id |
string | Yes | The call to get the transcript for |
get_call_recording
Get recording metadata (path, duration) for a persisted call.
| Param | Type | Required | Description |
|---|---|---|---|
call_id |
string | Yes | The call to look up |
get_call_summary
Stored summary, action items, and sentiment for a persisted call.
| Param | Type | Required | Description |
|---|---|---|---|
call_id |
string | Yes | The call to look up |
search_call_history
Search past call records.
| Param | Type | Required | Description |
|---|---|---|---|
phone_number |
string | No | Filter by phone number (partial match) |
intent |
string | No | Filter by intent text (partial match) |
limit |
int | No | Max results (default: 10) |
get_call_flow
Look up the stored IVR call flow for a phone number.
| Param | Type | Required | Description |
|---|---|---|---|
phone_number |
string | Yes | Number to look up (E.164) |
create_call_flow
Store a new IVR call flow by hand.
| Param | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Human-readable name |
phone_number |
string | Yes | Phone number (E.164) |
steps_json |
string | Yes | JSON array of call flow steps |
notes |
string | No | General notes |
learn_call_flow
Build (or refine) a reusable IVR call flow from a completed hold-slayer exploration call. Exploration calls record every IVR prompt heard and DTMF sent; this turns those discoveries into a stored flow so the next call navigates directly.
| Param | Type | Required | Description |
|---|---|---|---|
call_id |
string | Yes | A completed call that ran in exploration mode |
company_name |
string | No | Company name for labeling a new flow |
list_devices
List registered devices and their online/offline status. No parameters.
gateway_status
Trunk registration, device count, active calls, engine mode. No parameters.
Resources
| Resource URI | Description |
|---|---|
gateway://status |
Current gateway status — trunk registration, active calls |
gateway://call-flows |
List of all stored call flows |
gateway://active-calls |
All active calls with current status |
Connecting an AI Assistant
Claude Code:
claude mcp add hold-slayer --transport http http://localhost:8000/mcp/ \
--header "Authorization: Bearer $API_TOKEN"
Generic MCP client configuration:
{
"mcpServers": {
"hold-slayer": {
"url": "http://localhost:8000/mcp/",
"headers": {"Authorization": "Bearer <API_TOKEN>"}
}
}
}
Example Conversation
User: "Call Chase Bank and dispute the Amazon charge from December 15th"
Assistant actions:
- Calls
make_call(number="+18005551234", mode="hold_slayer", intent="dispute Amazon charge Dec 15th", call_flow_id="chase-bank-main") - Receives
call_id: "call_abc123" - Polls
get_call_status("call_abc123")periodically - Status progression:
initiating→ringing→connected→on_hold - Tells user: "I'm on hold with Chase Bank. Currently 4 minutes in. I'll let you know when someone picks up."
- Status changes to
transferring— human detected! - Tells user: "A live agent just picked up. I'm transferring the call to your desk phone now. Pick up!"
- After the call, calls
learn_call_flow("call_abc123", company_name="Chase Bank")to save the IVR path for next time.