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.'standalone mode' / 'SwarmD not configured' in the logs
'standalone mode' / 'SwarmD not configured' in the logs
Cause. Confirm with
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:curl http://localhost:8080/admin/status — it should report
"configured": true.'discovered 0 subscribed agent(s)' — configured, but empty
'discovered 0 subscribed agent(s)' — configured, but empty
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.'.env' isn't being read at all
'.env' isn't being read at all
Cause. Also check you’re running
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: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:
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:
No grant for that server
No grant for that server
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.Wrong token audience (only if you built your own transport)
Wrong token audience (only if you built your own transport)
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
503 'webhook secret not configured'
503 'webhook secret not configured'
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:webhook_secret omitted from a later configure() call is preserved, not
cleared — so this only bites on first configure.401 'signature mismatch'
401 'signature mismatch'
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.401 'timestamp out of window'
401 'timestamp out of window'
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.Nothing arrives at all
Nothing arrives at all
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:
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.
