Skip to content

MCP server

The Voqalize MCP server exposes Voqalize’s management surface as tools your editor’s agent (Claude Code, etc.) can call: create an agent, mint its keys, point its brain_url at your route, and read back what a call did — without leaving the editor.

The server hands your agent its own instructions on connect, and those link here. There is no skill to install and nothing to keep in sync: every page on this site is also served as raw markdown at the same URL plus .md, indexed at /llms.txt.

It is a hosted, remote MCP endpoint — you don’t install or run anything. Point your MCP client at the URL, authenticate once with Google in the browser, and the tools are available.

Add the server to your MCP client. In Claude Code:

Terminal window
claude mcp add --transport http voqalize https://app.voqalize.com/mcp

Or, project-scoped, drop an .mcp.json at your repo root:

{
"mcpServers": {
"voqalize": {
"type": "http",
"url": "https://app.voqalize.com/mcp"
}
}
}

On first use your client runs a browser Google sign-in and the tools light up. There is no API key, client ID, or secret to configure — the client registers itself dynamically and carries the resulting token.

  • Google OAuth (the same login as the console). Your MCP client authenticates with Google via Dynamic Client Registration — it self-registers, you complete the browser sign-in, and identity comes from your Google account. First sign-in provisions your Voqalize user automatically.
  • The token is the credential; tenant is a selector. Every scoped tool takes a required tenant slug and is checked against your membership before it runs — passing a slug you don’t belong to fails. You can’t act on a tenant just by naming it.
  • whoami first, then list_tenants. whoami returns your identity; list_tenants returns every workspace you can act on. Call them at the start of a session to learn which tenant slug to pass to everything else.

Sixteen tools. Every tool returns the control plane’s raw JSON. Errors surface with one of two codes — not_authorized (you’re not a member of that tenant, or your role is too low) or validation_error (bad input, e.g. a non-wss:// brain_url on a non-loopback host).

ToolSignatureDoes
whoami() -> dictIdentify the authenticated developer. Call first.
list_tenants() -> dictEvery tenant (workspace) you can act on.
create_tenant(about="", display_name="") -> dictCreate your workspace + seed demo agents. Idempotent — returns your existing tenant if you have one.
ToolSignatureDoes
create_agent(tenant, name, description="", brain_url="") -> dictCreate an agent. Returns {agent, session_key (sk_…, once)}.
create_agent_credentials(tenant, agent_id, label="") -> dictMint Cortex outbound credentials for a brain that can’t accept inbound (localhost, serverless, egress-only). Returns {agent_secret (sk_…, once), cortex_url, brain_url, key_id, usage}.
get_agent(tenant, agent_id) -> dictOne agent: id, name, description, status, brain_url, Playground test_url, timestamps. It does not return STT/TTS config.
list_agents(tenant, status="", limit=20) -> dictList agents; optional draft|active|archived filter.
update_agent(tenant, agent_id, name="", description="", brain_url="") -> dictRename, re-describe, and/or point the brain at a WS URL.
archive_agent(tenant, agent_id) -> dictSoft delete (stops serving new sessions).

There is no separate set_brain_url tool — pass brain_url to create_agent up front, or set it later with update_agent. It must be wss:// (ws:// only for localhost/127.0.0.1); an empty brain_url falls back to the hosted welcome demo brain so a bare agent still greets.

create_agent_credentials returns two different URLs and they are not interchangeable: cortex_url goes to the SDK’s cortex_url= argument (it already carries the /agent path — pass it verbatim), while brain_url is what the agent’s own brain_url must become so the runtime dials Cortex instead of your server. Setting it is not automatic — finish with update_agent(tenant, agent_id, brain_url=…). The sk_ secret is shown once, never expires, and minting revokes nothing, so rotation is: mint → redeploy → revoke the old key. This is what makes local development tunnel-free; see Cortex relay.

ToolSignatureDoes
create_api_key(tenant, agent_id, label, kind="secret", allowed_origins=None) -> dictMint another key for one agent. kind="publishable" (pk_, browser — pass origins) or "secret" (sk_, backend). Raw key shown once.
list_api_keys(tenant, include_revoked=False) -> dictList keys (prefixes only), each with the agent it names.
revoke_api_key(tenant, key_id) -> dictRevoke by id (irreversible).
ToolSignatureDoes
list_sessions(tenant, agent_id="", state="", limit=20, cursor="") -> dictList calls, most recent first; filter by agent/state. Page with next_cursor.
get_session(tenant, session_id) -> dictOne call in full: state, timing, agent_input, metadata, recordings summary.
get_session_events(tenant, session_id, source="all", frame="", disposition="", limit=2000) -> dictWhat happened, merged: lifecycle milestones and the wire between runtime and brain — transcripts, replies, actions, interruptions.
get_session_logs(tenant, session_id, level="INFO", service="", limit=500) -> dictThe voice runtime’s own log lines for that call.
get_recordings(tenant, session_id, ttl_seconds=900) -> dictAudio, one track per side, each with a short-lived signed download_url.
get_usage(tenant, period="") -> dictCounters for one YYYY-MM billing period, broken down per agent.

A call is a session, and that is the only noun. There is no Meeting above it: list_meetings / get_meeting / list_meeting_events / query_logs were removed on 2026-08-20 along with the entity, and the session id you already hold — the one in connect_params, in {brain_url}?session_id={session_id}, in every log line — is the id every one of these tools takes.

The inspect-a-call loop is events first, logs second:

  1. get_session_events — authoritative and versioned. Safe to assert on in tests. Pass source="platform" for the lifecycle milestones alone (cheap: it skips the wire read), or disposition="dropped_after_watermark" to see exactly what a barge-in threw away — the usual answer to “the agent replied but nothing happened”.
  2. get_session_logs — evidence, not contract. Written in our vocabulary and free to change; read them to understand a call, never to assert on one.

Both halves arrive as one bundle when the call ends, so a call still in progress has neither. Check the wire / logs_availability field before concluding a call was silent: found, missing (no bundle — still running, or the upload failed), unavailable (the store could not be read) or skipped. An empty list is not the same fact as any of those.

These are Voqalize’s records. Your brain runs in your own environment and logs there; session.id is the same string on both sides, so it joins them.

An agent with these tools connected takes a project from empty to a running voice agent in this order:

  1. Confirm the connectionwhoami, then list_tenants for the tenant slug every other tool requires.
  2. Write the brainon_session_start / on_user_message / on_rtvi / on_user_idle. See the SDK README (sdk/python/README.md).
  3. Create the agentcreate_agent(tenant, name){agent, session_key}.
  4. Run it and point brain_url at it — locally, create_agent_credentials and dial out over Cortex (no tunnel); in production, an inbound route. Either way finish with update_agent.
  5. Test it unattended — the conformance harness drives the brain in text mode, with no audio and no human. Then talk to it live at the agent’s test_url.
  6. Embed in the browsercreate_api_key(tenant, agent_id, label, kind="publishable", …)pk_…, then the handshake — no package to install.
  7. Instrument iton_finalize / on_error brain-side, list_sessions / get_session_events / get_session_logs on ours.