2525
2626from __future__ import annotations
2727
28+ import warnings
2829from contextlib import asynccontextmanager
2930from typing import TYPE_CHECKING , Any , Literal
3031from urllib .parse import urlparse
4950 )
5051 from adcp .server .auth import BearerTokenAuth
5152 from adcp .server .helpers import ResponseEnhancer
52- from adcp .server .serve import ASGIMiddlewareEntry , ContextFactory , SkillMiddleware
53+ from adcp .server .serve import (
54+ ASGIMiddlewareEntry ,
55+ ContextFactory ,
56+ LifespanHook ,
57+ SkillMiddleware ,
58+ )
5359 from adcp .server .spec_compat import PreValidationHooks
5460
5561
@@ -148,12 +154,17 @@ def build_asgi_app(
148154 context_factory : ContextFactory | None = None ,
149155 middleware : Sequence [SkillMiddleware ] | None = None ,
150156 streaming_responses : bool = False ,
157+ stateless_http : bool = False ,
158+ session_idle_timeout : float | None = 1800.0 ,
159+ max_active_sessions : int | None = None ,
151160 enable_dns_rebinding_protection : bool | None = None ,
152161 max_request_size : int | None = None ,
153162 validation : ValidationHookConfig | None = DEFAULT_VALIDATION ,
154163 discovery_base_url : str | None = None ,
155164 pre_validation_hooks : PreValidationHooks | None = None ,
156165 response_enhancer : ResponseEnhancer | None = None ,
166+ on_startup : Sequence [LifespanHook ] | None = None ,
167+ on_shutdown : Sequence [LifespanHook ] | None = None ,
157168 ** factory_kwargs : Any ,
158169) -> Any :
159170 """Build a Starlette ASGI app for in-process integration tests.
@@ -219,6 +230,15 @@ def build_asgi_app(
219230 every tool dispatch. Forwarded to :func:`create_mcp_server`.
220231 :param streaming_responses: Forwarded to :func:`create_mcp_server`.
221232 Default ``False``.
233+ :param stateless_http: Forwarded to :func:`create_mcp_server` for
234+ ``transport="mcp"`` and to the production composition path for
235+ ``transport="both"``. Ignored by ``transport="a2a"``.
236+ :param session_idle_timeout: Idle reap deadline for stateful MCP
237+ sessions. Defaults to 1800 seconds. Forwarded for ``"mcp"`` and
238+ ``"both"``; ignored by ``"a2a"``.
239+ :param max_active_sessions: Optional cap for active stateful MCP
240+ sessions. Forwarded for ``"mcp"`` and ``"both"``; ignored by
241+ ``"a2a"``.
222242 :param enable_dns_rebinding_protection: Forwarded to
223243 :func:`create_mcp_server`. ``None`` → FastMCP default.
224244 :param max_request_size: Request body size cap in bytes. ``None`` →
@@ -244,6 +264,10 @@ def build_asgi_app(
244264 :func:`create_mcp_server`, so in-process tests exercise the same
245265 enhancer wiring your production :func:`serve` call uses. ``None``
246266 → no enhancer (default).
267+ :param on_startup: Async zero-argument hooks run after the MCP and A2A
268+ framework lifespans start. Requires ``transport="both"``.
269+ :param on_shutdown: Async zero-argument hooks run before the MCP and A2A
270+ framework lifespans stop. Requires ``transport="both"``.
247271 :param factory_kwargs: Forwarded to
248272 :func:`create_adcp_server_from_platform`. Accepted keys:
249273 ``executor``, ``registry``, ``webhook_sender``,
@@ -257,6 +281,27 @@ def build_asgi_app(
257281 """
258282 if transport not in ("mcp" , "a2a" , "both" ):
259283 raise ValueError (f"Unsupported transport { transport !r} ; expected 'mcp', 'a2a', or 'both'." )
284+ if (on_startup or on_shutdown ) and transport != "both" :
285+ raise ValueError (
286+ "on_startup / on_shutdown hooks require transport='both', "
287+ f"got transport={ transport !r} ."
288+ )
289+ if transport == "a2a" :
290+ ignored_session_settings = []
291+ if stateless_http :
292+ ignored_session_settings .append ("stateless_http" )
293+ if session_idle_timeout != 1800.0 :
294+ ignored_session_settings .append ("session_idle_timeout" )
295+ if max_active_sessions is not None :
296+ ignored_session_settings .append ("max_active_sessions" )
297+ if ignored_session_settings :
298+ warnings .warn (
299+ "build_asgi_app sets MCP-only session fields "
300+ f"{ sorted (ignored_session_settings )} but transport='a2a'. "
301+ "These fields will be ignored." ,
302+ UserWarning ,
303+ stacklevel = 2 ,
304+ )
260305
261306 from adcp .decisioning .serve import create_adcp_server_from_platform
262307 from adcp .server .serve import (
@@ -291,6 +336,9 @@ def build_asgi_app(
291336 advertise_all = advertise_all ,
292337 max_request_size = max_request_size ,
293338 streaming_responses = streaming_responses ,
339+ stateless_http = stateless_http ,
340+ session_idle_timeout = session_idle_timeout ,
341+ max_active_sessions = max_active_sessions ,
294342 validation = validation ,
295343 pre_validation_hooks = pre_validation_hooks ,
296344 response_enhancer = response_enhancer ,
@@ -299,6 +347,8 @@ def build_asgi_app(
299347 allowed_origins = allowed_origins ,
300348 enable_dns_rebinding_protection = enable_dns_rebinding_protection ,
301349 auth = auth ,
350+ on_startup = on_startup ,
351+ on_shutdown = on_shutdown ,
302352 include_discovery = discovery_base_url is not None ,
303353 )
304354 return _apply_asgi_middleware (app , asgi_middleware )
@@ -331,6 +381,9 @@ def build_asgi_app(
331381 context_factory = context_factory ,
332382 middleware = middleware ,
333383 streaming_responses = streaming_responses ,
384+ stateless_http = stateless_http ,
385+ session_idle_timeout = session_idle_timeout ,
386+ max_active_sessions = max_active_sessions ,
334387 enable_dns_rebinding_protection = enable_dns_rebinding_protection ,
335388 validation = validation ,
336389 pre_validation_hooks = pre_validation_hooks ,
@@ -373,12 +426,17 @@ async def build_test_client(
373426 context_factory : ContextFactory | None = None ,
374427 middleware : Sequence [SkillMiddleware ] | None = None ,
375428 streaming_responses : bool = False ,
429+ stateless_http : bool = False ,
430+ session_idle_timeout : float | None = 1800.0 ,
431+ max_active_sessions : int | None = None ,
376432 enable_dns_rebinding_protection : bool | None = None ,
377433 max_request_size : int | None = None ,
378434 validation : ValidationHookConfig | None = DEFAULT_VALIDATION ,
379435 discovery_base_url : str | None = None ,
380436 pre_validation_hooks : PreValidationHooks | None = None ,
381437 response_enhancer : ResponseEnhancer | None = None ,
438+ on_startup : Sequence [LifespanHook ] | None = None ,
439+ on_shutdown : Sequence [LifespanHook ] | None = None ,
382440 ** factory_kwargs : Any ,
383441) -> AsyncIterator [httpx .AsyncClient ]:
384442 """Async context manager yielding an ``httpx.AsyncClient`` wired against
@@ -427,6 +485,9 @@ async def build_test_client(
427485 :param context_factory: Forwarded to :func:`build_asgi_app`.
428486 :param middleware: Forwarded to :func:`build_asgi_app`.
429487 :param streaming_responses: Forwarded to :func:`build_asgi_app`.
488+ :param stateless_http: Forwarded to :func:`build_asgi_app`.
489+ :param session_idle_timeout: Forwarded to :func:`build_asgi_app`.
490+ :param max_active_sessions: Forwarded to :func:`build_asgi_app`.
430491 :param enable_dns_rebinding_protection: Forwarded to
431492 :func:`build_asgi_app`.
432493 :param max_request_size: Forwarded to :func:`build_asgi_app`.
@@ -442,6 +503,10 @@ async def build_test_client(
442503 :param response_enhancer: Forwarded to :func:`build_asgi_app`. Wire
443504 the same enhancer your production :func:`serve` call uses so
444505 in-process tests exercise the enhancer path.
506+ :param on_startup: Forwarded to :func:`build_asgi_app`. Requires
507+ ``transport="both"``.
508+ :param on_shutdown: Forwarded to :func:`build_asgi_app`. Requires
509+ ``transport="both"``.
445510 :param factory_kwargs: Forwarded to
446511 :func:`create_adcp_server_from_platform` via :func:`build_asgi_app`
447512 (executor, registry, webhook_sender, etc.).
@@ -492,12 +557,17 @@ async def build_test_client(
492557 context_factory = context_factory ,
493558 middleware = middleware ,
494559 streaming_responses = streaming_responses ,
560+ stateless_http = stateless_http ,
561+ session_idle_timeout = session_idle_timeout ,
562+ max_active_sessions = max_active_sessions ,
495563 enable_dns_rebinding_protection = enable_dns_rebinding_protection ,
496564 max_request_size = max_request_size ,
497565 validation = validation ,
498566 discovery_base_url = discovery_base_url ,
499567 pre_validation_hooks = pre_validation_hooks ,
500568 response_enhancer = response_enhancer ,
569+ on_startup = on_startup ,
570+ on_shutdown = on_shutdown ,
501571 ** factory_kwargs ,
502572 )
503573 async with LifespanManager (app ):
0 commit comments