MCP server: let your coding agent test your voice agent
CallQA runs a Model Context Protocol server. Connect your coding agent (Claude Code, Cursor, Codex, VS Code, or any MCP client) and ask it, in plain words:
Test my voice agent on staging with CallQA, tell me what breaks and fix my prompt.
The agent lists your scenarios, starts a run on the right environment, waits for the verdicts, reads the failed calls (transcript, the judge's quotes) and fixes your code. When a test itself is wrong, it can ask the CallQA assistant for a corrected scenario and show you the diff before saving it.
It can also set CallQA up from zero, without the dashboard:
Set up CallQA for the voice agent in this repository: staging and production, tests every weekday morning, and the CI.
Connect
| URL | https://app.callqa.dev/mcp |
| Transport | Streamable HTTP (protocol 2026-07-28, and the earlier handshake versions) |
| Authentication | An API key of your organization: Authorization: Bearer cqa_... |
The quickest way: in the app, open Developer > MCP server.
An admin creates a key with the recommended scopes (read, write, run) and the page
shows the install command of each client with the key in it, and the prompt to paste in
your agent. The same block, with a placeholder for the key, is at the top of the
Quickstart.
Any API key works (see Authentication). The examples below read it from
the CALLQA_API_KEY environment variable, so no key lands in a file: set it first with
export CALLQA_API_KEY="cqa_..." (your own key).
Claude Code
claude mcp add --transport http callqa https://app.callqa.dev/mcp \
--header "Authorization: Bearer $CALLQA_API_KEY"
Add --scope user to use it in every project. Check with claude mcp list.
Cursor
In ~/.cursor/mcp.json (every project) or .cursor/mcp.json (one project):
{
"mcpServers": {
"callqa": {
"url": "https://app.callqa.dev/mcp",
"headers": { "Authorization": "Bearer ${env:CALLQA_API_KEY}" }
}
}
}
VS Code
In .vscode/mcp.json (VS Code asks for the key once and stores it securely):
{
"inputs": [
{ "type": "promptString", "id": "callqa-api-key", "description": "CallQA API key", "password": true }
],
"servers": {
"callqa": {
"type": "http",
"url": "https://app.callqa.dev/mcp",
"headers": { "Authorization": "Bearer ${input:callqa-api-key}" }
}
}
}
Codex
In ~/.codex/config.toml (Codex reads the key from CALLQA_API_KEY):
[mcp_servers.callqa]
url = "https://app.callqa.dev/mcp"
bearer_token_env_var = "CALLQA_API_KEY"
Other clients
Any client that speaks Streamable HTTP and lets you set a header works. Clients that only
start local (stdio) servers can go through the mcp-remote bridge:
npx mcp-remote https://app.callqa.dev/mcp --header "Authorization: Bearer $CALLQA_API_KEY"
From a script, with the Python SDK (pip install "mcp>=2.2"):
import asyncio, os
import httpx2
from mcp import Client
from mcp.client.streamable_http import streamable_http_client
async def main():
headers = {"Authorization": f"Bearer {os.environ['CALLQA_API_KEY']}"}
async with httpx2.AsyncClient(headers=headers, timeout=330) as http:
async with Client(streamable_http_client("https://app.callqa.dev/mcp", http_client=http)) as c:
print([t.name for t in (await c.list_tools()).tools])
print((await c.call_tool("list_scenarios", {})).structured_content)
asyncio.run(main())
To try the tools by hand: npx @modelcontextprotocol/inspector, transport "Streamable
HTTP", the URL above and the Authorization header.
Tools
Every tool has typed inputs and outputs (inputSchema, outputSchema) and a description
written for the model. Each needs the scope shown, like the matching REST endpoint.
| Tool | What it does | Scope |
|---|---|---|
list_scenarios |
Scenarios of the organization (filters: q, tag, include_archived) |
read |
get_scenario |
One scenario: full YAML (variants included), a readable summary and its compliance verdict |
read |
validate_scenario_yaml |
Checks a YAML against the grammar and the server rules, saves nothing | read |
create_scenario |
Creates a scenario from YAML; returns its compliance review (compliance: verdict, reasons to fix) |
write |
update_scenario |
Replaces a scenario with a complete YAML (variants matched by title); the new content is reviewed again | write |
list_environments |
Environments (development, staging, production...), default one, number mapping | read |
list_versions |
Versions of your code per environment (label, git_sha, git_ref) |
read |
create_version |
Records a deployed version (label, environment, git_sha, git_ref) |
write |
run_scenario |
Starts a run (real calls): environment, optional version, optional wait_s |
run |
get_run_report |
Verdict, score, one line per test case, failed checks with the judge's quotes | read |
get_call |
Readable transcript, verdicts, metrics, links to the call page and the audio | read |
export_for_llm |
The "Copy for an LLM" report of a scenario, a run or a call (Markdown or JSON) | read |
fix_test |
Runs the assistant on a failed call: proposed YAML and diff, not applied | write |
apply_fix |
Saves a proposal of fix_test |
write |
get_usage |
Quota, wallet credit, forecast of the month | read |
Setup tools:
| Tool | What it does | Scope |
|---|---|---|
get_setup_status |
Plan, credit, the key's scopes, and each setup step: done, next tool, what a person must do | read |
create_environment |
Creates an environment (name, order, is_default) |
write |
list_numbers |
Numbers under test, with their verification status | read |
add_phone_number |
Registers a number (or returns it when it exists), with the attestation text to show | write |
get_number_status |
Verification state, digits heard on a failed attempt, sandbox limits, next step | read |
start_number_verification |
A code and the phrase the line must say; signs the attestation when asked (see below) | run (+ write to sign) |
place_verification_call |
One real call, free, that listens for the code | run |
set_schedule |
Activates a scenario in an environment: schedule (cron or plain words), time zone, call window, pause; returns the month's forecast | write |
list_schedules |
Scenario x environment: schedule, next and last run, number dialed | read |
list_webhooks |
Webhook endpoints and every event type | read |
configure_webhook |
Creates an endpoint, or updates the one with the same URL; the secret is shown once | admin |
create_alert_channel |
Slack or email alerts (failure, score, latency) | admin |
get_ci_setup |
Ready-to-commit GitHub Actions or GitLab CI files for your scenarios, and the secret to set | read |
Support tools (see Getting help):
| Tool | What it does | Scope |
|---|---|---|
contact_support |
Opens a ticket with the CallQA team (bug, question, feedback, billing, abuse report); a person answers, usually within a few hours | read |
list_support_tickets |
The organization's tickets, status and unread answers | read |
get_support_ticket |
One ticket with every message, to read the team's answer | read |
reply_support_ticket |
Adds a message to a ticket (reopens it) | read |
run_scenario places real phone calls: each one consumes your monthly quota, then wallet
credit, under your wallet, spend cap and plan limits, exactly like a run started from the
API. wait_s (up to 300 seconds) keeps the request open until the run finishes and streams
progress notifications; without it the tool returns at once and the agent polls
get_run_report. set_schedule and the CI files lead to more of these calls, on their own:
their answers say how many. place_verification_call places one real call that costs
nothing. Nothing can be deleted or paid through the MCP server.
Resources and prompts
| Resource | Content |
|---|---|
callqa://grammar/scenario.schema.json |
JSON Schema of the scenario grammar |
callqa://grammar/guide |
The grammar on one page, with good practice |
callqa://examples/level-1-keypad-ivr |
Example: keypad menu (DTMF IVR), short form |
callqa://examples/level-2-voice-ivr |
Example: voice menu, typed assertions, test data, a variant |
callqa://examples/level-3-ai-agent |
Example: AI agent, latency budget, interruptions, edge cases |
Prompts (shown as slash commands by most clients): test-my-voice-agent (arguments
environment, scenario), investigate-failing-run (run_id), write-scenario
(description, phone) and set-up-callqa (description, phone, environments,
schedule).
Onboarding with an agent
The prompt set-up-callqa walks the agent through the setup in this order. Every step is a
tool call; the agent stops where a person has to act.
- Status.
get_setup_status: plan, credit, the key's scopes, what is done. - Environments.
create_environmentfor each deployment of the voice agent (development, staging, production). Every organization starts with one, the default. - Number and verification.
add_phone_number, then: - A person signs the attestation: the agent shows the text (
get_number_status) and signs withstart_number_verification(sign_attestation=true, signer_name=...)only in the name and with the agreement of the person who controls the line. It is a legal statement; the agent never signs on its own. - The line says the code: the agent adds the returned phrase to the voice agent's prompt or greeting when that prompt lives in your repository, and asks you to deploy. Otherwise you add it in your voice platform.
place_verification_call, thenget_number_statusuntilverified. Until then only manual calls work (sandbox: 3 a day, no schedule, no CI). See Number verification.- Scenarios from a plain description of the agent, or from its prompt and flows in your
repository:
validate_scenario_yaml,create_scenario. - A first run:
run_scenarioon one scenario,get_run_report. Real calls: the agent says how many before starting. - Schedules:
set_scheduleper scenario and environment, for exampleschedule="every weekday at 08:30",timezone="Europe/Paris",call_window="08:00-20:00". The answer has the month's forecast. - Webhooks and alerts:
configure_webhook,create_alert_channel. They need a key with theadminscope; with the recommended key, an admin sets them up in the app instead. - CI:
get_ci_setupreturns the files to commit (.github/workflows/callqa.ymland the CallQA action, or.gitlab-ci.callqa.yml), the CLI to download and the secret to set. A person stores a key with thereadandrunscopes as theCALLQA_API_KEYsecret (gh secret set CALLQA_API_KEY): no key is ever written in a file.
What only a person does: sign the attestation of a number, make the line say the
verification code when its prompt is not in the repository, choose a plan or top up the
wallet (never through an API key), create API keys, and, with a key without the admin
scope, webhooks and alerts.
Security
- Same rules as the REST API. The key's scopes, its IP allow list, its expiry and the
rate limit (120 requests per minute per key) apply to every MCP request. Objects of other
organizations answer
not_found. - Journal. Each request is a line of the API journal (Developer >
API journal), for example
POST /mcp tools/call run_scenario. Tool arguments are not logged. Changes are in the audit log under the key. - Least privilege. Give the agent
read,writeandrun, neveradmin. Areadonly key is enough for an agent that only reads reports. - Browsers. Requests carrying a browser
Originfrom another site are refused.
Errors
A tool that fails answers an error result whose text is JSON, with the stable codes of the REST API (see Errors) and a hint for the model:
{"error": {"code": "missing_scope", "status": 403,
"detail": "This API key lacks the 'run' scope (run.trigger).",
"hint": "Ask an admin of the organization for a key with this scope (...)."}}
Codes you will meet: missing_scope, feature_not_in_plan (with required_plan and
upgrade_url), not_found, invalid_scenario (with errors, one per field),
run_not_possible, blocked_insufficient_credit (no call of the run could be paid, with
run_id and block_reasons), blocked_policy (no call of the run could be placed for a
policy reason, same fields), call_not_judged, call_passed (fix_test on a call that
passed), not_pending, scenario_changed,
rate_limited; for the setup tools, already_exists, invalid_target,
attestation_required, no_active_code, invalid_schedule, unknown_event, unsafe_url,
no_scenario. Before the protocol starts, the HTTP layer answers 401 unauthorized,
403 ip_not_allowed or 429 rate_limited (with Retry-After).