Skip to main content

The core SDK

swarmd-sdk is the foundation both framework wrappers are built on. It has no opinion about how you write your agent — it only handles the parts that touch Swarmd. Use it directly when:
  • You’re using a framework we don’t wrap yet (CrewAI, Autogen, raw OpenAI, your own loop).
  • You want a plain API client — a script, a CI job, a backend service that lists agents or calls an MCP tool.
  • You’re using ADK or LangChain but need to deviate from what the wrapper does.
Using Google ADK or LangChain? The wrapper is strictly less work — start at Google ADK or LangChain and come back here when you need to drop a level.
This page builds up from a one-line client to a fully-served agent. Each part stands on its own; take what you need.
Assumes you’ve finished Setup — a registered agent and a .env with your three secrets.

Part 1 — Authenticate and discover

The smallest useful program: who am I subscribed to?
discover.py

Dissecting it

Reads SWARMD_AGENT_ID and SWARMD_CLIENT_SECRET (both required, raises ValueError if missing), plus four optional overrides:Running two agents in one process? Pass a prefix: SwarmDClient.from_env("WEATHER_AGENT") reads WEATHER_AGENT_AGENT_ID and so on.Prefer explicit config? SwarmDClient(agent_id=..., client_secret=...) or SwarmDClient.from_config(SwarmDConfig(...)).
SwarmDClient holds a pooled httpx.Client. The context manager closes it on exit. In a long-lived service you’d build the client once at startup and close it at shutdown instead — just don’t create one per request; you’d throw away the connection pool and the cached token every time.
You wrote one line. The SDK did five things:
  1. POST to the token endpoint with grant_type=client_credentials, scope=swarmd:api, resource={base_url} — the RFC 8707 parameter that binds the token’s audience to the gateway.
  2. Cached the token in memory, marked expired 60s before its real expiry.
  3. GET /registry/v1/agents/{id}/subscriptions?page=0&size=100 with Authorization: Bearer ….
  4. Followed pagination until totalPages was exhausted (capped at 100 pages).
  5. Validated each row into an AgentResponse pydantic model.
On a 401 it would have cleared the token and retried once. On a 5xx or a network error, retried with exponential backoff up to max_retries. On any other 4xx, failed immediately — retrying a 403 doesn’t help anyone.

Handling failure

The hierarchy: SwarmDSDKErrorAuthenticationErrorTokenRefreshError, and SwarmDSDKErrorAPIError. Catch SwarmDSDKError to catch everything the SDK raises.

Part 2 — Call an MCP server

MCP servers are granted to your agent the same way sub-agents are. McpClient discovers them and proxies JSON-RPC through the relay.
mcp_demo.py

Dissecting it

Because it needs a different token. McpClient mints mcp:call-scoped tokens bound to {base_url}/relay; the relay’s MCP path rejects the swarmd:api token SwarmDClient carries. Same credentials, different audience — see two tokens, not one.Everything else mirrors SwarmDClient: same constructor arguments, same from_env / from_config, same retry semantics, same context manager.
Discovery hits the registry (GET /registry/v1/agents/{id}/mcp-subscriptions), because the registry is the source of truth for grants. The relay only enforces grants on the call path.An agent principal is constrained to its own id server-side, so you can’t enumerate another agent’s grants with your token.
list_tools() and call_tool() are conveniences over call(). For anything else the MCP spec defines — resources/list, prompts/get, initialize — use call() directly:
It builds the JSON-RPC 2.0 envelope, generates a uuid4 request id if you don’t supply one, and returns the result field. A JSON-RPC error object in the response is raised as McpJsonRpcError with .code, .message and .data.
A 403 from the relay’s MCP proxy path specifically means you don’t have a grant for this server, and is raised as McpNotSubscribedError (a subclass of APIError) so you can distinguish it from generic failure:
403s from other paths stay as plain APIError — the SDK deliberately doesn’t paper over an RBAC gap on the registry side by calling it a subscription problem.

Part 3 — Keep discovery fresh

Grants change while your agent runs. AgentDirectory polls in the background and hands you an atomically-swapped snapshot.
start() refreshes once synchronously before spawning the thread, so directory.agents is populated the moment the block opens. Refresh failures are logged and swallowed — a registry blip leaves the previous snapshot in place rather than emptying your catalogue.

Part 4 — Serve an agent

Here’s the part the wrappers otherwise hide: a complete, self-serving agent using nothing but swarmd-sdk and Starlette.
main.py
Run it and check the admin surface:

Dissecting it

So credentials can arrive after boot. If you deploy an agent before its service account exists, leave the runtime unconfigured — runtime.configured is False, the agent still serves its card and health check, and an operator later does:
configure() builds fresh clients, swaps them in under a lock, and closes the old ones. base_url and token_url are required on the first configure and optional on subsequent ones, so a credential rotation only needs the two secrets.
Returns a FastAPI app with the four /admin endpoints. on_refresh is the only thing you supply — it’s called by both POST /admin/refresh and by POST /admin/webhook after the HMAC verifies.Your callback runs while holding the runtime’s lock, so it can’t race a concurrent configure(). It should be idempotent: the same webhook can arrive twice, and the SDK acks duplicates rather than trying to dedupe them.Omit on_refresh and /admin/refresh returns 501, while a verified webhook returns {"status": "ignored"} — acked, so the platform’s dispatcher doesn’t dead-letter a legitimate delivery.
CorrelationIdMiddleware is added to the outer Starlette app, so it wraps everything including the mounted admin app. It reads X-Correlation-Id off the inbound request into a ContextVar, and resets it in a finally so requests don’t leak ids into each other.Every outbound client the SDK builds reads that same var and forwards the header — which is what makes one user request show up as one trace across five agents in the audit log.
The registry caches your agent card from registration time. If your description or skills have drifted, ask it to re-read:
Call it with your own agent id — the registry compares it against the JWT subject and rejects a cross-agent refresh with 403. Treat failure as non-fatal: a registry blip at boot must not stop your agent serving.swarmd-google-adk does this for you inside create_llm_agent(). swarmd-langchain does not — call it yourself if you want it.

Part 5 — Call a sub-agent over A2A

The wrappers turn each subscription into a framework-native tool. Without a wrapper, you drive the A2A protocol yourself. Here’s the minimum, so you can see exactly what’s being automated for you.
call_subagent.py
That’s roughly 40 lines to call one sub-agent, and it’s still missing correlation-ID propagation, 401 retry, a wait cap, and conversion into whatever tool shape your LLM expects. Multiply by every subscription and that’s the argument for the wrappers.
If you’re on a framework we don’t wrap, you don’t have to write this from scratch either — use the a2a-sdk client and inject runtime.token_manager in an interceptor. That’s exactly what swarmd-google-adk and swarmd-langchain do; their helpers.py is a fine template.

What you’re doing by hand

Everything on this page, side by side with what a wrapper would have done:

Next

Reference

Every class, method, and environment variable in swarmd-sdk.

Troubleshooting

The failure modes you’ll actually hit, and what they mean.