Google ADK on SwarmD
swarmd-google-adk is a thin wrapper on top of Google ADK
that gives a Google ADK LlmAgent access to the SwarmD platform: OAuth2
authentication, sub-agent discovery, 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 Google ADK agent and want to move it onto
SwarmD, read this first to learn the helpers, then follow
Migrating from Google ADK. If you are starting
from scratch this page is the whole story.
What it gives you
The SDK exposes three helpers that, in order, produce a complete agent:| Helper | Returns | What it does |
|---|---|---|
create_runtime() | SwarmDRuntime | Reads SWARMD_* env vars, configures a runtime, and instantiates token managers (platform-API + MCP-relay). |
create_llm_agent(runtime, ...) | Google ADK LlmAgent | Picks the LLM provider, discovers subscribed sub-agents, discovers subscribed MCP servers, wires both into the LlmAgent’s sub_agents and tools. |
serve(agent, runtime) | runs forever | Mounts the A2A protocol, the agent card, the /admin surface, correlation middleware, and starts uvicorn. Refreshes the catalogue in place on signed webhooks. |
if __name__ == "__main__" guard.
Installation
swarmd-sdk as a transitive dependency plus google-adk,
a2a-sdk, and the relay HTTP client.
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/webhookendpoint.
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
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, it just won’t see any subscribed sub-agents or MCP tools.
This is useful for local development without a SwarmD account.
The whole agent, end to end
main.py
Found N subscribed agents and
Loaded N MCP toolset(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
SwarmDClientfor registry and audit calls. - An
McpClientfor MCP discovery. - Two
TokenManagerProxyobjects — 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.
create_llm_agent(runtime, name, description, instruction, tools, *, generate_content_config=None)
Returns a regular Google ADK LlmAgent you can pass around like any
other ADK agent. It does five things on top of LlmAgent(...):
- Picks the LLM provider.
- If
SWARMD_LLM_GATEWAY_IDis set, LiteLlm is built withapi_basepointed at${SWARMD_BASE_URL}/llm/v1/${id}andapi_keyset to the agent’s bearer — every LLM call flows through the relay and shows up in audit. No provider API key needed. - If only
OPENAI_API_KEYis set, LiteLlm goes direct to OpenAI. - If neither is set the call raises
ValueErrorrather than booting in a broken state.
- If
- Discovers sub-agents. Calls
fetch_remote_agents(runtime), which pulls subscriptions fromGET /registry/v1/agents/{id}/subscriptionsand wraps each one in aPollingRemoteA2aAgent. The polling wrapper is the important bit: when a sub-agent returns a non-terminal task state (working,submitted,input_required,auth_required) the wrapper pollstasks/getagainst the relay until it reaches a terminal state, then surfaces the final result to the parent LLM as a normal Event. ADK’s stockRemoteA2aAgentjust returns the non-terminal payload, which the LLM can’t do anything useful with. - Discovers MCP servers. Calls
fetch_mcp_tools(runtime), which lists MCP grants via the SDK’sMcpClient, then builds one ADKMcpToolsetper granted server 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 the LLM provider rejects. A header provider mints a freshmcp:call-scoped token on every request and propagates the current correlation ID. - Honours
OPENAI_TEMPERATURE. When set, it becomes aGenerateContentConfig(temperature=...)automatically. Passgenerate_content_config=...to override. - Composes the catalogue. Local tools you passed in + remote
sub-agents (as
PollingRemoteA2aAgentinsub_agents) + MCP toolsets (asMcpToolsetentries intools).
RemoteA2aAgents — drop
down to create_runtime, fetch_remote_agents, and fetch_mcp_tools
directly and build the LlmAgent yourself. They are all exported.
serve(agent, runtime)
The thickest of the three helpers because the A2A server is the agent’s
public face. In order, it:
- Mounts the A2A protocol. Builds an ADK
AgentCardBuilder, serialises the card at/.well-known/agent-card.json, attachesA2AStarletteApplication(which ownsmessage/send,message/stream,tasks/get,tasks/cancel, etc.), and wiresA2aAgentExecutorso the protocol calls reach yourLlmAgent. - Persists tasks and sessions. A SQLite-backed
DatabaseTaskStoreandDatabaseSessionServiceare created at/tmp/{agent}_tasks.dband/tmp/{agent}_sessions.db. Enough for local dev; in production point them at a real database by overriding the create calls inside your own copy ofserveif you need durability across pod restarts. - Adds correlation propagation. A
CorrelationIdMiddlewareextractsX-Correlation-Idoff inbound A2A calls into aContextVar, and the SDK’s outbound interceptors copy that var onto every downstream bearer-tokened request. One request through the relay shows up as one trace ID end-to-end in the audit log. - Mounts
/admin. Four endpoints —POST /admin/configure,POST /admin/refresh,POST /admin/webhook,GET /admin/status./admin/webhookis 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 againstSWARMD_WEBHOOK_SECRET, and calls the same refresh path as/admin/refresh. - Refreshes in place. The refresh callback re-runs
fetch_remote_agentsandfetch_mcp_tools, then swaps theLlmAgent’ssub_agentsandtoolslists atomically. Local Python tools you passed intocreate_llm_agentare preserved across the swap. No restart needed when subscriptions change. - Starts uvicorn on
HOST:PORT(defaults0.0.0.0:8080) withLOG_LEVELfrom env.
Verifying it works
Found N subscribed agents / Loaded N MCP toolset(s) appear in
the agent’s startup logs.
Advanced — using the SDK pieces directly
If you need to deviate from whatcreate_llm_agent and serve do, drop
down to the underlying pieces. The whole surface area is exported:
- Custom polling interval / max wait. Build
PollingRemoteA2aAgentinstances yourself withpoll_interval=andmax_wait=. - Hand-built
LlmAgentconfig. Use the discovery functions to get the lists, then assembleLlmAgent(...)with whatever extra callbacks, structured-output config, or custom tools you need. - Custom A2A server. Use
create_admin_app(runtime, on_refresh=...)for the/adminsurface and write your own Starlette / FastAPI bootstrap around it. The refresh callback is yours to define.
Reference implementation
See thetime_agent
in the repo — a complete agent in ~30 lines of main.py. It is the
canonical “smallest working agent” example and the file you should
diff against when you want to know what a clean migration looks like.
Next steps
- Migrating from Google ADK — step-by-step guide for moving an existing raw-ADK agent onto this SDK.
- LangChain on SwarmD — the same shape for LangChain.
- Configuration — env var reference and advanced
options on the base
swarmd-sdk.
