LangChain on Swarmd
swarmd-langchain wraps LangChain and
LangGraph so a compiled graph can
live on Swarmd: authenticated, discovering its sub-agents and MCP tools, served
over A2A, and rebuilding itself when grants change.
The helper trio deliberately mirrors
Google ADK — same names, same order, same
arguments. If you know one you know the other.
Assumes you’ve finished Setup. Ten minutes on
Core concepts will make the rest of this page obvious
rather than magical — but it isn’t required.
Step 1 — Install
swarmd-sdk, langchain, langgraph, langchain-openai,
langchain-mcp-adapters, mcp, and a2a-sdk. Use a virtualenv — the version
bounds are deliberately tight.
Step 2 — Add your LLM to .env
On top of the credentials from Setup:
.env
temperature=0. To change that, or to use a different
provider, assemble the graph yourself.
Step 3 — Write the agent
main.py
Local tools must be LangChain
BaseTools. The @tool decorator is the
easy path — it turns a plain function into one, using the docstring as the
description the model sees. A bare function passed in tools= will fail when
the graph compiles.Step 4 — Run it
logging module at INFO, so you need
logging configured to see it:
Found N subscribed agents and
Loaded N MCP tool(s) from M server(s).
Seeing 'SwarmD not configured - standalone mode'?
Seeing 'SwarmD not configured - standalone mode'?
create_runtime() only configures when all four of SWARMD_AGENT_ID,
SWARMD_CLIENT_SECRET, SWARMD_BASE_URL and SWARMD_TOKEN_URL are set. Miss
one and the agent runs as a plain LangChain agent against direct OpenAI,
discovering nothing.Deliberate escape hatch for offline development; also the single most common
cause of “why is my catalogue empty?”. Check all four.Step 5 — Talk to it
Dissected: create_runtime()
SwarmDRuntime and, when the four platform env vars are present,
calls runtime.configure(...). That builds a SwarmDClient, an McpClient,
and the two token manager proxies —
swarmd:api and mcp:call.
Dissected: create_llm_agent()
invoke / ainvoke like any other.
1
Picks the model
ChatOpenAI(model=OPENAI_MODEL or "gpt-4o", api_key=OPENAI_API_KEY, temperature=0). No key, no agent — ValueError.2
Discovers sub-agents
fetch_remote_agents(runtime) pulls
GET /registry/v1/agents/{id}/subscriptions and wraps each subscription in a
PollingA2aTool — a LangChain BaseTool your model calls with a
natural-language request.The tool owns the whole A2A exchange: it resolves the agent card, sends
message/send, and if the task comes back non-terminal it polls tasks/get
through the relay (every 5s, up to 600s) until it’s terminal, then returns the
final text. Your model never sees a {"state": "working"} payload it can’t
act on.Tool names are sanitised to ^[a-zA-Z0-9_-]+$ — anything else is rejected by
OpenAI with 400.3
Discovers MCP servers
fetch_mcp_tools(runtime) lists your grants, then hands
langchain-mcp-adapters’ MultiServerMCPClient one connection per server,
pointed at the relay’s proxy (/relay/v1/mcp-servers/{id}/mcp).Auth is an httpx.Auth implementation rather than static headers, so every
request gets a freshly-minted mcp:call token and the current correlation ID —
without the LangChain client needing to know anything about Swarmd.Connection keys are mcp_tool_namespace(server), which is also
langchain-mcp-adapters’ tool-name prefix, so a server called
"GitLab - swarmd.ai" can’t produce an illegal tool name.The sync/async bridge
The sync/async bridge
create_llm_agent() is synchronous; the MCP client is async-only. The helper
bridges with asyncio.run — or, if it’s already inside a running loop, by
running the coroutine on a side thread with a copied context, so the
correlation ID propagates across the thread boundary. ContextVars aren’t
inherited by threads otherwise.4
Compiles the graph
local_tools + remote_tools + mcp_tools go to
langchain.agents.create_agent() with your instruction as the system prompt
and your name as the graph name.Discovery failures are logged and swallowed — an agent that can’t reach the
registry at boot still compiles with its local tools.5
Stashes the build recipe on the graph
swarmd_name, swarmd_description, swarmd_instruction and
swarmd_local_tools are attached as attributes on the returned graph.That’s how serve() can rebuild it later without you threading the arguments
through again — and how your local tools survive a rebuild. If you build the
graph yourself and want auto-refresh, set these attributes too.Dissected: serve()
1
Wraps the graph in a LangChainA2aExecutor
The adapter that turns an inbound
message/send into a LangGraph
ainvoke, publishing the task as submitted → working → completed with
the final text as a single message. tasks/cancel routes to the executor’s
cancel; tasks/get is served by the A2A request handler out of the task
store, without touching the graph.2
Builds the agent card
From your
skills=[AgentSkill(...)] list, or — if you didn’t pass one — a
single catch-all skill synthesised from the agent’s description.3
Persists tasks
SQLite at
/tmp/{name}_tasks.db.4
Adds correlation propagation
CorrelationIdMiddleware captures inbound X-Correlation-Id; every outbound
client forwards it. One user request, one trace id, across every hop.5
Mounts /admin
POST /admin/configure, POST /admin/refresh, POST /admin/webhook,
GET /admin/status — see the
webhook-secret gap above for what currently works.6
Rebuilds on refresh
The default refresh callback recompiles the graph from the recipe stashed by
create_llm_agent() — re-discovering sub-agents and MCP tools — and swaps it
into the executor. Your local tools survive.If the rebuild throws, the previous graph is kept and the failure is logged.
A registry blip can’t take your agent down.Pass on_refresh= to replace this entirely.7
Starts uvicorn
On
HOST:PORT (default 0.0.0.0:8080) at LOG_LEVEL (default info).Going off the rails
Everything is exported:Building the runtime yourself
Building the runtime yourself
create_llm_agent() and serve() as normal. Useful when
the values come from somewhere other than environment variables — a secret
manager, or a config object you already have.A different model, or the Swarmd LLM gateway
A different model, or the Swarmd LLM gateway
Skip
create_llm_agent() and compile the graph yourself:Custom polling interval or wait cap
Custom polling interval or wait cap
Your own server, keeping the admin surface
Your own server, keeping the admin surface
executor.set_agent(new_graph) swaps the graph in place — that’s what the
default refresh callback uses.Next
Migrating an existing LangChain agent
Diff-shaped guide: what to delete from your current
main.py.Reference
Every helper, argument, and environment variable.
Google ADK
The same three helpers, for ADK.
Troubleshooting
Empty catalogues, 403s on MCP, webhooks that never fire.
