airbyte.mcp.agents

Airbyte Agents MCP operations.

⚠️ Experimental Tools — Insiders Only

The Airbyte Agents MCP tools are experimental and hidden by default. They are advertised only when insiders mode is enabled (AIRBYTE_MCP_INSIDERS for stdio servers, X-MCP-Insiders for hosted servers) or when the include-modules setting explicitly names agents. Tool names, arguments, and result shapes may change or be removed without notice between minor versions of PyAirbyte. Pin an exact PyAirbyte version if you depend on them.

These tools are also Cloud-only: they are hidden whenever AIRBYTE_CLOUD_API_URL / AIRBYTE_CLOUD_CONFIG_API_URL are overridden, unless AIRBYTE_AGENTS_API_URL is set.

agents module

MCP primitives registered by the agents module of the airbyte-mcp server: 7 tool(s), 0 prompt(s), 0 resource(s).

Tools (7)

execute_agent_connector

Hints: open-world

Availability: experimental, insiders only (AIRBYTE_MCP_INSIDERS=1 for stdio, X-MCP-Insiders: 1 for hosted servers, or name the module in the include-modules setting; AIRBYTE_MCP_INSIDERS=0 disables it regardless).

Execute a single action against an Airbyte Agents connector, including writes.

Prefer `execute_agent_connector_ro` when only reading, since it is available in
read-only mode. Entity types and actions are connector-specific, so call
`inspect_agent_connector` first. The connector must belong to the given workspace.

The Airbyte Agents API authenticates with Airbyte Cloud credentials. When connecting to a hosted MCP server, provide a bearer token via the Authorization header, or client credentials via the transport Client-Id and Client-Secret headers. For local or stdio connections, set the AIRBYTE_CLOUD_BEARER_TOKEN environment variable, or both AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET. Call list_agent_connectors to discover connector IDs, then inspect_agent_connector to learn which entities a connector supports, before calling execute_agent_connector. Use list_agent_skills to discover skills, and pass the docs.skill_id reported by inspect_agent_connector to read_agent_skill_docs for connector usage docs.

Parameters

Name Type Required Default Description
connector_id string yes — The ID of the Airbyte Agents connector.
entity_type string yes — The type of entity to act on, for example 'issues'. Call inspect_agent_connector to see the entity types a connector supports.
action enum("list", "get", "search", "sql_select") | enum("create", "update", "delete") yes — The action to run against the entity type. The search action is the connector's native API search, parallel to get and list. For sql_select, pass sql and sql_dialect (snowflake or bigquery) in api_args and any value for entity_type. The download action is deliberately absent because it returns a binary stream rather than JSON.
api_args object | string | null no null Connector-specific arguments for the action, as an object or a JSON object string. For example {'repository': 'airbytehq/PyAirbyte'}.
select_fields array<string> | string | null no null Fields to keep in the response, as a list or a CSV string.
exclude_fields array<string> | string | null no null Fields to drop from the response, as a list or a CSV string.
page_size integer | null no null Maximum number of entities to return in this page. Honored by Context Store search actions (sent as limit). Direct connector actions take their own page-size argument, if any, in api_args.
cursor string | null no null Pagination cursor for Context Store search actions and sql_select, taken from end_cursor of a previous result. Direct connector actions such as list do not read this; pass their own cursor argument in api_args instead (for example GitHub's after), as named in the skill docs.
intent string | null no null A short description of why the action is being run.
read_only boolean | null no null Set to true to reject write actions before any request is sent, when the caller wants a read guarantee from this tool.
workspace_id string | null no null Workspace ID. Hosted MCP connections pass it via the X-Airbyte-Workspace-Id header; local or stdio connections use the AIRBYTE_CLOUD_WORKSPACE_ID environment variable.
organization_id string | null no null Organization ID. Omit it when the credentials belong to exactly one organization, or when it is already configured via the X-Airbyte-Organization-Id header or the AIRBYTE_CLOUD_ORGANIZATION_ID environment variable. To discover organization IDs, call list_cloud_organizations to search organizations by name, or list_agent_workspaces, which reports the owning organization of each workspace. Workspace-scoped tools derive it from the workspace's parent organization when omitted.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "connector_id": {
      "description": "The ID of the Airbyte Agents connector.",
      "type": "string"
    },
    "entity_type": {
      "description": "The type of entity to act on, for example 'issues'. Call `inspect_agent_connector` to see the entity types a connector supports.",
      "type": "string"
    },
    "action": {
      "anyOf": [
        {
          "description": "Connector actions that only read data.\n\nThe `search` action is the connector's native API search, parallel to `get` and `list`.\nThe `sql_select` action runs one read-only SQL statement (or `SHOW TABLES`) on the query\nengine behind a destination connector. Pass `sql` and `sql_dialect` (and optionally\n`dry_run`) in `api_args`; `entity_type` is ignored for this action.\n\nThe `download` action is deliberately absent even though it reads: it returns a binary\nstream rather than JSON, which PyAirbyte does not yet support.",
          "enum": [
            "list",
            "get",
            "search",
            "sql_select"
          ],
          "type": "string"
        },
        {
          "description": "Connector actions that create, update, or delete data.",
          "enum": [
            "create",
            "update",
            "delete"
          ],
          "type": "string"
        }
      ],
      "description": "The action to run against the entity type. The `search` action is the connector's native API search, parallel to `get` and `list`. For `sql_select`, pass `sql` and `sql_dialect` (snowflake or bigquery) in `api_args` and any value for `entity_type`. The `download` action is deliberately absent because it returns a binary stream rather than JSON."
    },
    "api_args": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Connector-specific arguments for the action, as an object or a JSON object string. For example {'repository': 'airbytehq/PyAirbyte'}."
    },
    "select_fields": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Fields to keep in the response, as a list or a CSV string."
    },
    "exclude_fields": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Fields to drop from the response, as a list or a CSV string."
    },
    "page_size": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Maximum number of entities to return in this page. Honored by Context Store `search` actions (sent as `limit`). Direct connector actions take their own page-size argument, if any, in `api_args`."
    },
    "cursor": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Pagination cursor for Context Store `search` actions and `sql_select`, taken from `end_cursor` of a previous result. Direct connector actions such as `list` do not read this; pass their own cursor argument in `api_args` instead (for example GitHub's `after`), as named in the skill docs."
    },
    "intent": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "A short description of why the action is being run."
    },
    "read_only": {
      "anyOf": [
        {
          "type": "boolean"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Set to `true` to reject write actions before any request is sent, when the caller wants a read guarantee from this tool."
    },
    "workspace_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Workspace ID. Hosted MCP connections pass it via the `X-Airbyte-Workspace-Id` header; local or stdio connections use the `AIRBYTE_CLOUD_WORKSPACE_ID` environment variable."
    },
    "organization_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Organization ID. Omit it when the credentials belong to exactly one organization, or when it is already configured via the `X-Airbyte-Organization-Id` header or the `AIRBYTE_CLOUD_ORGANIZATION_ID` environment variable. To discover organization IDs, call `list_cloud_organizations` to search organizations by name, or `list_agent_workspaces`, which reports the owning organization of each workspace. Workspace-scoped tools derive it from the workspace's parent organization when omitted."
    }
  },
  "required": [
    "connector_id",
    "entity_type",
    "action"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of executing a single action against an Airbyte Agents connector.",
  "properties": {
    "status": {
      "type": "string"
    },
    "result": {
      "default": null,
      "title": "Result"
    },
    "has_next_page": {
      "default": false,
      "type": "boolean"
    },
    "end_cursor": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "execution_time_ms": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "warning": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "message": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "required": [
    "status"
  ],
  "type": "object"
}

execute_agent_connector_ro

Hints: read-only · idempotent · open-world

Availability: experimental, insiders only (AIRBYTE_MCP_INSIDERS=1 for stdio, X-MCP-Insiders: 1 for hosted servers, or name the module in the include-modules setting; AIRBYTE_MCP_INSIDERS=0 disables it regardless).

Read data from an Airbyte Agents connector, without modifying anything.

This tool only accepts read actions, so it stays available in read-only mode. Use
`execute_agent_connector` for actions that create, update, or delete data. Entity types
are connector-specific, so call `inspect_agent_connector` first. The connector must
belong to the given workspace.

To query a destination:
1. Run `SHOW TABLES` to discover tables.
2. Run `SELECT * FROM <table> LIMIT 1` with `"dry_run": true` in `api_args` to get the
   real column names; never guess them.
3. Select data using the discovered names.

Use the destination's `connector_id` and `sql_dialect` as reported by
`list_agent_connectors`. On Snowflake, identifiers are upper-cased; write them unquoted
(double-quoting makes them case-sensitive). Quote a name only when discovery returns it in
mixed or lower case, exactly as returned.

The Airbyte Agents API authenticates with Airbyte Cloud credentials. When connecting to a hosted MCP server, provide a bearer token via the Authorization header, or client credentials via the transport Client-Id and Client-Secret headers. For local or stdio connections, set the AIRBYTE_CLOUD_BEARER_TOKEN environment variable, or both AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET. Call list_agent_connectors to discover connector IDs, then inspect_agent_connector to learn which entities a connector supports, before calling execute_agent_connector. Use list_agent_skills to discover skills, and pass the docs.skill_id reported by inspect_agent_connector to read_agent_skill_docs for connector usage docs.

Parameters

Name Type Required Default Description
connector_id string yes — The ID of the Airbyte Agents connector, from list_agent_connectors. For sql_select, pass the connector_id of a destination entry (connector_kind="destination") from that listing.
entity_type string yes — The type of entity to act on, for example 'issues'. Call inspect_agent_connector to see the entity types a connector supports.
action enum("list", "get", "search", "sql_select") yes — The read action to run against the entity type. The search action is the connector's native API search, parallel to get and list. For sql_select, pass sql and sql_dialect (snowflake or bigquery) in api_args and any value for entity_type; the connector_id is a destination listed by list_agent_connectors; SHOW TABLES lists its tables and SELECT * FROM <table> LIMIT 1 with "dry_run": true in api_args returns its columns without reading rows; never guess columns. On Snowflake, write identifiers unquoted unless discovery returned mixed case. The download action is deliberately absent because it returns a binary stream rather than JSON.
api_args object | string | null no null Connector-specific arguments for the action, as an object or a JSON object string. For example {'repository': 'airbytehq/PyAirbyte'}.
select_fields array<string> | string | null no null Fields to keep in the response, as a list or a CSV string.
exclude_fields array<string> | string | null no null Fields to drop from the response, as a list or a CSV string.
page_size integer | null no null Maximum number of entities to return in this page. Honored by Context Store search actions (sent as limit). Direct connector actions take their own page-size argument, if any, in api_args.
cursor string | null no null Pagination cursor for Context Store search actions and sql_select, taken from end_cursor of a previous result. Direct connector actions such as list do not read this; pass their own cursor argument in api_args instead (for example GitHub's after), as named in the skill docs.
intent string | null no null A short description of why the action is being run.
workspace_id string | null no null Workspace ID. Hosted MCP connections pass it via the X-Airbyte-Workspace-Id header; local or stdio connections use the AIRBYTE_CLOUD_WORKSPACE_ID environment variable.
organization_id string | null no null Organization ID. Omit it when the credentials belong to exactly one organization, or when it is already configured via the X-Airbyte-Organization-Id header or the AIRBYTE_CLOUD_ORGANIZATION_ID environment variable. To discover organization IDs, call list_cloud_organizations to search organizations by name, or list_agent_workspaces, which reports the owning organization of each workspace. Workspace-scoped tools derive it from the workspace's parent organization when omitted.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "connector_id": {
      "description": "The ID of the Airbyte Agents connector, from `list_agent_connectors`. For `sql_select`, pass the `connector_id` of a destination entry (`connector_kind=\"destination\"`) from that listing.",
      "type": "string"
    },
    "entity_type": {
      "description": "The type of entity to act on, for example 'issues'. Call `inspect_agent_connector` to see the entity types a connector supports.",
      "type": "string"
    },
    "action": {
      "description": "The read action to run against the entity type. The `search` action is the connector's native API search, parallel to `get` and `list`. For `sql_select`, pass `sql` and `sql_dialect` (snowflake or bigquery) in `api_args` and any value for `entity_type`; the `connector_id` is a destination listed by `list_agent_connectors`; `SHOW TABLES` lists its tables and `SELECT * FROM <table> LIMIT 1` with `\"dry_run\": true` in `api_args` returns its columns without reading rows; never guess columns. On Snowflake, write identifiers unquoted unless discovery returned mixed case. The `download` action is deliberately absent because it returns a binary stream rather than JSON.",
      "enum": [
        "list",
        "get",
        "search",
        "sql_select"
      ],
      "type": "string"
    },
    "api_args": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Connector-specific arguments for the action, as an object or a JSON object string. For example {'repository': 'airbytehq/PyAirbyte'}."
    },
    "select_fields": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Fields to keep in the response, as a list or a CSV string."
    },
    "exclude_fields": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Fields to drop from the response, as a list or a CSV string."
    },
    "page_size": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Maximum number of entities to return in this page. Honored by Context Store `search` actions (sent as `limit`). Direct connector actions take their own page-size argument, if any, in `api_args`."
    },
    "cursor": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Pagination cursor for Context Store `search` actions and `sql_select`, taken from `end_cursor` of a previous result. Direct connector actions such as `list` do not read this; pass their own cursor argument in `api_args` instead (for example GitHub's `after`), as named in the skill docs."
    },
    "intent": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "A short description of why the action is being run."
    },
    "workspace_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Workspace ID. Hosted MCP connections pass it via the `X-Airbyte-Workspace-Id` header; local or stdio connections use the `AIRBYTE_CLOUD_WORKSPACE_ID` environment variable."
    },
    "organization_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Organization ID. Omit it when the credentials belong to exactly one organization, or when it is already configured via the `X-Airbyte-Organization-Id` header or the `AIRBYTE_CLOUD_ORGANIZATION_ID` environment variable. To discover organization IDs, call `list_cloud_organizations` to search organizations by name, or `list_agent_workspaces`, which reports the owning organization of each workspace. Workspace-scoped tools derive it from the workspace's parent organization when omitted."
    }
  },
  "required": [
    "connector_id",
    "entity_type",
    "action"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of executing a single action against an Airbyte Agents connector.",
  "properties": {
    "status": {
      "type": "string"
    },
    "result": {
      "default": null,
      "title": "Result"
    },
    "has_next_page": {
      "default": false,
      "type": "boolean"
    },
    "end_cursor": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "execution_time_ms": {
      "anyOf": [
        {
          "type": "integer"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "warning": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "message": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "required": [
    "status"
  ],
  "type": "object"
}

inspect_agent_connector

Hints: read-only · idempotent · open-world

Availability: experimental, insiders only (AIRBYTE_MCP_INSIDERS=1 for stdio, X-MCP-Insiders: 1 for hosted servers, or name the module in the include-modules setting; AIRBYTE_MCP_INSIDERS=0 disables it regardless).

Inspect an Airbyte Agents connector: metadata, readiness, warnings, and inline docs.

Call this before `execute_agent_connector` to learn what the connector exposes.
The connector's usage docs summary is returned inline in `docs`; use
`read_agent_skill_docs(skill_id=docs.skill_id, section=...)` for a single section's
full detail.
Airbyte Cloud destinations in the workspace are also accepted and resolve to
built-in docs under `connector-destination:<id>`.

The Airbyte Agents API authenticates with Airbyte Cloud credentials. When connecting to a hosted MCP server, provide a bearer token via the Authorization header, or client credentials via the transport Client-Id and Client-Secret headers. For local or stdio connections, set the AIRBYTE_CLOUD_BEARER_TOKEN environment variable, or both AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET. Call list_agent_connectors to discover connector IDs, then inspect_agent_connector to learn which entities a connector supports, before calling execute_agent_connector. Use list_agent_skills to discover skills, and pass the docs.skill_id reported by inspect_agent_connector to read_agent_skill_docs for connector usage docs.

Parameters

Name Type Required Default Description
connector_id string yes — The ID of a supported Airbyte source or destination with agent features enabled.
workspace_id string | null no null Workspace ID. Hosted MCP connections pass it via the X-Airbyte-Workspace-Id header; local or stdio connections use the AIRBYTE_CLOUD_WORKSPACE_ID environment variable.
organization_id string | null no null Organization ID. Omit it when the credentials belong to exactly one organization, or when it is already configured via the X-Airbyte-Organization-Id header or the AIRBYTE_CLOUD_ORGANIZATION_ID environment variable. To discover organization IDs, call list_cloud_organizations to search organizations by name, or list_agent_workspaces, which reports the owning organization of each workspace. Workspace-scoped tools derive it from the workspace's parent organization when omitted.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "connector_id": {
      "description": "The ID of a supported Airbyte source or destination with agent features enabled.",
      "type": "string"
    },
    "workspace_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Workspace ID. Hosted MCP connections pass it via the `X-Airbyte-Workspace-Id` header; local or stdio connections use the `AIRBYTE_CLOUD_WORKSPACE_ID` environment variable."
    },
    "organization_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Organization ID. Omit it when the credentials belong to exactly one organization, or when it is already configured via the `X-Airbyte-Organization-Id` header or the `AIRBYTE_CLOUD_ORGANIZATION_ID` environment variable. To discover organization IDs, call `list_cloud_organizations` to search organizations by name, or `list_agent_workspaces`, which reports the owning organization of each workspace. Workspace-scoped tools derive it from the workspace's parent organization when omitted."
    }
  },
  "required": [
    "connector_id"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Details about a single Airbyte Agents connector.",
  "properties": {
    "connector_id": {
      "type": "string"
    },
    "connector_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "workspace_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "integration_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "docs": {
      "anyOf": [
        {
          "description": "Docs summary embedded in `inspect_agent_connector` results.\n\nThe section outline is intentionally omitted here; `read_agent_skill_docs`\nreturns it as `outline`.",
          "properties": {
            "skill_id": {
              "type": "string"
            },
            "title": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "default": null
            },
            "content": {
              "type": "string"
            },
            "guidance": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "default": null
            },
            "warnings": {
              "anyOf": [
                {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                },
                {
                  "type": "null"
                }
              ],
              "default": null
            },
            "errors": {
              "anyOf": [
                {
                  "items": {
                    "type": "string"
                  },
                  "type": "array"
                },
                {
                  "type": "null"
                }
              ],
              "default": null
            }
          },
          "required": [
            "skill_id",
            "content"
          ],
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "warnings": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "errors": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "required": [
    "connector_id"
  ],
  "type": "object"
}

list_agent_connectors

Hints: read-only · idempotent · open-world

Availability: experimental, insiders only (AIRBYTE_MCP_INSIDERS=1 for stdio, X-MCP-Insiders: 1 for hosted servers, or name the module in the include-modules setting; AIRBYTE_MCP_INSIDERS=0 disables it regardless).

List the connectors configured in an Airbyte Agents workspace.

Sources come from the Agents API. SQL passthrough destinations (Snowflake, BigQuery) in
the Cloud workspace are appended with `connector_kind="destination"`; they support only
the `sql_select` action of `execute_agent_connector_ro`.

The Airbyte Agents API authenticates with Airbyte Cloud credentials. When connecting to a hosted MCP server, provide a bearer token via the Authorization header, or client credentials via the transport Client-Id and Client-Secret headers. For local or stdio connections, set the AIRBYTE_CLOUD_BEARER_TOKEN environment variable, or both AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET. Call list_agent_connectors to discover connector IDs, then inspect_agent_connector to learn which entities a connector supports, before calling execute_agent_connector. Use list_agent_skills to discover skills, and pass the docs.skill_id reported by inspect_agent_connector to read_agent_skill_docs for connector usage docs.

Parameters

Name Type Required Default Description
workspace_id string | null no null Workspace ID. Hosted MCP connections pass it via the X-Airbyte-Workspace-Id header; local or stdio connections use the AIRBYTE_CLOUD_WORKSPACE_ID environment variable.
organization_id string | null no null Organization ID. Omit it when the credentials belong to exactly one organization, or when it is already configured via the X-Airbyte-Organization-Id header or the AIRBYTE_CLOUD_ORGANIZATION_ID environment variable. To discover organization IDs, call list_cloud_organizations to search organizations by name, or list_agent_workspaces, which reports the owning organization of each workspace. Workspace-scoped tools derive it from the workspace's parent organization when omitted.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "workspace_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Workspace ID. Hosted MCP connections pass it via the `X-Airbyte-Workspace-Id` header; local or stdio connections use the `AIRBYTE_CLOUD_WORKSPACE_ID` environment variable."
    },
    "organization_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Organization ID. Omit it when the credentials belong to exactly one organization, or when it is already configured via the `X-Airbyte-Organization-Id` header or the `AIRBYTE_CLOUD_ORGANIZATION_ID` environment variable. To discover organization IDs, call `list_cloud_organizations` to search organizations by name, or `list_agent_workspaces`, which reports the owning organization of each workspace. Workspace-scoped tools derive it from the workspace's parent organization when omitted."
    }
  },
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of listing connectors in an Airbyte Agents workspace.",
  "properties": {
    "connectors": {
      "items": {
        "description": "Information about a connector configured on the Airbyte Agents platform.",
        "properties": {
          "connector_id": {
            "type": "string"
          },
          "connector_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "connector_kind": {
            "default": "source",
            "enum": [
              "source",
              "destination"
            ],
            "type": "string"
          },
          "supported_actions": {
            "anyOf": [
              {
                "items": {
                  "type": "string"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "sql_dialect": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "note": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "connector_id"
        ],
        "type": "object"
      },
      "type": "array"
    },
    "message": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "required": [
    "connectors"
  ],
  "type": "object"
}

list_agent_skills

Hints: read-only · idempotent · open-world

Availability: experimental, insiders only (AIRBYTE_MCP_INSIDERS=1 for stdio, X-MCP-Insiders: 1 for hosted servers, or name the module in the include-modules setting; AIRBYTE_MCP_INSIDERS=0 disables it regardless).

List all skills available to an Airbyte Agents workspace.

Skills are reusable documentation the Agents API serves, for example connector usage
docs. All pages are fetched, so no pagination arguments are needed. Pass a listed
skill's `skill_id` to `read_agent_skill_docs` to read it.

The Airbyte Agents API authenticates with Airbyte Cloud credentials. When connecting to a hosted MCP server, provide a bearer token via the Authorization header, or client credentials via the transport Client-Id and Client-Secret headers. For local or stdio connections, set the AIRBYTE_CLOUD_BEARER_TOKEN environment variable, or both AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET. Call list_agent_connectors to discover connector IDs, then inspect_agent_connector to learn which entities a connector supports, before calling execute_agent_connector. Use list_agent_skills to discover skills, and pass the docs.skill_id reported by inspect_agent_connector to read_agent_skill_docs for connector usage docs.

Parameters

Name Type Required Default Description
workspace_id string | null no null Workspace ID. Hosted MCP connections pass it via the X-Airbyte-Workspace-Id header; local or stdio connections use the AIRBYTE_CLOUD_WORKSPACE_ID environment variable.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "workspace_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Workspace ID. Hosted MCP connections pass it via the `X-Airbyte-Workspace-Id` header; local or stdio connections use the `AIRBYTE_CLOUD_WORKSPACE_ID` environment variable."
    }
  },
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of listing or searching skills on the Airbyte Agents platform.",
  "properties": {
    "skills": {
      "items": {
        "description": "A skill discoverable on the Airbyte Agents platform.",
        "properties": {
          "skill_id": {
            "type": "string"
          },
          "kind": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "summary": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "tags": {
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "skill_id",
          "tags"
        ],
        "type": "object"
      },
      "type": "array"
    },
    "message": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "required": [
    "skills"
  ],
  "type": "object"
}

list_agent_workspaces

Hints: read-only · idempotent · open-world

Availability: experimental, insiders only (AIRBYTE_MCP_INSIDERS=1 for stdio, X-MCP-Insiders: 1 for hosted servers, or name the module in the include-modules setting; AIRBYTE_MCP_INSIDERS=0 disables it regardless).

List the workspaces reachable through the Airbyte Agents API.

An organization ID is required when the credentials belong to more than one organization.

The Airbyte Agents API authenticates with Airbyte Cloud credentials. When connecting to a hosted MCP server, provide a bearer token via the Authorization header, or client credentials via the transport Client-Id and Client-Secret headers. For local or stdio connections, set the AIRBYTE_CLOUD_BEARER_TOKEN environment variable, or both AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET. Call list_agent_connectors to discover connector IDs, then inspect_agent_connector to learn which entities a connector supports, before calling execute_agent_connector. Use list_agent_skills to discover skills, and pass the docs.skill_id reported by inspect_agent_connector to read_agent_skill_docs for connector usage docs.

Parameters

Name Type Required Default Description
organization_id string | null no null Organization ID. Required when the credentials belong to more than one organization; the Agents API rejects the call with HTTP 400 otherwise. Omit it when the credentials belong to exactly one organization or when it is already configured via the X-Airbyte-Organization-Id header or the AIRBYTE_CLOUD_ORGANIZATION_ID environment variable. Discover organization IDs with list_cloud_organizations.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "organization_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Organization ID. Required when the credentials belong to more than one organization; the Agents API rejects the call with HTTP 400 otherwise. Omit it when the credentials belong to exactly one organization or when it is already configured via the `X-Airbyte-Organization-Id` header or the `AIRBYTE_CLOUD_ORGANIZATION_ID` environment variable. Discover organization IDs with `list_cloud_organizations`."
    }
  },
  "type": "object"
}

Show output JSON schema

{
  "description": "Result of listing workspaces on the Airbyte Agents platform.",
  "properties": {
    "workspaces": {
      "items": {
        "description": "Information about a workspace on the Airbyte Agents platform.",
        "properties": {
          "workspace_id": {
            "type": "string"
          },
          "workspace_name": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "organization_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          }
        },
        "required": [
          "workspace_id"
        ],
        "type": "object"
      },
      "type": "array"
    },
    "message": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "required": [
    "workspaces"
  ],
  "type": "object"
}

read_agent_skill_docs

Hints: read-only · idempotent · open-world

Availability: experimental, insiders only (AIRBYTE_MCP_INSIDERS=1 for stdio, X-MCP-Insiders: 1 for hosted servers, or name the module in the include-modules setting; AIRBYTE_MCP_INSIDERS=0 disables it regardless).

Read a skill's docs in an Airbyte Agents workspace.

Without `section`, this returns the skill's metadata, guidance, and the outline of
sections, which is the cheapest way to orient before reading a specific section.

The Airbyte Agents API authenticates with Airbyte Cloud credentials. When connecting to a hosted MCP server, provide a bearer token via the Authorization header, or client credentials via the transport Client-Id and Client-Secret headers. For local or stdio connections, set the AIRBYTE_CLOUD_BEARER_TOKEN environment variable, or both AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET. Call list_agent_connectors to discover connector IDs, then inspect_agent_connector to learn which entities a connector supports, before calling execute_agent_connector. Use list_agent_skills to discover skills, and pass the docs.skill_id reported by inspect_agent_connector to read_agent_skill_docs for connector usage docs.

Parameters

Name Type Required Default Description
skill_id string yes — Skill ID, e.g. the docs.skill_id reported by inspect_agent_connector, or a skill_id from list_agent_skills. inspect_agent_connector returns only a docs summary (docs.content plus docs.guidance); call this tool with no section for the full section outline, or with section for one section's full detail. SQL passthrough destinations use connector-destination:<destination_id> and merge the server docs with built-in sql_select guidance.
section string | null no null Omit to get metadata, guidance, and the outline of available sections. Pass an exact section id from the outline to read that section.
workspace_id string | null no null Workspace ID. Hosted MCP connections pass it via the X-Airbyte-Workspace-Id header; local or stdio connections use the AIRBYTE_CLOUD_WORKSPACE_ID environment variable.

Show input JSON schema

{
  "additionalProperties": false,
  "properties": {
    "skill_id": {
      "description": "Skill ID, e.g. the `docs.skill_id` reported by `inspect_agent_connector`, or a `skill_id` from `list_agent_skills`. `inspect_agent_connector` returns only a docs summary (`docs.content` plus `docs.guidance`); call this tool with no `section` for the full section outline, or with `section` for one section's full detail. SQL passthrough destinations use `connector-destination:<destination_id>` and merge the server docs with built-in `sql_select` guidance.",
      "type": "string"
    },
    "section": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Omit to get metadata, guidance, and the outline of available sections. Pass an exact section `id` from the outline to read that section."
    },
    "workspace_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Workspace ID. Hosted MCP connections pass it via the `X-Airbyte-Workspace-Id` header; local or stdio connections use the `AIRBYTE_CLOUD_WORKSPACE_ID` environment variable."
    }
  },
  "required": [
    "skill_id"
  ],
  "type": "object"
}

Show output JSON schema

{
  "description": "Documentation for a single skill on the Airbyte Agents platform.",
  "properties": {
    "skill_id": {
      "type": "string"
    },
    "title": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "section_id": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "outline": {
      "items": {
        "description": "A section of a skill's docs, as listed in the docs outline.",
        "properties": {
          "section_id": {
            "type": "string"
          },
          "title": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "summary": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "default": null
          },
          "available": {
            "default": true,
            "type": "boolean"
          }
        },
        "required": [
          "section_id"
        ],
        "type": "object"
      },
      "type": "array"
    },
    "content": {
      "type": "string"
    },
    "guidance": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "warnings": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    },
    "errors": {
      "anyOf": [
        {
          "items": {
            "type": "string"
          },
          "type": "array"
        },
        {
          "type": "null"
        }
      ],
      "default": null
    }
  },
  "required": [
    "skill_id",
    "outline",
    "content"
  ],
  "type": "object"
}

   1# Copyright (c) 2026 Airbyte, Inc., all rights reserved.
   2"""Airbyte Agents MCP operations.
   3
   4> ## ⚠️ Experimental Tools — Insiders Only
   5>
   6> **The Airbyte Agents MCP tools are experimental and hidden by default.** They are advertised
   7> only when insiders mode is enabled (`AIRBYTE_MCP_INSIDERS` for stdio servers, `X-MCP-Insiders`
   8> for hosted servers) or when the include-modules setting explicitly names `agents`. Tool names,
   9> arguments, and result shapes may change or be removed without notice between minor versions of
  10> PyAirbyte. Pin an exact PyAirbyte version if you depend on them.
  11>
  12> These tools are also Cloud-only: they are hidden whenever
  13> `AIRBYTE_CLOUD_API_URL` / `AIRBYTE_CLOUD_CONFIG_API_URL` are overridden, unless
  14> `AIRBYTE_AGENTS_API_URL` is set.
  15
  16.. include:: ../../docs/mcp-generated/agents.md
  17"""
  18
  19# No public Python API — MCP primitives are registered via decorators and
  20# documented via the generated Markdown include above. Setting `__all__` to an
  21# empty list tells pdoc (and other doc tools) not to surface the individual
  22# tool / helper definitions as a redundant "API Documentation" list.
  23__all__: list[str] = []
  24
  25import json
  26from http import HTTPStatus
  27from typing import Annotated, Any, Literal
  28
  29import requests
  30from fastmcp import Context, FastMCP
  31from fastmcp_extensions import get_mcp_config, mcp_tool, register_mcp_tools
  32from pydantic import BaseModel, Field
  33
  34from airbyte._direct_connectors.connector_docs import (
  35    DESTINATION_SKILL_PREFIX,
  36    LOCAL_DESTINATION_SECTION_IDS,
  37    SQL_PASSTHROUGH_DESTINATION_DEFINITION_IDS,
  38    SQL_PASSTHROUGH_DESTINATION_DIALECTS,
  39    build_destination_connector_details,
  40    build_destination_skill_docs,
  41    connector_id_from_skill_id,
  42    destination_skill_id,
  43    merge_destination_skill_docs,
  44)
  45from airbyte._direct_connectors.docs_markdown import render_docs_content_markdown
  46from airbyte._direct_connectors.models import AgentSkillDocs, AgentSkillInfo
  47from airbyte.agents.connectors import AgentAction, AgentConnector, AgentReadAction
  48from airbyte.agents.organizations import AgentOrganization
  49from airbyte.agents.workspaces import AgentWorkspace
  50from airbyte.cloud.connectors import CloudDestination, CloudSource
  51from airbyte.constants import (
  52    CLOUD_BEARER_TOKEN_ENV_VAR,
  53    CLOUD_CLIENT_ID_ENV_VAR,
  54    CLOUD_CLIENT_SECRET_ENV_VAR,
  55    CLOUD_ORGANIZATION_ID_ENV_VAR,
  56    CLOUD_WORKSPACE_ID_ENV_VAR,
  57    MCP_BEARER_TOKEN_HEADER,
  58    MCP_CONFIG_API_URL,
  59    MCP_CONFIG_BEARER_TOKEN,
  60    MCP_CONFIG_CLIENT_ID,
  61    MCP_CONFIG_CLIENT_SECRET,
  62    MCP_CONFIG_CONFIG_API_URL,
  63    MCP_CONFIG_ORGANIZATION_ID,
  64    MCP_CONFIG_WORKSPACE_ID,
  65    MCP_ORGANIZATION_ID_HEADER,
  66    MCP_WORKSPACE_ID_HEADER,
  67)
  68from airbyte.exceptions import AirbyteError, PyAirbyteInputError
  69from airbyte.mcp._arg_resolvers import resolve_list_of_strings
  70from airbyte.mcp._tool_utils import AIRBYTE_CLOUD_WORKSPACE_ID_IS_SET
  71from airbyte.mcp.cloud import (
  72    _add_defaults_for_exclude_args,
  73    _get_cloud_client,
  74    _get_cloud_workspace,
  75)
  76
  77
  78AGENTS_AUTH_TIP_TEXT = (
  79    f"The Airbyte Agents API authenticates with Airbyte Cloud credentials. When connecting "
  80    f"to a hosted MCP server, provide a bearer token via the `{MCP_BEARER_TOKEN_HEADER}` "
  81    f"header, or client credentials via the transport `Client-Id` and `Client-Secret` "
  82    f"headers. For local or stdio connections, set the `{CLOUD_BEARER_TOKEN_ENV_VAR}` "
  83    f"environment variable, or both `{CLOUD_CLIENT_ID_ENV_VAR}` and "
  84    f"`{CLOUD_CLIENT_SECRET_ENV_VAR}`. Call `list_agent_connectors` to discover connector "
  85    f"IDs, then `inspect_agent_connector` to learn which entities a connector supports, "
  86    f"before calling `execute_agent_connector`. Use `list_agent_skills` "
  87    f"to discover skills, and pass the `docs.skill_id` reported by "
  88    f"`inspect_agent_connector` to `read_agent_skill_docs` for connector usage docs."
  89)
  90WORKSPACE_ID_TIP_TEXT = (
  91    f"Workspace ID. Hosted MCP connections pass it via the `{MCP_WORKSPACE_ID_HEADER}` "
  92    f"header; local or stdio connections use the `{CLOUD_WORKSPACE_ID_ENV_VAR}` "
  93    f"environment variable."
  94)
  95ORGANIZATION_ID_TIP_TEXT = (
  96    f"Organization ID. Omit it when the credentials belong to exactly one organization, or "
  97    f"when it is already configured via the "
  98    f"`{MCP_ORGANIZATION_ID_HEADER}` header or the `{CLOUD_ORGANIZATION_ID_ENV_VAR}` "
  99    f"environment variable. To discover organization IDs, call `list_cloud_organizations` "
 100    f"to search organizations by name, or `list_agent_workspaces`, which reports the owning "
 101    f"organization of each workspace. Workspace-scoped tools derive it from the workspace's "
 102    f"parent organization when omitted."
 103)
 104LIST_WORKSPACES_ORGANIZATION_ID_TIP_TEXT = (
 105    f"Organization ID. Required when the credentials belong to more than one organization; "
 106    f"the Agents API rejects the call with HTTP 400 otherwise. Omit it when the credentials "
 107    f"belong to exactly one organization or when it is already configured via the "
 108    f"`{MCP_ORGANIZATION_ID_HEADER}` header or the `{CLOUD_ORGANIZATION_ID_ENV_VAR}` "
 109    f"environment variable. Discover organization IDs with `list_cloud_organizations`."
 110)
 111
 112AGENTS_ACCESS_DENIED_STATUS = "access_denied"
 113"""The `status` reported when the Agents API refused the request."""
 114
 115AGENTS_EXECUTION_FAILED_STATUS = "error"
 116"""The `status` reported when the Agents API rejected an action request."""
 117
 118AGENTS_UNAUTHORIZED_MESSAGE = (
 119    "The Airbyte Agents API rejected these credentials. Verify the Airbyte Cloud "
 120    "credentials, or ask the user for valid ones."
 121)
 122AGENTS_FORBIDDEN_MESSAGE = (
 123    "The Airbyte Agents API authenticated these credentials but denied access. Either the "
 124    "organization does not have an Airbyte Agents subscription, or these credentials lack "
 125    "access to this workspace. Ask the user to confirm which applies rather than retrying."
 126)
 127"""Fallback explanation for a 403 whose response body carries no `message`/`detail`."""
 128
 129INSPECT_DOCS_GUIDANCE_TEMPLATE = (
 130    "`docs` is a summary of the connector's usage docs. Before calling "
 131    "`execute_agent_connector`, read the target action's section for its exact parameter names: "
 132    "`read_agent_skill_docs(skill_id={skill_id!r}, section=<section_id>)`, e.g. "
 133    "`section={example!r}`. Call `read_agent_skill_docs(skill_id={skill_id!r})` with no "
 134    "`section` for the full section outline."
 135)
 136INSPECT_DOCS_GUIDANCE_NO_SECTIONS_TEMPLATE = (
 137    "`docs` is a summary. No sections are currently available; "
 138    "`read_agent_skill_docs(skill_id={skill_id!r})` returns the same summary."
 139)
 140
 141AGENTS_ACTOR_NOT_ENABLED_DETAIL = "Actor is not enabled for Agents access."
 142"""The Agents API `detail` when a source or destination exists but is not toggled on."""
 143
 144AGENTS_ENABLE_ACTOR_GUIDANCE = (
 145    "The connector or destination exists in the Airbyte Cloud workspace but has not been "
 146    "enabled for Agents access. An organization admin must enable it in Airbyte Cloud under "
 147    "Settings -> Context layer, by selecting the workspace and turning on the toggle for that "
 148    "source or destination. Do not retry until the user confirms it has been enabled."
 149)
 150AGENTS_DESTINATION_ACCESS_NOTE = (
 151    'Query with `execute_agent_connector_ro` and `action="sql_select"` only; `inspect` returns '
 152    "built-in docs; `SHOW TABLES` lists tables and "
 153    '`SELECT * FROM <table> LIMIT 1` with `"dry_run": true` returns columns. If '
 154    "`sql_select` returns `access_denied`, an organization admin must enable the destination "
 155    "in Airbyte Cloud under Settings -> Context layer (workspace -> Destinations toggle)."
 156)
 157"""Attached to destinations in `list_agent_connectors`, whose enabled state is not exposed."""
 158
 159
 160CONNECTOR_NOT_FOUND_MESSAGE = "No connector found with the given ID or name."
 161"""The `AgentWorkspace.get_connector` message when the Agents API does not list the ID."""
 162
 163CONTEXT_LAYER_SETTINGS_URL_TEMPLATE = (
 164    "https://cloud.airbyte.com/organization/{organization_id}/settings/context-layer"
 165)
 166"""The Airbyte Cloud webapp page where an organization admin enables Agents access."""
 167
 168
 169def _context_layer_location(organization_id: str | None) -> str:
 170    """Name the Context layer settings page, with its URL when the organization is known."""
 171    location = "Organization settings -> Context layer"
 172    if organization_id:
 173        url = CONTEXT_LAYER_SETTINGS_URL_TEMPLATE.format(organization_id=organization_id)
 174        return f"{location} ({url})"
 175    return location
 176
 177
 178def context_layer_enable_guidance(organization_id: str | None) -> str:
 179    """How the human, not the agent, turns on Agents access for an organization or connector."""
 180    return (
 181        "Agents access cannot be enabled from this tool. Ask the user to have an Airbyte Cloud "
 182        "organization admin enable it in the Airbyte Cloud webapp under "
 183        f"{_context_layer_location(organization_id)} (or from the connector's own settings "
 184        "page), then retry."
 185    )
 186
 187
 188def agents_no_connectors_enabled_message(organization_id: str | None) -> str:
 189    """For a workspace whose Cloud sources exist but none are enabled for Agents access."""
 190    return (
 191        "No connectors in this workspace are enabled for Agents access, so there is nothing to "
 192        f"list, inspect, or execute. {context_layer_enable_guidance(organization_id)}"
 193    )
 194
 195
 196def agents_workspace_has_no_sources_message(organization_id: str | None) -> str:
 197    """For a workspace with no Cloud sources at all; enablement guidance alone cannot help."""
 198    return (
 199        "This Airbyte Cloud workspace has no source connectors, so there is nothing to enable "
 200        "for Agents access. Ask the user to create a source in the Airbyte Cloud webapp first, "
 201        "then have an organization admin enable it for Agents access under "
 202        f"{_context_layer_location(organization_id)}."
 203    )
 204
 205
 206class _ConnectorNotEnabledError(AirbyteError):
 207    """A connector exists in the Cloud workspace but is not enabled for Agents access."""
 208
 209
 210class AgentWorkspaceResult(BaseModel):
 211    """Information about a workspace on the Airbyte Agents platform."""
 212
 213    workspace_id: str
 214    """The workspace ID."""
 215
 216    workspace_name: str | None = None
 217    """Display name of the workspace."""
 218
 219    organization_id: str | None = None
 220    """The organization that owns the workspace, when reported."""
 221
 222
 223class AgentWorkspaceListResult(BaseModel):
 224    """Result of listing workspaces on the Airbyte Agents platform."""
 225
 226    workspaces: list[AgentWorkspaceResult]
 227    """Workspaces reachable through the Agents API with these credentials."""
 228
 229    message: str | None = None
 230    """Why the listing is empty, when the Agents API denied the request."""
 231
 232
 233class AgentConnectorResult(BaseModel):
 234    """Information about a connector configured on the Airbyte Agents platform."""
 235
 236    connector_id: str
 237    """The connector ID, used as `connector_id` in the other Agents tools."""
 238
 239    connector_name: str | None = None
 240    """Display name of the connector."""
 241
 242    connector_kind: Literal["source", "destination"] = "source"
 243    """`source` for Agents API connectors; `destination` for SQL passthrough destinations."""
 244
 245    supported_actions: list[str] | None = None
 246    """Actions the connector supports, when limited. Destinations support only `sql_select`."""
 247
 248    sql_dialect: str | None = None
 249    """The `sql_dialect` to pass in `api_args` for `sql_select`. Destinations only."""
 250
 251    note: str | None = None
 252    """How to use this connector, when it differs from the standard Agents flow."""
 253
 254
 255class AgentConnectorListResult(BaseModel):
 256    """Result of listing connectors in an Airbyte Agents workspace."""
 257
 258    connectors: list[AgentConnectorResult]
 259    """Connectors configured in the workspace."""
 260
 261    message: str | None = None
 262    """Why the listing is empty, when the Agents API denied the request."""
 263
 264
 265class AgentSkillResult(BaseModel):
 266    """A skill discoverable on the Airbyte Agents platform."""
 267
 268    skill_id: str
 269    """The skill ID, used as `skill_id` in `read_agent_skill_docs`."""
 270
 271    kind: str | None = None
 272    """The skill category, for example `static` or `connector_source`."""
 273
 274    title: str | None = None
 275    """The human-readable skill title."""
 276
 277    summary: str | None = None
 278    """A short summary of what the skill documents."""
 279
 280    tags: list[str]
 281    """Search and categorization tags for the skill."""
 282
 283
 284class AgentSkillListResult(BaseModel):
 285    """Result of listing or searching skills on the Airbyte Agents platform."""
 286
 287    skills: list[AgentSkillResult]
 288    """Skills matching the listing or search, across all pages."""
 289
 290    message: str | None = None
 291    """Why the listing is empty, when the Agents API denied the request."""
 292
 293
 294class AgentSkillSectionResult(BaseModel):
 295    """A section of a skill's docs, as listed in the docs outline."""
 296
 297    section_id: str
 298    """The section ID, passed as `section` to `read_agent_skill_docs`."""
 299
 300    title: str | None = None
 301    """The human-readable section title."""
 302
 303    summary: str | None = None
 304    """A short summary of the section content."""
 305
 306    available: bool = True
 307    """Whether this section can currently be read."""
 308
 309
 310class AgentSkillDocsResult(BaseModel):
 311    """Documentation for a single skill on the Airbyte Agents platform."""
 312
 313    skill_id: str
 314    """The skill ID that was read."""
 315
 316    title: str | None = None
 317    """The human-readable skill title."""
 318
 319    section_id: str | None = None
 320    """The section that was read, or `None` for the default docs response."""
 321
 322    outline: list[AgentSkillSectionResult]
 323    """The sections available for this skill."""
 324
 325    content: str
 326    """The docs content rendered as a single Markdown document."""
 327
 328    guidance: str | None = None
 329    """How to read more of this skill's docs with `read_agent_skill_docs`, when applicable."""
 330
 331    warnings: list[str] | None = None
 332    """Non-fatal issues reported while building or reading the docs."""
 333
 334    errors: list[str] | None = None
 335    """Why the result is empty, when the request failed (for example access denied
 336    or connector not found)."""
 337
 338
 339class AgentConnectorDocsResult(BaseModel):
 340    """Docs summary embedded in `inspect_agent_connector` results.
 341
 342    The section outline is intentionally omitted here; `read_agent_skill_docs`
 343    returns it as `outline`.
 344    """
 345
 346    skill_id: str
 347    """The skill ID of the connector's usage docs."""
 348
 349    title: str | None = None
 350    """The human-readable docs title."""
 351
 352    content: str
 353    """The docs content rendered as a single Markdown document."""
 354
 355    guidance: str | None = None
 356    """How to read more of this connector's docs with `read_agent_skill_docs`."""
 357
 358    warnings: list[str] | None = None
 359    """Non-fatal issues reported while building or reading the docs."""
 360
 361    errors: list[str] | None = None
 362    """Why the result is empty, when the request failed (for example access denied
 363    or connector not found)."""
 364
 365
 366class AgentConnectorDetailsResult(BaseModel):
 367    """Details about a single Airbyte Agents connector."""
 368
 369    connector_id: str
 370    """The connector ID."""
 371
 372    connector_name: str | None = None
 373    """Display name of the connector."""
 374
 375    workspace_id: str | None = None
 376    """The workspace that owns the connector."""
 377
 378    integration_name: str | None = None
 379    """Name of the underlying integration, for example `GitHub` or `Snowflake`."""
 380
 381    docs: AgentConnectorDocsResult | None = None
 382    """Summary of the connector's usage docs, when available.
 383
 384    Pass `docs.skill_id` to `read_agent_skill_docs` for the section outline and a
 385    section's full detail.
 386    """
 387
 388    warnings: list[str] | None = None
 389    """Warnings the Agents API reported about this connector."""
 390
 391    errors: list[str] | None = None
 392    """Why the result is empty, when the request failed (for example access denied
 393    or connector not found)."""
 394
 395
 396class AgentExecuteToolResult(BaseModel):
 397    """Result of executing a single action against an Airbyte Agents connector."""
 398
 399    status: str
 400    """The execution status reported by the Agents API, for example `success`."""
 401
 402    result: Any = None
 403    """The action's payload. Entity-returning actions put a list of entities here."""
 404
 405    has_next_page: bool = False
 406    """Whether the connector reported more entities after this page."""
 407
 408    end_cursor: str | None = None
 409    """The cursor for the next page, when one is available. Pass it as `cursor` for Context Store
 410    `search`, or as the connector's own cursor argument in `api_args` for direct connector
 411    actions."""
 412
 413    execution_time_ms: int | None = None
 414    """How long the connector took to execute the action, when reported."""
 415
 416    warning: dict[str, Any] | None = None
 417    """A warning reported alongside an otherwise successful result."""
 418
 419    message: str | None = None
 420    """Why the action did not run, when the Agents API denied or rejected the request."""
 421
 422
 423def _resolve_api_args(api_args: dict[str, Any] | str | None) -> dict[str, Any] | None:
 424    """Resolve `api_args` from a dictionary or a JSON object string."""
 425    if api_args is None or isinstance(api_args, dict):
 426        return api_args
 427
 428    try:
 429        parsed: Any = json.loads(api_args)
 430    except json.JSONDecodeError as ex:
 431        raise PyAirbyteInputError(
 432            message="The `api_args` string is not valid JSON.",
 433            guidance="Pass `api_args` as an object, or as a JSON object string.",
 434        ) from ex
 435
 436    if not isinstance(parsed, dict):
 437        raise PyAirbyteInputError(
 438            message="The `api_args` string is not a JSON object.",
 439            guidance="Pass `api_args` as an object, or as a JSON object string.",
 440            context={"parsed_type": type(parsed).__name__},
 441        )
 442    return parsed
 443
 444
 445def _agents_access_message(error: AirbyteError) -> str | None:
 446    """Return a concise explanation of an Agents API authorization failure.
 447
 448    Returns `None` when the failure is not an authorization failure, so the caller can
 449    re-raise it with a bare `raise` and keep the original traceback.
 450    """
 451    if isinstance(error, _ConnectorNotEnabledError):
 452        return error.get_message()
 453    context = error.context or {}
 454    status_code = context.get("status_code")
 455    if status_code == HTTPStatus.UNAUTHORIZED:
 456        return AGENTS_UNAUTHORIZED_MESSAGE
 457    if status_code == HTTPStatus.FORBIDDEN:
 458        detail = _agents_error_detail(context.get("response_text"))
 459        if detail is None:
 460            return AGENTS_FORBIDDEN_MESSAGE
 461        if detail == AGENTS_ACTOR_NOT_ENABLED_DETAIL:
 462            return f"{detail} {AGENTS_ENABLE_ACTOR_GUIDANCE}"
 463        return f"The Airbyte Agents API denied access: {detail}"
 464    return None
 465
 466
 467def _agents_error_detail(response_text: object) -> str | None:
 468    """Extract the human-readable reason from an Agents API error body.
 469
 470    The API wraps `HTTPException.detail` as `{"message": ..., "errors": [{"message": ...}]}`;
 471    plain FastAPI bodies use `{"detail": ...}`. Returns `None` for anything else, including
 472    non-JSON bodies, so the caller falls back to the generic explanation.
 473    """
 474    if not isinstance(response_text, str) or not response_text.strip():
 475        return None
 476    try:
 477        body = json.loads(response_text)
 478    except json.JSONDecodeError:
 479        return None
 480    if not isinstance(body, dict):
 481        return None
 482    for key in ("message", "detail"):
 483        value = body.get(key)
 484        if isinstance(value, str) and value.strip():
 485            return value.strip()
 486    errors = body.get("errors")
 487    if isinstance(errors, list) and errors and isinstance(errors[0], dict):
 488        value = errors[0].get("message")
 489        if isinstance(value, str) and value.strip():
 490            return value.strip()
 491    return None
 492
 493
 494def _sql_error_guidance(detail: str) -> str | None:
 495    """Return actionable guidance for a known SQL execution error."""
 496    normalized_detail = detail.casefold()
 497    if "000904" in normalized_detail or "invalid identifier" in normalized_detail:
 498        return (
 499            "Snowflake identifiers are upper-cased by Airbyte and double-quoted identifiers "
 500            "are case-sensitive; write column names unquoted (or upper-cased), or run "
 501            '`SELECT * FROM <table> LIMIT 1` with `"dry_run": true` to list the real columns.'
 502        )
 503    if "002003" in normalized_detail or "does not exist or not authorized" in normalized_detail:
 504        return (
 505            "Run `SHOW TABLES` to list the tables this destination exposes, and qualify tables "
 506            "in another schema as `<database>.<schema>.<table>`."
 507        )
 508    if "unrecognized name" in normalized_detail:
 509        return (
 510            'Run `SELECT * FROM <table> LIMIT 1` with `"dry_run": true` to list the real '
 511            "columns, then use one of the listed column names in the query."
 512        )
 513    if "not found: table" in normalized_detail or "not found: dataset" in normalized_detail:
 514        return (
 515            "Run `SHOW TABLES` to list the tables this destination exposes, and qualify tables "
 516            "in another dataset as `<project>.<dataset>.<table>` (backticked)."
 517        )
 518    return None
 519
 520
 521def _agents_execution_failure_message(
 522    error: AirbyteError,
 523    action: AgentAction,
 524) -> str | None:
 525    """Return a concise explanation for a rejected Agents API action request."""
 526    context = error.context or {}
 527    if context.get("status_code") not in {
 528        HTTPStatus.BAD_REQUEST,
 529        HTTPStatus.UNPROCESSABLE_ENTITY,
 530    }:
 531        return None
 532    detail = _agents_error_detail(context.get("response_text"))
 533    if detail is None:
 534        return None
 535    guidance = _sql_error_guidance(detail) if action == AgentReadAction.SQL_SELECT else None
 536    return f"{detail} {guidance}" if guidance is not None else detail
 537
 538
 539def _is_not_found(error: AirbyteError) -> bool:
 540    """Return whether the Agents API reported the connector or skill as not found."""
 541    return (error.context or {}).get("status_code") == HTTPStatus.NOT_FOUND
 542
 543
 544def _resolve_cloud_destination(
 545    ctx: Context,
 546    connector_id: str,
 547    workspace_id: str | None = None,
 548    organization_id: str | None = None,
 549) -> CloudDestination | None:
 550    """Look a connector ID up in the Cloud workspace's destinations.
 551
 552    Destinations are targets of `sql_select` and are not listed by the Agents API. Returns
 553    `None` when the ID does not match any destination in the workspace; `list_destinations`
 554    is used so a bogus ID does not raise on lazy fetch.
 555    """
 556    workspace = _get_agent_workspace(ctx, workspace_id, organization_id)
 557    for destination in _get_cloud_workspace(ctx, workspace.workspace_id).list_destinations():
 558        if destination.connector_id == connector_id:
 559            return destination
 560    return None
 561
 562
 563def _resolve_cloud_source(
 564    ctx: Context,
 565    connector_id: str,
 566    workspace_id: str,
 567) -> CloudSource | None:
 568    """Look a connector ID up in the Cloud workspace's sources.
 569
 570    Returns `None` when the ID does not match any source in the workspace.
 571    """
 572    for source in _get_cloud_workspace(ctx, workspace_id).list_sources():
 573        if source.connector_id == connector_id:
 574            return source
 575    return None
 576
 577
 578def _source_not_enabled_message(
 579    source: CloudSource,
 580    workspace_id: str,
 581    organization_id: str | None,
 582) -> str:
 583    """Explain that a Cloud source exists but is not enabled for Agents access."""
 584    return (
 585        f"Source '{source.name}' ({source.connector_id}) exists in Airbyte Cloud "
 586        f"workspace {workspace_id} but is not enabled for Agents access. "
 587        f"{context_layer_enable_guidance(organization_id)}"
 588    )
 589
 590
 591def _connector_unavailable_error(
 592    ctx: Context,
 593    connector_id: str,
 594    workspace_id: str,
 595    organization_id: str | None,
 596) -> AirbyteError:
 597    """Build the error for a connector ID the Agents API does not list.
 598
 599    A `_ConnectorNotEnabledError` when the ID is a source in the Cloud workspace, so the
 600    caller can report the disabled state instead of a bare miss; otherwise a not-found
 601    error pointing at `list_agent_connectors`.
 602    """
 603    source = _resolve_cloud_source(ctx, connector_id, workspace_id)
 604    if source is not None:
 605        return _ConnectorNotEnabledError(
 606            message=_source_not_enabled_message(source, workspace_id, organization_id),
 607            context={"connector_id": connector_id, "workspace_id": workspace_id},
 608        )
 609    return AirbyteError(
 610        message=CONNECTOR_NOT_FOUND_MESSAGE,
 611        guidance=(
 612            "Use `list_agent_connectors` to see the connectors enabled for Agents access in "
 613            "this workspace."
 614        ),
 615        context={"connector_id": connector_id, "workspace_id": workspace_id},
 616    )
 617
 618
 619def _or_none(items: list[str]) -> list[str] | None:
 620    """Return `None` for empty lists so optional list fields stay unset."""
 621    return items or None
 622
 623
 624def _skill_docs_result(docs: AgentSkillDocs) -> AgentSkillDocsResult:
 625    """Shape an `AgentSkillDocs` into an `AgentSkillDocsResult`."""
 626    return AgentSkillDocsResult(
 627        skill_id=docs.metadata.id,
 628        title=docs.metadata.title,
 629        section_id=docs.section_id,
 630        outline=[
 631            AgentSkillSectionResult(
 632                section_id=docs_section.id,
 633                title=docs_section.title,
 634                summary=docs_section.summary,
 635                available=docs_section.available,
 636            )
 637            for docs_section in docs.outline
 638        ],
 639        content=render_docs_content_markdown(docs.content),
 640        warnings=_or_none([str(warning) for warning in docs.metadata.warnings]),
 641    )
 642
 643
 644def _inspect_docs_guidance(skill_id: str, outline: list[AgentSkillSectionResult]) -> str:
 645    """Build the `guidance` hint for a docs summary embedded in an inspect result."""
 646    example_section = next((section for section in outline if section.available), None)
 647    if example_section is None:
 648        return INSPECT_DOCS_GUIDANCE_NO_SECTIONS_TEMPLATE.format(skill_id=skill_id)
 649    return INSPECT_DOCS_GUIDANCE_TEMPLATE.format(
 650        skill_id=skill_id, example=example_section.section_id
 651    )
 652
 653
 654def _connector_docs_result(docs: AgentSkillDocsResult) -> AgentConnectorDocsResult:
 655    """Shape a skill docs result into the docs summary embedded in inspect results."""
 656    return AgentConnectorDocsResult(
 657        skill_id=docs.skill_id,
 658        title=docs.title,
 659        content=docs.content,
 660        guidance=_inspect_docs_guidance(docs.skill_id, docs.outline),
 661        warnings=docs.warnings,
 662        errors=docs.errors,
 663    )
 664
 665
 666def _inspect_destination_fallback(
 667    ctx: Context,
 668    connector_id: str,
 669    workspace_id: str | None = None,
 670    organization_id: str | None = None,
 671) -> AgentConnectorDetailsResult:
 672    """Build an inspect result for a connector ID the Agents API returned 404 for.
 673
 674    SQL passthrough destinations get built-in details; anything else gets an `errors`
 675    entry instead of raising.
 676    """
 677    agent_workspace = _get_agent_workspace(ctx, workspace_id, organization_id)
 678    resolved_workspace_id = agent_workspace.workspace_id
 679    destination = _resolve_cloud_destination(ctx, connector_id, workspace_id, organization_id)
 680    if (
 681        destination is not None
 682        and destination.definition_id in SQL_PASSTHROUGH_DESTINATION_DEFINITION_IDS
 683    ):
 684        details = build_destination_connector_details(destination)
 685        warnings: list[str] = []
 686        try:
 687            skill_docs = _skill_docs_result(
 688                _read_destination_skill_docs(agent_workspace, destination, None)
 689            )
 690        except (AirbyteError, requests.RequestException) as error:
 691            detail = error.get_message() if isinstance(error, AirbyteError) else str(error)
 692            docs_unavailable = f"Connector docs are unavailable: {detail}"
 693            warnings.append(docs_unavailable)
 694            docs_result = AgentConnectorDocsResult(
 695                skill_id=destination_skill_id(destination.connector_id),
 696                content="",
 697                warnings=[docs_unavailable],
 698            )
 699        else:
 700            docs_result = _connector_docs_result(skill_docs)
 701        return AgentConnectorDetailsResult(
 702            connector_id=details.connector_id,
 703            connector_name=details.name,
 704            workspace_id=details.workspace_id,
 705            integration_name=details.integration_name,
 706            docs=docs_result,
 707            warnings=_or_none(warnings),
 708        )
 709    if destination is not None:
 710        message = (
 711            f"Destination '{destination.name}' (definition {destination.definition_id}) is not "
 712            "a SQL passthrough destination; only Snowflake and BigQuery destinations support "
 713            "`sql_select`."
 714        )
 715    elif (source := _resolve_cloud_source(ctx, connector_id, resolved_workspace_id)) is not None:
 716        message = _source_not_enabled_message(
 717            source, resolved_workspace_id, agent_workspace.organization_id
 718        )
 719    else:
 720        message = (
 721            f"Connector {connector_id} was not found in the Agents API and is not a source or "
 722            f"destination in workspace {resolved_workspace_id}. Use `list_agent_connectors` to "
 723            "see the connectors enabled for Agents access, or "
 724            f"`read_agent_skill_docs(skill_id='connector-source:{connector_id}')`."
 725        )
 726    return AgentConnectorDetailsResult(
 727        connector_id=connector_id,
 728        errors=[message],
 729    )
 730
 731
 732def _read_destination_skill_docs(
 733    workspace: AgentWorkspace,
 734    destination: CloudDestination,
 735    section: str | None,
 736) -> AgentSkillDocs:
 737    """Read a SQL passthrough destination's docs, merging server docs with local guidance.
 738
 739    Local-only sections are built without calling the Agents API; otherwise server
 740    docs are augmented with local guidance, and a 404 falls back to local docs.
 741    """
 742    if section in LOCAL_DESTINATION_SECTION_IDS:
 743        return build_destination_skill_docs(destination, section=section)
 744    skill_id = destination_skill_id(destination.connector_id)
 745    try:
 746        server_docs = workspace.read_skill_docs(skill_id, section=section)
 747    except AirbyteError as error:
 748        if _is_not_found(error):
 749            return build_destination_skill_docs(destination, section=section)
 750        raise
 751    return merge_destination_skill_docs(server_docs, destination)
 752
 753
 754def _destination_skill_docs_fallback(
 755    ctx: Context,
 756    skill_id: str,
 757    section: str | None,
 758    workspace_id: str | None = None,
 759    *,
 760    destination: CloudDestination | None = None,
 761    destination_resolved: bool = False,
 762) -> AgentSkillDocsResult:
 763    """Build a skill docs result for a skill ID the Agents API returned 404 for."""
 764    connector_id = connector_id_from_skill_id(skill_id)
 765    resolved_workspace_id = _get_agent_workspace(ctx, workspace_id).workspace_id
 766    if not destination_resolved:
 767        destination = _resolve_cloud_destination(ctx, connector_id, workspace_id)
 768    if (
 769        destination is not None
 770        and destination.definition_id in SQL_PASSTHROUGH_DESTINATION_DEFINITION_IDS
 771    ):
 772        docs = build_destination_skill_docs(destination, section=section)
 773        return _skill_docs_result(docs)
 774    if destination is not None:
 775        message = (
 776            f"Destination '{destination.name}' (definition {destination.definition_id}) is not "
 777            "a SQL passthrough destination; only Snowflake and BigQuery destinations support "
 778            "`sql_select`."
 779        )
 780    else:
 781        message = (
 782            f"Skill {skill_id} was not found in the Agents API and connector {connector_id} "
 783            f"is not a destination in workspace {resolved_workspace_id}."
 784        )
 785    return AgentSkillDocsResult(
 786        skill_id=skill_id,
 787        section_id=section,
 788        outline=[],
 789        content="",
 790        errors=[message],
 791    )
 792
 793
 794def _get_agent_organization(ctx: Context, organization_id: str | None) -> AgentOrganization:
 795    """Build an `AgentOrganization` from MCP config."""
 796    return AgentOrganization(
 797        organization_id=organization_id or get_mcp_config(ctx, MCP_CONFIG_ORGANIZATION_ID),
 798        client_id=get_mcp_config(ctx, MCP_CONFIG_CLIENT_ID),
 799        client_secret=get_mcp_config(ctx, MCP_CONFIG_CLIENT_SECRET),
 800        bearer_token=get_mcp_config(ctx, MCP_CONFIG_BEARER_TOKEN),
 801        public_api_root=get_mcp_config(ctx, MCP_CONFIG_API_URL),
 802        config_api_root=get_mcp_config(ctx, MCP_CONFIG_CONFIG_API_URL),
 803    )
 804
 805
 806def _get_agent_workspace(
 807    ctx: Context,
 808    workspace_id: str | None,
 809    organization_id: str | None = None,
 810) -> AgentWorkspace:
 811    """Build an `AgentWorkspace`, deriving an absent organization from its workspace."""
 812    resolved_workspace_id = workspace_id or get_mcp_config(ctx, MCP_CONFIG_WORKSPACE_ID)
 813    resolved_organization_id = organization_id or get_mcp_config(ctx, MCP_CONFIG_ORGANIZATION_ID)
 814    if not resolved_workspace_id or not resolved_organization_id:
 815        client = _get_cloud_client(ctx)
 816        if not resolved_workspace_id:
 817            resolved_workspace_id = client.resolve_default_workspace_id()
 818        if resolved_workspace_id and not resolved_organization_id:
 819            resolved_organization_id = client.get_workspace_parent_organization_id(
 820                resolved_workspace_id
 821            )
 822    return AgentWorkspace(
 823        workspace_id=resolved_workspace_id,
 824        organization_id=resolved_organization_id,
 825        client_id=get_mcp_config(ctx, MCP_CONFIG_CLIENT_ID),
 826        client_secret=get_mcp_config(ctx, MCP_CONFIG_CLIENT_SECRET),
 827        bearer_token=get_mcp_config(ctx, MCP_CONFIG_BEARER_TOKEN),
 828        public_api_root=get_mcp_config(ctx, MCP_CONFIG_API_URL),
 829        config_api_root=get_mcp_config(ctx, MCP_CONFIG_CONFIG_API_URL),
 830    )
 831
 832
 833def _get_agent_connector(
 834    ctx: Context,
 835    connector_id: str,
 836    workspace_id: str | None = None,
 837    organization_id: str | None = None,
 838) -> AgentConnector:
 839    """Get an `AgentConnector` from its workspace, using MCP config.
 840
 841    The Agents API addresses a connector by ID alone, but the connector is fetched through
 842    its workspace anyway, so a connector ID belonging to another workspace raises before
 843    any action runs.
 844
 845    Destination connectors (targets of `sql_select`) are not listed by the Agents API, so IDs it
 846    does not know are verified against the Cloud workspace's destinations instead. An ID that
 847    is a Cloud source the Agents API does not list is reported as not enabled for Agents
 848    access, rather than as missing.
 849    """
 850    workspace = _get_agent_workspace(ctx, workspace_id, organization_id)
 851    try:
 852        return workspace.get_connector(connector_id)
 853    except AirbyteError as error:
 854        if error.get_message() != CONNECTOR_NOT_FOUND_MESSAGE:
 855            raise
 856        cloud_destination_ids = {
 857            destination.connector_id
 858            for destination in _get_cloud_workspace(ctx, workspace.workspace_id).list_destinations()
 859        }
 860        if connector_id not in cloud_destination_ids:
 861            raise _connector_unavailable_error(
 862                ctx, connector_id, workspace.workspace_id, workspace.organization_id
 863            ) from error
 864        return workspace.get_connector(connector_id=connector_id)
 865
 866
 867def _execute(  # noqa: PLR0913  # Mirrors the tool signatures it serves.
 868    ctx: Context,
 869    *,
 870    connector_id: str,
 871    workspace_id: str | None,
 872    organization_id: str | None,
 873    entity_type: str,
 874    action: AgentAction,
 875    api_args: dict[str, Any] | str | None,
 876    select_fields: list[str] | str | None,
 877    exclude_fields: list[str] | str | None,
 878    page_size: int | None,
 879    cursor: str | None,
 880    intent: str | None,
 881    read_only: bool | None = None,
 882) -> AgentExecuteToolResult:
 883    """Execute one connector action and shape it into an `AgentExecuteToolResult`.
 884
 885    When `read_only` is `True`, write actions are rejected before any request is sent.
 886    """
 887    if read_only and action not in set(AgentReadAction):
 888        raise PyAirbyteInputError(
 889            message="This action writes data and cannot run in read-only mode.",
 890            guidance=(
 891                "Read-only actions are: "
 892                f"{', '.join(member.value for member in AgentReadAction)}."
 893            ),
 894            context={"action": action},
 895        )
 896
 897    try:
 898        result = _get_agent_connector(
 899            ctx=ctx,
 900            connector_id=connector_id,
 901            workspace_id=workspace_id,
 902            organization_id=organization_id,
 903        ).execute(
 904            entity_type=entity_type,
 905            action=action,
 906            api_args=_resolve_api_args(api_args),
 907            select_fields=resolve_list_of_strings(select_fields),
 908            exclude_fields=resolve_list_of_strings(exclude_fields),
 909            page_size=page_size,
 910            cursor=cursor,
 911            workspace_id=workspace_id,
 912            intent=intent,
 913        )
 914    except AirbyteError as error:
 915        message = _agents_access_message(error)
 916        if message is not None:
 917            return AgentExecuteToolResult(
 918                status=AGENTS_ACCESS_DENIED_STATUS,
 919                message=message,
 920            )
 921        message = _agents_execution_failure_message(error, action)
 922        if message is not None:
 923            return AgentExecuteToolResult(
 924                status=AGENTS_EXECUTION_FAILED_STATUS,
 925                message=message,
 926            )
 927        raise
 928
 929    return AgentExecuteToolResult(
 930        status=result.status,
 931        result=result.result,
 932        has_next_page=result.has_next_page,
 933        end_cursor=result.end_cursor,
 934        execution_time_ms=result.execution_metadata.execution_time_ms,
 935        warning=result.warning,
 936    )
 937
 938
 939@mcp_tool(
 940    read_only=True,
 941    idempotent=True,
 942    open_world=True,
 943    extra_help_text=AGENTS_AUTH_TIP_TEXT,
 944)
 945def list_agent_workspaces(
 946    ctx: Context,
 947    *,
 948    organization_id: Annotated[
 949        str | None,
 950        Field(
 951            description=LIST_WORKSPACES_ORGANIZATION_ID_TIP_TEXT,
 952            default=None,
 953        ),
 954    ],
 955) -> AgentWorkspaceListResult:
 956    """List the workspaces reachable through the Airbyte Agents API.
 957
 958    An organization ID is required when the credentials belong to more than one organization.
 959    """
 960    organization = _get_agent_organization(ctx, organization_id)
 961    try:
 962        workspaces = organization.list_workspaces()
 963    except AirbyteError as error:
 964        message = _agents_access_message(error)
 965        if message is None:
 966            raise
 967        return AgentWorkspaceListResult(workspaces=[], message=message)
 968
 969    return AgentWorkspaceListResult(
 970        workspaces=[
 971            AgentWorkspaceResult(
 972                workspace_id=workspace.workspace_id,
 973                workspace_name=workspace.name,
 974                organization_id=workspace.organization_id,
 975            )
 976            for workspace in workspaces
 977        ]
 978    )
 979
 980
 981@mcp_tool(
 982    read_only=True,
 983    idempotent=True,
 984    open_world=True,
 985    extra_help_text=AGENTS_AUTH_TIP_TEXT,
 986)
 987def list_agent_connectors(
 988    ctx: Context,
 989    *,
 990    workspace_id: Annotated[
 991        str | None,
 992        Field(
 993            description=WORKSPACE_ID_TIP_TEXT,
 994            default=None,
 995        ),
 996    ],
 997    organization_id: Annotated[
 998        str | None,
 999        Field(
1000            description=ORGANIZATION_ID_TIP_TEXT,
1001            default=None,
1002        ),
1003    ],
1004) -> AgentConnectorListResult:
1005    """List the connectors configured in an Airbyte Agents workspace.
1006
1007    Sources come from the Agents API. SQL passthrough destinations (Snowflake, BigQuery) in
1008    the Cloud workspace are appended with `connector_kind="destination"`; they support only
1009    the `sql_select` action of `execute_agent_connector_ro`.
1010    """
1011    workspace = _get_agent_workspace(ctx, workspace_id, organization_id)
1012    try:
1013        connectors = workspace.list_connectors()
1014    except AirbyteError as error:
1015        message = _agents_access_message(error)
1016        if message is None:
1017            raise
1018        return AgentConnectorListResult(connectors=[], message=message)
1019
1020    results = [
1021        AgentConnectorResult(
1022            connector_id=connector.connector_id,
1023            connector_name=connector.name,
1024        )
1025        for connector in connectors
1026    ]
1027    results.extend(
1028        _destination_connector_result(destination)
1029        for destination in _list_sql_passthrough_destinations(ctx, workspace.workspace_id)
1030    )
1031    if not results:
1032        return AgentConnectorListResult(
1033            connectors=[],
1034            message=_empty_connector_list_message(
1035                ctx, workspace.workspace_id, workspace.organization_id
1036            ),
1037        )
1038    return AgentConnectorListResult(connectors=results)
1039
1040
1041def _empty_connector_list_message(
1042    ctx: Context,
1043    workspace_id: str,
1044    organization_id: str | None,
1045) -> str:
1046    """Explain an empty Agents connector list.
1047
1048    Enablement guidance only helps when the Cloud workspace actually has sources; an empty
1049    Cloud workspace needs a source created first.
1050    """
1051    if _get_cloud_workspace(ctx, workspace_id).list_sources():
1052        return agents_no_connectors_enabled_message(organization_id)
1053    return agents_workspace_has_no_sources_message(organization_id)
1054
1055
1056def _list_sql_passthrough_destinations(
1057    ctx: Context,
1058    workspace_id: str,
1059) -> list[CloudDestination]:
1060    """Return the Cloud workspace's destinations that `sql_select` can query."""
1061    return [
1062        destination
1063        for destination in _get_cloud_workspace(ctx, workspace_id).list_destinations()
1064        if destination.definition_id in SQL_PASSTHROUGH_DESTINATION_DEFINITION_IDS
1065    ]
1066
1067
1068def _destination_connector_result(destination: CloudDestination) -> AgentConnectorResult:
1069    """Describe a SQL passthrough destination as a `sql_select`-only connector."""
1070    return AgentConnectorResult(
1071        connector_id=destination.connector_id,
1072        connector_name=destination.name,
1073        connector_kind="destination",
1074        supported_actions=["sql_select"],
1075        sql_dialect=SQL_PASSTHROUGH_DESTINATION_DIALECTS[destination.definition_id],
1076        note=AGENTS_DESTINATION_ACCESS_NOTE,
1077    )
1078
1079
1080@mcp_tool(
1081    read_only=True,
1082    idempotent=True,
1083    open_world=True,
1084    extra_help_text=AGENTS_AUTH_TIP_TEXT,
1085)
1086def inspect_agent_connector(
1087    ctx: Context,
1088    connector_id: Annotated[
1089        str,
1090        Field(
1091            description=(
1092                "The ID of a supported Airbyte source or destination with agent features enabled."
1093            ),
1094        ),
1095    ],
1096    *,
1097    workspace_id: Annotated[
1098        str | None,
1099        Field(
1100            description=WORKSPACE_ID_TIP_TEXT,
1101            default=None,
1102        ),
1103    ],
1104    organization_id: Annotated[
1105        str | None,
1106        Field(
1107            description=ORGANIZATION_ID_TIP_TEXT,
1108            default=None,
1109        ),
1110    ],
1111) -> AgentConnectorDetailsResult:
1112    """Inspect an Airbyte Agents connector: metadata, readiness, warnings, and inline `docs`.
1113
1114    Call this before `execute_agent_connector` to learn what the connector exposes.
1115    The connector's usage docs summary is returned inline in `docs`; use
1116    `read_agent_skill_docs(skill_id=docs.skill_id, section=...)` for a single section's
1117    full detail.
1118    Airbyte Cloud destinations in the workspace are also accepted and resolve to
1119    built-in docs under `connector-destination:<id>`.
1120    """
1121    try:
1122        workspace = _get_agent_workspace(ctx, workspace_id, organization_id)
1123        details = workspace.get_connector(connector_id).inspect()
1124    except AirbyteError as error:
1125        if _is_not_found(error) or error.get_message() == CONNECTOR_NOT_FOUND_MESSAGE:
1126            return _inspect_destination_fallback(ctx, connector_id, workspace_id, organization_id)
1127        message = _agents_access_message(error)
1128        if message is None:
1129            raise
1130        return AgentConnectorDetailsResult(
1131            connector_id=connector_id,
1132            errors=[message],
1133        )
1134
1135    warnings = [str(warning) for warning in details.warnings]
1136    docs_result: AgentConnectorDocsResult | None = None
1137    if details.docs_skill_id:
1138        try:
1139            connector_docs = workspace.read_skill_docs(details.docs_skill_id)
1140        except (AirbyteError, requests.RequestException) as error:
1141            detail = error.get_message() if isinstance(error, AirbyteError) else str(error)
1142            docs_unavailable = f"Connector docs are unavailable: {detail}"
1143            warnings.append(docs_unavailable)
1144            docs_result = AgentConnectorDocsResult(
1145                skill_id=details.docs_skill_id,
1146                content="",
1147                warnings=[docs_unavailable],
1148            )
1149        else:
1150            docs_result = _connector_docs_result(_skill_docs_result(connector_docs))
1151
1152    return AgentConnectorDetailsResult(
1153        connector_id=details.connector_id,
1154        connector_name=details.name,
1155        workspace_id=details.workspace_id,
1156        integration_name=details.integration_name,
1157        docs=docs_result,
1158        warnings=_or_none(warnings),
1159    )
1160
1161
1162@mcp_tool(
1163    read_only=True,
1164    idempotent=True,
1165    open_world=True,
1166    extra_help_text=AGENTS_AUTH_TIP_TEXT,
1167)
1168def execute_agent_connector_ro(  # noqa: PLR0913  # Explicit args are the point of this tool.
1169    ctx: Context,
1170    connector_id: Annotated[
1171        str,
1172        Field(
1173            description=(
1174                "The ID of the Airbyte Agents connector, from `list_agent_connectors`. For "
1175                "`sql_select`, pass the `connector_id` of a destination entry "
1176                '(`connector_kind="destination"`) from that listing.'
1177            ),
1178        ),
1179    ],
1180    entity_type: Annotated[
1181        str,
1182        Field(
1183            description=(
1184                "The type of entity to act on, for example 'issues'. Call "
1185                "`inspect_agent_connector` to see the entity types a connector supports."
1186            ),
1187        ),
1188    ],
1189    action: Annotated[
1190        AgentReadAction,
1191        Field(
1192            description=(
1193                "The read action to run against the entity type. "
1194                "The `search` action is the connector's native API search, parallel to `get` "
1195                "and `list`. "
1196                "For `sql_select`, pass `sql` and `sql_dialect` (snowflake or bigquery) in "
1197                "`api_args` and any value for `entity_type`; the `connector_id` is a "
1198                "destination listed by `list_agent_connectors`; `SHOW TABLES` lists its tables "
1199                'and `SELECT * FROM <table> LIMIT 1` with `"dry_run": true` in `api_args` '
1200                "returns its columns without reading rows; never guess columns. On Snowflake, "
1201                "write identifiers unquoted unless discovery returned mixed case. The `download` "
1202                "action "
1203                "is deliberately absent because it returns a binary stream rather than JSON."
1204            ),
1205        ),
1206    ],
1207    api_args: Annotated[
1208        dict[str, Any] | str | None,
1209        Field(
1210            description=(
1211                "Connector-specific arguments for the action, as an object or a JSON "
1212                "object string. For example {'repository': 'airbytehq/PyAirbyte'}."
1213            ),
1214            default=None,
1215        ),
1216    ],
1217    *,
1218    select_fields: Annotated[
1219        list[str] | str | None,
1220        Field(
1221            description="Fields to keep in the response, as a list or a CSV string.",
1222            default=None,
1223        ),
1224    ],
1225    exclude_fields: Annotated[
1226        list[str] | str | None,
1227        Field(
1228            description="Fields to drop from the response, as a list or a CSV string.",
1229            default=None,
1230        ),
1231    ],
1232    page_size: Annotated[
1233        int | None,
1234        Field(
1235            description=(
1236                "Maximum number of entities to return in this page. Honored by Context Store "
1237                "`search` actions (sent as `limit`). Direct connector actions take their own "
1238                "page-size argument, if any, in `api_args`."
1239            ),
1240            default=None,
1241        ),
1242    ],
1243    cursor: Annotated[
1244        str | None,
1245        Field(
1246            description=(
1247                "Pagination cursor for Context Store `search` actions and `sql_select`, taken "
1248                "from `end_cursor` of a previous result. Direct connector "
1249                "actions such as `list` do not read this; pass their own cursor argument in "
1250                "`api_args` instead (for example GitHub's `after`), as named in the skill docs."
1251            ),
1252            default=None,
1253        ),
1254    ],
1255    intent: Annotated[
1256        str | None,
1257        Field(
1258            description="A short description of why the action is being run.",
1259            default=None,
1260        ),
1261    ],
1262    workspace_id: Annotated[
1263        str | None,
1264        Field(
1265            description=WORKSPACE_ID_TIP_TEXT,
1266            default=None,
1267        ),
1268    ],
1269    organization_id: Annotated[
1270        str | None,
1271        Field(
1272            description=ORGANIZATION_ID_TIP_TEXT,
1273            default=None,
1274        ),
1275    ],
1276) -> AgentExecuteToolResult:
1277    """Read data from an Airbyte Agents connector, without modifying anything.
1278
1279    This tool only accepts read actions, so it stays available in read-only mode. Use
1280    `execute_agent_connector` for actions that create, update, or delete data. Entity types
1281    are connector-specific, so call `inspect_agent_connector` first. The connector must
1282    belong to the given workspace.
1283
1284    To query a destination:
1285    1. Run `SHOW TABLES` to discover tables.
1286    2. Run `SELECT * FROM <table> LIMIT 1` with `"dry_run": true` in `api_args` to get the
1287       real column names; never guess them.
1288    3. Select data using the discovered names.
1289
1290    Use the destination's `connector_id` and `sql_dialect` as reported by
1291    `list_agent_connectors`. On Snowflake, identifiers are upper-cased; write them unquoted
1292    (double-quoting makes them case-sensitive). Quote a name only when discovery returns it in
1293    mixed or lower case, exactly as returned.
1294    """
1295    return _execute(
1296        ctx,
1297        connector_id=connector_id,
1298        workspace_id=workspace_id,
1299        organization_id=organization_id,
1300        entity_type=entity_type,
1301        action=action,
1302        api_args=api_args,
1303        select_fields=select_fields,
1304        exclude_fields=exclude_fields,
1305        page_size=page_size,
1306        cursor=cursor,
1307        intent=intent,
1308        read_only=True,
1309    )
1310
1311
1312@mcp_tool(
1313    open_world=True,
1314    extra_help_text=AGENTS_AUTH_TIP_TEXT,
1315)
1316def execute_agent_connector(  # noqa: PLR0913  # Explicit args are the point of this tool.
1317    ctx: Context,
1318    connector_id: Annotated[
1319        str,
1320        Field(description="The ID of the Airbyte Agents connector."),
1321    ],
1322    entity_type: Annotated[
1323        str,
1324        Field(
1325            description=(
1326                "The type of entity to act on, for example 'issues'. Call "
1327                "`inspect_agent_connector` to see the entity types a connector supports."
1328            ),
1329        ),
1330    ],
1331    action: Annotated[
1332        AgentAction,
1333        Field(
1334            description=(
1335                "The action to run against the entity type. "
1336                "The `search` action is the connector's native API search, parallel to `get` "
1337                "and `list`. "
1338                "For `sql_select`, pass `sql` and `sql_dialect` (snowflake or bigquery) in "
1339                "`api_args` and any value for `entity_type`. The `download` action "
1340                "is deliberately absent because it returns a binary stream rather than JSON."
1341            ),
1342        ),
1343    ],
1344    api_args: Annotated[
1345        dict[str, Any] | str | None,
1346        Field(
1347            description=(
1348                "Connector-specific arguments for the action, as an object or a JSON "
1349                "object string. For example {'repository': 'airbytehq/PyAirbyte'}."
1350            ),
1351            default=None,
1352        ),
1353    ],
1354    *,
1355    select_fields: Annotated[
1356        list[str] | str | None,
1357        Field(
1358            description="Fields to keep in the response, as a list or a CSV string.",
1359            default=None,
1360        ),
1361    ],
1362    exclude_fields: Annotated[
1363        list[str] | str | None,
1364        Field(
1365            description="Fields to drop from the response, as a list or a CSV string.",
1366            default=None,
1367        ),
1368    ],
1369    page_size: Annotated[
1370        int | None,
1371        Field(
1372            description=(
1373                "Maximum number of entities to return in this page. Honored by Context Store "
1374                "`search` actions (sent as `limit`). Direct connector actions take their own "
1375                "page-size argument, if any, in `api_args`."
1376            ),
1377            default=None,
1378        ),
1379    ],
1380    cursor: Annotated[
1381        str | None,
1382        Field(
1383            description=(
1384                "Pagination cursor for Context Store `search` actions and `sql_select`, taken "
1385                "from `end_cursor` of a previous result. Direct connector "
1386                "actions such as `list` do not read this; pass their own cursor argument in "
1387                "`api_args` instead (for example GitHub's `after`), as named in the skill docs."
1388            ),
1389            default=None,
1390        ),
1391    ],
1392    intent: Annotated[
1393        str | None,
1394        Field(
1395            description="A short description of why the action is being run.",
1396            default=None,
1397        ),
1398    ],
1399    read_only: Annotated[
1400        bool | None,
1401        Field(
1402            description=(
1403                "Set to `true` to reject write actions before any request is sent, when the "
1404                "caller wants a read guarantee from this tool."
1405            ),
1406            default=None,
1407        ),
1408    ],
1409    workspace_id: Annotated[
1410        str | None,
1411        Field(
1412            description=WORKSPACE_ID_TIP_TEXT,
1413            default=None,
1414        ),
1415    ],
1416    organization_id: Annotated[
1417        str | None,
1418        Field(
1419            description=ORGANIZATION_ID_TIP_TEXT,
1420            default=None,
1421        ),
1422    ],
1423) -> AgentExecuteToolResult:
1424    """Execute a single action against an Airbyte Agents connector, including writes.
1425
1426    Prefer `execute_agent_connector_ro` when only reading, since it is available in
1427    read-only mode. Entity types and actions are connector-specific, so call
1428    `inspect_agent_connector` first. The connector must belong to the given workspace.
1429    """
1430    return _execute(
1431        ctx,
1432        connector_id=connector_id,
1433        workspace_id=workspace_id,
1434        organization_id=organization_id,
1435        entity_type=entity_type,
1436        action=action,
1437        api_args=api_args,
1438        select_fields=select_fields,
1439        exclude_fields=exclude_fields,
1440        page_size=page_size,
1441        cursor=cursor,
1442        intent=intent,
1443        read_only=read_only,
1444    )
1445
1446
1447def _agent_skill_result(skill: AgentSkillInfo) -> AgentSkillResult:
1448    """Shape an `AgentSkillInfo` into an `AgentSkillResult`."""
1449    return AgentSkillResult(
1450        skill_id=skill.id,
1451        kind=skill.kind,
1452        title=skill.title,
1453        summary=skill.summary,
1454        tags=skill.tags,
1455    )
1456
1457
1458@mcp_tool(
1459    read_only=True,
1460    idempotent=True,
1461    open_world=True,
1462    extra_help_text=AGENTS_AUTH_TIP_TEXT,
1463)
1464def list_agent_skills(
1465    ctx: Context,
1466    *,
1467    workspace_id: Annotated[
1468        str | None,
1469        Field(
1470            description=WORKSPACE_ID_TIP_TEXT,
1471            default=None,
1472        ),
1473    ],
1474) -> AgentSkillListResult:
1475    """List all skills available to an Airbyte Agents workspace.
1476
1477    Skills are reusable documentation the Agents API serves, for example connector usage
1478    docs. All pages are fetched, so no pagination arguments are needed. Pass a listed
1479    skill's `skill_id` to `read_agent_skill_docs` to read it.
1480    """
1481    workspace = _get_agent_workspace(ctx, workspace_id)
1482    try:
1483        skills = workspace.list_skills()
1484    except AirbyteError as error:
1485        message = _agents_access_message(error)
1486        if message is None:
1487            raise
1488        return AgentSkillListResult(skills=[], message=message)
1489
1490    return AgentSkillListResult(
1491        skills=[_agent_skill_result(skill.info) for skill in skills],
1492    )
1493
1494
1495@mcp_tool(
1496    read_only=True,
1497    idempotent=True,
1498    open_world=True,
1499    extra_help_text=AGENTS_AUTH_TIP_TEXT,
1500)
1501def read_agent_skill_docs(
1502    ctx: Context,
1503    skill_id: Annotated[
1504        str,
1505        Field(
1506            description=(
1507                "Skill ID, e.g. the `docs.skill_id` reported by `inspect_agent_connector`, "
1508                "or a `skill_id` from `list_agent_skills`. `inspect_agent_connector` returns "
1509                "only a docs summary (`docs.content` plus `docs.guidance`); call "
1510                "this tool with no `section` for the full section outline, or with "
1511                "`section` for one section's full detail. SQL passthrough destinations "
1512                "use `connector-destination:<destination_id>` and merge the server docs "
1513                "with built-in `sql_select` guidance."
1514            ),
1515        ),
1516    ],
1517    *,
1518    section: Annotated[
1519        str | None,
1520        Field(
1521            description=(
1522                "Omit to get metadata, guidance, and the outline of available sections. "
1523                "Pass an exact section `id` from the outline to read that section."
1524            ),
1525            default=None,
1526        ),
1527    ],
1528    workspace_id: Annotated[
1529        str | None,
1530        Field(
1531            description=WORKSPACE_ID_TIP_TEXT,
1532            default=None,
1533        ),
1534    ],
1535) -> AgentSkillDocsResult:
1536    """Read a skill's docs in an Airbyte Agents workspace.
1537
1538    Without `section`, this returns the skill's metadata, guidance, and the outline of
1539    sections, which is the cheapest way to orient before reading a specific section.
1540    """
1541    workspace = _get_agent_workspace(ctx, workspace_id)
1542    destination: CloudDestination | None = None
1543    destination_resolved = False
1544    if skill_id.startswith(DESTINATION_SKILL_PREFIX):
1545        destination_resolved = True
1546        destination = _resolve_cloud_destination(
1547            ctx, connector_id_from_skill_id(skill_id), workspace_id
1548        )
1549        if (
1550            destination is not None
1551            and destination.definition_id in SQL_PASSTHROUGH_DESTINATION_DEFINITION_IDS
1552        ):
1553            try:
1554                docs = _read_destination_skill_docs(workspace, destination, section)
1555            except AirbyteError as error:
1556                message = _agents_access_message(error)
1557                if message is None:
1558                    raise
1559                return AgentSkillDocsResult(
1560                    skill_id=skill_id,
1561                    section_id=section,
1562                    outline=[],
1563                    content="",
1564                    errors=[message],
1565                )
1566            return _skill_docs_result(docs)
1567    try:
1568        docs = workspace.read_skill_docs(skill_id, section=section)
1569    except AirbyteError as error:
1570        if _is_not_found(error):
1571            return _destination_skill_docs_fallback(
1572                ctx,
1573                skill_id,
1574                section,
1575                workspace_id,
1576                destination=destination,
1577                destination_resolved=destination_resolved,
1578            )
1579        message = _agents_access_message(error)
1580        if message is None:
1581            raise
1582        return AgentSkillDocsResult(
1583            skill_id=skill_id,
1584            section_id=section,
1585            outline=[],
1586            content="",
1587            errors=[message],
1588        )
1589
1590    return _skill_docs_result(docs)
1591
1592
1593def register_agents_tools(app: FastMCP) -> None:
1594    """Register the Airbyte Agents tools with the FastMCP app."""
1595    exclude_args = ["workspace_id"] if AIRBYTE_CLOUD_WORKSPACE_ID_IS_SET else None
1596    if exclude_args:
1597        _add_defaults_for_exclude_args(exclude_args)
1598    register_mcp_tools(
1599        app,
1600        mcp_module=__name__,
1601        exclude_args=exclude_args,
1602    )