A2A Integration#
Install the optional official SDK dependency:
pip install "litestar-mcp[a2a]"
Build an official a2a.types.AgentCard whose A2A 1.0 JSON-RPC interface
points at the configured mount, then supply an official
a2a.server.request_handlers.RequestHandler:
from uuid import uuid4
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill, Message, Part, Role
from a2a.utils.errors import UnsupportedOperationError
from litestar import Litestar
from litestar_mcp.a2a import LitestarA2A
class EchoExecutor(AgentExecutor):
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
await event_queue.enqueue_event(
Message(message_id=str(uuid4()), role=Role.ROLE_AGENT, parts=[Part(text=context.get_user_input())])
)
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
raise UnsupportedOperationError(message="This agent returns messages without creating tasks")
def build() -> Litestar:
card = AgentCard(
name="Support agent",
description="Answers support questions",
version="1.0.0",
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
skills=[AgentSkill(id="echo", name="Echo", description="Repeat a message", tags=["example"])],
capabilities=AgentCapabilities(streaming=True),
supported_interfaces=[
AgentInterface(
url="https://example.com/a2a",
protocol_binding="JSONRPC",
protocol_version="1.0",
)
],
)
handler = DefaultRequestHandler(agent_executor=EchoExecutor(), task_store=InMemoryTaskStore(), agent_card=card)
return Litestar(plugins=[LitestarA2A(card, handler)])
The default RPC endpoint is /a2a and the card is served from
/.well-known/agent-card.json with ETag and Cache-Control headers
(A2AConfig.agent_card_max_age). Both routes are exempt from CSRF and hidden
from the OpenAPI schema unless A2AConfig.include_in_schema is set. Requests
must send A2A-Version: 1.0; valid 1.0.x patch versions are also accepted.
Missing, malformed, 0.3, and unsupported major/minor versions are rejected.
Only the 1.0 method names and protobuf JSON shapes are accepted. The card
must advertise an absolute JSONRPC interface URL for protocol 1.0 whose
path matches A2AConfig.path. Cards should describe the actual supported
input/output modes, skills and capabilities.
The extra installs the transport-neutral SDK without requiring Starlette, FastAPI or Uvicorn. Choose an ASGI server separately for deployment. MCP routes and A2A skills are registered independently; share business services between executors and MCP tools explicitly.
Support and ownership#
Layer |
Provided here |
Application responsibility |
|---|---|---|
Litestar transport |
ASGI routes, JSON, SSE, middleware, guards, lifecycle and disconnect cleanup |
Configure authentication, route policy and the deployment server |
Official SDK |
Protocol models, errors, RequestHandler and AgentExecutor contracts; DefaultRequestHandler task execution |
Supply an executor, supported capabilities and appropriately scoped stores |
Durable execution |
Adapter forwards task and subscription operations to the handler |
Durable records, worker coordination, recovery and long-running service scopes |
Push notifications |
Create/get/list/delete configuration methods reach the handler |
Enable SDK stores/sender; enforce ownership, callback URL policy, credentials, retries and delivery guarantees |
The JSON-RPC binding covers SendMessage, SendStreamingMessage,
GetTask, ListTasks, CancelTask, SubscribeToTask,
CreateTaskPushNotificationConfig, GetTaskPushNotificationConfig,
ListTaskPushNotificationConfigs, DeleteTaskPushNotificationConfig, and
GetExtendedAgentCard. Availability depends on the configured handler and
advertised capabilities. There is no gRPC or HTTP/REST binding and no new
distributed backend. SDK client/executor integration tests cover task
completion, continuation after input-required, artifacts, cancellation,
subscription, pagination, push configuration and extended cards. These are
repository integration tests, not an external A2A conformance certification.
Authentication and context#
Authenticate the RPC route with Litestar middleware and authorize access with
A2AConfig.guards or A2AConfig.route_opt. The public card route declares
exclude_from_auth; authentication middleware must honor that opt when
public discovery is wanted.
Card metadata does not enforce authorization.
context_builder(request, context) accepts a Litestar request and a prepared
SDK ServerCallContext. It may return the context synchronously or await
authorization before returning it. The prepared context contains the user,
requested tenant and extensions, plus state["auth"], state["headers"],
state["litestar_state"], state["method"] and state["request_id"].
The returned context is authoritative: the adapter does not overwrite its
authorized tenant or state with untrusted request parameters.
The prepared user's user_name is taken from the first of id, sub,
username or display_name found on the Litestar principal. A principal
that exposes none of them maps to the empty owner key the SDK uses for
anonymous callers, so such applications must supply a context_builder
that sets the user themselves; otherwise the SDK's owner-scoped stores treat
every such caller as one owner.
Resolve tenant membership from authenticated application identity. A tenant
parameter, task ID or context ID alone grants no access. Enforce the same
principal/tenant boundary on task get/list/continue/cancel/subscribe and push
configuration operations. A scoped store alone is insufficient with SDK
DefaultRequestHandler: its live execution registry can bypass store lookups
for cancellation and subscription. The authenticated example in
docs/examples/a2a_application/main.py uses a narrow handler authorization
boundary around these operations and scopes request services inside the
executor. That example's identity and callback sender are demonstrations;
production applications supply real identity validation and delivery policy.
Resource lifetime and streaming#
Litestar HTTP dependency providers may close when the route handler returns,
before its SSE response finishes. Executors must open their own
application/Dishka resource scope using the verified call context, including
work that continues after returnImmediately or a client disconnect.
Do not pass a request-scoped session into work that outlives that request.
The plugin calls the handler's aclose() hook, when available, at app shutdown.
SSE production and iterator closure run in the same producer task. A bounded
channel provides backpressure; A2AConfig.stream_cleanup_timeout (default
5.0 seconds) bounds cooperative cleanup after the response ends. Expiry is
logged as incomplete cleanup. Application finalizers must tolerate cancellation;
the adapter cannot force arbitrary application work to finish.
Activate extensions by putting a set of requested, advertised URI strings in
context.state["a2a_activated_extensions"] before returning a result or
yielding the first stream event. Only those activated extensions appear in
A2A-Extensions response headers. Required advertised extensions must be
requested. Later activation cannot change already-sent response headers.
This request-only adapter declines well-formed JSON-RPC notifications (an
omitted id) with HTTP 204 without executing them. An explicit null ID is
a correlated A2A request and receives a response. Malformed envelopes return
JSON-RPC errors. MCP has its own stricter request-ID contract; see
Migrating to 0.14.