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__)