|
44 | 44 | import typing |
45 | 45 | import warnings |
46 | 46 | from concurrent.futures import ThreadPoolExecutor |
47 | | -from typing import TYPE_CHECKING, Any |
| 47 | +from typing import TYPE_CHECKING, Any, Literal |
48 | 48 |
|
49 | 49 | from adcp.decisioning.account_projection import ( |
50 | 50 | strip_credentials_from_wire_result, |
@@ -1059,6 +1059,7 @@ def _build_request_context( |
1059 | 1059 | # Local import keeps the layering local — read the bearer ContextVar |
1060 | 1060 | # without forcing a top-level dep on adcp.server.auth. |
1061 | 1061 | from adcp.server.auth import current_principal as _current_principal |
| 1062 | + from adcp.server.auth import current_transport as _current_transport |
1062 | 1063 |
|
1063 | 1064 | if auth_info is None: |
1064 | 1065 | bearer_principal = _current_principal.get() |
@@ -1092,14 +1093,40 @@ def _build_request_context( |
1092 | 1093 | else: |
1093 | 1094 | caller_identity = tool_ctx.caller_identity |
1094 | 1095 |
|
| 1096 | + # Extract transport from metadata. In production paths RequestMetadata |
| 1097 | + # always populates metadata["transport"] before calling the context |
| 1098 | + # factory; None here means a test fixture supplied a bare ToolContext. |
| 1099 | + raw_transport = tool_ctx.metadata.get("transport") |
| 1100 | + if raw_transport not in ("mcp", "a2a", None): |
| 1101 | + raise ValueError( |
| 1102 | + f"metadata['transport'] must be 'mcp', 'a2a', or absent; got {raw_transport!r}" |
| 1103 | + ) |
| 1104 | + transport: Literal["mcp", "a2a"] | None = raw_transport |
| 1105 | + |
| 1106 | + # Set the ContextVar for code outside the handler call stack (webhook |
| 1107 | + # services, background helpers) that don't receive a RequestContext. |
| 1108 | + # No reset token is saved: asyncio tasks each get their own context |
| 1109 | + # copy, so set() is task-scoped and doesn't bleed across requests. |
| 1110 | + # Callers that need the previous value must save/restore it themselves |
| 1111 | + # (the test suite exercises this via asyncio.copy_context() isolation). |
| 1112 | + _current_transport.set(transport) |
| 1113 | + |
| 1114 | + # SDK-owned keys set by auth_context_factory / build_context examples |
| 1115 | + # ("transport", "tool_name") are framework-internal — strip them from |
| 1116 | + # the handler-visible metadata so adopters can't accidentally rely on |
| 1117 | + # undocumented dict paths and ctx.transport is the sole typed surface. |
| 1118 | + _sdk_metadata_keys = frozenset({"transport", "tool_name"}) |
| 1119 | + clean_metadata = {k: v for k, v in tool_ctx.metadata.items() if k not in _sdk_metadata_keys} |
| 1120 | + |
1095 | 1121 | # Build the RequestContext with the explicit state/resolve kwargs |
1096 | 1122 | # if provided; otherwise let the dataclass default factories |
1097 | 1123 | # supply the v6.0 stubs. |
1098 | 1124 | ctx_kwargs: dict[str, Any] = { |
1099 | 1125 | "request_id": tool_ctx.request_id, |
1100 | 1126 | "caller_identity": caller_identity, |
1101 | 1127 | "tenant_id": tool_ctx.tenant_id, |
1102 | | - "metadata": dict(tool_ctx.metadata), |
| 1128 | + "metadata": clean_metadata, |
| 1129 | + "transport": transport, |
1103 | 1130 | "account": account, |
1104 | 1131 | "auth_info": auth_info, |
1105 | 1132 | "auth_principal": auth_principal, |
|
0 commit comments