# A2A endpoint

The A2A endpoint exposes Nightingale as an agent that other agent platforms can discover and call; the agent card declares the address and supported methods.

Last reviewed 2026-10-10

Where this page ends: a Nightingale agent that a third-party agent platform can discover and call on its own, plus one verified `message:send` request.

## Not the same thing as MCP[​](#not-the-same-thing-as-mcp "Direct link to Not the same thing as MCP")

|                          | `/mcp`                                                  | `/a2a`                                       |
| ------------------------ | ------------------------------------------------------- | -------------------------------------------- |
| What the caller gets     | 74 fine-grained tools to orchestrate themselves         | One agent that talks                         |
| Interaction              | JSON-RPC tool calls                                     | Natural language in, natural language out    |
| Needs an LLM configured? | **No** — tools hit Nightingale's API directly           | **Yes** — it drives the built-in assistant   |
| Suits                    | Clients that bring their own model: Claude Code, Cursor | Somebody else's agent orchestration platform |

Both endpoints are on by default and share one auth path. **A question the assistant cannot answer in the UI, it cannot answer over A2A either** — get a conversation working first, see [Nightingale AI overview](https://n9e.github.io/docs/ai/nightingale-ai.md).

## The agent card is the only address a third party needs[​](#the-agent-card-is-the-only-address-a-third-party-needs "Direct link to The agent card is the only address a third party needs")

```
curl -s https://n9e.example.com/.well-known/agent-card.json
```

This address needs **no authentication** (the spec requires it to be discoverable). `/.well-known/agent.json` is an alias for older clients.

What the card declares:

| Field                                      | Value                                                                                  |
| ------------------------------------------ | -------------------------------------------------------------------------------------- |
| `name`                                     | `Nightingale Agent`                                                                    |
| `supportedInterfaces`                      | One HTTP+JSON interface at `<BaseURL>/a2a`                                             |
| `capabilities.streaming`                   | `true`                                                                                 |
| `defaultInputModes` / `defaultOutputModes` | Both `text`                                                                            |
| `securitySchemes`                          | `x-user-token` (apiKey, in a header); an extra `oidc` scheme appears when RSAuth is on |
| `skills`                                   | Every built-in skill, with its name, description and example prompts                   |

`skills` is generated from the built-in skills' frontmatter, so the caller sees what Nightingale is actually good at — diagnosing why a rule did not fire, analyzing a dashboard, generating PromQL — rather than one generic blurb.

An empty `BaseURL` is inferred from request headers. **Set it explicitly behind a reverse proxy**, or the third party may end up with an internal address:

```
[HTTP.A2A]

BaseURL = "https://n9e.example.com"
```

## Send a message[​](#send-a-message "Direct link to Send a message")

The REST binding path is `/a2a/message:send` (**no** `/v1` prefix). `messageId`, `role` and `parts` are all required, and the enum value for `role` is `ROLE_USER`:

```
curl -s -X POST https://n9e.example.com/a2a/message:send \

  -H 'Content-Type: application/json' \

  -H 'X-User-Token: YOUR_TOKEN' \

  -d '{

    "message": {

      "messageId": "req-0001",

      "role": "ROLE_USER",

      "parts": [{ "text": "Which alerts are firing right now?" }]

    },

    "metadata": { "lang": "en_US" }

  }'
```

Expected result: a `task` object carrying `id`, `contextId`, `status.state`, and `n9e.chat_id` / `n9e.seq_id` under `metadata`. A `status.state` of `TASK_STATE_COMPLETED` means the turn finished; the answer is in `artifacts`. This step can take tens of seconds while the model thinks and calls tools — that is normal.

For multi-turn, put `"contextId": "<the contextId you got back>"` inside `message`. `contextId` maps one-to-one onto Nightingale's `chat_id`, which means an A2A conversation is the same conversation you see in the Nightingale AI conversation list.

## Which methods are supported[​](#which-methods-are-supported "Direct link to Which methods are supported")

| Method                      | Path                             |
| --------------------------- | -------------------------------- |
| Send a message (unary)      | `POST /a2a/message:send`         |
| Send a message (SSE stream) | `POST /a2a/message:stream`       |
| Get a task                  | `GET /a2a/tasks/{id}`            |
| Cancel a task               | `POST /a2a/tasks/{id}:cancel`    |
| Resubscribe                 | `POST /a2a/tasks/{id}:subscribe` |

`tasks/list` is **deliberately not implemented** and returns `UnsupportedOperation` per the spec — enumerating tasks across users is meaningless for a built-in assistant and would leak other people's activity.

A bare GET on `/a2a` returns a short JSON hint listing the paths above. That exists so a client still using the legacy JSON-RPC style (`tasks/send`) gets a sentence it can read, rather than being redirected into the frontend SPA.

Task state lives in Redis with a 24-hour TTL, shared across instances. After it expires `tasks/get` returns task-not-found, but the conversation itself is still in the database.

## Reverse proxy and timeouts[​](#reverse-proxy-and-timeouts "Direct link to Reverse proxy and timeouts")

A2A sits at the root path and holds long connections:

```
location /.well-known/ {

    proxy_pass http://127.0.0.1:17000;

}



location /a2a/ {

    proxy_pass http://127.0.0.1:17000;

    proxy_http_version 1.1;

    proxy_set_header Host $host;



    proxy_buffering off;        # without this, streaming answers never emit anything

    proxy_read_timeout 3600s;   # the default 60s cuts answers in half

    proxy_send_timeout 3600s;

}
```

The server sends an empty "working" status heartbeat every 30 seconds precisely to survive gateway idle timeouts — but the gateway's own read timeout still has to be raised.

## The command-line client[​](#the-command-line-client "Direct link to The command-line client")

The repository ships a test client built on the official SDK:

```
go run ./cmd/a2a-cli --server http://127.0.0.1:17000 --token YOUR_TOKEN \

    --message "Show the alert events currently firing"



# Continue the same conversation

go run ./cmd/a2a-cli --server http://127.0.0.1:17000 --token YOUR_TOKEN \

    --context-id CHAT_ID --message "Analyze the first one in more detail"
```

It streams by default; `--get` calls `tasks/get` afterwards to prove the task really was stored.

## Turning it off[​](#turning-it-off "Direct link to Turning it off")

```
[HTTP.A2A]

Disable = true   # closes /a2a, /mcp and the agent card together
```

To close MCP only and keep A2A, use `DisableMCP = true`.

## Next[​](#next "Direct link to Next")

* Get the assistant answering first: [Nightingale AI overview](https://n9e.github.io/docs/ai/nightingale-ai.md)
* Let callers use their own accounts: [OAuth 2.1 and external identity providers](https://n9e.github.io/docs/ai/oauth.md)
* The fine-grained-tools path: [Enable the MCP endpoint](https://n9e.github.io/docs/ai/mcp-endpoint.md)
