7.2

A server

A server is a card, one handler per binding, three checks before any work, an executor, an owner-scoped task store, and one event queue that feeds streams and webhooks.

The three agents in the kit come from one file of 675 lines, written with the Python standard library only. Each agent serves a card, answers both JSON bindings, runs its own logic in a thread, and keeps its tasks in a dictionary. When you finish this section, you can name each part a server must build, and find it in that file and in the reference SDK.

Figure 7.2 follows one SendStreamingMessage through those parts, from the HTTP request to the stream frames and the webhook POST.

Fig. 7.2the parts of a server, in the order a request meets themstructure
The parts of a server, in the order a request meets them One SendStreamingMessage request enters the kit server at Handler.jsonrpc or Handler.rest. It passes authenticate, which answers HTTP 401 for a bad token, check_version, which answers VersionNotSupportedError -32009, and the capability check on the card, which answers UnsupportedOperationError -32004. Agent.send then validates the message and stores a task in Agent.tasks as TASK_STATE_SUBMITTED. Agent.work runs the behavior, the agent logic, in a thread. Agent.apply writes each event to the task record and Agent.broadcast copies it to every subscriber queue, which Handler.pump writes as SSE data frames, and to Agent.deliver, which POSTs it to each webhook URL. THE REQUEST, TOP TO BOTTOM WHAT A FAILED CHECK RETURNS SendStreamingMessage request POST /a2a/jsonrpc, A2A-Version: 1.0 Handler.jsonrpc, Handler.rest one method per binding, same checks authenticate bearer token, deployer only HTTP 401 WWW-Authenticate: Bearer check_version A2A-Version must be 1.0 VersionNotSupportedError -32009, FAILED_PRECONDITION card["capabilities"] streaming, pushNotifications UnsupportedOperationError -32004, or -32003 for push Agent.send validate_message, then one task Agent.tasks the task record, TASK_STATE_SUBMITTED Agent.work, behavior the agent logic, in its own thread a thread per request Agent.apply, Agent.broadcast the record, then every subscriber yields events writes the task Handler.pump data: frames on each SSE stream Agent.deliver POST to each webhook URL each event each event
A request passes authentication, the version check, and the capability check before Agent.send stores a task, and every event then leaves through one broadcast. Read the left column top to bottom. Each box is a method of the kit's server. Rose boxes on the right are the errors each check returns, with their JSON-RPC codes from the error capture.

The card and one handler per binding

Card route: the GET on /.well-known/agent-card.json that returns the card. The kit's Handler.do_GET answers it from published_card. The SDK's create_agent_card_routes serves the same path, named by AGENT_CARD_WELL_KNOWN_PATH.

Binding handler: one entry point per binding, which parses the request, runs the checks, and calls the same agent methods. The kit has Handler.jsonrpc at /a2a/jsonrpc and Handler.rest under /a2a/rest/, and no gRPC, as gRPC explains. The SDK has create_jsonrpc_routes, create_rest_routes, and GrpcHandler. All three call one DefaultRequestHandler, which is DefaultRequestHandlerV2.

the JSON-RPC handler, from the token to the first two methodspythonFull log
    def jsonrpc(self, raw):
        request_id = None
        if not self.authenticate():
            return None
        try:
            try:
                request = json.loads(raw)
            except ValueError:
                raise A2AError("JSONParseError")
            if not isinstance(request, dict) or request.get("jsonrpc") != "2.0" or "method" not in request:
                raise A2AError("InvalidRequestError")
            request_id = request.get("id")
            self.check_version()
            method = request["method"]
            params = request.get("params") or {}
            agent = self.agent
            if method == "SendMessage":
                return self.write_json(200, {"jsonrpc": "2.0", "id": request_id, "result": agent.send(params)}, "application/json")
            if method == "SendStreamingMessage":
                if not agent.card["capabilities"].get("streaming"):
                    raise A2AError("UnsupportedOperationError")
                stream = queue.Queue()
                agent.send(params, stream)
                self.open_stream()
                return self.pump(stream, lambda event: {"jsonrpc": "2.0", "id": request_id, "result": event})
            …
        except A2AError as error:
            return self.write_json(200, {"jsonrpc": "2.0", "id": request_id, "error": error.jsonrpc()}, "application/json")
The head of the jsonrpc method and its error branch. The other nine method branches are cut.

Three checks before any work

Authentication runs first. The kit's authenticate answers HTTP 401 with a WWW-Authenticate challenge before any method, on the deployer only. The SDK checks no credential itself: ServerCallContext.user comes from the web framework's middleware. Security schemes says what to check.

The version check runs second. check_version accepts exactly 1.0 and raises VersionNotSupportedError for anything else, because an agent must read an empty value as version 0.3. The SDK's validate_version decorator wraps every JSON-RPC and REST dispatcher method and compares the major version. It does not wrap the gRPC servicer. The version header gives the rules and each SDK's behavior.

The capability check runs third. Before a stream or a push operation, the kit reads card["capabilities"] and raises UnsupportedOperationError or PushNotificationNotSupportedError. The SDK does the same with a validate decorator on on_message_send_stream and on the push methods.

The executor and the task store

Executor: the code that runs the agent for one request and emits its events. The kit's Agent.send validates the message, creates the task in TASK_STATE_SUBMITTED, and starts Agent.work in a thread. work calls behavior, the agent's own logic, and applies each event it yields. The SDK's AgentExecutor.execute receives a RequestContext and an EventQueue, and returns when the work is done or the task enters an interrupted state.

What the executor calls stays hidden from the client. Figure 7.3 draws that line for the three kit agents.

Fig. 7.3A2A between agents, tools inside each onelayers
A2A between agents, tools inside each one The planner, the client agent, sends SendMessage over A2A to three remote agents: test-runner on port 41241 with skill run-tests, code-reviewer on port 41242 with skills review-diff and answer-question, and deployer on port 41243 with skill deploy. The planner sees only these cards and endpoints. Below a dashed line, hidden from the planner, each agent runs its own logic and calls its own systems as tools: Git and a CI runner for test-runner, Git for code-reviewer, and a deploy API for deployer. CLIENT AGENT planner delivers payments-api: tests, review, deploy A2A · WHAT THE PLANNER SEES HIDDEN FROM THE PLANNER The kit simulates these systems in Python. A real agent calls them through MCP or any API. SendMessage test-runner :41241 run-tests SendMessage code-reviewer :41242 review-diff answer-question SendMessage deployer :41243 deploy its own logic model and code its own logic model and code its own logic model and code tool call Git, CI runner check out, run the suite tool call Git fetch the base branch tool call deploy API roll out to staging
The planner sees only each agent's card and endpoint, while each agent calls its own logic and tools below a line that A2A never crosses. Read top to bottom. Above the dashed line is what the planner sees over A2A. Below it is what each agent runs on its own, which the kit simulates in Python.

Task store: the record of every task, read by GetTask, ListTasks, and each follow-up message. The kit's Agent.tasks is one dictionary with no owner, and the deployer's bearer token is its only guard. The SDK's TaskStore has save, get, list, and delete, each given the ServerCallContext. InMemoryTaskStore keys tasks by owner, and the default OwnerResolver returns context.user.user_name. Another caller's task is then TaskNotFoundError, but every caller without a user name shares one scope. A server must check authorization on every A2A operation request, so the integrator adds the middleware, as security schemes shows for each SDK.

One event queue for streams and webhooks

The kit's Agent.apply updates the task record. Agent.broadcast then puts a copy of the event on each subscriber queue and calls deliver for each push config. Handler.pump reads one subscriber queue and writes each event as a data: frame. broadcast closes the stream at a terminal state or TASK_STATE_INPUT_REQUIRED, and deliver POSTs the event to the webhook URL with X-A2A-Notification-Token.

The SDK's ActiveTask does the same with two queues. The executor writes to _event_queue_agent. A consumer reads it, saves the task, forwards each event to _event_queue_subscribers, and calls PushNotificationSender.send_notification. Streaming and push notification configs show both paths frame by frame.

A2A protocol 1.0.1 · 3303592 · 2026-05-28 · Edition 2026.10