feat: mount MCP server, add bearer auth, and guard outbound calls
The MCP server was created but never mounted — no client could reach
it. Mount it at /mcp/ over streamable HTTP with a combined lifespan,
resolving the gateway lazily so mounting happens at app construction.
Security and safety for the agent surface:
- One static API_TOKEN (SecretStr) enforced across REST (dependency),
WebSocket (query param/header before accept), and MCP
(StaticTokenVerifier). Startup refuses tokenless non-loopback binds.
- Emergency numbers (911/9911/112) always refused on make_call, plus a
MAX_CONCURRENT_CALLS cap; ValueError surfaces as 400/ToolError.
- Safe defaults: debug off, no credential in default DATABASE_URL,
SIP/LLM/TTS secrets as SecretStr.
Cleanups:
- Delete broken learn_call_flow tool (wrong ctor args, nonexistent
method) and the never-fed CallAnalytics service; keep
call_flow_learner for proper wiring later.
- Trim dial_plan to what is actually used (emergency guard, extension
allocation); delete the unreferenced matcher/normaliser.
- Register call_history before calls so /api/calls/history is no
longer shadowed by /api/calls/{call_id}.
- fastmcp pinned >=3.0 (http_app + StaticTokenVerifier).
New tests: MCP in-memory client (tool surface, lazy gateway, emergency
refusal, call cap) and API security (401 paths, route order, mount).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
71
README.md
71
README.md
@@ -6,12 +6,12 @@ You give it a phone number and an intent ("dispute a charge on my December state
|
||||
|
||||
> [!CAUTION]
|
||||
> **Emergency calling — 911**
|
||||
> Hold Slayer passes `911` and `9911` directly to the PSTN trunk.
|
||||
> **Your SIP trunk provider must support E911 on your DID and have your
|
||||
> correct registered location on file before this system is put into
|
||||
> service.** VoIP emergency calls are location-dependent — verify
|
||||
> with your provider. Do not rely on this system as your only means
|
||||
> of reaching emergency services.
|
||||
> Outbound calls to emergency numbers (`911`, `9911`, `112`) via the
|
||||
> REST API or MCP tools are **always refused** — an AI agent must never
|
||||
> place an emergency call, and API calls carry no E911 location data.
|
||||
> Do not rely on this system as any part of your means of reaching
|
||||
> emergency services; keep a phone with provider-registered E911
|
||||
> service available.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -73,13 +73,12 @@ You give it a phone number and an intent ("dispute a charge on my December state
|
||||
- **Transcription** (`services/transcription.py`) — Speaches/Whisper STT integration for live call transcription
|
||||
- **Recording** (`services/recording.py`) — WAV recording with date-organized storage, dual-channel support, persisted to the `recordings` table
|
||||
- **Call Persistence** (`services/call_persistence.py`) — Writes completed calls + transcript chunks to the database on hangup
|
||||
- **Call Analytics** (`services/call_analytics.py`) — Hold time stats, success rates, per-company patterns, time-of-day trends
|
||||
- **Notifications** (`services/notification.py`) — WebSocket + SMS alerts for human detection, call failures, hold status
|
||||
|
||||
### API Surface
|
||||
- **REST API** — Call management, call history, transcripts, recordings, routing rules, device DND, call flow CRUD
|
||||
- **WebSocket** — Real-time call events, transcripts, classification updates, receptionist state transitions
|
||||
- **MCP Server** — 10 tools for AI assistant integration (make calls, send DTMF, get transcripts, manage flows)
|
||||
- **MCP Server** — 14 tools + 3 resources for AI assistant integration (make calls, send DTMF, get transcripts, manage flows), served over streamable HTTP at `/mcp/`
|
||||
- **Dashboard** — SvelteKit UI served at `/dashboard` with live monitor, call history with transcript playback, and a routing-rules editor
|
||||
|
||||
### Data Models
|
||||
@@ -115,7 +114,6 @@ hold-slayer/
|
||||
│ ├── llm_client.py # OpenAI-compatible LLM client
|
||||
│ ├── transcription.py # Speaches/Whisper STT
|
||||
│ ├── recording.py # Call recording management
|
||||
│ ├── call_analytics.py # Call metrics and insights
|
||||
│ └── notification.py # WebSocket + SMS notifications
|
||||
├── api/
|
||||
│ ├── calls.py # Call management endpoints
|
||||
@@ -163,13 +161,25 @@ source .venv/bin/activate
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The PJSUA2 media pipeline needs the `pjsua2` Python bindings, which are
|
||||
> **not pip-installable** — they're built from pjproject (`./configure &&
|
||||
> make && make install` with `--enable-shared` and the Python SWIG target).
|
||||
> Without them the media layer runs in stub mode (signaling only).
|
||||
|
||||
### 2. Configure
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your SIP trunk credentials, LLM endpoint, etc.
|
||||
# Required: DATABASE_URL, and API_TOKEN unless HOST=127.0.0.1
|
||||
openssl rand -hex 32 # → API_TOKEN
|
||||
```
|
||||
|
||||
All REST, WebSocket, and MCP access requires `Authorization: Bearer
|
||||
$API_TOKEN` (WebSocket also accepts `?token=...`). An empty token is only
|
||||
permitted when bound to loopback.
|
||||
|
||||
### 3. Build the dashboard (optional but recommended)
|
||||
|
||||
```bash
|
||||
@@ -185,7 +195,7 @@ The gateway serves the built UI at `/dashboard` automatically when
|
||||
### 4. Run
|
||||
|
||||
```bash
|
||||
uvicorn main:app --host 0.0.0.0 --port 8100
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### 5. Test
|
||||
@@ -202,6 +212,7 @@ pytest tests/ -v
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/api/calls/hold-slayer \
|
||||
-H "Authorization: Bearer $API_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"number": "+18005551234",
|
||||
@@ -253,7 +264,7 @@ curl -X PATCH http://localhost:8000/api/routing/devices/dev_abc123/dnd \
|
||||
### WebSocket — Real-Time Events
|
||||
|
||||
```javascript
|
||||
const ws = new WebSocket("ws://localhost:8000/ws/events");
|
||||
const ws = new WebSocket(`ws://localhost:8000/ws/events?token=${API_TOKEN}`);
|
||||
ws.onmessage = (msg) => {
|
||||
const event = JSON.parse(msg.data);
|
||||
// event.type: "human_detected", "hold_detected", "ivr_step", etc.
|
||||
@@ -264,20 +275,33 @@ ws.onmessage = (msg) => {
|
||||
|
||||
### MCP — AI Assistant Integration
|
||||
|
||||
The MCP server exposes 10 tools that any MCP-compatible assistant can use:
|
||||
The MCP server is served over **streamable HTTP at `/mcp/`** (note the
|
||||
trailing slash) and authenticates with the same bearer token:
|
||||
|
||||
```bash
|
||||
claude mcp add hold-slayer --transport http http://localhost:8000/mcp/ \
|
||||
--header "Authorization: Bearer $API_TOKEN"
|
||||
```
|
||||
|
||||
It exposes 14 tools and 3 resources (`gateway://status`,
|
||||
`gateway://call-flows`, `gateway://active-calls`):
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `make_call` | Dial a number through the SIP trunk |
|
||||
| `end_call` | Hang up an active call |
|
||||
| `make_call` | Dial a real number through the SIP trunk (emergency numbers refused) |
|
||||
| `hangup` | Hang up an active call |
|
||||
| `transfer_call` | Transfer an active call to a device |
|
||||
| `send_dtmf` | Send touch-tone digits to navigate menus |
|
||||
| `get_call_status` | Check current state of a call |
|
||||
| `get_call_transcript` | Get live transcript of a call |
|
||||
| `get_call_recording` | Get recording metadata and file path |
|
||||
| `list_active_calls` | List all calls in progress |
|
||||
| `get_call_summary` | Analytics summary (hold times, success rates) |
|
||||
| `search_call_history` | Search past calls by number or company |
|
||||
| `learn_call_flow` | Build a reusable call flow from exploration data |
|
||||
| `list_devices` | List registered devices and status |
|
||||
| `gateway_status` | Trunk, devices, active calls, uptime |
|
||||
| `get_call_flow` | Look up a stored IVR flow for a number |
|
||||
| `create_call_flow` | Store a new IVR call flow |
|
||||
| `get_call_summary` | Stored summary and action items for a call |
|
||||
| `search_call_history` | Search past calls by number or intent |
|
||||
|
||||
## How It Works
|
||||
|
||||
@@ -307,6 +331,9 @@ All configuration is via environment variables (see `.env.example`):
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | — (required) |
|
||||
| `API_TOKEN` | Static bearer token for REST/WS/MCP | — (required unless `HOST=127.0.0.1`) |
|
||||
| `MAX_CONCURRENT_CALLS` | Cap on simultaneous outbound calls | `4` |
|
||||
| `SIP_TRUNK_HOST` | Your SIP provider hostname | — |
|
||||
| `SIP_TRUNK_USERNAME` | SIP auth username | — |
|
||||
| `SIP_TRUNK_PASSWORD` | SIP auth password | — |
|
||||
@@ -322,7 +349,6 @@ All configuration is via environment variables (see `.env.example`):
|
||||
| `RECEPTIONIST_ENABLED` | Answer inbound calls with the AI receptionist | `true` |
|
||||
| `RECEPTIONIST_GREETING_TEMPLATE` | Spoken greeting | `"Hi, you've reached Robert's line. Who's calling, and what's this about?"` |
|
||||
| `RECEPTIONIST_MESSAGE_MAX_SECONDS` | Voicemail cap | `90` |
|
||||
| `DATABASE_URL` | PostgreSQL or SQLite connection | SQLite fallback |
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -368,22 +394,21 @@ Full documentation is in [`/docs`](docs/README.md):
|
||||
- [x] Hold Slayer IVR navigation with LLM fallback for LISTEN steps
|
||||
- [x] Call Flow Learner — auto-builds reusable IVR trees from exploration
|
||||
- [x] Recording service with date-organized WAV storage
|
||||
- [x] Call analytics with hold time stats, per-company patterns
|
||||
- [x] Audio classifier with spectral analysis, DTMF detection, hold-to-human transition
|
||||
|
||||
### Phase 3: API & Integration ✅
|
||||
|
||||
- [x] REST API — calls, call flows, devices, DTMF
|
||||
- [x] WebSocket real-time event streaming
|
||||
- [x] MCP server with 16 tools + 3 resources
|
||||
- [x] MCP server with 14 tools + 3 resources, mounted at `/mcp/` (streamable HTTP)
|
||||
- [x] Notification service (WebSocket + SMS)
|
||||
- [x] Service wiring in main.py lifespan
|
||||
- [x] 75 passing tests across 4 test files
|
||||
|
||||
### Phase 4: Production Hardening 🔜
|
||||
### Phase 4: Production Hardening 🚧
|
||||
|
||||
- [ ] Alembic database migrations
|
||||
- [ ] API authentication (API keys / JWT)
|
||||
- [x] API authentication — static bearer token across REST/WS/MCP
|
||||
- [x] Emergency-number guard + concurrent-call cap on outbound calls
|
||||
- [ ] Rate limiting on API endpoints
|
||||
- [ ] Structured JSON logging
|
||||
- [ ] Health check endpoints for all dependencies
|
||||
|
||||
Reference in New Issue
Block a user