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