|
1 | 1 | # 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 | +""" |
3 | 12 |
|
4 | 13 | from __future__ import annotations |
5 | 14 |
|
6 | 15 | import asyncio |
| 16 | +import logging |
| 17 | +import os |
7 | 18 | import sys |
| 19 | +from typing import TYPE_CHECKING |
8 | 20 |
|
| 21 | +from fastmcp.server.auth.oidc_proxy import OIDCProxy |
9 | 22 | from fastmcp_extensions import mcp_server |
| 23 | +from starlette.responses import JSONResponse |
| 24 | + |
| 25 | + |
| 26 | +if TYPE_CHECKING: |
| 27 | + from starlette.requests import Request |
10 | 28 |
|
11 | 29 | from airbyte._util.meta import set_mcp_mode |
12 | 30 | from airbyte.mcp._config import load_secrets_to_env_vars |
|
65 | 83 | - Read-only mode: Disables all write operations for cloud resources |
66 | 84 | """.strip() |
67 | 85 |
|
| 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 | + |
68 | 130 | set_mcp_mode() |
69 | 131 | load_secrets_to_env_vars() |
70 | 132 |
|
|
89 | 151 | airbyte_module_filter, |
90 | 152 | airbyte_ui_support_filter, |
91 | 153 | ], |
| 154 | + auth=_create_oidc_auth(), |
92 | 155 | ) |
93 | 156 | """The Airbyte MCP Server application instance.""" |
94 | 157 |
|
|
100 | 163 | register_prompts(app) |
101 | 164 |
|
102 | 165 |
|
| 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 | + |
103 | 172 | def main() -> None: |
104 | 173 | """@private Main entry point for the MCP server. |
105 | 174 |
|
|
0 commit comments