Repository navigation
Description
Summary
register_test_controller calls the seller's context_factory with a RequestMetadata that has no
request_context. Every other MCP tool gets the in-flight Starlette request there. So under
stateful streamable-http, auth_context_factory finds no principal for comply_test_controller:
caller_identity is None, and metadata["adcp.auth_info"] is never set. A controller store that
scopes accounts or media buys by principal cannot find any, and the sandbox gate's account resolver
receives auth_info=None.
Versions
Present in v9.5.0 (latest stable), v10.0.0-beta.8, and main. Observed on 6.4.0.
-
src/adcp/server/test_controller.py(main, line 1522; v10.0.0-beta.8, line 1673):meta = _RequestMetadata(tool_name="comply_test_controller", transport="mcp") context = context_factory(meta)
-
Every other tool,
src/adcp/server/serve.py:request_context = _get_starlette_request_for_dispatch() meta = RequestMetadata(tool_name=name, transport="mcp", request_context=request_context) context = context_factory(meta)
auth_context_factory's docstring explains why that matters: its ContextVar fallback "works only when
the dispatch runs in the same async task as the middleware (i.e., stateless streamable-http and
A2A). In stateful streamable-http, these read None."
Reproduce
serve(..., auth=BearerTokenAuth(...), context_factory=auth_context_factory, test_controller=store)
with stateful streamable-http (the default).- Call
comply_test_controllerwith a valid bearer token, e.g.force_account_status. - In the store,
context.caller_identityisNone, though the same token on any other tool gives
the principal.
Proposed fix
Thread the request the same way serve.py does:
from adcp.server.serve import _get_starlette_request_for_dispatch
async def comply_test_controller(**kwargs: Any) -> dict[str, Any]:
...
if context_factory is not None:
meta = _RequestMetadata(
tool_name="comply_test_controller",
transport="mcp",
request_context=_get_starlette_request_for_dispatch(),
)
context = context_factory(meta)A regression test: build the app with BearerTokenAuth and auth_context_factory, call
comply_test_controller over stateful streamable-http with a token, and assert that the store's
context.caller_identity is the token's principal.
Workaround
Wrap context_factory to fill in the request when it is missing:
import dataclasses
from adcp.server import auth_context_factory
from adcp.server.serve import RequestMetadata, _get_starlette_request_for_dispatch
def context_factory(meta: RequestMetadata) -> ToolContext:
if meta.request_context is None:
meta = dataclasses.replace(meta, request_context=_get_starlette_request_for_dispatch())
return auth_context_factory(meta)With this in place, the 3.1.1 media_buy_seller/delivery_reporting storyboard passes on our seller.
We had excluded it because our store finds media buys by principal, and the controller had none.
Activity