Skip to main content
Glama

CallTool

CallTool is a self-hosted telephone execution service for AI agents. Clients create and control outbound calls through MCP or REST, and the same worker can answer inbound calls to a Telnyx number. LiveKit carries media between Telnyx SIP and the selected native realtime voice provider: Gemini Live or OpenAI Realtime.

The complete architecture and product decisions are documented in projects-v0.1.md. The versioned delivery plan is documented in ROADMAP.md. Deployment and Telnyx provisioning are documented in docs/self-hosting.md.

Development status

The repository is under active development on the feature/initial branch. The v0.1 target is a configurable multilingual inbound and outbound caller with durable state, policy-controlled commitments, human-in-the-loop input, and low-latency native audio conversation. German remains the default language.

Related MCP server: AgentPhone MCP Server

Prerequisites before docker compose

There are two separate requirements: Docker must be able to run the local stack, and Telnyx, LiveKit, and at least one supported realtime AI provider must be configured before real phone calls can work. Python and uv are not required on the host when everything is run through Compose; they are only needed for local development commands.

Host and Docker

You need:

  • A Linux server or workstation with a public, reachable IP address for real SIP calls. A private laptop behind NAT is fine for software tests, but Telnyx cannot deliver inbound SIP traffic to localhost.

  • Docker Engine and the Docker Compose plugin. Verify both with:

    docker --version
    docker compose version
    docker info
  • Permission for the current user to access Docker. If the user was just added to the docker group, open a new terminal or run newgrp docker before using Docker.

  • Outbound internet access from Docker. The first build downloads container images and Python packages from Docker Hub, GHCR, and PyPI. Test DNS before starting:

    docker run --rm busybox:1.36 nslookup files.pythonhosted.org

    If this fails with a DNS or network error, fix the Docker daemon's DNS/forwarding first. docker compose will otherwise fail during uv sync even if DNS works on the host itself.

  • Free host ports 80, 443, 5060, 7881, 10000-20000/udp, and 50000-60000/udp. The Compose file uses host networking for LiveKit and LiveKit SIP; another SIP service or web server must not already use these ports.

For an internet-facing deployment, allow these ports in both the server firewall and the hosting provider's firewall:

Port

Protocol

Purpose

80

TCP

Caddy HTTP and certificate validation

443

TCP/UDP

CallTool and LiveKit HTTPS/HTTP3

5060

TCP

Telnyx to LiveKit SIP signaling

7881

TCP

LiveKit RTC fallback

10000-20000

UDP

LiveKit SIP RTP media

50000-60000

UDP

LiveKit RTC media

The checked-in LiveKit configuration uses use_external_ip: true. The advertised address must therefore be reachable from the public internet. If the server is behind a router, forward the ports above and make sure the public IP and SIP hostname resolve to the router/server correctly.

DNS names

For a real deployment, create DNS A records pointing to the server's public IP:

Name

Used for

calltool.example.com

Public CallTool REST and MCP endpoint

livekit.example.com

Public LiveKit WebSocket endpoint through Caddy

sip.example.com

Telnyx FQDN SIP destination, direct to port 5060

calltool.example.com and livekit.example.com are entered as hostnames only, without https://, in CALLTOOL_DOMAIN and LIVEKIT_DOMAIN. The SIP hostname must not be put behind an HTTP reverse-proxy or CDN proxy; Telnyx connects to it as SIP/TCP, not HTTP. Caddy only handles the first two hostnames.

The defaults localhost and livekit.localhost are useful for local HTTP checks, but they are not sufficient for Telnyx inbound calls. Caddy also needs ports 80 and 443 reachable from the internet to obtain public certificates for real DNS names.

Telnyx account and information

The following must already exist in the Telnyx Mission Control Portal:

  1. A paid Telnyx account with the required identity/Level 2 verification completed. German numbers may require identity and address documents before activation.

  2. An active German phone number (DID), written in +E.164 format, for example +49301234567. The number must be assigned to the SIP connection below.

  3. An FQDN SIP Connection configured for both inbound and outbound calling:

    • FQDN destination: sip.example.com, port 5060 (replace the hostname). If the portal uses one SIP URI field, enter sip:sip.example.com:5060.

    • Inbound SIP region: Europe, unless another region is intentionally used.

    • Transport: TCP on port 5060.

    • Outbound authentication: Credentials with a dedicated SIP username and password. These are the values for TELNYX_SIP_USERNAME and TELNYX_SIP_PASSWORD.

    • AnchorSite: Latency is suitable for the first setup; a fixed European site can be selected later if required.

  4. An Outbound Voice Profile assigned to that SIP Connection. Allow Germany (DE) and every other destination the agent is allowed to call. If the portal asks for a traffic type/service plan, select Conversational and the Global plan intended for voice-bot traffic.

  5. The German number assigned to this SIP Connection under Telnyx's number settings.

This setup does not need a Telnyx API key in .env: the sip bootstrap command creates the LiveKit-side trunks through the self-hosted LiveKit API. A Telnyx API key is only needed if the Telnyx connection and number setup is automated through Telnyx's API. The Telnyx SIP username/password are not the Telnyx portal login and not the API key.

The current European Telnyx SIP signaling addresses are 185.246.41.140 and 185.246.41.141; they are already allow-listed in config/calltool.yaml. If you select another Telnyx SIP region, update calls.inbound.allowed_addresses from Telnyx's current SIP network information before running the bootstrap. Telnyx's current RTP networks and port requirements are listed on the same page and should be considered when restricting the server firewall.

AI provider and LiveKit credentials

You also need:

  • Credentials for the selected realtime voice provider:

    • Gemini: a Google AI Studio API key in GOOGLE_API_KEY, with access to the configured Gemini Live model.

    • OpenAI: an OpenAI API key in OPENAI_API_KEY, with Realtime API access. Supported models are gpt-realtime-2.1 and the faster, lower-cost gpt-realtime-2.1-mini. There is no OpenAI model named gpt-realtime-2.1-flash.

  • The default background supervisor and Gemini scripted-TTS features also use GOOGLE_API_KEY. To run with only an OpenAI key, disable voice.supervisor.enabled in config/calltool.yaml; Gemini scripted TTS is bypassed automatically for OpenAI Realtime calls. Keep optional Gemini shadow STT disabled as well.

  • A self-hosted LiveKit key and secret. For the first local test, keep the matching development values from config/livekit.yaml and config/sip.yaml: devkey / secret. For production, generate strong values and replace them in all three places: .env, config/livekit.yaml, and config/sip.yaml.

  • A strong random CALLTOOL_API_KEY. It protects all REST and MCP routes except health, readiness, and metrics. Do not leave change-me in an internet-facing deployment.

  • A strong random WEBHOOK_SIGNING_SECRET if WEBHOOK_URL is configured. Webhooks are optional; leave WEBHOOK_URL empty if no event receiver is needed.

Configure .env before starting

From the repository root:

cp .env.example .env

Edit .env and set at least these values for real calls:

CALLTOOL_ENV=development
CALLTOOL_API_KEY=<strong-calltool-api-key>

# Configure the provider key or keys that are used by your setup.
GOOGLE_API_KEY=<google-gemini-api-key>
OPENAI_API_KEY=<openai-api-key>

TELNYX_SIP_ADDRESS=sip.telnyx.eu
TELNYX_SIP_USERNAME=<telnyx-outbound-sip-username>
TELNYX_SIP_PASSWORD=<telnyx-outbound-sip-password>
TELNYX_FROM_NUMBER=+49...

CALLTOOL_DOMAIN=calltool.example.com
LIVEKIT_DOMAIN=livekit.example.com
LIVEKIT_SIP_TRUNK_ID=

Use +E.164 for both TELNYX_FROM_NUMBER and every number sent to the API. Keep LIVEKIT_SIP_TRUNK_ID empty initially; the bootstrap command prints the value after it creates the reusable outbound trunk. The Compose file supplies the internal database, Redis, and LiveKit URLs automatically. If you change the default PostgreSQL password, also set POSTGRES_PASSWORD in .env; Compose uses it for both PostgreSQL and CallTool. Set it before the first start; changing it later does not change the password inside an already-initialized PostgreSQL volume.

For production, change CALLTOOL_ENV to production only after replacing every development placeholder (change-me, devkey, and secret) and applying the matching LiveKit config changes described above. Keep .env private and never commit it.

Voice provider, model, language, and voice

The checked-in default is Gemini in German with the Puck voice. You can change the global default in config/calltool.yaml, or use these .env overrides without editing YAML:

# Gemini default
CALLTOOL_VOICE_PROVIDER=gemini
CALLTOOL_VOICE_MODEL=gemini-3.1-flash-live-preview
CALLTOOL_VOICE_LANGUAGE=de
CALLTOOL_VOICE_NAME=Puck

For OpenAI Realtime, choose either the full model or the official mini model:

OPENAI_API_KEY=<openai-api-key>
CALLTOOL_VOICE_PROVIDER=openai
CALLTOOL_VOICE_MODEL=gpt-realtime-2.1
CALLTOOL_VOICE_LANGUAGE=de
CALLTOOL_VOICE_NAME=marin

Use gpt-realtime-2.1-mini when lower latency and cost matter more than the full model's capability. Both variants are native speech-to-speech models. The supported built-in OpenAI voices are alloy, ash, ballad, coral, echo, sage, shimmer, verse, marin, and cedar. Eligible OpenAI custom voice IDs beginning with voice_ are accepted as well.

Language accepts a compact BCP-47 language tag such as de, en, en-US, or fr-FR. It is included in the system instructions and, for OpenAI, passed to input-audio transcription. The voice is selected before the realtime session starts and cannot be changed after that session has produced audio.

An individual outbound REST or MCP call can override all four values. Per-call values take precedence over .env, and .env takes precedence over config/calltool.yaml:

{
  "target": {"phone_number": "+49301234567", "name": "Test"},
  "objective": "Vereinbare einen Rückruf.",
  "voice": {
    "provider": "openai",
    "model": "gpt-realtime-2.1-mini",
    "language": "en-US",
    "voice": "cedar"
  }
}

For OpenAI calls, the native Realtime model also speaks the initial disclosure and greeting so the voice stays consistent from the first sentence. Gemini calls keep the pre-synthesized scripted greeting for German; other languages use the selected native realtime model for the localized greeting.

The exact model names and integration options are documented by OpenAI GPT-Realtime-2.1, OpenAI GPT-Realtime-2.1 Mini, and the LiveKit OpenAI Realtime plugin.

v0.1.1 voice and telephony test switches

The LiveKit-first v0.1.1 features are independently configurable. The checked-in self-hosted defaults use provider-native turn detection, VAD interruption handling, no Krisp, and keep IVR, AMD, and Cold Transfer disabled:

CALLTOOL_TURN_DETECTION_MODE=realtime_llm
CALLTOOL_INTERRUPTION_MODE=vad
CALLTOOL_IVR_ENABLED=false
CALLTOOL_AMD_ENABLED=false
CALLTOOL_COLD_TRANSFER_ENABLED=false
CALLTOOL_KRISP_ENABLED=false

Empty values use config/calltool.yaml. Valid turn modes are realtime_llm and livekit_v1_mini; the only interruption mode in this fully self-hosted build is vad. The local v1-mini path disables the realtime provider's server-side endpointing for that session, uses the local LiveKit VAD, and loads the approximately 108 MB local turn-detector model in the worker process. No external LiveKit service or inference gateway is used. Only one turn strategy is active per call.

For livekit_v1_mini, CALLTOOL_TURN_UNLIKELY_THRESHOLD and CALLTOOL_TURN_BACKCHANNEL_THRESHOLD optionally override the detector thresholds for the selected language. Keep them empty until German evaluation calls establish suitable values; this makes the threshold decision measurable instead of silently treating the English defaults as calibrated German values.

Enabling IVR exposes CallTool's audited send_dtmf tool on outbound calls and enables LiveKit IVR loop/silence handling. telephony.ivr controls allowed digits, maximum digits per action, inter-digit delay, the complete navigation timeout, and whether raw digits may be written to audit events. Raw digits are hidden by default.

AMD is outbound-only and runs once immediately after answer. It uses LiveKit AMD with a small provider-native text classifier (gemini-2.5-flash-lite or gpt-4.1-mini) and the realtime session's input transcript, so the fully self-hosted path does not require a second STT or an additional inference service. Keep voice.realtime.input_transcription: true. The telephony.amd policy can hang up, continue, leave the file-prompted voicemail from voicemail-instruction.md, or request a human decision through the existing MCP/REST respond flow. AMD category, transcript, timings, action outcome, and errors are stored in the call state and events.

Cold Transfer uses LiveKit's SIP transfer API/SIP REFER and is exposed only when both the global switch is enabled and the outbound request grants may_transfer. Telnyx must allow SIP REFER for the connection. A transfer-capable request includes:

{
  "target": {"phone_number": "+49301234567", "name": "Praxis"},
  "objective": "Rufe die Praxis an und übergib bei Bedarf an mich.",
  "permissions": {
    "may_commit": false,
    "may_accept_costs": false,
    "may_transfer": true,
    "may_disclose": []
  }
}

The transfer target passes the same country, emergency-number, and premium-number policy as a normal outbound call. Transfer ID, target, timeout result, LiveKit reason, and SIP status remain available in PostgreSQL through call status/conversation responses.

LiveKit plugin metrics are aggregated in Prometheus without synchronously writing raw samples to PostgreSQL. Per-turn EOT/transcription/response delays, native interruption events, false interruptions, usage, and a compact final session report are also captured. CallTool's /metrics endpoint exposes these alongside lifecycle, tool, policy, watchdog, and barge-in metrics.

File-based prompt profiles

System prompts and default greetings are not embedded in Python. The checked-in profile is located at config/prompts/default and contains:

File

Purpose

system-outbound.md

Complete system prompt for outbound calls

system-inbound.md

Complete system prompt for inbound calls

greeting-outbound.txt

Default outbound greeting source text

greeting-inbound.txt

Default inbound greeting source text

greeting-instruction.md

Instruction used when the native realtime model localizes the greeting

voicemail-instruction.md

Policy-safe message used when AMD selects leave_message

ivr-instruction.md

Navigation strategy used after AMD recognizes an IVR

watchdog-instruction.md

Model instruction used to recover from an unexpected silent turn

watchdog-fallback.txt

Final scripted recovery phrase if model recovery fails

supervisor.md

Prompt for the optional post-call outcome supervisor

To create a company-specific profile, copy the directory and edit the copied files:

cp -a config/prompts/default config/prompts/lwlp

Then select it in .env using its path inside the Compose container:

CALLTOOL_PROMPT_DIR=/app/config/prompts/lwlp

Alternatively, change voice.prompts.directory and the optional filenames in config/calltool.yaml. Compose mounts the complete local config directory read-only at /app/config, so prompt edits do not require rebuilding the image. A profile is read as one snapshot when a call starts; changes therefore affect the next call, never the middle of an active conversation.

Templates use deliberately limited {{ placeholder }} substitution without executable template code. Available placeholders are:

  • Call data: call_id, direction, objective, target_name, target_phone_number, caller_name, caller_phone_number, called_phone_number, and organization_name.

  • Runtime data: language, context_json, constraints_json, permissions_json, may_commit, may_accept_costs, may_transfer, and may_disclose_json.

  • Greeting instruction only: greeting_json, containing the rendered greeting as a JSON string.

  • Supervisor only: outcome_json, containing the structured call result.

Unknown or malformed placeholders, missing files, invalid UTF-8, empty files, and files larger than 128 KiB make the API/worker validation fail. Check a profile before calling:

docker compose run --rm calltool-api doctor

For security, MCP and REST callers cannot submit arbitrary server-side file paths. The operator selects the mounted profile, while call-specific objective, context, permissions, language, and voice continue to arrive through the normal request schema. Keep the AI disclosure in custom greeting and system templates where legally required.

Quick start

Once the prerequisites and .env are ready, validate and start the stack:

docker compose config --quiet
docker compose up -d --build
docker compose ps

Wait until PostgreSQL and Redis are healthy and inspect startup errors with:

docker compose logs --tail=100 calltool-api calltool-worker livekit livekit-sip

Create the reusable LiveKit outbound trunk, inbound trunk, and inbound dispatch rule once. This uses the Telnyx SIP values from .env:

docker compose run --rm calltool-api sip bootstrap

Copy the printed Set LIVEKIT_SIP_TRUNK_ID=... value into .env, then recreate the API and worker so they receive it:

docker compose up -d --force-recreate calltool-api calltool-worker
docker compose run --rm calltool-api doctor

The doctor must report READY before placing a real call. To test inbound calling, call TELNYX_FROM_NUMBER from another phone while following the logs:

docker compose logs -f calltool-worker livekit-sip

For an explicit outbound diagnostic call to a number you control:

docker compose run --rm calltool-api doctor --call +49...

Each inbound caller is routed to a dedicated room and the calltool worker answers with the greeting from config/prompts/default/greeting-inbound.txt or the selected custom prompt profile. The first inbound call should be made only after the Telnyx number is assigned to the SIP Connection and the bootstrap has completed.

Durable call history and conversations

PostgreSQL is the source of truth for every inbound and outbound call. The calls.direction column distinguishes inbound from outbound; created_at, connected_at, and ended_at record when the job arrived, when the telephone conversation actually started, and when it ended. History searches use started_at, which means connected_at when available and otherwise created_at for unanswered or not-yet-connected calls.

phone_call.list returns both directions newest-first by default. It can filter by direction, exact phone_number, case-insensitive partial target_name, status, and a time window. started_after is inclusive, started_before is exclusive, and both require an ISO 8601 timezone. Use next_cursor unchanged to fetch the next page. For example, an agent can find the latest outbound call to a doctor with:

{
  "direction": "outbound",
  "target_name": "Arzt",
  "limit": 1
}

To list inbound caller IDs from a defined period:

{
  "direction": "inbound",
  "started_after": "2026-08-01T00:00:00+02:00",
  "started_before": "2026-09-01T00:00:00+02:00",
  "limit": 50
}

Pass a returned call_id to phone_call.conversation. It returns the full call record, timing and duration, structured summary and facts, and the ordered user/assistant text transcript. The equivalent REST endpoints are:

GET /v1/calls
GET /v1/calls/{call_id}/conversation

Text transcripts are enabled through storage.transcript: true in config/calltool.yaml; audio recording remains disabled. Set transcript storage to false if full conversation text must not be retained. Existing calls and events remain in PostgreSQL across container restarts through the Compose volume.

During an active call, the worker owns one dedicated ActiveCallContext in RAM. Local voice tools read and update this hot state without a PostgreSQL lookup. A serialized background writer persists facts, candidates, transcript turns, and events in order; worker shutdown flushes the queue. Safety-critical commitment and completion writes are acknowledged by PostgreSQL before the tool reports success. PostgreSQL remains the source of truth after the call and after process restarts.

Local development without the full Compose stack

If only the API is being developed locally, install Python 3.13.15 and uv:

uv sync --all-extras
uv run calltool api

This mode does not replace the LiveKit, LiveKit SIP, PostgreSQL, and Redis services needed for real calls. Detailed public deployment and Telnyx provisioning notes are in docs/self-hosting.md.

Container releases

Every successful push to feature/initial runs linting, type checks, unit and PostgreSQL integration tests. The same workflow then creates the next development tag and publishes a multi-architecture image for linux/amd64 and linux/arm64 to:

ghcr.io/lwlp24/voice-mcp

Because v0.1.0 already exists as the initial repository release, current development builds target the next patch release and are numbered automatically:

v0.1.1-dev.1
v0.1.1-dev.2
v0.1.1-dev.3

Do not create these development tags manually. The workflow serializes runs for the branch, derives the release base from the PEP 440 version in pyproject.toml, determines the highest matching -dev.N tag, increments N, tags the tested commit, and builds that exact version. Development images receive both the raw v0.1.1-dev.N and normalized 0.1.1-dev.N image tags, but never update latest.

When the patch is ready, push a stable tag such as v0.1.1. A stable semantic version also publishes the 0.1, 0, and latest aliases. Manually pushed v* tags still run the complete quality gate before their image is published.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Let AI agents place real phone calls from your verified number, with transcripts and recordings.

  • Give your AI agent a phone: place calls, navigate IVRs, wait on hold, get structured answers.

  • Voice and chat for AI agents — Discord, Teams, Meet, Slack, Zoom, Telegram, WhatsApp, NC Talk, SIP

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LWLP24/voice-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server