airbyte_ops_mcp.mcp.connector_registry

MCP tools for the connector registry and monorepo catalog: registry reads/yank plus repo connector listing and version bumps.

MCP reference

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

Tools (7)

get_connector_registry_entry

Hints: read-only · idempotent · open-world

Read a connector's metadata from the GCS registry.

Returns the full metadata.yaml content for a connector at the specified version. Requires GCS_CREDENTIALS environment variable to be set.

Parameters:

Name Type Required Default Description
connector_name string yes The connector name (e.g., 'source-faker', 'destination-postgres')
version string no "latest" Version to read (e.g., 'latest', '1.2.3'). Defaults to 'latest'.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "connector_name": {
      "description": "The connector name (e.g., 'source-faker', 'destination-postgres')",
      "type": "string"
    },
    "version": {
      "default": "latest",
      "description": "Version to read (e.g., 'latest', '1.2.3'). Defaults to 'latest'.",
      "type": "string"
    }
  },
  "required": [
    "connector_name"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of reading a registry entry from GCS.\n\nThis model wraps the raw metadata dictionary with additional context.",
  "properties": {
    "connector_name": {
      "description": "The connector technical name",
      "type": "string"
    },
    "version": {
      "description": "The version that was read",
      "type": "string"
    },
    "bucket_name": {
      "description": "The GCS bucket name",
      "type": "string"
    },
    "gcs_path": {
      "description": "The GCS path that was read",
      "type": "string"
    },
    "metadata": {
      "additionalProperties": true,
      "description": "The raw metadata dictionary",
      "type": "object"
    }
  },
  "required": [
    "connector_name",
    "version",
    "bucket_name",
    "gcs_path",
    "metadata"
  ],
  "type": "object"
}

get_connector_registry_spec

Hints: read-only · idempotent · open-world

Read a connector's spec from the GCS registry.

Returns the spec.json content for a connector at the specified version. Requires GCS_CREDENTIALS environment variable to be set.

Parameters:

Name Type Required Default Description
connector_name string yes The connector name (e.g., 'source-faker', 'destination-postgres')
version string no "latest" Version to read (e.g., 'latest', '1.2.3'). Defaults to 'latest'.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "connector_name": {
      "description": "The connector name (e.g., 'source-faker', 'destination-postgres')",
      "type": "string"
    },
    "version": {
      "default": "latest",
      "description": "Version to read (e.g., 'latest', '1.2.3'). Defaults to 'latest'.",
      "type": "string"
    }
  },
  "required": [
    "connector_name"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of reading a connector spec from GCS.",
  "properties": {
    "connector_name": {
      "description": "The connector technical name",
      "type": "string"
    },
    "version": {
      "description": "The version that was read",
      "type": "string"
    },
    "bucket_name": {
      "description": "The GCS bucket name",
      "type": "string"
    },
    "gcs_path": {
      "description": "The GCS path that was read",
      "type": "string"
    },
    "spec": {
      "additionalProperties": true,
      "description": "The connector spec dictionary",
      "type": "object"
    }
  },
  "required": [
    "connector_name",
    "version",
    "bucket_name",
    "gcs_path",
    "spec"
  ],
  "type": "object"
}

get_connector_version_yank_detail

Hints: read-only · idempotent · open-world

Read the active yank marker for a single connector version.

Returns yanked=True with the marker's yanked_at, reason, and approval_url when the version currently has an active version-yank.yml marker, or yanked=False with empty fields when it is not yanked. Historical version-unyanked-*.yml audit markers are ignored.

Requires GCS_CREDENTIALS environment variable to be set.

Parameters:

Name Type Required Default Description
connector_name string yes The connector name (e.g., 'source-faker', 'destination-postgres').
version string yes Version to inspect (e.g., '1.2.3').
store string no "coral:prod" Store target to read (e.g. 'coral:prod', 'coral:dev'). Defaults to 'coral:prod'.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "connector_name": {
      "description": "The connector name (e.g., 'source-faker', 'destination-postgres').",
      "type": "string"
    },
    "version": {
      "description": "Version to inspect (e.g., '1.2.3').",
      "type": "string"
    },
    "store": {
      "default": "coral:prod",
      "description": "Store target to read (e.g. 'coral:prod', 'coral:dev'). Defaults to 'coral:prod'.",
      "type": "string"
    }
  },
  "required": [
    "connector_name",
    "version"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Active yank-marker detail for a single connector version.",
  "properties": {
    "connector_name": {
      "description": "The connector technical name",
      "type": "string"
    },
    "version": {
      "description": "The version that was checked",
      "type": "string"
    },
    "store": {
      "description": "The store target that was read",
      "type": "string"
    },
    "bucket_name": {
      "description": "The resolved GCS bucket name",
      "type": "string"
    },
    "yanked": {
      "description": "True when an active `version-yank.yml` marker exists",
      "type": "boolean"
    },
    "yanked_at": {
      "description": "When the version was yanked, if recorded",
      "type": "string"
    },
    "reason": {
      "description": "Reason recorded in the yank marker, if any",
      "type": "string"
    },
    "approval_url": {
      "description": "Approval evidence URL, if recorded",
      "type": "string"
    }
  },
  "required": [
    "connector_name",
    "version",
    "store",
    "bucket_name",
    "yanked",
    "yanked_at",
    "reason",
    "approval_url"
  ],
  "type": "object"
}

list_connector_versions_in_registry

Hints: read-only · idempotent · open-world

List all versions of a connector in the GCS registry.

Returns all published versions for a connector (excluding 'latest' and 'release_candidate'). Requires GCS_CREDENTIALS environment variable to be set.

Parameters:

Name Type Required Default Description
connector_name string yes The connector name (e.g., 'source-faker', 'destination-postgres')

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "connector_name": {
      "description": "The connector name (e.g., 'source-faker', 'destination-postgres')",
      "type": "string"
    }
  },
  "required": [
    "connector_name"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of listing versions for a connector.",
  "properties": {
    "connector_name": {
      "description": "The connector technical name",
      "type": "string"
    },
    "bucket_name": {
      "description": "The GCS bucket name",
      "type": "string"
    },
    "version_count": {
      "description": "Number of versions found",
      "type": "integer"
    },
    "versions": {
      "description": "List of version strings",
      "items": {
        "type": "string"
      },
      "type": "array"
    }
  },
  "required": [
    "connector_name",
    "bucket_name",
    "version_count",
    "versions"
  ],
  "type": "object"
}

list_connectors_in_registry

Hints: read-only · idempotent · open-world

List connectors in the production registry, with optional filtering.

Filters can be combined; name_contains is a case-insensitive substring match on the connector name (leading/trailing whitespace is ignored). To inspect a single known connector, prefer get_connector_registry_entry (metadata) or list_connector_versions_in_registry (versions) instead.

Parameters:

Name Type Required Default Description
certified boolean no false When True, return only certified connectors. Shorthand for support_level='certified'.
support_level string no "" Exact support level to match (e.g., certified, community, archived). Empty string means no filter.
min_support_level string no "" Minimum support level threshold (inclusive). Levels: archived < community < certified. Empty string means no filter.
connector_type string no "" Filter by connector type: source or destination. Empty string means no filter.
language string no "" Filter by implementation language (e.g., python, java, manifest-only). Empty string means no filter.
name_contains string no "" Case-insensitive substring to match against connector names (e.g., github). Empty string means no filter.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "certified": {
      "default": false,
      "description": "When `True`, return only certified connectors. Shorthand for `support_level='certified'`.",
      "type": "boolean"
    },
    "support_level": {
      "default": "",
      "description": "Exact support level to match (e.g., `certified`, `community`, `archived`). Empty string means no filter.",
      "type": "string"
    },
    "min_support_level": {
      "default": "",
      "description": "Minimum support level threshold (inclusive). Levels: `archived` < `community` < `certified`. Empty string means no filter.",
      "type": "string"
    },
    "connector_type": {
      "default": "",
      "description": "Filter by connector type: `source` or `destination`. Empty string means no filter.",
      "type": "string"
    },
    "language": {
      "default": "",
      "description": "Filter by implementation language (e.g., `python`, `java`, `manifest-only`). Empty string means no filter.",
      "type": "string"
    },
    "name_contains": {
      "default": "",
      "description": "Case-insensitive substring to match against connector names (e.g., `github`). Empty string means no filter.",
      "type": "string"
    }
  },
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of listing connectors in the registry.",
  "properties": {
    "bucket_name": {
      "description": "The GCS bucket name",
      "type": "string"
    },
    "connector_count": {
      "description": "Number of connectors found",
      "type": "integer"
    },
    "connectors": {
      "description": "List of connector names",
      "items": {
        "type": "string"
      },
      "type": "array"
    }
  },
  "required": [
    "bucket_name",
    "connector_count",
    "connectors"
  ],
  "type": "object"
}

list_yanked_connector_versions

Hints: read-only · idempotent · open-world

List every yanked connector version in a registry store.

Returns one entry per active version-yank.yml marker, sorted by connector name then version, with each marker's yanked_at, reason, and approval_url populated. Historical version-unyanked-*.yml audit markers are ignored — only live markers count.

Requires GCS_CREDENTIALS environment variable to be set.

Parameters:

Name Type Required Default Description
store string no "coral:prod" Store target to read (e.g. 'coral:prod', 'coral:dev'). Defaults to 'coral:prod'.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "store": {
      "default": "coral:prod",
      "description": "Store target to read (e.g. 'coral:prod', 'coral:dev'). Defaults to 'coral:prod'.",
      "type": "string"
    }
  },
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of listing yanked connector versions in a registry store.",
  "properties": {
    "store": {
      "description": "The store target that was read",
      "type": "string"
    },
    "bucket_name": {
      "description": "The resolved GCS bucket name",
      "type": "string"
    },
    "count": {
      "description": "Number of yanked versions found",
      "type": "integer"
    },
    "yanked_versions": {
      "description": "Yanked versions, sorted by connector name then version",
      "items": {
        "description": "A single yanked connector version discovered from its marker file.",
        "properties": {
          "connector_name": {
            "description": "The connector technical name",
            "type": "string"
          },
          "version": {
            "description": "The yanked version tag",
            "type": "string"
          },
          "yanked_at": {
            "description": "When the version was yanked, if recorded",
            "type": "string"
          },
          "reason": {
            "description": "Reason recorded in the yank marker, if any",
            "type": "string"
          },
          "approval_url": {
            "description": "Approval evidence URL, if recorded",
            "type": "string"
          }
        },
        "required": [
          "connector_name",
          "version",
          "yanked_at",
          "reason",
          "approval_url"
        ],
        "type": "object"
      },
      "type": "array"
    }
  },
  "required": [
    "store",
    "bucket_name",
    "count",
    "yanked_versions"
  ],
  "type": "object"
}

yank_connector_version

Hints: open-world

Yank or unyank a connector version after Slack/HITL approval.

This MCP tool requires approval for all stores, including dev/test stores, so the safety behavior is consistent and prod-impacting coral:prod requests cannot dispatch without approval.

Without approval_comment_url, returns the exact approval request summary and Slack message to send via escalate_to_human; no GitHub Actions workflow is triggered. With an approved Slack record URL, validates the approver and then triggers a workflow that marks the version as yanked (or unyanked) and recompiles the registry to update indexes and latest pointers.

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

Parameters:

Name Type Required Default Description
connector_name string yes Connector name (e.g., 'source-faker', 'destination-postgres').
version string yes Version to yank (e.g., '1.2.3').
store string yes Store target (e.g., 'coral:dev', 'coral:prod').
reason string no "" Reason for yanking this version.
unyank boolean no false Set to true to unyank (restore) the version instead of yanking it.
approval_comment_url string | null no null Slack approval record URL. Obtain this by calling escalate_to_human with approval_requested=True using the approval request details returned by this tool. The backend validates the approval record and resolves the approver's @airbyte.io email before dispatching the registry workflow.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "connector_name": {
      "description": "Connector name (e.g., 'source-faker', 'destination-postgres').",
      "type": "string"
    },
    "version": {
      "description": "Version to yank (e.g., '1.2.3').",
      "type": "string"
    },
    "store": {
      "description": "Store target (e.g., 'coral:dev', 'coral:prod').",
      "type": "string"
    },
    "reason": {
      "default": "",
      "description": "Reason for yanking this version.",
      "type": "string"
    },
    "unyank": {
      "default": false,
      "description": "Set to true to unyank (restore) the version instead of yanking it.",
      "type": "boolean"
    },
    "approval_comment_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Slack approval record URL. Obtain this by calling `escalate_to_human` with `approval_requested=True` using the approval request details returned by this tool. The backend validates the approval record and resolves the approver's `@airbyte.io` email before dispatching the registry workflow."
    }
  },
  "required": [
    "connector_name",
    "version",
    "store"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Response from triggering a yank connector version workflow.",
  "properties": {
    "approval_required": {
      "default": false,
      "description": "Whether the operation still requires Slack/HITL approval before dispatch.",
      "type": "boolean"
    },
    "message": {
      "description": "Human-readable status message",
      "type": "string"
    },
    "approval_request_summary": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Short approval summary to pass to `escalate_to_human` when approval is required."
    },
    "approval_request_message": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Detailed Slack message to pass to `escalate_to_human` when approval is required."
    },
    "approved_by": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Resolved `@airbyte.io` email for the Slack approver."
    },
    "workflow_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "URL to view the GitHub Actions workflow file"
    },
    "github_run_id": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "GitHub Actions workflow run ID (use with check_ci_workflow_status)"
    },
    "github_run_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Direct URL to the GitHub Actions workflow run"
    }
  },
  "required": [
    "message"
  ],
  "type": "object"
}

  1# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
  2"""MCP tools for the connector registry and monorepo catalog: registry reads/yank plus repo connector listing and version bumps.
  3
  4## MCP reference
  5
  6.. include:: ../../../docs/mcp-generated/connector_registry.md
  7    :start-line: 2
  8"""
  9
 10from __future__ import annotations
 11
 12__all__: list[str] = []
 13
 14from typing import Annotated, Any, Literal
 15
 16from fastmcp import FastMCP
 17from fastmcp_extensions import mcp_tool, register_mcp_tools
 18from pydantic import BaseModel, Field
 19
 20from airbyte_ops_mcp.airbyte_repo.bump_version import bump_connector_version
 21from airbyte_ops_mcp.airbyte_repo.list_connectors import list_connectors
 22from airbyte_ops_mcp.airbyte_repo.utils import resolve_diff_range
 23from airbyte_ops_mcp.approval_resolution import (
 24    ApprovalResolutionError,
 25    resolve_admin_email_from_approval,
 26)
 27from airbyte_ops_mcp.github_actions import trigger_workflow_dispatch
 28from airbyte_ops_mcp.github_api import resolve_ci_trigger_github_token
 29from airbyte_ops_mcp.human_in_the_loop import APPROVAL_REQUEST_SUMMARY_MAX_LENGTH
 30from airbyte_ops_mcp.registry import (
 31    PROD_METADATA_SERVICE_BUCKET_NAME,
 32    ConnectorListResult,
 33    RegistryEntryResult,
 34    VersionListResult,
 35    resolve_registry_store,
 36)
 37from airbyte_ops_mcp.registry._enums import (
 38    ConnectorLanguage,
 39    ConnectorType,
 40    SupportLevel,
 41)
 42from airbyte_ops_mcp.registry.operations import (
 43    get_registry_entry,
 44    get_registry_spec,
 45    list_connector_versions,
 46    list_registry_connectors,
 47    list_registry_connectors_filtered,
 48)
 49from airbyte_ops_mcp.registry.registry_store_base import get_registry
 50
 51
 52class RegistrySpecResult(BaseModel):
 53    """Result of reading a connector spec from GCS."""
 54
 55    connector_name: str = Field(description="The connector technical name")
 56    version: str = Field(description="The version that was read")
 57    bucket_name: str = Field(description="The GCS bucket name")
 58    gcs_path: str = Field(description="The GCS path that was read")
 59    spec: dict[str, Any] = Field(description="The connector spec dictionary")
 60
 61
 62@mcp_tool(
 63    read_only=True,
 64    idempotent=True,
 65    open_world=True,
 66)
 67def get_connector_registry_entry(
 68    connector_name: Annotated[
 69        str,
 70        "The connector name (e.g., 'source-faker', 'destination-postgres')",
 71    ],
 72    version: Annotated[
 73        str,
 74        "Version to read (e.g., 'latest', '1.2.3'). Defaults to 'latest'.",
 75    ] = "latest",
 76) -> RegistryEntryResult:
 77    """Read a connector's metadata from the GCS registry.
 78
 79    Returns the full metadata.yaml content for a connector at the specified version.
 80    Requires GCS_CREDENTIALS environment variable to be set.
 81    """
 82    bucket_name = PROD_METADATA_SERVICE_BUCKET_NAME
 83    metadata = get_registry_entry(
 84        connector_name=connector_name,
 85        bucket_name=bucket_name,
 86        version=version,
 87    )
 88    gcs_path = f"metadata/airbyte/{connector_name}/{version}/metadata.yaml"
 89    return RegistryEntryResult(
 90        connector_name=connector_name,
 91        version=version,
 92        bucket_name=bucket_name,
 93        gcs_path=gcs_path,
 94        metadata=metadata,
 95    )
 96
 97
 98@mcp_tool(
 99    read_only=True,
100    idempotent=True,
101    open_world=True,
102)
103def get_connector_registry_spec(
104    connector_name: Annotated[
105        str,
106        "The connector name (e.g., 'source-faker', 'destination-postgres')",
107    ],
108    version: Annotated[
109        str,
110        "Version to read (e.g., 'latest', '1.2.3'). Defaults to 'latest'.",
111    ] = "latest",
112) -> RegistrySpecResult:
113    """Read a connector's spec from the GCS registry.
114
115    Returns the spec.json content for a connector at the specified version.
116    Requires GCS_CREDENTIALS environment variable to be set.
117    """
118    bucket_name = PROD_METADATA_SERVICE_BUCKET_NAME
119    spec = get_registry_spec(
120        connector_name=connector_name,
121        bucket_name=bucket_name,
122        version=version,
123    )
124    gcs_path = f"metadata/airbyte/{connector_name}/{version}/spec.json"
125    return RegistrySpecResult(
126        connector_name=connector_name,
127        version=version,
128        bucket_name=bucket_name,
129        gcs_path=gcs_path,
130        spec=spec,
131    )
132
133
134@mcp_tool(
135    read_only=True,
136    idempotent=True,
137    open_world=True,
138)
139def list_connectors_in_registry(
140    certified: Annotated[
141        bool,
142        "When `True`, return only certified connectors. Shorthand for `support_level='certified'`.",
143    ] = False,
144    support_level: Annotated[
145        str,
146        "Exact support level to match (e.g., `certified`, `community`, `archived`). Empty string means no filter.",
147    ] = "",
148    min_support_level: Annotated[
149        str,
150        "Minimum support level threshold (inclusive). Levels: `archived` < `community` < `certified`. Empty string means no filter.",
151    ] = "",
152    connector_type: Annotated[
153        str,
154        "Filter by connector type: `source` or `destination`. Empty string means no filter.",
155    ] = "",
156    language: Annotated[
157        str,
158        "Filter by implementation language (e.g., `python`, `java`, `manifest-only`). Empty string means no filter.",
159    ] = "",
160    name_contains: Annotated[
161        str,
162        "Case-insensitive substring to match against connector names (e.g., `github`). Empty string means no filter.",
163    ] = "",
164) -> ConnectorListResult:
165    """List connectors in the production registry, with optional filtering.
166
167    Filters can be combined; `name_contains` is a case-insensitive substring
168    match on the connector name (leading/trailing whitespace is ignored). To
169    inspect a single known connector, prefer `get_connector_registry_entry`
170    (metadata) or `list_connector_versions_in_registry` (versions) instead.
171    """
172    bucket_name = PROD_METADATA_SERVICE_BUCKET_NAME
173
174    # Normalise empty strings to typed enums or `None` for downstream logic.
175    eff_support_level: SupportLevel | None = (
176        SupportLevel.parse(support_level) if support_level else None
177    )
178    eff_min_support_level: SupportLevel | None = (
179        SupportLevel.parse(min_support_level) if min_support_level else None
180    )
181    eff_connector_type: ConnectorType | None = (
182        ConnectorType.parse(connector_type) if connector_type else None
183    )
184    eff_language: ConnectorLanguage | None = (
185        ConnectorLanguage.parse(language) if language else None
186    )
187
188    # `certified=True` is sugar for `support_level="certified"`.
189    if certified:
190        if eff_support_level and eff_support_level != SupportLevel.CERTIFIED:
191            raise ValueError(
192                "`certified=True` conflicts with `support_level="
193                f"{eff_support_level!r}`. Use one or the other."
194            )
195        eff_support_level = SupportLevel.CERTIFIED
196
197    has_filters = any(
198        [eff_support_level, eff_min_support_level, eff_connector_type, eff_language]
199    )
200
201    if has_filters:
202        connectors = list_registry_connectors_filtered(
203            bucket_name=bucket_name,
204            support_level=eff_support_level,
205            min_support_level=eff_min_support_level,
206            connector_type=eff_connector_type,
207            language=eff_language,
208        )
209    else:
210        connectors = list_registry_connectors(bucket_name=bucket_name)
211
212    needle = name_contains.strip().lower()
213    if needle:
214        connectors = [c for c in connectors if needle in c.lower()]
215
216    return ConnectorListResult(
217        bucket_name=bucket_name,
218        connector_count=len(connectors),
219        connectors=connectors,
220    )
221
222
223@mcp_tool(
224    read_only=True,
225    idempotent=True,
226    open_world=True,
227)
228def list_connector_versions_in_registry(
229    connector_name: Annotated[
230        str,
231        "The connector name (e.g., 'source-faker', 'destination-postgres')",
232    ],
233) -> VersionListResult:
234    """List all versions of a connector in the GCS registry.
235
236    Returns all published versions for a connector (excluding 'latest' and 'release_candidate').
237    Requires GCS_CREDENTIALS environment variable to be set.
238    """
239    bucket_name = PROD_METADATA_SERVICE_BUCKET_NAME
240    versions = list_connector_versions(
241        connector_name=connector_name,
242        bucket_name=bucket_name,
243    )
244    return VersionListResult(
245        connector_name=connector_name,
246        bucket_name=bucket_name,
247        version_count=len(versions),
248        versions=versions,
249    )
250
251
252class YankedVersionEntry(BaseModel):
253    """A single yanked connector version discovered from its marker file."""
254
255    connector_name: str = Field(description="The connector technical name")
256    version: str = Field(description="The yanked version tag")
257    yanked_at: str = Field(description="When the version was yanked, if recorded")
258    reason: str = Field(description="Reason recorded in the yank marker, if any")
259    approval_url: str = Field(description="Approval evidence URL, if recorded")
260
261
262class YankedVersionsResult(BaseModel):
263    """Result of listing yanked connector versions in a registry store."""
264
265    store: str = Field(description="The store target that was read")
266    bucket_name: str = Field(description="The resolved GCS bucket name")
267    count: int = Field(description="Number of yanked versions found")
268    yanked_versions: list[YankedVersionEntry] = Field(
269        description="Yanked versions, sorted by connector name then version"
270    )
271
272
273class YankDetailResult(BaseModel):
274    """Active yank-marker detail for a single connector version."""
275
276    connector_name: str = Field(description="The connector technical name")
277    version: str = Field(description="The version that was checked")
278    store: str = Field(description="The store target that was read")
279    bucket_name: str = Field(description="The resolved GCS bucket name")
280    yanked: bool = Field(
281        description="True when an active `version-yank.yml` marker exists"
282    )
283    yanked_at: str = Field(description="When the version was yanked, if recorded")
284    reason: str = Field(description="Reason recorded in the yank marker, if any")
285    approval_url: str = Field(description="Approval evidence URL, if recorded")
286
287
288@mcp_tool(
289    read_only=True,
290    idempotent=True,
291    open_world=True,
292)
293def list_yanked_connector_versions(
294    store: Annotated[
295        str,
296        "Store target to read (e.g. 'coral:prod', 'coral:dev'). Defaults to 'coral:prod'.",
297    ] = "coral:prod",
298) -> YankedVersionsResult:
299    """List every yanked connector version in a registry store.
300
301    Returns one entry per active `version-yank.yml` marker, sorted by connector
302    name then version, with each marker's `yanked_at`, `reason`, and
303    `approval_url` populated. Historical `version-unyanked-*.yml` audit markers
304    are ignored — only live markers count.
305
306    Requires GCS_CREDENTIALS environment variable to be set.
307    """
308    registry = get_registry(resolve_registry_store(store=store))
309    yanked = registry.list_yanked_versions()
310    return YankedVersionsResult(
311        store=store,
312        bucket_name=registry.bucket_name,
313        count=len(yanked),
314        yanked_versions=[
315            YankedVersionEntry(
316                connector_name=item.connector_name,
317                version=item.version,
318                yanked_at=item.yanked_at,
319                reason=item.reason,
320                approval_url=item.approval_url,
321            )
322            for item in yanked
323        ],
324    )
325
326
327@mcp_tool(
328    read_only=True,
329    idempotent=True,
330    open_world=True,
331)
332def get_connector_version_yank_detail(
333    connector_name: Annotated[
334        str,
335        "The connector name (e.g., 'source-faker', 'destination-postgres').",
336    ],
337    version: Annotated[
338        str,
339        "Version to inspect (e.g., '1.2.3').",
340    ],
341    store: Annotated[
342        str,
343        "Store target to read (e.g. 'coral:prod', 'coral:dev'). Defaults to 'coral:prod'.",
344    ] = "coral:prod",
345) -> YankDetailResult:
346    """Read the active yank marker for a single connector version.
347
348    Returns `yanked=True` with the marker's `yanked_at`, `reason`, and
349    `approval_url` when the version currently has an active `version-yank.yml`
350    marker, or `yanked=False` with empty fields when it is not yanked.
351    Historical `version-unyanked-*.yml` audit markers are ignored.
352
353    Requires GCS_CREDENTIALS environment variable to be set.
354    """
355    registry = get_registry(resolve_registry_store(store=store))
356    marker = registry.get_yank_marker(connector_name=connector_name, version=version)
357    return YankDetailResult(
358        connector_name=connector_name,
359        version=version,
360        store=store,
361        bucket_name=registry.bucket_name,
362        yanked=marker is not None,
363        yanked_at=marker.yanked_at if marker else "",
364        reason=marker.reason if marker else "",
365        approval_url=marker.approval_url if marker else "",
366    )
367
368
369# =============================================================================
370# Yank Workflow Configuration
371# =============================================================================
372
373YANK_WORKFLOW_REPO_OWNER = "airbytehq"
374
375YANK_WORKFLOW_REPO_NAME = "airbyte"
376
377YANK_WORKFLOW_DEFAULT_BRANCH = "master"
378
379YANK_WORKFLOW_FILE = "version-yank-command.yml"
380
381
382class YankConnectorVersionResponse(BaseModel):
383    """Response from triggering a yank connector version workflow."""
384
385    approval_required: bool = Field(
386        default=False,
387        description=(
388            "Whether the operation still requires Slack/HITL approval before dispatch."
389        ),
390    )
391    message: str = Field(description="Human-readable status message")
392    approval_request_summary: str | None = Field(
393        default=None,
394        description=(
395            "Short approval summary to pass to `escalate_to_human` when approval is required."
396        ),
397    )
398    approval_request_message: str | None = Field(
399        default=None,
400        description=(
401            "Detailed Slack message to pass to `escalate_to_human` when approval is required."
402        ),
403    )
404    approved_by: str | None = Field(
405        default=None,
406        description="Resolved `@airbyte.io` email for the Slack approver.",
407    )
408    workflow_url: str | None = Field(
409        default=None,
410        description="URL to view the GitHub Actions workflow file",
411    )
412    github_run_id: int | None = Field(
413        default=None,
414        description="GitHub Actions workflow run ID (use with check_ci_workflow_status)",
415    )
416    github_run_url: str | None = Field(
417        default=None,
418        description="Direct URL to the GitHub Actions workflow run",
419    )
420
421
422def _format_yank_action(unyank: bool) -> str:
423    """Return the registry action label for a yank tool call."""
424    return "unyank" if unyank else "yank"
425
426
427def _sanitize_approval_request_summary_text(value: str) -> str:
428    """Return text safe for Slack approval request formatting."""
429    return value.replace("`", "'")
430
431
432def _build_yank_approval_request_summary(
433    *,
434    connector_name: str,
435    version: str,
436    store: str,
437    reason: str,
438    unyank: bool,
439) -> str:
440    """Build a Slack confirmation summary for registry yank approval."""
441    action = _format_yank_action(unyank)
442    sanitized_connector_name = _sanitize_approval_request_summary_text(connector_name)
443    sanitized_version = _sanitize_approval_request_summary_text(version)
444    sanitized_store = _sanitize_approval_request_summary_text(store)
445    summary = (
446        f"{action} {sanitized_connector_name}@{sanitized_version} in "
447        f"{sanitized_store}; registry will be recompiled"
448    )
449    if reason:
450        sanitized_reason = _sanitize_approval_request_summary_text(reason)
451        summary = f"{summary}; reason: {sanitized_reason}"
452    if len(summary) <= APPROVAL_REQUEST_SUMMARY_MAX_LENGTH:
453        return summary
454    return f"{summary[: APPROVAL_REQUEST_SUMMARY_MAX_LENGTH - 3].rstrip()}..."
455
456
457def _build_yank_approval_request_message(
458    *,
459    connector_name: str,
460    version: str,
461    store: str,
462    reason: str,
463    unyank: bool,
464) -> str:
465    """Build the Slack message body for registry yank approval."""
466    action = _format_yank_action(unyank)
467    sanitized_connector_name = _sanitize_approval_request_summary_text(connector_name)
468    sanitized_version = _sanitize_approval_request_summary_text(version)
469    sanitized_store = _sanitize_approval_request_summary_text(store)
470    reason_text = (
471        _sanitize_approval_request_summary_text(reason) if reason else "(none provided)"
472    )
473    return (
474        "Approval requested for an MCP registry connector-version operation.\n\n"
475        f"- Action: `{action}`\n"
476        f"- Connector: `{sanitized_connector_name}`\n"
477        f"- Version: `{sanitized_version}`\n"
478        f"- Store: `{sanitized_store}`\n"
479        f"- Reason: {reason_text}\n"
480        "- Consequence: after approval, the MCP tool will dispatch "
481        "`airbyte/.github/workflows/version-yank-command.yml`; that workflow "
482        "will update the yank marker and run `airbyte-ops registry store compile`, "
483        "recompiling registry indexes and latest pointers.\n\n"
484        "After Slack approval, copy the Slack approval record URL into "
485        "`approval_comment_url` and call `yank_connector_version` again with "
486        "the same connector name, version, store, reason, and unyank values."
487    )
488
489
490@mcp_tool(
491    read_only=False,
492    idempotent=False,
493    open_world=True,
494)
495def yank_connector_version(
496    connector_name: Annotated[
497        str,
498        "Connector name (e.g., 'source-faker', 'destination-postgres').",
499    ],
500    version: Annotated[
501        str,
502        "Version to yank (e.g., '1.2.3').",
503    ],
504    store: Annotated[
505        str,
506        "Store target (e.g., 'coral:dev', 'coral:prod').",
507    ],
508    reason: Annotated[
509        str,
510        "Reason for yanking this version.",
511    ] = "",
512    unyank: Annotated[
513        bool,
514        "Set to true to unyank (restore) the version instead of yanking it.",
515    ] = False,
516    approval_comment_url: Annotated[
517        str | None,
518        Field(
519            description=(
520                "Slack approval record URL. Obtain this by calling "
521                "`escalate_to_human` with `approval_requested=True` using the "
522                "approval request details returned by this tool. The backend "
523                "validates the approval record and resolves the approver's "
524                "`@airbyte.io` email before dispatching the registry workflow."
525            ),
526            default=None,
527        ),
528    ] = None,
529) -> YankConnectorVersionResponse:
530    """Yank or unyank a connector version after Slack/HITL approval.
531
532    This MCP tool requires approval for all stores, including dev/test stores,
533    so the safety behavior is consistent and prod-impacting `coral:prod`
534    requests cannot dispatch without approval.
535
536    Without `approval_comment_url`, returns the exact approval request summary
537    and Slack message to send via `escalate_to_human`; no GitHub Actions
538    workflow is triggered. With an approved Slack record URL, validates the
539    approver and then triggers a workflow that marks the version as yanked
540    (or unyanked) and recompiles the registry to update indexes and latest
541    pointers.
542
543    Requires GITHUB_CI_WORKFLOW_TRIGGER_PAT or GITHUB_TOKEN environment variable
544    with 'actions:write' permission.
545    """
546    action = _format_yank_action(unyank)
547    action_title = action.capitalize()
548
549    approval_request_summary = _build_yank_approval_request_summary(
550        connector_name=connector_name,
551        version=version,
552        store=store,
553        reason=reason,
554        unyank=unyank,
555    )
556    approval_request_message = _build_yank_approval_request_message(
557        connector_name=connector_name,
558        version=version,
559        store=store,
560        reason=reason,
561        unyank=unyank,
562    )
563
564    if not approval_comment_url:
565        return YankConnectorVersionResponse(
566            approval_required=True,
567            message=(
568                f"Slack/HITL approval is required before dispatching the {action} "
569                f"workflow for {connector_name}@{version} on {store}. Call "
570                "`escalate_to_human` with `approval_requested=True`, "
571                "`request_type='approval'`, the returned "
572                "`approval_request_summary`, and the returned "
573                "`approval_request_message`. After approval, call this tool "
574                "again with the Slack approval record URL as `approval_comment_url`."
575            ),
576            approval_request_summary=approval_request_summary,
577            approval_request_message=approval_request_message,
578        )
579
580    try:
581        approved_by = resolve_admin_email_from_approval(
582            approval_comment_url=approval_comment_url,
583        )
584    except ApprovalResolutionError as e:
585        return YankConnectorVersionResponse(
586            approval_required=True,
587            message=str(e),
588            approval_request_summary=approval_request_summary,
589            approval_request_message=approval_request_message,
590        )
591
592    try:
593        token = resolve_ci_trigger_github_token()
594    except ValueError as e:
595        return YankConnectorVersionResponse(
596            message=str(e),
597            approved_by=approved_by,
598        )
599
600    workflow_inputs: dict[str, str] = {
601        "connector-name": connector_name,
602        "version": version,
603        "store": store,
604        "unyank": str(unyank).lower(),
605        "approval-url": approval_comment_url,
606    }
607    if reason:
608        workflow_inputs["reason"] = reason
609
610    dispatch_result = trigger_workflow_dispatch(
611        owner=YANK_WORKFLOW_REPO_OWNER,
612        repo=YANK_WORKFLOW_REPO_NAME,
613        workflow_file=YANK_WORKFLOW_FILE,
614        ref=YANK_WORKFLOW_DEFAULT_BRANCH,
615        inputs=workflow_inputs,
616        token=token,
617    )
618
619    view_url = dispatch_result.run_url or dispatch_result.workflow_url
620    reason_info = f" (reason: {reason})" if reason else ""
621    return YankConnectorVersionResponse(
622        message=(
623            f"{action_title} workflow triggered for {connector_name}@{version} "
624            f"on {store}{reason_info} after approval by {approved_by}. "
625            f"View progress at: {view_url}"
626        ),
627        approved_by=approved_by,
628        workflow_url=dispatch_result.workflow_url,
629        github_run_id=dispatch_result.run_id,
630        github_run_url=dispatch_result.run_url,
631    )
632
633
634class ConnectorListResponse(BaseModel):
635    """Response model for list_connectors MCP tool."""
636
637    connectors: list[str]
638    count: int
639
640
641class BumpVersionResponse(BaseModel):
642    """Response model for bump_connector_version MCP tool."""
643
644    connector: str
645    previous_version: str
646    new_version: str
647    files_modified: list[str]
648    dry_run: bool
649
650
651@mcp_tool(
652    read_only=True,
653    idempotent=True,
654    open_world=False,
655    requires_client_filesystem=True,
656)
657def list_connectors_in_repo(
658    repo_path: Annotated[str, "Absolute path to the Airbyte monorepo"],
659    certified: Annotated[
660        bool | None,
661        "Filter by certification: True=certified only, False=non-certified only, None=all",
662    ] = None,
663    modified: Annotated[
664        bool | None,
665        "Filter by modification: True=modified only, False=not-modified only, None=all",
666    ] = None,
667    language_filter: Annotated[
668        set[str] | None,
669        "Set of languages to include (python, java, low-code, manifest-only)",
670    ] = None,
671    language_exclude: Annotated[
672        set[str] | None,
673        "Set of languages to exclude (mutually exclusive with language_filter)",
674    ] = None,
675    connector_type: Annotated[
676        Literal["source", "destination"] | None,
677        "Filter by connector type: 'source' or 'destination', None=all",
678    ] = None,
679    connector_subtype: Annotated[
680        Literal["api", "database", "file", "custom"] | None,
681        "Filter by connector subtype: 'api', 'database', 'file', 'custom', None=all",
682    ] = None,
683    pr_num_or_url: Annotated[
684        str | None,
685        "PR number (e.g., '123'), GitHub URL, or None to auto-detect from GITHUB_REF environment variable",
686    ] = None,
687    gh_token: Annotated[
688        str | None,
689        "GitHub API token. When provided together with pr_num_or_url, the GitHub API is used "
690        "to detect modified files instead of local git diff (avoids shallow-clone issues).",
691    ] = None,
692) -> ConnectorListResponse:
693    """List connectors in the Airbyte monorepo with flexible filtering.
694
695    Filters can be combined to narrow results. PR context (if provided or auto-detected)
696    determines the git diff range for modification detection.
697    """
698    # Resolve PR info to base_ref and head_ref (MCP-specific: supports PR URL/number input)
699    base_ref, head_ref, pr_number, pr_owner, pr_repo = resolve_diff_range(pr_num_or_url)
700
701    # Delegate to capability function
702    result = list_connectors(
703        repo_path=repo_path,
704        certified=certified,
705        modified=modified,
706        language_filter=language_filter,
707        language_exclude=language_exclude,
708        connector_type=connector_type,
709        connector_subtype=connector_subtype,
710        base_ref=base_ref,
711        head_ref=head_ref,
712        pr_number=pr_number,
713        pr_owner=pr_owner,
714        pr_repo=pr_repo,
715        gh_token=gh_token,
716    )
717
718    return ConnectorListResponse(
719        connectors=result.connectors,
720        count=result.count,
721    )
722
723
724@mcp_tool(
725    read_only=False,
726    idempotent=False,
727    open_world=False,
728    requires_client_filesystem=True,
729)
730def bump_version_in_repo(
731    repo_path: Annotated[str, "Absolute path to the Airbyte monorepo"],
732    connector_name: Annotated[str, "Connector technical name (e.g., 'source-github')"],
733    bump_type: Annotated[
734        Literal[
735            "patch",
736            "minor",
737            "major",
738            "patch_rc",
739            "minor_rc",
740            "major_rc",
741            "rc",
742            "promote",
743        ]
744        | None,
745        "Version bump type. Standard: 'patch', 'minor', 'major'. "
746        "RC: 'patch_rc', 'minor_rc', 'major_rc' (create RC for next version), "
747        "'rc' (smart default: minor_rc if not RC, else bump RC number). "
748        "'promote' (strip RC suffix to finalize release).",
749    ] = None,
750    new_version: Annotated[
751        str | None,
752        "Explicit new version (overrides bump_type if provided)",
753    ] = None,
754    changelog_message: Annotated[
755        str | None,
756        "Message to add to changelog (optional)",
757    ] = None,
758    pr_number: Annotated[
759        int | None,
760        "PR number for changelog entry (optional)",
761    ] = None,
762    dry_run: Annotated[
763        bool,
764        "If True, show what would be changed without modifying files",
765    ] = False,
766) -> BumpVersionResponse:
767    """Bump a connector's version across all relevant files.
768
769    Updates version in metadata.yaml (always), pyproject.toml (if exists),
770    and documentation changelog (if changelog_message provided).
771
772    Either bump_type or new_version must be provided.
773
774    RC bump types (patch_rc, minor_rc, major_rc) only work on non-RC versions.
775    If the version is already an RC, use 'rc' to bump the RC number or 'promote' to finalize.
776    """
777    # Delegate to capability function (validation happens there)
778    result = bump_connector_version(
779        repo_path=repo_path,
780        connector_name=connector_name,
781        bump_type=bump_type,
782        new_version=new_version,
783        changelog_message=changelog_message,
784        pr_number=pr_number,
785        dry_run=dry_run,
786    )
787
788    return BumpVersionResponse(
789        connector=result.connector,
790        previous_version=result.previous_version,
791        new_version=result.new_version,
792        files_modified=result.files_modified,
793        dry_run=result.dry_run,
794    )
795
796
797def register_connector_registry_tools(app: FastMCP) -> None:
798    """Register connector_registry tools with the FastMCP app."""
799    register_mcp_tools(app, mcp_module=__name__)