Skip to content

feat(mcp): reuse caller's verified token for Airbyte Cloud API passthrough - #1078

Closed
Aaron ("AJ") Steers (aaronsteers) wants to merge 2 commits into
mainfrom
devin/1784059452-cloud-mcp-token-passthrough
Closed

feat(mcp): reuse caller's verified token for Airbyte Cloud API passthrough#1078
Aaron ("AJ") Steers (aaronsteers) wants to merge 2 commits into
mainfrom
devin/1784059452-cloud-mcp-token-passthrough

Conversation

@aaronsteers

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

Copy link
Copy Markdown
Member

Summary

The Cloud MCP (hosted at cloud-mcp) should call the Airbyte Cloud API as the caller, not solely as a statically-configured application. When an agent authenticates the MCP transport with an Airbyte-Cloud-issued bearer token (verified against the _airbyte-application-clients realm when MCP_AUTH_AIRBYTE_CLOUD=true), that same JWT is itself a valid Cloud API bearer — so it should flow through to downstream Cloud calls, delegating them to the caller's identity. Configured client_id/client_secret remain the fallback for stdio/local mode where there is no caller token.

The transport-side wiring for this already landed in #1073 (_resolve_transport_bearer_tokenBEARER_TOKEN_CONFIG_ARG defaulting to the verified transport token, plus the Airbyte Cloud realm defaults). The missing piece was the "bearer wins over client creds" resolution at the point the MCP tools build a CloudClient.

_get_cloud_client read all three of bearer_token, client_id, client_secret from MCP config and passed them together to CloudClient. But CloudClient (via _AirbyteCredentials.from_auth) rejects receiving both a bearer token and client credentials at once. In hosted mode — transport token present and AIRBYTE_CLOUD_CLIENT_ID/SECRET set in the environment — this raised Cannot use both client credentials and bearer token authentication, breaking the passthrough.

This PR makes the MCP client-construction helper prefer the bearer token, mirroring Ops MCP's _get_access_token ("if bearer_token is provided, use it directly; otherwise exchange client credentials"):

# airbyte/mcp/cloud.py :: _get_cloud_client
bearer_token = get_mcp_config(ctx, MCP_CONFIG_BEARER_TOKEN) or None
client_id = get_mcp_config(ctx, MCP_CONFIG_CLIENT_ID) or None
client_secret = get_mcp_config(ctx, MCP_CONFIG_CLIENT_SECRET) or None
...
if bearer_token:            # bearer wins; CloudClient rejects both at once
    client_id = None
    client_secret = None
return CloudClient(client_id=..., client_secret=..., bearer_token=bearer_token, ...)

How it mirrors Ops MCP

  • Realm-verified token + BEARER_TOKEN config arg defaulting to the transport token — already present in PyAirbyte from feat(mcp): support headless bearer-token transport auth #1073 (server.py::_create_auth, _tool_utils.py::_resolve_transport_bearer_token / BEARER_TOKEN_CONFIG_ARG), identical in shape to airbyte_ops_mcp/mcp/server.py.
  • _get_access_token-style "bearer wins over client creds" — Ops MCP centralizes this in cloud_admin/api_client.py::_get_access_token. In PyAirbyte the low-level api_util.get_airbyte_server_instance intentionally requires exactly one of bearer vs. client creds (a documented CloudClient/CloudClientConfig invariant), so the analogous, scoped place to apply "bearer wins" is the MCP client-construction helper _get_cloud_client. This keeps the change confined to the MCP auth layer without altering the public Cloud client's mutual-exclusivity contract.

Expected behavior

  • Hosted (cloud-mcp): caller's verified Airbyte Cloud token → used directly as the Cloud API bearer (delegated identity). Env client creds present but ignored while a token is passed.
  • stdio / local: no transport token (_resolve_transport_bearer_token returns "") → falls back to AIRBYTE_CLOUD_CLIENT_ID/AIRBYTE_CLOUD_CLIENT_SECRET.

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

Test Plan

Added focused unit tests:

  • tests/unit_tests/test_mcp_cloud.py::test_get_cloud_client_prefers_bearer_token_over_client_credentials — parametrized: bearer token present → CloudClient gets the bearer and client_id/client_secret are dropped (no mutual-exclusivity error); empty bearer (stdio) → falls back to client creds.
  • tests/unit_tests/test_mcp_tool_utils.py::test_resolve_transport_bearer_token — parametrized: get_access_token() returns None (stdio) or an empty token → ""; a verified token → returned as-is.

Local checks (all green):

  • uv run ruff format --check . (changed files)
  • uv run ruff check . (changed files)
  • uv run pyrefly check (0 errors; the 3 warnings are pre-existing redundant-cast in cloud.py)
  • uv run pytest tests/unit_tests/test_mcp_cloud.py tests/unit_tests/test_mcp_tool_utils.py → 18 passed

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

Summary by CodeRabbit

  • Bug Fixes

    • Improved authentication handling for cloud connections by prioritizing bearer-token authentication when available.
    • Added a reliable fallback to client ID and secret authentication when no bearer token is provided.
    • Improved handling of empty or unavailable access tokens.
  • Tests

    • Added coverage for authentication precedence and access-token resolution scenarios.

…rough

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

@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/1784059452-cloud-mcp-token-passthrough' pyairbyte --help

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

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.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@devin-ai-integration[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b9a50707-67bb-4987-9c4c-ac738fb443e1

📥 Commits

Reviewing files that changed from the base of the PR and between 0fbe113 and 57b2462.

📒 Files selected for processing (2)
  • airbyte/mcp/cloud.py
  • tests/unit_tests/test_mcp_cloud.py
📝 Walkthrough

Walkthrough

MCP cloud client creation now gives bearer tokens precedence over client credentials, while empty tokens trigger credential fallback. Unit tests cover cloud-client selection and transport bearer-token resolution across missing, empty, and verified token cases.

Changes

MCP bearer authentication

Layer / File(s) Summary
Cloud client authentication precedence
airbyte/mcp/cloud.py, tests/unit_tests/test_mcp_cloud.py
_get_cloud_client normalizes authentication values, clears client credentials when a bearer token is present, and tests bearer-token precedence plus fallback behavior.
Transport token resolution validation
tests/unit_tests/test_mcp_tool_utils.py
Tests verify bearer-token resolution for missing, empty, and verified access tokens.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: reusing the caller's verified token for Airbyte Cloud API passthrough.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1784059452-cloud-mcp-token-passthrough

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.

@github-code-quality

github-code-quality Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Python

Python / code-coverage/pytest-fast

The overall coverage in the devin/1784059452-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/1784059452-clo... 57b2462 +/-
airbyte/mcp/cloud.py 52% 53% +1%
airbyte/_util/api_util.py 36% 37% +1%
airbyte/registry.py 70% 72% +2%
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/1784059452-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/1784059452-clo... 57b2462 +/-
airbyte/mcp/cloud.py 52% 53% +1%
airbyte/_util/api_util.py 36% 37% +1%
airbyte/registry.py 70% 72% +2%
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/1784059452-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/1784059452-clo... 57b2462 +/-
airbyte/mcp/cloud.py 52% 53% +1%
airbyte/registry.py 70% 72% +2%
airbyte/mcp/server.py 69% 78% +9%
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 14, 2026 21:15 UTC
Code Coverage is in Public Preview. Learn more and provide us with your feedback.

@aaronsteers
Aaron ("AJ") Steers (aaronsteers) marked this pull request as ready for review July 14, 2026 20:41
Copilot AI review requested due to automatic review settings July 14, 2026 20:41
Comment thread airbyte/mcp/cloud.py Outdated
Comment on lines +292 to +295
if bearer_token:
client_id = None
client_secret = None

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Before we throw away the client ID and client secret, should we check if the bearer token is even valid? If it's not valid, then using the client ID and secret instead to get a new token would be the preferred approach, rather than failing hard when all three are provided

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.

🙋 Human Input Needed: which fallback behavior do you want? Below is why I think the passthrough path is already safe, and the options for the config-injected-bearer edge case.

The passthrough path is already covered by transport verification. In hosted mode (MCP_AUTH_AIRBYTE_CLOUD=true), the JWTVerifier cryptographically validates the bearer (signature, issuer, audience, exp) before any tool runs. An expired/tampered/invalid token is rejected with a 401 at the transport layer, so _get_cloud_client never executes — by the time we drop the client creds, the token is guaranteed valid. This is the same guarantee Ops MCP relies on (its AGENTS.md matrix: no token → 401, valid → 200, tampered → 401), and its _get_access_token returns the bearer directly with no re-validation or fallback.

So the "invalid bearer + creds both present" case only arises off the passthrough path — i.e. when a bearer is injected via the AIRBYTE_CLOUD_BEARER_TOKEN env var / Authorization header on a server that isn't doing realm verification. That's an operator-config path, not the delegated-caller flow this PR targets.

Options if we want to harden that edge case:

  1. Local expiry check — decode the JWT exp and fall back to client creds if it's expired. Cheap and no network, but only catches expiry (not revocation/wrong-signature) and assumes the bearer is a decodable JWT rather than an opaque token.
  2. Runtime fallback — try the bearer, and on 401/403 retry with client creds. Most robust, but CloudClient/api_util has no such dual-credential retry today, so it's meaningfully more machinery and spans the whole cloud client — larger than this focused auth PR.
  3. Keep as-is — bearer wins, matching the Ops MCP reference exactly; track graceful fallback separately if/when we want to support the static-bearer-plus-creds config.

My recommendation: keep this PR at option 3 (scoped precedence fix that mirrors Ops MCP) and open a follow-up for option 1 or 2 if you want the config path hardened. But happy to fold option 1 in here if you'd prefer it in one go — it's the only self-contained one. Which way do you want to go?


Devin session

@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: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

Copilot AI 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.

Pull request overview

This PR fixes Cloud MCP authentication passthrough so that, when a verified transport bearer token is available, downstream Airbyte Cloud API calls are made as the caller (using that token) rather than failing due to mutually-exclusive auth inputs.

Changes:

  • Update _get_cloud_client to prefer bearer_token and drop client_id/client_secret when a bearer is present.
  • Add unit tests covering _get_cloud_client bearer-vs-client-credentials precedence.
  • Add unit tests for _resolve_transport_bearer_token behavior in stdio vs verified-token contexts.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
airbyte/mcp/cloud.py Prefer bearer-token auth for CloudClient construction to avoid mutual-exclusivity errors and enable delegated Cloud API calls.
tests/unit_tests/test_mcp_cloud.py Adds tests asserting bearer-token precedence and fallback to client credentials when bearer token is empty.
tests/unit_tests/test_mcp_tool_utils.py Adds tests for transport bearer-token resolution behavior (None/empty/verified token).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread airbyte/mcp/cloud.py Outdated
…ecedence

Co-Authored-By: AJ Steers <aj@airbyte.io>
Comment thread airbyte/mcp/cloud.py
Comment on lines +303 to +308
return CloudClient(
bearer_token=bearer_token,
public_api_root=api_url,
config_api_root=config_api_url,
organization_id=organization_id,
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I don't understand why we shouldn't keep the prior implementation and make CloudClient handle this for us.

    return CloudClient(
        client_id=client_id,
        client_secret=client_secret,
        bearer_token=bearer_token,
        public_api_root=api_url,
        config_api_root=config_api_url,
        organization_id=organization_id,
    )

I also don't understand the claim that the CloudClient class would fail auth or fail to auth in the prior implementaton. Everything appears to have been correct before this PR.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Closing per discussion with AJ (Devin session: https://app.devin.ai/sessions/a8d355a69ca34409bd3b13d2a8122123). This MCP-layer precedence fix was misguided: the pre-existing code already handled pure-passthrough and pure-stdio correctly. The real issue is that _AirbyteCredentials.from_auth raises Cannot use both... when a bearer token and client credentials are both present. The fix belongs there — gracefully preferring the bearer — so all CloudClient callers benefit. Opening a focused PR against airbyte/cloud/_credentials.py instead.

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.

2 participants