Blog: Product

Build a 24x7 voice NOC agent on Open Voice Shield

By the TelecomsXChange engineering team ยท

Open Voice Shield can now be operated entirely by software, through an MCP server, OAuth sign-in for agents, test calls through your own route and one-click install. This post covers what shipped and gives a quick start for an agent that tests every destination around the clock and wakes a person only when something moved.

What shipped

Five pieces, one interface

An MCP server with typed outputs
/api/mcp speaks the Model Context Protocol over Streamable HTTP: tools from get_started to get_transcript, each a thin wrapper on one REST endpoint. Every tool declares an outputSchema taken from that endpoint's OpenAPI response, so an agent gets per-leg estimated MOS, jitter and loss, post-dial and answer delay, codec, the route alert and the verdict as typed fields it can validate and compare over time, rather than prose it has to parse.
OAuth sign-in for agent clients
The endpoint starts open. The first tool that needs an account answers 401 with a WWW-Authenticate challenge. An OAuth-capable client registers dynamically and opens our consent page, where the person signs in or creates the account (passkey included), then continues with a rotating token. A client that holds an API key sends it as a bearer header instead. Credentials are never tool arguments.
Test calls the platform places
place_test_call dials a number through one of your destinations, plays a natural-voice announcement or a known scam script for the seconds you asked, and hangs up. The call is recorded, transcribed and analyzed like any other and appears with origin test, with no switch needed. Since 29 September every test call opens with a recorded announcement that it is an automated, recorded test call, and presents the platform's own number or one of the account's verified numbers. The scam script rings only verified numbers, and a call asked for through the MCP server rings only after a person confirms it on a portal page the tool links to.
Discovery and install
/llms.txt, an agent card at /.well-known/agent.json and /api/openapi.json let an agent orient itself without HTML. The agent guide has the one step for each client: a command for Claude Code, install links for Cursor and VS Code, one request field for agents on the Claude API, an agent definition for Claude Managed Agents, and an Add to Claude link for the chat app.
Everything else unchanged
The same authorization path, audit log and rate limit whichever door an agent uses. Every write it makes is in the account's audit log under the key that made it, and revoking the key stops it.

How it works

The loop, from signup to a verdict

An agent reads the discovery files, connects to the MCP server, has the person sign in or up through OAuth, adds the destination calls should go on to, sets routing, alerts and the webhook, asks for a test call the person confirms, and reads the result. We ran that journey on the production service with an off-the-shelf coding agent, from a fresh account to a call ringing on a mobile, and wrote it up in an earlier post. What the agent reads back after a test call looks like this, trimmed:

get_call, structuredContent
{
  "status": "completed", "origin": "test", "billsec": 30, "codec": "PCMU",
  "pdd_ms": 3180, "answer_delay_ms": 8258, "hangup_by": "callee",
  "analysis": {"probability": 5, "recommendation": "allow", "route_alert": null, "red_flags": []},
  "qos": {"mos": 4.4, "packet_loss_pct": 0.0, "jitter_ms": 0.17,
          "legs": {"caller": {"mos": 4.4, "lost": 0}, "callee": {"mos": 4.4, "lost": 0}}}
}

Those are the fields a NOC cares about: whether the route answered and how fast, what the audio was like on each leg, whether the route cheated, and whether the announcement was heard on the far end, which the transcript answers. Everything below is built on them.

Quick start

A voice NOC agent in four steps

Every destination is tested every hour. The agent judges each call against that route's own last seven days, waits for a second call to agree before alerting, and leaves the decision to a person.

1. An account, a destination, a key

Sign up, add the carrier or switch calls should go on to, and create an API key on the Developers page of the portal. The guide has the same steps with curl. Keep the key for steps 3 and 4.

curl -s -X POST https://ovs.telecomsxchange.com/api/customer/destinations \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"host":"203.0.113.20","port":5060,"transport":"udp","label":"carrier-a","is_default":true}'
# repeat per carrier; note each destination id

2. The agent definition

On a hosted agent platform, declare the server and the tools it may use without asking. Placing calls is switched off. Through the MCP server every test call waits for a person to confirm it, which suits a call someone asks for but not an hourly probe. The calls come from the script in step 4, and the agent reads and judges them.

{
  "name": "Voice NOC",
  "mcp_servers": [
    {"type": "url", "name": "open-voice-shield", "url": "https://ovs.telecomsxchange.com/api/mcp"}
  ],
  "tools": [
    {"type": "mcp_toolset", "mcp_server_name": "open-voice-shield",
     "default_config": {"permission_policy": "always_allow"},
     "configs": [
       {"name": "place_test_call", "enabled": false},
       {"name": "cancel_test_call", "enabled": false},
       {"name": "whitelist_ip", "enabled": false}
     ]}
  ]
}

3. The credential

Store the key as a vault credential for our URL. The platform sends it as a bearer token on every request. The agent never sees it.

{
  "display_name": "Open Voice Shield key",
  "auth": {"type": "static_bearer",
           "mcp_server_url": "https://ovs.telecomsxchange.com/api/mcp",
           "token": "ovs_..."}
}

4. The schedule, and the instruction

Every hour, run the placement script under Without an agent platform with the key: on the REST API a key's own test calls are placed directly. Ten minutes later, start a session with the vault attached and this instruction. The numbers are starting points. The agent replaces them with each route's own history after a week.

You are the voice NOC for this account. Every run:

1. list_destinations. A destination whose monitor_status is down has stopped
   answering SIP OPTIONS: check_destination once more, and if it is still down open
   the case straight away (step 7); the script skips it.
   For every other destination, find this hour's route test: list_calls with
   origin=test and from = one hour ago.
2. get_call until analysis_status is done. Fetch get_transcript.
3. Read: sip_code, answered, pdd_ms, answer_delay_ms, codec, billsec,
   qos.legs.callee (mos, loss_pct, jitter_avg_ms), analysis.route_alert,
   and whether the transcript contains "automated route test".
4. Baseline: list_calls with origin=test and from = seven days ago; compute each
   route's median pdd_ms, answer_delay_ms and callee mos from its own calls.
5. Something moved when: not answered, or sip_code != 200; pdd_ms > 8000 or > 2x
   the median; callee mos < 3.6 or 0.4 below the median while the caller leg is
   clean; loss_pct > 1; route_alert present; the announcement is missing from the
   transcript; codec changed; billsec far from 20 plus the opening announcement.
6. Before alerting, compare with that route's previous test call. Alert only if
   both agree; a single bad call is noted and judged again on the next run.
7. On a confirmed change: open a case through the REST API (POST /api/cases) with
   the route, the call id, the figures, what moved and since when, and the report
   link from report_links. The account's alert rules and Slack channel carry the
   notice; do not build a second alerting path. On an early_answer_fas route_alert,
   open the case and stop testing that route until a person clears it.
8. Otherwise record the figures and say nothing. At 08:00 local, post a one-line
   summary per route from list_calls: calls, median PDD, median callee MOS, alerts.
9. Check account_balance first; if it is under $2, stop and say so.

Step 7 keeps the agent quiet. The account's own alert rules and Slack channel are the delivery path, and the agent decides and attaches evidence rather than becoming a second alerting system.

Without an agent platform

The same loop runs as a cron job that places, polls, reads, compares and confirms. Below is the placement half, which is also the script step 4 runs. The comparison is a few lines of whatever you already use to keep numbers. Each call opens with the platform's recorded announcement, presents the platform's own number, and dials a number of yours that a route test may ring.

#!/usr/bin/env bash
# every hour: one 20 s route test per destination, results appended as JSON lines
# 15550100200 is our own IVR; route_test may ring any number
API=https://ovs.telecomsxchange.com/api
for DEST in $(curl -s $API/customer/destinations -H "Authorization: Bearer $KEY" | jq -r '.[] | select(.monitor_status != "down") | .id'); do
  TC=$(curl -s -X POST $API/customer/test-calls -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
       -d "{\"number\":\"15550100200\",\"destination_id\":\"$DEST\",\"seconds\":20,\"speech\":\"route_test\"}" | jq -r .id)
  until S=$(curl -s $API/customer/test-calls/$TC -H "Authorization: Bearer $KEY" | jq -r .status); [ "$S" = done ] || [ "$S" = failed ]; do sleep 5; done
  CALL=$(curl -s $API/customer/test-calls/$TC -H "Authorization: Bearer $KEY" | jq -r .call_id)
  [ "$CALL" = null ] && continue
  until [ "$(curl -s $API/calls/$CALL -H "Authorization: Bearer $KEY" | jq -r .analysis_status)" = "done" ]; do sleep 10; done
  curl -s $API/calls/$CALL -H "Authorization: Bearer $KEY" \
    | jq -c '{dest:"'$DEST'", pdd_ms, answer_delay_ms, codec, billsec, callee: .qos.legs.callee, route_alert: .analysis.route_alert}' >> noc.jsonl
done

What to expect

What it catches, what it costs, what it does not claim

Catches. A route that stops answering, answers slowly, answers on ringback, switches to a recording, degrades on the far leg, changes codec or bills more than it should, with the call, its recording, its transcript and a PDF report attached to the case. A person sees a route that changed, with the evidence, before the first customer complaint. Between test calls, the platform's own destination monitor sends each destination a SIP OPTIONS every minute, so a trunk that stops answering shows up as monitor_status: down on the next list_destinations without waiting for the hourly call.

Costs. Per analyzed second plus one analysis fee per test call. Twenty seconds per route per hour is well under a dollar a day per route at pay-as-you-go rates, before your carrier's own charge for the outbound leg. Fewer, longer calls beat many short ones when you also want the verdict.

Does not claim. It measures the test calls it placed, not every customer call. It sees the routes it dials at the cadence it dials them. The quality figures are estimates from loss and jitter. The thresholds above are starting points, and no false-alarm rate for the design is published. Section 9 of the white paper has the full reference design.

Try it in an afternoon

An account starts with $5 of credit, enough for a day of hourly tests on a handful of routes. Point one destination at your carrier, run the cron version once by hand, and read the figures. The agent version is the same loop with a schedule and a judgement. The agent guide has every install step.

Questions

Frequently asked questions

What does the agent interface consist of?

An MCP server at /api/mcp with tools that wrap the REST API, each with an output schema taken from the OpenAPI document; OAuth 2.1 with dynamic client registration and a consent page, or an API key as a bearer header; API test calls that dial a number through your own destination, which a person confirms when an agent asks for one through the MCP server; and install links for the common agent clients. Discovery is /llms.txt, /.well-known/agent.json and /api/openapi.json.

What does a voice NOC agent actually do every hour?

A scheduled script places a short route test on each destination with the API key. The agent then waits for the analysis and reads post-dial delay, answer delay, the SIP code, codec, per-leg estimated MOS, jitter and loss, any route alert, and whether the transcript contains the announcement. It compares each figure with that route's own last seven days, waits for a second call to agree on anything that moved, and only then alerts, opens a case or diverts the route.

Do I need an agent platform to run it?

No. The same loop runs as a cron job with curl against the REST API, or in Claude Code, Cursor, VS Code or any framework with an HTTP client. A hosted agent platform adds scheduling, a credential vault and a permission policy. The post shows both.

What does it cost to run?

Per analyzed second plus one analysis fee per test call. Twenty seconds per route per hour is well under a dollar a day per route at pay-as-you-go rates, before your carrier's own charge for the outbound leg.

How accurate is the monitoring?

It measures how a route behaved for the test calls it placed, judged against that route's own history, and it confirms before alerting. The thresholds are starting points, quality figures are estimates from loss and jitter, and no false-alarm rate is published yet.