- What Pydantic AI ships — a typed agent loop, structured
output_typevalidation, dependency injection (deps_type), tool/toolset composition, streaming, and first-class MCP client support. - What it doesn’t ship — a way for those agents 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.
How the pairing works
- Pydantic AI registers any MCP server as a toolset — pass
MCPServerSSE(...)to anAgent(or toagent.run(toolsets=[...])) and its tools are discovered automatically. - 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.
- So the integration is: mint a session for one user → hand its SSE URL + scoped bearer to a Pydantic AI agent as a toolset → every tool call runs as that user, gated server-side.
Tested against: Naive API v2 (hosted MCP server over SSE, per-user sessions),
pydantic-ai 1.x (pydantic_ai.Agent, pydantic_ai.mcp.MCPServerSSE, output_type,
per-run toolsets), and the mcp Python package 1.x, on Python 3.10–3.13.Naive’s MCP server uses SSE transport, so this guide uses MCPServerSSE and passes the
scoped bearer via its headers argument. In pydantic-ai 1.x, MCPServerSSE is being
superseded by the FastMCP-based MCPToolset (same role, still functional until v2) — the
swap is one line: MCPToolset(session["mcp"]["url"], headers=session["mcp"]["headers"]).
There is no Naive Python SDK today — provision the control plane over the REST API, the
dashboard, the CLI, or the Node SDK (@usenaive-sdk/server).
Pin your versions and
set the model to one you have access to.Prerequisites
- A Naive API key (
nv_sk_...) — get one from the dashboard. - A model provider key for the model that runs the agent (this guide uses OpenAI via
OPENAI_API_KEY). - Python 3.10–3.13.
Minimal viable integration
The shortest path to a Pydantic AI agent that can actually transact: define a policy, provision a user (control plane, once), then at runtime mint a per-user MCP session and hand it to the agent as a toolset.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:
Mint a per-user MCP session
At runtime, mint a short-lived session scoped to one user. It
returns an SSE URL plus a scoped bearer that lives only in the headers — never in the URL —
and expires (default 15 min, max 24h):
Give the agent the session — and let it transact
Wrap the session in The agent discovers GitHub (
MCPServerSSE and register it as the agent’s toolset. Pydantic AI
discovers Naive’s tools automatically and runs the typed agent loop for you. Because it’s
Pydantic AI, you can also pin a validated output_type so the run returns a typed object,
not a blob of text:naive_connections_connect), returns a connect link for Alice
to authorize, and attempts to issue the card (naive_cards_create) — a real card on
Alice’s account, capped by her kit. The whole agent loop is Pydantic AI’s; the real-world
actions are Naive’s.SetupReport back.
The session’s tool list is already filtered by Alice’s kit — the agent never sees a tool the
policy forbids. To narrow further per agent, register the server with a
tool_prefix or
compose it with a filtered toolset. To
swap the session per request instead of per agent, pass it at run time:
await agent.run(prompt, toolsets=[naive]).Extension: human-in-the-loop spend
Because the kit setcards.requiresApproval: true, the agent cannot silently spend. When
it calls the card-issuing tool, Naive freezes the request and the tool result comes back as a
pending approval (HTTP 202) instead of a live card:
202 response uses action; approval records from the approvals API use
action_type.
The agent relays the pending status (card_status: "pending_approval" in the typed report)
instead of claiming success. Your app then resolves it out of band — and on approval, Naive
replays the frozen action server-side:
pending → executed / failed / denied).
To detect the pending state inside the run, register the server with a
process_tool_call hook —
it wraps every MCP call, so you can inspect the result, tag a pending_approval, and shape
how the agent reports it. The server-side gate holds regardless of what the hook does.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 operator’s own default user, bypass the
gate — so end-user agents stay governed while your own automation isn’t slowed down.
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 (default 15 min) and you can
DELETE /v1/users/:user_id/sessions/:idto revoke it early.
Next steps
- MCP server — the hosted SSE server and its full tool list
- Sessions — per-user MCP sessions, TTL, and revocation
- Account Kits — author spend/capability policy
- Approvals — the human-in-the-loop lifecycle
- CrewAI · LangGraph — the same MCP-session pairing for other Python and TypeScript stacks