-
Notifications
You must be signed in to change notification settings - Fork 74
feat(mcp): always-on HTTP auth via typed build_mcp_auth with Airbyte Cloud defaults #1084
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Aaron ("AJ") Steers (aaronsteers)
wants to merge
25
commits into
main
Choose a base branch
from
devin/1784611568-cloud-mcp-airbyte-branded-auth
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
feb9cec
feat(mcp): always-on HTTP auth with baked Airbyte Cloud defaults
devin-ai-integration[bot] 48efff2
fix(mcp): resolve JWT signing-key source so a static public key isn't…
devin-ai-integration[bot] 1d28bfa
feat(mcp): opt-in HTTP Basic client-credentials transport auth
devin-ai-integration[bot] c42c32b
Merge origin/main into devin/1784611568-cloud-mcp-airbyte-branded-auth
devin-ai-integration[bot] f5019b3
build(mcp): declare httpx and uvicorn as direct deps
devin-ai-integration[bot] f5e7af1
fix(mcp): check legacy OIDC env presence via membership, not value read
devin-ai-integration[bot] 2cf089f
fix(mcp): fail closed on token-exchange errors; per-credential locking
devin-ai-integration[bot] b3bb53c
fix(mcp): resolve CodeQL clear-text-logging alert; address Copilot re…
devin-ai-integration[bot] 866a368
docs(mcp): note httpx/uvicorn are directly-imported transitive deps
devin-ai-integration[bot] 938817d
refactor(mcp): bound credential cache, clarify Basic terminology, war…
devin-ai-integration[bot] 3b498a1
test(mcp): prefix unused test args with underscore (ARG)
devin-ai-integration[bot] c83a373
fix(mcp): treat blank auth env vars as unset so baked defaults apply
devin-ai-integration[bot] e6c19dc
fix(mcp): treat blank client-credentials token URL override as unset
devin-ai-integration[bot] d957db3
fix(mcp): harden expires_in coercion and align http_main server-url h…
devin-ai-integration[bot] eb022b1
docs(mcp): correct http_main auth-failure wording (blank falls back t…
devin-ai-integration[bot] c98f870
fix(mcp): read token cache clock inside per-credential lock
devin-ai-integration[bot] c6a062c
test(mcp): drop stale AIRBYTE_MCP_ENV_FILE before importing server
devin-ai-integration[bot] a2b530a
refactor(mcp): consume shared client-credentials middleware from fast…
devin-ai-integration[bot] f61d232
build(mcp): drop now-unused direct httpx dependency
devin-ai-integration[bot] f6ab1ba
test(mcp): restore AIRBYTE_MCP_ENV_FILE after server import
devin-ai-integration[bot] d2d47a8
refactor(mcp): drop legacy auth env-var migration warning
devin-ai-integration[bot] 26fc1d2
refactor(mcp): migrate cloud-mcp auth to typed build_mcp_auth on 0.14.0
devin-ai-integration[bot] 0c92ea8
docs(mcp): align auth docstring with typed build_mcp_auth (no env par…
devin-ai-integration[bot] 2a3472a
test(mcp): restore AIRBYTE_MCP_ENV_FILE via try/finally on import
devin-ai-integration[bot] f14290d
docs(mcp): fix stray apostrophe in fastmcp-extensions reference
devin-ai-integration[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| # Copyright (c) 2025 Airbyte, Inc., all rights reserved. | ||
| """Opt-in HTTP Basic client-credentials transport auth for the MCP server. | ||
|
|
||
| The headless bearer path verifies an already-minted, short-lived (~15 min) JWT. | ||
| That works for MCP clients that run the OAuth flow and refresh tokens | ||
| automatically, but not for a truly headless agent that can only set a *static* | ||
| `Authorization` header value and cannot re-mint on a timer. | ||
|
|
||
| This module bridges that gap, behind an opt-in flag. When enabled, the server | ||
| accepts the long-lived `client_id` / `client_secret` presented on the inbound MCP | ||
| request via standard HTTP Basic auth | ||
| (`Authorization: Basic base64(client_id:client_secret)`, the same credential | ||
| encoding OAuth's `client_secret_basic` uses). The server then runs a | ||
| client-credentials exchange against the Airbyte token endpoint to obtain a | ||
| short-lived access token and rewrites the request to `Authorization: Bearer | ||
| <token>` so the existing `JWTVerifier` validates it unchanged. The agent thus | ||
| presents a durable credential once; the server owns the short-lived-token churn. | ||
|
|
||
| The provider-neutral exchange middleware lives in `fastmcp_extensions` | ||
| (`wrap_client_credentials`). This module owns only the Airbyte-specific policy: | ||
| the opt-in env toggle and the Airbyte Cloud token endpoint (overridable for | ||
| self-hosted deployments). It resolves those to plain values and hands them to | ||
| the generic library, so no Airbyte literal or env-var name leaks into the lib. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from fastmcp_extensions import wrap_client_credentials | ||
|
|
||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Mapping | ||
|
|
||
| from starlette.types import ASGIApp | ||
|
|
||
|
|
||
| # Opt-in flag. Off by default: accepting long-lived credentials at the transport | ||
| # is a deliberate escalation, so a deployment must explicitly turn it on. | ||
| ALLOW_CLIENT_CREDENTIALS_ENV = "AIRBYTE_MCP_AUTH_ALLOW_CLIENT_CREDENTIALS" | ||
|
|
||
| # Airbyte token endpoint that mints an application access token from a | ||
| # `client_id` / `client_secret`. Defaults to Airbyte Cloud; overridable for | ||
| # self-hosted deployments pointing at their own Airbyte instance. | ||
| AIRBYTE_CLOUD_TOKEN_URL = "https://api.airbyte.com/v1/applications/token" | ||
| TOKEN_URL_ENV = "AIRBYTE_MCP_AUTH_CLIENT_CREDENTIALS_TOKEN_URL" | ||
|
|
||
| _TRUTHY = frozenset({"1", "true", "t", "yes", "y", "on"}) | ||
|
|
||
|
|
||
| def client_credentials_enabled(env: Mapping[str, str] | None = None) -> bool: | ||
| """Return whether the opt-in HTTP Basic client-credentials grant is enabled.""" | ||
| source = env if env is not None else os.environ | ||
| return source.get(ALLOW_CLIENT_CREDENTIALS_ENV, "").strip().lower() in _TRUTHY | ||
|
|
||
|
|
||
| def _token_url() -> str: | ||
| """Return the token endpoint, defaulting to Airbyte Cloud. | ||
|
|
||
| A blank or whitespace-only override is treated as unset so the Airbyte Cloud | ||
| default still applies, rather than POSTing to an invalid URL and failing every | ||
| Basic-auth request closed. | ||
| """ | ||
| return os.getenv(TOKEN_URL_ENV, "").strip() or AIRBYTE_CLOUD_TOKEN_URL | ||
|
|
||
|
|
||
| def wrap_if_enabled(app: ASGIApp) -> ASGIApp: | ||
| """Wrap `app` with the client-credentials exchange when the flag is set. | ||
|
|
||
| Returns `app` unchanged when the opt-in flag is unset, so the standard | ||
| bearer/OIDC transport auth is the only path. When enabled, wraps `app` as the | ||
| outermost ASGI layer (via `fastmcp_extensions.wrap_client_credentials`) so the | ||
| Basic-to-Bearer rewrite happens before FastMCP's auth verifier runs. | ||
| """ | ||
| return wrap_client_credentials( | ||
| app, | ||
| enabled=client_credentials_enabled(), | ||
| token_url=_token_url(), | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.