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:
HelperReturnsWhat it does
create_runtime()SwarmDRuntimeReads SWARMD_* env vars, configures a runtime, and instantiates token managers (platform-API + MCP-relay).
create_llm_agent(runtime, ...)LangGraph CompiledStateGraphPicks the model, discovers subscribed sub-agents (as PollingA2aTool instances), discovers subscribed MCP servers, compiles a LangGraph with the combined toolset.
serve(agent, runtime)runs foreverWraps the graph in an A2A executor, builds the agent card, mounts the /admin surface, correlation middleware, and starts uvicorn. Recompiles the graph in place on signed webhooks.
The full agent file is usually 20–30 lines: a @tool-decorated function, three calls, and an if __name__ == "__main__" guard.

Installation

pip install swarmd-langchain
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
# LLM provider
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-4o        # optional, default gpt-4o

# SwarmD platform credentials
SWARMD_AGENT_ID=00000000-0000-0000-0000-000000000000
SWARMD_CLIENT_SECRET=...
SWARMD_WEBHOOK_SECRET=...
SWARMD_BASE_URL=https://api.swarmd.ai
SWARMD_TOKEN_URL=https://auth.swarmd.ai/realms/swarmd/protocol/openid-connect/token

# Server (optional)
HOST=0.0.0.0
PORT=8080
LOG_LEVEL=INFO
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
from dotenv import load_dotenv
load_dotenv()

from langchain_core.tools import tool

from swarmd_langchain import create_llm_agent, create_runtime, serve


@tool
def get_time(city: str) -> str:
    """Get the current time for any city."""
    return f"The time in {city} is 18:00"


runtime = create_runtime()
agent = create_llm_agent(
    runtime,
    name="time_agent",
    description="A simple agent that provides time information",
    instruction=(
        "You are a helpful time agent. Use the get_time tool to tell users "
        "the time in any city they ask about. You can also collaborate with "
        "other agents and call MCP tools for additional capabilities."
    ),
    tools=[get_time],
)


if __name__ == "__main__":
    serve(agent, runtime)
Run it:
python3 main.py
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

# Agent card with discovered tool catalogue
curl http://localhost:8080/.well-known/agent-card.json

# A2A message via JSON-RPC
curl -X POST http://localhost:8080/ \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0", "id": 1, "method": "message/send",
    "params": {
      "message": {
        "role": "user",
        "parts": [{"kind": "text", "text": "What time is it in Tokyo?"}],
        "messageId": "11111111-1111-1111-1111-111111111111"
      }
    }
  }'

# Admin status (configured? agent_id?)
curl http://localhost:8080/admin/status
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:
from swarmd_langchain import (
    create_runtime,
    fetch_remote_agents,        # returns List[PollingA2aTool]
    fetch_mcp_tools,             # returns List[BaseTool]
    PollingA2aTool,              # LangChain BaseTool + A2A + polling
    LangChainA2aExecutor,        # AgentExecutor adapter for A2A protocol
    create_a2a_client_factory,
)
from swarmd_sdk import create_admin_app, SwarmDRuntime
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