Skip to main content

Core concepts

Every helper in the Python SDKs is a thin wrapper over one of six ideas. Learn them once here and the rest of the documentation reads as reference rather than magic. If you’d rather see code first, go build something and come back: Core SDK · Google ADK · LangChain.

The shape of it

The one-sentence version: your agent never talks to a sub-agent, an MCP server, or an LLM provider directly — it talks to Swarmd, which checks the grant, forwards the call, and writes the audit row. That indirection is the whole product. It’s also why the SDK exists: without it you’d be hand-rolling OAuth2 against two different audiences and rewriting every downstream URL yourself.

1. The runtime holds your identity

SwarmDRuntime is the object everything else hangs off. It owns your credentials and the clients built from them.
After configure() the runtime exposes: The framework wrappers give you create_runtime(), which does the same thing from environment variables.
Why “configure later” rather than a constructor? Because credentials can arrive after boot. POST /admin/configure lets an operator inject them into a running agent, and the runtime swaps its clients in place — no restart. That’s also why token_manager is a proxy: helpers grab it once at startup and it keeps working across a reconfigure.

2. Two tokens, not one

This is the concept that trips people up most, so it’s worth the paragraph. Your agent holds one OAuth2 client credential (agent_id + client_secret), but it exchanges that credential for two different kinds of access token, bound to two different audiences: They are not interchangeable. The relay’s MCP path enforces a path-scoped audience check; present a swarmd:api token there and you get 403, not a helpful error about scopes.
If you’re writing your own MCP transport rather than using McpClient or a wrapper’s fetch_mcp_tools, this is the mistake you will make. Use runtime.mcp_token_manager for anything under /relay/v1/mcp-servers/.
Token handling itself is done for you: both managers cache the token, treat it as expired 60 seconds early, refresh under a double-checked lock so concurrent requests don’t stampede the token endpoint, and evict the cache on a 401 so the next call re-mints instead of looping on a stale token.

3. Subscriptions are your agent’s address book

Your agent cannot call an arbitrary agent. It can only call agents it has been subscribed to — a grant an operator creates in the dashboard or via the registry API. Both ends of a grant have to be bootstrapped first. The registry rejects a subscription whose source or sink is still an agent shell with no card attached — see Setup, step 7.
Two things to notice:
  1. The card URL is a relay URL, not the sub-agent’s real address. You never learn where the other agent actually lives. Calls go through the relay, which enforces the grant and records the hop.
  2. This is a live list, not config. Grants change while your agent is running. See refresh.
The framework wrappers turn each entry into a native tool — an ADK sub_agent, a LangChain BaseTool — so your LLM sees subscribed agents in the same catalogue as local functions.
AgentDirectory polls subscriptions on a background thread and hands you an immutable snapshot:
The list is a tuple that gets swapped atomically, so an in-flight task always sees a consistent view. Pass on_change= to be told which ids were added or removed. Use it when you want polling instead of — or as belt and braces alongside — webhook-driven refresh.

4. MCP servers are granted the same way

MCP servers work like sub-agents: an operator grants your agent access, and the SDK discovers what it has.
Discovery hits the registry (source of truth for grants); calls go through the relay (which enforces the grant on every request and writes the audit row). Both legs are handled by McpClient.

Tool namespacing

MCP server names are free-form: "GitLab - swarmd.ai". Tool names, as far as OpenAI is concerned, must match ^[a-zA-Z0-9_-]+$ — anything else fails the whole completion with 400. So the SDK derives a safe namespace per server:
Disallowed characters become _, and the first 8 hex digits of the server UUID are appended so two servers whose names sanitise identically still get distinct prefixes. Both wrappers apply this automatically — you’ll see it in your tool names, and now you know why.

5. Polling: sub-agents that take their time

A2A tasks are asynchronous. When your agent sends message/send to a sub-agent, the response may come back in a non-terminal state: Stock A2A clients return that non-terminal payload immediately — and an LLM handed {"state": "working"} will cheerfully report back “the sub-agent is working on it” and stop. Which is useless. Both wrappers solve this with a polling wrapper — PollingRemoteA2aAgent (ADK) and PollingA2aTool (LangChain). On a non-terminal response they poll tasks/get against the relay until the task reaches a terminal state, then return the final content:
  • Poll interval: 5 seconds
  • Max wait: 600 seconds (10 minutes)
Both are constructor arguments if you build the wrappers yourself.

6. Refresh keeps the catalogue live

Your agent discovers its sub-agents and MCP tools at boot. Grants change after boot. Without something in between, you’d be restarting pods every time an operator ticks a box. The SDK’s create_admin_app() mounts four endpoints at /admin: POST /admin/configure is the agent-side half of a credential rotation: when POST /registry/v1/agents/{agentId}/credential hands an operator a new clientSecret, they push it in here and the runtime swaps its clients in place — no restart, and the agent id is unchanged either way.
Swarmd derives your webhook URL from your agent card URL — it strips everything from /.well-known/ onwards and appends /admin/webhook. There is no webhookUrl field anywhere in the API. Mount the admin app at /admin (which serve() does for you) and it lines up.The same derivation is why an agent shell never receives a webhook: until you attach a card with PUT /registry/v1/agents/{agentId} (Setup, step 7) there is no card URL to derive from, so there is no target to deliver to. Push starts working the moment the card is attached.

Signature verification

Every delivery carries X-Swarmd-Timestamp and X-Swarmd-Signature. The signature is HMAC-SHA256(secret, "{timestamp}." + raw_body), compared in constant time, with a ±5 minute window on the timestamp to bound replay. create_admin_app() does this for you. If you’re serving /admin/webhook yourself, use verify_webhook() — and pass the raw body bytes, not a re-serialised dict:

What triggers a refresh

Four event types, delivered in the X-Swarmd-Event-Type header: SUBSCRIPTION_CHANGED, AGENT_LIFECYCLE, TENANT_LIFECYCLE, MCP_GRANTS_CHANGED. All four mean the same thing — something about your world changed, re-pull — so the SDK maps them all to one refresh callback. Unknown event types are acknowledged rather than rejected, so a new event type on the platform can’t dead-letter your agent.

Bonus: correlation IDs

One user request can fan out across five agents and a dozen MCP calls. To make that legible in the audit log, every hop carries the same X-Correlation-Id. CorrelationIdMiddleware captures the inbound header into a ContextVar, and every outbound client the SDK builds copies that var onto its requests. The first hop in a chain adopts the id the relay generated; every subsequent hop inherits it. You get this for free from serve(). If you’re building your own server:

Now go build

Core SDK

Use these pieces directly, in any framework.

Google ADK

Three helpers, one running ADK agent.

LangChain

The same three helpers, for LangGraph.