Skip to main content

LangChain on SwarmD

swarmd-langchain is a thin wrapper on top of LangChain and LangGraph that gives a LangChain agent access to the SwarmD platform: OAuth2 authentication, sub-agent discovery (as BaseTools), MCP tool discovery, polling for long-running sub-agents, an A2A server, signed-webhook refresh, and an admin surface for operators. If you are already running a LangChain agent and want to move it onto SwarmD, read this first to learn the helpers, then follow Migrating from LangChain. If you are starting from scratch this page is the whole story. The shape mirrors Google ADK on SwarmD on purpose — the helper trio (create_runtime, create_llm_agent, serve) is the same.

What it gives you

The SDK exposes three helpers that, in order, produce a complete agent: The full agent file is usually 20–30 lines: a @tool-decorated function, three calls, and an if __name__ == "__main__" guard.

Installation

That pulls in swarmd-sdk as a transitive dependency, plus langchain, langgraph, langchain-openai, langchain-mcp-adapters, and a2a-sdk.

Credentials

Every agent registered on SwarmD receives three secrets:
  • agentId — the agent’s UUID and OAuth2 client ID.
  • clientSecret — used for outbound calls (registry, A2A relay, MCP, LLM gateway).
  • webhookSecret — used by the platform to sign inbound webhooks to your agent’s /admin/webhook endpoint.
All three are shown once in the dashboard or in the POST /registry/v1/agents response. Store them with your deployment configuration. The webhook secret is technically optional — outbound calls still work without it — but without it your agent has to be manually refreshed (POST /admin/refresh) every time a subscription or grant changes upstream.

Environment

.env
The four SWARMD_AGENT_ID / SWARMD_CLIENT_SECRET / SWARMD_BASE_URL / SWARMD_TOKEN_URL values are the platform-attached set. Leave any of them unset and the runtime stays in “standalone mode” — the agent still runs locally as a plain LangChain agent against direct OpenAI, it just won’t see any subscribed sub-agents or MCP tools.

The whole agent, end to end

main.py
Run it:
You should see a banner with the agent name and bind address, then — once the runtime is configured — Found N subscribed agents and Loaded N MCP tool(s) from M server(s) in the logs.

The three helpers, in detail

create_runtime()

Builds a SwarmDRuntime object and, if the four SWARMD_* vars are set, calls runtime.configure(...). That instantiates:
  • A platform-API SwarmDClient for registry and audit calls.
  • An McpClient for MCP discovery.
  • Two TokenManagerProxy objects — one for the platform-API audience (swarmd:api), one for the MCP relay audience (mcp:call). Both do double-check-locked OAuth2 client-credentials grants with caching, jittered backoff retries on 5xx, and a single 401 retry.
You never instantiate or thread these directly — every other helper grabs them off the runtime when it needs to mint a bearer.

create_llm_agent(runtime, name, description, instruction, tools)

Returns a LangGraph CompiledStateGraph you can ainvoke like any other LangGraph. It does five things on top of langchain.agents.create_agent(...):
  1. Picks the model. Reads OPENAI_MODEL (default gpt-4o) and OPENAI_API_KEY and instantiates ChatOpenAI with temperature=0. If you need a different temperature or a different provider, drop down to fetch_remote_agents + fetch_mcp_tools and assemble the graph yourself — they are all exported.
  2. Discovers sub-agents. Calls fetch_remote_agents(runtime), which pulls subscriptions from GET /registry/v1/agents/{id}/subscriptions and wraps each one in a PollingA2aTool — a LangChain BaseTool. The tool itself owns A2A message/send and polls tasks/get against the relay until the downstream task reaches a terminal state. So a long-running sub-agent doesn’t block your LLM and your tool never returns a stale intermediate payload.
  3. Discovers MCP servers. Calls fetch_mcp_tools(runtime), which lists MCP grants via the SDK’s McpClient, then connects to each granted server via langchain-mcp-adapters (MultiServerMCPClient), pointed at the relay’s MCP proxy (/relay/v1/mcp-servers/{id}/mcp). Tools are namespaced with a sanitised, UUID-seeded prefix so a free-form server name like "GitLab - swarmd.ai" can’t produce a tool name OpenAI rejects. An httpx.Auth impl mints a fresh mcp:call-scoped token on every request and propagates the current correlation ID.
  4. Compiles the LangGraph. Local tools + remote sub-agent tools + MCP tools are concatenated and handed to langchain.agents.create_agent with your instruction as the system prompt and your name as the graph name.
  5. Stashes the recipe on the graph. Attaches swarmd_name, swarmd_description, swarmd_instruction, and swarmd_local_tools to the returned CompiledStateGraph so serve() can rebuild the graph in place when subscriptions change, without you having to pass everything through again.
If you want fine-grained control — a custom model, structured outputs, a memory layer — drop down to runtime, fetch_remote_agents, and fetch_mcp_tools directly and call langchain.agents.create_agent yourself.

serve(agent, runtime, *, skills=None, on_refresh=None)

The thickest of the three helpers because the A2A server is the agent’s public face. In order, it:
  1. Wraps the LangGraph in a LangChainA2aExecutor. This is the adapter that translates incoming A2A protocol calls (message/send, message/stream, tasks/get, tasks/cancel) into LangGraph ainvokes and streams events back as A2A task updates.
  2. Builds the agent card. Either from an explicit skills=[AgentSkill(...), ...] list, or — if you didn’t pass one — a single catch-all skill is synthesised from the agent’s description. The card is exposed at /.well-known/agent-card.json.
  3. Persists tasks. A SQLite-backed DatabaseTaskStore is created at /tmp/{agent}_tasks.db. Enough for local dev; for production point it at a real database by overriding the create call inside your own copy of serve if you need durability across pod restarts.
  4. Adds correlation propagation. A CorrelationIdMiddleware extracts X-Correlation-Id off inbound A2A calls into a ContextVar, and the SDK’s outbound interceptors copy that var onto every downstream bearer-tokened request.
  5. Mounts /admin. Four endpoints — POST /admin/configure, POST /admin/refresh, POST /admin/webhook, GET /admin/status. /admin/webhook is the platform-driven kick: when a subscription, MCP grant, agent lifecycle, or tenant lifecycle event fires, the platform POSTs a signed payload, the SDK verifies the HMAC against SWARMD_WEBHOOK_SECRET, and calls the refresh path.
  6. Refreshes in place. The default refresh callback rebuilds the LangGraph from the recipe stashed by create_llm_agent — re-discovering sub-agents and MCP tools — and swaps it into the executor atomically. Local tools survive the swap. No restart needed when subscriptions change. Pass on_refresh=... to override.
  7. Starts uvicorn on HOST:PORT (defaults 0.0.0.0:8080) with LOG_LEVEL from env.

Verifying it works

If the agent card response is missing sub-agents or MCP tools you expect to see, check that the subscription is live in the dashboard and that Found N subscribed agents / Loaded N MCP tool(s) appear in the agent’s startup logs.

Advanced — using the SDK pieces directly

If you need to deviate from what create_llm_agent and serve do, drop down to the underlying pieces. The whole surface area is exported:
Common reasons to drop down:
  • Custom polling interval / max wait. Build PollingA2aTool instances yourself.
  • Hand-built LangGraph. Use the discovery functions to get the tool lists, then call langchain.agents.create_agent (or build a raw LangGraph) with whatever model, memory, or structured-output config you need.
  • Custom A2A server. Use LangChainA2aExecutor(graph) directly and bolt it onto your own Starlette / FastAPI app — call create_admin_app(runtime, on_refresh=...) for the /admin surface.

Next steps