Skip to content
Merged
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
25 changes: 25 additions & 0 deletions Dockerfile.mcp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
FROM python:3.12-slim

COPY --from=ghcr.io/astral-sh/uv:0.8.17 /uv /uvx /bin/

RUN apt-get update \
&& apt-get install --no-install-recommends -y git \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY pyproject.toml uv.lock README.md /app/
COPY airbyte/ /app/airbyte/

RUN uv sync --frozen --no-dev

RUN useradd -m -u 10001 appuser \
&& chown -R appuser:appuser /app

ENV PATH="/app/.venv/bin:$PATH"

EXPOSE 8080

USER appuser

CMD ["airbyte-mcp-http"]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
44 changes: 44 additions & 0 deletions airbyte/mcp/http_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
"""HTTP transport entry point for the Airbyte MCP server.

Starts the MCP server with HTTP transport, suitable for hosted deployment
behind a load balancer. When OIDC env vars are set, authentication is
handled by `OIDCProxy` (configured in `server.py`).

Environment variables:

- `MCP_SERVER_URL`: Public base URL (for OIDC redirect callbacks)
- `OIDC_CONFIG_URL`: Keycloak OIDC discovery URL (enables auth with all three OIDC vars)
- `OIDC_CLIENT_ID`: OIDC client identifier
- `OIDC_CLIENT_SECRET`: OIDC client secret
"""

from __future__ import annotations

import logging

from airbyte.mcp.server import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT, app


logger = logging.getLogger(__name__)


def main() -> None:
"""Start the Airbyte MCP server with HTTP transport."""
logging.basicConfig(level=logging.INFO)
logger.info(
"Starting Airbyte MCP HTTP server on %s:%d",
DEFAULT_HTTP_HOST,
DEFAULT_HTTP_PORT,
)

app.run(
transport="streamable-http",
host=DEFAULT_HTTP_HOST,
port=DEFAULT_HTTP_PORT,
stateless_http=True,
Comment thread
aaronsteers marked this conversation as resolved.
)
Comment thread
aaronsteers marked this conversation as resolved.


if __name__ == "__main__":
main()
71 changes: 70 additions & 1 deletion airbyte/mcp/server.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
"""Experimental MCP (Model Context Protocol) server for PyAirbyte connector management."""
"""MCP (Model Context Protocol) server for PyAirbyte connector management.

Supports two transport modes:

- **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`. When `OIDC_CONFIG_URL`, `OIDC_CLIENT_ID`, and
`OIDC_CLIENT_SECRET` are all set, enables Keycloak OIDC authentication
via `OIDCProxy`.
"""

from __future__ import annotations

import asyncio
import logging
import os
import sys
from typing import TYPE_CHECKING

from fastmcp.server.auth.oidc_proxy import OIDCProxy
from fastmcp_extensions import mcp_server
from starlette.responses import JSONResponse


if TYPE_CHECKING:
from starlette.requests import Request

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

logger = logging.getLogger(__name__)

OIDC_CONFIG_URL_ENV = "OIDC_CONFIG_URL"
OIDC_CLIENT_ID_ENV = "OIDC_CLIENT_ID"
OIDC_CLIENT_SECRET_ENV = "OIDC_CLIENT_SECRET"
MCP_SERVER_URL_ENV = "MCP_SERVER_URL"

DEFAULT_HTTP_HOST = "0.0.0.0"
DEFAULT_HTTP_PORT = 8080


def _create_oidc_auth() -> OIDCProxy | None:
"""Create an `OIDCProxy` auth provider when OIDC env vars are configured.

When `OIDC_CONFIG_URL`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET` are all
set, returns an `OIDCProxy` that handles the Keycloak Authorization Code +
PKCE flow. Otherwise returns `None` (no auth, standard local behavior).
"""
config_url = os.getenv(OIDC_CONFIG_URL_ENV, "")
client_id = os.getenv(OIDC_CLIENT_ID_ENV, "")
client_secret = os.getenv(OIDC_CLIENT_SECRET_ENV, "")

if not config_url or not client_id or not client_secret:
return None
Comment thread
coderabbitai[bot] marked this conversation as resolved.

server_url = os.getenv(
MCP_SERVER_URL_ENV,
f"http://localhost:{DEFAULT_HTTP_PORT}",
)

logger.info(
"OIDC auth enabled (issuer=%s, client_id=%s, base_url=%s)",
config_url,
client_id,
server_url,
)
return OIDCProxy(
config_url=config_url,
client_id=client_id,
client_secret=client_secret,
base_url=server_url,
)


set_mcp_mode()
load_secrets_to_env_vars()

Expand All @@ -89,6 +151,7 @@
airbyte_module_filter,
airbyte_ui_support_filter,
],
auth=_create_oidc_auth(),
)
"""The Airbyte MCP Server application instance."""

Expand All @@ -100,6 +163,12 @@
register_prompts(app)


@app.custom_route("/health", methods=["GET"])
async def health_check(request: Request) -> JSONResponse: # noqa: ARG001, RUF029
"""Health check endpoint for load balancer probes."""
return JSONResponse({"status": "ok"})
Comment thread
aaronsteers marked this conversation as resolved.


def main() -> None:
"""@private Main entry point for the MCP server.

Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Comment thread
aaronsteers marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies = [
"uuid7>=0.1.0,<1.0",
"fastmcp>=3.0,<4.0",
"fastmcp-extensions>=0.5.0,<1.0.0",
"starlette",
"uv>=0.5.0,<0.9.0",
"prefab-ui>=0.20.1,<0.21",
]
Expand Down Expand Up @@ -81,6 +82,7 @@ dev = [
pyairbyte = "airbyte.cli.pyab:cli"
pyab = "airbyte.cli.pyab:cli"
airbyte-mcp = "airbyte.mcp.server:main"
airbyte-mcp-http = "airbyte.mcp.http_main:main"
source-smoke-test = "airbyte.cli.smoke_test_source.run:run"

[build-system]
Expand Down Expand Up @@ -206,3 +208,4 @@ DEP002 = [
# Used as subprocess tool, not imported directly:
"uv",
]
DEP003 = []
2 changes: 2 additions & 0 deletions uv.lock

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

Loading