Skip to main content

Troubleshooting

Symptoms, causes, and fixes — ordered roughly by how often they bite.

My agent discovers nothing

By far the most common report. Three distinct causes, distinguishable by one log line.
Cause. create_runtime() only calls configure() when all four of SWARMD_AGENT_ID, SWARMD_CLIENT_SECRET, SWARMD_BASE_URL and SWARMD_TOKEN_URL are set. Any one missing and it silently returns an unconfigured runtime.The trap is that SWARMD_BASE_URL and SWARMD_TOKEN_URL have sensible defaults in swarmd-sdk, so people leave them out of .env and assume the wrappers behave the same way. They don’t.Fix. Set all four explicitly:
Confirm with curl http://localhost:8080/admin/status — it should report "configured": true.
Cause. The call worked; you genuinely have no subscriptions. Grants are created by an operator, not by your agent.Fix. In the dashboard, subscribe your agent to the agents it should be able to call, and grant it the MCP servers it needs. Then either wait for the webhook, or curl -X POST http://localhost:8080/admin/refresh.A brand-new agent showing 0 is correct, not broken.
Cause. load_dotenv() ran after the SDK import. Module import reads the environment; loading a .env afterwards is too late.Fix. This ordering, exactly — yes, it violates PEP 8, and yes, it’s necessary:
Also check you’re running python main.py from the directory that holds .env.

Authentication fails at startup

Symptom. AuthenticationError or TokenRefreshError from the first call. Isolate it from your agent entirely:
An access_token back means your credentials are fine and the problem is elsewhere.

403 when calling an MCP server

Symptom. McpNotSubscribedError, or a bare 403 from /relay/v1/mcp-servers/{id}/mcp. Two causes that look identical from the outside:
The relay checks the grant on every call. runtime.mcp.list_available() tells you what you actually have — if the server isn’t in that list, ask an operator to grant it.
The relay’s MCP path enforces a path-scoped RFC 8707 audience check. A swarmd:api token is rejected there with 403 — it does not tell you the problem is the scope.Fix. Use runtime.mcp_token_manager (scope mcp:call, resource {base_url}/relay) for anything under /relay/v1/mcp-servers/. Both wrappers’ fetch_mcp_tools() and McpClient already do; this only bites when you’re hand-rolling the transport.Decode the JWT and check its aud claim if you’re unsure.

403 from get_agent_subscriptions

You passed an agent id that isn’t your own. The endpoint is dual-auth: tenant admins may query any agent, but an agent principal is constrained to itself and the registry rejects a mismatch against the JWT subject. Pass UUID(runtime.config.agent_id).

Webhooks never fire

SWARMD_WEBHOOK_SECRET never reached the runtime.On LangChain this is a known gap — create_runtime() doesn’t forward it. Either configure the runtime yourself, or push the secret in over the admin API:
On ADK or the core SDK, the variable simply isn’t set. Note that a webhook_secret omitted from a later configure() call is preserved, not cleared — so this only bites on first configure.
Almost always because the body was re-serialised before verification. HMAC is over the exact bytes received; json.dumps(await request.json()) changes key order and whitespace.Pass await request.body(). create_admin_app() does this correctly — this only applies if you’re verifying by hand.
Clock drift over ±5 minutes between Swarmd and your pod, in either direction. Check NTP on the host. Raising max_age_seconds widens the replay window and is not the fix.
Swarmd derives your webhook URL from your agent card URL: it strips /.well-known/agent-card.json and appends /admin/webhook. There’s no webhookUrl field at registration.So https://my-agent.example.com/.well-known/agent-card.json means Swarmd POSTs to https://my-agent.example.com/admin/webhook. Confirm that URL is publicly reachable and that your admin app is mounted at /admin.That derivation needs a card URL on file. An agent you registered but never bootstrapped — no PUT /registry/v1/agents/{agentId} yet — has no card URL, so there is no webhook target to derive and nothing will ever arrive. See Setup, step 7.

400 from OpenAI on tool names

Symptom. The whole completion fails with a 400 mentioning an invalid tool name. Cause. A tool name outside ^[a-zA-Z0-9_-]+$. MCP server names are free-form ("GitLab - swarmd.ai"), so anything derived from one has to be sanitised. Fix. Both wrappers namespace MCP tools via mcp_tool_namespace() automatically. If you’re building tools yourself, run names through it — or through swarmd_langchain’s _safe_tool_name equivalent for local tools.

Sub-agent replies “I’m working on it” and stops

Cause. A non-terminal A2A task state (working, submitted, input_required, auth_required) reached the model, which dutifully reported it. Fix. Use PollingRemoteA2aAgent (ADK) or PollingA2aTool (LangChain) — both wrappers do by default. If you built the A2A client yourself, you must poll tasks/get until the state is terminal. If it’s the parent that’s tool-shy — replying “Let me check…” without ever calling the sub-agent — that’s a different problem, and on ADK tool_choice="required" is the lever. Only use it on agents whose reply path always ends in a tool call, or the completion loop can’t halt.

Sub-agent calls time out after 10 minutes

max_wait defaults to 600s. Build the polling wrapper yourself with a longer cap:

Tasks disappear after a restart

serve() stores tasks in SQLite under /tmp — plus sessions, on ADK — and /tmp doesn’t survive a pod restart. For durable state, copy serve() into your own module and point DatabaseTaskStore (and DatabaseSessionService on ADK) at a real database — they accept any SQLAlchemy async URL.

Dependency resolution conflicts

Symptom. ImportError on McpToolset, StreamableHTTPConnectionParams, a2a.client.middleware, or a2a.types.TextPart. Cause. A resolver picked a version outside the tested range — usually because another package in your environment widened a bound. Fix. Check the installed versions against dependency ranges:
The three that matter: google-adk<3.0, a2a-sdk<1.0, mcp<2. Each bound exists because crossing it broke a real build.

LLM calls fail on a gateway-routed ADK agent

Symptom. WARN: SWARMD_LLM_GATEWAY_ID=… is set but the SwarmDRuntime is unconfigured at boot, then 401s on every completion. Cause. SWARMD_LLM_GATEWAY_ID is set but the runtime isn’t configured, so the helper built a placeholder LlmAgent rather than crashing the pod. That’s deliberate: a fresh agent’s credentials often don’t exist on its first deploy, and a CrashLoopBackOff would roll the whole release back over an agent that only needed to serve its card. Fix. Supply the four platform env vars and redeploy. The next boot builds a properly routed agent.

Still stuck

Check platform status

Rule out an incident before debugging your agent.

Email support

Include your agent id, the SDK version, and the failing log lines.