Skip to content

fix(mcp): forward verified upstream token, not proxy reference JWT, to Cloud API - #1081

Merged
Aaron ("AJ") Steers (aaronsteers) merged 2 commits into
mainfrom
devin/1784148484-cloud-mcp-forward-verified-token
Jul 15, 2026
Merged

fix(mcp): forward verified upstream token, not proxy reference JWT, to Cloud API#1081
Aaron ("AJ") Steers (aaronsteers) merged 2 commits into
mainfrom
devin/1784148484-cloud-mcp-forward-verified-token

Conversation

@aaronsteers

@aaronsteers Aaron ("AJ") Steers (aaronsteers) commented Jul 15, 2026

Copy link
Copy Markdown
Member

Summary

Hosted Cloud MCP (cloud-mcp) was forwarding the wrong token to the Airbyte Cloud API, causing every authenticated Cloud tool call to 401 after credential resolution succeeded.

cloud-mcp runs FastMCP's OAuthProxy/OIDCProxy, which brokers the caller's Keycloak → Okta login but then mints its own short-lived reference JWT for the MCP client (iss/aud = the MCP server itself, scope="email profile"). That reference JWT is what lands in the raw Authorization header. The Airbyte Cloud API never issued it, so it rejects it with 401.

The bug was one of source precedence in how the downstream bearer was resolved. BEARER_TOKEN_CONFIG_ARG declared http_header_key=MCP_BEARER_TOKEN_HEADER, and fastmcp_extensions's resolver checks the HTTP header before the default. So the raw Authorization header (the reference JWT) always won over _resolve_transport_bearer_token, which returns the verified token from get_access_token().

FastMCP already does the right thing internally — OAuthProxy.load_access_token() swaps the reference JWT for the upstream Airbyte access token and exposes it via get_access_token(). This is the same class of token the Ops Webapp uses successfully against the Cloud public/config APIs. The fix simply forwards that verified token instead of the raw header.

 BEARER_TOKEN_CONFIG_ARG = MCPServerConfigArg(
     name=MCP_CONFIG_BEARER_TOKEN,
-    http_header_key=MCP_BEARER_TOKEN_HEADER,   # raw header (reference JWT) won → 401
     env_var=CLOUD_BEARER_TOKEN_ENV_VAR,
     normalize_fn=_normalize_bearer_token,
     default=_resolve_transport_bearer_token,   # now the primary source
     ...
 )
def _resolve_transport_bearer_token() -> str:
    access_token = get_access_token()          # verified upstream Airbyte token
    if access_token and access_token.token:
        return access_token.token
    # fallback: raw Authorization header, for servers with no transport auth
    # provider (client passes a real Airbyte token directly)
    headers = get_http_headers()
    ...
    return ""

Precedence after this change: explicit AIRBYTE_CLOUD_BEARER_TOKEN env override → verified transport token (get_access_token) → raw Authorization header fallback → empty (falls through to client-credentials auth for stdio/local).

Why the verified token is the right one (source trace)

  • OIDCProxy._get_verification_token() returns upstream_token_set.access_token.
  • JWTVerifier.verify_token(token) returns AccessToken(token=token, ...).
  • Therefore get_access_token().token is the upstream Airbyte access token, not the proxy's minted reference JWT.

Test Plan

Added tests/unit_tests/test_mcp_tool_utils.py::test_resolve_transport_bearer_token (parametrized):

  • verified token present + Authorization header present → returns the verified token (regression guard for the 401)
  • no verified token + Bearer <tok> header → returns <tok>
  • no verified token + bare-token header (case-insensitive) → returned as-is
  • empty verified token + header present → falls back to header
  • neither present → ""

Local checks (all green):

  • uv run --no-sync pytest tests/unit_tests/test_mcp_tool_utils.py → 11 passed
  • uv run --no-sync ruff format / ruff check (changed files) → clean
  • uv run --no-sync pyrefly check airbyte/mcp/_tool_utils.py → 0 errors

Live end-to-end confirmation (verified upstream token accepted by the Cloud API) will come from a hosted retest once this deploys, or a local OAuth round trip — the source trace above establishes the correct token now flows.

Requested by AJ Steers (Aaron ("AJ") Steers (@aaronsteers)).

Link to Devin session: https://app.devin.ai/sessions/9274bd4f3e0a4b0880795fc1ef9c806b

Summary by CodeRabbit

  • Bug Fixes
    • Improved bearer-token resolution by prioritizing verified transport authentication credentials.
    • When no verified token is available, the system now falls back to the request’s Authorization header (case-insensitive lookup).
    • Supports bearer tokens both with and without the Bearer prefix, including correct empty-token behavior.
  • Tests
    • Added unit test coverage for token precedence, header variations, and fallback/empty scenarios.

…o Cloud API

The downstream Airbyte Cloud bearer was resolved from the raw Authorization header first. Behind FastMCP's OAuthProxy/OIDCProxy that header carries the proxy's self-minted reference JWT (iss/aud = the MCP server), which the Airbyte Cloud API rejects with 401. FastMCP already swaps in the verified upstream Airbyte token and exposes it via get_access_token(); drop the header-first precedence so that verified token is forwarded, keeping the raw header only as a fallback for servers with no transport auth provider.

Co-Authored-By: AJ Steers <aj@airbyte.io>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review July 15, 2026 20:48
@github-actions

Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This PyAirbyte Version

You can test this version of PyAirbyte using the following:

# Run PyAirbyte CLI from this branch:
uvx --from 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1784148484-cloud-mcp-forward-verified-token' pyairbyte --help

# Install PyAirbyte from this branch for development:
pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1784148484-cloud-mcp-forward-verified-token'

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /fix-pr - Fixes most formatting and linting issues
  • /uv-lock - Updates uv.lock file
  • /test-pr - Runs tests with the updated PyAirbyte
  • /prerelease - Builds and publishes a prerelease version to PyPI
📚 Show Repo Guidance

Helpful Resources

Community Support

Questions? Join the #pyairbyte channel in our Slack workspace.

📝 Edit this welcome message.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment thread airbyte/mcp/_tool_utils.py Outdated
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 565f5040-da96-4471-8013-461aed42cfff

📥 Commits

Reviewing files that changed from the base of the PR and between 6dd6f7d and 82962d0.

📒 Files selected for processing (2)
  • airbyte/mcp/_tool_utils.py
  • tests/unit_tests/test_mcp_tool_utils.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit_tests/test_mcp_tool_utils.py
  • airbyte/mcp/_tool_utils.py

📝 Walkthrough

Walkthrough

The MCP bearer-token resolver now prioritizes verified transport access tokens, falls back to normalized Authorization headers, and returns an empty string when neither source provides a token. Unit tests cover precedence, case-insensitive lookup, prefix handling, and empty cases.

Changes

Bearer token resolution

Layer / File(s) Summary
Resolver and configuration update
airbyte/mcp/_tool_utils.py
_resolve_transport_bearer_token checks the verified access token before consulting request headers, while BEARER_TOKEN_CONFIG_ARG delegates header fallback to the resolver.
Resolver behavior tests
tests/unit_tests/test_mcp_tool_utils.py
Parameterized tests validate verified-token precedence, case-insensitive header lookup, bare and prefixed tokens, and empty fallback behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BEARER_TOKEN_CONFIG_ARG
  participant _resolve_transport_bearer_token
  participant get_access_token
  participant get_http_headers
  BEARER_TOKEN_CONFIG_ARG->>_resolve_transport_bearer_token: Resolve default bearer token
  _resolve_transport_bearer_token->>get_access_token: Request verified access token
  get_access_token-->>_resolve_transport_bearer_token: Return token or empty
  _resolve_transport_bearer_token->>get_http_headers: Read Authorization header if needed
  get_http_headers-->>_resolve_transport_bearer_token: Return header token or empty
  _resolve_transport_bearer_token-->>BEARER_TOKEN_CONFIG_ARG: Return normalized token
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: preferring the verified upstream token over the proxy-issued JWT for Cloud API auth.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1784148484-cloud-mcp-forward-verified-token

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

get_http_headers strips the authorization header by default, so the raw
header fallback never saw a token. Explicitly opt in via
include={"authorization"} and assert the opt-in in the regression test.

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copilot AI review requested due to automatic review settings July 15, 2026 20:54
@aaronsteers
Aaron ("AJ") Steers (aaronsteers) removed the request for review from Copilot July 15, 2026 20:54
@github-code-quality

github-code-quality Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Python

Python / code-coverage/pytest-fast

The overall coverage in the devin/1784148484-clo... branch is 67%. The coverage in the main branch is 65%.

Show a code coverage summary of the most impacted files.
File main d9f652f devin/1784148484-clo... 82962d0 +/-
airbyte/_util/api_util.py 36% 37% +1%
airbyte/mcp/_tool_utils.py 72% 79% +7%
airbyte/mcp/registry.py 53% 70% +17%
airbyte/mcp/int...c_history_ui.py 0% 36% +36%
airbyte/mcp/int...hared_models.py 0% 81% +81%
airbyte/cloud/models.py 0% 91% +91%
airbyte/mcp/int..._registry_ui.py 0% 92% +92%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%
airbyte/mcp/int...ive/__init__.py 0% 100% +100%
airbyte/mcp/int...tive/_prefab.py 0% 100% +100%

Python / code-coverage/pytest-no-creds

The overall coverage in the devin/1784148484-clo... branch is 67%. The coverage in the main branch is 65%.

Show a code coverage summary of the most impacted files.
File main d9f652f devin/1784148484-clo... 82962d0 +/-
airbyte/_util/api_util.py 36% 37% +1%
airbyte/mcp/_tool_utils.py 72% 79% +7%
airbyte/mcp/registry.py 53% 70% +17%
airbyte/mcp/int...c_history_ui.py 0% 36% +36%
airbyte/mcp/int...hared_models.py 0% 81% +81%
airbyte/cloud/models.py 0% 91% +91%
airbyte/mcp/int..._registry_ui.py 0% 92% +92%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%
airbyte/mcp/int...ive/__init__.py 0% 100% +100%
airbyte/mcp/int...tive/_prefab.py 0% 100% +100%

Python / code-coverage/pytest

The overall coverage in the devin/1784148484-clo... branch is 72%. The coverage in the main branch is 71%.

Show a code coverage summary of the most impacted files.
File main d9f652f devin/1784148484-clo... 82962d0 +/-
airbyte/registry.py 70% 72% +2%
airbyte/mcp/_tool_utils.py 72% 79% +7%
airbyte/mcp/registry.py 53% 70% +17%
airbyte/mcp/int...c_history_ui.py 0% 36% +36%
airbyte/mcp/int...hared_models.py 0% 81% +81%
airbyte/mcp/int..._registry_ui.py 0% 92% +92%
airbyte/cloud/models.py 0% 93% +93%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%
airbyte/mcp/int...ive/__init__.py 0% 100% +100%
airbyte/mcp/int...tive/_prefab.py 0% 100% +100%

Updated July 15, 2026 21:20 UTC
Code Coverage is in Public Preview. Learn more and provide us with your feedback.

@aaronsteers
Aaron ("AJ") Steers (aaronsteers) merged commit 1d9b3ca into main Jul 15, 2026
22 checks passed
@aaronsteers
Aaron ("AJ") Steers (aaronsteers) deleted the devin/1784148484-cloud-mcp-forward-verified-token branch July 15, 2026 22:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant