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