> ## Documentation Index
> Fetch the complete documentation index at: https://docs.swarmd.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> The failure modes you'll actually hit with the Python SDKs, and what each one means.

# 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.

<AccordionGroup>
  <Accordion title="'standalone mode' / 'SwarmD not configured' in the logs">
    **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:

    ```bash theme={null}
    SWARMD_AGENT_ID=...
    SWARMD_CLIENT_SECRET=...
    SWARMD_BASE_URL=https://api.dev.swarmd.ai
    SWARMD_TOKEN_URL=https://auth.dev.swarmd.ai/realms/swarmd/protocol/openid-connect/token
    ```

    Confirm with `curl http://localhost:8080/admin/status` — it should report
    `"configured": true`.
  </Accordion>

  <Accordion title="'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.
  </Accordion>

  <Accordion title="'.env' isn't being read at all">
    **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:

    ```python theme={null}
    from dotenv import load_dotenv

    load_dotenv()

    from swarmd_google_adk import create_llm_agent, create_runtime, serve
    ```

    Also check you're running `python main.py` from the directory that holds
    `.env`.
  </Accordion>
</AccordionGroup>

***

## Authentication fails at startup

**Symptom.** `AuthenticationError` or `TokenRefreshError` from the first call.

| Cause                                    | How to tell                                                                                                                                    |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `SWARMD_CLIENT_SECRET` truncated on copy | The secret has no separators, so a partial paste looks plausible. Compare its length against what you saved.                                   |
| Agent was deregistered                   | Deregistration revokes the service account. Check the dashboard.                                                                               |
| Wrong environment                        | Credentials from one environment against another's `SWARMD_TOKEN_URL`. `SWARMD_BASE_URL` and `SWARMD_TOKEN_URL` must point at the same estate. |
| Wrong realm in the token URL             | The path is `/realms/swarmd/protocol/openid-connect/token`.                                                                                    |

Isolate it from your agent entirely:

```bash theme={null}
curl -X POST "$SWARMD_TOKEN_URL" \
  -d grant_type=client_credentials \
  -d client_id="$SWARMD_AGENT_ID" \
  -d client_secret="$SWARMD_CLIENT_SECRET" \
  -d scope=swarmd:api
```

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:

<AccordionGroup>
  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>

***

## `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

<AccordionGroup>
  <Accordion title="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:

    ```bash theme={null}
    curl -X POST http://localhost:8080/admin/configure \
      -H 'Content-Type: application/json' \
      -d '{"agent_id":"'"$SWARMD_AGENT_ID"'","client_secret":"'"$SWARMD_CLIENT_SECRET"'","webhook_secret":"'"$SWARMD_WEBHOOK_SECRET"'"}'
    ```

    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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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](/sdks/python/setup#step-7-attach-your-agent-card).
  </Accordion>
</AccordionGroup>

***

## `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:

```python theme={null}
PollingRemoteA2aAgent(..., poll_interval=15.0, max_wait=3600.0)
```

***

## 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](/sdks/python/reference#dependency-ranges):

```bash theme={null}
pip list | grep -E "google-adk|a2a-sdk|^mcp |langchain"
```

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 `401`s 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

<CardGroup cols={2}>
  <Card title="Check platform status" icon="signal" href="https://status.swarmd.ai">
    Rule out an incident before debugging your agent.
  </Card>

  <Card title="Email support" icon="envelope" href="mailto:support@swarmd.ai">
    Include your agent id, the SDK version, and the failing log lines.
  </Card>
</CardGroup>
