Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f882e93
feat(cloud): Add CloudWorkspace.from_env() classmethod (#909)
aaronsteers Dec 11, 2025
60a087f
feat: Add `/prerelease` slash command for PyPI prereleases (#910)
aaronsteers Dec 13, 2025
3869ac4
fix: Add required permissions for prerelease workflow (#911)
aaronsteers Dec 14, 2025
cd1b390
fix: Use workflow_dispatch instead of workflow_call for OIDC compatib…
aaronsteers Dec 14, 2025
9f3c707
fix: Use GitHub App token for prerelease workflow dispatch (#913)
aaronsteers Dec 14, 2025
5591792
feat(mcp): Add HTTP header bearer token authentication support
devin-ai-integration[bot] Dec 16, 2025
954dd1a
fix(lint): Remove unused noqa directive in CloudWorkspace
devin-ai-integration[bot] Dec 16, 2025
ab593dd
refactor: Move imports to top-level per repo standards
devin-ai-integration[bot] Dec 16, 2025
5298ade
Merge remote-tracking branch 'origin/devin/1763010134-bearer-token-au…
devin-ai-integration[bot] Dec 16, 2025
59ad29a
fix(types): Fix type errors from base PR merge - pass bearer_token to…
devin-ai-integration[bot] Dec 16, 2025
3b08a87
Merge main to get noqa comments for SLF001 lint errors
devin-ai-integration[bot] Dec 16, 2025
4c0e44c
fix(lint): Add noqa comment for PLR0904 on CloudWorkspace class
devin-ai-integration[bot] Dec 16, 2025
dcde4aa
fix(lint): Update noqa comments to force merge conflict resolution in…
devin-ai-integration[bot] Dec 16, 2025
96ed8d4
Merge base PR branch and resolve conflicts in favor of noqa comments
devin-ai-integration[bot] Dec 16, 2025
d0215fd
refactor: Remove unrelated CI type fixes, keep only spec changes and …
devin-ai-integration[bot] Dec 16, 2025
98d6d26
chore: Remove internal task tracking file
devin-ai-integration[bot] Dec 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions airbyte/cloud/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Authentication-related constants and utilities for the Airbyte Cloud."""

from airbyte import constants
from airbyte.mcp._util import get_bearer_token_from_headers
Copy link
Contributor

Choose a reason for hiding this comment

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

Devin, Can you create a follow-on issue to refactor slightly...

Ideally nothing outside the MCP module would import from the MCP module. That said, this is expedient and doesn't seem to create circular dependencies or side effects (that I can see). So I'm inclined to merge and create follow-on issue for refactoring.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created follow-on issue: #915

The issue proposes moving get_bearer_token_from_headers() to a shared utility location (like airbyte/_util/http_auth.py) so the MCP module can import from it rather than the other way around.

from airbyte.secrets import SecretString
from airbyte.secrets.util import get_secret, try_get_secret

Expand All @@ -26,8 +27,23 @@ def resolve_cloud_bearer_token(
input_value: str | SecretString | None = None,
/,
) -> SecretString | None:
"""Get the Airbyte Cloud bearer token from the environment."""
return try_get_secret(constants.CLOUD_BEARER_TOKEN_ENV_VAR, default=input_value)
"""Get the Airbyte Cloud bearer token.

Resolution order:
1. Explicit input_value parameter
2. HTTP Authorization header (when running as MCP HTTP server)
3. AIRBYTE_CLOUD_BEARER_TOKEN environment variable
"""
if input_value:
return SecretString(input_value)

# Try HTTP header first (for MCP HTTP transport)
header_token = get_bearer_token_from_headers()
if header_token:
return SecretString(header_token)

# Fall back to environment variable
return try_get_secret(constants.CLOUD_BEARER_TOKEN_ENV_VAR, default=None)


def resolve_cloud_api_url(
Expand Down
6 changes: 3 additions & 3 deletions airbyte/cloud/connectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ def _from_source_response(
workspace=workspace,
connector_id=source_response.source_id,
)
result._connector_info = source_response
result._connector_info = source_response # noqa: SLF001 # Non-public API
return result


Expand Down Expand Up @@ -343,7 +343,7 @@ def _from_destination_response(
workspace=workspace,
connector_id=destination_response.destination_id,
)
result._connector_info = destination_response
result._connector_info = destination_response # noqa: SLF001 # Non-public API
return result


Expand Down Expand Up @@ -647,7 +647,7 @@ def _from_yaml_response(
definition_id=response.id,
definition_type="yaml",
)
result._definition_info = response
result._definition_info = response # noqa: SLF001 # Non-public API
return result

def deploy_source(
Expand Down
2 changes: 1 addition & 1 deletion airbyte/cloud/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class CloudOrganization:
"""Display name of the organization."""


@dataclass
@dataclass # noqa: PLR0904 # Too many public methods
class CloudWorkspace:
"""A remote workspace on the Airbyte Cloud.

Expand Down
13 changes: 13 additions & 0 deletions airbyte/mcp/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import dotenv
import yaml
from fastmcp.server.dependencies import get_http_headers

from airbyte._util.meta import is_interactive
from airbyte.secrets import (
Expand All @@ -24,6 +25,18 @@
AIRBYTE_MCP_DOTENV_PATH_ENVVAR = "AIRBYTE_MCP_ENV_FILE"


def get_bearer_token_from_headers() -> str | None:
"""Extract bearer token from HTTP Authorization header.
Returns None if not running over HTTP transport or no header present.
"""
headers = get_http_headers()
auth_header = headers.get("authorization", "")
if auth_header.startswith("Bearer "):
return auth_header[7:] # Strip "Bearer " prefix
return None


def _load_dotenv_file(dotenv_path: Path | str) -> None:
"""Load environment variables from a .env file."""
if isinstance(dotenv_path, str):
Expand Down
Loading