airbyte_ops_mcp.mcp.server
Airbyte Admin MCP server implementation.
This module provides the main MCP server for Airbyte admin operations.
The server can run in two modes:
- stdio mode (default): For direct MCP client connections via stdin/stdout
- HTTP mode: HTTP transport is always authenticated, defaulting to
Airbyte Cloud with zero auth config. This server maps its own
AIRBYTE_MCP_*env vars into the typed configs thatfastmcp_extensions.build_mcp_authconsumes, which supports two client shapes on the same deployment:- Interactive (humans in a browser): Keycloak Authorization Code + PKCE
via
OIDCProxy, active onceAIRBYTE_MCP_OIDC_CLIENT_IDandAIRBYTE_MCP_OIDC_CLIENT_SECRETare 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 aJWTVerifieragainst Airbyte Cloud's application-client realm by default (no browser, no stored/rotating refresh token). When both are active they are combined viaMultiAuth.
- Interactive (humans in a browser): Keycloak Authorization Code + PKCE
via
This module owns the Airbyte Cloud realm defaults (non-secret, publicly
discoverable) and maps its AIRBYTE_MCP_OIDC_* / AIRBYTE_MCP_AUTH_* env vars
into the typed OIDCAuthConfig / JWTAuthConfig objects that build_mcp_auth
consumes, so the extensions library stays provider-neutral and reads no env
itself. A self-hosted deployment pointing at its own Airbyte instance overrides
any default via the matching env var.
An agent 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 AIRBYTE_CLOUD_BEARER_TOKEN), because an Airbyte-Cloud-issued JWT
is itself a valid Cloud API bearer.
HTTP mode environment variables (the headless JWT-verifier vars default to
Airbyte Cloud and are optional overrides for self-hosted deployments; the
interactive OIDC client credentials have no default and must be supplied to
enable interactive login):
MCP_SERVER_URL: Public base URL for the MCP server (also used for OIDC
redirect callbacks). Defaults to http://localhost:8080.
AIRBYTE_MCP_OIDC_CONFIG_URL: Keycloak OIDC discovery URL (defaults to
Airbyte Cloud)
AIRBYTE_MCP_OIDC_CLIENT_ID: OAuth client ID for interactive OIDC (no
default; supply to activate interactive login)
AIRBYTE_MCP_OIDC_CLIENT_SECRET: OAuth client secret for interactive OIDC
AIRBYTE_MCP_AUTH_JWKS_URI: JWKS URL for verifying headless bearer tokens
AIRBYTE_MCP_AUTH_JWT_PUBLIC_KEY: Static public key alternative to
AIRBYTE_MCP_AUTH_JWKS_URI
AIRBYTE_MCP_AUTH_ISSUER: Expected iss claim for headless tokens
AIRBYTE_MCP_AUTH_AUDIENCE: Expected aud claim for headless tokens
AIRBYTE_MCP_AUTH_ALGORITHM: JWT signing algorithm
AIRBYTE_MCP_AUTH_ALLOW_CLIENT_CREDENTIALS: Set truthy to also accept
Authorization: Basic base64(client_id:client_secret). The server
exchanges those long-lived credentials for a short-lived bearer token
server-side and rewrites the request to Authorization: Bearer <token>
so the headless verifier above validates it. Off by default. This is for
headless agents that can only set a static Authorization header and
cannot re-mint short-lived tokens themselves. See
airbyte_ops_mcp.mcp._client_credentials.
AIRBYTE_MCP_AUTH_CLIENT_CREDENTIALS_TOKEN_URL: Token endpoint used for the
exchange above (defaults to Airbyte Cloud; override for self-hosted).
1# Copyright (c) 2025 Airbyte, Inc., all rights reserved. 2"""Airbyte Admin MCP server implementation. 3 4This module provides the main MCP server for Airbyte admin operations. 5 6The server can run in two modes: 7- **stdio mode** (default): For direct MCP client connections via stdin/stdout 8- **HTTP mode**: HTTP transport is **always authenticated**, defaulting to 9 Airbyte Cloud with zero auth config. This server maps its own `AIRBYTE_MCP_*` 10 env vars into the typed configs that `fastmcp_extensions.build_mcp_auth` 11 consumes, which supports two client shapes on the same deployment: 12 - **Interactive** (humans in a browser): Keycloak Authorization Code + PKCE 13 via `OIDCProxy`, active once `AIRBYTE_MCP_OIDC_CLIENT_ID` and 14 `AIRBYTE_MCP_OIDC_CLIENT_SECRET` are supplied (the OIDC discovery URL 15 defaults to Airbyte Cloud). 16 - **Headless** (agents, CI): the client mints its own short-lived bearer 17 token via the OAuth 2.0 client credentials grant and sends it as 18 `Authorization: Bearer <token>`. The server verifies it with a 19 `JWTVerifier` against Airbyte Cloud's application-client realm by default 20 (no browser, no stored/rotating refresh token). 21 When both are active they are combined via `MultiAuth`. 22 23This module owns the Airbyte Cloud realm defaults (non-secret, publicly 24discoverable) and maps its `AIRBYTE_MCP_OIDC_*` / `AIRBYTE_MCP_AUTH_*` env vars 25into the typed `OIDCAuthConfig` / `JWTAuthConfig` objects that `build_mcp_auth` 26consumes, so the extensions library stays provider-neutral and reads no env 27itself. A self-hosted deployment pointing at its own Airbyte instance overrides 28any default via the matching env var. 29 30An agent mints an Airbyte Cloud access token from its `AIRBYTE_CLOUD_CLIENT_ID` / 31`AIRBYTE_CLOUD_CLIENT_SECRET` (the `<api_root>/applications/token` endpoint) and 32sends it as `Authorization: Bearer`. That single token both authenticates 33transport (verified here) and authorizes downstream Cloud API calls (the same 34header feeds `AIRBYTE_CLOUD_BEARER_TOKEN`), because an Airbyte-Cloud-issued JWT 35is itself a valid Cloud API bearer. 36 37HTTP mode environment variables (the headless JWT-verifier vars default to 38Airbyte Cloud and are optional overrides for self-hosted deployments; the 39interactive OIDC client credentials have no default and must be supplied to 40enable interactive login): 41 MCP_SERVER_URL: Public base URL for the MCP server (also used for OIDC 42 redirect callbacks). Defaults to `http://localhost:8080`. 43 AIRBYTE_MCP_OIDC_CONFIG_URL: Keycloak OIDC discovery URL (defaults to 44 Airbyte Cloud) 45 AIRBYTE_MCP_OIDC_CLIENT_ID: OAuth client ID for interactive OIDC (no 46 default; supply to activate interactive login) 47 AIRBYTE_MCP_OIDC_CLIENT_SECRET: OAuth client secret for interactive OIDC 48 AIRBYTE_MCP_AUTH_JWKS_URI: JWKS URL for verifying headless bearer tokens 49 AIRBYTE_MCP_AUTH_JWT_PUBLIC_KEY: Static public key alternative to 50 `AIRBYTE_MCP_AUTH_JWKS_URI` 51 AIRBYTE_MCP_AUTH_ISSUER: Expected `iss` claim for headless tokens 52 AIRBYTE_MCP_AUTH_AUDIENCE: Expected `aud` claim for headless tokens 53 AIRBYTE_MCP_AUTH_ALGORITHM: JWT signing algorithm 54 AIRBYTE_MCP_AUTH_ALLOW_CLIENT_CREDENTIALS: Set truthy to also accept 55 `Authorization: Basic base64(client_id:client_secret)`. The server 56 exchanges those long-lived credentials for a short-lived bearer token 57 server-side and rewrites the request to `Authorization: Bearer <token>` 58 so the headless verifier above validates it. Off by default. This is for 59 headless agents that can only set a static `Authorization` header and 60 cannot re-mint short-lived tokens themselves. See 61 `airbyte_ops_mcp.mcp._client_credentials`. 62 AIRBYTE_MCP_AUTH_CLIENT_CREDENTIALS_TOKEN_URL: Token endpoint used for the 63 exchange above (defaults to Airbyte Cloud; override for self-hosted). 64""" 65 66import asyncio 67import logging 68import os 69import sys 70from pathlib import Path 71from urllib.parse import urlparse 72 73import uvicorn 74from airbyte.cloud.auth import resolve_cloud_client_id, resolve_cloud_client_secret 75from dotenv import load_dotenv 76from fastmcp import FastMCP 77from fastmcp.server.auth import AuthProvider 78from fastmcp.server.dependencies import get_access_token 79from fastmcp_extensions import ( 80 JWTAuthConfig, 81 MCPServerConfigArg, 82 OIDCAuthConfig, 83 ToolCallTelemetryMiddleware, 84 build_mcp_auth, 85 mcp_server, 86 register_landing_page, 87) 88from starlette.requests import Request 89from starlette.responses import JSONResponse 90 91from airbyte_ops_mcp._sentry import _SENTRY_DSN, init_sentry_tracking 92from airbyte_ops_mcp.constants import ( 93 HEADER_AIRBYTE_CLOUD_CLIENT_ID, 94 HEADER_AIRBYTE_CLOUD_CLIENT_SECRET, 95 MCP_SERVER_NAME, 96 ServerConfigKey, 97) 98from airbyte_ops_mcp.mcp._client_credentials import wrap_if_enabled 99from airbyte_ops_mcp.mcp._guidance import MCP_SERVER_INSTRUCTIONS 100from airbyte_ops_mcp.mcp._oidc_storage import resolve_oidc_client_storage 101from airbyte_ops_mcp.mcp.connection_medic import register_connection_medic_tools 102from airbyte_ops_mcp.mcp.connector_qa import register_connector_qa_tools 103from airbyte_ops_mcp.mcp.connector_registry import register_connector_registry_tools 104from airbyte_ops_mcp.mcp.connector_versions import register_connector_version_tools 105from airbyte_ops_mcp.mcp.context_store_ops import register_context_store_ops_tools 106from airbyte_ops_mcp.mcp.devin_ops import register_devin_ops_tools 107from airbyte_ops_mcp.mcp.github_ops import register_github_ops_tools 108from airbyte_ops_mcp.mcp.human_in_the_loop import register_human_in_the_loop_tools 109from airbyte_ops_mcp.mcp.logging import register_logging_tools 110from airbyte_ops_mcp.mcp.organization_admin import register_organization_admin_tools 111from airbyte_ops_mcp.mcp.prod_db_ops import register_prod_db_ops_tools 112from airbyte_ops_mcp.mcp.prompts import register_prompts 113from airbyte_ops_mcp.mcp.zendesk_ops import register_zendesk_ops_tools 114from airbyte_ops_mcp.telemetry import _DEFAULT_SEGMENT_WRITE_KEY 115 116logger = logging.getLogger(__name__) 117 118# Default HTTP server configuration 119DEFAULT_HTTP_HOST = "0.0.0.0" 120DEFAULT_HTTP_PORT = 8080 121 122# Public base URL of this deployment, used to derive the mounted MCP path and the 123# OIDC redirect base. 124MCP_SERVER_URL_ENV = "MCP_SERVER_URL" 125 126# Default public base URL, mirroring the HTTP entrypoint default so the OIDC 127# redirect base is well-formed even when `MCP_SERVER_URL` is unset (local dev). 128DEFAULT_MCP_SERVER_URL = f"http://localhost:{DEFAULT_HTTP_PORT}" 129 130# Airbyte Cloud's public Keycloak realms. These are non-secret, publicly 131# discoverable endpoints used as the zero-config auth defaults so the hosted 132# Airbyte Cloud MCP server needs no auth env beyond its OIDC client credentials. 133# Interactive human login uses the `airbyte` realm; headless application-client 134# tokens are issued by (and verified against) the `_airbyte-application-clients` 135# realm. Because the same headless token is a valid Airbyte Cloud API bearer, one 136# token both authenticates transport and authorizes downstream Cloud API calls. 137AIRBYTE_CLOUD_OIDC_CONFIG_URL = ( 138 "https://cloud.airbyte.com/auth/realms/airbyte/.well-known/openid-configuration" 139) 140AIRBYTE_CLOUD_ISSUER = ( 141 "https://cloud.airbyte.com/auth/realms/_airbyte-application-clients" 142) 143AIRBYTE_CLOUD_JWKS_URI = f"{AIRBYTE_CLOUD_ISSUER}/protocol/openid-connect/certs" 144AIRBYTE_CLOUD_AUDIENCE = "account" 145AIRBYTE_CLOUD_ALGORITHM = "RS256" 146 147# Headless JWT verifier claim/algorithm family. This server's Airbyte-branded 148# env vars, each paired with the Airbyte Cloud default `_create_auth` applies. 149# Because these defaults are always present, HTTP transport always verifies 150# bearer tokens. Setting any matching env var overrides the Cloud default — the 151# escape hatch for self-hosted deployments pointing at their own Airbyte 152# instance. These carry the `AUTH` segment; `OIDC_*` vars keep `OIDC` alone (it 153# already denotes auth). 154# 155# The signing-key source (`AIRBYTE_MCP_AUTH_JWKS_URI` / 156# `AIRBYTE_MCP_AUTH_JWT_PUBLIC_KEY`) is resolved separately in `_create_auth`, 157# because the JWKS default must apply only when neither key source is set (see 158# `_resolve_signing_key`). 159JWT_ISSUER_ENV = "AIRBYTE_MCP_AUTH_ISSUER" 160JWT_AUDIENCE_ENV = "AIRBYTE_MCP_AUTH_AUDIENCE" 161JWT_ALGORITHM_ENV = "AIRBYTE_MCP_AUTH_ALGORITHM" 162 163# Signing-key sources for the headless JWT verifier. A deployment may point at a 164# JWKS endpoint (`AIRBYTE_MCP_AUTH_JWKS_URI`) or supply a static public key 165# (`AIRBYTE_MCP_AUTH_JWT_PUBLIC_KEY`, for self-hosted realms without a JWKS 166# endpoint). The Airbyte Cloud JWKS default applies only when neither is set. 167JWKS_URI_ENV = "AIRBYTE_MCP_AUTH_JWKS_URI" 168JWT_PUBLIC_KEY_ENV = "AIRBYTE_MCP_AUTH_JWT_PUBLIC_KEY" 169 170# Interactive OIDC env vars. The client credentials are secret, so they have no 171# default and must be supplied by the deployment to activate the interactive 172# path. The discovery URL defaults to Airbyte Cloud but is only injected when 173# the credentials are present (see `_create_auth`). 174OIDC_CLIENT_ID_ENV = "AIRBYTE_MCP_OIDC_CLIENT_ID" 175OIDC_CLIENT_SECRET_ENV = "AIRBYTE_MCP_OIDC_CLIENT_SECRET" 176OIDC_CONFIG_URL_ENV = "AIRBYTE_MCP_OIDC_CONFIG_URL" 177# CIMD (Client ID Metadata Document) is enabled by default so broad OAuth 178# clients that only implement CIMD can authenticate — notably Goose Desktop, 179# which hardcodes a metadata-document URL as its `client_id` and has no DCR 180# fallback. An operator can force it off with `...=false` to mitigate an auth 181# issue without a redeploy. `OIDCAuthConfig.enable_cimd` defaults to `False` 182# upstream, so this server opts in explicitly. 183OIDC_ENABLE_CIMD_ENV = "AIRBYTE_MCP_OIDC_ENABLE_CIMD" 184 185# Human-facing landing page shown when a browser GETs the MCP endpoint. 186MCP_LANDING_TITLE = "Airbyte Ops MCP Server" 187MCP_LANDING_DOCS_URL = "https://github.com/airbytehq/airbyte-ops-mcp#readme" 188 189 190def _normalize_bearer_token(value: str) -> str | None: 191 """Extract bearer token from Authorization header value. 192 193 Parses "Bearer <token>" format (case-insensitive prefix). 194 Returns None if the value doesn't have the Bearer prefix. 195 """ 196 if value.lower().startswith("bearer "): 197 token = value[7:].strip() 198 return token if token else None 199 return None 200 201 202def _resolve_transport_bearer_token() -> str: 203 """Resolve the verified transport bearer token if available. 204 205 FastMCP stores the access token of the current request after the transport 206 auth provider verifies it — the Okta token for interactive `OIDCProxy`, or 207 the client-minted JWT for headless `JWTVerifier`. Both are Airbyte Cloud 208 tokens when the server verifies against Airbyte Cloud's realm, so reusing 209 the token as the downstream Cloud API bearer gives the caller's identity 210 delegated access without a second credential. 211 212 Returns empty string when no verified token is present (e.g. stdio mode). 213 """ 214 access_token = get_access_token() 215 if access_token and access_token.token: 216 return access_token.token 217 return "" 218 219 220def _env_or_default(name: str, default: str) -> str: 221 """Return the stripped value of env var `name`, or `default` when unset/blank. 222 223 An env var set to an empty or whitespace-only string is treated as unset, so 224 the baked default still applies and no blank value is propagated downstream 225 (a `" "` JWKS URI or server URL would otherwise break auth resolution). 226 """ 227 return os.getenv(name, "").strip() or default 228 229 230def _env_bool(name: str, *, default: bool) -> bool: 231 """Return the boolean value of env var `name`, or `default` when unset/blank. 232 233 Recognizes `true`/`false`, `1`/`0`, `yes`/`no`, `on`/`off` (case-insensitive). 234 A blank or whitespace-only value is treated as unset so the baked default 235 applies. An unrecognized value raises `ValueError` rather than silently 236 coercing a typo (e.g. `flase`) to `False`. 237 """ 238 raw = os.getenv(name, "").strip().lower() 239 if not raw: 240 return default 241 if raw in ("true", "1", "yes", "on"): 242 return True 243 if raw in ("false", "0", "no", "off"): 244 return False 245 raise ValueError( 246 f"{name} must be a boolean (true/false/1/0/yes/no/on/off), got '{raw}'." 247 ) 248 249 250def _resolve_signing_key() -> tuple[str, str]: 251 """Resolve the headless JWT verifier's signing-key source. 252 253 Returns the `(jwks_uri, public_key)` pair. A deployment may set either env 254 var to point at its own realm; the Airbyte Cloud JWKS default applies only 255 when *neither* is set, so a self-hosted static public key isn't shadowed by a 256 leftover Cloud JWKS URI. Blank or whitespace-only values are treated as 257 unset, and an unset member is returned as the empty string. 258 """ 259 jwks_uri = os.getenv(JWKS_URI_ENV, "").strip() 260 public_key = os.getenv(JWT_PUBLIC_KEY_ENV, "").strip() 261 if not jwks_uri and not public_key: 262 jwks_uri = AIRBYTE_CLOUD_JWKS_URI 263 return jwks_uri, public_key 264 265 266def _create_auth() -> AuthProvider | None: 267 """Assemble the transport auth provider, defaulting to Airbyte Cloud. 268 269 Reads this server's `AIRBYTE_MCP_*` env vars (falling back to Airbyte Cloud's 270 public realm defaults), maps them into the typed `JWTAuthConfig` / 271 `OIDCAuthConfig` objects that `fastmcp_extensions.build_mcp_auth` consumes, 272 and lets it wire up a headless `JWTVerifier` and/or an interactive 273 `OIDCProxy`, combined via `MultiAuth`. Because a JWKS default is always 274 present, HTTP transport always verifies bearer tokens; the interactive path 275 additionally activates once the OIDC client credentials are supplied. 276 """ 277 base_url = _env_or_default(MCP_SERVER_URL_ENV, DEFAULT_MCP_SERVER_URL) 278 279 # Headless JWT verification is always configured (the Airbyte Cloud JWKS 280 # default is present whenever the deployment sets no key source of its own). 281 jwks_uri, public_key = _resolve_signing_key() 282 jwt = JWTAuthConfig( 283 jwks_uri=jwks_uri or None, 284 public_key=public_key or None, 285 issuer=_env_or_default(JWT_ISSUER_ENV, AIRBYTE_CLOUD_ISSUER), 286 audience=_env_or_default(JWT_AUDIENCE_ENV, AIRBYTE_CLOUD_AUDIENCE), 287 algorithm=_env_or_default(JWT_ALGORITHM_ENV, AIRBYTE_CLOUD_ALGORITHM), 288 base_url=base_url, 289 ) 290 291 # Interactive OIDC activates only when both client credentials are present. 292 # Building it on the headless/bearer-only path would advertise an OIDC 293 # discovery URL with no credentials behind it. 294 oidc: OIDCAuthConfig | None = None 295 oidc_client_id = os.getenv(OIDC_CLIENT_ID_ENV, "").strip() 296 oidc_client_secret = os.getenv(OIDC_CLIENT_SECRET_ENV, "").strip() 297 if oidc_client_id and oidc_client_secret: 298 # Durable, encrypted backend for `OIDCProxy`'s OAuth state so interactive 299 # sessions survive restarts and span replicas. Returns `None` (keeping 300 # the in-memory default) unless `AIRBYTE_MCP_OIDC_STORAGE=firestore`. The 301 # encryption key is derived from the OIDC client secret this server 302 # already holds, so no separate encryption secret is provisioned. 303 oidc = OIDCAuthConfig( 304 config_url=_env_or_default( 305 OIDC_CONFIG_URL_ENV, AIRBYTE_CLOUD_OIDC_CONFIG_URL 306 ), 307 client_id=oidc_client_id, 308 client_secret=oidc_client_secret, 309 base_url=base_url, 310 # Advertise and accept the CIMD flow (URL `client_id`) so broad OAuth 311 # clients that only implement CIMD — notably Goose Desktop — can 312 # authenticate. The key-normalizing storage wrapper (see 313 # `_oidc_storage`) is what makes the URL `client_id` storable; 314 # without it the CIMD `/authorize` path crashes with a Firestore 315 # `InvalidArgument`. 316 enable_cimd=_env_bool(OIDC_ENABLE_CIMD_ENV, default=True), 317 client_storage=resolve_oidc_client_storage( 318 encryption_source_material=oidc_client_secret 319 ), 320 ) 321 322 return build_mcp_auth(oidc=oidc, jwt=jwt, base_url=base_url) 323 324 325# Create the MCP server with built-in server info resource 326app = mcp_server( 327 name=MCP_SERVER_NAME, 328 instructions=MCP_SERVER_INSTRUCTIONS, 329 package_name="airbyte-internal-ops", 330 advertised_properties={ 331 "docs_url": "https://github.com/airbytehq/airbyte-ops-mcp", 332 "release_history_url": "https://github.com/airbytehq/airbyte-ops-mcp/releases", 333 }, 334 server_config_args=[ 335 MCPServerConfigArg( 336 name=ServerConfigKey.BEARER_TOKEN, 337 http_header_key="Authorization", 338 env_var="AIRBYTE_CLOUD_BEARER_TOKEN", 339 normalize_fn=_normalize_bearer_token, 340 default=_resolve_transport_bearer_token, 341 required=False, 342 sensitive=True, 343 ), 344 MCPServerConfigArg( 345 name=ServerConfigKey.CLIENT_ID, 346 http_header_key=HEADER_AIRBYTE_CLOUD_CLIENT_ID, 347 default=lambda: str(resolve_cloud_client_id()), 348 required=True, 349 sensitive=True, 350 ), 351 MCPServerConfigArg( 352 name=ServerConfigKey.CLIENT_SECRET, 353 http_header_key=HEADER_AIRBYTE_CLOUD_CLIENT_SECRET, 354 default=lambda: str(resolve_cloud_client_secret()), 355 required=True, 356 sensitive=True, 357 ), 358 ], 359 include_standard_tool_filters=True, 360 auth=_create_auth(), 361) 362 363 364def register_server_assets(app: FastMCP) -> None: 365 """Register all server assets (tools, prompts, resources) with the FastMCP app. 366 367 Tools are grouped into domain-oriented modules to keep the generated pdoc 368 reference navigable: 369 370 - `connector_versions`: cloud version overrides, rollouts, pre-release publish 371 - `connector_registry`: registry reads/yank plus monorepo list/bump 372 - `connector_qa`: regression tests and release blocking 373 - `connection_medic`: connection state/catalog reads plus emergency writes 374 - `prod_db_ops`: Prod Cloud DB-replica SQL queries 375 - `logging`: GCP Cloud Logging backend-error lookup 376 - `context_store_ops`: MotherDuck / context-store diagnostics 377 - `organization_admin`: is_agentic flag, payment config, customer tiers 378 - `github_ops`: CI workflow trigger/status, Docker image info, subscriptions 379 - `human_in_the_loop`: human escalation, team-roster lookup, Slack newsletter posting 380 - `devin_ops`: reminders, secret requests, session feedback and naming 381 - `zendesk_ops`: read-only Zendesk Support ticket retrieval 382 - `prompts`: prompt templates for common workflows 383 384 Tools annotated with `requires_client_filesystem=True` are automatically 385 hidden when `MCP_NO_CLIENT_FILESYSTEM=1` via the standard tool filter. 386 387 Note: Server info resource is now built-in via `mcp_server()` helper. 388 389 Args: 390 app: FastMCP application instance 391 """ 392 register_connector_version_tools(app) 393 register_connector_registry_tools(app) 394 register_connector_qa_tools(app) 395 register_connection_medic_tools(app) 396 register_prod_db_ops_tools(app) 397 register_logging_tools(app) 398 register_context_store_ops_tools(app) 399 register_organization_admin_tools(app) 400 register_github_ops_tools(app) 401 register_human_in_the_loop_tools(app) 402 register_devin_ops_tools(app) 403 register_zendesk_ops_tools(app) 404 register_prompts(app) 405 406 407register_server_assets(app) 408app.add_middleware( 409 ToolCallTelemetryMiddleware( 410 package_name="airbyte-internal-ops", 411 sentry_dsn=_SENTRY_DSN, 412 segment_write_key=_DEFAULT_SEGMENT_WRITE_KEY, 413 ) 414) 415 416 417@app.custom_route("/health", methods=["GET"]) 418async def health_check(request: Request) -> JSONResponse: 419 """Health check endpoint for Cloud Run liveness/readiness probes.""" 420 return JSONResponse({"status": "ok"}) 421 422 423def _load_env() -> None: 424 """Load environment variables from .env file if present.""" 425 env_file = Path.cwd() / ".env" 426 if env_file.exists(): 427 load_dotenv(env_file) 428 print(f"Loaded environment from: {env_file}", flush=True, file=sys.stderr) 429 430 431def main() -> None: 432 """Main entry point for the Airbyte Admin MCP server (stdio mode). 433 434 This is the default entry point that runs the server in stdio mode, 435 suitable for direct MCP client connections. 436 """ 437 _load_env() 438 init_sentry_tracking() 439 440 print("=" * 60, flush=True, file=sys.stderr) 441 print("Starting Airbyte Admin MCP server (stdio mode).", file=sys.stderr) 442 try: 443 asyncio.run(app.run_stdio_async(show_banner=False)) 444 except KeyboardInterrupt: 445 print("Airbyte Admin MCP server interrupted by user.", file=sys.stderr) 446 447 print("Airbyte Admin MCP server stopped.", file=sys.stderr) 448 print("=" * 60, flush=True, file=sys.stderr) 449 450 451def main_http() -> None: 452 """HTTP entry point for the Airbyte Admin MCP server. 453 454 Runs the server in HTTP mode. When OIDC env vars are configured, 455 Keycloak authentication is enabled automatically. 456 """ 457 _load_env() 458 init_sentry_tracking() 459 460 host = DEFAULT_HTTP_HOST 461 port = DEFAULT_HTTP_PORT 462 463 # When deployed behind a path-stripping LB (MCP_SERVER_URL has a path 464 # component like /ops-mcp), serve the MCP endpoint at root so the 465 # public URL is just the base path. Otherwise keep the FastMCP default. 466 server_url = _env_or_default(MCP_SERVER_URL_ENV, DEFAULT_MCP_SERVER_URL) 467 mcp_path = "/" if urlparse(server_url).path.strip("/") else "/mcp" 468 469 if getattr(app, "auth", None) is None: 470 logger.warning( 471 "HTTP transport starting without authentication: no headless " 472 "bearer-token or interactive OIDC auth resolved, so every request " 473 "is unauthenticated. This is unexpected — headless verification " 474 "defaults to the Airbyte Cloud realm, so auth should normally always " 475 "be active. Reaching this state means the signing-key source could " 476 "not be resolved (e.g. `AIRBYTE_MCP_AUTH_JWKS_URI` set to an " 477 "unreachable URL). Verify your `AIRBYTE_MCP_AUTH_*` overrides." 478 ) 479 480 # The advertised endpoint must match where the MCP route is actually mounted: 481 # the bare server URL when mounted at root, otherwise the server URL + mcp_path. 482 endpoint_url = server_url if mcp_path == "/" else server_url.rstrip("/") + mcp_path 483 484 # Serve a browser-friendly landing page on GET at the MCP path. In stateless 485 # mode FastMCP only binds POST/DELETE there, so this GET route does not 486 # interfere with MCP traffic. 487 register_landing_page( 488 app, 489 path=mcp_path, 490 title=MCP_LANDING_TITLE, 491 endpoint_url=endpoint_url, 492 docs_url=MCP_LANDING_DOCS_URL, 493 ) 494 495 print("=" * 60, flush=True, file=sys.stderr) 496 print( 497 f"Starting Airbyte Admin MCP server (HTTP mode) on {host}:{port}" 498 f" (mcp_path={mcp_path!r})", 499 file=sys.stderr, 500 ) 501 # Build the ASGI app ourselves (rather than `app.run`) so the optional 502 # client-credentials exchange can wrap it as the *outermost* layer — ahead 503 # of FastMCP's auth middleware — so its Basic-to-Bearer rewrite is what the 504 # verifier sees. When the opt-in flag is unset, `wrap_if_enabled` returns the 505 # app unchanged. The Starlette app owns the session-manager lifespan, so 506 # running it under uvicorn directly is equivalent to `app.run`. 507 http_app = app.http_app( 508 path=mcp_path, 509 transport="streamable-http", 510 stateless_http=True, 511 ) 512 try: 513 uvicorn.run( 514 wrap_if_enabled(http_app), 515 host=host, 516 port=port, 517 ) 518 except KeyboardInterrupt: 519 print("Airbyte Admin MCP server interrupted by user.", file=sys.stderr) 520 521 print("Airbyte Admin MCP server stopped.", file=sys.stderr) 522 print("=" * 60, flush=True, file=sys.stderr) 523 524 525if __name__ == "__main__": 526 main()
365def register_server_assets(app: FastMCP) -> None: 366 """Register all server assets (tools, prompts, resources) with the FastMCP app. 367 368 Tools are grouped into domain-oriented modules to keep the generated pdoc 369 reference navigable: 370 371 - `connector_versions`: cloud version overrides, rollouts, pre-release publish 372 - `connector_registry`: registry reads/yank plus monorepo list/bump 373 - `connector_qa`: regression tests and release blocking 374 - `connection_medic`: connection state/catalog reads plus emergency writes 375 - `prod_db_ops`: Prod Cloud DB-replica SQL queries 376 - `logging`: GCP Cloud Logging backend-error lookup 377 - `context_store_ops`: MotherDuck / context-store diagnostics 378 - `organization_admin`: is_agentic flag, payment config, customer tiers 379 - `github_ops`: CI workflow trigger/status, Docker image info, subscriptions 380 - `human_in_the_loop`: human escalation, team-roster lookup, Slack newsletter posting 381 - `devin_ops`: reminders, secret requests, session feedback and naming 382 - `zendesk_ops`: read-only Zendesk Support ticket retrieval 383 - `prompts`: prompt templates for common workflows 384 385 Tools annotated with `requires_client_filesystem=True` are automatically 386 hidden when `MCP_NO_CLIENT_FILESYSTEM=1` via the standard tool filter. 387 388 Note: Server info resource is now built-in via `mcp_server()` helper. 389 390 Args: 391 app: FastMCP application instance 392 """ 393 register_connector_version_tools(app) 394 register_connector_registry_tools(app) 395 register_connector_qa_tools(app) 396 register_connection_medic_tools(app) 397 register_prod_db_ops_tools(app) 398 register_logging_tools(app) 399 register_context_store_ops_tools(app) 400 register_organization_admin_tools(app) 401 register_github_ops_tools(app) 402 register_human_in_the_loop_tools(app) 403 register_devin_ops_tools(app) 404 register_zendesk_ops_tools(app) 405 register_prompts(app)
Register all server assets (tools, prompts, resources) with the FastMCP app.
Tools are grouped into domain-oriented modules to keep the generated pdoc reference navigable:
connector_versions: cloud version overrides, rollouts, pre-release publishconnector_registry: registry reads/yank plus monorepo list/bumpconnector_qa: regression tests and release blockingconnection_medic: connection state/catalog reads plus emergency writesprod_db_ops: Prod Cloud DB-replica SQL querieslogging: GCP Cloud Logging backend-error lookupcontext_store_ops: MotherDuck / context-store diagnosticsorganization_admin: is_agentic flag, payment config, customer tiersgithub_ops: CI workflow trigger/status, Docker image info, subscriptionshuman_in_the_loop: human escalation, team-roster lookup, Slack newsletter postingdevin_ops: reminders, secret requests, session feedback and namingzendesk_ops: read-only Zendesk Support ticket retrievalprompts: prompt templates for common workflows
Tools annotated with requires_client_filesystem=True are automatically
hidden when MCP_NO_CLIENT_FILESYSTEM=1 via the standard tool filter.
Note: Server info resource is now built-in via mcp_server() helper.
Arguments:
- app: FastMCP application instance
418@app.custom_route("/health", methods=["GET"]) 419async def health_check(request: Request) -> JSONResponse: 420 """Health check endpoint for Cloud Run liveness/readiness probes.""" 421 return JSONResponse({"status": "ok"})
Health check endpoint for Cloud Run liveness/readiness probes.
432def main() -> None: 433 """Main entry point for the Airbyte Admin MCP server (stdio mode). 434 435 This is the default entry point that runs the server in stdio mode, 436 suitable for direct MCP client connections. 437 """ 438 _load_env() 439 init_sentry_tracking() 440 441 print("=" * 60, flush=True, file=sys.stderr) 442 print("Starting Airbyte Admin MCP server (stdio mode).", file=sys.stderr) 443 try: 444 asyncio.run(app.run_stdio_async(show_banner=False)) 445 except KeyboardInterrupt: 446 print("Airbyte Admin MCP server interrupted by user.", file=sys.stderr) 447 448 print("Airbyte Admin MCP server stopped.", file=sys.stderr) 449 print("=" * 60, flush=True, file=sys.stderr)
Main entry point for the Airbyte Admin MCP server (stdio mode).
This is the default entry point that runs the server in stdio mode, suitable for direct MCP client connections.
452def main_http() -> None: 453 """HTTP entry point for the Airbyte Admin MCP server. 454 455 Runs the server in HTTP mode. When OIDC env vars are configured, 456 Keycloak authentication is enabled automatically. 457 """ 458 _load_env() 459 init_sentry_tracking() 460 461 host = DEFAULT_HTTP_HOST 462 port = DEFAULT_HTTP_PORT 463 464 # When deployed behind a path-stripping LB (MCP_SERVER_URL has a path 465 # component like /ops-mcp), serve the MCP endpoint at root so the 466 # public URL is just the base path. Otherwise keep the FastMCP default. 467 server_url = _env_or_default(MCP_SERVER_URL_ENV, DEFAULT_MCP_SERVER_URL) 468 mcp_path = "/" if urlparse(server_url).path.strip("/") else "/mcp" 469 470 if getattr(app, "auth", None) is None: 471 logger.warning( 472 "HTTP transport starting without authentication: no headless " 473 "bearer-token or interactive OIDC auth resolved, so every request " 474 "is unauthenticated. This is unexpected — headless verification " 475 "defaults to the Airbyte Cloud realm, so auth should normally always " 476 "be active. Reaching this state means the signing-key source could " 477 "not be resolved (e.g. `AIRBYTE_MCP_AUTH_JWKS_URI` set to an " 478 "unreachable URL). Verify your `AIRBYTE_MCP_AUTH_*` overrides." 479 ) 480 481 # The advertised endpoint must match where the MCP route is actually mounted: 482 # the bare server URL when mounted at root, otherwise the server URL + mcp_path. 483 endpoint_url = server_url if mcp_path == "/" else server_url.rstrip("/") + mcp_path 484 485 # Serve a browser-friendly landing page on GET at the MCP path. In stateless 486 # mode FastMCP only binds POST/DELETE there, so this GET route does not 487 # interfere with MCP traffic. 488 register_landing_page( 489 app, 490 path=mcp_path, 491 title=MCP_LANDING_TITLE, 492 endpoint_url=endpoint_url, 493 docs_url=MCP_LANDING_DOCS_URL, 494 ) 495 496 print("=" * 60, flush=True, file=sys.stderr) 497 print( 498 f"Starting Airbyte Admin MCP server (HTTP mode) on {host}:{port}" 499 f" (mcp_path={mcp_path!r})", 500 file=sys.stderr, 501 ) 502 # Build the ASGI app ourselves (rather than `app.run`) so the optional 503 # client-credentials exchange can wrap it as the *outermost* layer — ahead 504 # of FastMCP's auth middleware — so its Basic-to-Bearer rewrite is what the 505 # verifier sees. When the opt-in flag is unset, `wrap_if_enabled` returns the 506 # app unchanged. The Starlette app owns the session-manager lifespan, so 507 # running it under uvicorn directly is equivalent to `app.run`. 508 http_app = app.http_app( 509 path=mcp_path, 510 transport="streamable-http", 511 stateless_http=True, 512 ) 513 try: 514 uvicorn.run( 515 wrap_if_enabled(http_app), 516 host=host, 517 port=port, 518 ) 519 except KeyboardInterrupt: 520 print("Airbyte Admin MCP server interrupted by user.", file=sys.stderr) 521 522 print("Airbyte Admin MCP server stopped.", file=sys.stderr) 523 print("=" * 60, flush=True, file=sys.stderr)
HTTP entry point for the Airbyte Admin MCP server.
Runs the server in HTTP mode. When OIDC env vars are configured, Keycloak authentication is enabled automatically.