Letting an AI agent place real phone calls: what our MCP server limits, and what it doesn't
When the model gets something wrong, an MCP tool that only reads a database does limited harm. A tool that dials a phone reaches a real person, and you can't take the call back. We recently built an MCP server for a dia
When the model gets something wrong, an MCP tool that only reads a database does limited harm. A tool that dials a phone reaches a real person, and you can't take the call back.
We recently built an MCP server for a dialer that customers run on their own machines. This article covers the design choices we made, why we made them, and the gaps that remain. The reasoning is our judgment, and we'd like to hear where you disagree.
Tools that are off don't exist
Our server has 20 tools in four groups, and each group except the status tools has its own environment variable. If a group is turned off, its tools aren't registered at all. Out of the box, 14 tools are registered. The six campaign tools appear only when you turn campaigns on:
| Setting | Tools |
|---|---|
| Always on |
call_status, campaign_status, import_status
|
On unless VOICENT_ALLOW_CALLS=0
|
call_text, call_audio, call_ivr, send_sms, call_remove
|
On unless VOICENT_ALLOW_CRM=0
|
crm_get_field, crm_add_or_update, crm_set_field, crm_add_note, crm_schedule_callback, crm_notify_agent
|
Off unless VOICENT_ALLOW_CAMPAIGNS=1
|
campaign_import_list, campaign_upload_audio, campaign_export, campaign_run_autodialer, campaign_run_text, campaign_run_email
|
We prefer this to registering a tool that returns "not allowed". A model can't try a tool it was never offered, and it won't spend turns arguing with an error. The idea isn't new, and other MCP servers do the same.
On by default, so set limits before connecting
Single calls, texts and the CRM tools are on as soon as you connect the server, and the agent can call or text any number. That makes the server useful right away, but it means you set the limits before connecting, not after. To restrict single calls and texts to a few numbers, for example while testing:
json
{
"mcpServers": {
"voicent": {
"command": "C:\\path\\to\\voicent-mcp.exe",
"env": {
"VOICENT_GATEWAY_URL": "http://localhost:8155",
"VOICENT_ALLOWED_NUMBERS": "650-555-0100,650-555-0101"
}
}
}
}
`VOICENT_ALLOW_CALLS=0` and `VOICENT_ALLOW_CRM=0` turn those groups off. Each time the server starts, its log repeats these warnings about what the agent can do.
### The number list can't cover campaigns
This is the part we'd most like you to notice. A campaign contacts everyone on its list, and the allowed-numbers setting doesn't apply to it. Checking a list of thousands against a short allow list doesn't fit how anyone uses a dialer. So campaigns got their own switch, and the docs carry a plain warning instead.
`VOICENT_ALLOW_CAMPAIGNS=1` is a much larger grant than single calls, and it's the one group that stays off until you set it explicitly. Only set it where an agent starting a campaign on its own is acceptable.
The same switch also covers list imports, and they aren't limited the way the CRM tools are. With campaigns on, a list import can update any column of matching contacts, including clearing do-not-call, from files in `VOICENT_FILE_ROOT`. Only put files there that you'd import yourself.
### CRM records leave your server, and some changes are one-way
The CRM tools read and change customer records. A read isn't just a local lookup: the record goes to your AI provider. Writes are limited. Only known fields can be read or changed, and adding a customer only fills empty values unless the agent asks to overwrite. The agent can mark a customer do-not-call or do-not-email, but through the CRM tools, only a person can clear do-not-call or do-not-email. A list import is a separate path, covered in the campaigns section above.
There's no CRM search, so the agent needs a record ID, a phone number or an email. Lookups by phone or email can match the wrong record when there are duplicates.
### Requests are built from a fixed set of fields
The gateway API is older than MCP and was written for trusted callers. Some of its fields do far more than place a call. These include fields that run programs on the gateway, fields that run database queries, and a field that bypasses do-not-call handling.
The server doesn't forward whatever arguments the model supplies. It builds each request from a fixed set of fields, and it never sends the fields above. If the model invents an extra argument, the argument doesn't reach the gateway.
### Validate values, not just types
A phone number looks like a harmless string. But our gateway treats certain values in the phone number field as special commands rather than numbers. So the server requires a single number in a strict format, which blocks those values.
If you're wrapping an older API, check what its string fields accept besides the obvious. That's where we found the surprises.
### Refuse rather than guess
`call_ivr` refuses app names that aren't deployed, because the gateway handles unknown names badly. An error the agent can read is better than a call that goes wrong.
### When the docs and the source disagree, follow the source
We built request fields and response formats from the gateway source, not only from its developer docs. The two didn't always match. For email campaigns, the docs describe `msgtype=file` and `msgfilepath`, while the code reads inline content as `msginfo` and `msgbody`. A server built only from the docs would have sent fields the gateway never reads.
We also test against a fake gateway, so nothing is dialed. That's necessary, but it hides problems. Audio calls passed against the fake gateway and failed on real installs, because the gateway copied audio into a folder those installs didn't create. That was one of the gateway fixes behind the 10.10.3 requirement.
### What doesn't work, and what none of this protects
- **Email campaigns are only tested in simulation.**
- **There's no CRM search,** and phone or email lookups can match the wrong record when there are duplicates.
- **`call_ivr` can't check that an app is outbound,** so that's left to the operator.
- **An IVR app with no answering machine message** can occasionally end as failed when it reaches voicemail. The fix is to add the message to the app in IVR Studio.
- **There's no confirmation step before a campaign starts, and no audit log in the server.** A server-enforced confirmation would be a reasonable design, but we don't have one.
- **List imports can undo a do-not-call.** Through the CRM tools, only a person can clear do-not-call or do-not-email. But with campaigns on, a list import can update any column of matching contacts, including clearing do-not-call, from files in `VOICENT_FILE_ROOT`. Only put files there that you'd import yourself.
- **These limits only cover what the MCP server sends.** They do nothing for a gateway that other people can reach directly, so set its Remote Connect Password and keep its port off the internet.
- **Data still goes through your AI client.** Whatever the agent reads or sends, including numbers, message text, results and CRM records, passes through the client you connect, and possibly its cloud model.
- **Your agent reads untrusted text.** General MCP advice applies here too: be careful about combining campaign tools with tools that read content from outside.
- **None of this is about consent.** Who you may call or text is a legal question that a tool filter can't answer. The server doesn't make any calling legally compliant, and that stays with the operator.
### If you're building something similar
1. Don't register what isn't enabled.
2. Make the operator turn on the largest grant, such as campaigns, explicitly, and say plainly what's on by default.
3. Say plainly where a limit stops applying, including side doors such as imports.
4. Read the source of the API you wrap, including its special values.
5. Test at least once against a real system. A fake one only confirms what you already assumed.
---
*Disclosure: We make this product. The server is [open source under MIT](https://github.com/voicentmcp/voicent-mcp), and production use needs a licensed Voicent Gateway. This article was created with the help of AI.*
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.