@@ -810,50 +810,63 @@ def _validate_card_url(url: str) -> str:
810810 return url
811811
812812
813- def _wrap_with_per_request_card (
814- inner : Any ,
815- * ,
816- resolver : PublicUrlResolver ,
817- handler : ADCPHandler [Any ],
818- name : str ,
819- port : int ,
820- description : str | None ,
821- version : str ,
822- extra_skills : list [pb .AgentSkill ] | None ,
823- advertise_all : bool ,
824- push_notifications_supported : bool ,
825- auth : BearerTokenAuth | None ,
826- ) -> Any :
827- """Wrap an ASGI app to serve agent-card endpoints per-request.
813+ _CARD_PATHS : frozenset [str ] = frozenset ({"/.well-known/agent-card.json" , "/.well-known/agent.json" })
814+
815+
816+ class _PerRequestCardMiddleware :
817+ """ASGI middleware that serves agent-card endpoints per-request.
828818
829819 Intercepts GET ``/.well-known/agent-card.json`` and
830- ``/.well-known/agent.json``; all other requests pass through to the
831- inner app unchanged.
820+ ``/.well-known/agent.json``; all other scopes (including
821+ ``lifespan``) pass through unchanged.
832822
833- Used when :func:`create_a2a_server` receives a
834- :data:`PublicUrlResolver` callable — the a2a-sdk's
835- ``create_agent_card_routes`` bakes the card at construction time
836- and cannot surface per-request context .
823+ Installed via :meth:`starlette.applications.Starlette.add_middleware`
824+ so the wrapped object remains a Starlette app — its ``.router``
825+ stays reachable for lifespan composition in
826+ :func:`adcp.server.serve._serve_mcp_and_a2a` .
837827 """
838- import inspect
839-
840- from a2a .server .routes .agent_card_routes import agent_card_to_dict # type: ignore[attr-defined]
841- from starlette .requests import Request
842- from starlette .responses import JSONResponse
843828
844- _card_paths : frozenset [str ] = frozenset (
845- {"/.well-known/agent-card.json" , "/.well-known/agent.json" }
846- )
829+ def __init__ (
830+ self ,
831+ app : Any ,
832+ * ,
833+ resolver : PublicUrlResolver ,
834+ handler : ADCPHandler [Any ],
835+ name : str ,
836+ port : int ,
837+ description : str | None ,
838+ version : str ,
839+ extra_skills : list [pb .AgentSkill ] | None ,
840+ advertise_all : bool ,
841+ push_notifications_supported : bool ,
842+ auth : BearerTokenAuth | None ,
843+ ) -> None :
844+ self .app = app
845+ self .resolver = resolver
846+ self .handler = handler
847+ self .name = name
848+ self .port = port
849+ self .description = description
850+ self .version = version
851+ self .extra_skills = extra_skills
852+ self .advertise_all = advertise_all
853+ self .push_notifications_supported = push_notifications_supported
854+ self .auth = auth
855+
856+ async def __call__ (self , scope : Any , receive : Any , send : Any ) -> None :
857+ import inspect
858+
859+ from starlette .requests import Request
860+ from starlette .responses import JSONResponse
847861
848- async def _middleware (scope : Any , receive : Any , send : Any ) -> None :
849862 if (
850863 scope .get ("type" ) == "http"
851- and scope .get ("path" ) in _card_paths
864+ and scope .get ("path" ) in _CARD_PATHS
852865 and scope .get ("method" ) == "GET"
853866 ):
854867 request = Request (scope , receive )
855868 try :
856- raw_url : str | Awaitable [str ] = resolver (request )
869+ raw_url : str | Awaitable [str ] = self . resolver (request )
857870 if inspect .isawaitable (raw_url ):
858871 raw_url = await raw_url
859872 assert isinstance (raw_url , str )
@@ -866,23 +879,24 @@ async def _middleware(scope: Any, receive: Any, send: Any) -> None:
866879 await error_response (scope , receive , send )
867880 return
868881 card = _build_agent_card (
869- handler ,
870- name = name ,
871- port = port ,
872- description = description ,
873- version = version ,
874- extra_skills = extra_skills ,
875- advertise_all = advertise_all ,
876- push_notifications_supported = push_notifications_supported ,
877- auth = auth ,
882+ self . handler ,
883+ name = self . name ,
884+ port = self . port ,
885+ description = self . description ,
886+ version = self . version ,
887+ extra_skills = self . extra_skills ,
888+ advertise_all = self . advertise_all ,
889+ push_notifications_supported = self . push_notifications_supported ,
890+ auth = self . auth ,
878891 public_url = url ,
879892 )
893+ from a2a .server .routes import agent_card_routes as _card_routes_mod
894+
895+ agent_card_to_dict = _card_routes_mod .agent_card_to_dict # type: ignore[attr-defined]
880896 card_response : Any = JSONResponse (agent_card_to_dict (card ))
881897 await card_response (scope , receive , send )
882898 return
883- await inner (scope , receive , send )
884-
885- return _middleware
899+ await self .app (scope , receive , send )
886900
887901
888902def create_a2a_server (
@@ -1085,7 +1099,7 @@ def agent_card_url(request: Request) -> str:
10851099 # DefaultRequestHandler's internal GetAgentCard RPC (buyers probe
10861100 # /.well-known/agent-card.json directly; the RPC fallback is rarely
10871101 # used). The well-known endpoints are served by
1088- # _wrap_with_per_request_card which builds a fresh card per GET.
1102+ # _PerRequestCardMiddleware which builds a fresh card per GET.
10891103 fallback_card = _build_agent_card (
10901104 handler ,
10911105 name = name ,
@@ -1109,9 +1123,13 @@ def agent_card_url(request: Request) -> str:
11091123 )
11101124 jsonrpc_kwargs ["request_handler" ] = request_handler
11111125 routes = list (create_jsonrpc_routes (** jsonrpc_kwargs ))
1126+ # Install the per-request card intercept via ``add_middleware``
1127+ # so ``app`` stays a Starlette instance — the unified-transport
1128+ # lifespan composer in ``serve._serve_mcp_and_a2a`` reaches
1129+ # ``a2a_inner.router.lifespan_context`` on this object.
11121130 app = Starlette (routes = routes )
1113- app = _wrap_with_per_request_card (
1114- app ,
1131+ app . add_middleware (
1132+ _PerRequestCardMiddleware ,
11151133 resolver = resolved_public_url ,
11161134 handler = handler ,
11171135 name = name ,
0 commit comments