
Letta is a trademark of its respective owner, used here for identification and integration guidance only. No endorsement, partnership, or affiliation is implied.
- What Letta ships — a persistent, memory-native agent runtime (Letta Cloud or self-hosted),
the agent loop and context management, MCP server registration over
streamable-http,sse, andstdio(with auth headers on remote servers), per-tool approval rules, agent-scoped variables, and a Python/TypeScript SDK. - 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
- Letta registers an MCP server once on the Letta server, discovers its tools, and lets you attach those tools to any agent by id. Tool execution is forwarded server-side to the MCP server — the agent never holds the credentials.
- 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.
- Register a Naive session as an MCP server, attach its tools to a Letta agent, and every tool call runs as that one user — gated server-side. Because Letta agents are persistent and per-user, one Letta agent maps cleanly to one Naive user.
Tested against:
letta-client 1.12.1 (Python — Letta, client.mcp_servers.create,
client.mcp_servers.tools.list, client.agents.create, client.agents.messages.create,
RequiresApprovalToolRule), against Letta Cloud or a current self-hosted Letta server, and
Naive API v2 (hosted MCP server over SSE + per-user sessions), on Python ≥ 3.10. A matching
TypeScript SDK (@letta-ai/letta-client) mirrors this surface (client.mcpServers.create,
client.agents.create).Naive’s session URL contains /mcp/sse/…, so register the MCP server with
mcp_server_type: "sse". Letta marks SSE as a legacy transport in favor of streamable-http,
but it’s still fully supported — use it to match Naive’s endpoint. The scoped bearer rides in the
auth_header / auth_token fields (the Authorization header), never in the URL. 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. A Naive session is short-lived (default 15 min, max 24h) while a Letta MCP
registration is persistent — see rotation below. Pin your
versions and set the model to a handle you have access to.Prerequisites
- A Naive API key (
nv_sk_...) — get one from the dashboard. - A Letta server: a Letta Cloud API key (
LETTA_API_KEY) or a self-hosted server (docker run ... letta/letta, reachable athttp://localhost:8283). - Model + embedding access configured on that server (OpenAI, Anthropic, …).
- Python ≥ 3.10.
Minimal viable integration
The shortest path to a Letta agent that can actually transact: define a policy and provision a user (Naive control plane, once), then mint a per-user MCP session, register it on Letta, and attach its tools to a stateful agent.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 session for the user. It returns the scoped SSE
endpoint and a bearer that lives in the headers — never in the URL. A Letta MCP registration is
persistent, so mint with a longer
ttl_ms (up to 24h) and plan to rotate it:Register Naive, attach its tools, and build the agent
Register the session’s scoped SSE endpoint as an MCP server on Letta, list the tools it
exposes, and attach them to a new stateful agent. The The agent discovers GitHub (
Authorization header is split into
Letta’s auth_header + auth_token fields: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 loop and the memory are Letta’s; the real-world actions are
Naive’s — and because the agent is persistent, the next message already remembers the card it
set up.Extension: human-in-the-loop spend
Because the kit setcards.requiresApproval: true, the agent cannot silently spend. Letta
lets you add a client-side checkpoint too, and Naive enforces the server-side one — so you
get defense in depth: Letta can pause before the tool runs, but even if a call gets through, the
spend gate still lives on Naive’s servers.
- In Letta — add a
requires_approvaltool rule so the agent pauses before a sensitive tool runs and emits an approval request instead of calling it. - On the server — even if a call gets through, Naive freezes the sensitive action and
returns a pending approval (HTTP
202) instead of a live card. This holds no matter what runtime calls it.
naive_cards_create, the run stops with an approval request. Your app
shows it to a human, then resumes the loop by approving (or denying) the specific tool call:
pending → executed / failed / denied) and the deny endpoint.
Letta’s
requires_approval tool rule is a convenience for your own UX — the real enforcement is
Naive’s server-side approval gate above, which can’t be bypassed from the agent config, the tool
list, or the tool rule.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.
Alternative: one agent per tenant
Letta agents are already per-user and persistent — so serving every tenant is natural: give each end-user their own agent, each backed by their own Naive session. Register the Naive server once with an agent-scoped variable in the token, then set each agent’s variable to that user’s freshly minted session bearer:client.mcp_servers.update(...). Revoke a session early with
DELETE /v1/users/{user_id}/sessions/{id}.
Prefer TypeScript? Letta also ships
@letta-ai/letta-client (client.mcpServers.create,
client.agents.create), and Naive plugs into TS agent stacks. On TypeScript you can provision
the control plane with the @usenaive-sdk/server SDK (naive.accountKits.create,
naive.users.create, client.session(...)) and hand the scoped MCP session to any MCP-aware
runtime — see the Vercel AI SDK and Mastra
guides.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
- Pydantic AI · LlamaIndex · Agno · smolagents · — the same MCP-session pairing for other Python stacks