Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/a2a/server/apps/jsonrpc/fastapi_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def add_routes_to_app(
self.handle_deprecated_agent_card_path
)

# TODO: deprecated endpoint to be removed in a future release
if self.agent_card.supports_authenticated_extended_card:
app.get(extended_agent_card_url)(
self._handle_get_authenticated_extended_agent_card
Expand Down
26 changes: 25 additions & 1 deletion src/a2a/server/apps/jsonrpc/jsonrpc_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@
AgentCard,
CancelTaskRequest,
DeleteTaskPushNotificationConfigRequest,
GetAuthenticatedExtendedCardRequest,
GetTaskPushNotificationConfigRequest,
GetTaskRequest,
InternalError,
InvalidRequestError,
JSONParseError,
JSONRPCError,
JSONRPCErrorResponse,
JSONRPCRequest,
JSONRPCResponse,
ListTaskPushNotificationConfigRequest,
SendMessageRequest,
Expand Down Expand Up @@ -143,7 +145,9 @@ def __init__(
self.agent_card = agent_card
self.extended_agent_card = extended_agent_card
self.handler = JSONRPCHandler(
agent_card=agent_card, request_handler=http_handler
agent_card=agent_card,
request_handler=http_handler,
extended_agent_card=extended_agent_card,
)
if (
self.agent_card.supports_authenticated_extended_card
Expand Down Expand Up @@ -213,7 +217,16 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911

try:
body = await request.json()
if isinstance(body, dict):
request_id = body.get('id')

# First, validate the basic JSON-RPC structure. This is crucial
# because the A2ARequest model is a discriminated union where some
# request types have default values for the 'method' field
JSONRPCRequest.model_validate(body)

a2a_request = A2ARequest.model_validate(body)

call_context = self._context_builder.build(request)

request_id = a2a_request.root.id
Expand Down Expand Up @@ -353,6 +366,13 @@ async def _process_non_streaming_request(
context,
)
)
case GetAuthenticatedExtendedCardRequest():
handler_result = (
await self.handler.get_authenticated_extended_card(
request_obj,
context,
)
)
case _:
logger.error(
f'Unhandled validated request type: {type(request_obj)}'
Expand Down Expand Up @@ -450,6 +470,10 @@ async def _handle_get_authenticated_extended_agent_card(
self, request: Request
) -> JSONResponse:
"""Handles GET requests for the authenticated extended agent card."""
logger.warning(
'HTTP GET for authenticated extended card has been called by a client. '
'This endpoint is deprecated in favor of agent/authenticatedExtendedCard JSON-RPC method and will be removed in a future release.'
)
if not self.agent_card.supports_authenticated_extended_card:
return JSONResponse(
{'error': 'Extended agent card not supported or not enabled.'},
Expand Down
1 change: 1 addition & 0 deletions src/a2a/server/apps/jsonrpc/starlette_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def routes(
)
)

# TODO: deprecated endpoint to be removed in a future release
if self.agent_card.supports_authenticated_extended_card:
app_routes.append(
Route(
Expand Down
35 changes: 35 additions & 0 deletions src/a2a/server/request_handlers/jsonrpc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
from a2a.server.request_handlers.response_helpers import prepare_response_object
from a2a.types import (
AgentCard,
AuthenticatedExtendedCardNotConfiguredError,
CancelTaskRequest,
CancelTaskResponse,
CancelTaskSuccessResponse,
DeleteTaskPushNotificationConfigRequest,
DeleteTaskPushNotificationConfigResponse,
DeleteTaskPushNotificationConfigSuccessResponse,
GetAuthenticatedExtendedCardRequest,
GetAuthenticatedExtendedCardResponse,
GetAuthenticatedExtendedCardSuccessResponse,
GetTaskPushNotificationConfigRequest,
GetTaskPushNotificationConfigResponse,
GetTaskPushNotificationConfigSuccessResponse,
Expand Down Expand Up @@ -57,15 +61,18 @@ def __init__(
self,
agent_card: AgentCard,
request_handler: RequestHandler,
extended_agent_card: AgentCard | None = None,
):
"""Initializes the JSONRPCHandler.

Args:
agent_card: The AgentCard describing the agent's capabilities.
request_handler: The underlying `RequestHandler` instance to delegate requests to.
extended_agent_card: An optional, distinct Extended AgentCard to be served
"""
self.agent_card = agent_card
self.request_handler = request_handler
self.extended_agent_card = extended_agent_card

async def on_message_send(
self,
Expand Down Expand Up @@ -395,3 +402,31 @@ async def delete_push_notification_config(
id=request.id, error=e.error if e.error else InternalError()
)
)

async def get_authenticated_extended_card(
self,
request: GetAuthenticatedExtendedCardRequest,
context: ServerCallContext | None = None,
) -> GetAuthenticatedExtendedCardResponse:
"""Handles the 'agent/authenticatedExtendedCard' JSON-RPC method.

Args:
request: The incoming `GetAuthenticatedExtendedCardRequest` object.
context: Context provided by the server.

Returns:
A `GetAuthenticatedExtendedCardResponse` object containing the config or a JSON-RPC error.
"""
if self.extended_agent_card is None:
return GetAuthenticatedExtendedCardResponse(
root=JSONRPCErrorResponse(
id=request.id,
error=AuthenticatedExtendedCardNotConfiguredError(),
)
)

return GetAuthenticatedExtendedCardResponse(
root=GetAuthenticatedExtendedCardSuccessResponse(
id=request.id, result=self.extended_agent_card
)
)
60 changes: 60 additions & 0 deletions tests/server/request_handlers/test_jsonrpc_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@
AgentCapabilities,
AgentCard,
Artifact,
AuthenticatedExtendedCardNotConfiguredError,
CancelTaskRequest,
CancelTaskSuccessResponse,
DeleteTaskPushNotificationConfigParams,
DeleteTaskPushNotificationConfigRequest,
DeleteTaskPushNotificationConfigSuccessResponse,
GetAuthenticatedExtendedCardRequest,
GetAuthenticatedExtendedCardResponse,
GetAuthenticatedExtendedCardSuccessResponse,
GetTaskPushNotificationConfigParams,
GetTaskPushNotificationConfigRequest,
GetTaskPushNotificationConfigResponse,
Expand Down Expand Up @@ -1189,3 +1193,59 @@ async def test_on_delete_push_notification_error(self) -> None:
# Assert
self.assertIsInstance(response.root, JSONRPCErrorResponse)
self.assertEqual(response.root.error, UnsupportedOperationError()) # type: ignore

async def test_get_authenticated_extended_card_success(self) -> None:
"""Test successful retrieval of the authenticated extended agent card."""
# Arrange
mock_request_handler = AsyncMock(spec=DefaultRequestHandler)
mock_extended_card = AgentCard(
name='Extended Card',
description='More details',
url='http://agent.example.com/api',
version='1.1',
capabilities=AgentCapabilities(),
default_input_modes=['text/plain'],
default_output_modes=['application/json'],
skills=[],
)
handler = JSONRPCHandler(
self.mock_agent_card,
mock_request_handler,
extended_agent_card=mock_extended_card,
)
request = GetAuthenticatedExtendedCardRequest(id='ext-card-req-1')
call_context = ServerCallContext(state={'foo': 'bar'})

# Act
response: GetAuthenticatedExtendedCardResponse = (
await handler.get_authenticated_extended_card(request, call_context)
)

# Assert
self.assertIsInstance(
response.root, GetAuthenticatedExtendedCardSuccessResponse
)
self.assertEqual(response.root.id, 'ext-card-req-1')
self.assertEqual(response.root.result, mock_extended_card)

async def test_get_authenticated_extended_card_not_configured(self) -> None:
"""Test error when authenticated extended agent card is not configured."""
# Arrange
mock_request_handler = AsyncMock(spec=DefaultRequestHandler)
handler = JSONRPCHandler(
self.mock_agent_card, mock_request_handler, extended_agent_card=None
)
request = GetAuthenticatedExtendedCardRequest(id='ext-card-req-2')
call_context = ServerCallContext(state={'foo': 'bar'})

# Act
response: GetAuthenticatedExtendedCardResponse = (
await handler.get_authenticated_extended_card(request, call_context)
)

# Assert
self.assertIsInstance(response.root, JSONRPCErrorResponse)
self.assertEqual(response.root.id, 'ext-card-req-2')
self.assertIsInstance(
response.root.error, AuthenticatedExtendedCardNotConfiguredError
)
Loading