diff --git a/src/a2a/server/apps/jsonrpc/fastapi_app.py b/src/a2a/server/apps/jsonrpc/fastapi_app.py index 6d258cc9..579ff79c 100644 --- a/src/a2a/server/apps/jsonrpc/fastapi_app.py +++ b/src/a2a/server/apps/jsonrpc/fastapi_app.py @@ -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 diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py index b2e9ad4d..6f374848 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py @@ -32,6 +32,7 @@ AgentCard, CancelTaskRequest, DeleteTaskPushNotificationConfigRequest, + GetAuthenticatedExtendedCardRequest, GetTaskPushNotificationConfigRequest, GetTaskRequest, InternalError, @@ -39,6 +40,7 @@ JSONParseError, JSONRPCError, JSONRPCErrorResponse, + JSONRPCRequest, JSONRPCResponse, ListTaskPushNotificationConfigRequest, SendMessageRequest, @@ -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 @@ -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 @@ -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)}' @@ -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.'}, diff --git a/src/a2a/server/apps/jsonrpc/starlette_app.py b/src/a2a/server/apps/jsonrpc/starlette_app.py index b1a5d6e6..51974fc1 100644 --- a/src/a2a/server/apps/jsonrpc/starlette_app.py +++ b/src/a2a/server/apps/jsonrpc/starlette_app.py @@ -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( diff --git a/src/a2a/server/request_handlers/jsonrpc_handler.py b/src/a2a/server/request_handlers/jsonrpc_handler.py index 88a169fe..a0657859 100644 --- a/src/a2a/server/request_handlers/jsonrpc_handler.py +++ b/src/a2a/server/request_handlers/jsonrpc_handler.py @@ -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, @@ -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, @@ -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 + ) + ) diff --git a/tests/server/request_handlers/test_jsonrpc_handler.py b/tests/server/request_handlers/test_jsonrpc_handler.py index 5f3930c5..ef43b05f 100644 --- a/tests/server/request_handlers/test_jsonrpc_handler.py +++ b/tests/server/request_handlers/test_jsonrpc_handler.py @@ -27,11 +27,15 @@ AgentCapabilities, AgentCard, Artifact, + AuthenticatedExtendedCardNotConfiguredError, CancelTaskRequest, CancelTaskSuccessResponse, DeleteTaskPushNotificationConfigParams, DeleteTaskPushNotificationConfigRequest, DeleteTaskPushNotificationConfigSuccessResponse, + GetAuthenticatedExtendedCardRequest, + GetAuthenticatedExtendedCardResponse, + GetAuthenticatedExtendedCardSuccessResponse, GetTaskPushNotificationConfigParams, GetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigResponse, @@ -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 + )