Portal MCP Server¶
Connect an MCP client — Claude Code, Claude Desktop, or anything else that speaks the protocol — to your QVOICE Platform account and let it read and change the account's configuration in plain language.
Ask it "which personas do we have and on what extensions?" and it answers from your account. Ask it to change an AI agent's greeting and it changes it, through the same path the portal uses.
Note
This is a different surface from the MCP Server an AI agent uses while it is on a call. That one offers the tools a call needs — transfer, hang up, look up a contact. This one offers the tools an administrator needs, and neither is offered the other's.
Connect¶
https://{{ domain }}:9443/api/v2/mcp/portal
Transport is HTTP Streamable. Authenticate with an API key:
| Header | Description |
|---|---|
X-API-Key |
An API key created in your account |
A bearer token works too (Authorization: Bearer <jwt>), which is what a browser-based client uses. Either way the account is resolved on the server — from the key's own record, or from the token — so a client cannot ask about an account it was not issued for.
Working on a sub-account¶
A partner whose key belongs to a parent account can act on one of its children by naming it:
| Header | Description |
|---|---|
X-Account-ID |
The account to work on, if the key's account is above it |
Naming an account the key has no claim to is refused. This is the same check that guards every other endpoint, so what a key can reach here is exactly what it can reach through the API.
Claude Code¶
claude mcp add --transport http --scope user qvoice-portal \
https://{{ domain }}:9443/api/v2/mcp/portal \
-H "X-API-Key: YOUR_API_KEY"
--scope user makes it available in every project; leave it out to add it only where you run the command.
Any other client¶
Point it at the URL above with the X-API-Key header. Most clients take a JSON block:
{
"mcpServers": {
"qvoice-portal": {
"type": "http",
"url": "https://{{ domain }}:9443/api/v2/mcp/portal",
"headers": { "X-API-Key": "YOUR_API_KEY" }
}
}
}
See API Key Authentication to create the key.
Warning
The key is the scope: it decides which accounts the client may see and change — its own, and any beneath it. Give each person their own; sharing one shares everything it reaches. An administrator's key is required, because these tools do what an administrator does in the portal.
Tools¶
Reading the account¶
| Tool | Answers |
|---|---|
get_account_info |
Name, timezone, features, basic configuration |
list_users / get_user |
Users, their extensions and status |
list_queues / get_queue |
Queues and how they are configured |
list_devices |
Phones, optionally for one user |
list_callflows |
Call routing |
list_agents / get_agent_states |
Contact centre agents and what they are doing now |
AI personas¶
| Tool | Does |
|---|---|
list_personas |
Every persona: name, job, extension, conversation shape |
get_persona |
One in full — prompt, greeting, voice, extension, gate flow |
create_persona |
Creates one, with an extension and the routing that makes it answer |
update_persona |
Changes prompt, greeting, voice, extension or conversation shape |
Queues¶
| Tool | Does |
|---|---|
create_queue |
Creates one, ready to take calls on an extension |
update_queue |
Changes the roster, the strategy, the ring and wrap-up times |
Campaigns¶
| Tool | Does |
|---|---|
list_campaigns |
Every campaign: queue, type, status, how many leads |
get_campaign |
One in full — queue, retries, cool-off, caller id |
create_campaign |
Creates one against a queue, stopped |
add_campaign_lead |
Adds one lead to dial |
list_campaign_leads |
Where each lead is: waiting, being dialled, cooling off, finished |
set_campaign_status |
Starts, pauses, stops, ends or archives it |
Creating a persona¶
Four things are required: a name, the job it does, a description, and the prompt that tells it how to behave.
Create a persona called Front Desk that answers our main line, greets
callers in English, finds out who is calling and why, and offers to take
a message when it cannot help.
The persona comes back with an extension already assigned and its inbound routing in place — it can take a call the moment it exists. Name an extension yourself and it is used, unless another callflow already answers it, in which case the request is refused rather than taking the number away from whatever has it.
Note
The greeting is spoken as written. Write the sentence the caller should hear — "Thanks for calling Acme, how can I help?" — not an instruction like "greet the caller warmly", which is what the caller would hear read out.
Changing a persona¶
Only the fields you name are changed; everything else is left alone. Changing a voice does not mean restating the prompt.
Change the Front Desk greeting to "Thanks for calling Acme, how can I help?"
A change made here is the same change the portal makes: the persona's inbound routing is rebuilt to match, and the same rules apply — an extension another callflow already answers is refused, with a message naming what holds it.
Building a queue¶
A queue created here is a queue the portal would create: give it a name, an extension for callers to reach it on, and the people who answer.
Create a queue called sales on extension 9500 with Maria and Carlos as agents.
It can take a call as soon as it exists — the routing that sends its extension to its agents is set up with it.
Agents are named by user id; list_users and list_agents find them. Adding someone later keeps whoever is already in the queue:
Add Sebastian to the sales queue.
There is a separate way to replace the roster outright, which drops anyone not named. Adding and replacing are kept apart on purpose, so "put this person in the queue" cannot quietly remove everyone else.
Setting up a campaign¶
A campaign is an outbound run: it belongs to a queue, takes its agents from that queue, and works through a list of leads. The queue is the part that catches people out. A campaign without one never dials and gives no sign why, so create_campaign refuses to create it rather than leaving you with a campaign that looks fine and does nothing.
The order is: queue, campaign, leads, start.
Create a progressive campaign called Q3 winback on the sales queue,
add 555-0101 as a lead, and start it.
A campaign is created stopped on purpose, so leads can be added before anything rings. set_campaign_status with start is what actually puts them on the phone, and stop takes them off.
Note
set_campaign_status takes an action, not a status. Not every action makes sense from where the campaign is: a running one can be paused or stopped, a stopped one started, paused or ended. Asking for something it cannot do is refused, and the refusal tells you what state it is actually in.
Watch a run with list_campaign_leads. The status says where each lead is: new and selectable are waiting, on_call is being dialled now, cooloff is waiting to be retried, success and failed are finished.
Conversation shape¶
A persona runs a call one of two ways:
| Shape | The call is |
|---|---|
legacy |
One prompt, for the whole conversation |
gate_runtime |
A graph of stages, each with its own prompt and its own tools |
The second is what stops a stage being talked into something it has no business doing: a stage that has no reason to transfer a call is not given the tool to transfer one. Draw the graph in the portal, or pass it as JSON to update_persona. A flow that will not parse, or that names no entry stage, is refused rather than saved for a call to fail on.
Reading a persona returns the flow summarised — its entry stage and the stages in it — because a flow runs to thousands of characters and its shape is usually what you want. Ask for the document itself with include_gate_flow.
What it will not do¶
- Take an account it was not given. The account comes from the key or the token, and an
X-Account-IDis honoured only when the account tree allows it — never on the client's word alone. - Touch a live call. Transferring and hanging up belong to the agent's surface; this one is not offered them.
- Work without an administrator. These tools do what an administrator does in the portal, and the same permission is required.