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