Skip to main content
LiveKit

LiveKit runs the voice pipeline · Naive lets it transact

LiveKit and LiveKit Agents are trademarks of their respective owners, used here for identification and integration guidance only. No endorsement, partnership, or affiliation is implied.
LiveKit Agents is the framework for building realtime voice AI: an AgentSession wires up speech-to-text, an LLM, text-to-speech, and turn detection into one low-latency loop, and a worker dispatches a fresh agent per caller. Its MCP support lets that agent call any MCP server’s tools — spoken request in, real action out.
  • What LiveKit ships — a realtime AgentSession pipeline (STT → LLM → TTS + turn detection), a worker/dispatch model that spins up one agent per participant, first-class tools and mcp.MCPServerHTTP for remote MCP servers, and the rooms/telephony transport underneath.
  • What it doesn’t ship — a way for that voice agent to act as a real account: sign up for a SaaS tool, hold a funded card, or spend within a budget you control, per end-user.
That’s the half Naive adds. You keep LiveKit’s realtime loop; Naive gives each caller’s agent a tenant identity, a virtual card, 1,000+ third-party connections, and an Account Kit that bounds exactly what the agent can do — enforced server-side, with human approval on the sensitive actions.

How the pairing works

  • Naive ships a hosted MCP server and mints per-user sessions — short-lived, revocable SSE endpoints whose tool list is the fused native + third-party toolset, already filtered by that user’s Account Kit. See the delegated MCP sessions explainer for the full lifecycle model.
  • In the LiveKit entrypoint (one job per caller), you mint a session for that participant and hand its URL + headers to mcp.MCPServerHTTP(..., transport_type="sse", headers=...) — the scoped bearer rides in the request headers, never in the URL. Pass it to the agent via mcp_servers=[...].
  • The AgentSession runs the loop: the caller speaks → STT → the LLM picks a Naive tool → the result is spoken back via TTS. Every call runs as that one user, gated on Naive’s servers. Sensitive actions resolve to a pending_approval payload the agent can read aloud.
Tested against: livekit-agents 1.6.5 (AgentServer, AgentSession, Agent, and livekit.agents.llm.mcp.MCPServerHTTP), the MCP Python SDK 1.28.1 (the transport under MCPServerHTTP), and Naive API v2 (hosted MCP server over SSE + per-user sessions), on Python ≥ 3.9. Live-verified: passing Naive’s session headers to MCPServerHTTP(transport_type="sse", headers=...) discovers Naive’s tools (and allowed_tools=[...] narrows them); a wrong bearer fails MCP session initialization.Version assumptions: MCPServerHTTP takes headers at construction and does not support rotating them after connect (livekit/agents#5542), so mint the Naive session inside the entrypoint (per job/caller) and give it a ttl_ms that covers the whole call — up to 24h. Naive’s session URL contains /mcp/sse/…, so set transport_type="sse" (LiveKit also supports "streamable_http" for …/mcp endpoints). There is no Python Naive SDK yet, so the control plane (Account Kit, user, session) is shown over the REST API; you can equally provision from the dashboard, the CLI, or the Node SDK. Pin your versions and set the STT/LLM/TTS models to providers you have access to.

Prerequisites

  • A Naive API key (nv_sk_...) — get one from the dashboard.
  • A LiveKit project (LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET) plus model access for the STT/LLM/TTS you pick (this guide uses LiveKit Inference; provider plugins like openai, deepgram, cartesia, silero work identically).
  • Python ≥ 3.9.

Minimal viable integration

The shortest path to a LiveKit voice agent that can actually transact: define a policy and provision a user (Naive control plane, once), then — inside the per-caller entrypoint — mint a session, point mcp.MCPServerHTTP at its scoped SSE endpoint, and hand it to the agent.
1

Define the policy, then provision a user

An Account Kit is the spend/capability policy. Here a tenant user gets a card (capped at $500, approval required), the vault, and an allowlist of apps. Everything the agent does is bounded by this kit — server-side. These are one-time control-plane calls:
2

Mint a per-user MCP session

At runtime, mint a session for the user. It returns the scoped SSE endpoint and a bearer that lives in the headers — never in the URL. Because MCPServerHTTP headers are fixed at construction, ask for a ttl_ms that comfortably covers the call:
3

Wire Naive into the voice agent

In the LiveKit entrypoint (one job per caller), resolve which user this call belongs to, mint their session, and build mcp.MCPServerHTTP from mcp.url + mcp.headers. Pass it to the Agent via mcp_servers=[...] — LiveKit discovers Naive’s tools and the LLM can call them mid-conversation:
On a call, Alice can say “connect my GitHub, then issue a $50 card called ‘Ads budget’” — the agent discovers GitHub (naive_connections_connect), returns a connect link for her to authorize, and attempts to issue the card (naive_cards_create) — a real card on Alice’s account, capped by her kit. The realtime pipeline is LiveKit’s; the real-world actions are Naive’s.
That’s the moat: the same voice loop that would otherwise just talk about spending money now issues a policy-bounded card on a specific caller’s account.
mcp_servers also exists on AgentSession(...) if you’d rather scope the toolset to the whole session than to a single Agent. Either way LiveKit initializes the MCP connection when the session starts and closes it when the session ends.

Extension: human-in-the-loop spend

Because the kit set cards.requiresApproval: true, the agent cannot silently spend. You get two complementary layers — pair them for defense in depth:
  • In the pipeline (scope + phrasing) — narrow the toolset with allowed_tools=[...] so the agent can only touch the primitives you intend, and use a tool_result_resolver to turn a pending_approval payload into a clean sentence the TTS reads back. Treat this as UX.
  • On the server (the real boundary) — Naive freezes the action and returns a pending approval (HTTP 202) instead of a live card. This holds no matter what runtime calls it, and can’t be bypassed from the prompt or the agent config.
LiveKit has no native pre-tool rejection hook — allowed_tools decides what the agent may call, and tool_result_resolver only shapes the result after Naive has already gated it. The real enforcement is Naive’s server-side approval gate below, which can’t be bypassed from the agent config, the tool list, or the resolver.
Independently, when a call reaches Naive, the tool result comes back as a pending approval rather than a live card — regardless of what the client did:
Your app resolves it out of band — the human approves in your dashboard or UI, and on approval Naive replays the frozen action server-side:
See Approvals for the full lifecycle (pending → executed / failed / denied) and the deny endpoint.
Approvals are only enforced for agent (API-key / MCP) calls on real tenant users. A human acting in your dashboard, and agent calls on the account’s own default agent profile, bypass the gate — so end-user agents stay governed while your own automation isn’t slowed down.

Alternative: one agent per tenant

A LiveKit worker already dispatches one job per caller, so multi-tenant is the natural shape: resolve the user from the job’s metadata, mint a fresh session for them, and build the MCP server bound to it — inside the entrypoint. allowed_tools narrows a caller’s toolset to exactly the primitives you want that agent to touch — a second layer on top of the Account Kit:
Nothing about the agent widens what a user may do: the toolset is the intersection of the session, that user’s Account Kit, and any allowed_tools filter — enforced on Naive’s servers. Because MCPServerHTTP headers are fixed at connect, mint the session per job with a ttl_ms that covers the call, and DELETE /v1/users/{user_id}/sessions/{id} to revoke it early when the call ends.
Long calls? A Naive session defaults to 15 minutes (max 24h) and its bearer can’t be rotated mid-connection. Set ttl_ms to cover the expected call length when you mint it in the entrypoint; for very long-running sessions, end the call and start a fresh job (which mints a new session) rather than trying to update the header in place.

What stays enforced

No matter how the agent is wired, the policy is enforced where it matters — on Naive’s servers, not in your prompt or your agent config:
  • Identity — every action runs as a specific tenant user, fully isolated from your other users.
  • Capability bounds — the Account Kit decides which primitives and which apps the agent can touch (allowlist / blocklist / per-tool).
  • Scoped spend — virtual cards are capped per card and per user; the model can’t raise its own limit.
  • Human-in-the-loop — sensitive actions (cards, domains, KYC, formation, connecting an app) freeze as approvals until a human says yes.
  • Scoped, expiring access — the agent holds a per-user session bearer, not your API key; it expires and you can DELETE /v1/users/:user_id/sessions/:id to revoke it early.

Next steps