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 that fastmcp_extensions.build_mcp_auth consumes, which supports two client shapes on the same deployment:
    • Interactive (humans in a browser): Keycloak Authorization Code + PKCE via OIDCProxy, active once AIRBYTE_MCP_OIDC_CLIENT_ID and AIRBYTE_MCP_OIDC_CLIENT_SECRET are 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 a JWTVerifier against Airbyte Cloud's application-client realm by default (no browser, no stored/rotating refresh token). When both are active they are combined via MultiAuth.

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()
logger = <Logger airbyte_ops_mcp.mcp.server (WARNING)>
DEFAULT_HTTP_HOST = '0.0.0.0'
DEFAULT_HTTP_PORT = 8080
MCP_SERVER_URL_ENV = 'MCP_SERVER_URL'
DEFAULT_MCP_SERVER_URL = 'http://localhost:8080'
AIRBYTE_CLOUD_OIDC_CONFIG_URL = 'https://cloud.airbyte.com/auth/realms/airbyte/.well-known/openid-configuration'
AIRBYTE_CLOUD_ISSUER = 'https://cloud.airbyte.com/auth/realms/_airbyte-application-clients'
AIRBYTE_CLOUD_JWKS_URI = 'https://cloud.airbyte.com/auth/realms/_airbyte-application-clients/protocol/openid-connect/certs'
AIRBYTE_CLOUD_AUDIENCE = 'account'
AIRBYTE_CLOUD_ALGORITHM = 'RS256'
JWT_ISSUER_ENV = 'AIRBYTE_MCP_AUTH_ISSUER'
JWT_AUDIENCE_ENV = 'AIRBYTE_MCP_AUTH_AUDIENCE'
JWT_ALGORITHM_ENV = 'AIRBYTE_MCP_AUTH_ALGORITHM'
JWKS_URI_ENV = 'AIRBYTE_MCP_AUTH_JWKS_URI'
JWT_PUBLIC_KEY_ENV = 'AIRBYTE_MCP_AUTH_JWT_PUBLIC_KEY'
OIDC_CLIENT_ID_ENV = 'AIRBYTE_MCP_OIDC_CLIENT_ID'
OIDC_CLIENT_SECRET_ENV = 'AIRBYTE_MCP_OIDC_CLIENT_SECRET'
OIDC_CONFIG_URL_ENV = 'AIRBYTE_MCP_OIDC_CONFIG_URL'
OIDC_ENABLE_CIMD_ENV = 'AIRBYTE_MCP_OIDC_ENABLE_CIMD'
MCP_LANDING_TITLE = 'Airbyte Ops MCP Server'
MCP_LANDING_DOCS_URL = 'https://github.com/airbytehq/airbyte-ops-mcp#readme'
app = FastMCP('airbyte-internal-ops')
def register_server_assets(app: fastmcp.server.server.FastMCP) -> None:
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 publish
  • connector_registry: registry reads/yank plus monorepo list/bump
  • connector_qa: regression tests and release blocking
  • connection_medic: connection state/catalog reads plus emergency writes
  • prod_db_ops: Prod Cloud DB-replica SQL queries
  • logging: GCP Cloud Logging backend-error lookup
  • context_store_ops: MotherDuck / context-store diagnostics
  • organization_admin: is_agentic flag, payment config, customer tiers
  • github_ops: CI workflow trigger/status, Docker image info, subscriptions
  • human_in_the_loop: human escalation, team-roster lookup, Slack newsletter posting
  • devin_ops: reminders, secret requests, session feedback and naming
  • zendesk_ops: read-only Zendesk Support ticket retrieval
  • prompts: 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
@app.custom_route('/health', methods=['GET'])
async def health_check(request: starlette.requests.Request) -> starlette.responses.JSONResponse:
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.

def main() -> None:
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.

def main_http() -> None:
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.