Skip to main content

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.

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​

/mcp/a2a
What the caller gets74 fine-grained tools to orchestrate themselvesOne agent that talks
InteractionJSON-RPC tool callsNatural language in, natural language out
Needs an LLM configured?No — tools hit Nightingale's API directlyYes — it drives the built-in assistant
SuitsClients that bring their own model: Claude Code, CursorSomebody 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.

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:

FieldValue
nameNightingale Agent
supportedInterfacesOne HTTP+JSON interface at <BaseURL>/a2a
capabilities.streamingtrue
defaultInputModes / defaultOutputModesBoth text
securitySchemesx-user-token (apiKey, in a header); an extra oidc scheme appears when RSAuth is on
skillsEvery 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​

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​

MethodPath
Send a message (unary)POST /a2a/message:send
Send a message (SSE stream)POST /a2a/message:stream
Get a taskGET /a2a/tasks/{id}
Cancel a taskPOST /a2a/tasks/{id}:cancel
ResubscribePOST /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​

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 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​

[HTTP.A2A]
Disable = true # closes /a2a, /mcp and the agent card together

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

Next​