Skip to content

fix(mcp): route interactive Cloud API reads through the Config API - #1090

Closed
Aaron ("AJ") Steers (aaronsteers) wants to merge 2 commits into
mainfrom
devin/1784919112-cloud-mcp-config-api-user-token
Closed

fix(mcp): route interactive Cloud API reads through the Config API#1090
Aaron ("AJ") Steers (aaronsteers) wants to merge 2 commits into
mainfrom
devin/1784919112-cloud-mcp-config-api-user-token

Conversation

@aaronsteers

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

Copy link
Copy Markdown
Member

Summary

Interactive OIDC logins to the hosted Cloud MCP produce a user-realm bearer token with no client credentials. The public API (api.airbyte.com/v1) only accepts application-client tokens, so authenticated cloud tool calls (check_airbyte_cloud_workspace, describe_cloud_connection) returned 401 after a successful interactive login. This routes those bearer-only reads through the Config API (cloud.airbyte.com/api/v1), which accepts user tokens, while leaving the public API path unchanged for headless application credentials.

Routing key: a workspace/connection is "bearer-only" when it has a bearer_token and no client_id/client_secret.

# CloudWorkspace
@property
def _uses_bearer_only_auth(self) -> bool:
    return self.bearer_token is not None and self.client_id is None and self.client_secret is None

def get_workspace_info(self) -> CloudWorkspaceInfo:
    if self._uses_bearer_only_auth:
        raw = api_util.get_workspace_via_config_api(...)   # POST /v1/workspaces/get
        return CloudWorkspaceInfo.from_mapping(raw)
    return CloudWorkspaceInfo.from_api_response(api_util.get_workspace(...))  # public API
# CloudConnection._fetch_connection_info
if self.workspace._uses_bearer_only_auth:
    raw = api_util.get_connection_via_config_api(...)      # POST /v1/web_backend/connections/get
    result = CloudConnectionInfo.from_config_api_response(raw, fallback_workspace_id=...)
else:
    result = CloudConnectionInfo.from_api_response(api_util.get_connection(...))  # public API

Both new api_util helpers reuse the existing _make_config_api_request() (which forwards a bearer token directly). No Airbyte-specific realm/deployment values are introduced — PyAirbyte stays provider-neutral; concrete realm/issuer/audience values remain owned by the deployment repo.

The Config API WebBackendConnectionRead response embeds the full source/destination objects and syncCatalog, so describe_cloud_connection now resolves connector names and stream names from that single response instead of issuing extra public API connector lookups. CloudConnectionInfo gains optional source_name, destination_name, and stream_names fields (populated only on the Config API path); configurations is now optional (None on the Config API path). The source_name/destination_name/stream_names properties on CloudConnection prefer the embedded values and fall back to the prior public API lookups.

Test plan

  • New tests/unit_tests/test_cloud_config_api_routing.py: _uses_bearer_only_auth truth table; CloudConnectionInfo.from_config_api_response parsing (ids, names, streams, prefix, status, workspace-id fallback); CloudWorkspaceInfo.from_mapping; and routing assertions that bearer-only auth hits the Config API helpers while client-credential auth hits the public API helpers.
  • ruff format, ruff check, and pyrefly check clean on touched files; existing test_mcp_cloud.py, test_cloud_api_util.py, test_cloud_credentials.py, test_cloud_api_roots.py pass.
  • End-to-end validation against a deployed cloud-mcp-preview via Goose Desktop (interactive OIDC as airbyte-support-bot@airbyte.io) is pending as a follow-up on the deployment side.

Link to Devin session: https://app.devin.ai/sessions/a5b9501ef92c412aad0408b7c74ef9c8
Requested by: Aaron ("AJ") Steers (@aaronsteers)

Summary by CodeRabbit

  • New Features
    • Added support for retrieving workspace and connection details via the Config API when using bearer-only (interactive OIDC) authentication.
    • Connection metadata and stream listing now prefer embedded source/destination names and stream names when available, with graceful fallbacks.
  • Bug Fixes
    • Improved connection/workspace visibility for bearer-only auth scenarios that the public API can’t handle.
  • Tests
    • Added unit coverage for auth-based API routing and Config API metadata parsing, including stream selection and fallback workspace ID handling.

Interactive OIDC logins produce a user-realm bearer token with no client
credentials. The public API (api.airbyte.com) rejects such tokens, so
workspace and connection metadata reads returned 401. Route bearer-only
reads through the Config API (which accepts user tokens) while keeping the
public API path for application client credentials.

The Config API connection response embeds the source/destination objects
and sync catalog, so connector names and stream names resolve from one
response instead of extra public API lookups.

Co-Authored-By: AJ Steers <aj@airbyte.io>
Copilot AI review requested due to automatic review settings July 24, 2026 18:57
@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/1784919112-cloud-mcp-config-api-user-token' pyairbyte --help

# Install PyAirbyte from this branch for development:
pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1784919112-cloud-mcp-config-api-user-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.

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

Prerelease Build Started

Building and publishing prerelease package from this PR...
Check job output.
Prerelease Build/Publish Failed

The prerelease encountered an error.
Check publish workflow output for details.

You can still install directly from this PR branch:

pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1784919112-cloud-mcp-config-api-user-token'

@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review July 24, 2026 18:58

@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 2 additional findings in Devin Review.

Open in Devin Review

Comment thread airbyte/cloud/models.py
@coderabbitai

coderabbitai Bot commented Jul 24, 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 Plus

Run ID: 51039491-c12f-4869-bb83-940d4c3b4b03

📥 Commits

Reviewing files that changed from the base of the PR and between 9246059 and 8dd2ebf.

📒 Files selected for processing (3)
  • airbyte/cloud/models.py
  • airbyte/cloud/workspaces.py
  • tests/unit_tests/test_cloud_config_api_routing.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • airbyte/cloud/models.py
  • airbyte/cloud/workspaces.py
  • tests/unit_tests/test_cloud_config_api_routing.py

📝 Walkthrough

Walkthrough

Cloud workspace and connection reads now route bearer-only authentication through internal Config API endpoints. Response models preserve embedded names and selected streams, while client-credential flows continue using the public API.

Changes

Cloud Config API routing

Layer / File(s) Summary
Config API helpers and response parsing
airbyte/_util/api_util.py, airbyte/cloud/models.py, tests/unit_tests/test_cloud_config_api_routing.py
Adds workspace and connection Config API helpers and parses embedded connection metadata, workspace IDs, names, prefixes, statuses, and selected stream names.
Workspace authentication routing
airbyte/cloud/workspaces.py, airbyte/mcp/cloud.py, tests/unit_tests/test_cloud_config_api_routing.py
Routes bearer-only workspace reads through Config API while retaining public API reads for client credentials and reusing the routing during connection checks.
Connection metadata and presentation
airbyte/cloud/connections.py, airbyte/mcp/cloud.py, tests/unit_tests/test_cloud_config_api_routing.py
Routes bearer-only connection reads through Config API and exposes embedded source, destination, and stream names with public API fallbacks.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant CloudWorkspace
  participant CloudConnection
  participant ConfigAPI
  Client->>CloudWorkspace: request workspace information
  CloudWorkspace->>ConfigAPI: fetch workspace for bearer-only auth
  ConfigAPI-->>CloudWorkspace: workspace mapping
  Client->>CloudConnection: request connection metadata
  CloudConnection->>ConfigAPI: fetch connection withRefreshedCatalog false
  ConfigAPI-->>CloudConnection: embedded connection payload
  CloudConnection-->>Client: names, streams, and connection details
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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: routing interactive Cloud API reads through the Config API.
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/1784919112-cloud-mcp-config-api-user-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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@airbyte/cloud/models.py`:
- Around line 189-193: Update the stream_names comprehension in CloudConnection
to include only entries whose config indicates selected=True, while preserving
the existing stream mapping and nonempty-name checks. Extend the relevant
fixture with a deselected stream and assert it is excluded from selected_streams
and the MCP connection description.

In `@airbyte/cloud/workspaces.py`:
- Around line 229-253: Update CloudWorkspace.connect() to reuse
get_workspace_info() instead of calling api_util.get_workspace() directly,
ensuring bearer-only credentials follow the Config API routing already
implemented by get_workspace_info(). Preserve the existing connection setup and
workspace metadata behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c700cd47-097d-4787-a29b-4a5694bcf96a

📥 Commits

Reviewing files that changed from the base of the PR and between bacbe7b and 9246059.

📒 Files selected for processing (6)
  • airbyte/_util/api_util.py
  • airbyte/cloud/connections.py
  • airbyte/cloud/models.py
  • airbyte/cloud/workspaces.py
  • airbyte/mcp/cloud.py
  • tests/unit_tests/test_cloud_config_api_routing.py

Comment thread airbyte/cloud/models.py
Comment on lines +189 to +193
stream_names = [
entry["stream"]["name"]
for entry in stream_entries
if isinstance(entry.get("stream"), Mapping) and entry["stream"].get("name")
]

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Filter out deselected catalog streams?

Could we require entry["config"]["selected"] here, wdyt? This currently exposes all catalog streams as selected_streams, so disabled streams are reported by CloudConnection.stream_names and the MCP connection description.

Proposed fix
 stream_names = [
     entry["stream"]["name"]
     for entry in stream_entries
-    if isinstance(entry.get("stream"), Mapping) and entry["stream"].get("name")
+    if (
+        isinstance(entry.get("stream"), Mapping)
+        and entry["stream"].get("name")
+        and isinstance(entry.get("config"), Mapping)
+        and entry["config"].get("selected") is True
+    )
 ]

Please add a deselected stream to the fixture and assert it is excluded.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stream_names = [
entry["stream"]["name"]
for entry in stream_entries
if isinstance(entry.get("stream"), Mapping) and entry["stream"].get("name")
]
stream_names = [
entry["stream"]["name"]
for entry in stream_entries
if (
isinstance(entry.get("stream"), Mapping)
and entry["stream"].get("name")
and isinstance(entry.get("config"), Mapping)
and entry["config"].get("selected") is True
)
]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@airbyte/cloud/models.py` around lines 189 - 193, Update the stream_names
comprehension in CloudConnection to include only entries whose config indicates
selected=True, while preserving the existing stream mapping and nonempty-name
checks. Extend the relevant fixture with a deselected stream and assert it is
excluded from selected_streams and the MCP connection description.

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.

☑️ Resolved in 8dd2ebf. Agreed — filtered stream_names to selected streams. I use a _is_stream_selected helper rather than a strict config.selected is True: when config is present I honor selected, but a missing config defaults to selected (matching the platform default) so unusual payloads don't silently drop streams. Added a deselected stream to the fixture and assert it's excluded.

Comment thread airbyte/cloud/workspaces.py
Address review findings:
- from_config_api_response now filters syncCatalog to selected streams
  (config.selected), matching public API stream_names behavior; a missing
  config defaults to selected.
- CloudWorkspace.connect() now uses get_workspace_info() so bearer-only
  (interactive OIDC) credentials are validated via the Config API.

Co-Authored-By: AJ Steers <aj@airbyte.io>
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

Prerelease Build Started

Building and publishing prerelease package from this PR...
Check job output.
Prerelease Build/Publish Failed

The prerelease encountered an error.
Check publish workflow output for details.

You can still install directly from this PR branch:

pip install 'git+https://github.com/airbytehq/PyAirbyte.git@devin/1784919112-cloud-mcp-config-api-user-token'

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Closing — this approach is wrong. It presumed the interactive user token is rejected by the public API and reroutes reads to the internal Config API, but that premise was never verified and is incorrect. The real cause of the interactive 401 is token piping (the wrong token being sent downstream), not the API root. Abandoning this in favor of fixing the token that reaches the public API.

@github-code-quality

github-code-quality Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: Python

Python / code-coverage/pytest-fast

The overall coverage in commit 8dd2ebf in the devin/1784919112-clo... branch is 68%. The coverage in commit d9f652f in the main branch is 65%.

Show a code coverage summary of the most impacted files.
File main d9f652f devin/1784919112-clo... 8dd2ebf +/-
airbyte/mcp/_tool_utils.py 72% 84% +12%
airbyte/cloud/connections.py 26% 39% +13%
airbyte/mcp/registry.py 53% 70% +17%
airbyte/mcp/server.py 69% 88% +19%
airbyte/mcp/_arg_resolvers.py 13% 44% +31%
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% 94% +94%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%

Python / code-coverage/pytest-no-creds

The overall coverage in commit 8dd2ebf in the devin/1784919112-clo... branch is 68%. The coverage in commit d9f652f in the main branch is 65%.

Show a code coverage summary of the most impacted files.
File main d9f652f devin/1784919112-clo... 8dd2ebf +/-
airbyte/mcp/_tool_utils.py 72% 84% +12%
airbyte/cloud/connections.py 26% 39% +13%
airbyte/mcp/registry.py 53% 70% +17%
airbyte/mcp/server.py 69% 88% +19%
airbyte/mcp/_arg_resolvers.py 13% 44% +31%
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% 94% +94%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%

Python / code-coverage/pytest

The overall coverage in commit 8dd2ebf in the devin/1784919112-clo... branch is 73%. The coverage in commit d9f652f in the main branch is 71%.

Show a code coverage summary of the most impacted files.
File main d9f652f devin/1784919112-clo... 8dd2ebf +/-
airbyte/cloud/connections.py 34% 41% +7%
airbyte/mcp/_tool_utils.py 72% 84% +12%
airbyte/mcp/registry.py 53% 70% +17%
airbyte/mcp/server.py 69% 88% +19%
airbyte/mcp/_arg_resolvers.py 13% 44% +31%
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% 96% +96%
airbyte/mcp/int...nc_status_ui.py 0% 97% +97%

Updated July 24, 2026 19:28 UTC

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 authenticated Airbyte Cloud MCP reads after interactive OIDC login by routing “bearer-only” metadata reads (user token without client credentials) through the Cloud Config API, while keeping the existing public API path for headless client-credential auth.

Changes:

  • Add a bearer-only auth detector on CloudWorkspace and route workspace/connection reads through Config API helpers when applicable.
  • Extend CloudConnectionInfo / CloudConnection to consume embedded source/destination names and sync-catalog stream names from the Config API response, reducing extra public API lookups.
  • Add unit tests covering the routing behavior and Config API response parsing.

Reviewed changes

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

Show a summary per file
File Description
tests/unit_tests/test_cloud_config_api_routing.py Adds unit tests for bearer-only routing and Config API response parsing.
airbyte/mcp/cloud.py Switches MCP tools to rely on CloudWorkspace.get_workspace_info() and new connection name properties.
airbyte/cloud/workspaces.py Adds _uses_bearer_only_auth and get_workspace_info() routing logic.
airbyte/cloud/models.py Adds Config API parsing for connections and makes configurations optional when using Config API.
airbyte/cloud/connections.py Routes connection reads via Config API for bearer-only auth and adds name/stream fallbacks.
airbyte/_util/api_util.py Introduces Config API helper functions for workspace/connection reads.

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

Comment thread airbyte/cloud/models.py
Comment on lines +189 to +193
stream_names = [
entry["stream"]["name"]
for entry in stream_entries
if isinstance(entry.get("stream"), Mapping) and entry["stream"].get("name")
]
Copilot AI review requested due to automatic review settings July 24, 2026 19:12

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

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment thread airbyte/_util/api_util.py
Comment on lines +297 to +301
"""Get a workspace via the Config API endpoint `POST /v1/workspaces/get`.

Unlike `get_workspace`, this uses the internal Config API rather than the public API.
The Config API accepts user-realm bearer tokens (from an interactive OIDC login),
which the public API rejects because it only accepts application-client tokens.
Comment thread airbyte/_util/api_util.py
Comment on lines +838 to +842
"""Get a connection via the Config API endpoint `POST /v1/web_backend/connections/get`.

Unlike `get_connection`, this uses the internal Config API rather than the public API.
The Config API accepts user-realm bearer tokens (from an interactive OIDC login),
which the public API rejects because it only accepts application-client tokens.
Comment thread airbyte/cloud/models.py
Comment on lines +210 to +215
configurations=None,
prefix=connection.get("prefix"),
status=_enum_value(connection["status"]),
source_name=source.get("name"),
destination_name=destination.get("name"),
stream_names=stream_names,
assert connection.destination_name == "My Snowflake"
assert connection.stream_names == ["users", "orders", "legacy"]
assert connection.table_prefix == "raw_"
assert calls == ["config"]
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