airbyte_ops_mcp.mcp.prerelease

MCP tools for triggering connector pre-release workflows.

This module provides MCP tools for triggering the publish-connectors-prerelease workflow in the airbytehq/airbyte repository (for OSS connectors) or the publish_enterprise_connectors workflow in airbytehq/airbyte-enterprise (for enterprise connectors) via GitHub's workflow dispatch API.

MCP reference

MCP primitives registered by the prerelease module of the airbyte-internal-ops server: 1 tool(s), 0 prompt(s), 0 resource(s).

Tools (1)

publish_connector_to_airbyte_registry

Hints: open-world

Publish a connector to the Airbyte registry.

Currently only supports pre-release publishing. This tool triggers the publish-connectors-prerelease workflow in the airbytehq/airbyte repository (for OSS connectors) or the publish_enterprise_connectors workflow in airbytehq/airbyte-enterprise (for enterprise connectors), which publishes a pre-release version of the specified connector from the PR branch.

Pre-release versions are tagged with the format: {version}-preview.{7-char-git-sha} These versions are available for version pinning via the scoped_configuration API.

Requires GITHUB_CONNECTOR_PUBLISHING_PAT or GITHUB_TOKEN environment variable with 'actions:write' permission.

Parameters:

Name Type Required Default Description
connector_name string yes The connector name to publish (e.g., 'source-github', 'destination-postgres')
pr_number integer yes The pull request number containing the connector changes
repo enum("airbyte", "airbyte-enterprise") no "airbyte" Repository where the connector PR is located. Use 'airbyte' for OSS connectors (default) or 'airbyte-enterprise' for enterprise connectors.
prerelease boolean no true Must be True. Only prerelease publishing is supported at this time.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "connector_name": {
      "description": "The connector name to publish (e.g., 'source-github', 'destination-postgres')",
      "type": "string"
    },
    "pr_number": {
      "description": "The pull request number containing the connector changes",
      "type": "integer"
    },
    "repo": {
      "description": "Repository where the connector PR is located. Use 'airbyte' for OSS connectors (default) or 'airbyte-enterprise' for enterprise connectors.",
      "enum": [
        "airbyte",
        "airbyte-enterprise"
      ],
      "type": "string",
      "default": "airbyte"
    },
    "prerelease": {
      "const": true,
      "default": true,
      "description": "Must be True. Only prerelease publishing is supported at this time.",
      "type": "boolean"
    }
  },
  "required": [
    "connector_name",
    "pr_number"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Response model for publish_connector_to_airbyte_registry MCP tool.",
  "properties": {
    "success": {
      "type": "boolean"
    },
    "message": {
      "type": "string"
    },
    "workflow_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "connector_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "pr_number": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "docker_image": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "docker_image_tag": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "required": [
    "success",
    "message"
  ],
  "type": "object"
}

  1# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
  2"""MCP tools for triggering connector pre-release workflows.
  3
  4This module provides MCP tools for triggering the publish-connectors-prerelease
  5workflow in the airbytehq/airbyte repository (for OSS connectors) or the
  6publish_enterprise_connectors workflow in airbytehq/airbyte-enterprise
  7(for enterprise connectors) via GitHub's workflow dispatch API.
  8
  9## MCP reference
 10
 11.. include:: ../../../docs/mcp-generated/prerelease.md
 12    :start-line: 2
 13"""
 14
 15from __future__ import annotations
 16
 17__all__: list[str] = []
 18
 19import base64
 20from enum import StrEnum
 21from typing import Annotated, Literal
 22
 23import requests
 24import yaml
 25from fastmcp import FastMCP
 26from fastmcp_extensions import mcp_tool, register_mcp_tools
 27from pydantic import BaseModel, Field
 28
 29from airbyte_ops_mcp.airbyte_repo.bump_version import strip_prerelease_suffix
 30from airbyte_ops_mcp.github_actions import trigger_workflow_dispatch
 31from airbyte_ops_mcp.github_api import (
 32    GITHUB_API_BASE,
 33    get_pr_head_ref,
 34    resolve_ci_trigger_github_token,
 35)
 36
 37
 38class ConnectorRepo(StrEnum):
 39    """Repository where connector code is located."""
 40
 41    AIRBYTE = "airbyte"
 42    AIRBYTE_ENTERPRISE = "airbyte-enterprise"
 43
 44
 45DEFAULT_REPO_OWNER = "airbytehq"
 46DEFAULT_REPO_NAME = ConnectorRepo.AIRBYTE
 47DEFAULT_BRANCH = "master"
 48PRERELEASE_WORKFLOW_FILE = "publish-connectors-prerelease-command.yml"
 49CONNECTOR_PATH_PREFIX = "airbyte-integrations/connectors"
 50
 51# Enterprise repository constants
 52ENTERPRISE_REPO_NAME = ConnectorRepo.AIRBYTE_ENTERPRISE
 53ENTERPRISE_DEFAULT_BRANCH = "main"
 54ENTERPRISE_PRERELEASE_WORKFLOW_FILE = "publish_enterprise_connectors.yml"
 55
 56# Token env vars for prerelease publishing (in order of preference)
 57PRERELEASE_TOKEN_ENV_VARS = [
 58    "GITHUB_CONNECTOR_PUBLISHING_PAT",
 59    "GITHUB_CI_WORKFLOW_TRIGGER_PAT",
 60    "GITHUB_TOKEN",
 61]
 62
 63# =============================================================================
 64# Pre-release Version Tag Constants
 65# =============================================================================
 66
 67PRERELEASE_TAG_PREFIX = "preview"
 68"""The prefix used for pre-release version tags (e.g., '1.2.3-preview.abcde12')."""
 69
 70PRERELEASE_SHA_LENGTH = 7
 71"""The number of characters from the git SHA to include in pre-release tags."""
 72
 73
 74def compute_prerelease_docker_image_tag(base_version: str, sha: str) -> str:
 75    """Compute the pre-release docker image tag.
 76
 77    This is the SINGLE SOURCE OF TRUTH for pre-release version format.
 78    All other code should receive this value as a parameter, not recompute it.
 79
 80    The format is: {base_version}-preview.{short_sha}
 81
 82    Where:
 83        - base_version: The base version from metadata.yaml (e.g., "1.2.3"),
 84          which may already contain a pre-release suffix (e.g., "2.23.16-rc.1").
 85          Any existing pre-release suffix is stripped before applying the preview tag.
 86        - short_sha: The first 7 characters of the git commit SHA
 87
 88    Examples:
 89        >>> compute_prerelease_docker_image_tag("1.2.3", "abcdef1234567890")
 90        '1.2.3-preview.abcdef1'
 91        >>> compute_prerelease_docker_image_tag("0.1.0", "1234567")
 92        '0.1.0-preview.1234567'
 93        >>> compute_prerelease_docker_image_tag("2.23.16-rc.1", "abcdef1234567890")
 94        '2.23.16-preview.abcdef1'
 95
 96    Args:
 97        base_version: The base version from metadata.yaml (e.g., "1.2.3" or "2.23.16-rc.1")
 98        sha: The full git commit SHA (or at least 7 characters)
 99
100    Returns:
101        Pre-release version tag (e.g., "1.2.3-preview.abcde12")
102    """
103    short_sha = sha[:PRERELEASE_SHA_LENGTH]
104    clean_version = strip_prerelease_suffix(base_version)
105    return f"{clean_version}-{PRERELEASE_TAG_PREFIX}.{short_sha}"
106
107
108class PrereleaseWorkflowResult(BaseModel):
109    """Response model for publish_connector_to_airbyte_registry MCP tool."""
110
111    success: bool
112    message: str
113    workflow_url: str | None = None
114    connector_name: str | None = None
115    pr_number: int | None = None
116    docker_image: str | None = None
117    docker_image_tag: str | None = None
118
119
120def _get_connector_metadata(
121    owner: str,
122    repo: str,
123    connector_name: str,
124    ref: str,
125    token: str,
126) -> dict | None:
127    """Fetch and parse connector metadata.yaml from the repository.
128
129    Args:
130        owner: Repository owner (e.g., "airbytehq")
131        repo: Repository name (e.g., "airbyte")
132        connector_name: Connector name (e.g., "source-github")
133        ref: Git ref to fetch from (branch name or SHA)
134        token: GitHub API token
135
136    Returns:
137        Parsed metadata dictionary, or None if not found.
138    """
139    metadata_path = f"{CONNECTOR_PATH_PREFIX}/{connector_name}/metadata.yaml"
140    url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/contents/{metadata_path}"
141    headers = {
142        "Authorization": f"Bearer {token}",
143        "Accept": "application/vnd.github+json",
144        "X-GitHub-Api-Version": "2022-11-28",
145    }
146    params = {"ref": ref}
147
148    response = requests.get(url, headers=headers, params=params, timeout=30)
149
150    # Guard: Return None if metadata file not found
151    if response.status_code == 404:
152        return None
153
154    response.raise_for_status()
155
156    content_data = response.json()
157
158    # Guard: Return None if content is not base64 encoded
159    if content_data.get("encoding") != "base64":
160        return None
161
162    content = base64.b64decode(content_data["content"]).decode("utf-8")
163    return yaml.safe_load(content)
164
165
166@mcp_tool(
167    read_only=False,
168    destructive=False,
169    idempotent=False,
170    open_world=True,
171)
172def publish_connector_to_airbyte_registry(
173    connector_name: Annotated[
174        str,
175        Field(
176            description="The connector name to publish (e.g., 'source-github', 'destination-postgres')"
177        ),
178    ],
179    pr_number: Annotated[
180        int,
181        Field(description="The pull request number containing the connector changes"),
182    ],
183    repo: Annotated[
184        ConnectorRepo,
185        Field(
186            default=ConnectorRepo.AIRBYTE,
187            description="Repository where the connector PR is located. "
188            "Use 'airbyte' for OSS connectors (default) or 'airbyte-enterprise' for enterprise connectors.",
189        ),
190    ],
191    prerelease: Annotated[
192        Literal[True],
193        Field(
194            default=True,
195            description="Must be True. Only prerelease publishing is supported at this time.",
196        ),
197    ],
198) -> PrereleaseWorkflowResult:
199    """Publish a connector to the Airbyte registry.
200
201    Currently only supports pre-release publishing. This tool triggers the
202    publish-connectors-prerelease workflow in the airbytehq/airbyte repository
203    (for OSS connectors) or the publish_enterprise_connectors workflow in
204    airbytehq/airbyte-enterprise (for enterprise connectors), which publishes
205    a pre-release version of the specified connector from the PR branch.
206
207    Pre-release versions are tagged with the format: {version}-preview.{7-char-git-sha}
208    These versions are available for version pinning via the scoped_configuration API.
209
210    Requires GITHUB_CONNECTOR_PUBLISHING_PAT or GITHUB_TOKEN environment variable
211    with 'actions:write' permission.
212    """
213    # Guard: Only prerelease publishing is supported
214    if prerelease is not True:
215        raise NotImplementedError(
216            "Non-prerelease publishing is not implemented yet. Set prerelease=True."
217        )
218
219    # Guard: Check for required token
220    token = resolve_ci_trigger_github_token(PRERELEASE_TOKEN_ENV_VARS)
221
222    # Determine repo-specific settings
223    is_enterprise = repo == ConnectorRepo.AIRBYTE_ENTERPRISE
224    target_repo_name = ENTERPRISE_REPO_NAME if is_enterprise else DEFAULT_REPO_NAME
225    target_branch = ENTERPRISE_DEFAULT_BRANCH if is_enterprise else DEFAULT_BRANCH
226    target_workflow = (
227        ENTERPRISE_PRERELEASE_WORKFLOW_FILE
228        if is_enterprise
229        else PRERELEASE_WORKFLOW_FILE
230    )
231
232    # Get the PR's head SHA for computing the docker image tag
233    # Note: We no longer pass gitref to the workflow - it derives the ref from PR number
234    head_info = get_pr_head_ref(DEFAULT_REPO_OWNER, target_repo_name, pr_number, token)
235
236    # Prepare workflow inputs
237    workflow_inputs = {
238        "repo": f"{DEFAULT_REPO_OWNER}/{target_repo_name}",
239        "pr": str(pr_number),
240        "connector": connector_name,
241    }
242
243    # Trigger the workflow on the default branch
244    # The workflow will checkout the PR branch via inputs.gitref
245    dispatch_result = trigger_workflow_dispatch(
246        owner=DEFAULT_REPO_OWNER,
247        repo=target_repo_name,
248        workflow_file=target_workflow,
249        ref=target_branch,
250        inputs=workflow_inputs,
251        token=token,
252        find_run=True,
253    )
254    # Use the specific run URL if found, otherwise fall back to the workflow URL
255    workflow_url = dispatch_result.run_url or dispatch_result.workflow_url
256
257    # Try to compute docker_image and docker_image_tag from connector metadata
258    docker_image: str | None = None
259    docker_image_tag: str | None = None
260    metadata = _get_connector_metadata(
261        DEFAULT_REPO_OWNER,
262        target_repo_name,
263        connector_name,
264        head_info.sha,
265        token,
266    )
267    if metadata and "data" in metadata:
268        data = metadata["data"]
269        docker_image = data.get("dockerRepository")
270        base_version = data.get("dockerImageTag")
271        if base_version:
272            docker_image_tag = compute_prerelease_docker_image_tag(
273                base_version, head_info.sha
274            )
275
276    repo_info = f" from {repo}" if is_enterprise else ""
277    return PrereleaseWorkflowResult(
278        success=True,
279        message=f"Successfully triggered pre-release workflow for {connector_name}{repo_info} from PR #{pr_number}",
280        workflow_url=workflow_url,
281        connector_name=connector_name,
282        pr_number=pr_number,
283        docker_image=docker_image,
284        docker_image_tag=docker_image_tag,
285    )
286
287
288def register_prerelease_tools(app: FastMCP) -> None:
289    """Register pre-release workflow tools with the FastMCP app.
290
291    Args:
292        app: FastMCP application instance
293    """
294    register_mcp_tools(app, mcp_module=__name__)