airbyte_ops_mcp.mcp.human_in_the_loop

MCP tool for human-in-the-loop escalation.

This module exposes the HITL escalation operation as an MCP tool for AI agents. It is a thin wrapper around the core dispatch function in human_in_the_loop module.

MCP reference

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

Tools (1)

escalate_to_human

Hints: open-world

Escalate to a human team member via Slack.

Posts a formatted message to the #human-in-the-loop Slack channel, tagging the specified person(s). The message includes clickable buttons for the Devin session, PR, and issue links when provided, plus any additional freeform action buttons.

The Slack message is sent by a GitHub Actions workflow so that Slack credentials are never exposed to the calling agent. The workflow resolves person identifiers (email, GitHub handle, or Slack ID) to Slack user IDs using the internal team roster.

Use this tool when you need human input, approval, or help that you cannot resolve on your own.

Parameters:

Name Type Required Default Description
target_person string yes Primary person to notify. Accepts an email address (e.g. 'aj@airbyte.io'), a GitHub handle prefixed with @ (e.g. '@aaronsteers'), or a Slack user ID (e.g. 'U05AKF1BCC9').
message string yes The message body to deliver to the human. Format using Slack mrkdwn: bold, _italic_, code, code blocks, > blockquotes, - bullet lists, and links. Should clearly explain what you need help with or what decision is required.
agent_session_url string yes Your agent session URL so the human can view the full context. Use the session URL from your system prompt.
cc array<string> | null no null Optional list of additional people to tag on the message. Each entry uses the same identifier format as target_person.
pr_url string | null no null Optional URL to a related pull request for the 'View PR' button.
issue_url string | null no null Optional URL to a related GitHub issue for the 'View Issue' button.
additional_actions object | null no null Optional dictionary of label -> URL pairs for extra action buttons. Example: {'Start Workflow': 'https://github.com/...actions/...'}.
approval_requested boolean no false Set to True to add 'Approve' and 'Reject' buttons that post back to the Slack app. Each button includes a confirmation dialog (if approval_request_summary is provided). When either button is clicked, both buttons morph into non-interactive status text (e.g. ':white_check_mark: Approved by @user' or ':x: Rejected by @user').
approval_request_summary string | null no null Short description of what the user is approving, shown in a Slack confirmation dialog before the Approve button fires. Rendered as a blockquote. MUST be at most 280 characters; over-limit or unbalanced-backtick inputs are rejected at call time. Keep it to the minimum info the approver needs to identify what they are approving. Example: 'Pinning source-hubspot prerelease 4.5.3-preview to workspace for testing'.
approval_request_detail_url string | null no null Optional URL where the reviewer can read full details of what they are being asked to approve. Rendered as a 'View Details' button in the Slack message.
connector_name string | null no null Optional connector name to display prominently in the Slack message. For example, when combined with request_type='action', the header may be rendered as '🔧 Action Requested — source-salesloft'. If request_type is omitted, the header remains the generic '🙋 Human-in-the-loop request' and the connector name is still included for additional context. Always provide this when the escalation is about a specific connector.
request_type enum("action", "review", "input", "guidance", "approval", "blocked") | null no null Type of escalation request. Controls the Slack message header emoji and label. Accepted values: 'action' (🔧 Action Requested), 'review' (👀 Review Requested), 'input' (❓ Input Needed), 'guidance' (🧭 Guidance Needed), 'approval' (✅ Approval Requested), 'blocked' (🚫 Still Blocked). When omitted, defaults to the generic 'Human-in-the-loop request' header.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "target_person": {
      "description": "Primary person to notify. Accepts an email address (e.g. 'aj@airbyte.io'), a GitHub handle prefixed with @ (e.g. '@aaronsteers'), or a Slack user ID (e.g. 'U05AKF1BCC9').",
      "type": "string"
    },
    "message": {
      "description": "The message body to deliver to the human. Format using Slack mrkdwn: *bold*, _italic_, `code`, ```code blocks```, > blockquotes, - bullet lists, and <url|label> links. Should clearly explain what you need help with or what decision is required.",
      "type": "string"
    },
    "agent_session_url": {
      "description": "Your agent session URL so the human can view the full context. Use the session URL from your system prompt.",
      "type": "string"
    },
    "cc": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional list of additional people to tag on the message. Each entry uses the same identifier format as target_person."
    },
    "pr_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional URL to a related pull request for the 'View PR' button."
    },
    "issue_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional URL to a related GitHub issue for the 'View Issue' button."
    },
    "additional_actions": {
      "anyOf": [
        {
          "additionalProperties": {
            "type": "string"
          },
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional dictionary of label -> URL pairs for extra action buttons. Example: {'Start Workflow': 'https://github.com/...actions/...'}."
    },
    "approval_requested": {
      "default": false,
      "description": "Set to True to add 'Approve' and 'Reject' buttons that post back to the Slack app. Each button includes a confirmation dialog (if approval_request_summary is provided). When either button is clicked, both buttons morph into non-interactive status text (e.g. ':white_check_mark: Approved by @user' or ':x: Rejected by @user').",
      "type": "boolean"
    },
    "approval_request_summary": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Short description of what the user is approving, shown in a Slack confirmation dialog before the Approve button fires. Rendered as a blockquote. MUST be at most 280 characters; over-limit or unbalanced-backtick inputs are rejected at call time. Keep it to the minimum info the approver needs to identify what they are approving. Example: 'Pinning source-hubspot prerelease 4.5.3-preview to workspace for testing'."
    },
    "approval_request_detail_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional URL where the reviewer can read full details of what they are being asked to approve. Rendered as a 'View Details' button in the Slack message."
    },
    "connector_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Optional connector name to display prominently in the Slack message. For example, when combined with request_type='action', the header may be rendered as '\ud83d\udd27 Action Requested \u2014 source-salesloft'. If request_type is omitted, the header remains the generic '\ud83d\ude4b Human-in-the-loop request' and the connector name is still included for additional context. Always provide this when the escalation is about a specific connector."
    },
    "request_type": {
      "anyOf": [
        {
          "description": "Type of escalation request, controlling the Slack header emoji and label.",
          "enum": [
            "action",
            "review",
            "input",
            "guidance",
            "approval",
            "blocked"
          ],
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Type of escalation request. Controls the Slack message header emoji and label. Accepted values: 'action' (\ud83d\udd27 Action Requested), 'review' (\ud83d\udc40 Review Requested), 'input' (\u2753 Input Needed), 'guidance' (\ud83e\udded Guidance Needed), 'approval' (\u2705 Approval Requested), 'blocked' (\ud83d\udeab Still Blocked). When omitted, defaults to the generic 'Human-in-the-loop request' header."
    }
  },
  "required": [
    "target_person",
    "message",
    "agent_session_url"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Response from the human-in-the-loop escalation tool.",
  "properties": {
    "success": {
      "description": "Whether the workflow was triggered successfully",
      "type": "boolean"
    },
    "message": {
      "description": "Human-readable status message",
      "type": "string"
    },
    "workflow_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "URL to view the GitHub Actions workflow file"
    },
    "run_id": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "GitHub Actions workflow run ID"
    },
    "run_url": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Direct URL to the GitHub Actions workflow run"
    }
  },
  "required": [
    "success",
    "message"
  ],
  "type": "object"
}

  1# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
  2"""MCP tool for human-in-the-loop escalation.
  3
  4This module exposes the HITL escalation operation as an MCP tool for AI agents.
  5It is a thin wrapper around the core dispatch function in human_in_the_loop module.
  6
  7## MCP reference
  8
  9.. include:: ../../../docs/mcp-generated/human_in_the_loop.md
 10    :start-line: 2
 11"""
 12
 13from __future__ import annotations
 14
 15__all__: list[str] = []
 16
 17from enum import StrEnum
 18from typing import Annotated
 19
 20from fastmcp import FastMCP
 21from fastmcp_extensions import mcp_tool, register_mcp_tools
 22from pydantic import BaseModel, Field
 23
 24from airbyte_ops_mcp.human_in_the_loop import dispatch_escalation
 25
 26
 27class RequestType(StrEnum):
 28    """Type of escalation request, controlling the Slack header emoji and label."""
 29
 30    ACTION = "action"
 31    REVIEW = "review"
 32    INPUT = "input"
 33    GUIDANCE = "guidance"
 34    APPROVAL = "approval"
 35    BLOCKED = "blocked"
 36
 37
 38# Maps request_type values to (header_emoji, header_label) tuples.
 39_REQUEST_TYPE_HEADERS: dict[RequestType, tuple[str, str]] = {
 40    RequestType.ACTION: ("🔧", "Action Requested"),
 41    RequestType.REVIEW: ("👀", "Review Requested"),
 42    RequestType.INPUT: ("❓", "Input Needed"),
 43    RequestType.GUIDANCE: ("🧭", "Guidance Needed"),
 44    RequestType.APPROVAL: ("✅", "Approval Requested"),
 45    RequestType.BLOCKED: ("🚫", "Still Blocked"),
 46}
 47
 48
 49class EscalateToHumanResponse(BaseModel):
 50    """Response from the human-in-the-loop escalation tool."""
 51
 52    success: bool = Field(description="Whether the workflow was triggered successfully")
 53    message: str = Field(description="Human-readable status message")
 54    workflow_url: str | None = Field(
 55        default=None,
 56        description="URL to view the GitHub Actions workflow file",
 57    )
 58    run_id: int | None = Field(
 59        default=None,
 60        description="GitHub Actions workflow run ID",
 61    )
 62    run_url: str | None = Field(
 63        default=None,
 64        description="Direct URL to the GitHub Actions workflow run",
 65    )
 66
 67
 68@mcp_tool(
 69    read_only=False,
 70    idempotent=False,
 71    open_world=True,
 72)
 73def escalate_to_human(
 74    target_person: Annotated[
 75        str,
 76        "Primary person to notify. Accepts an email address (e.g. 'aj@airbyte.io'), "
 77        "a GitHub handle prefixed with @ (e.g. '@aaronsteers'), "
 78        "or a Slack user ID (e.g. 'U05AKF1BCC9').",
 79    ],
 80    message: Annotated[
 81        str,
 82        "The message body to deliver to the human. Format using Slack mrkdwn: "
 83        "*bold*, _italic_, `code`, ```code blocks```, > blockquotes, "
 84        "- bullet lists, and <url|label> links. Should clearly explain "
 85        "what you need help with or what decision is required.",
 86    ],
 87    agent_session_url: Annotated[
 88        str,
 89        "Your agent session URL so the human can view the full context. "
 90        "Use the session URL from your system prompt.",
 91    ],
 92    cc: Annotated[
 93        list[str] | None,
 94        "Optional list of additional people to tag on the message. "
 95        "Each entry uses the same identifier format as target_person.",
 96    ] = None,
 97    pr_url: Annotated[
 98        str | None,
 99        "Optional URL to a related pull request for the 'View PR' button.",
100    ] = None,
101    issue_url: Annotated[
102        str | None,
103        "Optional URL to a related GitHub issue for the 'View Issue' button.",
104    ] = None,
105    additional_actions: Annotated[
106        dict[str, str] | None,
107        "Optional dictionary of label -> URL pairs for extra action buttons. "
108        "Example: {'Start Workflow': 'https://github.com/...actions/...'}.",
109    ] = None,
110    approval_requested: Annotated[
111        bool,
112        "Set to True to add 'Approve' and 'Reject' buttons that post back to the Slack app. "
113        "Each button includes a confirmation dialog (if approval_request_summary is provided). "
114        "When either button is clicked, both buttons morph into non-interactive status text "
115        "(e.g. ':white_check_mark: Approved by @user' or ':x: Rejected by @user').",
116    ] = False,
117    approval_request_summary: Annotated[
118        str | None,
119        "Short description of what the user is approving, shown in a Slack "
120        "confirmation dialog before the Approve button fires. Rendered as a "
121        "blockquote. MUST be at most 280 characters; over-limit or "
122        "unbalanced-backtick inputs are rejected at call time. Keep it to the "
123        "minimum info the approver needs to identify what they are approving. "
124        "Example: 'Pinning source-hubspot prerelease 4.5.3-preview to workspace for testing'.",
125    ] = None,
126    approval_request_detail_url: Annotated[
127        str | None,
128        "Optional URL where the reviewer can read full details of what they are "
129        "being asked to approve. Rendered as a 'View Details' button in the Slack message.",
130    ] = None,
131    connector_name: Annotated[
132        str | None,
133        "Optional connector name to display prominently in the Slack message. "
134        "For example, when combined with request_type='action', the header may be "
135        "rendered as '🔧 Action Requested — source-salesloft'. If request_type is omitted, "
136        "the header remains the generic '🙋 Human-in-the-loop request' and the connector "
137        "name is still included for additional context. Always provide this when the "
138        "escalation is about a specific connector.",
139    ] = None,
140    request_type: Annotated[
141        RequestType | None,
142        "Type of escalation request. Controls the Slack message header emoji and label. "
143        "Accepted values: 'action' (🔧 Action Requested), "
144        "'review' (👀 Review Requested), 'input' (❓ Input Needed), "
145        "'guidance' (🧭 Guidance Needed), 'approval' (✅ Approval Requested), "
146        "'blocked' (🚫 Still Blocked). "
147        "When omitted, defaults to the generic 'Human-in-the-loop request' header.",
148    ] = None,
149) -> EscalateToHumanResponse:
150    """Escalate to a human team member via Slack.
151
152    Posts a formatted message to the #human-in-the-loop Slack channel,
153    tagging the specified person(s). The message includes clickable buttons
154    for the Devin session, PR, and issue links when provided, plus any
155    additional freeform action buttons.
156
157    The Slack message is sent by a GitHub Actions workflow so that Slack
158    credentials are never exposed to the calling agent. The workflow
159    resolves person identifiers (email, GitHub handle, or Slack ID) to
160    Slack user IDs using the internal team roster.
161
162    Use this tool when you need human input, approval, or help that you
163    cannot resolve on your own.
164    """
165    # Resolve request_type to header_emoji and header_label
166    header_emoji: str | None = None
167    header_label: str | None = None
168    if request_type is not None:
169        header_emoji, header_label = _REQUEST_TYPE_HEADERS[request_type]
170
171    result = dispatch_escalation(
172        target_person=target_person,
173        message=message,
174        agent_session_url=agent_session_url,
175        cc=cc,
176        pr_url=pr_url,
177        issue_url=issue_url,
178        additional_actions=additional_actions,
179        approval_requested=approval_requested,
180        approval_request_summary=approval_request_summary,
181        approval_request_detail_url=approval_request_detail_url,
182        connector_name=connector_name,
183        header_emoji=header_emoji,
184        header_label=header_label,
185    )
186
187    view_url = result.run_url or result.workflow_url
188    return EscalateToHumanResponse(
189        success=True,
190        message=(
191            f"Escalation sent to '{target_person}' via #human-in-the-loop. "
192            f"View progress at: {view_url}"
193        ),
194        workflow_url=result.workflow_url,
195        run_id=result.run_id,
196        run_url=result.run_url,
197    )
198
199
200def register_human_in_the_loop_tools(app: FastMCP) -> None:
201    """Register human-in-the-loop tools with the FastMCP app."""
202    register_mcp_tools(app, mcp_module=__name__)