airbyte_ops_mcp.mcp.github_actions
MCP tools for GitHub workflow and Docker operations.
This module provides MCP tools for checking GitHub Actions workflow status, Docker image availability, and other related operations.
MCP reference
MCP primitives registered by the github_actions module of the airbyte-internal-ops server: 3 tool(s), 0 prompt(s), 0 resource(s).
Tools (3)
check_ci_workflow_status
Hints: read-only · idempotent · open-world
Check the status of a GitHub Actions workflow run.
You can provide either:
- A full workflow URL (workflow_url parameter), OR
- The component parts (owner, repo, run_id parameters)
Returns the current status, conclusion, and other details about the workflow run.
Uses the CI trigger token (GITHUB_CI_WORKFLOW_TRIGGER_PAT) so that workflow runs in private repositories are accessible.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
workflow_url |
string | null |
no | null |
Full GitHub Actions workflow run URL (e.g., 'https://github.com/owner/repo/actions/runs/12345') |
owner |
string | null |
no | null |
Repository owner (e.g., 'airbytehq') |
repo |
string | null |
no | null |
Repository name (e.g., 'airbyte') |
run_id |
integer | null |
no | null |
Workflow run ID |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"workflow_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Full GitHub Actions workflow run URL (e.g., 'https://github.com/owner/repo/actions/runs/12345')"
},
"owner": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Repository owner (e.g., 'airbytehq')"
},
"repo": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Repository name (e.g., 'airbyte')"
},
"run_id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null,
"description": "Workflow run ID"
}
},
"type": "object"
}
Show output JSON schema
{
"description": "Response model for check_ci_workflow_status MCP tool.",
"properties": {
"run_id": {
"type": "integer"
},
"status": {
"type": "string"
},
"conclusion": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
]
},
"workflow_name": {
"type": "string"
},
"head_branch": {
"type": "string"
},
"head_sha": {
"type": "string"
},
"html_url": {
"type": "string"
},
"created_at": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"run_started_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"jobs_url": {
"type": "string"
},
"jobs": {
"default": [],
"items": {
"description": "Information about a single job in a workflow run.",
"properties": {
"job_id": {
"type": "integer"
},
"name": {
"type": "string"
},
"status": {
"type": "string"
},
"conclusion": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"started_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"completed_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"job_id",
"name",
"status"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"run_id",
"status",
"conclusion",
"workflow_name",
"head_branch",
"head_sha",
"html_url",
"created_at",
"updated_at",
"jobs_url"
],
"type": "object"
}
get_docker_image_info
Hints: read-only · idempotent · open-world
Check if a Docker image exists on DockerHub.
Returns information about the image if it exists, or indicates if it doesn't exist. This is useful for confirming that a pre-release connector was successfully published.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
image |
string |
yes | — | Docker image name (e.g., 'airbyte/source-github') |
tag |
string |
yes | — | Image tag (e.g., '2.1.5-preview.abc1234') |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"image": {
"description": "Docker image name (e.g., 'airbyte/source-github')",
"type": "string"
},
"tag": {
"description": "Image tag (e.g., '2.1.5-preview.abc1234')",
"type": "string"
}
},
"required": [
"image",
"tag"
],
"type": "object"
}
Show output JSON schema
{
"description": "Response model for get_docker_image_info MCP tool.",
"properties": {
"exists": {
"type": "boolean"
},
"image": {
"type": "string"
},
"tag": {
"type": "string"
},
"full_name": {
"type": "string"
},
"digest": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"last_updated": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"size_bytes": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null
},
"architecture": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
},
"os": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"exists",
"image",
"tag",
"full_name"
],
"type": "object"
}
trigger_ci_workflow
Hints: open-world
Trigger a GitHub Actions CI workflow via workflow_dispatch.
This tool triggers a workflow in any GitHub repository that has workflow_dispatch enabled. It resolves PR numbers to branch names automatically since GitHub's workflow_dispatch API only accepts branch names, not refs/pull/{pr}/head format.
Requires GITHUB_CI_WORKFLOW_TRIGGER_PAT or GITHUB_TOKEN environment variable with 'actions:write' permission.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
owner |
string |
yes | — | Repository owner (e.g., 'airbytehq') |
repo |
string |
yes | — | Repository name (e.g., 'airbyte') |
workflow_file |
string |
yes | — | Workflow file name (e.g., 'connector-regression-test.yml') |
workflow_definition_ref |
string | null |
no | null |
Branch name or PR number for the workflow definition to use. If a PR number (integer string) is provided, it resolves to the PR's head branch name. If a branch name is provided, it is used directly. Defaults to the repository's default branch if not specified. |
inputs |
object | null |
no | null |
Workflow inputs as a dictionary of string key-value pairs. These are passed to the workflow_dispatch event. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"owner": {
"description": "Repository owner (e.g., 'airbytehq')",
"type": "string"
},
"repo": {
"description": "Repository name (e.g., 'airbyte')",
"type": "string"
},
"workflow_file": {
"description": "Workflow file name (e.g., 'connector-regression-test.yml')",
"type": "string"
},
"workflow_definition_ref": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Branch name or PR number for the workflow definition to use. If a PR number (integer string) is provided, it resolves to the PR's head branch name. If a branch name is provided, it is used directly. Defaults to the repository's default branch if not specified."
},
"inputs": {
"anyOf": [
{
"additionalProperties": {
"type": "string"
},
"type": "object"
},
{
"type": "null"
}
],
"default": null,
"description": "Workflow inputs as a dictionary of string key-value pairs. These are passed to the workflow_dispatch event."
}
},
"required": [
"owner",
"repo",
"workflow_file"
],
"type": "object"
}
Show output JSON schema
{
"description": "Response model for trigger_ci_workflow MCP tool.",
"properties": {
"success": {
"type": "boolean"
},
"message": {
"type": "string"
},
"workflow_url": {
"type": "string"
},
"run_id": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"default": null
},
"run_url": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null
}
},
"required": [
"success",
"message",
"workflow_url"
],
"type": "object"
}
1# Copyright (c) 2025 Airbyte, Inc., all rights reserved. 2"""MCP tools for GitHub workflow and Docker operations. 3 4This module provides MCP tools for checking GitHub Actions workflow status, 5Docker image availability, and other related operations. 6 7## MCP reference 8 9.. include:: ../../../docs/mcp-generated/github_actions.md 10 :start-line: 2 11""" 12 13from __future__ import annotations 14 15__all__: list[str] = [] 16 17import re 18from typing import Annotated 19 20import requests 21from fastmcp import FastMCP 22from fastmcp_extensions import mcp_tool, register_mcp_tools 23from pydantic import BaseModel, Field 24 25from airbyte_ops_mcp.github_actions import ( 26 get_workflow_jobs, 27 trigger_workflow_dispatch, 28) 29from airbyte_ops_mcp.github_api import ( 30 GITHUB_API_BASE, 31 get_pr_head_ref, 32 resolve_ci_trigger_github_token, 33) 34 35DOCKERHUB_API_BASE = "https://hub.docker.com/v2" 36 37 38class JobInfo(BaseModel): 39 """Information about a single job in a workflow run.""" 40 41 job_id: int 42 name: str 43 status: str 44 conclusion: str | None = None 45 started_at: str | None = None 46 completed_at: str | None = None 47 48 49class WorkflowRunStatus(BaseModel): 50 """Response model for check_ci_workflow_status MCP tool.""" 51 52 run_id: int 53 status: str 54 conclusion: str | None 55 workflow_name: str 56 head_branch: str 57 head_sha: str 58 html_url: str 59 created_at: str 60 updated_at: str 61 run_started_at: str | None = None 62 jobs_url: str 63 jobs: list[JobInfo] = [] 64 65 66def _parse_workflow_url(url: str) -> tuple[str, str, int]: 67 """Parse a GitHub Actions workflow run URL into components. 68 69 Args: 70 url: GitHub Actions workflow run URL 71 (e.g., "https://github.com/owner/repo/actions/runs/12345") 72 73 Returns: 74 Tuple of (owner, repo, run_id) 75 76 Raises: 77 ValueError: If URL format is invalid. 78 """ 79 pattern = r"https://github\.com/([^/]+)/([^/]+)/actions/runs/(\d+)" 80 match = re.match(pattern, url) 81 if not match: 82 raise ValueError( 83 f"Invalid workflow URL format: {url}. " 84 "Expected format: https://github.com/owner/repo/actions/runs/12345" 85 ) 86 return match.group(1), match.group(2), int(match.group(3)) 87 88 89def _get_workflow_run( 90 owner: str, 91 repo: str, 92 run_id: int, 93 token: str, 94) -> dict: 95 """Get workflow run details from GitHub API. 96 97 Args: 98 owner: Repository owner (e.g., "airbytehq") 99 repo: Repository name (e.g., "airbyte") 100 run_id: Workflow run ID 101 token: GitHub API token 102 103 Returns: 104 Workflow run data dictionary. 105 106 Raises: 107 ValueError: If workflow run not found. 108 requests.HTTPError: If API request fails. 109 """ 110 url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/actions/runs/{run_id}" 111 headers = { 112 "Authorization": f"Bearer {token}", 113 "Accept": "application/vnd.github+json", 114 "X-GitHub-Api-Version": "2022-11-28", 115 } 116 117 response = requests.get(url, headers=headers, timeout=30) 118 if response.status_code == 404: 119 raise ValueError(f"Workflow run {owner}/{repo}/actions/runs/{run_id} not found") 120 response.raise_for_status() 121 122 return response.json() 123 124 125@mcp_tool( 126 read_only=True, 127 idempotent=True, 128 open_world=True, 129) 130def check_ci_workflow_status( 131 workflow_url: Annotated[ 132 str | None, 133 Field( 134 description="Full GitHub Actions workflow run URL (e.g., 'https://github.com/owner/repo/actions/runs/12345')" 135 ), 136 ] = None, 137 owner: Annotated[ 138 str | None, 139 Field(description="Repository owner (e.g., 'airbytehq')"), 140 ] = None, 141 repo: Annotated[ 142 str | None, 143 Field(description="Repository name (e.g., 'airbyte')"), 144 ] = None, 145 run_id: Annotated[ 146 int | None, 147 Field(description="Workflow run ID"), 148 ] = None, 149) -> WorkflowRunStatus: 150 """Check the status of a GitHub Actions workflow run. 151 152 You can provide either: 153 - A full workflow URL (workflow_url parameter), OR 154 - The component parts (owner, repo, run_id parameters) 155 156 Returns the current status, conclusion, and other details about the workflow run. 157 158 Uses the CI trigger token (GITHUB_CI_WORKFLOW_TRIGGER_PAT) so that 159 workflow runs in private repositories are accessible. 160 """ 161 # Guard: Validate input parameters 162 if workflow_url: 163 # Parse URL to get components 164 owner, repo, run_id = _parse_workflow_url(workflow_url) 165 elif owner and repo and run_id: 166 # Use provided components 167 pass 168 else: 169 raise ValueError( 170 "Must provide either workflow_url OR all of (owner, repo, run_id)" 171 ) 172 173 # Guard: Check for required token 174 # Use the CI trigger token (same as trigger_ci_workflow) so that 175 # private-repo workflow runs are accessible. 176 token = resolve_ci_trigger_github_token() 177 178 # Get workflow run details 179 run_data = _get_workflow_run(owner, repo, run_id, token) 180 181 # Get jobs for the workflow run, passing the same token 182 workflow_jobs = get_workflow_jobs(owner, repo, run_id, token=token) 183 184 # Convert dataclass objects to Pydantic models for the response 185 jobs = [ 186 JobInfo( 187 job_id=job.job_id, 188 name=job.name, 189 status=job.status, 190 conclusion=job.conclusion, 191 started_at=job.started_at, 192 completed_at=job.completed_at, 193 ) 194 for job in workflow_jobs 195 ] 196 197 return WorkflowRunStatus( 198 run_id=run_data["id"], 199 status=run_data["status"], 200 conclusion=run_data["conclusion"], 201 workflow_name=run_data["name"], 202 head_branch=run_data["head_branch"], 203 head_sha=run_data["head_sha"], 204 html_url=run_data["html_url"], 205 created_at=run_data["created_at"], 206 updated_at=run_data["updated_at"], 207 run_started_at=run_data.get("run_started_at"), 208 jobs_url=run_data["jobs_url"], 209 jobs=jobs, 210 ) 211 212 213class TriggerCIWorkflowResult(BaseModel): 214 """Response model for trigger_ci_workflow MCP tool.""" 215 216 success: bool 217 message: str 218 workflow_url: str 219 run_id: int | None = None 220 run_url: str | None = None 221 222 223@mcp_tool( 224 read_only=False, 225 idempotent=False, 226 open_world=True, 227) 228def trigger_ci_workflow( 229 owner: Annotated[ 230 str, 231 Field(description="Repository owner (e.g., 'airbytehq')"), 232 ], 233 repo: Annotated[ 234 str, 235 Field(description="Repository name (e.g., 'airbyte')"), 236 ], 237 workflow_file: Annotated[ 238 str, 239 Field(description="Workflow file name (e.g., 'connector-regression-test.yml')"), 240 ], 241 workflow_definition_ref: Annotated[ 242 str | None, 243 Field( 244 description="Branch name or PR number for the workflow definition to use. " 245 "If a PR number (integer string) is provided, it resolves to the PR's head branch name. " 246 "If a branch name is provided, it is used directly. " 247 "Defaults to the repository's default branch if not specified." 248 ), 249 ] = None, 250 inputs: Annotated[ 251 dict[str, str] | None, 252 Field( 253 description="Workflow inputs as a dictionary of string key-value pairs. " 254 "These are passed to the workflow_dispatch event." 255 ), 256 ] = None, 257) -> TriggerCIWorkflowResult: 258 """Trigger a GitHub Actions CI workflow via workflow_dispatch. 259 260 This tool triggers a workflow in any GitHub repository that has workflow_dispatch 261 enabled. It resolves PR numbers to branch names automatically since GitHub's 262 workflow_dispatch API only accepts branch names, not refs/pull/{pr}/head format. 263 264 Requires GITHUB_CI_WORKFLOW_TRIGGER_PAT or GITHUB_TOKEN environment variable 265 with 'actions:write' permission. 266 """ 267 # Guard: Check for required token 268 token = resolve_ci_trigger_github_token() 269 270 # Resolve workflow definition ref 271 # If a PR number is provided (integer string), resolve to the PR's head branch name 272 # Otherwise use the provided branch name or default to repo's default branch 273 if workflow_definition_ref: 274 if workflow_definition_ref.isdigit(): 275 # Resolve PR number to branch name via GitHub API 276 pr_head_info = get_pr_head_ref( 277 owner, 278 repo, 279 int(workflow_definition_ref), 280 token, 281 ) 282 resolved_ref = pr_head_info.ref 283 else: 284 resolved_ref = workflow_definition_ref 285 else: 286 # Default to main (most common default branch) 287 resolved_ref = "main" 288 289 # Trigger the workflow 290 result = trigger_workflow_dispatch( 291 owner=owner, 292 repo=repo, 293 workflow_file=workflow_file, 294 ref=resolved_ref, 295 inputs=inputs or {}, 296 token=token, 297 find_run=True, 298 ) 299 300 # Build response message 301 if result.run_id: 302 message = f"Successfully triggered workflow {workflow_file} on {owner}/{repo} (ref: {resolved_ref}). Run ID: {result.run_id}" 303 else: 304 message = f"Successfully triggered workflow {workflow_file} on {owner}/{repo} (ref: {resolved_ref}). Run ID not yet available." 305 306 return TriggerCIWorkflowResult( 307 success=True, 308 message=message, 309 workflow_url=result.workflow_url, 310 run_id=result.run_id, 311 run_url=result.run_url, 312 ) 313 314 315class DockerImageInfo(BaseModel): 316 """Response model for get_docker_image_info MCP tool.""" 317 318 exists: bool 319 image: str 320 tag: str 321 full_name: str 322 digest: str | None = None 323 last_updated: str | None = None 324 size_bytes: int | None = None 325 architecture: str | None = None 326 os: str | None = None 327 328 329def _check_dockerhub_image( 330 image: str, 331 tag: str, 332) -> dict | None: 333 """Check if a Docker image tag exists on DockerHub. 334 335 Args: 336 image: Docker image name (e.g., "airbyte/source-github") 337 tag: Image tag (e.g., "2.1.5-preview.abc1234") 338 339 Returns: 340 Tag data dictionary if found, None if not found. 341 """ 342 # DockerHub API endpoint for tag info 343 url = f"{DOCKERHUB_API_BASE}/repositories/{image}/tags/{tag}" 344 345 response = requests.get(url, timeout=30) 346 if response.status_code == 404: 347 return None 348 response.raise_for_status() 349 350 return response.json() 351 352 353@mcp_tool( 354 read_only=True, 355 idempotent=True, 356 open_world=True, 357) 358def get_docker_image_info( 359 image: Annotated[ 360 str, 361 Field(description="Docker image name (e.g., 'airbyte/source-github')"), 362 ], 363 tag: Annotated[ 364 str, 365 Field(description="Image tag (e.g., '2.1.5-preview.abc1234')"), 366 ], 367) -> DockerImageInfo: 368 """Check if a Docker image exists on DockerHub. 369 370 Returns information about the image if it exists, or indicates if it doesn't exist. 371 This is useful for confirming that a pre-release connector was successfully published. 372 """ 373 full_name = f"{image}:{tag}" 374 tag_data = _check_dockerhub_image(image, tag) 375 376 if not tag_data: 377 return DockerImageInfo( 378 exists=False, 379 image=image, 380 tag=tag, 381 full_name=full_name, 382 ) 383 384 # Extract image details from the first image in the list (if available) 385 images = tag_data.get("images", []) 386 first_image = images[0] if images else {} 387 388 return DockerImageInfo( 389 exists=True, 390 image=image, 391 tag=tag, 392 full_name=full_name, 393 digest=tag_data.get("digest"), 394 last_updated=tag_data.get("last_updated"), 395 size_bytes=first_image.get("size"), 396 architecture=first_image.get("architecture"), 397 os=first_image.get("os"), 398 ) 399 400 401def register_github_actions_tools(app: FastMCP) -> None: 402 """Register GitHub Actions tools with the FastMCP app. 403 404 Args: 405 app: FastMCP application instance 406 """ 407 register_mcp_tools(app, mcp_module=__name__)