Letta Adapter
The LettaAdapter connects a Letta agent to Band. Letta keeps agent state on the Letta server: each agent owns memory blocks, so context persists between turns and across process restarts instead of living in your process. The adapter maps Band chat rooms onto Letta agents, records the Letta agent id on the room so a restarted process resumes the same agent, and wires Band’s platform tools into the Letta turn so the agent can send messages, manage participants, and create rooms.
The two SDKs reach Band’s platform tools differently. Python registers a Band MCP server with Letta and the Letta server calls the tools itself. TypeScript passes the tool schemas inline as Letta client_tools and executes tool calls locally through Letta’s approval flow. Configuration is not interchangeable between them. See Configuration Options.
Prerequisites
Complete the Setup tutorial first:
- Agent created on the platform
- Credentials configured (
agent_config.yaml) .envwith your platform URLs- Verified your setup works
You also need a Letta server: Letta Cloud with an API key, or a self-hosted Letta server.
On the Python path you also need a Band MCP server on a publicly resolvable host. The Letta server fetches the tools itself and rejects private addresses, so a laptop-local MCP server does not work. See Letta Server Connection.
Install the SDK with Letta support:
Python
TypeScript
The letta extra pulls letta-client and mcp.
Create Your Agent
Python
TypeScript
model must be a full Letta model handle with the provider prefix, for example openai/gpt-4o or anthropic/claude-haiku-4-5. A bare model name is rejected by Letta.
BAND_MCP_URL must point at a Band MCP server on a publicly resolvable host, for example https://your-band-mcp.example.com/sse. The Letta server calls that URL itself and refuses private addresses, including the mcp defaults. See Letta Server Connection.
Run the Agent
Python
TypeScript
You should see:
Test Your Agent
Add Agent to a Chat Room
Go to Band and open or create a chat room. Add your agent as a participant under the Remote section.
See the Response
The agent replies in the room. The reply arrives through the platform send tool, or, in Python, through auto-relay if the model answered without calling the tool.
Verify Memory Persisted
Ask a follow-up in the same room:
The answer comes from the Letta agent’s stored memory rather than from replayed chat history. In Python, this survives a process restart too: the adapter records the Letta agent id as a task event on the room and resumes the same agent on the next session. In TypeScript, per-room agents are deleted on room cleanup, so set lettaAgentId when you need one agent’s memory to outlive rooms and restarts.
How It Works
Both SDKs subscribe to the rooms your agent participates in, filter for messages that mention it, then run one Letta turn per message. What differs is the tool path and the room-to-agent mapping.
Python
TypeScript
- Startup -
on_startedrenders the system prompt, creates anAsyncLettaclient, and wires the MCP tool path. A registration Letta refuses raisesRuntimeError, so startup fails. - Tool path -
LettaMCPBridgestarts an in-process Band MCP server, registers it with Letta, and attaches the resulting tool ids to the agent. Tool calls execute in your process, resolved against the calling room’s tools. A registration Letta accepts but discovers no tools on is not treated as a failure: the bridge logs a warning plusDiscovered 0 MCP tools: [], reports itself ready with an empty tool id list, and the agent runs with no platform tools. Check that log line before assuming the tool path is live, see Letta Server Connection. - Agent resolution - In
per_roommode each room gets its own Letta agent. The adapter resumesletta_agent_idfrom the room’s task-event metadata; if the agent is gone, it creates a new one and seeds it with the room’s history lines as prior context. - Turn composition - Letta takes one user message per call, so the seed, the rejoin note (“You have rejoined this room after 4h”, plus the previous topic), participants, and contacts updates ride inline as
[System]:lines ahead of the triggering message. - Turn execution - The turn runs under
turn_timeout_s. The adapter observestool_call_messageandtool_return_messageevents for execution reporting; it does not execute the platform tools itself. - Response - If the agent called the MCP send tool, the message is already on the platform. If it did not,
auto_relayrelays the assistant text and logs a warning, because an unused tool path would otherwise hide behind a successful reply. - Cleanup - Letta agents are kept by default so resume-by-id works.
consolidate_memory_on_cleanupsends a final consolidation prompt so the agent writes key context to memory;delete_agents_on_cleanupdeletes the agent instead.
Self-healing tool attachment: if Letta reports a tool id as gone from the organization (a 404), the adapter re-registers the MCP path, re-attaches the fresh ids, and marks other rooms so they re-verify attachment on their next turn.
Configuration Options
The two SDKs expose different option sets. Python takes a LettaAdapterConfig dataclass; TypeScript takes a flat LettaAdapterOptions object.
Python
TypeScript
LettaAdapter(config=None, history_converter=None, **features)
LettaAdapterConfig fields:
LettaAdapterConfig rejects unknown field names, so a typo fails construction rather than vanishing. The removed enable_task_events, enable_memory_tools, and enable_execution_reporting booleans are unknown names now: pass emit and capabilities to the adapter instead.
Most fields also read a LETTA_-prefixed environment variable, for example LETTA_BASE_URL, LETTA_MODEL, and LETTA_EMBEDDING. provider_key additionally accepts LETTA_API_KEY, matching Letta Cloud’s own naming. An explicit constructor argument always wins over the environment.
LettaMCPConfig fields:
Capabilities and event emission are keyword arguments on the adapter, not fields on the config:
Supported capabilities are MEMORY and CONTACTS, both opt-in. Supported emissions are TOOL_CALLS, TASK_EVENTS, and USAGE, and omitting emit resolves to all three, so the example above is the default spelled out. emit=() silences the adapter; naming any other Emit member raises BandConfigError at construction. Token usage is only available on the per_room path, since the shared-mode Conversations stream carries no aggregate usage.
Emit.TASK_EVENTS is load-bearing here, not narration: letta_agent_id is recorded in task-event metadata and read back to resume the server-side agent. Narrowing emit so it excludes Emit.TASK_EVENTS means every restart creates a fresh Letta agent instead of reattaching.
api_key, mcp_server_url, and mcp_server_name are deprecated on LettaAdapterConfig and emit DeprecationWarning. Use provider_key and mcp=LettaMCPConfig(...). Passing both api_key and provider_key raises BandConfigError.
Letta Server Connection
Python
TypeScript
Three fields configure the connection. Each also reads a LETTA_-prefixed environment variable, so wire your own settings in explicitly when you do not want that fallback.
Agent identity is not configured for per_room mode. The adapter records letta_agent_id in a task event and the history converter reads it back on the next session, so restarts resume the same Letta agent. Passing agent_id only takes effect in shared mode.
To run a self-hosted Letta server, start it detached, with a persistent volume and at least one model provider key:
Letta syncs each provider’s model list at startup, so a server started without a provider key exposes only the letta/letta-free handle and model="anthropic/claude-haiku-4-5" does not resolve. Add the key, restart the container, then check what the server exposes with curl -fsS http://localhost:8283/v1/models/. Without the volume, agents and MCP registrations are lost when the container is removed.
Reaching Band’s tools
MCP is the Python SDK’s only platform-tool transport: the Letta server calls back into a Band MCP server over HTTP. Letta validates that URL against its own SSRF guard, letta/helpers/url_validation.py, which rejects localhost, any literal private IP, and any hostname that resolves to a non-global IP. The Band MCP server therefore has to sit on a publicly resolvable host, whichever Letta you point at.
Two configurations work today:
The defaults reach nothing. LettaMCPConfig() self-hosts on 127.0.0.1, and Letta rejects that registration with 422 Non-public IP not allowed: 127.0.0.1, so on_started raises and the agent never starts. The server_url default, http://localhost:8002/sse, is refused the same way as Blocked internal hostname: localhost.
A Letta server running in Docker on your laptop cannot use the Python tool path. advertised_host="host.docker.internal" clears registration, because the request schema calls the validator with resolve_hostname=False, but the tool sync that follows does resolve the hostname:
The adapter surfaces this as a warning plus Discovered 0 MCP tools: [] and keeps running, so the symptom is an agent with no send, participant or room tools that answers through auto_relay text only. Both transport="sse" and transport="streamable_http" fail identically, and no environment variable, flag or allowlist disables the guard. No value of bind_host or advertised_host helps, because every address that reaches your laptop is private. To develop against a local Letta server, expose the Band MCP server through a public tunnel and register the tunnel URL with mode="external".
If Letta answers INVALID_ARGUMENT: The model handle should be in the format provider/model-name, your model is missing its provider prefix. List the handles your server exposes with curl -fsS http://localhost:8283/v1/models/.
Complete Example
Python
TypeScript
Letta Cloud, an external Band MCP server on a public host, memory and contacts capabilities on, execution and usage events emitted, and a custom memory block: