Skip to content

comply_test_controller's context_factory gets no request_context, so it sees no principal under stateful streamable-http #1821

Open

Description

@austinwiltshire

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

  1. serve(..., auth=BearerTokenAuth(...), context_factory=auth_context_factory, test_controller=store)
    with stateful streamable-http (the default).
  2. Call comply_test_controller with a valid bearer token, e.g. force_account_status.
  3. In the store, context.caller_identity is None, 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions