Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 11 additions & 4 deletions airbyte/mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,18 +198,24 @@
}
```

### Opt-in: static client credentials via HTTP Basic
### Opt-in: static client credentials

The headless bearer path assumes the client can re-mint a token every ~15 min.
An agent that can only set a **static** `Authorization` value (no refresh hook)
can't do that. For this case the server can optionally accept the long-lived
`client_id` / `client_secret` directly on the inbound request, via standard HTTP
Basic (the same credential encoding OAuth's `client_secret_basic` uses):
`client_id` / `client_secret` directly on the inbound request, in either form —
standard HTTP Basic (the same credential encoding OAuth's `client_secret_basic`
uses), or separate flat headers for clients that can't Base64-encode the pair:
Comment on lines +201 to +208

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 e91e7d9. Updated the AIRBYTE_MCP_AUTH_ALLOW_CLIENT_CREDENTIALS bullet to say it "enables the inbound static client-credentials path described above (Basic or the Client-Id/Client-Secret headers)" so it matches the expanded behavior.


Devin session


```http
Authorization: Basic base64(client_id:client_secret)
```

```http
Client-Id: <client_id>
Client-Secret: <client_secret>
```

When `AIRBYTE_MCP_AUTH_ALLOW_CLIENT_CREDENTIALS` is truthy, the server runs a
client-credentials exchange server-side (posting the credentials to the Airbyte
token endpoint) for a short-lived token, caches it until shortly before expiry,
Expand Down Expand Up @@ -243,7 +249,8 @@
algorithm; default to Airbyte Cloud (`.../realms/_airbyte-application-clients`,
`account`, `RS256`).
- `AIRBYTE_MCP_AUTH_ALLOW_CLIENT_CREDENTIALS` — opt-in flag (off by default) that
enables the inbound HTTP Basic client-credentials path described above.
enables the inbound static client-credentials path described above (Basic or
the `Client-Id`/`Client-Secret` headers).
- `AIRBYTE_MCP_AUTH_CLIENT_CREDENTIALS_TOKEN_URL` — token endpoint used for that
exchange; defaults to Airbyte Cloud's `https://api.airbyte.com/v1/applications/token`.

Expand Down
12 changes: 7 additions & 5 deletions airbyte/mcp/_client_credentials.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
"""Opt-in HTTP Basic client-credentials transport auth for the MCP server.
"""Opt-in 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
Expand All @@ -8,9 +8,11 @@

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
request either 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
encoding OAuth's `client_secret_basic` uses) or via separate flat `Client-Id` /
`Client-Secret` request headers for clients that can't Base64-encode the pair.
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
Expand Down Expand Up @@ -51,7 +53,7 @@


def client_credentials_enabled(env: Mapping[str, str] | None = None) -> bool:
"""Return whether the opt-in HTTP Basic client-credentials grant is enabled."""
"""Return whether the opt-in 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

Expand All @@ -72,7 +74,7 @@ def wrap_if_enabled(app: ASGIApp) -> ASGIApp:
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.
credentials-to-Bearer rewrite happens before FastMCP's auth verifier runs.
"""
return wrap_client_credentials(
app,
Expand Down
6 changes: 4 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ dependencies = [
"uuid7>=0.1.0,<1.0",
"fastmcp>=3.0,<4.0",
# 0.14.0 drops the library's env-var auth mapping (`resolve_mcp_auth`) in
# favor of the pure typed `build_mcp_auth` API this server now consumes.
"fastmcp-extensions>=0.14.0,<1.0.0",
# favor of the pure typed `build_mcp_auth` API this server now consumes; 0.15.0
# adds the separate `Client-Id` / `Client-Secret` credential-header form to the
# client-credentials exchange middleware (`wrap_client_credentials`).
"fastmcp-extensions>=0.15.0,<1.0.0",
# Transitive dep of `fastmcp` that we import directly (in `airbyte.mcp`).
"starlette",
"uv>=0.5.0,<0.9.0",
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading