Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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] Jul 21, 2026
48efff2
fix(mcp): resolve JWT signing-key source so a static public key isn't…
devin-ai-integration[bot] Jul 21, 2026
1d28bfa
feat(mcp): opt-in HTTP Basic client-credentials transport auth
devin-ai-integration[bot] Jul 21, 2026
c42c32b
Merge origin/main into devin/1784611568-cloud-mcp-airbyte-branded-auth
devin-ai-integration[bot] Jul 21, 2026
f5019b3
build(mcp): declare httpx and uvicorn as direct deps
devin-ai-integration[bot] Jul 21, 2026
f5e7af1
fix(mcp): check legacy OIDC env presence via membership, not value read
devin-ai-integration[bot] Jul 21, 2026
2cf089f
fix(mcp): fail closed on token-exchange errors; per-credential locking
devin-ai-integration[bot] Jul 21, 2026
b3bb53c
fix(mcp): resolve CodeQL clear-text-logging alert; address Copilot re…
devin-ai-integration[bot] Jul 21, 2026
866a368
docs(mcp): note httpx/uvicorn are directly-imported transitive deps
devin-ai-integration[bot] Jul 22, 2026
938817d
refactor(mcp): bound credential cache, clarify Basic terminology, war…
devin-ai-integration[bot] Jul 22, 2026
3b498a1
test(mcp): prefix unused test args with underscore (ARG)
devin-ai-integration[bot] Jul 22, 2026
c83a373
fix(mcp): treat blank auth env vars as unset so baked defaults apply
devin-ai-integration[bot] Jul 22, 2026
e6c19dc
fix(mcp): treat blank client-credentials token URL override as unset
devin-ai-integration[bot] Jul 22, 2026
d957db3
fix(mcp): harden expires_in coercion and align http_main server-url h…
devin-ai-integration[bot] Jul 22, 2026
eb022b1
docs(mcp): correct http_main auth-failure wording (blank falls back t…
devin-ai-integration[bot] Jul 22, 2026
c98f870
fix(mcp): read token cache clock inside per-credential lock
devin-ai-integration[bot] Jul 22, 2026
c6a062c
test(mcp): drop stale AIRBYTE_MCP_ENV_FILE before importing server
devin-ai-integration[bot] Jul 22, 2026
a2b530a
refactor(mcp): consume shared client-credentials middleware from fast…
devin-ai-integration[bot] Jul 22, 2026
f61d232
build(mcp): drop now-unused direct httpx dependency
devin-ai-integration[bot] Jul 22, 2026
f6ab1ba
test(mcp): restore AIRBYTE_MCP_ENV_FILE after server import
devin-ai-integration[bot] Jul 22, 2026
d2d47a8
refactor(mcp): drop legacy auth env-var migration warning
devin-ai-integration[bot] Jul 23, 2026
26fc1d2
refactor(mcp): migrate cloud-mcp auth to typed build_mcp_auth on 0.14.0
devin-ai-integration[bot] Jul 24, 2026
0c92ea8
docs(mcp): align auth docstring with typed build_mcp_auth (no env par…
devin-ai-integration[bot] Jul 24, 2026
2a3472a
test(mcp): restore AIRBYTE_MCP_ENV_FILE via try/finally on import
devin-ai-integration[bot] Jul 24, 2026
f14290d
docs(mcp): fix stray apostrophe in fastmcp-extensions reference
devin-ai-integration[bot] Jul 24, 2026
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
58 changes: 34 additions & 24 deletions airbyte/mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,17 @@
credentials that matter are your Airbyte Cloud creds in the dotenv file.

When the server is instead exposed over **HTTP** (`airbyte-mcp-http` /
`poe mcp-serve-http`), it verifies an `Authorization: Bearer <token>` on every
request. Two client shapes are supported on the same deployment (combined
automatically when both are configured):
`poe mcp-serve-http`), transport auth is **always on** and targets Airbyte Cloud
out of the box — it verifies an `Authorization: Bearer <token>` on every request
against Airbyte Cloud's public Keycloak realms with no auth config required. Two
client shapes are supported on the same deployment (combined automatically):

### Humans → interactive OIDC

Set `OIDC_CONFIG_URL`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET`. Interactive
clients open a browser (Keycloak Authorization Code + PKCE) and the resulting
token is verified by the server. No bearer token to manage by hand.
Supply `AIRBYTE_MCP_OIDC_CLIENT_ID` and `AIRBYTE_MCP_OIDC_CLIENT_SECRET` (the
OIDC discovery URL defaults to Airbyte Cloud). Interactive clients open a browser
(Keycloak Authorization Code + PKCE) and the resulting token is verified by the
server. No bearer token to manage by hand.

### Machines / agents → headless bearer token

Expand All @@ -168,15 +170,16 @@
sends it as `Authorization: Bearer <token>`; the server verifies the signature
(no browser, no stored/rotating refresh token).

For Airbyte Cloud, set `MCP_AUTH_AIRBYTE_CLOUD=true` on the server so it verifies
tokens against Airbyte Cloud's application-client realm without hand-configuring
URLs. The agent mints an Airbyte Cloud access token from its
By default the server verifies against Airbyte Cloud's application-client realm,
so an agent just mints an Airbyte Cloud access token from its
`AIRBYTE_CLOUD_CLIENT_ID` / `AIRBYTE_CLOUD_CLIENT_SECRET` (the
`https://api.airbyte.com/v1/applications/token` endpoint) and sends it as the
bearer. That single token both authenticates transport (verified by the server)
and authorizes downstream Cloud API calls, because an Airbyte-Cloud-issued token
is itself a valid Cloud API bearer. Tokens are short-lived (~15 min), so
re-mint on expiry / on a `401` rather than pinning a static token.
re-mint on expiry / on a `401` rather than pinning a static token. A self-hosted
deployment pointing at its own Airbyte instance overrides the realm via the
`AIRBYTE_MCP_AUTH_*` vars below.

Clients that support HTTP transports can pass the token via a `headers` block in
their MCP config:
Expand All @@ -196,22 +199,29 @@

### Server environment variables (HTTP mode)

All auth settings default to Airbyte Cloud's public (non-secret) Keycloak realms,
so the hosted Airbyte Cloud deployment needs none of them except the interactive
OIDC client credentials. Each is an override for a self-hosted deployment that
points at its own Airbyte instance:

- `MCP_SERVER_URL` — public base URL of the server (also used for OIDC redirect
callbacks); defaults to `http://localhost:8080`.
- `OIDC_CONFIG_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` — enable interactive
OIDC (all three required).
- `MCP_AUTH_AIRBYTE_CLOUD` — set truthy to verify headless tokens against Airbyte
Cloud's realm (fills JWKS URI / issuer / audience / algorithm with Airbyte
Cloud defaults; each stays overridable).
- `MCP_AUTH_JWKS_URI` / `MCP_AUTH_JWT_PUBLIC_KEY` — JWKS URL or static public key
for verifying headless tokens.
- `MCP_AUTH_ISSUER` / `MCP_AUTH_AUDIENCE` / `MCP_AUTH_ALGORITHM` — expected `iss`
/ `aud` claims and signing algorithm (recommended for headless).

If no transport auth variables are set, the HTTP server starts
**unauthenticated** and logs a warning — acceptable only behind a trusted
network boundary. The verifier assembly is provided by
[`fastmcp-extensions`](https://github.com/airbytehq/fastmcp-extensions).
- `AIRBYTE_MCP_OIDC_CLIENT_ID` / `AIRBYTE_MCP_OIDC_CLIENT_SECRET` — OAuth client
credentials that activate the interactive OIDC path (no default; deployment
supplies them).
- `AIRBYTE_MCP_OIDC_CONFIG_URL` — OIDC discovery URL; defaults to Airbyte Cloud's
`airbyte` realm.
- `AIRBYTE_MCP_AUTH_JWKS_URI` — JWKS URL for verifying headless bearer tokens;
defaults to Airbyte Cloud's application-client realm certs.
- `AIRBYTE_MCP_AUTH_ISSUER` / `AIRBYTE_MCP_AUTH_AUDIENCE` /
`AIRBYTE_MCP_AUTH_ALGORITHM` — expected `iss` / `aud` claims and signing
algorithm; default to Airbyte Cloud (`.../realms/_airbyte-application-clients`,
`account`, `RS256`).
Comment thread
aaronsteers marked this conversation as resolved.

This module owns those Airbyte-branded names and translates them to the generic
names consumed by
[`fastmcp-extensions`](https://github.com/airbytehq/fastmcp-extensions), which
assembles the verifiers and keeps the extensions library provider-agnostic.
Comment thread
aaronsteers marked this conversation as resolved.
Outdated

## Troubleshooting

Expand Down
44 changes: 22 additions & 22 deletions airbyte/mcp/http_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,33 @@
"""HTTP transport entry point for the Airbyte MCP server.

Starts the MCP server with HTTP transport, suitable for hosted deployment
behind a load balancer. Transport auth is assembled in `server.py` via
`fastmcp_extensions.resolve_mcp_auth`, which supports interactive OIDC and
headless bearer-token verification (combined via `MultiAuth` when both are
configured). See `server.py` for details.
behind a load balancer. Transport auth is assembled in `server.py`, which
defaults every setting to Airbyte Cloud's public Keycloak realms and supports
interactive OIDC plus headless bearer-token verification (combined via
`MultiAuth`). See `server.py` for details.

Environment variables:

- `MCP_SERVER_URL`: Public base URL. Used for OIDC redirect callbacks and to
derive the MCP endpoint mount path (serves at `/` when the URL has a path
prefix, otherwise defaults to `/mcp`).

Interactive OIDC (Keycloak Authorization Code + PKCE), enabled when all three
are set:
Interactive OIDC (Keycloak Authorization Code + PKCE), active once the client
credentials are supplied:

- `OIDC_CONFIG_URL`: Keycloak OIDC discovery URL
- `OIDC_CLIENT_ID`: OIDC client identifier
- `OIDC_CLIENT_SECRET`: OIDC client secret
- `AIRBYTE_MCP_OIDC_CLIENT_ID`: OIDC client identifier
- `AIRBYTE_MCP_OIDC_CLIENT_SECRET`: OIDC client secret
- `AIRBYTE_MCP_OIDC_CONFIG_URL`: Keycloak OIDC discovery URL (defaults to
Airbyte Cloud)

Headless bearer-token verification (for agents/CI that mint their own
short-lived token via the client credentials grant), enabled when
`MCP_AUTH_JWKS_URI` or `MCP_AUTH_JWT_PUBLIC_KEY` is set:

- `MCP_AUTH_JWKS_URI`: JWKS endpoint used to verify token signatures
- `MCP_AUTH_JWT_PUBLIC_KEY`: static public key (alternative to `MCP_AUTH_JWKS_URI`)
- `MCP_AUTH_ISSUER`: expected token issuer
- `MCP_AUTH_AUDIENCE`: expected token audience
- `MCP_AUTH_ALGORITHM`: signing algorithm override
short-lived token via the client credentials grant); on by default against
Airbyte Cloud, overridable for self-hosted deployments:

- `AIRBYTE_MCP_AUTH_JWKS_URI`: JWKS endpoint used to verify token signatures
Comment thread
aaronsteers marked this conversation as resolved.
- `AIRBYTE_MCP_AUTH_ISSUER`: expected token issuer
- `AIRBYTE_MCP_AUTH_AUDIENCE`: expected token audience
- `AIRBYTE_MCP_AUTH_ALGORITHM`: signing algorithm
"""

from __future__ import annotations
Expand Down Expand Up @@ -89,11 +89,11 @@ def main() -> None:

if getattr(app, "auth", None) is None:
logger.warning(
"HTTP transport starting without authentication: no interactive "
"OIDC or headless bearer-token auth is configured, so every "
"request is unauthenticated. Set `OIDC_CONFIG_URL`/`OIDC_CLIENT_ID`/"
"`OIDC_CLIENT_SECRET` (interactive) or `MCP_AUTH_JWKS_URI`/"
"`MCP_AUTH_JWT_PUBLIC_KEY` (headless) to require auth."
"HTTP transport starting without authentication: no bearer-token or "
"interactive OIDC auth resolved, so every request is "
"unauthenticated. This is unexpected for the default Airbyte Cloud "
"configuration; check that the `AIRBYTE_MCP_AUTH_*` overrides were "
"not blanked out."
)
Comment thread
aaronsteers marked this conversation as resolved.
Comment thread
aaronsteers marked this conversation as resolved.

logger.info(
Expand Down
122 changes: 69 additions & 53 deletions airbyte/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,33 @@

- **stdio** (default): For local MCP clients (Claude Desktop, etc.)
- **HTTP**: For hosted deployment. Start via `airbyte-mcp-http` entry point or
`poe mcp-serve-http`. Transport auth is assembled by
`fastmcp_extensions.resolve_mcp_auth`, which supports two client shapes on the
same deployment:
`poe mcp-serve-http`. Running over HTTP always requires transport auth, and it
targets Airbyte Cloud out of the box — no auth env vars are needed for the
hosted case beyond the interactive OIDC client credentials. Two client shapes
are supported on the same deployment:
- **Interactive** (humans in a browser): Keycloak Authorization Code + PKCE
via `OIDCProxy`, enabled when `OIDC_CONFIG_URL`, `OIDC_CLIENT_ID`, and
`OIDC_CLIENT_SECRET` are all set.
via `OIDCProxy`, active once `AIRBYTE_MCP_OIDC_CLIENT_ID` and
`AIRBYTE_MCP_OIDC_CLIENT_SECRET` are supplied (the OIDC discovery URL
defaults to Airbyte Cloud).
- **Headless** (agents, CI): the client mints its own short-lived bearer
token via the OAuth 2.0 client credentials grant and sends it as
`Authorization: Bearer <token>`. The server verifies it with a
`JWTVerifier` (no browser, no stored/rotating refresh token), enabled
when `MCP_AUTH_JWKS_URI` (or `MCP_AUTH_JWT_PUBLIC_KEY`) is set.
`JWTVerifier` against Airbyte Cloud's application-client realm by default.
When both are configured they are combined via `MultiAuth`.

For Airbyte Cloud, set `MCP_AUTH_AIRBYTE_CLOUD=true` to verify against Airbyte
Cloud's application-client realm without hand-configuring URLs. An agent then
mints an Airbyte Cloud access token from its `AIRBYTE_CLOUD_CLIENT_ID` /
`AIRBYTE_CLOUD_CLIENT_SECRET` (the `<api_root>/applications/token` endpoint) and
sends it as `Authorization: Bearer`. That single token both authenticates
transport (verified here) and authorizes downstream Cloud API calls (the same
header feeds the Cloud bearer token), because an Airbyte-Cloud-issued JWT is
itself a valid Cloud API bearer.
All auth settings default to Airbyte Cloud's public (non-secret) Keycloak realms.
A self-hosted deployment pointing at its own Airbyte instance overrides any of
them via the matching env var (`AIRBYTE_MCP_OIDC_CONFIG_URL`,
`AIRBYTE_MCP_AUTH_JWKS_URI`, `AIRBYTE_MCP_AUTH_ISSUER`,
`AIRBYTE_MCP_AUTH_AUDIENCE`, `AIRBYTE_MCP_AUTH_ALGORITHM`). This module owns those
Airbyte-specific names and translates them to the generic names that
`fastmcp_extensions.resolve_mcp_auth` consumes, so the extensions library stays
provider-agnostic.

For the headless path a token minted from an `AIRBYTE_CLOUD_CLIENT_ID` /
`AIRBYTE_CLOUD_CLIENT_SECRET` (the `<api_root>/applications/token` endpoint) both
authenticates transport (verified here) and authorizes downstream Cloud API
calls, because an Airbyte-Cloud-issued JWT is itself a valid Cloud API bearer.
"""

from __future__ import annotations
Expand All @@ -37,7 +43,6 @@
from typing import TYPE_CHECKING

from fastmcp_extensions import (
JWTAuthConfig,
mcp_server,
resolve_mcp_auth,
)
Expand Down Expand Up @@ -108,51 +113,62 @@
logger = logging.getLogger(__name__)

# Public base URL of this deployment; consumed by `http_main` to derive the
# mounted MCP path. All other transport auth env vars (`OIDC_*` interactive,
# `MCP_AUTH_*` headless) are read by `fastmcp_extensions.resolve_mcp_auth`.
# mounted MCP path and by `resolve_mcp_auth` as the OIDC redirect base.
MCP_SERVER_URL_ENV = "MCP_SERVER_URL"

# Flag that opts headless verification into Airbyte Cloud's application-client
# realm; this server owns only this provider literal.
MCP_AUTH_AIRBYTE_CLOUD_ENV = "MCP_AUTH_AIRBYTE_CLOUD"

# Airbyte Cloud's application-client realm. Tokens minted from an Airbyte Cloud
# `client_id`/`client_secret` via `<api_root>/applications/token` are RS256 JWTs
# issued by this realm, and the same token is a valid Airbyte Cloud API bearer.
# Verifying against this realm lets one token both authenticate transport and
# authorize downstream Cloud calls. Enable with `MCP_AUTH_AIRBYTE_CLOUD=true`.
AIRBYTE_CLOUD_REALM_ISSUER = "https://cloud.airbyte.com/auth/realms/_airbyte-application-clients"
AIRBYTE_CLOUD_JWKS_URI = f"{AIRBYTE_CLOUD_REALM_ISSUER}/protocol/openid-connect/certs"
AIRBYTE_CLOUD_JWT_AUDIENCE = "account"
AIRBYTE_CLOUD_JWT_ALGORITHM = "RS256"

DEFAULT_HTTP_HOST = "0.0.0.0"
DEFAULT_HTTP_PORT = 8080

# Airbyte Cloud's public Keycloak realms. These are non-secret, publicly
# discoverable endpoints used as the zero-config auth defaults so the hosted
# Airbyte Cloud MCP server needs no auth env beyond its OIDC client credentials.
# Interactive human login uses the `airbyte` realm; headless application-client
# tokens are issued by (and verified against) the `_airbyte-application-clients`
# realm.
AIRBYTE_CLOUD_OIDC_CONFIG_URL = (
"https://cloud.airbyte.com/auth/realms/airbyte/.well-known/openid-configuration"
)
AIRBYTE_CLOUD_ISSUER = "https://cloud.airbyte.com/auth/realms/_airbyte-application-clients"
AIRBYTE_CLOUD_JWKS_URI = f"{AIRBYTE_CLOUD_ISSUER}/protocol/openid-connect/certs"
AIRBYTE_CLOUD_AUDIENCE = "account"
AIRBYTE_CLOUD_ALGORITHM = "RS256"

# Maps this server's Airbyte-branded auth env vars to the generic name that
# `fastmcp_extensions.resolve_mcp_auth` consumes, paired with the Airbyte Cloud
# default. A blank default means the value is deployment-provided (the OIDC
# client credentials, which are secret and cannot be baked in). Setting any
# matching env var overrides the Cloud default — the escape hatch for
# self-hosted deployments pointing at their own Airbyte instance. `OIDC_*` vars
# keep the `OIDC` segment (it already denotes auth); the generic JWT verifier
# vars carry the `AUTH` segment.
_AUTH_ENV_MAP: dict[str, tuple[str, str]] = {
"AIRBYTE_MCP_OIDC_CONFIG_URL": ("OIDC_CONFIG_URL", AIRBYTE_CLOUD_OIDC_CONFIG_URL),
Comment thread
aaronsteers marked this conversation as resolved.
Outdated
"AIRBYTE_MCP_OIDC_CLIENT_ID": ("OIDC_CLIENT_ID", ""),
"AIRBYTE_MCP_OIDC_CLIENT_SECRET": ("OIDC_CLIENT_SECRET", ""),
"AIRBYTE_MCP_AUTH_JWKS_URI": ("MCP_AUTH_JWKS_URI", AIRBYTE_CLOUD_JWKS_URI),
"AIRBYTE_MCP_AUTH_ISSUER": ("MCP_AUTH_ISSUER", AIRBYTE_CLOUD_ISSUER),
"AIRBYTE_MCP_AUTH_AUDIENCE": ("MCP_AUTH_AUDIENCE", AIRBYTE_CLOUD_AUDIENCE),
"AIRBYTE_MCP_AUTH_ALGORITHM": ("MCP_AUTH_ALGORITHM", AIRBYTE_CLOUD_ALGORITHM),
}
Comment thread
aaronsteers marked this conversation as resolved.
Outdated


def _create_auth() -> AuthProvider | None:
"""Assemble the transport auth provider from environment configuration.

Delegates env parsing to `fastmcp_extensions.resolve_mcp_auth`, which wires
up interactive `OIDCProxy` (from `OIDC_*`) and/or headless `JWTVerifier`
(from `MCP_AUTH_*`), combining them via `MultiAuth` when both are set and
returning `None` when neither is — so the server falls back to standard
local (no-auth) behavior.

When `MCP_AUTH_AIRBYTE_CLOUD` is truthy, the headless verifier defaults to
Airbyte Cloud's application-client realm (JWKS / issuer / audience /
algorithm); individual `MCP_AUTH_*` vars still override those fields. This
is the only provider literal the server owns.
"""Assemble the transport auth provider, defaulting to Airbyte Cloud.

Reads this server's `AIRBYTE_MCP_*` env vars (falling back to Airbyte Cloud's
public realm defaults), translates them to the generic names that
`fastmcp_extensions.resolve_mcp_auth` consumes, and lets it wire up an
interactive `OIDCProxy` and/or a headless `JWTVerifier`, combined via
`MultiAuth`. Because the JWKS default is always present, HTTP transport
always verifies bearer tokens; the interactive path additionally activates
once the OIDC client credentials are supplied.
"""
jwt_defaults: JWTAuthConfig | None = None
if os.getenv(MCP_AUTH_AIRBYTE_CLOUD_ENV, "").strip().lower() in {"1", "true", "yes"}:
jwt_defaults = JWTAuthConfig(
jwks_uri=AIRBYTE_CLOUD_JWKS_URI,
issuer=AIRBYTE_CLOUD_REALM_ISSUER,
audience=AIRBYTE_CLOUD_JWT_AUDIENCE,
algorithm=AIRBYTE_CLOUD_JWT_ALGORITHM,
)
return resolve_mcp_auth(jwt_defaults=jwt_defaults)
resolved_env = {
generic_name: os.getenv(our_name, default)
for our_name, (generic_name, default) in _AUTH_ENV_MAP.items()
}
resolved_env[MCP_SERVER_URL_ENV] = os.getenv(MCP_SERVER_URL_ENV, "")
return resolve_mcp_auth(env=resolved_env)


set_mcp_mode()
Expand Down
Loading