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"
},
"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__)