Skip to content

Commit ad6b43f

Browse files
feat(mcp): add OIDC auth, HTTP entry point, and Dockerfile for hosted deployment (#1046)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent c5d5a58 commit ad6b43f

5 files changed

Lines changed: 144 additions & 1 deletion

File tree

Dockerfile.mcp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM python:3.12-slim
2+
3+
COPY --from=ghcr.io/astral-sh/uv:0.8.17 /uv /uvx /bin/
4+
5+
RUN apt-get update \
6+
&& apt-get install --no-install-recommends -y git \
7+
&& rm -rf /var/lib/apt/lists/*
8+
9+
WORKDIR /app
10+
11+
COPY pyproject.toml uv.lock README.md /app/
12+
COPY airbyte/ /app/airbyte/
13+
14+
RUN uv sync --frozen --no-dev
15+
16+
RUN useradd -m -u 10001 appuser \
17+
&& chown -R appuser:appuser /app
18+
19+
ENV PATH="/app/.venv/bin:$PATH"
20+
21+
EXPOSE 8080
22+
23+
USER appuser
24+
25+
CMD ["airbyte-mcp-http"]

airbyte/mcp/http_main.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2+
"""HTTP transport entry point for the Airbyte MCP server.
3+
4+
Starts the MCP server with HTTP transport, suitable for hosted deployment
5+
behind a load balancer. When OIDC env vars are set, authentication is
6+
handled by `OIDCProxy` (configured in `server.py`).
7+
8+
Environment variables:
9+
10+
- `MCP_SERVER_URL`: Public base URL (for OIDC redirect callbacks)
11+
- `OIDC_CONFIG_URL`: Keycloak OIDC discovery URL (enables auth with all three OIDC vars)
12+
- `OIDC_CLIENT_ID`: OIDC client identifier
13+
- `OIDC_CLIENT_SECRET`: OIDC client secret
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import logging
19+
20+
from airbyte.mcp.server import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, app
21+
22+
23+
logger = logging.getLogger(__name__)
24+
25+
26+
def main() -> None:
27+
"""Start the Airbyte MCP server with HTTP transport."""
28+
logging.basicConfig(level=logging.INFO)
29+
logger.info(
30+
"Starting Airbyte MCP HTTP server on %s:%d",
31+
DEFAULT_HTTP_HOST,
32+
DEFAULT_HTTP_PORT,
33+
)
34+
35+
app.run(
36+
transport="streamable-http",
37+
host=DEFAULT_HTTP_HOST,
38+
port=DEFAULT_HTTP_PORT,
39+
stateless_http=True,
40+
)
41+
42+
43+
if __name__ == "__main__":
44+
main()

airbyte/mcp/server.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,30 @@
11
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2-
"""Experimental MCP (Model Context Protocol) server for PyAirbyte connector management."""
2+
"""MCP (Model Context Protocol) server for PyAirbyte connector management.
3+
4+
Supports two transport modes:
5+
6+
- **stdio** (default): For local MCP clients (Claude Desktop, etc.)
7+
- **HTTP**: For hosted deployment. Start via `airbyte-mcp-http` entry point or
8+
`poe mcp-serve-http`. When `OIDC_CONFIG_URL`, `OIDC_CLIENT_ID`, and
9+
`OIDC_CLIENT_SECRET` are all set, enables Keycloak OIDC authentication
10+
via `OIDCProxy`.
11+
"""
312

413
from __future__ import annotations
514

615
import asyncio
16+
import logging
17+
import os
718
import sys
19+
from typing import TYPE_CHECKING
820

21+
from fastmcp.server.auth.oidc_proxy import OIDCProxy
922
from fastmcp_extensions import mcp_server
23+
from starlette.responses import JSONResponse
24+
25+
26+
if TYPE_CHECKING:
27+
from starlette.requests import Request
1028

1129
from airbyte._util.meta import set_mcp_mode
1230
from airbyte.mcp._config import load_secrets_to_env_vars
@@ -65,6 +83,50 @@
6583
- Read-only mode: Disables all write operations for cloud resources
6684
""".strip()
6785

86+
logger = logging.getLogger(__name__)
87+
88+
OIDC_CONFIG_URL_ENV = "OIDC_CONFIG_URL"
89+
OIDC_CLIENT_ID_ENV = "OIDC_CLIENT_ID"
90+
OIDC_CLIENT_SECRET_ENV = "OIDC_CLIENT_SECRET"
91+
MCP_SERVER_URL_ENV = "MCP_SERVER_URL"
92+
93+
DEFAULT_HTTP_HOST = "0.0.0.0"
94+
DEFAULT_HTTP_PORT = 8080
95+
96+
97+
def _create_oidc_auth() -> OIDCProxy | None:
98+
"""Create an `OIDCProxy` auth provider when OIDC env vars are configured.
99+
100+
When `OIDC_CONFIG_URL`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET` are all
101+
set, returns an `OIDCProxy` that handles the Keycloak Authorization Code +
102+
PKCE flow. Otherwise returns `None` (no auth, standard local behavior).
103+
"""
104+
config_url = os.getenv(OIDC_CONFIG_URL_ENV, "")
105+
client_id = os.getenv(OIDC_CLIENT_ID_ENV, "")
106+
client_secret = os.getenv(OIDC_CLIENT_SECRET_ENV, "")
107+
108+
if not config_url or not client_id or not client_secret:
109+
return None
110+
111+
server_url = os.getenv(
112+
MCP_SERVER_URL_ENV,
113+
f"http://localhost:{DEFAULT_HTTP_PORT}",
114+
)
115+
116+
logger.info(
117+
"OIDC auth enabled (issuer=%s, client_id=%s, base_url=%s)",
118+
config_url,
119+
client_id,
120+
server_url,
121+
)
122+
return OIDCProxy(
123+
config_url=config_url,
124+
client_id=client_id,
125+
client_secret=client_secret,
126+
base_url=server_url,
127+
)
128+
129+
68130
set_mcp_mode()
69131
load_secrets_to_env_vars()
70132

@@ -89,6 +151,7 @@
89151
airbyte_module_filter,
90152
airbyte_ui_support_filter,
91153
],
154+
auth=_create_oidc_auth(),
92155
)
93156
"""The Airbyte MCP Server application instance."""
94157

@@ -100,6 +163,12 @@
100163
register_prompts(app)
101164

102165

166+
@app.custom_route("/health", methods=["GET"])
167+
async def health_check(request: Request) -> JSONResponse: # noqa: ARG001, RUF029
168+
"""Health check endpoint for load balancer probes."""
169+
return JSONResponse({"status": "ok"})
170+
171+
103172
def main() -> None:
104173
"""@private Main entry point for the MCP server.
105174

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies = [
4747
"uuid7>=0.1.0,<1.0",
4848
"fastmcp>=3.0,<4.0",
4949
"fastmcp-extensions>=0.5.0,<1.0.0",
50+
"starlette",
5051
"uv>=0.5.0,<0.9.0",
5152
"prefab-ui>=0.20.1,<0.21",
5253
]
@@ -81,6 +82,7 @@ dev = [
8182
pyairbyte = "airbyte.cli.pyab:cli"
8283
pyab = "airbyte.cli.pyab:cli"
8384
airbyte-mcp = "airbyte.mcp.server:main"
85+
airbyte-mcp-http = "airbyte.mcp.http_main:main"
8486
source-smoke-test = "airbyte.cli.smoke_test_source.run:run"
8587

8688
[build-system]
@@ -206,3 +208,4 @@ DEP002 = [
206208
# Used as subprocess tool, not imported directly:
207209
"uv",
208210
]
211+
DEP003 = []

uv.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)