# MCP server: let your coding agent test your voice agent

CallQA runs a [Model Context Protocol](https://modelcontextprotocol.io) 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.

See [Onboarding with an agent](#onboarding-with-an-agent).

## 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](https://app.callqa.dev/developer/mcp/).
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](quickstart.md#set-up-with-your-coding-agent-recommended).

Any API key works (see [Authentication](authentication.md)). 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

```bash
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):

```json
{
  "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):

```json
{
  "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`):

```toml
[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:

```bash
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"`):

```python
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.md) (`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](support.md)):

| 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.

1. **Status.** `get_setup_status`: plan, credit, the key's scopes, what is done.
2. **Environments.** `create_environment` for each deployment of the voice agent
   (development, staging, production). Every organization starts with one, the default.
3. **Number and verification.** `add_phone_number`, then:
   - **A person signs the attestation**: the agent shows the text (`get_number_status`) and
     signs with `start_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`, then `get_number_status` until `verified`. Until then only
     manual calls work (sandbox: 3 a day, no schedule, no CI). See
     [Number verification](number-verification.md).
4. **Scenarios** from a plain description of the agent, or from its prompt and flows in your
   repository: `validate_scenario_yaml`, `create_scenario`.
5. **A first run**: `run_scenario` on one scenario, `get_run_report`. Real calls: the agent
   says how many before starting.
6. **Schedules**: `set_schedule` per scenario and environment, for example
   `schedule="every weekday at 08:30"`, `timezone="Europe/Paris"`,
   `call_window="08:00-20:00"`. The answer has the month's forecast.
7. **Webhooks and alerts**: `configure_webhook`, `create_alert_channel`. They need a key with
   the `admin` scope; with the recommended key, an admin sets them up in the app instead.
8. **CI**: `get_ci_setup` returns the files to commit (`.github/workflows/callqa.yml` and the
   CallQA action, or `.gitlab-ci.callqa.yml`), the CLI to download and the secret to set.
   **A person** stores a key with the `read` and `run` scopes as the `CALLQA_API_KEY`
   secret (`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](authentication.md) (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`, `write` and `run`, never `admin`. A `read`
  only key is enough for an agent that only reads reports.
- **Browsers.** Requests carrying a browser `Origin` from 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](errors.md)) and a hint for the model:

```json
{"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`).
