u/ictinnovations

pbx-mcp: an MCP server for Asterisk and FreeSWITCH, read-only by default
▲ 6 r/mcp

pbx-mcp: an MCP server for Asterisk and FreeSWITCH, read-only by default

I look after open source telephony for a living, and the thing that eats the most time isn't fixing the PBX. It's answering "is the trunk down" and "who is on a call right now" for people who will never touch a CLI.

So I wrote an MCP server for it. It talks to Asterisk over the Manager Interface and to FreeSWITCH over the Event Socket, and hands the live state back as tools.

There wasn't one already. There is a project with asterisk in the name on npm, but it belongs to a code security company that happens to share the name and has nothing to do with the PBX. For FreeSWITCH there was nothing at all.

What it can read:

- active channels with caller ID, state, bridge and how long they have been up

- PJSIP endpoints and device state, falling back to chan_sip peers on older boxes

- Sofia profiles and gateways, so you can see whether a trunk is actually registered upstream

- dialplan context dumps

- raw CLI and API commands, filtered

The part that took the longest was the safety model, because pointing a model at a live switch is a bad idea if you do it lazily.

Write tools like originate and hangup aren't blocked at runtime, they are never registered with the model at all unless you set PBX_MCP_ALLOW_WRITE=true. A model can't call a tool it can't see. On top of that there are allow lists for which CLI and API commands can run, shell metacharacters get rejected rather than escaped, and AMI header injection is guarded, since AMI is newline delimited and a caller ID field with a CRLF in it is otherwise a free command. Output is clamped at 20k characters too, because show channels on a busy box will happily eat your whole context window.

Two ways to run it. Either npx:

npx -y pbx-mcp

or the container, which is handy if you would rather not put Node on the PBX host:

docker run -i --rm -e ASTERISK_AMI_HOST=10.0.0.5 -e ASTERISK_AMI_USERNAME=mcp -e ASTERISK_AMI_PASSWORD=secret ghcr.io/ictinnovations/pbx-mcp

It speaks stdio, so there is no port to expose.

MIT, TypeScript, and the only runtime deps are the MCP SDK and Zod. The AMI and ESL clients are hand rolled and also published separately if you want the protocol layer without the MCP part.

GitHub: https://github.com/ictinnovations/pbx-mcp

npm: https://www.npmjs.com/package/pbx-mcp

Docker Hub: https://hub.docker.com/r/ictinnovations/pbx-mcp

GHCR: ghcr.io/ictinnovations/pbx-mcp

MCP Registry: io.github.ictinnovations/pbx-mcp

For anyone who has shipped a read-only server: has the read-only default held up for you, or do people just flip the write flag on day one and defeat the point?

u/ictinnovations — 10 days ago
▲ 14 r/Asterisk+1 crossposts

Asterisk AI voice agent: why writing a whole TTS sentence to AudioSocket means the caller only hears the end of it

Spent longer than I want to admit on this one, so posting it in case it saves someone else an afternoon.

We were building an AI voice agent against app_audiosocket. Your text to speech hands you a finished sentence, say 2.4 seconds of slin16 at 8 kHz, which is 120 frames of 320 bytes. The obvious thing is to write all of it to the socket and move on.

Don't. app_audiosocket forwards every frame to the channel the moment it arrives. It doesn't buffer for you. So all 120 frames hit the far end in a few milliseconds, the jitter buffer keeps a handful, and the rest get thrown away. The caller hears the tail of the sentence and nothing before it. Your logs look clean the whole time, which is what makes it annoying to chase.

The fix is to write one 320 byte frame, then wait until the next 20 ms deadline. The part that caught us a second time: re-clamp the deadline on every frame. If synthesis stalls for half a second and you work out the next deadline from where you are right now, the writer bursts to catch up and you're dropping audio again.

Same idea with barge-in. When the caller starts talking you have to drop the frames already queued, not just stop writing, or they keep hearing the old sentence for however deep your buffer runs.

We open sourced what we ended up with. asterisk-ai-voice-agent is the Python sidecar (Whisper or ElevenLabs Scribe in, Claude for the turn, Piper locally or ElevenLabs out, barge-in, tool calls posted to a webhook you own). asterisk-audiosocket is just the protocol layer for Node if you'd rather build your own and skip rediscovering the pacing thing. Both MIT, both v0.1.0, so lab material rather than something to point a live queue at.

https://github.com/ictinnovations/asterisk-ai-voice-agent

https://github.com/ictinnovations/asterisk-audiosocket

Happy to answer anything on the AudioSocket side. Curious whether anyone here went the ARI external media route instead and how the pacing compares.

u/ictinnovations — 7 days ago