airbyte_ops_mcp.mcp.connection_medic
MCP tools for connection state and catalog: read-only inspection plus emergency (medic) writes.
MCP reference
MCP primitives registered by the connection_medic module of the airbyte-internal-ops server: 2 tool(s), 0 prompt(s), 0 resource(s).
Tools (2)
get_connection_catalog
Hints: read-only · idempotent · open-world
Get the configured catalog for an Airbyte connection.
Returns the connection's configured catalog, which defines which streams are synced, their sync modes, primary keys, and cursor fields.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
workspace_id |
string | enum("266ebdfe-0d7b-4540-9817-de7e4505ba61") |
yes | — | The Airbyte Cloud workspace ID (UUID) or alias. Aliases: '@devin-ai-sandbox'. |
connection_id |
string |
yes | — | The connection ID (UUID) to fetch catalog for. |
config_api_root |
string | null |
no | null |
Optional API root URL override. Defaults to Airbyte Cloud. Use this to target local or self-hosted deployments. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"workspace_id": {
"anyOf": [
{
"type": "string"
},
{
"description": "Workspace ID aliases that can be used in place of UUIDs.\n\nEach member's name is the alias (e.g., \"@devin-ai-sandbox\") and its value\nis the actual workspace UUID. Use `WorkspaceAliasEnum.resolve()` to\nresolve aliases to actual IDs.",
"enum": [
"266ebdfe-0d7b-4540-9817-de7e4505ba61"
],
"type": "string"
}
],
"description": "The Airbyte Cloud workspace ID (UUID) or alias. Aliases: '@devin-ai-sandbox'."
},
"connection_id": {
"description": "The connection ID (UUID) to fetch catalog for.",
"type": "string"
},
"config_api_root": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional API root URL override. Defaults to Airbyte Cloud. Use this to target local or self-hosted deployments."
}
},
"required": [
"workspace_id",
"connection_id"
],
"type": "object"
}
Show output JSON schema
{
"additionalProperties": true,
"type": "object"
}
get_connection_state
Hints: read-only · idempotent · open-world
Get the current state for an Airbyte connection.
Returns the connection's sync state in Airbyte protocol format (snake_case). The state can be one of: stream (per-stream), global, legacy, or not_set.
When stream_name is provided, returns a dict with connection_id,
stream_name, stream_namespace, and stream_state keys.
Otherwise returns the full state as a list of AirbyteStateMessage dicts.
Parameters:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
workspace_id |
string | enum("266ebdfe-0d7b-4540-9817-de7e4505ba61") |
yes | — | The Airbyte Cloud workspace ID (UUID) or alias. Aliases: '@devin-ai-sandbox'. |
connection_id |
string |
yes | — | The connection ID (UUID) to fetch state for. |
stream_name |
string | null |
no | null |
Optional stream name to filter state for a single stream. When provided, only the matching stream's inner state blob is returned. |
stream_namespace |
string | null |
no | null |
Optional stream namespace to narrow the stream filter. Only used when stream_name is also provided. |
config_api_root |
string | null |
no | null |
Optional API root URL override. Defaults to Airbyte Cloud. Use this to target local or self-hosted deployments. |
Show input JSON schema
{
"additionalProperties": false,
"properties": {
"workspace_id": {
"anyOf": [
{
"type": "string"
},
{
"description": "Workspace ID aliases that can be used in place of UUIDs.\n\nEach member's name is the alias (e.g., \"@devin-ai-sandbox\") and its value\nis the actual workspace UUID. Use `WorkspaceAliasEnum.resolve()` to\nresolve aliases to actual IDs.",
"enum": [
"266ebdfe-0d7b-4540-9817-de7e4505ba61"
],
"type": "string"
}
],
"description": "The Airbyte Cloud workspace ID (UUID) or alias. Aliases: '@devin-ai-sandbox'."
},
"connection_id": {
"description": "The connection ID (UUID) to fetch state for.",
"type": "string"
},
"stream_name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional stream name to filter state for a single stream. When provided, only the matching stream's inner state blob is returned."
},
"stream_namespace": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional stream namespace to narrow the stream filter. Only used when stream_name is also provided."
},
"config_api_root": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"default": null,
"description": "Optional API root URL override. Defaults to Airbyte Cloud. Use this to target local or self-hosted deployments."
}
},
"required": [
"workspace_id",
"connection_id"
],
"type": "object"
}
Show output JSON schema
{
"properties": {
"result": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"items": {
"additionalProperties": true,
"type": "object"
},
"type": "array"
}
]
}
},
"required": [
"result"
],
"type": "object",
"x-fastmcp-wrap-result": true
}
1# Copyright (c) 2025 Airbyte, Inc., all rights reserved. 2"""MCP tools for connection state and catalog: read-only inspection plus emergency (medic) writes. 3 4## MCP reference 5 6.. include:: ../../../docs/mcp-generated/connection_medic.md 7 :start-line: 2 8""" 9 10# NOTE: We intentionally do NOT use `from __future__ import annotations` here. 11# FastMCP has issues resolving forward references when PEP 563 deferred annotations 12# are used. See: https://github.com/jlowin/fastmcp/issues/905 13# Python 3.12+ supports modern type hint syntax natively, so this is not needed. 14 15__all__: list[str] = [] 16 17import json 18import os 19from typing import Annotated, Any 20 21from airbyte import constants 22from airbyte.cloud.connections import CloudConnection 23from airbyte.cloud.workspaces import CloudWorkspace 24from airbyte.secrets.base import SecretString 25from fastmcp import Context, FastMCP 26from fastmcp_extensions import get_mcp_config, mcp_tool, register_mcp_tools 27from pydantic import Field 28 29import airbyte_ops_mcp.cloud_admin.connection_state as cloud_connection_state 30from airbyte_ops_mcp.cloud_admin.auth import CloudAuthError 31from airbyte_ops_mcp.constants import ( 32 MEDIC_MODE_ENV_VAR, 33 ServerConfigKey, 34 WorkspaceAliasEnum, 35) 36 37 38def _is_medic_mode_enabled() -> bool: 39 return os.getenv(MEDIC_MODE_ENV_VAR, "").lower() in ("1", "true") 40 41 42def _resolve_cloud_auth( 43 ctx: Context, 44) -> tuple[str | None, str | None, str | None]: 45 """Resolve authentication credentials for API calls. 46 47 Returns: 48 Tuple of (bearer_token, client_id, client_secret). 49 """ 50 bearer_token = get_mcp_config(ctx, ServerConfigKey.BEARER_TOKEN) 51 if bearer_token: 52 return bearer_token, None, None 53 54 try: 55 client_id = get_mcp_config(ctx, ServerConfigKey.CLIENT_ID) 56 client_secret = get_mcp_config(ctx, ServerConfigKey.CLIENT_SECRET) 57 return None, client_id, client_secret 58 except ValueError as e: 59 raise CloudAuthError( 60 f"Failed to resolve credentials. Ensure credentials are provided " 61 f"via Authorization header (Bearer token), " 62 f"HTTP headers (X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret), " 63 f"or environment variables. Error: {e}" 64 ) from e 65 66 67@mcp_tool( 68 destructive=True, 69 idempotent=False, 70 open_world=True, 71) 72def update_connection_state( 73 workspace_id: Annotated[ 74 str | WorkspaceAliasEnum, 75 Field( 76 description="The Airbyte Cloud workspace ID (UUID) or alias. " 77 "Aliases: '@devin-ai-sandbox'.", 78 ), 79 ], 80 connection_id: Annotated[ 81 str, 82 Field(description="The connection ID (UUID) to update state for."), 83 ], 84 connection_state_json: Annotated[ 85 str, 86 Field( 87 description="The connection state as a JSON string. Must include: " 88 "'stateType' (one of: 'global', 'stream', 'legacy'), " 89 "and one of: 'state' (for legacy), 'streamState' (for stream), " 90 "'globalState' (for global). " 91 "Tip: Use get_connection_state first to see the current format." 92 ), 93 ], 94 config_api_root: Annotated[ 95 str | None, 96 Field( 97 description="Optional API root URL override. " 98 "Defaults to Airbyte Cloud. " 99 "Use this to target local or self-hosted deployments.", 100 default=None, 101 ), 102 ] = None, 103 *, 104 ctx: Context, 105) -> list[dict[str, Any]]: 106 """Update the full state for an Airbyte connection. 107 108 WARNING: This is a destructive emergency operation. Use with caution. 109 Uses the safe variant that prevents updates while a sync is running (HTTP 423). 110 Get the current state first with get_connection_state, modify it, then pass 111 the full state object back here. 112 """ 113 resolved_workspace_id = WorkspaceAliasEnum.resolve(workspace_id) 114 if resolved_workspace_id is None: 115 raise ValueError( 116 f"Unable to resolve workspace_id {workspace_id!r} to a concrete workspace ID. " 117 "Ensure you provided a valid UUID or supported alias." 118 ) 119 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 120 121 conn = _get_cloud_connection( 122 workspace_id=resolved_workspace_id, 123 connection_id=connection_id, 124 api_root=config_api_root or constants.CLOUD_API_ROOT, 125 bearer_token=bearer_token, 126 client_id=client_id, 127 client_secret=client_secret, 128 ) 129 130 try: 131 connection_state = json.loads(connection_state_json) 132 except json.JSONDecodeError as e: 133 raise ValueError( 134 "Invalid JSON for parameter 'connection_state_json'. " 135 "Expected a JSON object representing Airbyte connection state " 136 "(for example, an object with a 'stateType' field and related state data)." 137 ) from e 138 conn.import_raw_state(connection_state) 139 return conn.dump_raw_state() 140 141 142@mcp_tool( 143 destructive=True, 144 idempotent=False, 145 open_world=True, 146) 147def update_stream_state( 148 workspace_id: Annotated[ 149 str | WorkspaceAliasEnum, 150 Field( 151 description="The Airbyte Cloud workspace ID (UUID) or alias. " 152 "Aliases: '@devin-ai-sandbox'.", 153 ), 154 ], 155 connection_id: Annotated[ 156 str, 157 Field(description="The connection ID (UUID) to update state for."), 158 ], 159 stream_name: Annotated[ 160 str, 161 Field(description="The name of the stream to update state for."), 162 ], 163 stream_state_json: Annotated[ 164 str, 165 Field( 166 description="The state blob for this stream as a JSON string. " 167 "This is the inner state object (e.g., {'cursor': '2024-01-01'}), " 168 "not the full connection state. " 169 "Tip: Use get_connection_state with stream_name first to see the current format." 170 ), 171 ], 172 stream_namespace: Annotated[ 173 str | None, 174 Field( 175 description="Optional stream namespace to identify the stream.", 176 default=None, 177 ), 178 ] = None, 179 config_api_root: Annotated[ 180 str | None, 181 Field( 182 description="Optional API root URL override. " 183 "Defaults to Airbyte Cloud. " 184 "Use this to target local or self-hosted deployments.", 185 default=None, 186 ), 187 ] = None, 188 *, 189 ctx: Context, 190) -> list[dict[str, Any]]: 191 """Update the state for a single stream within a connection. 192 193 WARNING: This is a destructive emergency operation. Use with caution. 194 Fetches the current full state, replaces only the specified stream's state, 195 then sends the updated state back. If the stream doesn't exist, it is appended. 196 Uses the safe variant that prevents updates while a sync is running (HTTP 423). 197 """ 198 resolved_workspace_id = WorkspaceAliasEnum.resolve(workspace_id) 199 if resolved_workspace_id is None: 200 raise ValueError( 201 f"Failed to resolve workspace_id '{workspace_id}' to a concrete workspace ID. " 202 "Use a valid workspace UUID or a supported alias." 203 ) 204 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 205 206 conn = _get_cloud_connection( 207 workspace_id=resolved_workspace_id, 208 connection_id=connection_id, 209 api_root=config_api_root or constants.CLOUD_API_ROOT, 210 bearer_token=bearer_token, 211 client_id=client_id, 212 client_secret=client_secret, 213 ) 214 215 try: 216 stream_state_dict = json.loads(stream_state_json) 217 except json.JSONDecodeError as exc: 218 raise ValueError( 219 "Invalid JSON provided for 'stream_state_json'. " 220 "Please supply a valid JSON object representing the stream state." 221 ) from exc 222 conn.set_stream_state( 223 stream_name=stream_name, 224 state_blob_dict=stream_state_dict, 225 stream_namespace=stream_namespace, 226 ) 227 return conn.dump_raw_state() 228 229 230@mcp_tool( 231 destructive=True, 232 idempotent=False, 233 open_world=True, 234) 235def reset_stream_state( 236 workspace_id: Annotated[ 237 str | WorkspaceAliasEnum, 238 Field( 239 description="The Airbyte Cloud workspace ID (UUID) or alias. " 240 "Aliases: '@devin-ai-sandbox'.", 241 ), 242 ], 243 connection_id: Annotated[ 244 str, 245 Field(description="The connection ID (UUID) to update state for."), 246 ], 247 stream_name: Annotated[ 248 str, 249 Field(description="The configured stream name whose state should be reset."), 250 ], 251 stream_namespace: Annotated[ 252 str | None, 253 Field( 254 description="Optional stream namespace to identify the stream.", 255 default=None, 256 ), 257 ] = None, 258 config_api_root: Annotated[ 259 str | None, 260 Field( 261 description="Optional API root URL override. " 262 "Defaults to Airbyte Cloud. " 263 "Use this to target local or self-hosted deployments.", 264 default=None, 265 ), 266 ] = None, 267 *, 268 ctx: Context, 269) -> cloud_connection_state.ResetStreamResult: 270 """Reset a configured stream's state so the next sync full-refreshes it. 271 272 WARNING: This is a destructive emergency operation. Use with caution. 273 Uses the safe variant that prevents updates while a sync is running (HTTP 423). 274 Returns `previous_state_backup` in raw Config API format so the state can be restored. 275 """ 276 resolved_workspace_id = WorkspaceAliasEnum.resolve(workspace_id) 277 if resolved_workspace_id is None: 278 raise ValueError( 279 f"Failed to resolve workspace_id '{workspace_id}' to a concrete workspace ID. " 280 "Use a valid workspace UUID or a supported alias." 281 ) 282 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 283 284 conn = _get_cloud_connection( 285 workspace_id=resolved_workspace_id, 286 connection_id=connection_id, 287 api_root=config_api_root or constants.CLOUD_API_ROOT, 288 bearer_token=bearer_token, 289 client_id=client_id, 290 client_secret=client_secret, 291 ) 292 return cloud_connection_state.reset_stream_state( 293 conn, 294 stream_name=stream_name, 295 stream_namespace=stream_namespace, 296 ) 297 298 299@mcp_tool( 300 destructive=True, 301 idempotent=False, 302 open_world=True, 303) 304def update_connection_catalog( 305 workspace_id: Annotated[ 306 str | WorkspaceAliasEnum, 307 Field( 308 description="The Airbyte Cloud workspace ID (UUID) or alias. " 309 "Aliases: '@devin-ai-sandbox'.", 310 ), 311 ], 312 connection_id: Annotated[ 313 str, 314 Field(description="The connection ID (UUID) to update catalog for."), 315 ], 316 configured_catalog_json: Annotated[ 317 str, 318 Field( 319 description="The configured catalog as a JSON string. " 320 "This replaces the entire configured catalog for the connection. " 321 "Tip: Use get_connection_catalog first to see the current format." 322 ), 323 ], 324 config_api_root: Annotated[ 325 str | None, 326 Field( 327 description="Optional API root URL override. " 328 "Defaults to Airbyte Cloud. " 329 "Use this to target local or self-hosted deployments.", 330 default=None, 331 ), 332 ] = None, 333 *, 334 ctx: Context, 335) -> dict[str, Any]: 336 """Replace the configured catalog for an Airbyte connection. 337 338 WARNING: This is a destructive emergency operation. Use with extreme caution. 339 This replaces the entire configured catalog, which controls which streams 340 are synced, their sync modes, primary keys, and cursor fields. 341 Get the current catalog first, modify it, then pass the full catalog back here. 342 """ 343 resolved_workspace_id = WorkspaceAliasEnum.resolve(workspace_id) 344 if resolved_workspace_id is None: 345 raise ValueError( 346 f"Unable to resolve workspace_id {workspace_id!r} to a concrete workspace ID. " 347 "Ensure you provided a valid UUID or supported alias." 348 ) 349 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 350 351 conn = _get_cloud_connection( 352 workspace_id=resolved_workspace_id, 353 connection_id=connection_id, 354 api_root=config_api_root or constants.CLOUD_API_ROOT, 355 bearer_token=bearer_token, 356 client_id=client_id, 357 client_secret=client_secret, 358 ) 359 360 try: 361 configured_catalog = json.loads(configured_catalog_json) 362 except json.JSONDecodeError as e: 363 raise ValueError( 364 "Invalid JSON for parameter 'configured_catalog_json'. " 365 "Expected a JSON object representing an Airbyte configured catalog." 366 ) from e 367 conn.import_raw_catalog(configured_catalog) 368 result = conn.dump_raw_catalog() 369 if result is None: 370 raise RuntimeError( 371 "Failed to retrieve catalog after import. " 372 f"workspace_id={resolved_workspace_id!r}, connection_id={connection_id!r}" 373 ) 374 return result 375 376 377def _get_cloud_connection( 378 workspace_id: str, 379 connection_id: str, 380 api_root: str, 381 bearer_token: str | None = None, 382 client_id: str | None = None, 383 client_secret: str | None = None, 384) -> CloudConnection: 385 """Create a CloudConnection from credentials.""" 386 workspace = CloudWorkspace( 387 workspace_id=workspace_id, 388 api_root=api_root, 389 client_id=SecretString(client_id) if client_id else None, 390 client_secret=SecretString(client_secret) if client_secret else None, 391 bearer_token=SecretString(bearer_token) if bearer_token else None, 392 ) 393 return workspace.get_connection(connection_id) 394 395 396@mcp_tool( 397 read_only=True, 398 idempotent=True, 399 open_world=True, 400) 401def get_connection_state( 402 workspace_id: Annotated[ 403 str | WorkspaceAliasEnum, 404 Field( 405 description="The Airbyte Cloud workspace ID (UUID) or alias. " 406 "Aliases: '@devin-ai-sandbox'.", 407 ), 408 ], 409 connection_id: Annotated[ 410 str, 411 Field(description="The connection ID (UUID) to fetch state for."), 412 ], 413 stream_name: Annotated[ 414 str | None, 415 Field( 416 description="Optional stream name to filter state for a single stream. " 417 "When provided, only the matching stream's inner state blob is returned.", 418 default=None, 419 ), 420 ] = None, 421 stream_namespace: Annotated[ 422 str | None, 423 Field( 424 description="Optional stream namespace to narrow the stream filter. " 425 "Only used when stream_name is also provided.", 426 default=None, 427 ), 428 ] = None, 429 config_api_root: Annotated[ 430 str | None, 431 Field( 432 description="Optional API root URL override. " 433 "Defaults to Airbyte Cloud. " 434 "Use this to target local or self-hosted deployments.", 435 default=None, 436 ), 437 ] = None, 438 *, 439 ctx: Context, 440) -> dict[str, Any] | list[dict[str, Any]]: 441 """Get the current state for an Airbyte connection. 442 443 Returns the connection's sync state in Airbyte protocol format (snake_case). 444 The state can be one of: stream (per-stream), global, legacy, or not_set. 445 446 When `stream_name` is provided, returns a dict with `connection_id`, 447 `stream_name`, `stream_namespace`, and `stream_state` keys. 448 Otherwise returns the full state as a list of `AirbyteStateMessage` dicts. 449 """ 450 resolved_workspace_id = WorkspaceAliasEnum.resolve(workspace_id) 451 if resolved_workspace_id is None: 452 raise ValueError( 453 f"Invalid workspace ID or alias: {workspace_id!r}. " 454 f"Supported aliases: {', '.join(sorted(alias.name.lower().replace('_', '-') for alias in WorkspaceAliasEnum))}" 455 ) 456 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 457 458 conn = _get_cloud_connection( 459 workspace_id=resolved_workspace_id, 460 connection_id=connection_id, 461 api_root=config_api_root or constants.CLOUD_API_ROOT, 462 bearer_token=bearer_token, 463 client_id=client_id, 464 client_secret=client_secret, 465 ) 466 467 if stream_name is not None: 468 stream_state = conn.get_stream_state( 469 stream_name=stream_name, 470 stream_namespace=stream_namespace, 471 ) 472 return { 473 "connection_id": connection_id, 474 "stream_name": stream_name, 475 "stream_namespace": stream_namespace, 476 "stream_state": stream_state, 477 } 478 479 return conn.dump_raw_state() 480 481 482@mcp_tool( 483 read_only=True, 484 idempotent=True, 485 open_world=True, 486) 487def get_connection_catalog( 488 workspace_id: Annotated[ 489 str | WorkspaceAliasEnum, 490 Field( 491 description="The Airbyte Cloud workspace ID (UUID) or alias. " 492 "Aliases: '@devin-ai-sandbox'.", 493 ), 494 ], 495 connection_id: Annotated[ 496 str, 497 Field(description="The connection ID (UUID) to fetch catalog for."), 498 ], 499 config_api_root: Annotated[ 500 str | None, 501 Field( 502 description="Optional API root URL override. " 503 "Defaults to Airbyte Cloud. " 504 "Use this to target local or self-hosted deployments.", 505 default=None, 506 ), 507 ] = None, 508 *, 509 ctx: Context, 510) -> dict[str, Any]: 511 """Get the configured catalog for an Airbyte connection. 512 513 Returns the connection's configured catalog, which defines which streams 514 are synced, their sync modes, primary keys, and cursor fields. 515 """ 516 resolved_workspace_id = WorkspaceAliasEnum.resolve(workspace_id) 517 if resolved_workspace_id is None: 518 raise ValueError( 519 f"Invalid workspace ID or alias: {workspace_id!r}. " 520 f"Supported aliases: {', '.join(sorted(alias.name.lower().replace('_', '-') for alias in WorkspaceAliasEnum))}" 521 ) 522 bearer_token, client_id, client_secret = _resolve_cloud_auth(ctx) 523 524 conn = _get_cloud_connection( 525 workspace_id=resolved_workspace_id, 526 connection_id=connection_id, 527 api_root=config_api_root or constants.CLOUD_API_ROOT, 528 bearer_token=bearer_token, 529 client_id=client_id, 530 client_secret=client_secret, 531 ) 532 533 result = conn.dump_raw_catalog() 534 if result is None: 535 raise ValueError("No configured catalog found for this connection.") 536 return result 537 538 539# Destructive "break glass" tools that mutate production connection state. 540# These are gated behind AIRBYTE_OPS_MEDIC_MODE so they are only exposed when 541# an operator has explicitly opted into medic mode. 542_MEDIC_WRITE_TOOLS = ( 543 "update_connection_state", 544 "update_stream_state", 545 "reset_stream_state", 546 "update_connection_catalog", 547) 548 549 550def register_connection_medic_tools(app: FastMCP) -> None: 551 """Register connection_medic tools with the FastMCP app. 552 553 The read-only inspection tools (`get_connection_state`, 554 `get_connection_catalog`) are always registered. The destructive 555 "break glass" write tools are only exposed when medic mode 556 (`AIRBYTE_OPS_MEDIC_MODE`) is enabled. 557 """ 558 register_mcp_tools(app, mcp_module=__name__) 559 if not _is_medic_mode_enabled(): 560 for tool_name in _MEDIC_WRITE_TOOLS: 561 app.local_provider.remove_tool(tool_name)