From b0727aee66f13466c6380cec6fa1039dc1268181 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Wed, 19 Aug 2026 13:23:48 -0700 Subject: [PATCH 1/6] feat(integrations): add Bitbucket Cloud --- apps/docs/components/icons.tsx | 32 + apps/docs/components/ui/icon-mapping.ts | 2 + .../docs/en/integrations/bitbucket.mdx | 1548 +++++++++++++++++ .../content/docs/en/integrations/meta.json | 1 + .../self-hosting/integrations-oauth.mdx | 8 + .../bitbucket/repositories/route.test.ts | 282 +++ .../api/tools/bitbucket/repositories/route.ts | 168 ++ .../tools/bitbucket/workspaces/route.test.ts | 284 +++ .../api/tools/bitbucket/workspaces/route.ts | 168 ++ .../connect-oauth-modal.tsx | 2 +- apps/sim/blocks/blocks/bitbucket.test.ts | 374 ++++ apps/sim/blocks/blocks/bitbucket.ts | 1224 +++++++++++++ apps/sim/blocks/registry-maps.ts | 3 + apps/sim/components/icons.tsx | 32 + .../providers/bitbucket/selectors.test.ts | 213 +++ .../providers/bitbucket/selectors.ts | 95 + apps/sim/hooks/selectors/registry.ts | 2 + apps/sim/hooks/selectors/types.ts | 4 + .../lib/api/contracts/selectors/bitbucket.ts | 187 ++ apps/sim/lib/api/contracts/selectors/index.ts | 7 + apps/sim/lib/auth/connectors/providers.ts | 112 ++ apps/sim/lib/core/config/env.ts | 2 + apps/sim/lib/integrations/icon-mapping.ts | 2 + .../lib/integrations/oauth-service.test.ts | 1 + apps/sim/lib/oauth/oauth.test.ts | 146 ++ apps/sim/lib/oauth/oauth.ts | 38 + apps/sim/lib/oauth/types.ts | 2 + apps/sim/lib/oauth/utils.test.ts | 15 + apps/sim/lib/oauth/utils.ts | 19 +- .../lib/workflows/subblocks/context.test.ts | 29 + apps/sim/lib/workflows/subblocks/context.ts | 1 + .../tools/bitbucket/approve_pull_request.ts | 46 + apps/sim/tools/bitbucket/create_branch.ts | 65 + .../tools/bitbucket/create_pull_request.ts | 121 ++ .../bitbucket/create_pull_request_comment.ts | 72 + .../tools/bitbucket/decline_pull_request.ts | 46 + apps/sim/tools/bitbucket/delete_branch.ts | 39 + apps/sim/tools/bitbucket/get_commit.ts | 58 + apps/sim/tools/bitbucket/get_file.ts | 157 ++ apps/sim/tools/bitbucket/get_file_metadata.ts | 65 + .../tools/bitbucket/get_merge_task_status.ts | 105 ++ apps/sim/tools/bitbucket/get_pipeline.ts | 57 + .../tools/bitbucket/get_pipeline_step_log.ts | 82 + apps/sim/tools/bitbucket/get_pull_request.ts | 48 + .../tools/bitbucket/get_pull_request_diff.ts | 130 ++ .../bitbucket/get_pull_request_diffstat.ts | 133 ++ apps/sim/tools/bitbucket/get_repository.ts | 48 + apps/sim/tools/bitbucket/index.ts | 96 + apps/sim/tools/bitbucket/list_branches.ts | 75 + apps/sim/tools/bitbucket/list_commits.ts | 56 + apps/sim/tools/bitbucket/list_directory.ts | 96 + .../tools/bitbucket/list_pipeline_steps.ts | 66 + apps/sim/tools/bitbucket/list_pipelines.ts | 160 ++ .../bitbucket/list_pull_request_comments.ts | 75 + .../list_pull_request_commit_statuses.ts | 75 + .../sim/tools/bitbucket/list_pull_requests.ts | 86 + apps/sim/tools/bitbucket/list_repositories.ts | 92 + apps/sim/tools/bitbucket/list_workspaces.ts | 79 + .../sim/tools/bitbucket/merge_pull_request.ts | 152 ++ apps/sim/tools/bitbucket/pipelines.test.ts | 445 +++++ .../sim/tools/bitbucket/pull-requests.test.ts | 773 ++++++++ .../tools/bitbucket/repository-source.test.ts | 861 +++++++++ .../bitbucket/request_pull_request_changes.ts | 46 + apps/sim/tools/bitbucket/stop_pipeline.ts | 39 + apps/sim/tools/bitbucket/trigger_pipeline.ts | 81 + apps/sim/tools/bitbucket/types.ts | 864 +++++++++ apps/sim/tools/bitbucket/utils.server.test.ts | 188 ++ apps/sim/tools/bitbucket/utils.server.ts | 238 +++ apps/sim/tools/bitbucket/utils.test.ts | 418 +++++ apps/sim/tools/bitbucket/utils.ts | 1038 +++++++++++ apps/sim/tools/bitbucket/validation.ts | 78 + apps/sim/tools/generated/tool-ids.ts | 2 +- apps/sim/tools/generated/tool-metadata.ts | 2 +- apps/sim/tools/generated/tool-outputs.ts | 2 +- apps/sim/tools/registry.ts | 62 + .../deployment-config/src/env-capabilities.ts | 1 + .../deployment-config/src/integrations.json | 140 ++ packages/sim-setup/src/capability-config.ts | 4 + scripts/check-api-validation-contracts.ts | 4 +- 79 files changed, 12661 insertions(+), 8 deletions(-) create mode 100644 apps/docs/content/docs/en/integrations/bitbucket.mdx create mode 100644 apps/sim/app/api/tools/bitbucket/repositories/route.test.ts create mode 100644 apps/sim/app/api/tools/bitbucket/repositories/route.ts create mode 100644 apps/sim/app/api/tools/bitbucket/workspaces/route.test.ts create mode 100644 apps/sim/app/api/tools/bitbucket/workspaces/route.ts create mode 100644 apps/sim/blocks/blocks/bitbucket.test.ts create mode 100644 apps/sim/blocks/blocks/bitbucket.ts create mode 100644 apps/sim/hooks/selectors/providers/bitbucket/selectors.test.ts create mode 100644 apps/sim/hooks/selectors/providers/bitbucket/selectors.ts create mode 100644 apps/sim/lib/api/contracts/selectors/bitbucket.ts create mode 100644 apps/sim/tools/bitbucket/approve_pull_request.ts create mode 100644 apps/sim/tools/bitbucket/create_branch.ts create mode 100644 apps/sim/tools/bitbucket/create_pull_request.ts create mode 100644 apps/sim/tools/bitbucket/create_pull_request_comment.ts create mode 100644 apps/sim/tools/bitbucket/decline_pull_request.ts create mode 100644 apps/sim/tools/bitbucket/delete_branch.ts create mode 100644 apps/sim/tools/bitbucket/get_commit.ts create mode 100644 apps/sim/tools/bitbucket/get_file.ts create mode 100644 apps/sim/tools/bitbucket/get_file_metadata.ts create mode 100644 apps/sim/tools/bitbucket/get_merge_task_status.ts create mode 100644 apps/sim/tools/bitbucket/get_pipeline.ts create mode 100644 apps/sim/tools/bitbucket/get_pipeline_step_log.ts create mode 100644 apps/sim/tools/bitbucket/get_pull_request.ts create mode 100644 apps/sim/tools/bitbucket/get_pull_request_diff.ts create mode 100644 apps/sim/tools/bitbucket/get_pull_request_diffstat.ts create mode 100644 apps/sim/tools/bitbucket/get_repository.ts create mode 100644 apps/sim/tools/bitbucket/index.ts create mode 100644 apps/sim/tools/bitbucket/list_branches.ts create mode 100644 apps/sim/tools/bitbucket/list_commits.ts create mode 100644 apps/sim/tools/bitbucket/list_directory.ts create mode 100644 apps/sim/tools/bitbucket/list_pipeline_steps.ts create mode 100644 apps/sim/tools/bitbucket/list_pipelines.ts create mode 100644 apps/sim/tools/bitbucket/list_pull_request_comments.ts create mode 100644 apps/sim/tools/bitbucket/list_pull_request_commit_statuses.ts create mode 100644 apps/sim/tools/bitbucket/list_pull_requests.ts create mode 100644 apps/sim/tools/bitbucket/list_repositories.ts create mode 100644 apps/sim/tools/bitbucket/list_workspaces.ts create mode 100644 apps/sim/tools/bitbucket/merge_pull_request.ts create mode 100644 apps/sim/tools/bitbucket/pipelines.test.ts create mode 100644 apps/sim/tools/bitbucket/pull-requests.test.ts create mode 100644 apps/sim/tools/bitbucket/repository-source.test.ts create mode 100644 apps/sim/tools/bitbucket/request_pull_request_changes.ts create mode 100644 apps/sim/tools/bitbucket/stop_pipeline.ts create mode 100644 apps/sim/tools/bitbucket/trigger_pipeline.ts create mode 100644 apps/sim/tools/bitbucket/types.ts create mode 100644 apps/sim/tools/bitbucket/utils.server.test.ts create mode 100644 apps/sim/tools/bitbucket/utils.server.ts create mode 100644 apps/sim/tools/bitbucket/utils.test.ts create mode 100644 apps/sim/tools/bitbucket/utils.ts create mode 100644 apps/sim/tools/bitbucket/validation.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 8580bb9ea85..28fe90c33e8 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -754,6 +754,38 @@ export function GithubOutlineIcon(props: SVGProps) { ) } +export function BitbucketIcon(props: SVGProps) { + const id = useId() + const gradientId = `bitbucket_original_a_${id}` + + return ( + + + + + + + + + + + ) +} + export function GitLabIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 695d4b0c232..cbf910d4b24 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -24,6 +24,7 @@ import { AttioIcon, AzureDataExplorerIcon, AzureIcon, + BitbucketIcon, BoxCompanyIcon, BrainIcon, BrandfetchIcon, @@ -288,6 +289,7 @@ export const blockTypeToIconMap: Record = { attio: AttioIcon, azure_data_explorer: AzureDataExplorerIcon, azure_devops: AzureIcon, + bitbucket: BitbucketIcon, box: BoxCompanyIcon, brandfetch: BrandfetchIcon, brex: BrexIcon, diff --git a/apps/docs/content/docs/en/integrations/bitbucket.mdx b/apps/docs/content/docs/en/integrations/bitbucket.mdx new file mode 100644 index 00000000000..8d7529ce0c8 --- /dev/null +++ b/apps/docs/content/docs/en/integrations/bitbucket.mdx @@ -0,0 +1,1548 @@ +--- +title: Bitbucket +description: Work with Bitbucket Cloud repositories, pull requests, and pipelines +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Connect Bitbucket Cloud to inspect repositories and source, collaborate on pull requests, and diagnose or control pipelines. This action integration uses OAuth and does not create webhooks or triggers. + + + +## Actions + +### Bitbucket List Workspaces + +List Bitbucket Cloud workspaces available to the authenticated account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `sort` | string | No | Workspace sort field; Bitbucket currently supports slug | +| `administrator` | boolean | No | Filter by whether the caller is a workspace administrator | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Workspace access records | +| ↳ `type` | string | Bitbucket workspace-access object type | +| ↳ `slug` | string | Workspace slug | +| ↳ `uuid` | string | Workspace UUID | +| ↳ `administrator` | boolean | Whether the caller administers the workspace | +| ↳ `selfUrl` | string | Workspace API URL | +| ↳ `avatarUrl` | string | Workspace avatar URL | + +### Bitbucket List Repositories + +List repositories in a Bitbucket Cloud workspace + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `role` | string | No | Caller role filter: admin, contributor, member, or owner | +| `q` | string | No | Bitbucket filtering expression | +| `sort` | string | No | Bitbucket sort expression | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Repositories | +| ↳ `type` | string | Bitbucket repository object type | +| ↳ `uuid` | string | Repository UUID | +| ↳ `slug` | string | Repository slug | +| ↳ `name` | string | Repository name | +| ↳ `fullName` | string | Workspace and repository full name | +| ↳ `description` | string | Repository description | +| ↳ `isPrivate` | boolean | Whether the repository is private | +| ↳ `scm` | string | Source control system | +| ↳ `language` | string | Primary repository language | +| ↳ `size` | number | Repository size in bytes | +| ↳ `createdOn` | string | Repository creation timestamp | +| ↳ `updatedOn` | string | Repository update timestamp | +| ↳ `mainBranch` | string | Main branch name | +| ↳ `owner` | object | Repository owner | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `project` | object | Containing Bitbucket project | +| ↳ `uuid` | string | Project UUID | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `selfUrl` | string | Repository API URL | +| ↳ `htmlUrl` | string | Repository web URL | + +### Bitbucket Get Repository + +Get a Bitbucket Cloud repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `repository` | object | Repository details | +| ↳ `type` | string | Bitbucket repository object type | +| ↳ `uuid` | string | Repository UUID | +| ↳ `slug` | string | Repository slug | +| ↳ `name` | string | Repository name | +| ↳ `fullName` | string | Workspace and repository full name | +| ↳ `description` | string | Repository description | +| ↳ `isPrivate` | boolean | Whether the repository is private | +| ↳ `scm` | string | Source control system | +| ↳ `language` | string | Primary repository language | +| ↳ `size` | number | Repository size in bytes | +| ↳ `createdOn` | string | Repository creation timestamp | +| ↳ `updatedOn` | string | Repository update timestamp | +| ↳ `mainBranch` | string | Main branch name | +| ↳ `owner` | object | Repository owner | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `project` | object | Containing Bitbucket project | +| ↳ `uuid` | string | Project UUID | +| ↳ `key` | string | Project key | +| ↳ `name` | string | Project name | +| ↳ `selfUrl` | string | Repository API URL | +| ↳ `htmlUrl` | string | Repository web URL | + +### Bitbucket List Branches + +List branches in a Bitbucket Cloud repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `q` | string | No | Bitbucket branch filtering expression | +| `sort` | string | No | Bitbucket branch sort expression | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Branches | +| ↳ `type` | string | Bitbucket branch object type | +| ↳ `name` | string | Branch name | +| ↳ `target` | object | Commit targeted by the branch | +| ↳ `type` | string | Bitbucket commit object type | +| ↳ `hash` | string | Commit hash | +| ↳ `date` | string | Commit timestamp | +| ↳ `message` | string | Full commit message | +| ↳ `summary` | string | Raw commit summary | +| ↳ `authorRaw` | string | Raw author value stored by Git | +| ↳ `author` | object | Matched Bitbucket account, when available | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `committerRaw` | string | Raw committer value stored by Git | +| ↳ `committer` | object | Matched Bitbucket committer account, when available | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `parents` | array | Parent commits | +| ↳ `hash` | string | Parent commit hash | +| ↳ `selfUrl` | string | Commit API URL | +| ↳ `htmlUrl` | string | Commit web URL | +| ↳ `mergeStrategies` | array | Merge strategies available for the branch | +| ↳ `defaultMergeStrategy` | string | Default merge strategy | +| ↳ `selfUrl` | string | Branch API URL | +| ↳ `htmlUrl` | string | Branch web URL | + +### Bitbucket Create Branch + +Create a branch at a commit hash or existing ref + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `name` | string | Yes | New branch name without refs/heads prefix | +| `target` | string | Yes | Full commit hash or existing ref to target | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `branch` | object | Created branch | +| ↳ `type` | string | Bitbucket branch object type | +| ↳ `name` | string | Branch name | +| ↳ `target` | object | Commit targeted by the branch | +| ↳ `type` | string | Bitbucket commit object type | +| ↳ `hash` | string | Commit hash | +| ↳ `date` | string | Commit timestamp | +| ↳ `message` | string | Full commit message | +| ↳ `summary` | string | Raw commit summary | +| ↳ `authorRaw` | string | Raw author value stored by Git | +| ↳ `author` | object | Matched Bitbucket account, when available | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `committerRaw` | string | Raw committer value stored by Git | +| ↳ `committer` | object | Matched Bitbucket committer account, when available | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `parents` | array | Parent commits | +| ↳ `hash` | string | Parent commit hash | +| ↳ `selfUrl` | string | Commit API URL | +| ↳ `htmlUrl` | string | Commit web URL | +| ↳ `mergeStrategies` | array | Merge strategies available for the branch | +| ↳ `defaultMergeStrategy` | string | Default merge strategy | +| ↳ `selfUrl` | string | Branch API URL | +| ↳ `htmlUrl` | string | Branch web URL | + +### Bitbucket Delete Branch + +Delete a branch from a Bitbucket Cloud repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `name` | string | Yes | Branch name to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `deleted` | boolean | Whether the branch was deleted | + +### Bitbucket List Commits + +List repository commits in reverse chronological order + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Commits | +| ↳ `type` | string | Bitbucket commit object type | +| ↳ `hash` | string | Commit hash | +| ↳ `date` | string | Commit timestamp | +| ↳ `message` | string | Full commit message | +| ↳ `summary` | string | Raw commit summary | +| ↳ `authorRaw` | string | Raw author value stored by Git | +| ↳ `author` | object | Matched Bitbucket account, when available | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `committerRaw` | string | Raw committer value stored by Git | +| ↳ `committer` | object | Matched Bitbucket committer account, when available | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `parents` | array | Parent commits | +| ↳ `hash` | string | Parent commit hash | +| ↳ `selfUrl` | string | Commit API URL | +| ↳ `htmlUrl` | string | Commit web URL | + +### Bitbucket Get Commit + +Get a repository commit by its full SHA-1 + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `commit` | string | Yes | Full 40-character commit SHA-1 | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `commit` | object | Commit details | +| ↳ `type` | string | Bitbucket commit object type | +| ↳ `hash` | string | Commit hash | +| ↳ `date` | string | Commit timestamp | +| ↳ `message` | string | Full commit message | +| ↳ `summary` | string | Raw commit summary | +| ↳ `authorRaw` | string | Raw author value stored by Git | +| ↳ `author` | object | Matched Bitbucket account, when available | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `committerRaw` | string | Raw committer value stored by Git | +| ↳ `committer` | object | Matched Bitbucket committer account, when available | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `parents` | array | Parent commits | +| ↳ `hash` | string | Parent commit hash | +| ↳ `selfUrl` | string | Commit API URL | +| ↳ `htmlUrl` | string | Commit web URL | + +### Bitbucket List Directory + +List one shallow repository directory at a full commit SHA-1 + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `commit` | string | Yes | Full 40-character commit SHA-1 | +| `path` | string | No | Repository-relative directory path; omit for the root | +| `q` | string | No | Bitbucket tree-entry filtering expression | +| `sort` | string | No | Bitbucket tree-entry sort expression | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Directory entries | +| ↳ `type` | string | Entry type, such as commit_file or commit_directory | +| ↳ `path` | string | Repository-relative path | +| ↳ `commitHash` | string | Resolved commit hash | +| ↳ `size` | number | File size in bytes when the entry is a file | +| ↳ `attributes` | array | File attributes when the entry is a file | +| ↳ `isBinary` | boolean | Whether file attributes include the binary marker | +| ↳ `selfUrl` | string | Source API URL | +| ↳ `metadataUrl` | string | Source metadata API URL | + +### Bitbucket Get File Metadata + +Inspect file size and attributes at a full repository commit SHA-1 + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `commit` | string | Yes | Full 40-character commit SHA-1 | +| `path` | string | Yes | Repository-relative file path | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `file` | object | File metadata | +| ↳ `type` | string | Entry type \(commit_file\) | +| ↳ `path` | string | Repository-relative path | +| ↳ `commitHash` | string | Resolved commit hash | +| ↳ `escapedPath` | string | Escaped display path | +| ↳ `size` | number | File size in bytes | +| ↳ `attributes` | array | File attributes reported by Bitbucket | +| ↳ `isBinary` | boolean | Whether the documented attributes include the binary marker | + +### Bitbucket Get File + +Read bounded UTF-8 text from a file at a full repository commit SHA-1 + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `commit` | string | Yes | Full 40-character commit SHA-1 | +| `path` | string | Yes | Repository-relative file path | +| `maxCharacters` | number | No | Maximum text characters to return \(1-500000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `content` | string | Bounded UTF-8 file text; null for binary content | +| `binary` | boolean | Whether documented metadata identifies binary content; null when unknown | +| `truncated` | boolean | Whether later content was omitted; null when binary size is unknown | +| `returnedBytes` | number | Provider bytes read for the returned file | +| `fullBytes` | number | Full file byte size when reported | +| `contentType` | string | Response MIME type | + +### Bitbucket List Pull Requests + +List pull requests in a Bitbucket Cloud repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `state` | string | No | State filter: OPEN, MERGED, DECLINED, or SUPERSEDED | +| `q` | string | No | Bitbucket pull request filtering expression | +| `sort` | string | No | Bitbucket pull request sort expression | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Pull requests | +| ↳ `type` | string | Bitbucket pull request object type | +| ↳ `id` | number | Repository-scoped pull request ID | +| ↳ `title` | string | Pull request title | +| ↳ `description` | string | Pull request description | +| ↳ `state` | string | Pull request state | +| ↳ `draft` | boolean | Whether the pull request is a draft | +| ↳ `queued` | boolean | Whether the pull request is queued | +| ↳ `author` | object | Pull request author | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `closedBy` | object | Account that closed the pull request | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `source` | object | Source endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `destination` | object | Destination endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `mergeCommitHash` | string | Merge commit hash | +| ↳ `commentCount` | number | Comment count | +| ↳ `taskCount` | number | Open task count | +| ↳ `closeSourceBranch` | boolean | Whether merging closes the source branch | +| ↳ `reason` | string | Reason the pull request was declined | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `updatedOn` | string | Update timestamp | +| ↳ `reviewers` | array | Explicit reviewers | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `participants` | array | Pull request participants | +| ↳ `type` | string | Bitbucket participant object type | +| ↳ `user` | object | Participating account | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `role` | string | Participant role | +| ↳ `approved` | boolean | Whether the participant approved | +| ↳ `state` | string | Review state | +| ↳ `participatedOn` | string | Timestamp of the participant action | +| ↳ `selfUrl` | string | Pull request API URL | +| ↳ `htmlUrl` | string | Pull request web URL | + +### Bitbucket Get Pull Request + +Get a pull request by repository-scoped ID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pullRequest` | object | Pull request details | +| ↳ `type` | string | Bitbucket pull request object type | +| ↳ `id` | number | Repository-scoped pull request ID | +| ↳ `title` | string | Pull request title | +| ↳ `description` | string | Pull request description | +| ↳ `state` | string | Pull request state | +| ↳ `draft` | boolean | Whether the pull request is a draft | +| ↳ `queued` | boolean | Whether the pull request is queued | +| ↳ `author` | object | Pull request author | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `closedBy` | object | Account that closed the pull request | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `source` | object | Source endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `destination` | object | Destination endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `mergeCommitHash` | string | Merge commit hash | +| ↳ `commentCount` | number | Comment count | +| ↳ `taskCount` | number | Open task count | +| ↳ `closeSourceBranch` | boolean | Whether merging closes the source branch | +| ↳ `reason` | string | Reason the pull request was declined | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `updatedOn` | string | Update timestamp | +| ↳ `reviewers` | array | Explicit reviewers | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `participants` | array | Pull request participants | +| ↳ `type` | string | Bitbucket participant object type | +| ↳ `user` | object | Participating account | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `role` | string | Participant role | +| ↳ `approved` | boolean | Whether the participant approved | +| ↳ `state` | string | Review state | +| ↳ `participatedOn` | string | Timestamp of the participant action | +| ↳ `selfUrl` | string | Pull request API URL | +| ↳ `htmlUrl` | string | Pull request web URL | + +### Bitbucket Create Pull Request + +Create a pull request between repository branches + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `title` | string | Yes | Pull request title | +| `sourceBranch` | string | Yes | Source branch name | +| `destinationBranch` | string | Yes | Destination branch name | +| `description` | string | No | Pull request description | +| `closeSourceBranch` | boolean | No | Close the source branch after merge | +| `draft` | boolean | No | Create the pull request as a draft | +| `reviewerUuids` | array | No | Bitbucket account UUIDs to add as reviewers | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pullRequest` | object | Created pull request | +| ↳ `type` | string | Bitbucket pull request object type | +| ↳ `id` | number | Repository-scoped pull request ID | +| ↳ `title` | string | Pull request title | +| ↳ `description` | string | Pull request description | +| ↳ `state` | string | Pull request state | +| ↳ `draft` | boolean | Whether the pull request is a draft | +| ↳ `queued` | boolean | Whether the pull request is queued | +| ↳ `author` | object | Pull request author | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `closedBy` | object | Account that closed the pull request | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `source` | object | Source endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `destination` | object | Destination endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `mergeCommitHash` | string | Merge commit hash | +| ↳ `commentCount` | number | Comment count | +| ↳ `taskCount` | number | Open task count | +| ↳ `closeSourceBranch` | boolean | Whether merging closes the source branch | +| ↳ `reason` | string | Reason the pull request was declined | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `updatedOn` | string | Update timestamp | +| ↳ `reviewers` | array | Explicit reviewers | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `participants` | array | Pull request participants | +| ↳ `type` | string | Bitbucket participant object type | +| ↳ `user` | object | Participating account | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `role` | string | Participant role | +| ↳ `approved` | boolean | Whether the participant approved | +| ↳ `state` | string | Review state | +| ↳ `participatedOn` | string | Timestamp of the participant action | +| ↳ `selfUrl` | string | Pull request API URL | +| ↳ `htmlUrl` | string | Pull request web URL | + +### Bitbucket Merge Pull Request + +Start an asynchronous pull request merge and return a task to poll when needed + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | +| `mergeStrategy` | string | No | Merge strategy: merge_commit, squash, fast_forward, squash_fast_forward, rebase_fast_forward, or rebase_merge | +| `message` | string | No | Merge commit message \(maximum 128 KiB encoded as UTF-8\) | +| `closeSourceBranch` | boolean | No | Delete the source branch after merging | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Whether the merge completed or remains pending | +| `taskId` | string | Async merge task ID | +| `taskUrl` | string | Validated task polling URL | +| `pullRequest` | object | Merged pull request when completed synchronously | +| ↳ `type` | string | Bitbucket pull request object type | +| ↳ `id` | number | Repository-scoped pull request ID | +| ↳ `title` | string | Pull request title | +| ↳ `description` | string | Pull request description | +| ↳ `state` | string | Pull request state | +| ↳ `draft` | boolean | Whether the pull request is a draft | +| ↳ `queued` | boolean | Whether the pull request is queued | +| ↳ `author` | object | Pull request author | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `closedBy` | object | Account that closed the pull request | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `source` | object | Source endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `destination` | object | Destination endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `mergeCommitHash` | string | Merge commit hash | +| ↳ `commentCount` | number | Comment count | +| ↳ `taskCount` | number | Open task count | +| ↳ `closeSourceBranch` | boolean | Whether merging closes the source branch | +| ↳ `reason` | string | Reason the pull request was declined | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `updatedOn` | string | Update timestamp | +| ↳ `reviewers` | array | Explicit reviewers | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `participants` | array | Pull request participants | +| ↳ `type` | string | Bitbucket participant object type | +| ↳ `user` | object | Participating account | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `role` | string | Participant role | +| ↳ `approved` | boolean | Whether the participant approved | +| ↳ `state` | string | Review state | +| ↳ `participatedOn` | string | Timestamp of the participant action | +| ↳ `selfUrl` | string | Pull request API URL | +| ↳ `htmlUrl` | string | Pull request web URL | + +### Bitbucket Get Merge Task Status + +Poll the status of an asynchronous pull request merge task + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | +| `taskId` | string | Yes | Merge task ID returned by Bitbucket Merge Pull Request | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `taskStatus` | string | PENDING or SUCCESS | +| `selfUrl` | string | Merge task API URL | +| `mergeResult` | object | Merged pull request when the task succeeds | +| ↳ `type` | string | Bitbucket pull request object type | +| ↳ `id` | number | Repository-scoped pull request ID | +| ↳ `title` | string | Pull request title | +| ↳ `description` | string | Pull request description | +| ↳ `state` | string | Pull request state | +| ↳ `draft` | boolean | Whether the pull request is a draft | +| ↳ `queued` | boolean | Whether the pull request is queued | +| ↳ `author` | object | Pull request author | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `closedBy` | object | Account that closed the pull request | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `source` | object | Source endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `destination` | object | Destination endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `mergeCommitHash` | string | Merge commit hash | +| ↳ `commentCount` | number | Comment count | +| ↳ `taskCount` | number | Open task count | +| ↳ `closeSourceBranch` | boolean | Whether merging closes the source branch | +| ↳ `reason` | string | Reason the pull request was declined | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `updatedOn` | string | Update timestamp | +| ↳ `reviewers` | array | Explicit reviewers | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `participants` | array | Pull request participants | +| ↳ `type` | string | Bitbucket participant object type | +| ↳ `user` | object | Participating account | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `role` | string | Participant role | +| ↳ `approved` | boolean | Whether the participant approved | +| ↳ `state` | string | Review state | +| ↳ `participatedOn` | string | Timestamp of the participant action | +| ↳ `selfUrl` | string | Pull request API URL | +| ↳ `htmlUrl` | string | Pull request web URL | + +### Bitbucket Decline Pull Request + +Decline an open pull request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pullRequest` | object | Declined pull request | +| ↳ `type` | string | Bitbucket pull request object type | +| ↳ `id` | number | Repository-scoped pull request ID | +| ↳ `title` | string | Pull request title | +| ↳ `description` | string | Pull request description | +| ↳ `state` | string | Pull request state | +| ↳ `draft` | boolean | Whether the pull request is a draft | +| ↳ `queued` | boolean | Whether the pull request is queued | +| ↳ `author` | object | Pull request author | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `closedBy` | object | Account that closed the pull request | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `source` | object | Source endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `destination` | object | Destination endpoint | +| ↳ `branchName` | string | Branch name | +| ↳ `commitHash` | string | Commit hash | +| ↳ `repositoryUuid` | string | Repository UUID | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `mergeCommitHash` | string | Merge commit hash | +| ↳ `commentCount` | number | Comment count | +| ↳ `taskCount` | number | Open task count | +| ↳ `closeSourceBranch` | boolean | Whether merging closes the source branch | +| ↳ `reason` | string | Reason the pull request was declined | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `updatedOn` | string | Update timestamp | +| ↳ `reviewers` | array | Explicit reviewers | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `participants` | array | Pull request participants | +| ↳ `type` | string | Bitbucket participant object type | +| ↳ `user` | object | Participating account | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `role` | string | Participant role | +| ↳ `approved` | boolean | Whether the participant approved | +| ↳ `state` | string | Review state | +| ↳ `participatedOn` | string | Timestamp of the participant action | +| ↳ `selfUrl` | string | Pull request API URL | +| ↳ `htmlUrl` | string | Pull request web URL | + +### Bitbucket Approve Pull Request + +Approve a pull request as the authenticated account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `participant` | object | Approval participant record | +| ↳ `type` | string | Bitbucket participant object type | +| ↳ `user` | object | Participating account | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `role` | string | Participant role | +| ↳ `approved` | boolean | Whether the participant approved | +| ↳ `state` | string | Review state | +| ↳ `participatedOn` | string | Timestamp of the participant action | + +### Bitbucket Request Pull Request Changes + +Request changes on a pull request as the authenticated account + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `participant` | object | Change-request participant record | +| ↳ `type` | string | Bitbucket participant object type | +| ↳ `user` | object | Participating account | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `role` | string | Participant role | +| ↳ `approved` | boolean | Whether the participant approved | +| ↳ `state` | string | Review state | +| ↳ `participatedOn` | string | Timestamp of the participant action | + +### Bitbucket Get Pull Request Diff + +Read a bounded UTF-8 unified diff for one pull request file + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | +| `path` | string | Yes | Repository-relative file path to include in the diff | +| `maxCharacters` | number | No | Maximum diff characters to return \(1-500000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `diff` | string | Bounded unified diff text decoded as UTF-8 | +| `decodingLossy` | boolean | Whether invalid UTF-8 source bytes were replaced while decoding | +| `truncated` | boolean | Whether later diff text was omitted | +| `returnedBytes` | number | Provider bytes read for the returned diff | +| `fullBytes` | number | Full diff byte size when reported | + +### Bitbucket Get Pull Request Diffstat + +List per-file change statistics for a pull request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Per-file diff statistics | +| ↳ `type` | string | Diffstat object type | +| ↳ `status` | string | File change status | +| ↳ `linesAdded` | number | Lines added | +| ↳ `linesRemoved` | number | Lines removed | +| ↳ `oldPath` | string | Old file path | +| ↳ `newPath` | string | New file path | +| ↳ `oldCommitHash` | string | Old file commit hash | +| ↳ `newCommitHash` | string | New file commit hash | + +### Bitbucket List Pull Request Comments + +List global, inline, and reply comments on a pull request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | +| `q` | string | No | Bitbucket comment filtering expression | +| `sort` | string | No | Bitbucket comment sort expression | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Pull request comments | +| ↳ `type` | string | Bitbucket comment object type | +| ↳ `id` | number | Comment ID | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `updatedOn` | string | Update timestamp | +| ↳ `content` | string | Raw comment content | +| ↳ `user` | object | Comment author | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `deleted` | boolean | Whether the comment was deleted | +| ↳ `parentId` | number | Parent comment ID | +| ↳ `inline` | object | Inline comment anchor | +| ↳ `path` | string | Anchored file path | +| ↳ `from` | number | Ending line in the old file | +| ↳ `to` | number | Ending line in the new file | +| ↳ `startFrom` | number | Starting line in the old file | +| ↳ `startTo` | number | Starting line in the new file | +| ↳ `pending` | boolean | Whether the comment is pending | +| ↳ `resolution` | object | Comment resolution details | +| ↳ `resolver` | object | Account that resolved the comment | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `resolvedOn` | string | Resolution timestamp | +| ↳ `selfUrl` | string | Comment API URL | +| ↳ `htmlUrl` | string | Comment web URL | + +### Bitbucket Create Pull Request Comment + +Create a global comment or reply on a pull request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | +| `content` | string | Yes | Raw comment content | +| `parentId` | number | No | Parent comment ID when creating a reply | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `comment` | object | Created comment | +| ↳ `type` | string | Bitbucket comment object type | +| ↳ `id` | number | Comment ID | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `updatedOn` | string | Update timestamp | +| ↳ `content` | string | Raw comment content | +| ↳ `user` | object | Comment author | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `deleted` | boolean | Whether the comment was deleted | +| ↳ `parentId` | number | Parent comment ID | +| ↳ `inline` | object | Inline comment anchor | +| ↳ `path` | string | Anchored file path | +| ↳ `from` | number | Ending line in the old file | +| ↳ `to` | number | Ending line in the new file | +| ↳ `startFrom` | number | Starting line in the old file | +| ↳ `startTo` | number | Starting line in the new file | +| ↳ `pending` | boolean | Whether the comment is pending | +| ↳ `resolution` | object | Comment resolution details | +| ↳ `resolver` | object | Account that resolved the comment | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `resolvedOn` | string | Resolution timestamp | +| ↳ `selfUrl` | string | Comment API URL | +| ↳ `htmlUrl` | string | Comment web URL | + +### Bitbucket List Pull Request Commit Statuses + +List commit statuses associated with a pull request + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `prId` | number | Yes | Repository-scoped pull request ID | +| `q` | string | No | Bitbucket commit status filtering expression | +| `sort` | string | No | Bitbucket commit status sort expression | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Pull request commit statuses | +| ↳ `type` | string | Bitbucket commit-status object type | +| ↳ `key` | string | Vendor-unique status key | +| ↳ `refName` | string | Reference name at status creation time | +| ↳ `url` | string | External build URL | +| ↳ `state` | string | Commit status state | +| ↳ `name` | string | Build name | +| ↳ `description` | string | Build description | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `updatedOn` | string | Update timestamp | +| ↳ `selfUrl` | string | Status API URL | +| ↳ `commitUrl` | string | Commit API URL | + +### Bitbucket List Pipelines + +List pipelines for a Bitbucket Cloud repository + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `refType` | string | No | Reference type filter: BRANCH, TAG, or ANNOTATED_TAG | +| `refName` | string | No | Reference name filter | +| `commitHash` | string | No | Full 40-character target commit SHA-1 filter | +| `selectorType` | string | No | Selector type filter: BRANCH, TAG, CUSTOM, PULLREQUESTS, or DEFAULT | +| `selectorPattern` | string | No | Pipeline selector pattern filter | +| `triggerType` | string | No | Trigger filter: PUSH, MANUAL, SCHEDULED, or PARENT_STEP | +| `status` | string | No | Pipeline status filter | +| `sort` | string | No | Bitbucket pipeline sort expression | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Pipelines | +| ↳ `type` | string | Bitbucket pipeline object type | +| ↳ `uuid` | string | Pipeline UUID | +| ↳ `buildNumber` | number | Pipeline build number | +| ↳ `creator` | object | Pipeline creator | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `target` | object | Pipeline target | +| ↳ `type` | string | Target object type | +| ↳ `refType` | string | Reference type | +| ↳ `refName` | string | Reference name | +| ↳ `commitHash` | string | Target commit hash | +| ↳ `selectorType` | string | Pipeline selector type | +| ↳ `selectorPattern` | string | Pipeline selector pattern | +| ↳ `triggerType` | string | Pipeline trigger object type | +| ↳ `state` | object | Pipeline state | +| ↳ `name` | string | State name | +| ↳ `stage` | string | In-progress stage name | +| ↳ `result` | string | Completed result name | +| ↳ `errorKey` | string | Completed-error key | +| ↳ `errorMessage` | string | Completed-error message | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `completedOn` | string | Completion timestamp | +| ↳ `buildSecondsUsed` | number | Build seconds used | +| ↳ `selfUrl` | string | Pipeline API URL | +| ↳ `stepsUrl` | string | Pipeline steps API URL | + +### Bitbucket Get Pipeline + +Get a pipeline by UUID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `pipelineUuid` | string | Yes | Pipeline UUID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipeline` | object | Pipeline details | +| ↳ `type` | string | Bitbucket pipeline object type | +| ↳ `uuid` | string | Pipeline UUID | +| ↳ `buildNumber` | number | Pipeline build number | +| ↳ `creator` | object | Pipeline creator | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `target` | object | Pipeline target | +| ↳ `type` | string | Target object type | +| ↳ `refType` | string | Reference type | +| ↳ `refName` | string | Reference name | +| ↳ `commitHash` | string | Target commit hash | +| ↳ `selectorType` | string | Pipeline selector type | +| ↳ `selectorPattern` | string | Pipeline selector pattern | +| ↳ `triggerType` | string | Pipeline trigger object type | +| ↳ `state` | object | Pipeline state | +| ↳ `name` | string | State name | +| ↳ `stage` | string | In-progress stage name | +| ↳ `result` | string | Completed result name | +| ↳ `errorKey` | string | Completed-error key | +| ↳ `errorMessage` | string | Completed-error message | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `completedOn` | string | Completion timestamp | +| ↳ `buildSecondsUsed` | number | Build seconds used | +| ↳ `selfUrl` | string | Pipeline API URL | +| ↳ `stepsUrl` | string | Pipeline steps API URL | + +### Bitbucket Trigger Pipeline + +Run the repository pipeline selected by a branch or ref target + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `refType` | string | Yes | Reference type: branch, tag, named_branch, or bookmark | +| `refName` | string | Yes | Reference name | +| `commitHash` | string | No | Full 40-character commit SHA-1 to run in the reference context | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `pipeline` | object | Triggered pipeline | +| ↳ `type` | string | Bitbucket pipeline object type | +| ↳ `uuid` | string | Pipeline UUID | +| ↳ `buildNumber` | number | Pipeline build number | +| ↳ `creator` | object | Pipeline creator | +| ↳ `type` | string | Bitbucket account object type | +| ↳ `uuid` | string | Bitbucket account UUID | +| ↳ `accountId` | string | Atlassian account ID | +| ↳ `displayName` | string | Account display name | +| ↳ `createdOn` | string | Account creation timestamp | +| ↳ `selfUrl` | string | Account API URL | +| ↳ `htmlUrl` | string | Account web URL | +| ↳ `avatarUrl` | string | Account avatar URL | +| ↳ `repositoryFullName` | string | Repository full name | +| ↳ `target` | object | Pipeline target | +| ↳ `type` | string | Target object type | +| ↳ `refType` | string | Reference type | +| ↳ `refName` | string | Reference name | +| ↳ `commitHash` | string | Target commit hash | +| ↳ `selectorType` | string | Pipeline selector type | +| ↳ `selectorPattern` | string | Pipeline selector pattern | +| ↳ `triggerType` | string | Pipeline trigger object type | +| ↳ `state` | object | Pipeline state | +| ↳ `name` | string | State name | +| ↳ `stage` | string | In-progress stage name | +| ↳ `result` | string | Completed result name | +| ↳ `errorKey` | string | Completed-error key | +| ↳ `errorMessage` | string | Completed-error message | +| ↳ `createdOn` | string | Creation timestamp | +| ↳ `completedOn` | string | Completion timestamp | +| ↳ `buildSecondsUsed` | number | Build seconds used | +| ↳ `selfUrl` | string | Pipeline API URL | +| ↳ `stepsUrl` | string | Pipeline steps API URL | + +### Bitbucket Stop Pipeline + +Stop a running pipeline + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `pipelineUuid` | string | Yes | Pipeline UUID | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `stopped` | boolean | Whether the stop request succeeded | + +### Bitbucket List Pipeline Steps + +List the steps in a pipeline + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `pipelineUuid` | string | Yes | Pipeline UUID | +| `nextUrl` | string | No | Opaque next-page URL returned by a previous Bitbucket call | +| `pageLen` | number | No | Results per page \(1-100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `page` | object | Pagination information | +| ↳ `size` | number | Total result count reported by Bitbucket | +| ↳ `page` | number | Current page number | +| ↳ `pageLen` | number | Number of results requested per page | +| ↳ `nextUrl` | string | Validated URL for the next page | +| ↳ `previousUrl` | string | Validated URL for the previous page | +| `items` | array | Pipeline steps | +| ↳ `type` | string | Bitbucket pipeline-step object type | +| ↳ `uuid` | string | Pipeline step UUID | +| ↳ `startedOn` | string | Step start timestamp | +| ↳ `completedOn` | string | Step completion timestamp | +| ↳ `state` | object | Pipeline step state | +| ↳ `name` | string | State name | +| ↳ `result` | string | Completed result name | +| ↳ `errorKey` | string | Completed-error key | +| ↳ `errorMessage` | string | Completed-error message | +| ↳ `imageName` | string | Build container image name | +| ↳ `setupCommands` | array | Setup commands | +| ↳ `name` | string | Command name | +| ↳ `command` | string | Executable command | +| ↳ `scriptCommands` | array | Build script commands | +| ↳ `name` | string | Command name | +| ↳ `command` | string | Executable command | + +### Bitbucket Get Pipeline Step Log + +Read a bounded UTF-8 tail of a pipeline step log + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `workspaceSlug` | string | Yes | Bitbucket workspace slug or UUID | +| `repoSlug` | string | Yes | Bitbucket repository slug or UUID | +| `pipelineUuid` | string | Yes | Pipeline UUID | +| `stepUuid` | string | Yes | Pipeline step UUID | +| `maxCharacters` | number | No | Maximum trailing log characters to return \(1-200000\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `log` | string | Bounded trailing UTF-8 log text | +| `truncated` | boolean | Whether earlier log output was omitted | +| `totalBytes` | number | Full log byte size when reported | + + diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json index dbe4a21bcda..bf435caffba 100644 --- a/apps/docs/content/docs/en/integrations/meta.json +++ b/apps/docs/content/docs/en/integrations/meta.json @@ -24,6 +24,7 @@ "attio-service-account", "azure_data_explorer", "azure_devops", + "bitbucket", "box", "box-service-account", "brandfetch", diff --git a/apps/docs/content/docs/en/platform/self-hosting/integrations-oauth.mdx b/apps/docs/content/docs/en/platform/self-hosting/integrations-oauth.mdx index cbb33d9fc5f..03f4f1c5def 100644 --- a/apps/docs/content/docs/en/platform/self-hosting/integrations-oauth.mdx +++ b/apps/docs/content/docs/en/platform/self-hosting/integrations-oauth.mdx @@ -127,6 +127,7 @@ The same variables also power "Sign in with Microsoft". | ClickUp | `CLICKUP_CLIENT_ID` / `CLICKUP_CLIENT_SECRET` | `clickup` | | Monday | `MONDAY_CLIENT_ID` / `MONDAY_CLIENT_SECRET` | `monday` | | Airtable | `AIRTABLE_CLIENT_ID` / `AIRTABLE_CLIENT_SECRET` | `airtable` | +| Bitbucket | `BITBUCKET_CLIENT_ID` / `BITBUCKET_CLIENT_SECRET` | `bitbucket` | | HubSpot | `HUBSPOT_CLIENT_ID` / `HUBSPOT_CLIENT_SECRET` | `hubspot` | | Salesforce | `SALESFORCE_CLIENT_ID` / `SALESFORCE_CLIENT_SECRET` | `salesforce` | | Pipedrive | `PIPEDRIVE_CLIENT_ID` / `PIPEDRIVE_CLIENT_SECRET` | `pipedrive` | @@ -146,6 +147,13 @@ The same variables also power "Sign in with Microsoft". | Spotify | `SPOTIFY_CLIENT_ID` / `SPOTIFY_CLIENT_SECRET` | `spotify` | | TikTok | `TIKTOK_CLIENT_ID` / `TIKTOK_CLIENT_SECRET` | `tiktok` | +For Bitbucket, create an OAuth consumer and register +`https:///api/auth/oauth2/callback/bitbucket` as its callback URL. Bitbucket fixes +permissions on the consumer instead of narrowing them per authorization request. Enable exactly +Account read, Repositories read/write, Pull requests read/write, and Pipelines read/write +(`account`, `repository`, `repository:write`, `pullrequest`, `pullrequest:write`, `pipeline`, and +`pipeline:write`). Webhook permission is not required for the integration-only release. + ### Services with a different flow | Service | Configuration | Notes | diff --git a/apps/sim/app/api/tools/bitbucket/repositories/route.test.ts b/apps/sim/app/api/tools/bitbucket/repositories/route.test.ts new file mode 100644 index 00000000000..c92a401bdd3 --- /dev/null +++ b/apps/sim/app/api/tools/bitbucket/repositories/route.test.ts @@ -0,0 +1,282 @@ +/** + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockAuthorizeCredentialUse, + mockCheckSessionOrInternalAuth, + mockFetch, + mockGetCredential, + mockRefreshAccessTokenIfNeeded, +} = vi.hoisted(() => ({ + mockAuthorizeCredentialUse: vi.fn(), + mockCheckSessionOrInternalAuth: vi.fn(), + mockFetch: vi.fn(), + mockGetCredential: vi.fn(), + mockRefreshAccessTokenIfNeeded: vi.fn(), +})) + +vi.mock('@/lib/auth/credential-access', () => ({ + authorizeCredentialUse: mockAuthorizeCredentialUse, +})) +vi.mock('@/lib/auth/hybrid', () => ({ + checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, +})) +vi.mock('@/lib/oauth/credential-service', () => ({ + getCredential: mockGetCredential, + refreshAccessTokenIfNeeded: mockRefreshAccessTokenIfNeeded, +})) + +import { POST } from '@/app/api/tools/bitbucket/repositories/route' + +const URL = 'http://localhost:3000/api/tools/bitbucket/repositories' +const FIRST_PAGE_URL = 'https://api.bitbucket.org/2.0/repositories/acme-platform?pagelen=100' +const SECOND_PAGE_URL = + 'https://api.bitbucket.org/2.0/repositories/acme-platform?page=2&pagelen=100' +const REQUEST_BODY = { + credential: 'credential-1', + workflowId: 'workflow-1', + workspaceSlug: 'acme-platform', +} as const + +function request(body: unknown): NextRequest { + return new NextRequest(URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: typeof body === 'string' ? body : JSON.stringify(body), + }) +} + +function providerResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }) +} + +async function json(response: Response): Promise> { + return (await response.json()) as Record +} + +describe('POST /api/tools/bitbucket/repositories', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('fetch', mockFetch) + mockCheckSessionOrInternalAuth.mockResolvedValue({ success: true, userId: 'caller-1' }) + mockAuthorizeCredentialUse.mockResolvedValue({ + ok: true, + credentialOwnerUserId: 'owner-1', + resolvedCredentialId: 'account-1', + credentialType: 'oauth', + }) + mockGetCredential.mockResolvedValue({ providerId: 'bitbucket' }) + mockRefreshAccessTokenIfNeeded.mockResolvedValue('server-only-token') + mockFetch.mockResolvedValue(providerResponse({ values: [] })) + }) + + afterAll(() => vi.unstubAllGlobals()) + + it('authenticates before attempting to parse an invalid body', async () => { + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ + success: false, + error: 'Authentication required', + }) + + const response = await POST(request('{not-json'), {}) + + expect(response.status).toBe(401) + expect(mockAuthorizeCredentialUse).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('requires the workspace dependency before credential authorization', async () => { + const response = await POST( + request({ credential: 'credential-1', workflowId: 'workflow-1' }), + {} + ) + + expect(response.status).toBe(400) + expect(mockAuthorizeCredentialUse).not.toHaveBeenCalled() + expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('authorizes the exact credential before resolving or refreshing it', async () => { + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(200) + expect(mockAuthorizeCredentialUse).toHaveBeenCalledWith(expect.any(NextRequest), { + credentialId: 'credential-1', + workflowId: 'workflow-1', + callerUserId: 'caller-1', + }) + expect(mockGetCredential).toHaveBeenCalledWith(expect.any(String), 'account-1', 'owner-1') + expect(mockRefreshAccessTokenIfNeeded).toHaveBeenCalledWith( + 'account-1', + 'owner-1', + expect.any(String) + ) + }) + + it('fails closed when credential authorization is denied', async () => { + mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: false, error: 'Forbidden' }) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(403) + expect(mockGetCredential).not.toHaveBeenCalled() + expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('does not send a credential for another provider to Bitbucket', async () => { + mockGetCredential.mockResolvedValueOnce({ providerId: 'gitlab' }) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(400) + expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('returns an auth-required response when token refresh cannot resolve a token', async () => { + mockRefreshAccessTokenIfNeeded.mockResolvedValueOnce(null) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(401) + expect(await json(response)).toMatchObject({ authRequired: true }) + expect(mockFetch).not.toHaveBeenCalled() + }) + + it.each([ + ['plain HTTP', 'http://api.bitbucket.org/2.0/repositories/acme-platform?page=2'], + [ + 'lookalike host', + 'https://api.bitbucket.org.evil.example/2.0/repositories/acme-platform?page=2', + ], + ['different workspace', 'https://api.bitbucket.org/2.0/repositories/other-team?page=2'], + ['wrong endpoint', 'https://api.bitbucket.org/2.0/user/workspaces?page=2'], + [ + 'embedded credentials', + 'https://attacker:secret@api.bitbucket.org/2.0/repositories/acme-platform?page=2', + ], + ])('rejects a %s cursor before resolving a bearer token', async (_label, cursor) => { + const response = await POST(request({ ...REQUEST_BODY, cursor }), {}) + + expect(response.status).toBe(400) + expect(mockAuthorizeCredentialUse).not.toHaveBeenCalled() + expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('normalizes a page and returns slug ids with UUID/full-name data, never the token', async () => { + mockFetch.mockResolvedValueOnce( + providerResponse({ + values: [ + { + uuid: '{repository-uuid}', + name: 'Payments API', + full_name: 'acme-platform/payments-api', + links: { html: { href: 'https://bitbucket.org/acme-platform/payments-api' } }, + }, + ], + next: SECOND_PAGE_URL, + }) + ) + + const response = await POST(request(REQUEST_BODY), {}) + const body = await json(response) + + expect(response.status).toBe(200) + expect(mockFetch).toHaveBeenCalledWith( + FIRST_PAGE_URL, + expect.objectContaining({ method: 'GET', redirect: 'error' }) + ) + const init = mockFetch.mock.calls[0]?.[1] as RequestInit + expect(new Headers(init.headers).get('Authorization')).toBe('Bearer server-only-token') + expect(body).toEqual({ + repositories: [ + { + slug: 'payments-api', + uuid: '{repository-uuid}', + name: 'Payments API', + fullName: 'acme-platform/payments-api', + }, + ], + nextCursor: SECOND_PAGE_URL, + }) + expect(JSON.stringify(body)).not.toContain('server-only-token') + }) + + it('uses a validated cursor for one page without dropping the workspace scope', async () => { + const response = await POST(request({ ...REQUEST_BODY, cursor: SECOND_PAGE_URL }), {}) + + expect(response.status).toBe(200) + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(mockFetch).toHaveBeenCalledWith(SECOND_PAGE_URL, expect.any(Object)) + }) + + it.each([ + ['missing values', {}], + ['non-array values', { values: { slug: 'payments-api' } }], + [ + 'malformed repository', + { + values: [ + { + slug: 'payments-api', + uuid: '{repository-uuid}', + name: 'Payments API', + }, + ], + }, + ], + [ + 'oversized page', + { + values: Array.from({ length: 101 }, (_, index) => ({ + slug: `repository-${index}`, + uuid: `{repository-${index}}`, + name: `Repository ${index}`, + full_name: `acme-platform/repository-${index}`, + })), + }, + ], + ])('fails closed on a %s provider response', async (_label, providerBody) => { + mockFetch.mockResolvedValueOnce(providerResponse(providerBody)) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(502) + expect(await json(response)).toEqual({ + error: 'Bitbucket returned an invalid repository response.', + }) + }) + + it('fails closed on invalid provider JSON', async () => { + mockFetch.mockResolvedValueOnce(new Response('{not-json', { status: 200 })) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(502) + }) + + it('rejects a provider next link that crosses the selected workspace', async () => { + mockFetch.mockResolvedValueOnce( + providerResponse({ + values: [], + next: 'https://api.bitbucket.org/2.0/repositories/other-team?page=2', + }) + ) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(502) + expect(await json(response)).toEqual({ + error: 'Bitbucket returned an invalid repository response.', + }) + }) +}) diff --git a/apps/sim/app/api/tools/bitbucket/repositories/route.ts b/apps/sim/app/api/tools/bitbucket/repositories/route.ts new file mode 100644 index 00000000000..6ebee6b655f --- /dev/null +++ b/apps/sim/app/api/tools/bitbucket/repositories/route.ts @@ -0,0 +1,168 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { + BITBUCKET_SELECTOR_PAGE_SIZE, + bitbucketRepositoriesSelectorContract, + bitbucketRepositoryProviderPageSchema, + isBitbucketRepositoriesCursor, +} from '@/lib/api/contracts/selectors/bitbucket' +import { parseRequest } from '@/lib/api/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getCredential, refreshAccessTokenIfNeeded } from '@/lib/oauth/credential-service' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('BitbucketRepositoriesAPI') +const BITBUCKET_PROVIDER_ID = 'bitbucket' +const BITBUCKET_REPOSITORIES_URL = 'https://api.bitbucket.org/2.0/repositories' +const SELECTOR_REQUEST_MAX_BYTES = 8 * 1024 +const PROVIDER_RESPONSE_MAX_BYTES = 1024 * 1024 + +function bitbucketFailureResponse(status: number): NextResponse { + if (status === 401) { + return NextResponse.json( + { + error: 'Bitbucket rejected this credential. Reconnect it and try again.', + authRequired: true, + }, + { status: 401 } + ) + } + if (status === 403) { + return NextResponse.json( + { error: 'Bitbucket denied access to repository discovery.' }, + { status: 403 } + ) + } + if (status === 429) { + return NextResponse.json( + { error: 'Bitbucket rate-limited repository discovery. Try again shortly.' }, + { status: 429 } + ) + } + return NextResponse.json({ error: 'Bitbucket repository discovery failed.' }, { status: 502 }) +} + +/** Lists one workspace-scoped page for the `bitbucket.repositories` selector. */ +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + const caller = await checkSessionOrInternalAuth(request, { requireWorkflowId: true }) + if (!caller.success || !caller.userId) { + return NextResponse.json({ error: caller.error || 'Authentication required' }, { status: 401 }) + } + + const parsed = await parseRequest( + bitbucketRepositoriesSelectorContract, + request, + {}, + { + maxBodyBytes: SELECTOR_REQUEST_MAX_BYTES, + } + ) + if (!parsed.success) return parsed.response + const { credential, workflowId, workspaceSlug, cursor } = parsed.data.body + + const authorization = await authorizeCredentialUse(request, { + credentialId: credential, + workflowId, + callerUserId: caller.userId, + }) + if (!authorization.ok || !authorization.credentialOwnerUserId) { + return NextResponse.json({ error: authorization.error || 'Unauthorized' }, { status: 403 }) + } + + const resolvedCredentialId = authorization.resolvedCredentialId ?? credential + const storedCredential = await getCredential( + requestId, + resolvedCredentialId, + authorization.credentialOwnerUserId + ) + if (!storedCredential || storedCredential.providerId !== BITBUCKET_PROVIDER_ID) { + return NextResponse.json({ error: 'Select a Bitbucket OAuth credential.' }, { status: 400 }) + } + + const accessToken = await refreshAccessTokenIfNeeded( + resolvedCredentialId, + authorization.credentialOwnerUserId, + requestId + ) + if (!accessToken) { + return NextResponse.json( + { error: 'Could not retrieve a Bitbucket access token.', authRequired: true }, + { status: 401 } + ) + } + + const providerUrl = + cursor ?? + `${BITBUCKET_REPOSITORIES_URL}/${encodeURIComponent(workspaceSlug)}?pagelen=${BITBUCKET_SELECTOR_PAGE_SIZE}` + + let response: Response + try { + response = await fetch(providerUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + redirect: 'error', + signal: request.signal, + }) + } catch (error) { + if (request.signal.aborted) throw error + logger.warn('Bitbucket repository request failed', { + workspaceSlug, + errorType: error instanceof Error ? error.name : 'unknown', + }) + return NextResponse.json({ error: 'Bitbucket repository discovery failed.' }, { status: 502 }) + } + + if (!response.ok) return bitbucketFailureResponse(response.status) + + let providerBody: unknown + try { + providerBody = await readResponseJsonWithLimit(response, { + label: 'Bitbucket repository response', + maxBytes: PROVIDER_RESPONSE_MAX_BYTES, + signal: request.signal, + }) + } catch (error) { + if (request.signal.aborted) throw error + logger.warn('Bitbucket repository response was not bounded JSON', { + workspaceSlug, + errorType: error instanceof Error ? error.name : 'unknown', + }) + return NextResponse.json( + { error: 'Bitbucket returned an invalid repository response.' }, + { status: 502 } + ) + } + + const page = bitbucketRepositoryProviderPageSchema.safeParse(providerBody) + if ( + !page.success || + (page.data.next && !isBitbucketRepositoriesCursor(page.data.next, workspaceSlug)) || + page.data.values.some((repository) => !repository.full_name.startsWith(`${workspaceSlug}/`)) + ) { + logger.warn('Bitbucket returned a malformed repository page', { workspaceSlug }) + return NextResponse.json( + { error: 'Bitbucket returned an invalid repository response.' }, + { status: 502 } + ) + } + + return NextResponse.json({ + repositories: page.data.values.map((repository) => ({ + slug: repository.slug, + uuid: repository.uuid, + name: repository.name, + fullName: repository.full_name, + })), + ...(page.data.next ? { nextCursor: page.data.next } : {}), + }) +}) diff --git a/apps/sim/app/api/tools/bitbucket/workspaces/route.test.ts b/apps/sim/app/api/tools/bitbucket/workspaces/route.test.ts new file mode 100644 index 00000000000..a565841d7d6 --- /dev/null +++ b/apps/sim/app/api/tools/bitbucket/workspaces/route.test.ts @@ -0,0 +1,284 @@ +/** + * @vitest-environment node + */ +import { NextRequest } from 'next/server' +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' + +const { + mockAuthorizeCredentialUse, + mockCheckSessionOrInternalAuth, + mockFetch, + mockGetCredential, + mockRefreshAccessTokenIfNeeded, +} = vi.hoisted(() => ({ + mockAuthorizeCredentialUse: vi.fn(), + mockCheckSessionOrInternalAuth: vi.fn(), + mockFetch: vi.fn(), + mockGetCredential: vi.fn(), + mockRefreshAccessTokenIfNeeded: vi.fn(), +})) + +vi.mock('@/lib/auth/credential-access', () => ({ + authorizeCredentialUse: mockAuthorizeCredentialUse, +})) +vi.mock('@/lib/auth/hybrid', () => ({ + checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth, +})) +vi.mock('@/lib/oauth/credential-service', () => ({ + getCredential: mockGetCredential, + refreshAccessTokenIfNeeded: mockRefreshAccessTokenIfNeeded, +})) + +import { POST } from '@/app/api/tools/bitbucket/workspaces/route' + +const URL = 'http://localhost:3000/api/tools/bitbucket/workspaces' +const FIRST_PAGE_URL = + 'https://api.bitbucket.org/2.0/user/workspaces?pagelen=100&fields=%2Bvalues.workspace.name' +const SECOND_PAGE_URL = 'https://api.bitbucket.org/2.0/user/workspaces?page=2&pagelen=100' +const REQUEST_BODY = { credential: 'credential-1', workflowId: 'workflow-1' } as const + +function request(body: unknown): NextRequest { + return new NextRequest(URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: typeof body === 'string' ? body : JSON.stringify(body), + }) +} + +function providerResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { 'content-type': 'application/json' }, + }) +} + +async function json(response: Response): Promise> { + return (await response.json()) as Record +} + +describe('POST /api/tools/bitbucket/workspaces', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('fetch', mockFetch) + mockCheckSessionOrInternalAuth.mockResolvedValue({ success: true, userId: 'caller-1' }) + mockAuthorizeCredentialUse.mockResolvedValue({ + ok: true, + credentialOwnerUserId: 'owner-1', + resolvedCredentialId: 'account-1', + credentialType: 'oauth', + }) + mockGetCredential.mockResolvedValue({ providerId: 'bitbucket' }) + mockRefreshAccessTokenIfNeeded.mockResolvedValue('server-only-token') + mockFetch.mockResolvedValue(providerResponse({ values: [] })) + }) + + afterAll(() => vi.unstubAllGlobals()) + + it('authenticates before attempting to parse an invalid body', async () => { + mockCheckSessionOrInternalAuth.mockResolvedValueOnce({ + success: false, + error: 'Authentication required', + }) + + const response = await POST(request('{not-json'), {}) + + expect(response.status).toBe(401) + expect(mockCheckSessionOrInternalAuth).toHaveBeenCalledWith(expect.any(NextRequest), { + requireWorkflowId: true, + }) + expect(mockAuthorizeCredentialUse).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('authorizes the exact credential before resolving or refreshing it', async () => { + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(200) + expect(mockAuthorizeCredentialUse).toHaveBeenCalledWith(expect.any(NextRequest), { + credentialId: 'credential-1', + workflowId: 'workflow-1', + callerUserId: 'caller-1', + }) + expect(mockGetCredential).toHaveBeenCalledWith(expect.any(String), 'account-1', 'owner-1') + expect(mockRefreshAccessTokenIfNeeded).toHaveBeenCalledWith( + 'account-1', + 'owner-1', + expect.any(String) + ) + }) + + it('fails closed when credential authorization is denied', async () => { + mockAuthorizeCredentialUse.mockResolvedValueOnce({ ok: false, error: 'Forbidden' }) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(403) + expect(await json(response)).toMatchObject({ error: 'Forbidden' }) + expect(mockGetCredential).not.toHaveBeenCalled() + expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('does not send a credential for another provider to Bitbucket', async () => { + mockGetCredential.mockResolvedValueOnce({ providerId: 'github' }) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(400) + expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('returns an auth-required response when token refresh cannot resolve a token', async () => { + mockRefreshAccessTokenIfNeeded.mockResolvedValueOnce(null) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(401) + expect(await json(response)).toMatchObject({ authRequired: true }) + expect(mockFetch).not.toHaveBeenCalled() + }) + + it.each([ + ['plain HTTP', 'http://api.bitbucket.org/2.0/user/workspaces?page=2'], + ['lookalike host', 'https://api.bitbucket.org.evil.example/2.0/user/workspaces?page=2'], + ['non-default port', 'https://api.bitbucket.org:444/2.0/user/workspaces?page=2'], + ['wrong v2 endpoint', 'https://api.bitbucket.org/2.0/repositories/acme?page=2'], + ['embedded credentials', 'https://attacker:secret@api.bitbucket.org/2.0/user/workspaces'], + ])('rejects a %s cursor before resolving a bearer token', async (_label, cursor) => { + const response = await POST(request({ ...REQUEST_BODY, cursor }), {}) + + expect(response.status).toBe(400) + expect(mockAuthorizeCredentialUse).not.toHaveBeenCalled() + expect(mockRefreshAccessTokenIfNeeded).not.toHaveBeenCalled() + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('normalizes a page and returns only the provider cursor, never the bearer token', async () => { + mockFetch.mockResolvedValueOnce( + providerResponse({ + values: [ + { + administrator: true, + workspace: { + slug: 'acme-platform', + uuid: '{workspace-uuid}', + name: 'Acme Platform', + links: { html: { href: 'https://bitbucket.org/acme-platform' } }, + }, + }, + ], + next: SECOND_PAGE_URL, + }) + ) + + const response = await POST(request(REQUEST_BODY), {}) + const body = await json(response) + + expect(response.status).toBe(200) + expect(mockFetch).toHaveBeenCalledWith( + FIRST_PAGE_URL, + expect.objectContaining({ method: 'GET', redirect: 'error' }) + ) + const init = mockFetch.mock.calls[0]?.[1] as RequestInit + expect(new Headers(init.headers).get('Authorization')).toBe('Bearer server-only-token') + expect(body).toEqual({ + workspaces: [ + { + slug: 'acme-platform', + uuid: '{workspace-uuid}', + name: 'Acme Platform', + administrator: true, + }, + ], + nextCursor: SECOND_PAGE_URL, + }) + expect(JSON.stringify(body)).not.toContain('server-only-token') + }) + + it('uses a validated provider cursor for exactly one progressive page', async () => { + const response = await POST(request({ ...REQUEST_BODY, cursor: SECOND_PAGE_URL }), {}) + + expect(response.status).toBe(200) + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(mockFetch).toHaveBeenCalledWith(SECOND_PAGE_URL, expect.any(Object)) + }) + + it('uses the slug when the current workspace-access shape omits a display name', async () => { + mockFetch.mockResolvedValueOnce( + providerResponse({ + values: [ + { + administrator: false, + workspace: { slug: 'acme-platform', uuid: '{workspace-uuid}' }, + }, + ], + }) + ) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(200) + expect(await json(response)).toEqual({ + workspaces: [ + { + slug: 'acme-platform', + uuid: '{workspace-uuid}', + name: 'acme-platform', + administrator: false, + }, + ], + }) + }) + + it.each([ + ['missing values', {}], + ['non-array values', { values: 'not-an-array' }], + ['malformed workspace', { values: [{ workspace: { slug: 'acme', uuid: 42, name: 'Acme' } }] }], + [ + 'oversized page', + { + values: Array.from({ length: 101 }, (_, index) => ({ + workspace: { + slug: `workspace-${index}`, + uuid: `{workspace-${index}}`, + name: `Workspace ${index}`, + }, + })), + }, + ], + ])('fails closed on a %s provider response', async (_label, providerBody) => { + mockFetch.mockResolvedValueOnce(providerResponse(providerBody)) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(502) + expect(await json(response)).toEqual({ + error: 'Bitbucket returned an invalid workspace response.', + }) + }) + + it('fails closed on invalid provider JSON', async () => { + mockFetch.mockResolvedValueOnce(new Response('{not-json', { status: 200 })) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(502) + }) + + it('rejects a provider next link that could redirect the bearer token elsewhere', async () => { + mockFetch.mockResolvedValueOnce( + providerResponse({ + values: [], + next: 'https://evil.example/2.0/user/workspaces?page=2', + }) + ) + + const response = await POST(request(REQUEST_BODY), {}) + + expect(response.status).toBe(502) + expect(await json(response)).toEqual({ + error: 'Bitbucket returned an invalid workspace response.', + }) + }) +}) diff --git a/apps/sim/app/api/tools/bitbucket/workspaces/route.ts b/apps/sim/app/api/tools/bitbucket/workspaces/route.ts new file mode 100644 index 00000000000..af6433d3191 --- /dev/null +++ b/apps/sim/app/api/tools/bitbucket/workspaces/route.ts @@ -0,0 +1,168 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { + BITBUCKET_SELECTOR_PAGE_SIZE, + bitbucketWorkspaceProviderPageSchema, + bitbucketWorkspacesSelectorContract, + isBitbucketWorkspacesCursor, +} from '@/lib/api/contracts/selectors/bitbucket' +import { parseRequest } from '@/lib/api/server' +import { authorizeCredentialUse } from '@/lib/auth/credential-access' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' +import { withRouteHandler } from '@/lib/core/utils/with-route-handler' +import { getCredential, refreshAccessTokenIfNeeded } from '@/lib/oauth/credential-service' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('BitbucketWorkspacesAPI') +const BITBUCKET_PROVIDER_ID = 'bitbucket' +const BITBUCKET_WORKSPACES_URL = 'https://api.bitbucket.org/2.0/user/workspaces' +const BITBUCKET_WORKSPACE_FIELDS = '+values.workspace.name' +const SELECTOR_REQUEST_MAX_BYTES = 8 * 1024 +const PROVIDER_RESPONSE_MAX_BYTES = 1024 * 1024 + +function bitbucketFailureResponse(status: number): NextResponse { + if (status === 401) { + return NextResponse.json( + { + error: 'Bitbucket rejected this credential. Reconnect it and try again.', + authRequired: true, + }, + { status: 401 } + ) + } + if (status === 403) { + return NextResponse.json( + { error: 'Bitbucket denied access to workspace discovery.' }, + { status: 403 } + ) + } + if (status === 429) { + return NextResponse.json( + { error: 'Bitbucket rate-limited workspace discovery. Try again shortly.' }, + { status: 429 } + ) + } + return NextResponse.json({ error: 'Bitbucket workspace discovery failed.' }, { status: 502 }) +} + +/** Lists one normalized page for the `bitbucket.workspaces` selector. */ +export const POST = withRouteHandler(async (request: NextRequest) => { + const requestId = generateRequestId() + + const caller = await checkSessionOrInternalAuth(request, { requireWorkflowId: true }) + if (!caller.success || !caller.userId) { + return NextResponse.json({ error: caller.error || 'Authentication required' }, { status: 401 }) + } + + const parsed = await parseRequest( + bitbucketWorkspacesSelectorContract, + request, + {}, + { + maxBodyBytes: SELECTOR_REQUEST_MAX_BYTES, + } + ) + if (!parsed.success) return parsed.response + const { credential, workflowId, cursor } = parsed.data.body + + const authorization = await authorizeCredentialUse(request, { + credentialId: credential, + workflowId, + callerUserId: caller.userId, + }) + if (!authorization.ok || !authorization.credentialOwnerUserId) { + return NextResponse.json({ error: authorization.error || 'Unauthorized' }, { status: 403 }) + } + + const resolvedCredentialId = authorization.resolvedCredentialId ?? credential + const storedCredential = await getCredential( + requestId, + resolvedCredentialId, + authorization.credentialOwnerUserId + ) + if (!storedCredential || storedCredential.providerId !== BITBUCKET_PROVIDER_ID) { + return NextResponse.json({ error: 'Select a Bitbucket OAuth credential.' }, { status: 400 }) + } + + const accessToken = await refreshAccessTokenIfNeeded( + resolvedCredentialId, + authorization.credentialOwnerUserId, + requestId + ) + if (!accessToken) { + return NextResponse.json( + { error: 'Could not retrieve a Bitbucket access token.', authRequired: true }, + { status: 401 } + ) + } + + const firstPage = new URL(BITBUCKET_WORKSPACES_URL) + firstPage.searchParams.set('pagelen', String(BITBUCKET_SELECTOR_PAGE_SIZE)) + /** + * The current endpoint returns a `workspace_base` by default, whose documented + * sample omits `name`; additive fields keep the default shape and request it. + */ + firstPage.searchParams.set('fields', BITBUCKET_WORKSPACE_FIELDS) + const providerUrl = cursor ?? firstPage.toString() + + let response: Response + try { + response = await fetch(providerUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + redirect: 'error', + signal: request.signal, + }) + } catch (error) { + if (request.signal.aborted) throw error + logger.warn('Bitbucket workspace request failed', { + errorType: error instanceof Error ? error.name : 'unknown', + }) + return NextResponse.json({ error: 'Bitbucket workspace discovery failed.' }, { status: 502 }) + } + + if (!response.ok) return bitbucketFailureResponse(response.status) + + let providerBody: unknown + try { + providerBody = await readResponseJsonWithLimit(response, { + label: 'Bitbucket workspace response', + maxBytes: PROVIDER_RESPONSE_MAX_BYTES, + signal: request.signal, + }) + } catch (error) { + if (request.signal.aborted) throw error + logger.warn('Bitbucket workspace response was not bounded JSON', { + errorType: error instanceof Error ? error.name : 'unknown', + }) + return NextResponse.json( + { error: 'Bitbucket returned an invalid workspace response.' }, + { status: 502 } + ) + } + + const page = bitbucketWorkspaceProviderPageSchema.safeParse(providerBody) + if (!page.success || (page.data.next && !isBitbucketWorkspacesCursor(page.data.next))) { + logger.warn('Bitbucket returned a malformed workspace page') + return NextResponse.json( + { error: 'Bitbucket returned an invalid workspace response.' }, + { status: 502 } + ) + } + + return NextResponse.json({ + workspaces: page.data.values.map(({ administrator, workspace }) => ({ + slug: workspace.slug, + uuid: workspace.uuid, + name: workspace.name ?? workspace.slug, + administrator, + })), + ...(page.data.next ? { nextCursor: page.data.next } : {}), + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx index 6e67c2da29f..d18cbf39970 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/connect-oauth-modal/connect-oauth-modal.tsx @@ -416,7 +416,7 @@ export function ConnectOAuthModal(props: ConnectOAuthModalProps) { {displayScopes.map((scope) => ( - {getScopeDescription(scope)} + {getScopeDescription(scope, providerId)} {!isConnect && newScopesSet.has(scope) && ( New diff --git a/apps/sim/blocks/blocks/bitbucket.test.ts b/apps/sim/blocks/blocks/bitbucket.test.ts new file mode 100644 index 00000000000..ed18f8399f3 --- /dev/null +++ b/apps/sim/blocks/blocks/bitbucket.test.ts @@ -0,0 +1,374 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { BitbucketBlock, BitbucketBlockMeta } from '@/blocks/blocks/bitbucket' +import { tools as toolRegistry } from '@/tools/registry' + +vi.unmock('@/tools/registry') + +const SOURCE_COMMIT_SHA = 'a'.repeat(40) +const PIPELINE_COMMIT_SHA = 'b'.repeat(40) +const TARGET_COMMIT_SHA = 'c'.repeat(40) + +const EXPECTED_TOOL_IDS = [ + 'bitbucket_list_workspaces', + 'bitbucket_list_repositories', + 'bitbucket_get_repository', + 'bitbucket_list_branches', + 'bitbucket_create_branch', + 'bitbucket_delete_branch', + 'bitbucket_list_commits', + 'bitbucket_get_commit', + 'bitbucket_list_directory', + 'bitbucket_get_file_metadata', + 'bitbucket_get_file', + 'bitbucket_list_pull_requests', + 'bitbucket_get_pull_request', + 'bitbucket_create_pull_request', + 'bitbucket_merge_pull_request', + 'bitbucket_get_pull_request_merge_task_status', + 'bitbucket_decline_pull_request', + 'bitbucket_approve_pull_request', + 'bitbucket_request_pull_request_changes', + 'bitbucket_get_pull_request_diff', + 'bitbucket_get_pull_request_diffstat', + 'bitbucket_list_pull_request_comments', + 'bitbucket_create_pull_request_comment', + 'bitbucket_list_pull_request_commit_statuses', + 'bitbucket_list_pipelines', + 'bitbucket_get_pipeline', + 'bitbucket_trigger_pipeline', + 'bitbucket_stop_pipeline', + 'bitbucket_list_pipeline_steps', + 'bitbucket_get_pipeline_step_log', +] as const + +const SAMPLE_VALUES: Record = { + oauthCredential: 'credential-1', + workspaceSlug: 'acme', + repoSlug: 'platform', + pageLen: '25', + nextUrl: 'https://api.bitbucket.org/2.0/example?page=2', + query: 'state = "OPEN"', + sort: '-updated_on', + administrator: 'true', + role: 'contributor', + branchName: 'feature/review', + target: SOURCE_COMMIT_SHA, + revision: SOURCE_COMMIT_SHA, + path: 'src/index.ts', + state: 'OPEN', + prId: '42', + title: 'Improve pipeline diagnostics', + sourceBranch: 'feature/review', + destinationBranch: 'main', + description: 'Adds bounded log diagnostics.', + reviewerAccountIds: '{reviewer-a}, {reviewer-b}', + closeSourceBranch: 'true', + draft: 'true', + mergeStrategy: 'squash', + message: 'Merge pull request 42', + taskId: 'task-1', + content: 'Looks good after the test fix.', + parentId: '7', + pipelineRefType: 'BRANCH', + pipelineRefName: 'main', + pipelineCommitHash: PIPELINE_COMMIT_SHA, + pipelineSelectorType: 'BRANCH', + pipelineSelectorPattern: 'main', + pipelineTriggerType: 'MANUAL', + pipelineStatus: 'FAILED', + pipelineUuid: '{pipeline-uuid}', + targetRef: 'main', + targetCommitHash: TARGET_COMMIT_SHA, + stepUuid: '{step-uuid}', + maxCharacters: '65536', +} + +type BitbucketOperation = (typeof EXPECTED_TOOL_IDS)[number] + +function mapBlockParams( + operation: BitbucketOperation, + overrides: Record = {} +): Record { + const mapper = BitbucketBlock.tools.config?.params + if (!mapper) throw new Error('Bitbucket block is missing tools.config.params') + return mapper({ operation, ...SAMPLE_VALUES, ...overrides }) as Record +} + +function buildRequestUrl( + operation: BitbucketOperation, + overrides: Record = {} +): string { + const toolId = BitbucketBlock.tools.config?.tool({ operation }) + if (!toolId) throw new Error(`Bitbucket operation ${operation} did not resolve to a tool`) + const url = toolRegistry[toolId].request.url + if (typeof url !== 'function') return url + return url({ ...mapBlockParams(operation, overrides), accessToken: 'oauth-token' } as never) +} + +function buildRequestBody( + operation: BitbucketOperation, + overrides: Record = {} +): unknown { + const toolId = BitbucketBlock.tools.config?.tool({ operation }) + if (!toolId) throw new Error(`Bitbucket operation ${operation} did not resolve to a tool`) + const body = toolRegistry[toolId].request.body + if (!body) throw new Error(`Bitbucket tool ${toolId} is missing request.body`) + return body({ ...mapBlockParams(operation, overrides), accessToken: 'oauth-token' } as never) +} + +describe('BitbucketBlock', () => { + it('exposes every Bitbucket action one-to-one through the operation dropdown', () => { + const operation = BitbucketBlock.subBlocks.find((subBlock) => subBlock.id === 'operation') + const options = + typeof operation?.options === 'function' ? operation.options() : operation?.options + + expect(options?.map((option) => option.id).sort()).toEqual([...EXPECTED_TOOL_IDS].sort()) + expect([...BitbucketBlock.tools.access].sort()).toEqual([...EXPECTED_TOOL_IDS].sort()) + expect(new Set(options?.map((option) => option.id)).size).toBe(EXPECTED_TOOL_IDS.length) + }) + + it.each(EXPECTED_TOOL_IDS)('%s resolves to a registered tool', (operation) => { + const toolId = BitbucketBlock.tools.config?.tool({ operation }) + expect(toolId).toBe(operation) + expect(toolRegistry[toolId]).toBeDefined() + }) + + it.each(EXPECTED_TOOL_IDS)('%s supplies every required user-facing tool param', (operation) => { + const toolId = BitbucketBlock.tools.config?.tool({ operation }) as string + const mapped = BitbucketBlock.tools.config?.params?.({ operation, ...SAMPLE_VALUES }) ?? {} + const required = Object.entries(toolRegistry[toolId].params ?? {}) + .filter(([id, config]) => config.required && id !== 'accessToken') + .map(([id]) => id) + + expect(required.filter((id) => mapped[id] === undefined || mapped[id] === '')).toEqual([]) + }) + + it.each(EXPECTED_TOOL_IDS)('%s maps every non-hidden tool parameter', (operation) => { + const toolId = BitbucketBlock.tools.config?.tool({ operation }) as string + const mapped = BitbucketBlock.tools.config?.params?.({ operation, ...SAMPLE_VALUES }) ?? {} + const exposed = Object.entries(toolRegistry[toolId].params ?? {}) + .filter(([id, config]) => id !== 'accessToken' && config.visibility !== 'hidden') + .map(([id]) => id) + + expect(exposed.filter((id) => mapped[id] === undefined)).toEqual([]) + }) + + it.each(EXPECTED_TOOL_IDS)('%s does not emit unsupported execution params', (operation) => { + const toolId = BitbucketBlock.tools.config?.tool({ operation }) as string + const mapped = BitbucketBlock.tools.config?.params?.({ operation, ...SAMPLE_VALUES }) ?? {} + const accepted = new Set([...Object.keys(toolRegistry[toolId].params ?? {}), 'oauthCredential']) + + expect(Object.keys(mapped).filter((key) => !accepted.has(key))).toEqual([]) + }) + + it.each(EXPECTED_TOOL_IDS)('%s exposes every tool output through the block', (operation) => { + const toolOutputs = Object.keys(toolRegistry[operation].outputs ?? {}) + const blockOutputs = new Set(Object.keys(BitbucketBlock.outputs)) + + expect(toolOutputs.filter((key) => !blockOutputs.has(key))).toEqual([]) + }) + + it('uses canonical picker/manual pairs without colliding with visual subblock IDs', () => { + const expectedPairs = { + oauthCredential: ['accountPicker', 'credentialIdInput'], + workspaceSlug: ['workspacePicker', 'workspaceSlugInput'], + repoSlug: ['repositoryPicker', 'repositorySlugInput'], + } + + for (const [canonicalParamId, ids] of Object.entries(expectedPairs)) { + const members = BitbucketBlock.subBlocks.filter( + (subBlock) => subBlock.canonicalParamId === canonicalParamId + ) + expect(members.map((member) => member.id).sort()).toEqual([...ids].sort()) + expect(members.filter((member) => member.mode === 'basic')).toHaveLength(1) + expect(members.filter((member) => member.mode === 'advanced')).toHaveLength(1) + expect(members[0].required).toEqual(members[1].required) + expect(members[0].condition).toEqual(members[1].condition) + expect(BitbucketBlock.subBlocks.some((member) => member.id === canonicalParamId)).toBe(false) + } + + expect( + BitbucketBlock.subBlocks.find((member) => member.id === 'workspacePicker')?.dependsOn + ).toEqual(['accountPicker']) + expect( + BitbucketBlock.subBlocks.find((member) => member.id === 'workspaceSlugInput')?.dependsOn + ).toEqual(['credentialIdInput']) + expect( + BitbucketBlock.subBlocks.find((member) => member.id === 'repositoryPicker')?.dependsOn + ).toEqual(['accountPicker', 'workspacePicker']) + expect( + BitbucketBlock.subBlocks.find((member) => member.id === 'repositorySlugInput')?.dependsOn + ).toEqual(['credentialIdInput', 'workspaceSlugInput']) + }) + + it('coerces execution values and fixes pipeline triggering to a branch ref target', () => { + expect( + BitbucketBlock.tools.config?.params?.({ + operation: 'bitbucket_create_pull_request', + ...SAMPLE_VALUES, + }) + ).toMatchObject({ + reviewerUuids: ['{reviewer-a}', '{reviewer-b}'], + closeSourceBranch: true, + }) + + expect( + BitbucketBlock.tools.config?.params?.({ + operation: 'bitbucket_trigger_pipeline', + ...SAMPLE_VALUES, + }) + ).toMatchObject({ refType: 'branch', refName: 'main' }) + + expect( + BitbucketBlock.tools.config?.params?.({ + operation: 'bitbucket_list_repositories', + ...SAMPLE_VALUES, + }) + ).toMatchObject({ pageLen: 25 }) + + expect( + BitbucketBlock.tools.config?.params?.({ + operation: 'bitbucket_get_pull_request_diff', + ...SAMPLE_VALUES, + }) + ).toMatchObject({ prId: 42, path: 'src/index.ts', maxCharacters: 65536 }) + + expect( + BitbucketBlock.tools.config?.params?.({ + operation: 'bitbucket_get_file_metadata', + ...SAMPLE_VALUES, + }) + ).toMatchObject({ commit: SOURCE_COMMIT_SHA, path: 'src/index.ts' }) + + expect( + BitbucketBlock.tools.config?.params?.({ + operation: 'bitbucket_get_pull_request_merge_task_status', + ...SAMPLE_VALUES, + }) + ).toMatchObject({ prId: 42, taskId: 'task-1' }) + }) + + it.each([undefined, null, '', ' '])( + 'treats %p as omission across optional execution value kinds', + (value) => { + expect( + mapBlockParams('bitbucket_create_pull_request_comment', { parentId: value }).parentId + ).toBeUndefined() + expect( + mapBlockParams('bitbucket_create_pull_request', { draft: value }).draft + ).toBeUndefined() + expect( + mapBlockParams('bitbucket_create_pull_request', { description: value }).description + ).toBeUndefined() + expect(mapBlockParams('bitbucket_list_branches', { query: value }).q).toBeUndefined() + expect( + mapBlockParams('bitbucket_create_pull_request', { reviewerAccountIds: value }).reviewerUuids + ).toBeUndefined() + } + ) + + it('coerces canonical positive integer strings and reaches the real tool URL', () => { + expect(buildRequestUrl('bitbucket_decline_pull_request', { prId: '42' })).toMatch( + /\/pullrequests\/42\/decline$/ + ) + }) + + it.each([ + true, + false, + [7], + { value: 7 }, + 0, + 1.5, + Number.MAX_SAFE_INTEGER + 1, + 'false', + ' 42 ', + '01', + '7.0', + '1e2', + '-1', + ])('rejects malformed PR ids before a mutation request is built: %p', (prId) => { + expect(() => buildRequestUrl('bitbucket_decline_pull_request', { prId })).toThrow(/prId/) + }) + + it('does not turn a malformed optional parent ID into a top-level comment', () => { + expect(() => + buildRequestBody('bitbucket_create_pull_request_comment', { parentId: { id: 7 } }) + ).toThrow(/parentId/) + }) + + it('preserves explicit false booleans and rejects other present boolean shapes', () => { + expect( + buildRequestBody('bitbucket_create_pull_request', { + closeSourceBranch: false, + draft: 'false', + }) + ).toMatchObject({ close_source_branch: false, draft: false }) + + for (const draft of [0, 1, 'yes', ' false ', [], {}]) { + expect( + () => mapBlockParams('bitbucket_create_pull_request', { draft }), + String(draft) + ).toThrow(/draft/) + } + }) + + it('validates reviewer lists atomically instead of filtering malformed elements', () => { + expect( + mapBlockParams('bitbucket_create_pull_request', { + reviewerAccountIds: [' {reviewer-a} ', '{reviewer-b}'], + }).reviewerUuids + ).toEqual(['{reviewer-a}', '{reviewer-b}']) + expect( + mapBlockParams('bitbucket_create_pull_request', { reviewerAccountIds: [] }).reviewerUuids + ).toEqual([]) + + for (const reviewerAccountIds of [ + ['{reviewer-a}', 7], + ['{reviewer-a}', ' '], + '{reviewer-a},, {reviewer-b}', + { uuid: '{reviewer-a}' }, + ]) { + expect( + () => mapBlockParams('bitbucket_create_pull_request', { reviewerAccountIds }), + JSON.stringify(reviewerAccountIds) + ).toThrow(/reviewerAccountIds/) + } + }) + + it('rejects malformed present string values instead of silently omitting them', () => { + expect(() => mapBlockParams('bitbucket_list_branches', { query: false })).toThrow(/query/) + expect(() => mapBlockParams('bitbucket_create_pull_request', { description: {} })).toThrow( + /description/ + ) + }) + + it('preserves meaningful whitespace in repository paths and authored text', () => { + expect( + BitbucketBlock.tools.config?.params?.({ + operation: 'bitbucket_get_file', + ...SAMPLE_VALUES, + path: ' docs/release notes.md ', + }) + ).toMatchObject({ path: ' docs/release notes.md ' }) + + expect( + BitbucketBlock.tools.config?.params?.({ + operation: 'bitbucket_create_pull_request_comment', + ...SAMPLE_VALUES, + content: ' indented Markdown\n', + }) + ).toMatchObject({ content: ' indented Markdown\n' }) + }) + + it('covers every action with a canvas sentence and declares no trigger support', () => { + const sentences = BitbucketBlock.canvasPresentation?.sentences?.byOperation ?? {} + expect(Object.keys(sentences).sort()).toEqual([...EXPECTED_TOOL_IDS].sort()) + expect(BitbucketBlock.triggerAllowed).toBeUndefined() + expect(BitbucketBlock.triggers).toBeUndefined() + expect(BitbucketBlockMeta.tags).not.toContain('webhooks') + }) +}) diff --git a/apps/sim/blocks/blocks/bitbucket.ts b/apps/sim/blocks/blocks/bitbucket.ts new file mode 100644 index 00000000000..2129d7171b1 --- /dev/null +++ b/apps/sim/blocks/blocks/bitbucket.ts @@ -0,0 +1,1224 @@ +import { BitbucketIcon } from '@/components/icons' +import { getScopesForService } from '@/lib/oauth/utils' +import type { BlockConfig, BlockMeta } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' + +const WORKSPACE_FIELD = ['workspacePicker', 'workspaceSlugInput'] as const +const REPOSITORY_FIELD = ['repositoryPicker', 'repositorySlugInput'] as const + +const OPERATIONS = [ + 'bitbucket_list_workspaces', + 'bitbucket_list_repositories', + 'bitbucket_get_repository', + 'bitbucket_list_branches', + 'bitbucket_create_branch', + 'bitbucket_delete_branch', + 'bitbucket_list_commits', + 'bitbucket_get_commit', + 'bitbucket_list_directory', + 'bitbucket_get_file_metadata', + 'bitbucket_get_file', + 'bitbucket_list_pull_requests', + 'bitbucket_get_pull_request', + 'bitbucket_create_pull_request', + 'bitbucket_merge_pull_request', + 'bitbucket_get_pull_request_merge_task_status', + 'bitbucket_decline_pull_request', + 'bitbucket_approve_pull_request', + 'bitbucket_request_pull_request_changes', + 'bitbucket_get_pull_request_diff', + 'bitbucket_get_pull_request_diffstat', + 'bitbucket_list_pull_request_comments', + 'bitbucket_create_pull_request_comment', + 'bitbucket_list_pull_request_commit_statuses', + 'bitbucket_list_pipelines', + 'bitbucket_get_pipeline', + 'bitbucket_trigger_pipeline', + 'bitbucket_stop_pipeline', + 'bitbucket_list_pipeline_steps', + 'bitbucket_get_pipeline_step_log', +] as const + +type BitbucketOperation = (typeof OPERATIONS)[number] + +const PAGINATED_OPERATIONS: BitbucketOperation[] = [ + 'bitbucket_list_workspaces', + 'bitbucket_list_repositories', + 'bitbucket_list_branches', + 'bitbucket_list_commits', + 'bitbucket_list_directory', + 'bitbucket_list_pull_requests', + 'bitbucket_get_pull_request_diffstat', + 'bitbucket_list_pull_request_comments', + 'bitbucket_list_pull_request_commit_statuses', + 'bitbucket_list_pipelines', + 'bitbucket_list_pipeline_steps', +] +const PULL_REQUEST_ID_OPERATIONS: BitbucketOperation[] = [ + 'bitbucket_get_pull_request', + 'bitbucket_merge_pull_request', + 'bitbucket_get_pull_request_merge_task_status', + 'bitbucket_decline_pull_request', + 'bitbucket_approve_pull_request', + 'bitbucket_request_pull_request_changes', + 'bitbucket_get_pull_request_diff', + 'bitbucket_get_pull_request_diffstat', + 'bitbucket_list_pull_request_comments', + 'bitbucket_create_pull_request_comment', + 'bitbucket_list_pull_request_commit_statuses', +] +const PIPELINE_ID_OPERATIONS: BitbucketOperation[] = [ + 'bitbucket_get_pipeline', + 'bitbucket_stop_pipeline', + 'bitbucket_list_pipeline_steps', + 'bitbucket_get_pipeline_step_log', +] + +function isOmittedValue(value: unknown): boolean { + return value === undefined || value === null || (typeof value === 'string' && !value.trim()) +} + +function optionalString(value: unknown, name: string): string | undefined { + if (isOmittedValue(value)) return undefined + if (typeof value !== 'string') throw new Error(`${name} must be a string`) + return value.trim() +} + +function optionalText(value: unknown, name: string): string | undefined { + if (isOmittedValue(value)) return undefined + if (typeof value !== 'string') throw new Error(`${name} must be a string`) + return value +} + +function optionalInteger(value: unknown, name: string): number | undefined { + if (isOmittedValue(value)) return undefined + + let parsed: number + if (typeof value === 'number') { + parsed = value + } else if (typeof value === 'string') { + if (!/^[1-9]\d*$/.test(value)) { + throw new Error(`${name} must be a positive integer`) + } + parsed = Number(value) + } else { + throw new Error(`${name} must be a positive integer`) + } + + if (!Number.isSafeInteger(parsed) || parsed < 1) { + throw new Error(`${name} must be a positive safe integer`) + } + return parsed +} + +function optionalBoolean(value: unknown, name: string): boolean | undefined { + if (isOmittedValue(value)) return undefined + if (value === true || value === 'true') return true + if (value === false || value === 'false') return false + throw new Error(`${name} must be a boolean or the string "true" or "false"`) +} + +function stringList(value: unknown, name: string): string[] | undefined { + if (isOmittedValue(value)) return undefined + + if (Array.isArray(value)) { + if (!value.every((item) => typeof item === 'string' && item.trim().length > 0)) { + throw new Error(`${name} must contain only non-empty strings`) + } + return value.map((item) => item.trim()) + } + if (typeof value !== 'string') { + throw new Error(`${name} must be an array of strings or a comma-separated string`) + } + const values = value.split(',').map((item) => item.trim()) + if (values.some((item) => item.length === 0)) { + throw new Error(`${name} must contain only non-empty strings`) + } + return values +} + +function isBitbucketOperation(value: unknown): value is BitbucketOperation { + return typeof value === 'string' && (OPERATIONS as readonly string[]).includes(value) +} + +export const BitbucketBlock: BlockConfig = { + type: 'bitbucket', + name: 'Bitbucket', + description: 'Work with Bitbucket Cloud repositories, pull requests, and pipelines', + longDescription: + 'Connect Bitbucket Cloud to inspect repositories and source, collaborate on pull requests, and diagnose or control pipelines. This action integration uses OAuth and does not create webhooks or triggers.', + docsLink: 'https://docs.sim.ai/integrations/bitbucket', + category: 'tools', + integrationType: IntegrationType.DevOps, + authMode: AuthMode.OAuth, + bgColor: '#FFFFFF', + iconColor: '#2684FF', + icon: BitbucketIcon, + canvasPresentation: { + defaultTitle: 'Bitbucket', + operationRowTitle: 'Action', + sentences: { + byOperation: { + bitbucket_list_workspaces: ['List workspaces'], + bitbucket_list_repositories: [ + { text: 'List repositories in', field: WORKSPACE_FIELD, core: true }, + ], + bitbucket_get_repository: [ + { text: 'Read repository', field: REPOSITORY_FIELD, core: true }, + ], + bitbucket_list_branches: [ + { text: 'List branches in', field: REPOSITORY_FIELD, core: true }, + ], + bitbucket_create_branch: [ + { text: 'Create branch', field: 'branchName', core: true }, + { text: 'from', field: 'target' }, + { text: 'in', field: REPOSITORY_FIELD }, + ], + bitbucket_delete_branch: [ + { text: 'Delete branch', field: 'branchName', core: true }, + { text: 'from', field: REPOSITORY_FIELD }, + ], + bitbucket_list_commits: [{ text: 'List commits in', field: REPOSITORY_FIELD, core: true }], + bitbucket_get_commit: [ + { text: 'Read commit', field: 'revision', core: true }, + { text: 'in', field: REPOSITORY_FIELD }, + ], + bitbucket_list_directory: [ + { text: 'List directory at', field: 'revision', core: true }, + { text: ', under', field: 'path' }, + { text: 'in', field: REPOSITORY_FIELD }, + ], + bitbucket_get_file_metadata: [ + { text: 'Inspect file', field: 'path', core: true }, + { text: 'at', field: 'revision' }, + ], + bitbucket_get_file: [ + { text: 'Read file', field: 'path', core: true }, + { text: 'at', field: 'revision' }, + ], + bitbucket_list_pull_requests: [ + { text: 'List pull requests in', field: REPOSITORY_FIELD, core: true }, + { text: ', with state', field: 'state' }, + ], + bitbucket_get_pull_request: [ + { text: 'Read pull request', field: 'prId', core: true }, + { text: 'in', field: REPOSITORY_FIELD }, + ], + bitbucket_create_pull_request: [ + { text: 'Create pull request', field: 'title', core: true }, + { text: 'from', field: 'sourceBranch' }, + { text: 'into', field: 'destinationBranch' }, + ], + bitbucket_merge_pull_request: [ + { text: 'Merge pull request', field: 'prId', core: true }, + { text: 'using', field: 'mergeStrategy' }, + ], + bitbucket_get_pull_request_merge_task_status: [ + { text: 'Check merge task', field: 'taskId', core: true }, + { text: 'for pull request', field: 'prId' }, + ], + bitbucket_decline_pull_request: [ + { text: 'Decline pull request', field: 'prId', core: true }, + ], + bitbucket_approve_pull_request: [ + { text: 'Approve pull request', field: 'prId', core: true }, + ], + bitbucket_request_pull_request_changes: [ + { text: 'Request changes on pull request', field: 'prId', core: true }, + ], + bitbucket_get_pull_request_diff: [ + { text: 'Read diff for file', field: 'path', core: true }, + { text: 'in pull request', field: 'prId' }, + ], + bitbucket_get_pull_request_diffstat: [ + { text: 'List changed files in pull request', field: 'prId', core: true }, + ], + bitbucket_list_pull_request_comments: [ + { text: 'List comments on pull request', field: 'prId', core: true }, + ], + bitbucket_create_pull_request_comment: [ + { text: 'Comment', field: 'content', core: true }, + { text: 'on pull request', field: 'prId' }, + ], + bitbucket_list_pull_request_commit_statuses: [ + { text: 'List commit statuses for pull request', field: 'prId', core: true }, + ], + bitbucket_list_pipelines: [ + { text: 'List pipelines in', field: REPOSITORY_FIELD, core: true }, + ], + bitbucket_get_pipeline: [{ text: 'Read pipeline', field: 'pipelineUuid', core: true }], + bitbucket_trigger_pipeline: [ + { text: 'Trigger pipeline on', field: 'targetRef', core: true }, + { text: 'in', field: REPOSITORY_FIELD }, + ], + bitbucket_stop_pipeline: [{ text: 'Stop pipeline', field: 'pipelineUuid', core: true }], + bitbucket_list_pipeline_steps: [ + { text: 'List steps in pipeline', field: 'pipelineUuid', core: true }, + ], + bitbucket_get_pipeline_step_log: [ + { text: 'Read log for step', field: 'stepUuid', core: true }, + { text: 'in pipeline', field: 'pipelineUuid' }, + ], + }, + }, + }, + subBlocks: [ + { + id: 'operation', + title: 'Action', + type: 'dropdown', + options: [ + { label: 'List Workspaces', id: 'bitbucket_list_workspaces' }, + { label: 'List Repositories', id: 'bitbucket_list_repositories' }, + { label: 'Get Repository', id: 'bitbucket_get_repository' }, + { label: 'List Branches', id: 'bitbucket_list_branches' }, + { label: 'Create Branch', id: 'bitbucket_create_branch' }, + { label: 'Delete Branch', id: 'bitbucket_delete_branch' }, + { label: 'List Commits', id: 'bitbucket_list_commits' }, + { label: 'Get Commit', id: 'bitbucket_get_commit' }, + { label: 'List Directory', id: 'bitbucket_list_directory' }, + { label: 'Get File Metadata', id: 'bitbucket_get_file_metadata' }, + { label: 'Get File', id: 'bitbucket_get_file' }, + { label: 'List Pull Requests', id: 'bitbucket_list_pull_requests' }, + { label: 'Get Pull Request', id: 'bitbucket_get_pull_request' }, + { label: 'Create Pull Request', id: 'bitbucket_create_pull_request' }, + { label: 'Merge Pull Request', id: 'bitbucket_merge_pull_request' }, + { + label: 'Get Pull Request Merge Task Status', + id: 'bitbucket_get_pull_request_merge_task_status', + }, + { label: 'Decline Pull Request', id: 'bitbucket_decline_pull_request' }, + { label: 'Approve Pull Request', id: 'bitbucket_approve_pull_request' }, + { + label: 'Request Pull Request Changes', + id: 'bitbucket_request_pull_request_changes', + }, + { label: 'Get Pull Request Diff', id: 'bitbucket_get_pull_request_diff' }, + { label: 'Get Pull Request Diffstat', id: 'bitbucket_get_pull_request_diffstat' }, + { label: 'List Pull Request Comments', id: 'bitbucket_list_pull_request_comments' }, + { label: 'Create Pull Request Comment', id: 'bitbucket_create_pull_request_comment' }, + { + label: 'List Pull Request Commit Statuses', + id: 'bitbucket_list_pull_request_commit_statuses', + }, + { label: 'List Pipelines', id: 'bitbucket_list_pipelines' }, + { label: 'Get Pipeline', id: 'bitbucket_get_pipeline' }, + { label: 'Trigger Pipeline', id: 'bitbucket_trigger_pipeline' }, + { label: 'Stop Pipeline', id: 'bitbucket_stop_pipeline' }, + { label: 'List Pipeline Steps', id: 'bitbucket_list_pipeline_steps' }, + { label: 'Get Pipeline Step Log', id: 'bitbucket_get_pipeline_step_log' }, + ], + value: () => 'bitbucket_list_repositories', + }, + { + id: 'accountPicker', + title: 'Bitbucket Account', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + serviceId: 'bitbucket', + requiredScopes: getScopesForService('bitbucket'), + placeholder: 'Select Bitbucket account', + required: true, + }, + { + id: 'credentialIdInput', + title: 'Bitbucket Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + { + id: 'workspacePicker', + title: 'Workspace', + type: 'project-selector', + canonicalParamId: 'workspaceSlug', + serviceId: 'bitbucket', + selectorKey: 'bitbucket.workspaces', + dependsOn: ['accountPicker'], + mode: 'basic', + condition: { field: 'operation', value: 'bitbucket_list_workspaces', not: true }, + required: { field: 'operation', value: 'bitbucket_list_workspaces', not: true }, + placeholder: 'Select Bitbucket workspace', + }, + { + id: 'workspaceSlugInput', + title: 'Workspace Slug', + type: 'short-input', + canonicalParamId: 'workspaceSlug', + dependsOn: ['credentialIdInput'], + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_workspaces', not: true }, + required: { field: 'operation', value: 'bitbucket_list_workspaces', not: true }, + placeholder: 'Enter workspace slug', + }, + { + id: 'repositoryPicker', + title: 'Repository', + type: 'project-selector', + canonicalParamId: 'repoSlug', + serviceId: 'bitbucket', + selectorKey: 'bitbucket.repositories', + dependsOn: ['accountPicker', 'workspacePicker'], + mode: 'basic', + condition: { + field: 'operation', + value: ['bitbucket_list_workspaces', 'bitbucket_list_repositories'], + not: true, + }, + required: { + field: 'operation', + value: ['bitbucket_list_workspaces', 'bitbucket_list_repositories'], + not: true, + }, + placeholder: 'Select Bitbucket repository', + }, + { + id: 'repositorySlugInput', + title: 'Repository Slug', + type: 'short-input', + canonicalParamId: 'repoSlug', + dependsOn: ['credentialIdInput', 'workspaceSlugInput'], + mode: 'advanced', + condition: { + field: 'operation', + value: ['bitbucket_list_workspaces', 'bitbucket_list_repositories'], + not: true, + }, + required: { + field: 'operation', + value: ['bitbucket_list_workspaces', 'bitbucket_list_repositories'], + not: true, + }, + placeholder: 'Enter repository slug', + }, + { + id: 'pageLen', + title: 'Page Size', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: PAGINATED_OPERATIONS }, + placeholder: '1-100', + }, + { + id: 'nextUrl', + title: 'Next Page URL', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: PAGINATED_OPERATIONS }, + placeholder: 'Validated URL returned by the previous page', + }, + { + id: 'query', + title: 'Bitbucket Query', + type: 'long-input', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'bitbucket_list_repositories', + 'bitbucket_list_branches', + 'bitbucket_list_directory', + 'bitbucket_list_pull_requests', + 'bitbucket_list_pull_request_comments', + 'bitbucket_list_pull_request_commit_statuses', + ], + }, + placeholder: 'Optional Bitbucket filtering expression', + }, + { + id: 'sort', + title: 'Sort Expression', + type: 'short-input', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'bitbucket_list_workspaces', + 'bitbucket_list_repositories', + 'bitbucket_list_branches', + 'bitbucket_list_directory', + 'bitbucket_list_pull_requests', + 'bitbucket_list_pull_request_comments', + 'bitbucket_list_pull_request_commit_statuses', + 'bitbucket_list_pipelines', + ], + }, + placeholder: 'For example: -updated_on', + }, + { + id: 'administrator', + title: 'Administrator Access', + type: 'dropdown', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_workspaces' }, + options: [ + { label: 'Any', id: '' }, + { label: 'Administrator', id: 'true' }, + { label: 'Not Administrator', id: 'false' }, + ], + }, + { + id: 'role', + title: 'Repository Role', + type: 'dropdown', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_repositories' }, + options: [ + { label: 'Any', id: '' }, + { label: 'Owner', id: 'owner' }, + { label: 'Administrator', id: 'admin' }, + { label: 'Contributor', id: 'contributor' }, + { label: 'Member', id: 'member' }, + ], + }, + { + id: 'branchName', + title: 'Branch Name', + type: 'short-input', + required: true, + condition: { + field: 'operation', + value: ['bitbucket_create_branch', 'bitbucket_delete_branch'], + }, + placeholder: 'feature/my-branch', + }, + { + id: 'target', + title: 'Start Point', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'bitbucket_create_branch' }, + placeholder: 'Full commit hash or existing ref', + }, + { + id: 'revision', + title: 'Commit SHA', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'bitbucket_get_commit', + 'bitbucket_list_directory', + 'bitbucket_get_file_metadata', + 'bitbucket_get_file', + ], + }, + required: { + field: 'operation', + value: [ + 'bitbucket_get_commit', + 'bitbucket_list_directory', + 'bitbucket_get_file_metadata', + 'bitbucket_get_file', + ], + }, + placeholder: 'Full 40-character commit SHA', + }, + { + id: 'path', + title: 'Path', + type: 'short-input', + condition: { + field: 'operation', + value: [ + 'bitbucket_list_directory', + 'bitbucket_get_file_metadata', + 'bitbucket_get_file', + 'bitbucket_get_pull_request_diff', + ], + }, + required: { + field: 'operation', + value: [ + 'bitbucket_get_file_metadata', + 'bitbucket_get_file', + 'bitbucket_get_pull_request_diff', + ], + }, + placeholder: 'src/index.ts', + }, + { + id: 'state', + title: 'Pull Request State', + type: 'dropdown', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_pull_requests' }, + options: [ + { label: 'Open', id: 'OPEN' }, + { label: 'Merged', id: 'MERGED' }, + { label: 'Declined', id: 'DECLINED' }, + { label: 'Superseded', id: 'SUPERSEDED' }, + ], + }, + { + id: 'prId', + title: 'Pull Request ID', + type: 'short-input', + required: true, + condition: { field: 'operation', value: PULL_REQUEST_ID_OPERATIONS }, + placeholder: '123', + }, + { + id: 'title', + title: 'Title', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'bitbucket_create_pull_request' }, + }, + { + id: 'sourceBranch', + title: 'Source Branch', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'bitbucket_create_pull_request' }, + }, + { + id: 'destinationBranch', + title: 'Destination Branch', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'bitbucket_create_pull_request' }, + placeholder: 'main', + }, + { + id: 'description', + title: 'Description', + type: 'long-input', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_create_pull_request' }, + }, + { + id: 'reviewerAccountIds', + title: 'Reviewer UUIDs', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_create_pull_request' }, + placeholder: 'Comma-separated Bitbucket user UUIDs', + }, + { + id: 'closeSourceBranch', + title: 'Close Source Branch', + type: 'dropdown', + mode: 'advanced', + condition: { + field: 'operation', + value: ['bitbucket_create_pull_request', 'bitbucket_merge_pull_request'], + }, + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + }, + { + id: 'draft', + title: 'Draft Pull Request', + type: 'dropdown', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_create_pull_request' }, + options: [ + { label: 'No', id: 'false' }, + { label: 'Yes', id: 'true' }, + ], + }, + { + id: 'mergeStrategy', + title: 'Merge Strategy', + type: 'dropdown', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_merge_pull_request' }, + options: [ + { label: 'Merge Commit', id: 'merge_commit' }, + { label: 'Squash', id: 'squash' }, + { label: 'Fast Forward', id: 'fast_forward' }, + { label: 'Squash and Fast Forward', id: 'squash_fast_forward' }, + { label: 'Rebase and Fast Forward', id: 'rebase_fast_forward' }, + { label: 'Rebase and Merge', id: 'rebase_merge' }, + ], + }, + { + id: 'message', + title: 'Merge Commit Message', + type: 'long-input', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_merge_pull_request' }, + }, + { + id: 'taskId', + title: 'Merge Task ID', + type: 'short-input', + required: true, + condition: { + field: 'operation', + value: 'bitbucket_get_pull_request_merge_task_status', + }, + }, + { + id: 'content', + title: 'Comment', + type: 'long-input', + required: true, + condition: { field: 'operation', value: 'bitbucket_create_pull_request_comment' }, + }, + { + id: 'parentId', + title: 'Parent Comment ID', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_create_pull_request_comment' }, + placeholder: 'Reply to an existing general comment', + }, + { + id: 'pipelineRefType', + title: 'Pipeline Ref Type', + type: 'dropdown', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_pipelines' }, + options: [ + { label: 'Any', id: '' }, + { label: 'Branch', id: 'BRANCH' }, + { label: 'Tag', id: 'TAG' }, + { label: 'Annotated Tag', id: 'ANNOTATED_TAG' }, + ], + }, + { + id: 'pipelineRefName', + title: 'Pipeline Ref Name', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_pipelines' }, + }, + { + id: 'pipelineCommitHash', + title: 'Pipeline Commit SHA', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_pipelines' }, + placeholder: 'Full 40-character commit SHA', + }, + { + id: 'pipelineSelectorType', + title: 'Pipeline Selector Type', + type: 'dropdown', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_pipelines' }, + options: [ + { label: 'Any', id: '' }, + { label: 'Branch', id: 'BRANCH' }, + { label: 'Tag', id: 'TAG' }, + { label: 'Custom', id: 'CUSTOM' }, + { label: 'Pull Requests', id: 'PULLREQUESTS' }, + { label: 'Default', id: 'DEFAULT' }, + ], + }, + { + id: 'pipelineSelectorPattern', + title: 'Pipeline Selector Pattern', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_pipelines' }, + }, + { + id: 'pipelineTriggerType', + title: 'Pipeline Trigger Type', + type: 'dropdown', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_pipelines' }, + options: [ + { label: 'Any', id: '' }, + { label: 'Push', id: 'PUSH' }, + { label: 'Manual', id: 'MANUAL' }, + { label: 'Scheduled', id: 'SCHEDULED' }, + { label: 'Parent Step', id: 'PARENT_STEP' }, + ], + }, + { + id: 'pipelineStatus', + title: 'Pipeline Status', + type: 'dropdown', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_list_pipelines' }, + options: [ + { label: 'Any', id: '' }, + { label: 'Parsing', id: 'PARSING' }, + { label: 'Pending', id: 'PENDING' }, + { label: 'Paused', id: 'PAUSED' }, + { label: 'Halted', id: 'HALTED' }, + { label: 'Building', id: 'BUILDING' }, + { label: 'Error', id: 'ERROR' }, + { label: 'Passed', id: 'PASSED' }, + { label: 'Failed', id: 'FAILED' }, + { label: 'Stopped', id: 'STOPPED' }, + { label: 'Unknown', id: 'UNKNOWN' }, + ], + }, + { + id: 'pipelineUuid', + title: 'Pipeline UUID', + type: 'short-input', + required: true, + condition: { field: 'operation', value: PIPELINE_ID_OPERATIONS }, + placeholder: '{pipeline-uuid}', + }, + { + id: 'targetRef', + title: 'Branch', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'bitbucket_trigger_pipeline' }, + placeholder: 'main', + }, + { + id: 'targetCommitHash', + title: 'Target Commit SHA', + type: 'short-input', + mode: 'advanced', + condition: { field: 'operation', value: 'bitbucket_trigger_pipeline' }, + placeholder: 'Optional full commit SHA on the selected branch', + }, + { + id: 'stepUuid', + title: 'Step UUID', + type: 'short-input', + required: true, + condition: { field: 'operation', value: 'bitbucket_get_pipeline_step_log' }, + placeholder: '{step-uuid}', + }, + { + id: 'maxCharacters', + title: 'Maximum Characters', + type: 'short-input', + mode: 'advanced', + condition: { + field: 'operation', + value: [ + 'bitbucket_get_file', + 'bitbucket_get_pull_request_diff', + 'bitbucket_get_pipeline_step_log', + ], + }, + placeholder: 'Maximum bounded content to return', + }, + ], + tools: { + // Keep this list literal so the documentation generator can statically discover every action. + access: [ + 'bitbucket_list_workspaces', + 'bitbucket_list_repositories', + 'bitbucket_get_repository', + 'bitbucket_list_branches', + 'bitbucket_create_branch', + 'bitbucket_delete_branch', + 'bitbucket_list_commits', + 'bitbucket_get_commit', + 'bitbucket_list_directory', + 'bitbucket_get_file_metadata', + 'bitbucket_get_file', + 'bitbucket_list_pull_requests', + 'bitbucket_get_pull_request', + 'bitbucket_create_pull_request', + 'bitbucket_merge_pull_request', + 'bitbucket_get_pull_request_merge_task_status', + 'bitbucket_decline_pull_request', + 'bitbucket_approve_pull_request', + 'bitbucket_request_pull_request_changes', + 'bitbucket_get_pull_request_diff', + 'bitbucket_get_pull_request_diffstat', + 'bitbucket_list_pull_request_comments', + 'bitbucket_create_pull_request_comment', + 'bitbucket_list_pull_request_commit_statuses', + 'bitbucket_list_pipelines', + 'bitbucket_get_pipeline', + 'bitbucket_trigger_pipeline', + 'bitbucket_stop_pipeline', + 'bitbucket_list_pipeline_steps', + 'bitbucket_get_pipeline_step_log', + ], + config: { + tool: (params) => { + if (!isBitbucketOperation(params.operation)) { + throw new Error(`Invalid Bitbucket operation: ${String(params.operation)}`) + } + return params.operation + }, + params: (params) => { + if (!isBitbucketOperation(params.operation)) { + throw new Error(`Invalid Bitbucket operation: ${String(params.operation)}`) + } + + const common = { + oauthCredential: params.oauthCredential, + workspaceSlug: optionalString(params.workspaceSlug, 'workspaceSlug'), + repoSlug: optionalString(params.repoSlug, 'repoSlug'), + } + const pagination = { + pageLen: optionalInteger(params.pageLen, 'pageLen'), + nextUrl: optionalString(params.nextUrl, 'nextUrl'), + } + + switch (params.operation) { + case 'bitbucket_list_workspaces': + return { + oauthCredential: params.oauthCredential, + sort: optionalString(params.sort, 'sort'), + administrator: optionalBoolean(params.administrator, 'administrator'), + ...pagination, + } + case 'bitbucket_list_repositories': + return { + oauthCredential: params.oauthCredential, + workspaceSlug: optionalString(params.workspaceSlug, 'workspaceSlug'), + role: optionalString(params.role, 'role'), + q: optionalString(params.query, 'query'), + sort: optionalString(params.sort, 'sort'), + ...pagination, + } + case 'bitbucket_list_branches': + return { + ...common, + q: optionalString(params.query, 'query'), + sort: optionalString(params.sort, 'sort'), + ...pagination, + } + case 'bitbucket_list_pipelines': + return { + ...common, + refType: optionalString(params.pipelineRefType, 'pipelineRefType'), + refName: optionalString(params.pipelineRefName, 'pipelineRefName'), + commitHash: optionalString(params.pipelineCommitHash, 'pipelineCommitHash'), + selectorType: optionalString(params.pipelineSelectorType, 'pipelineSelectorType'), + selectorPattern: optionalString( + params.pipelineSelectorPattern, + 'pipelineSelectorPattern' + ), + triggerType: optionalString(params.pipelineTriggerType, 'pipelineTriggerType'), + status: optionalString(params.pipelineStatus, 'pipelineStatus'), + sort: optionalString(params.sort, 'sort'), + ...pagination, + } + case 'bitbucket_get_repository': + return common + case 'bitbucket_create_branch': + return { + ...common, + name: optionalString(params.branchName, 'branchName'), + target: optionalString(params.target, 'target'), + } + case 'bitbucket_delete_branch': + return { ...common, name: optionalString(params.branchName, 'branchName') } + case 'bitbucket_list_commits': + return { ...common, ...pagination } + case 'bitbucket_get_commit': + return { ...common, commit: optionalString(params.revision, 'revision') } + case 'bitbucket_list_directory': + return { + ...common, + commit: optionalString(params.revision, 'revision'), + path: optionalText(params.path, 'path'), + q: optionalString(params.query, 'query'), + sort: optionalString(params.sort, 'sort'), + ...pagination, + } + case 'bitbucket_get_file_metadata': + return { + ...common, + commit: optionalString(params.revision, 'revision'), + path: optionalText(params.path, 'path'), + } + case 'bitbucket_get_file': + return { + ...common, + commit: optionalString(params.revision, 'revision'), + path: optionalText(params.path, 'path'), + maxCharacters: optionalInteger(params.maxCharacters, 'maxCharacters'), + } + case 'bitbucket_list_pull_requests': + return { + ...common, + state: optionalString(params.state, 'state'), + q: optionalString(params.query, 'query'), + sort: optionalString(params.sort, 'sort'), + ...pagination, + } + case 'bitbucket_get_pull_request': + case 'bitbucket_decline_pull_request': + case 'bitbucket_approve_pull_request': + case 'bitbucket_request_pull_request_changes': + return { ...common, prId: optionalInteger(params.prId, 'prId') } + case 'bitbucket_create_pull_request': + return { + ...common, + title: optionalString(params.title, 'title'), + sourceBranch: optionalString(params.sourceBranch, 'sourceBranch'), + destinationBranch: optionalString(params.destinationBranch, 'destinationBranch'), + description: optionalText(params.description, 'description'), + reviewerUuids: stringList(params.reviewerAccountIds, 'reviewerAccountIds'), + closeSourceBranch: optionalBoolean(params.closeSourceBranch, 'closeSourceBranch'), + draft: optionalBoolean(params.draft, 'draft'), + } + case 'bitbucket_merge_pull_request': + return { + ...common, + prId: optionalInteger(params.prId, 'prId'), + mergeStrategy: optionalString(params.mergeStrategy, 'mergeStrategy'), + message: optionalText(params.message, 'message'), + closeSourceBranch: optionalBoolean(params.closeSourceBranch, 'closeSourceBranch'), + } + case 'bitbucket_get_pull_request_merge_task_status': + return { + ...common, + prId: optionalInteger(params.prId, 'prId'), + taskId: optionalString(params.taskId, 'taskId'), + } + case 'bitbucket_get_pull_request_diff': + return { + ...common, + prId: optionalInteger(params.prId, 'prId'), + path: optionalText(params.path, 'path'), + maxCharacters: optionalInteger(params.maxCharacters, 'maxCharacters'), + } + case 'bitbucket_get_pull_request_diffstat': + return { ...common, prId: optionalInteger(params.prId, 'prId'), ...pagination } + case 'bitbucket_list_pull_request_comments': + case 'bitbucket_list_pull_request_commit_statuses': + return { + ...common, + prId: optionalInteger(params.prId, 'prId'), + q: optionalString(params.query, 'query'), + sort: optionalString(params.sort, 'sort'), + ...pagination, + } + case 'bitbucket_create_pull_request_comment': + return { + ...common, + prId: optionalInteger(params.prId, 'prId'), + content: optionalText(params.content, 'content'), + parentId: optionalInteger(params.parentId, 'parentId'), + } + case 'bitbucket_get_pipeline': + case 'bitbucket_stop_pipeline': + return { ...common, pipelineUuid: optionalString(params.pipelineUuid, 'pipelineUuid') } + case 'bitbucket_trigger_pipeline': + return { + ...common, + refType: 'branch', + refName: optionalString(params.targetRef, 'targetRef'), + commitHash: optionalString(params.targetCommitHash, 'targetCommitHash'), + } + case 'bitbucket_list_pipeline_steps': + return { + ...common, + pipelineUuid: optionalString(params.pipelineUuid, 'pipelineUuid'), + ...pagination, + } + case 'bitbucket_get_pipeline_step_log': + return { + ...common, + pipelineUuid: optionalString(params.pipelineUuid, 'pipelineUuid'), + stepUuid: optionalString(params.stepUuid, 'stepUuid'), + maxCharacters: optionalInteger(params.maxCharacters, 'maxCharacters'), + } + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Bitbucket operation to perform' }, + oauthCredential: { type: 'string', description: 'Bitbucket OAuth credential' }, + workspaceSlug: { type: 'string', description: 'Bitbucket workspace slug' }, + repoSlug: { type: 'string', description: 'Bitbucket repository slug' }, + pageLen: { type: 'number', description: 'Requested page size' }, + nextUrl: { type: 'string', description: 'Validated next-page URL' }, + query: { type: 'string', description: 'Bitbucket filtering expression' }, + sort: { type: 'string', description: 'Bitbucket sorting expression' }, + administrator: { type: 'boolean', description: 'Workspace administrator filter' }, + role: { type: 'string', description: 'Repository role filter' }, + branchName: { type: 'string', description: 'Branch name' }, + target: { type: 'string', description: 'Full commit hash or existing ref for the new branch' }, + revision: { type: 'string', description: 'Full commit SHA' }, + path: { type: 'string', description: 'Repository-relative file or directory path' }, + state: { type: 'string', description: 'Pull request state filter' }, + prId: { type: 'number', description: 'Pull request ID' }, + title: { type: 'string', description: 'Pull request title' }, + sourceBranch: { type: 'string', description: 'Pull request source branch' }, + destinationBranch: { type: 'string', description: 'Pull request destination branch' }, + description: { type: 'string', description: 'Pull request description' }, + reviewerAccountIds: { type: 'array', description: 'Reviewer Bitbucket user UUIDs' }, + closeSourceBranch: { type: 'boolean', description: 'Whether to close the source branch' }, + draft: { type: 'boolean', description: 'Whether to create a draft pull request' }, + mergeStrategy: { type: 'string', description: 'Pull request merge strategy' }, + message: { type: 'string', description: 'Merge commit message' }, + taskId: { type: 'string', description: 'Asynchronous merge task ID' }, + content: { type: 'string', description: 'Pull request comment content' }, + parentId: { type: 'number', description: 'Parent pull request comment ID' }, + pipelineRefType: { type: 'string', description: 'Pipeline list reference type filter' }, + pipelineRefName: { type: 'string', description: 'Pipeline list reference name filter' }, + pipelineCommitHash: { type: 'string', description: 'Pipeline list commit SHA filter' }, + pipelineSelectorType: { type: 'string', description: 'Pipeline selector type filter' }, + pipelineSelectorPattern: { type: 'string', description: 'Pipeline selector pattern filter' }, + pipelineTriggerType: { type: 'string', description: 'Pipeline trigger type filter' }, + pipelineStatus: { type: 'string', description: 'Pipeline status filter' }, + pipelineUuid: { type: 'string', description: 'Pipeline UUID' }, + targetRef: { type: 'string', description: 'Pipeline branch target' }, + targetCommitHash: { type: 'string', description: 'Optional pipeline target commit SHA' }, + stepUuid: { type: 'string', description: 'Pipeline step UUID' }, + maxCharacters: { type: 'number', description: 'Maximum bounded raw content length' }, + }, + outputs: { + items: { type: 'array', description: 'Items returned by a list operation' }, + page: { type: 'json', description: 'Bitbucket pagination metadata' }, + repository: { type: 'json', description: 'Repository details' }, + branch: { type: 'json', description: 'Branch details' }, + deleted: { type: 'boolean', description: 'Whether a branch was deleted' }, + commit: { type: 'json', description: 'Commit details' }, + file: { type: 'json', description: 'File metadata' }, + content: { type: 'string', description: 'Bounded file content' }, + binary: { type: 'boolean', description: 'Whether file content is binary' }, + truncated: { type: 'boolean', description: 'Whether raw content was truncated' }, + decodingLossy: { type: 'boolean', description: 'Whether invalid UTF-8 bytes were replaced' }, + returnedBytes: { type: 'number', description: 'Provider bytes read for raw content' }, + fullBytes: { type: 'number', description: 'Full raw-content byte size when reported' }, + contentType: { type: 'string', description: 'Raw file response content type' }, + pullRequest: { type: 'json', description: 'Pull request details' }, + status: { type: 'string', description: 'Merge request status' }, + taskId: { type: 'string', description: 'Asynchronous merge task ID' }, + taskUrl: { type: 'string', description: 'Asynchronous merge task URL' }, + taskStatus: { type: 'string', description: 'Asynchronous merge task status' }, + selfUrl: { type: 'string', description: 'Merge task API URL' }, + mergeResult: { type: 'json', description: 'Completed asynchronous merge result' }, + participant: { type: 'json', description: 'Pull request review participant' }, + diff: { type: 'string', description: 'Bounded unified diff text' }, + comment: { type: 'json', description: 'Pull request comment details' }, + pipeline: { type: 'json', description: 'Pipeline details' }, + stopped: { type: 'boolean', description: 'Whether a pipeline stop request succeeded' }, + log: { type: 'string', description: 'Bounded pipeline step log tail' }, + totalBytes: { type: 'number', description: 'Full pipeline log byte size when reported' }, + }, +} + +export const BitbucketBlockMeta = { + tags: ['version-control', 'ci-cd', 'automation'], + url: 'https://bitbucket.org', + templates: [ + { + icon: BitbucketIcon, + title: 'Bitbucket pull request review assistant', + prompt: + 'Build a workflow that reads a Bitbucket pull request, discovers its changed files with diffstat, reviews each bounded file diff against our engineering standards, and posts one concise general pull request comment with findings and suggested fixes.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['engineering', 'automation'], + }, + { + icon: BitbucketIcon, + title: 'Bitbucket pipeline failure diagnosis', + prompt: + 'Create a workflow that inspects a failed Bitbucket pipeline, lists its steps, reads the bounded tail of each failed step log, identifies the likely root cause, and sends an actionable diagnosis to Slack.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['ci-cd', 'monitoring'], + alsoIntegrations: ['slack'], + }, + { + icon: BitbucketIcon, + title: 'Bitbucket merge readiness report', + prompt: + 'Build a workflow that reads an open Bitbucket pull request, its reviewers, commit statuses, and changed-file summary, then reports approvals, failing checks, risky changes, and the remaining review work without merging it.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['engineering', 'reporting'], + }, + { + icon: BitbucketIcon, + title: 'Bitbucket release notes generator', + prompt: + 'Create a workflow that lists recent Bitbucket commits in a repository, reads ambiguous commit details, groups the changes by area, and drafts clear release notes for review.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['engineering', 'content'], + }, + { + icon: BitbucketIcon, + title: 'Bitbucket stale pull request review', + prompt: + 'Build a scheduled workflow that lists open Bitbucket pull requests, identifies stale work from its update timestamps and review state, summarizes the next action for each, and posts a weekly reminder in Slack.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'engineering', + tags: ['team', 'automation'], + alsoIntegrations: ['slack'], + }, + { + icon: BitbucketIcon, + title: 'Bitbucket engineering activity digest', + prompt: + 'Create a scheduled workflow that summarizes recent Bitbucket commits, pull request activity, and pipeline results across a repository into a concise engineering digest delivered by email.', + modules: ['scheduled', 'agent', 'workflows'], + category: 'productivity', + tags: ['reporting', 'team'], + alsoIntegrations: ['gmail'], + }, + { + icon: BitbucketIcon, + title: 'Bitbucket and Jira delivery automation', + prompt: + 'Build a workflow that reads a Bitbucket pull request and its head commit, extracts Jira issue keys from the title, description, source branch, and commit message, summarizes delivery and pipeline status, and adds the update to the matching Jira issue.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['project-management', 'automation'], + alsoIntegrations: ['jira'], + }, + ], + skills: [ + { + name: 'review-bitbucket-pull-request', + description: + 'Review a Bitbucket pull request file by file and produce a concise, evidence-backed general comment.', + content: + '# Review a Bitbucket Pull Request\n\n## Steps\n1. Read the pull request and use diffstat to discover changed files.\n2. Fetch each file-scoped diff separately and respect truncation metadata.\n3. Evaluate correctness, security, tests, and maintainability using only evidence in the diff.\n4. Group duplicate findings and distinguish blockers from suggestions.\n5. Post one general pull request comment only when asked.\n\n## Output\nSummarize the risk, list findings with file paths, and state whether human review is still needed.', + }, + { + name: 'diagnose-bitbucket-pipeline', + description: + 'Trace a failed Bitbucket pipeline to the failing step and explain the most likely root cause.', + content: + '# Diagnose a Bitbucket Pipeline\n\n## Steps\n1. Read the pipeline and list all steps.\n2. Focus on failed or stopped steps.\n3. Read bounded log tails and account for partial or truncated output.\n4. Identify the first causal error rather than downstream noise.\n5. Recommend the smallest verifiable fix.\n\n## Output\nReport the failed step, evidence from the log, likely cause, and next diagnostic or fix.', + }, + { + name: 'assess-bitbucket-merge-readiness', + description: + 'Assess approvals, commit statuses, and changed-file risk before a Bitbucket merge.', + content: + '# Assess Bitbucket Merge Readiness\n\n## Steps\n1. Read the pull request and current participants.\n2. List commit statuses and changed files.\n3. Identify missing approvals, failed or pending checks, and risky changes.\n4. Do not infer that a status is required or claim conflict status when the API data does not say so.\n5. Never merge unless the user explicitly requests it.\n\n## Output\nReturn a ready, blocked, or needs-review assessment with each observed blocker and its evidence.', + }, + { + name: 'draft-bitbucket-release-notes', + description: + 'Turn Bitbucket commit history into audience-friendly release notes without inventing changes.', + content: + '# Draft Bitbucket Release Notes\n\n## Steps\n1. List recent commits in the requested repository.\n2. Read commit details where summaries are ambiguous.\n3. Group changes into features, fixes, performance, and maintenance.\n4. Preserve contributor attribution and relevant links when present.\n5. Flag unclear commits rather than guessing their user impact.\n\n## Output\nProduce a short release summary followed by categorized bullets and known upgrade risks.', + }, + { + name: 'triage-stale-bitbucket-pull-requests', + description: + 'Identify stale Bitbucket pull requests and recommend the next owner and action for each.', + content: + '# Triage Stale Bitbucket Pull Requests\n\n## Steps\n1. List open pull requests and read candidates that have not changed recently.\n2. Check reviewers, comments, changed files, and commit statuses.\n3. Distinguish waiting-for-author, waiting-for-review, failing-CI, and obsolete work.\n4. Recommend a next action and responsible participant.\n5. Decline or comment only when explicitly requested.\n\n## Output\nReturn a prioritized table of stale pull requests, age, blocker, owner, and next action.', + }, + { + name: 'summarize-bitbucket-activity', + description: + 'Summarize repository commits, pull requests, and pipeline activity for an engineering update.', + content: + '# Summarize Bitbucket Activity\n\n## Steps\n1. Gather recent commits, pull requests, and pipelines for the repository.\n2. Deduplicate activity that represents the same change.\n3. Highlight merged work, active reviews, failures, and delivery risks.\n4. Link to source records when URLs are available.\n\n## Output\nProvide a concise digest with shipped, in review, CI health, and attention-needed sections.', + }, + { + name: 'sync-bitbucket-delivery-to-jira', + description: + 'Connect Bitbucket pull request and pipeline evidence to the matching Jira delivery record.', + content: + '# Sync Bitbucket Delivery to Jira\n\n## Steps\n1. Read the pull request, source branch, head commit, and pipeline status.\n2. Extract Jira keys conservatively and verify the intended Jira issue.\n3. Summarize implementation, review state, and CI status.\n4. Add a Jira update only when a unique issue match is established.\n5. Never transition or close the Jira issue unless explicitly requested.\n\n## Output\nReport the matched issue, linked pull request, delivery state, and any ambiguity that blocked an update.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry-maps.ts b/apps/sim/blocks/registry-maps.ts index 46ad3b0fe5a..515aaac4df2 100644 --- a/apps/sim/blocks/registry-maps.ts +++ b/apps/sim/blocks/registry-maps.ts @@ -23,6 +23,7 @@ import { AzureDataExplorerBlockMeta, } from '@/blocks/blocks/azure_data_explorer' import { AzureDevOpsBlock, AzureDevOpsBlockMeta } from '@/blocks/blocks/azure_devops' +import { BitbucketBlock, BitbucketBlockMeta } from '@/blocks/blocks/bitbucket' import { BoxBlock, BoxBlockMeta } from '@/blocks/blocks/box' import { BrandfetchBlock, BrandfetchBlockMeta } from '@/blocks/blocks/brandfetch' import { BrexBlock, BrexBlockMeta } from '@/blocks/blocks/brex' @@ -386,6 +387,7 @@ export const BLOCK_REGISTRY: Record = { attio: AttioBlock, azure_data_explorer: AzureDataExplorerBlock, azure_devops: AzureDevOpsBlock, + bitbucket: BitbucketBlock, box: BoxBlock, brandfetch: BrandfetchBlock, brex: BrexBlock, @@ -730,6 +732,7 @@ export const BLOCK_META_REGISTRY: Record = { attio: AttioBlockMeta, azure_data_explorer: AzureDataExplorerBlockMeta, azure_devops: AzureDevOpsBlockMeta, + bitbucket: BitbucketBlockMeta, box: BoxBlockMeta, brandfetch: BrandfetchBlockMeta, brex: BrexBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 8580bb9ea85..28fe90c33e8 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -754,6 +754,38 @@ export function GithubOutlineIcon(props: SVGProps) { ) } +export function BitbucketIcon(props: SVGProps) { + const id = useId() + const gradientId = `bitbucket_original_a_${id}` + + return ( + + + + + + + + + + + ) +} + export function GitLabIcon(props: SVGProps) { return ( diff --git a/apps/sim/hooks/selectors/providers/bitbucket/selectors.test.ts b/apps/sim/hooks/selectors/providers/bitbucket/selectors.test.ts new file mode 100644 index 00000000000..5219819b0e2 --- /dev/null +++ b/apps/sim/hooks/selectors/providers/bitbucket/selectors.test.ts @@ -0,0 +1,213 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockRequestJson } = vi.hoisted(() => ({ mockRequestJson: vi.fn() })) + +vi.mock('@/lib/api/client/request', () => ({ requestJson: mockRequestJson })) + +import { getSelectorDefinition } from '@/hooks/selectors/registry' +import type { SelectorQueryArgs } from '@/hooks/selectors/types' + +const workspaces = getSelectorDefinition('bitbucket.workspaces') +const repositories = getSelectorDefinition('bitbucket.repositories') + +const workspaceArgs = ( + overrides: Partial = {} +): SelectorQueryArgs => ({ + key: 'bitbucket.workspaces', + context: { oauthCredential: 'credential-1', workflowId: 'workflow-1', ...overrides }, +}) + +const repositoryArgs = ( + overrides: Partial = {} +): SelectorQueryArgs => ({ + key: 'bitbucket.repositories', + context: { + oauthCredential: 'credential-1', + workflowId: 'workflow-1', + workspaceSlug: 'acme-platform', + ...overrides, + }, +}) + +describe('bitbucket.workspaces selector', () => { + beforeEach(() => vi.clearAllMocks()) + + it('is enabled only after a credential is selected and isolates each authorization context', () => { + expect(workspaces.enabled?.(workspaceArgs())).toBe(true) + expect(workspaces.enabled?.(workspaceArgs({ oauthCredential: undefined }))).toBe(false) + expect(workspaces.getQueryKey(workspaceArgs())).toEqual([ + 'selectors', + 'bitbucket.workspaces', + 'credential-1', + 'workflow-1', + ]) + expect(workspaces.getQueryKey(workspaceArgs({ oauthCredential: 'credential-2' }))).toEqual([ + 'selectors', + 'bitbucket.workspaces', + 'credential-2', + 'workflow-1', + ]) + expect(workspaces.getQueryKey(workspaceArgs({ workflowId: 'workflow-2' }))).toEqual([ + 'selectors', + 'bitbucket.workspaces', + 'credential-1', + 'workflow-2', + ]) + }) + + it('loads one page using a credential id and preserves provider metadata', async () => { + const nextCursor = 'https://api.bitbucket.org/2.0/user/workspaces?page=2&pagelen=100' + mockRequestJson.mockResolvedValue({ + workspaces: [ + { + slug: 'acme-platform', + uuid: '{workspace-uuid}', + name: 'Acme Platform', + administrator: true, + }, + ], + nextCursor, + }) + + const page = await workspaces.fetchPage?.({ + ...workspaceArgs(), + cursor: 'https://api.bitbucket.org/2.0/user/workspaces?page=1&pagelen=100', + }) + + expect(mockRequestJson).toHaveBeenCalledWith( + expect.objectContaining({ path: '/api/tools/bitbucket/workspaces' }), + expect.objectContaining({ + body: { + credential: 'credential-1', + workflowId: 'workflow-1', + cursor: 'https://api.bitbucket.org/2.0/user/workspaces?page=1&pagelen=100', + }, + }) + ) + expect(page).toEqual({ + items: [ + { + id: 'acme-platform', + label: 'Acme Platform', + meta: { + slug: 'acme-platform', + uuid: '{workspace-uuid}', + fullName: 'Acme Platform', + administrator: true, + }, + }, + ], + nextCursor, + }) + }) + + it('rejects a missing credential before making a route request', async () => { + await expect( + workspaces.fetchPage?.(workspaceArgs({ oauthCredential: undefined })) + ).rejects.toThrow(/Missing credential/) + expect(mockRequestJson).not.toHaveBeenCalled() + }) +}) + +describe('bitbucket.repositories selector', () => { + beforeEach(() => vi.clearAllMocks()) + + it('waits for both dependencies and isolates pages by auth context and workspace', () => { + expect(repositories.enabled?.(repositoryArgs())).toBe(true) + expect(repositories.enabled?.(repositoryArgs({ oauthCredential: undefined }))).toBe(false) + expect(repositories.enabled?.(repositoryArgs({ workspaceSlug: undefined }))).toBe(false) + + expect(repositories.getQueryKey(repositoryArgs())).toEqual([ + 'selectors', + 'bitbucket.repositories', + 'credential-1', + 'workflow-1', + 'acme-platform', + ]) + expect(repositories.getQueryKey(repositoryArgs({ workspaceSlug: 'other-team' }))).toEqual([ + 'selectors', + 'bitbucket.repositories', + 'credential-1', + 'workflow-1', + 'other-team', + ]) + expect(repositories.getQueryKey(repositoryArgs({ oauthCredential: 'credential-2' }))).toEqual([ + 'selectors', + 'bitbucket.repositories', + 'credential-2', + 'workflow-1', + 'acme-platform', + ]) + expect(repositories.getQueryKey(repositoryArgs({ workflowId: 'workflow-2' }))).toEqual([ + 'selectors', + 'bitbucket.repositories', + 'credential-1', + 'workflow-2', + 'acme-platform', + ]) + }) + + it('keeps the workspace dependency on every page and maps slug ids with UUID/full-name metadata', async () => { + const nextCursor = 'https://api.bitbucket.org/2.0/repositories/acme-platform?page=3&pagelen=100' + mockRequestJson.mockResolvedValue({ + repositories: [ + { + slug: 'payments-api', + uuid: '{repository-uuid}', + name: 'Payments API', + fullName: 'acme-platform/payments-api', + }, + ], + nextCursor, + }) + + const page = await repositories.fetchPage?.({ + ...repositoryArgs(), + cursor: 'https://api.bitbucket.org/2.0/repositories/acme-platform?page=2&pagelen=100', + }) + + expect(mockRequestJson).toHaveBeenCalledWith( + expect.objectContaining({ path: '/api/tools/bitbucket/repositories' }), + expect.objectContaining({ + body: { + credential: 'credential-1', + workflowId: 'workflow-1', + workspaceSlug: 'acme-platform', + cursor: 'https://api.bitbucket.org/2.0/repositories/acme-platform?page=2&pagelen=100', + }, + }) + ) + expect(page).toEqual({ + items: [ + { + id: 'payments-api', + label: 'Payments API', + meta: { + slug: 'payments-api', + uuid: '{repository-uuid}', + fullName: 'acme-platform/payments-api', + workspaceSlug: 'acme-platform', + }, + }, + ], + nextCursor, + }) + }) + + it('rejects a missing workspace dependency instead of issuing an unscoped request', async () => { + await expect( + repositories.fetchPage?.(repositoryArgs({ workspaceSlug: undefined })) + ).rejects.toThrow(/Missing workspace slug/) + expect(mockRequestJson).not.toHaveBeenCalled() + }) + + it('rejects a missing credential before making a route request', async () => { + await expect( + repositories.fetchPage?.(repositoryArgs({ oauthCredential: undefined })) + ).rejects.toThrow(/Missing credential/) + expect(mockRequestJson).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/hooks/selectors/providers/bitbucket/selectors.ts b/apps/sim/hooks/selectors/providers/bitbucket/selectors.ts new file mode 100644 index 00000000000..63dab44c923 --- /dev/null +++ b/apps/sim/hooks/selectors/providers/bitbucket/selectors.ts @@ -0,0 +1,95 @@ +import { requestJson } from '@/lib/api/client/request' +import * as selectorContracts from '@/lib/api/contracts/selectors' +import { ensureCredential, SELECTOR_STALE } from '@/hooks/selectors/providers/shared' +import type { SelectorDefinition, SelectorKey, SelectorQueryArgs } from '@/hooks/selectors/types' + +export const bitbucketSelectors = { + 'bitbucket.workspaces': { + key: 'bitbucket.workspaces', + contracts: [selectorContracts.bitbucketWorkspacesSelectorContract], + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'bitbucket.workspaces', + context.oauthCredential ?? 'none', + context.workflowId ?? 'none', + ], + enabled: ({ context }) => Boolean(context.oauthCredential), + /** Loads one Bitbucket page so the shared selector hook can progressively drain it. */ + fetchPage: async ({ context, cursor, signal }) => { + const credentialId = ensureCredential(context, 'bitbucket.workspaces') + const data = await requestJson(selectorContracts.bitbucketWorkspacesSelectorContract, { + body: { + credential: credentialId, + workflowId: context.workflowId, + cursor, + }, + signal, + }) + + return { + items: data.workspaces.map((workspace) => ({ + id: workspace.slug, + label: workspace.name, + meta: { + slug: workspace.slug, + uuid: workspace.uuid, + fullName: workspace.name, + administrator: workspace.administrator, + }, + })), + nextCursor: data.nextCursor, + } + }, + }, + 'bitbucket.repositories': { + key: 'bitbucket.repositories', + contracts: [selectorContracts.bitbucketRepositoriesSelectorContract], + staleTime: SELECTOR_STALE, + getQueryKey: ({ context }: SelectorQueryArgs) => [ + 'selectors', + 'bitbucket.repositories', + context.oauthCredential ?? 'none', + context.workflowId ?? 'none', + context.workspaceSlug ?? 'none', + ], + enabled: ({ context }) => Boolean(context.oauthCredential && context.workspaceSlug), + /** + * Repository discovery is scoped to the selected workspace on every page; + * no unscoped request is sent while the dependency is absent. + */ + fetchPage: async ({ context, cursor, signal }) => { + const credentialId = ensureCredential(context, 'bitbucket.repositories') + if (!context.workspaceSlug) { + throw new Error('Missing workspace slug for bitbucket.repositories selector') + } + + const data = await requestJson(selectorContracts.bitbucketRepositoriesSelectorContract, { + body: { + credential: credentialId, + workflowId: context.workflowId, + workspaceSlug: context.workspaceSlug, + cursor, + }, + signal, + }) + + return { + items: data.repositories.map((repository) => ({ + id: repository.slug, + label: repository.name, + meta: { + slug: repository.slug, + uuid: repository.uuid, + fullName: repository.fullName, + workspaceSlug: context.workspaceSlug, + }, + })), + nextCursor: data.nextCursor, + } + }, + }, +} satisfies Record< + Extract, + SelectorDefinition +> diff --git a/apps/sim/hooks/selectors/registry.ts b/apps/sim/hooks/selectors/registry.ts index 9c99d0cd9bc..ac1c943226e 100644 --- a/apps/sim/hooks/selectors/registry.ts +++ b/apps/sim/hooks/selectors/registry.ts @@ -2,6 +2,7 @@ import { airtableSelectors } from '@/hooks/selectors/providers/airtable/selector import { asanaSelectors } from '@/hooks/selectors/providers/asana/selectors' import { attioSelectors } from '@/hooks/selectors/providers/attio/selectors' import { bigquerySelectors } from '@/hooks/selectors/providers/bigquery/selectors' +import { bitbucketSelectors } from '@/hooks/selectors/providers/bitbucket/selectors' import { calcomSelectors } from '@/hooks/selectors/providers/calcom/selectors' import { clickupSelectors } from '@/hooks/selectors/providers/clickup/selectors' import { cloudwatchSelectors } from '@/hooks/selectors/providers/cloudwatch/selectors' @@ -37,6 +38,7 @@ export const selectorRegistry = { ...asanaSelectors, ...attioSelectors, ...bigquerySelectors, + ...bitbucketSelectors, ...calcomSelectors, ...confluenceSelectors, ...jsmSelectors, diff --git a/apps/sim/hooks/selectors/types.ts b/apps/sim/hooks/selectors/types.ts index a2f5954650c..402f9d6b698 100644 --- a/apps/sim/hooks/selectors/types.ts +++ b/apps/sim/hooks/selectors/types.ts @@ -10,6 +10,8 @@ export type SelectorKey = | 'attio.objects' | 'bigquery.datasets' | 'bigquery.tables' + | 'bitbucket.workspaces' + | 'bitbucket.repositories' | 'calcom.eventTypes' | 'calcom.schedules' | 'clickup.workspaces' @@ -121,6 +123,8 @@ export interface SelectorContext { schema?: string /** Zoho Desk organization (portal) id — the `orgId` header every Desk call but `/organizations` requires. */ orgId?: string + /** Bitbucket Cloud workspace slug that scopes repository discovery. */ + workspaceSlug?: string } export interface SelectorQueryArgs { diff --git a/apps/sim/lib/api/contracts/selectors/bitbucket.ts b/apps/sim/lib/api/contracts/selectors/bitbucket.ts new file mode 100644 index 00000000000..e0839fcfa24 --- /dev/null +++ b/apps/sim/lib/api/contracts/selectors/bitbucket.ts @@ -0,0 +1,187 @@ +import { z } from 'zod' +import { + credentialWorkflowBodySchema, + definePostSelector, + optionalString, +} from '@/lib/api/contracts/selectors/shared' +import type { ContractBody, ContractJsonResponse } from '@/lib/api/contracts/types' + +const BITBUCKET_API_ORIGIN = 'https://api.bitbucket.org' +const BITBUCKET_WORKSPACES_PATH = '/2.0/user/workspaces' +const BITBUCKET_REPOSITORIES_PATH = '/2.0/repositories' +const BITBUCKET_CURSOR_MAX_LENGTH = 4_096 + +export const BITBUCKET_SELECTOR_PAGE_SIZE = 100 + +const bitbucketSlugSchema = z + .string() + .trim() + .min(1, 'Bitbucket slug is required') + .max(255, 'Bitbucket slug must be 255 characters or fewer') + +const bitbucketRepositorySlugSchema = bitbucketSlugSchema.refine( + (slug) => slug.length <= 62, + 'Bitbucket repository slug must be 62 characters or fewer' +) + +/** + * Parses only absolute Bitbucket Cloud API URLs that are safe to receive an + * OAuth bearer token. Userinfo, fragments, non-default ports, lookalike hosts, + * and paths outside the v2 API are rejected. + */ +function parseBitbucketApiCursor(value: string): URL | null { + try { + const url = new URL(value) + if ( + url.origin !== BITBUCKET_API_ORIGIN || + url.username || + url.password || + url.hash || + !url.pathname.startsWith('/2.0/') + ) { + return null + } + return url + } catch { + return null + } +} + +/** Validates a provider cursor for the authenticated user's workspace stream. */ +export function isBitbucketWorkspacesCursor(value: string): boolean { + return parseBitbucketApiCursor(value)?.pathname === BITBUCKET_WORKSPACES_PATH +} + +/** + * Validates a repository cursor against the workspace dependency selected in + * the block, preventing a stale or crafted cursor from crossing workspaces. + */ +export function isBitbucketRepositoriesCursor(value: string, workspaceSlug: string): boolean { + const url = parseBitbucketApiCursor(value) + return url?.pathname === `${BITBUCKET_REPOSITORIES_PATH}/${encodeURIComponent(workspaceSlug)}` +} + +const bitbucketCursorSchema = z + .string() + .min(1, 'Bitbucket cursor cannot be empty') + .max(BITBUCKET_CURSOR_MAX_LENGTH, 'Bitbucket cursor is too long') + +export const bitbucketWorkspacesBodySchema = credentialWorkflowBodySchema.extend({ + cursor: bitbucketCursorSchema + .refine(isBitbucketWorkspacesCursor, 'Invalid Bitbucket workspaces cursor') + .optional(), +}) + +export const bitbucketRepositoriesBodySchema = credentialWorkflowBodySchema + .extend({ + workspaceSlug: bitbucketSlugSchema, + cursor: bitbucketCursorSchema.optional(), + }) + .superRefine((body, ctx) => { + if (body.cursor && !isBitbucketRepositoriesCursor(body.cursor, body.workspaceSlug)) { + ctx.addIssue({ + code: 'custom', + path: ['cursor'], + message: 'Invalid Bitbucket repositories cursor', + }) + } + }) + +const bitbucketUuidSchema = z.string().trim().min(1).max(100) +const bitbucketNameSchema = z.string().trim().min(1).max(512) + +const bitbucketWorkspaceProviderSchema = z + .object({ + administrator: z.boolean(), + workspace: z + .object({ + slug: bitbucketSlugSchema, + uuid: bitbucketUuidSchema, + name: bitbucketNameSchema.optional(), + }) + .passthrough(), + }) + .passthrough() + +const bitbucketRepositoryProviderSchema = z + .object({ + slug: bitbucketRepositorySlugSchema.optional(), + uuid: bitbucketUuidSchema, + name: bitbucketNameSchema, + full_name: bitbucketNameSchema, + }) + .passthrough() + .refine((repository) => { + const slash = repository.full_name.indexOf('/') + if (slash <= 0 || slash !== repository.full_name.lastIndexOf('/')) return false + const fullNameSlug = repository.full_name.slice(slash + 1) + return ( + bitbucketRepositorySlugSchema.safeParse(fullNameSlug).success && + (!repository.slug || repository.slug === fullNameSlug) + ) + }, 'Bitbucket repository full_name does not match its slug') + .transform((repository) => ({ + ...repository, + slug: repository.slug ?? repository.full_name.slice(repository.full_name.indexOf('/') + 1), + })) + +/** Strictly narrows the untrusted Bitbucket Cloud workspace page. */ +export const bitbucketWorkspaceProviderPageSchema = z + .object({ + values: z.array(bitbucketWorkspaceProviderSchema).max(BITBUCKET_SELECTOR_PAGE_SIZE), + next: bitbucketCursorSchema.optional(), + }) + .passthrough() + +/** Strictly narrows the untrusted Bitbucket Cloud repository page. */ +export const bitbucketRepositoryProviderPageSchema = z + .object({ + values: z.array(bitbucketRepositoryProviderSchema).max(BITBUCKET_SELECTOR_PAGE_SIZE), + next: bitbucketCursorSchema.optional(), + }) + .passthrough() + +const bitbucketWorkspaceSchema = z.object({ + slug: bitbucketSlugSchema, + uuid: bitbucketUuidSchema, + name: bitbucketNameSchema, + administrator: z.boolean(), +}) + +const bitbucketRepositorySchema = z.object({ + slug: bitbucketRepositorySlugSchema, + uuid: bitbucketUuidSchema, + name: bitbucketNameSchema, + fullName: bitbucketNameSchema, +}) + +export const bitbucketWorkspacesSelectorContract = definePostSelector( + '/api/tools/bitbucket/workspaces', + bitbucketWorkspacesBodySchema, + z.object({ + workspaces: z.array(bitbucketWorkspaceSchema).max(BITBUCKET_SELECTOR_PAGE_SIZE), + nextCursor: optionalString, + }) +) + +export const bitbucketRepositoriesSelectorContract = definePostSelector( + '/api/tools/bitbucket/repositories', + bitbucketRepositoriesBodySchema, + z.object({ + repositories: z.array(bitbucketRepositorySchema).max(BITBUCKET_SELECTOR_PAGE_SIZE), + nextCursor: optionalString, + }) +) + +export type BitbucketWorkspacesSelectorBody = ContractBody< + typeof bitbucketWorkspacesSelectorContract +> +export type BitbucketRepositoriesSelectorBody = ContractBody< + typeof bitbucketRepositoriesSelectorContract +> +export type BitbucketWorkspacesSelectorResponse = ContractJsonResponse< + typeof bitbucketWorkspacesSelectorContract +> +export type BitbucketRepositoriesSelectorResponse = ContractJsonResponse< + typeof bitbucketRepositoriesSelectorContract +> diff --git a/apps/sim/lib/api/contracts/selectors/index.ts b/apps/sim/lib/api/contracts/selectors/index.ts index 8924e5ba962..2e8cc1835db 100644 --- a/apps/sim/lib/api/contracts/selectors/index.ts +++ b/apps/sim/lib/api/contracts/selectors/index.ts @@ -11,6 +11,10 @@ import { bigQueryDatasetsSelectorContract, bigQueryTablesSelectorContract, } from '@/lib/api/contracts/selectors/bigquery' +import { + bitbucketRepositoriesSelectorContract, + bitbucketWorkspacesSelectorContract, +} from '@/lib/api/contracts/selectors/bitbucket' import { calcomEventTypesSelectorContract, calcomSchedulesSelectorContract, @@ -120,6 +124,7 @@ export * from '@/lib/api/contracts/selectors/airtable' export * from '@/lib/api/contracts/selectors/asana' export * from '@/lib/api/contracts/selectors/attio' export * from '@/lib/api/contracts/selectors/bigquery' +export * from '@/lib/api/contracts/selectors/bitbucket' export * from '@/lib/api/contracts/selectors/calcom' export * from '@/lib/api/contracts/selectors/clickup' export * from '@/lib/api/contracts/selectors/cloudwatch' @@ -153,6 +158,8 @@ export const selectorContractsByPath = { '/api/tools/attio/lists': attioListsSelectorContract, '/api/tools/google_bigquery/datasets': bigQueryDatasetsSelectorContract, '/api/tools/google_bigquery/tables': bigQueryTablesSelectorContract, + '/api/tools/bitbucket/workspaces': bitbucketWorkspacesSelectorContract, + '/api/tools/bitbucket/repositories': bitbucketRepositoriesSelectorContract, '/api/tools/calcom/event-types': calcomEventTypesSelectorContract, '/api/tools/calcom/schedules': calcomSchedulesSelectorContract, '/api/tools/clickup/workspaces': clickupWorkspacesSelectorContract, diff --git a/apps/sim/lib/auth/connectors/providers.ts b/apps/sim/lib/auth/connectors/providers.ts index 7db19580df6..8f65dac5412 100644 --- a/apps/sim/lib/auth/connectors/providers.ts +++ b/apps/sim/lib/auth/connectors/providers.ts @@ -79,6 +79,22 @@ interface AttioWorkspaceMemberResponse { } } +/** + * Shape of `GET https://api.bitbucket.org/2.0/user` for the authenticated user. + * @see https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-get + */ +interface BitbucketCurrentUserResponse { + account_id?: string | null + uuid?: string | null + display_name?: string | null + nickname?: string | null + links?: { + avatar?: { + href?: string | null + } + } +} + /** * Builds a Salesforce connector bound to one login host — `genericOAuth` takes * static endpoints, so each authorization server needs its own registration. @@ -1458,6 +1474,102 @@ export function buildConnectorProviders(): GenericOAuthConfig[] { }, }, + { + providerId: 'bitbucket', + clientId: env.BITBUCKET_CLIENT_ID as string, + clientSecret: env.BITBUCKET_CLIENT_SECRET as string, + authorizationUrl: 'https://bitbucket.org/site/oauth2/authorize', + tokenUrl: 'https://bitbucket.org/site/oauth2/access_token', + userInfoUrl: 'https://api.bitbucket.org/2.0/user', + scopes: getCanonicalScopesForProvider('bitbucket'), + responseType: 'code', + pkce: false, + authentication: 'basic', + accessTokenExpiresIn: 3600, + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/bitbucket`, + getToken: async ({ code, redirectURI }) => { + const basicAuth = Buffer.from( + `${env.BITBUCKET_CLIENT_ID as string}:${env.BITBUCKET_CLIENT_SECRET as string}` + ).toString('base64') + const response = await fetch('https://bitbucket.org/site/oauth2/access_token', { + method: 'POST', + headers: { + Authorization: `Basic ${basicAuth}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + code, + grant_type: 'authorization_code', + redirect_uri: redirectURI, + }).toString(), + }) + const data = await readResponseJsonWithLimit>(response, { + maxBytes: 1024 * 1024, + label: 'Bitbucket OAuth token response', + }) + + if (!response.ok || !isRecordLike(data)) { + logger.error('Bitbucket OAuth token exchange failed', { status: response.status }) + throw new Error(`Bitbucket OAuth token exchange failed with HTTP ${response.status}`) + } + + const tokens = getOAuth2Tokens(data) + if (!tokens.accessToken) { + throw new Error('Bitbucket OAuth token response did not include an access token') + } + + const grantedScopes = data.scopes ?? data.scope + if (typeof grantedScopes === 'string') { + tokens.scopes = grantedScopes.split(/\s+/).filter(Boolean) + } else if (Array.isArray(grantedScopes)) { + tokens.scopes = grantedScopes.filter( + (scope): scope is string => typeof scope === 'string' + ) + } + + return tokens + }, + getUserInfo: async (tokens) => { + try { + const response = await fetch('https://api.bitbucket.org/2.0/user', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + + if (!response.ok) { + await response.text().catch(() => {}) + logger.error('Error fetching Bitbucket user info:', { + status: response.status, + statusText: response.statusText, + }) + return null + } + + const data: BitbucketCurrentUserResponse = await response.json() + const stableId = data.account_id ?? data.uuid + if (!stableId) { + logger.error('Bitbucket user info did not include an account_id or uuid') + return null + } + + const now = new Date() + return { + id: `${stableId}-${generateId()}`, + name: data.display_name || data.nickname || 'Bitbucket User', + email: syntheticConnectorEmail('bitbucket', stableId), + image: data.links?.avatar?.href || undefined, + emailVerified: false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Bitbucket getUserInfo:', { error }) + return null + } + }, + }, + { providerId: 'notion', clientId: env.NOTION_CLIENT_ID as string, diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 8d9d784bd32..8ca857023bf 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -431,6 +431,8 @@ export const env = createEnv({ ASANA_CLIENT_SECRET: z.string().optional(), // Asana OAuth client secret AIRTABLE_CLIENT_ID: z.string().optional(), // Airtable OAuth client ID AIRTABLE_CLIENT_SECRET: z.string().optional(), // Airtable OAuth client secret + BITBUCKET_CLIENT_ID: z.string().optional(), // Bitbucket OAuth consumer key + BITBUCKET_CLIENT_SECRET: z.string().optional(), // Bitbucket OAuth consumer secret APOLLO_API_KEY: z.string().optional(), // Apollo API key (optional system-wide config) SUPABASE_CLIENT_ID: z.string().optional(), // Supabase OAuth client ID SUPABASE_CLIENT_SECRET: z.string().optional(), // Supabase OAuth client secret diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts index 6bb0b44589b..7492abbd04c 100644 --- a/apps/sim/lib/integrations/icon-mapping.ts +++ b/apps/sim/lib/integrations/icon-mapping.ts @@ -24,6 +24,7 @@ import { AttioIcon, AzureDataExplorerIcon, AzureIcon, + BitbucketIcon, BoxCompanyIcon, BrainIcon, BrandfetchIcon, @@ -285,6 +286,7 @@ export const blockTypeToIconMap: Record = { attio: AttioIcon, azure_data_explorer: AzureDataExplorerIcon, azure_devops: AzureIcon, + bitbucket: BitbucketIcon, box: BoxCompanyIcon, brandfetch: BrandfetchIcon, brex: BrexIcon, diff --git a/apps/sim/lib/integrations/oauth-service.test.ts b/apps/sim/lib/integrations/oauth-service.test.ts index f5a5e2dc661..22a70eceef3 100644 --- a/apps/sim/lib/integrations/oauth-service.test.ts +++ b/apps/sim/lib/integrations/oauth-service.test.ts @@ -24,6 +24,7 @@ const EXPECTED_PROVIDER_BY_SLUG: Record = { asana: 'asana', attio: 'attio', 'azure-ad': 'microsoft-ad', + bitbucket: 'bitbucket', box: 'box', 'cal-com': 'calcom', confluence: 'confluence', diff --git a/apps/sim/lib/oauth/oauth.test.ts b/apps/sim/lib/oauth/oauth.test.ts index cabdaf0daa5..df10a3d3d01 100644 --- a/apps/sim/lib/oauth/oauth.test.ts +++ b/apps/sim/lib/oauth/oauth.test.ts @@ -1,8 +1,10 @@ +import { getOAuth2Tokens } from '@better-auth/core/oauth2' import { createMockFetch, resetEnvMock, setEnv } from '@sim/testing' import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' beforeAll(() => { setEnv({ + NEXT_PUBLIC_APP_URL: 'http://localhost:3000', GOOGLE_CLIENT_ID: 'google_client_id', GOOGLE_CLIENT_SECRET: 'google_client_secret', GITHUB_CLIENT_ID: 'github_client_id', @@ -19,6 +21,8 @@ beforeAll(() => { JIRA_CLIENT_SECRET: 'jira_client_secret', AIRTABLE_CLIENT_ID: 'airtable_client_id', AIRTABLE_CLIENT_SECRET: 'airtable_client_secret', + BITBUCKET_CLIENT_ID: 'bitbucket_client_id', + BITBUCKET_CLIENT_SECRET: 'bitbucket_client_secret', NOTION_CLIENT_ID: 'notion_client_id', NOTION_CLIENT_SECRET: 'notion_client_secret', MICROSOFT_CLIENT_ID: 'microsoft_client_id', @@ -64,6 +68,7 @@ beforeAll(() => { afterAll(resetEnvMock) import { GoogleIcon, GoogleVaultIcon } from '@/components/icons' +import { buildConnectorProviders } from '@/lib/auth/connectors/providers' import { DEFAULT_MAX_ERROR_BODY_BYTES } from '@/lib/core/utils/stream-limits' import { OAUTH_PROVIDERS, refreshOAuthToken } from '@/lib/oauth' import { REDDIT_USER_AGENT } from '@/tools/reddit/constants' @@ -100,6 +105,121 @@ describe('OAuth Provider Branding', () => { }) }) +function getBitbucketConnector() { + const connector = buildConnectorProviders().find( + (candidate) => candidate.providerId === 'bitbucket' + ) + if (!connector) throw new Error('Bitbucket OAuth connector is not configured in this test') + return connector +} + +describe('Bitbucket OAuth Connector', () => { + it('uses the canonical endpoints, scopes, Basic auth, and one-hour expiry', () => { + expect(getBitbucketConnector()).toMatchObject({ + providerId: 'bitbucket', + authorizationUrl: 'https://bitbucket.org/site/oauth2/authorize', + tokenUrl: 'https://bitbucket.org/site/oauth2/access_token', + userInfoUrl: 'https://api.bitbucket.org/2.0/user', + scopes: [ + 'account', + 'repository', + 'repository:write', + 'pullrequest', + 'pullrequest:write', + 'pipeline', + 'pipeline:write', + ], + responseType: 'code', + pkce: false, + authentication: 'basic', + accessTokenExpiresIn: 3600, + redirectURI: 'http://localhost:3000/api/auth/oauth2/callback/bitbucket', + }) + }) + + it('exchanges the authorization code with Basic auth and normalizes plural scopes', async () => { + const connector = getBitbucketConnector() + const getToken = connector.getToken + if (!getToken) throw new Error('Bitbucket connector must define getToken') + + const scopes = [ + 'account', + 'repository', + 'repository:write', + 'pullrequest', + 'pullrequest:write', + 'pipeline', + 'pipeline:write', + ] + const mockFetch = createMockFetch({ + json: { + access_token: 'bitbucket_access_token', + expires_in: 3600, + refresh_token: 'bitbucket_refresh_token', + scopes: scopes.join(' '), + token_type: 'bearer', + }, + }) + + const tokens = await withMockFetch(mockFetch, () => + getToken({ + code: 'authorization_code', + redirectURI: 'http://localhost:3000/api/auth/oauth2/callback/bitbucket', + }) + ) + + expect(tokens.accessToken).toBe('bitbucket_access_token') + expect(tokens.refreshToken).toBe('bitbucket_refresh_token') + expect(tokens.scopes).toEqual(scopes) + expect(tokens.accessTokenExpiresAt).toBeInstanceOf(Date) + + const [endpoint, requestOptions] = mockFetch.mock.calls[0] as [ + string, + { headers: Record; body: string }, + ] + expect(endpoint).toBe('https://bitbucket.org/site/oauth2/access_token') + expect(requestOptions.headers.Authorization).toBe( + `Basic ${Buffer.from('bitbucket_client_id:bitbucket_client_secret').toString('base64')}` + ) + expect(Object.fromEntries(new URLSearchParams(requestOptions.body))).toEqual({ + code: 'authorization_code', + grant_type: 'authorization_code', + redirect_uri: 'http://localhost:3000/api/auth/oauth2/callback/bitbucket', + }) + }) + + it('uses account_id before uuid and always synthesizes an internal email', async () => { + const connector = getBitbucketConnector() + const getUserInfo = connector.getUserInfo + if (!getUserInfo) throw new Error('Bitbucket connector must define getUserInfo') + const tokens = getOAuth2Tokens({ access_token: 'bitbucket_access_token' }) + + const accountIdentity = await withMockFetch( + createMockFetch({ + json: { + account_id: 'account-123', + uuid: '{uuid-ignored}', + display_name: 'Ada Lovelace', + links: { avatar: { href: 'https://example.invalid/avatar.png' } }, + }, + }), + () => getUserInfo(tokens) + ) + expect(accountIdentity?.id).toMatch(/^account-123-/) + expect(accountIdentity?.email).toBe('bitbucket-account-123@connectors.sim.invalid') + expect(accountIdentity?.name).toBe('Ada Lovelace') + expect(accountIdentity?.image).toBe('https://example.invalid/avatar.png') + + const uuidIdentity = await withMockFetch( + createMockFetch({ json: { uuid: '{uuid-456}', nickname: 'grace' } }), + () => getUserInfo(tokens) + ) + expect(uuidIdentity?.id).toMatch(/^\{uuid-456\}-/) + expect(uuidIdentity?.email).toBe('bitbucket-uuid-456@connectors.sim.invalid') + expect(uuidIdentity?.name).toBe('grace') + }) +}) + describe('OAuth Token Refresh', () => { describe('Basic Auth Providers', () => { const basicAuthProviders = [ @@ -108,6 +228,11 @@ describe('OAuth Token Refresh', () => { providerId: 'airtable', endpoint: 'https://airtable.com/oauth2/v1/token', }, + { + name: 'Bitbucket', + providerId: 'bitbucket', + endpoint: 'https://bitbucket.org/site/oauth2/access_token', + }, { name: 'X (Twitter)', providerId: 'x', endpoint: 'https://api.x.com/2/oauth2/token' }, { name: 'Confluence', @@ -502,6 +627,27 @@ describe('OAuth Token Refresh', () => { }) }) + it.concurrent('should return Bitbucket rotating refresh tokens', async () => { + const mockFetch = createMockFetch({ + json: { + access_token: 'new_bitbucket_access_token', + expires_in: 3600, + refresh_token: 'rotated_bitbucket_refresh_token', + }, + }) + + const result = await withMockFetch(mockFetch, () => + refreshOAuthToken('bitbucket', 'old_bitbucket_refresh_token') + ) + + expect(result).toEqual({ + ok: true, + accessToken: 'new_bitbucket_access_token', + expiresIn: 3600, + refreshToken: 'rotated_bitbucket_refresh_token', + }) + }) + it.concurrent( 'should rotate refresh token for Microsoft providers (microsoft, outlook, onedrive, sharepoint)', async () => { diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 1414440c3fb..4d927c1d0c2 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -7,6 +7,7 @@ import { AtlassianIcon, AttioIcon, AzureIcon, + BitbucketIcon, BoxCompanyIcon, CalComIcon, ClaudeIcon, @@ -689,6 +690,29 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'airtable', }, + bitbucket: { + name: 'Bitbucket', + icon: BitbucketIcon, + services: { + bitbucket: { + name: 'Bitbucket', + description: 'Read repositories, collaborate on pull requests, and manage pipelines.', + providerId: 'bitbucket', + icon: BitbucketIcon, + baseProviderIcon: BitbucketIcon, + scopes: [ + 'account', + 'repository', + 'repository:write', + 'pullrequest', + 'pullrequest:write', + 'pipeline', + 'pipeline:write', + ], + }, + }, + defaultService: 'bitbucket', + }, notion: { name: 'Notion', icon: NotionIcon, @@ -1443,6 +1467,20 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: true, } } + case 'bitbucket': { + const { clientId, clientSecret } = getConfiguredClientCredentials( + 'bitbucket', + 'BITBUCKET_CLIENT_ID', + 'BITBUCKET_CLIENT_SECRET' + ) + return { + tokenEndpoint: 'https://bitbucket.org/site/oauth2/access_token', + clientId, + clientSecret, + useBasicAuth: true, + supportsRefreshTokenRotation: true, + } + } case 'notion': { const { clientId, clientSecret } = getConfiguredClientCredentials( 'notion', diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index 628ae367c49..129164cb10f 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -51,6 +51,7 @@ export type OAuthProvider = | 'tiktok' | 'confluence' | 'airtable' + | 'bitbucket' | 'notion' | 'jira' | 'atlassian-service-account' @@ -107,6 +108,7 @@ export type OAuthService = | 'tiktok' | 'confluence' | 'airtable' + | 'bitbucket' | 'notion' | 'jira' | 'atlassian-service-account' diff --git a/apps/sim/lib/oauth/utils.test.ts b/apps/sim/lib/oauth/utils.test.ts index 2193efebac1..760b91ec51d 100644 --- a/apps/sim/lib/oauth/utils.test.ts +++ b/apps/sim/lib/oauth/utils.test.ts @@ -8,6 +8,7 @@ import { getCanonicalScopesForProvider, getMissingRequiredScopes, getProviderIdFromServiceId, + getScopeDescription, getScopesForService, getServiceByProviderAndId, getServiceConfigByProviderId, @@ -384,6 +385,20 @@ describe('getCanonicalScopesForProvider', () => { }) }) +describe('getScopeDescription', () => { + it.concurrent('uses provider-specific labels for Bitbucket scope names', () => { + expect(getScopeDescription('account', 'bitbucket')).toBe( + 'View your Bitbucket account and workspace memberships' + ) + expect(getScopeDescription('pipeline:write', 'bitbucket')).toBe('Run and stop pipelines') + }) + + it.concurrent('preserves the existing Reddit meaning of the account scope', () => { + expect(getScopeDescription('account', 'reddit')).toBe('Update account preferences and settings') + expect(getScopeDescription('account')).toBe('Update account preferences and settings') + }) +}) + describe('parseProvider', () => { it.concurrent('should parse simple provider without hyphen', () => { const config = parseProvider('slack' as OAuthProvider) diff --git a/apps/sim/lib/oauth/utils.ts b/apps/sim/lib/oauth/utils.ts index 5166a3861fe..83452b68560 100644 --- a/apps/sim/lib/oauth/utils.ts +++ b/apps/sim/lib/oauth/utils.ts @@ -470,12 +470,27 @@ export const SCOPE_DESCRIPTIONS: Record = { 'me:read': 'Read your user profile', } +/** Scope labels that cannot be keyed by scope alone because providers reuse names. */ +const PROVIDER_SCOPE_DESCRIPTIONS: Readonly>>> = { + bitbucket: { + account: 'View your Bitbucket account and workspace memberships', + repository: 'View repositories and source code', + 'repository:write': 'Create and modify repositories, branches, and source code', + pullrequest: 'View pull requests, comments, approvals, and statuses', + 'pullrequest:write': 'Create, update, approve, decline, and merge pull requests', + pipeline: 'View pipelines, steps, and logs', + 'pipeline:write': 'Run and stop pipelines', + }, +} + /** * Get a human-readable description for a scope. * Falls back to the raw scope string if no description is found. */ -export function getScopeDescription(scope: string): string { - return SCOPE_DESCRIPTIONS[scope] || scope +export function getScopeDescription(scope: string, providerId?: string): string { + return ( + PROVIDER_SCOPE_DESCRIPTIONS[providerId ?? '']?.[scope] || SCOPE_DESCRIPTIONS[scope] || scope + ) } /** diff --git a/apps/sim/lib/workflows/subblocks/context.test.ts b/apps/sim/lib/workflows/subblocks/context.test.ts index 991520f9963..515856e121e 100644 --- a/apps/sim/lib/workflows/subblocks/context.test.ts +++ b/apps/sim/lib/workflows/subblocks/context.test.ts @@ -123,6 +123,35 @@ describe('buildSelectorContextFromBlock', () => { expect(ctx.jobId).toBe('job-7') }) + it('exposes the active Bitbucket workspace slug to repository selectors', () => { + const subBlocks = { + operation: { + id: 'operation', + type: 'dropdown', + value: 'bitbucket_get_repository', + }, + workspacePicker: { + id: 'workspacePicker', + type: 'project-selector', + value: 'acme-platform', + }, + workspaceSlugInput: { + id: 'workspaceSlugInput', + type: 'short-input', + value: 'advanced-team', + }, + } + + expect(buildSelectorContextFromBlock('bitbucket', subBlocks).workspaceSlug).toBe( + 'acme-platform' + ) + expect( + buildSelectorContextFromBlock('bitbucket', subBlocks, { + canonicalModes: { workspaceSlug: 'advanced' }, + }).workspaceSlug + ).toBe('advanced-team') + }) + it('should ignore subblock keys not in SELECTOR_CONTEXT_FIELDS', () => { const ctx = buildSelectorContextFromBlock('knowledge', { operation: { id: 'operation', type: 'dropdown', value: 'search' }, diff --git a/apps/sim/lib/workflows/subblocks/context.ts b/apps/sim/lib/workflows/subblocks/context.ts index 44552000d36..f466a68be22 100644 --- a/apps/sim/lib/workflows/subblocks/context.ts +++ b/apps/sim/lib/workflows/subblocks/context.ts @@ -41,6 +41,7 @@ export const SELECTOR_CONTEXT_FIELDS = new Set([ 'orgId', 'database', 'schema', + 'workspaceSlug', ]) /** diff --git a/apps/sim/tools/bitbucket/approve_pull_request.ts b/apps/sim/tools/bitbucket/approve_pull_request.ts new file mode 100644 index 00000000000..40d333422fa --- /dev/null +++ b/apps/sim/tools/bitbucket/approve_pull_request.ts @@ -0,0 +1,46 @@ +import { + BITBUCKET_PARTICIPANT_OUTPUT_PROPERTIES, + type BitbucketParticipant, + type BitbucketPullRequestParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PULL_REQUEST_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketPullRequestPath, + normalizeBitbucketParticipant, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketApprovePullRequestTool: ToolConfig< + BitbucketPullRequestParams, + BitbucketToolResponse<{ participant: BitbucketParticipant }> +> = { + id: 'bitbucket_approve_pull_request', + name: 'Bitbucket Approve Pull Request', + description: 'Approve a pull request as the authenticated account', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest:write'] }, + params: { ...BITBUCKET_PULL_REQUEST_PARAMS }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/approve`, + method: 'POST', + headers: (params) => bitbucketHeaders(params.accessToken), + }, + transformResponse: async (response) => ({ + success: true, + output: { participant: normalizeBitbucketParticipant(await bitbucketJson(response)) }, + }), + outputs: { + participant: { + type: 'object', + description: 'Approval participant record', + properties: BITBUCKET_PARTICIPANT_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/create_branch.ts b/apps/sim/tools/bitbucket/create_branch.ts new file mode 100644 index 00000000000..7a4efdcdbcf --- /dev/null +++ b/apps/sim/tools/bitbucket/create_branch.ts @@ -0,0 +1,65 @@ +import { + BITBUCKET_BRANCH_OUTPUT_PROPERTIES, + type BitbucketBranch, + type BitbucketCreateBranchParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + normalizeBitbucketBranch, + requireBitbucketString, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketCreateBranchTool: ToolConfig< + BitbucketCreateBranchParams, + BitbucketToolResponse<{ branch: BitbucketBranch }> +> = { + id: 'bitbucket_create_branch', + name: 'Bitbucket Create Branch', + description: 'Create a branch at a commit hash or existing ref', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository:write'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New branch name without refs/heads prefix', + }, + target: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full commit hash or existing ref to target', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/refs/branches`, + method: 'POST', + headers: (params) => bitbucketHeaders(params.accessToken, { json: true }), + body: (params) => ({ + name: requireBitbucketString(params.name, 'name'), + target: { hash: requireBitbucketString(params.target, 'target') }, + }), + }, + transformResponse: async (response) => ({ + success: true, + output: { branch: normalizeBitbucketBranch(await bitbucketJson(response)) }, + }), + outputs: { + branch: { + type: 'object', + description: 'Created branch', + properties: BITBUCKET_BRANCH_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/create_pull_request.ts b/apps/sim/tools/bitbucket/create_pull_request.ts new file mode 100644 index 00000000000..065ae9a5c2e --- /dev/null +++ b/apps/sim/tools/bitbucket/create_pull_request.ts @@ -0,0 +1,121 @@ +import { + BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + type BitbucketCreatePullRequestParams, + type BitbucketPullRequest, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + normalizeBitbucketPullRequest, + requireBitbucketString, +} from '@/tools/bitbucket/utils' +import { + optionalBitbucketBoolean, + optionalBitbucketStringArray, +} from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketCreatePullRequestTool: ToolConfig< + BitbucketCreatePullRequestParams, + BitbucketToolResponse<{ pullRequest: BitbucketPullRequest }> +> = { + id: 'bitbucket_create_pull_request', + name: 'Bitbucket Create Pull Request', + description: 'Create a pull request between repository branches', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest:write'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Pull request title', + }, + sourceBranch: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Source branch name', + }, + destinationBranch: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Destination branch name', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pull request description', + }, + closeSourceBranch: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Close the source branch after merge', + }, + draft: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Create the pull request as a draft', + }, + reviewerUuids: { + type: 'array', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket account UUIDs to add as reviewers', + items: { type: 'string' }, + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/pullrequests`, + method: 'POST', + headers: (params) => bitbucketHeaders(params.accessToken, { json: true }), + body: (params) => { + const closeSourceBranch = optionalBitbucketBoolean( + params.closeSourceBranch, + 'closeSourceBranch' + ) + const draft = optionalBitbucketBoolean(params.draft, 'draft') + const reviewerUuids = optionalBitbucketStringArray( + params.reviewerUuids, + 'reviewerUuids', + 'reviewer UUID' + ) + return { + title: requireBitbucketString(params.title, 'title'), + source: { branch: { name: requireBitbucketString(params.sourceBranch, 'sourceBranch') } }, + destination: { + branch: { name: requireBitbucketString(params.destinationBranch, 'destinationBranch') }, + }, + ...(params.description !== undefined ? { description: params.description } : {}), + ...(closeSourceBranch !== undefined ? { close_source_branch: closeSourceBranch } : {}), + ...(draft !== undefined ? { draft } : {}), + ...(reviewerUuids !== undefined + ? { reviewers: reviewerUuids.map((uuid) => ({ uuid })) } + : {}), + } + }, + }, + transformResponse: async (response) => ({ + success: true, + output: { pullRequest: normalizeBitbucketPullRequest(await bitbucketJson(response)) }, + }), + outputs: { + pullRequest: { + type: 'object', + description: 'Created pull request', + properties: BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/create_pull_request_comment.ts b/apps/sim/tools/bitbucket/create_pull_request_comment.ts new file mode 100644 index 00000000000..0781be26989 --- /dev/null +++ b/apps/sim/tools/bitbucket/create_pull_request_comment.ts @@ -0,0 +1,72 @@ +import { + BITBUCKET_COMMENT_OUTPUT_PROPERTIES, + type BitbucketComment, + type BitbucketCreatePullRequestCommentParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PULL_REQUEST_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketPullRequestPath, + normalizeBitbucketComment, +} from '@/tools/bitbucket/utils' +import { requireBitbucketPositiveInteger } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketCreatePullRequestCommentTool: ToolConfig< + BitbucketCreatePullRequestCommentParams, + BitbucketToolResponse<{ comment: BitbucketComment }> +> = { + id: 'bitbucket_create_pull_request_comment', + name: 'Bitbucket Create Pull Request Comment', + description: 'Create a global comment or reply on a pull request', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest'] }, + params: { + ...BITBUCKET_PULL_REQUEST_PARAMS, + content: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Raw comment content', + }, + parentId: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Parent comment ID when creating a reply', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/comments`, + method: 'POST', + headers: (params) => bitbucketHeaders(params.accessToken, { json: true }), + body: (params) => { + if (typeof params.content !== 'string' || params.content.trim().length === 0) { + throw new Error('content must be a non-empty string') + } + return { + content: { raw: params.content }, + ...(params.parentId !== undefined + ? { parent: { id: requireBitbucketPositiveInteger(params.parentId, 'parentId') } } + : {}), + } + }, + }, + transformResponse: async (response) => ({ + success: true, + output: { comment: normalizeBitbucketComment(await bitbucketJson(response)) }, + }), + outputs: { + comment: { + type: 'object', + description: 'Created comment', + properties: BITBUCKET_COMMENT_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/decline_pull_request.ts b/apps/sim/tools/bitbucket/decline_pull_request.ts new file mode 100644 index 00000000000..532e20aeb50 --- /dev/null +++ b/apps/sim/tools/bitbucket/decline_pull_request.ts @@ -0,0 +1,46 @@ +import { + BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + type BitbucketPullRequest, + type BitbucketPullRequestParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PULL_REQUEST_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketPullRequestPath, + normalizeBitbucketPullRequest, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketDeclinePullRequestTool: ToolConfig< + BitbucketPullRequestParams, + BitbucketToolResponse<{ pullRequest: BitbucketPullRequest }> +> = { + id: 'bitbucket_decline_pull_request', + name: 'Bitbucket Decline Pull Request', + description: 'Decline an open pull request', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest:write'] }, + params: { ...BITBUCKET_PULL_REQUEST_PARAMS }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/decline`, + method: 'POST', + headers: (params) => bitbucketHeaders(params.accessToken), + }, + transformResponse: async (response) => ({ + success: true, + output: { pullRequest: normalizeBitbucketPullRequest(await bitbucketJson(response)) }, + }), + outputs: { + pullRequest: { + type: 'object', + description: 'Declined pull request', + properties: BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/delete_branch.ts b/apps/sim/tools/bitbucket/delete_branch.ts new file mode 100644 index 00000000000..f7cc1b8cf56 --- /dev/null +++ b/apps/sim/tools/bitbucket/delete_branch.ts @@ -0,0 +1,39 @@ +import type { BitbucketDeleteBranchParams, BitbucketToolResponse } from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketRepositoryPath, + encodeBitbucketSegment, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketDeleteBranchTool: ToolConfig< + BitbucketDeleteBranchParams, + BitbucketToolResponse<{ deleted: boolean }> +> = { + id: 'bitbucket_delete_branch', + name: 'Bitbucket Delete Branch', + description: 'Delete a branch from a Bitbucket Cloud repository', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository:write'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Branch name to delete', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/refs/branches/${encodeBitbucketSegment(params.name, 'name')}`, + method: 'DELETE', + headers: (params) => bitbucketHeaders(params.accessToken), + }, + transformResponse: async () => ({ success: true, output: { deleted: true } }), + outputs: { deleted: { type: 'boolean', description: 'Whether the branch was deleted' } }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_commit.ts b/apps/sim/tools/bitbucket/get_commit.ts new file mode 100644 index 00000000000..f7620a71687 --- /dev/null +++ b/apps/sim/tools/bitbucket/get_commit.ts @@ -0,0 +1,58 @@ +import { + BITBUCKET_COMMIT_OUTPUT_PROPERTIES, + type BitbucketCommit, + type BitbucketGetCommitParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + encodeBitbucketSegment, + normalizeBitbucketCommit, +} from '@/tools/bitbucket/utils' +import { requireBitbucketSha1 } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketGetCommitTool: ToolConfig< + BitbucketGetCommitParams, + BitbucketToolResponse<{ commit: BitbucketCommit }> +> = { + id: 'bitbucket_get_commit', + name: 'Bitbucket Get Commit', + description: 'Get a repository commit by its full SHA-1', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + commit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full 40-character commit SHA-1', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/commit/${encodeBitbucketSegment(requireBitbucketSha1(params.commit, 'commit'), 'commit')}`, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: { commit: normalizeBitbucketCommit(await bitbucketJson(response)) }, + }), + outputs: { + commit: { + type: 'object', + description: 'Commit details', + properties: BITBUCKET_COMMIT_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_file.ts b/apps/sim/tools/bitbucket/get_file.ts new file mode 100644 index 00000000000..72bef8e16e7 --- /dev/null +++ b/apps/sim/tools/bitbucket/get_file.ts @@ -0,0 +1,157 @@ +import type { BitbucketGetFileParams, BitbucketToolResponse } from '@/tools/bitbucket/types' +import { + assertBitbucketResponseOk, + BITBUCKET_API_BASE, + BITBUCKET_DEFAULT_MAX_CHARACTERS, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_RAW_TRANSFER_MAX_BYTES, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketHeadRange, + bitbucketJson, + bitbucketMaxCharacters, + bitbucketRawHead, + bitbucketRepositoryPath, + encodeBitbucketRepositoryPath, + encodeBitbucketSegment, + normalizeBitbucketFileMetadata, +} from '@/tools/bitbucket/utils' +import { requireBitbucketSha1 } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +interface BitbucketFileOutput { + content: string | null + binary: boolean | null + truncated: boolean | null + returnedBytes: number + fullBytes: number | null + contentType: string | null +} + +function fileUrl(params: BitbucketGetFileParams, metadata = false): string { + const commit = requireBitbucketSha1(params.commit, 'commit') + const url = `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/src/${encodeBitbucketSegment(commit, 'commit')}/${encodeBitbucketRepositoryPath(params.path)}` + return metadata ? `${url}?format=meta` : url +} + +export const bitbucketGetFileTool: ToolConfig< + BitbucketGetFileParams, + BitbucketToolResponse +> = { + id: 'bitbucket_get_file', + name: 'Bitbucket Get File', + description: 'Read bounded UTF-8 text from a file at a full repository commit SHA-1', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + commit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full 40-character commit SHA-1', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository-relative file path', + }, + maxCharacters: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum text characters to return (1-500000)', + default: BITBUCKET_DEFAULT_MAX_CHARACTERS, + }, + }, + directExecution: async (params, signal) => { + bitbucketMaxCharacters(params.maxCharacters) + const { secureBitbucketRead } = await import('@/tools/bitbucket/utils.server') + const metadataResponse = await secureBitbucketRead( + fileUrl(params, true), + bitbucketHeaders(params.accessToken), + 256 * 1024, + { signal } + ) + await assertBitbucketResponseOk(metadataResponse) + const metadata = normalizeBitbucketFileMetadata(await bitbucketJson(metadataResponse)) + if (metadata.isBinary === true) { + return { + success: true, + output: { + content: null, + binary: true, + truncated: metadata.size === null ? null : metadata.size > 0, + returnedBytes: 0, + fullBytes: metadata.size, + contentType: null, + }, + } + } + + const rawResponse = await secureBitbucketRead( + fileUrl(params), + bitbucketHeaders(params.accessToken, { + json: false, + range: bitbucketHeadRange(params.maxCharacters), + }), + BITBUCKET_RAW_TRANSFER_MAX_BYTES, + { stripAuthOnRedirect: true, signal } + ) + await assertBitbucketResponseOk(rawResponse) + const raw = await bitbucketRawHead(rawResponse, params.maxCharacters, metadata.isBinary) + const fullBytes = raw.fullBytes ?? metadata.size + return { + success: true, + output: { + ...raw, + truncated: + raw.binary === true && raw.truncated === null && fullBytes !== null + ? fullBytes > 0 + : raw.truncated, + fullBytes, + }, + } + }, + request: { + url: (params) => fileUrl(params), + method: 'GET', + headers: (params) => + bitbucketHeaders(params.accessToken, { + json: false, + range: bitbucketHeadRange(params.maxCharacters), + }), + retry: BITBUCKET_READ_RETRY, + stripAuthOnRedirect: true, + }, + transformResponse: async () => { + throw new Error('Bitbucket file reads require the metadata preflight direct execution path') + }, + outputs: { + content: { + type: 'string', + description: 'Bounded UTF-8 file text; null for binary content', + nullable: true, + }, + binary: { + type: 'boolean', + description: 'Whether documented metadata identifies binary content; null when unknown', + nullable: true, + }, + truncated: { + type: 'boolean', + description: 'Whether later content was omitted; null when binary size is unknown', + nullable: true, + }, + returnedBytes: { type: 'number', description: 'Provider bytes read for the returned file' }, + fullBytes: { + type: 'number', + description: 'Full file byte size when reported', + nullable: true, + }, + contentType: { type: 'string', description: 'Response MIME type', nullable: true }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_file_metadata.ts b/apps/sim/tools/bitbucket/get_file_metadata.ts new file mode 100644 index 00000000000..fb37950944e --- /dev/null +++ b/apps/sim/tools/bitbucket/get_file_metadata.ts @@ -0,0 +1,65 @@ +import { + BITBUCKET_FILE_METADATA_OUTPUT_PROPERTIES, + type BitbucketFileMetadata, + type BitbucketFileParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + encodeBitbucketRepositoryPath, + encodeBitbucketSegment, + normalizeBitbucketFileMetadata, +} from '@/tools/bitbucket/utils' +import { requireBitbucketSha1 } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketGetFileMetadataTool: ToolConfig< + BitbucketFileParams, + BitbucketToolResponse<{ file: BitbucketFileMetadata }> +> = { + id: 'bitbucket_get_file_metadata', + name: 'Bitbucket Get File Metadata', + description: 'Inspect file size and attributes at a full repository commit SHA-1', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + commit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full 40-character commit SHA-1', + }, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository-relative file path', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/src/${encodeBitbucketSegment(requireBitbucketSha1(params.commit, 'commit'), 'commit')}/${encodeBitbucketRepositoryPath(params.path)}?format=meta`, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: { file: normalizeBitbucketFileMetadata(await bitbucketJson(response)) }, + }), + outputs: { + file: { + type: 'object', + description: 'File metadata', + properties: BITBUCKET_FILE_METADATA_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_merge_task_status.ts b/apps/sim/tools/bitbucket/get_merge_task_status.ts new file mode 100644 index 00000000000..e7b0194bc31 --- /dev/null +++ b/apps/sim/tools/bitbucket/get_merge_task_status.ts @@ -0,0 +1,105 @@ +import { + BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + type BitbucketGetMergeTaskStatusParams, + type BitbucketPullRequest, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PULL_REQUEST_PARAMS, + BITBUCKET_READ_RETRY, + bitbucketHeaders, + bitbucketJson, + bitbucketPullRequestPath, + encodeBitbucketSegment, + normalizeBitbucketPullRequest, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +interface BitbucketMergeTaskOutput { + taskStatus: 'PENDING' | 'SUCCESS' + selfUrl: string | null + mergeResult: BitbucketPullRequest | null +} + +function record(value: unknown): Record | null { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? (value as Record) + : null +} + +function stringField(value: unknown): string | null { + return typeof value === 'string' ? value : null +} + +export const bitbucketGetMergeTaskStatusTool: ToolConfig< + BitbucketGetMergeTaskStatusParams, + BitbucketToolResponse +> = { + id: 'bitbucket_get_pull_request_merge_task_status', + name: 'Bitbucket Get Merge Task Status', + description: 'Poll the status of an asynchronous pull request merge task', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest'] }, + params: { + ...BITBUCKET_PULL_REQUEST_PARAMS, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Merge task ID returned by Bitbucket Merge Pull Request', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/merge/task-status/${encodeBitbucketSegment(params.taskId, 'taskId')}`, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => { + const data = await bitbucketJson(response) + if (data.type === 'error') { + const message = stringField(record(data.error)?.message) + if (!message?.trim()) throw new Error('Bitbucket returned a malformed merge task error') + throw new Error(message) + } + + const taskStatus = data.task_status + if (taskStatus !== 'PENDING' && taskStatus !== 'SUCCESS') { + throw new Error('Bitbucket merge task status must be PENDING or SUCCESS') + } + + const links = record(data.links) + const self = record(links?.self) + let mergeResult: BitbucketPullRequest | null = null + if (taskStatus === 'SUCCESS') { + const result = record(data.merge_result) + if (!result) throw new Error('Bitbucket successful merge task omitted merge_result') + mergeResult = normalizeBitbucketPullRequest(result) + } else if (data.merge_result !== undefined && data.merge_result !== null) { + throw new Error('Bitbucket pending merge task returned an unexpected merge_result') + } + + return { + success: true, + output: { + taskStatus, + selfUrl: stringField(self?.href), + mergeResult, + }, + } + }, + outputs: { + taskStatus: { type: 'string', description: 'PENDING or SUCCESS' }, + selfUrl: { type: 'string', description: 'Merge task API URL', nullable: true }, + mergeResult: { + type: 'object', + description: 'Merged pull request when the task succeeds', + nullable: true, + properties: BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_pipeline.ts b/apps/sim/tools/bitbucket/get_pipeline.ts new file mode 100644 index 00000000000..2891050b25f --- /dev/null +++ b/apps/sim/tools/bitbucket/get_pipeline.ts @@ -0,0 +1,57 @@ +import { + BITBUCKET_PIPELINE_OUTPUT_PROPERTIES, + type BitbucketPipeline, + type BitbucketPipelineParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + encodeBitbucketSegment, + normalizeBitbucketPipeline, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketGetPipelineTool: ToolConfig< + BitbucketPipelineParams, + BitbucketToolResponse<{ pipeline: BitbucketPipeline }> +> = { + id: 'bitbucket_get_pipeline', + name: 'Bitbucket Get Pipeline', + description: 'Get a pipeline by UUID', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pipeline'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + pipelineUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Pipeline UUID', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/pipelines/${encodeBitbucketSegment(params.pipelineUuid, 'pipelineUuid')}`, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: { pipeline: normalizeBitbucketPipeline(await bitbucketJson(response)) }, + }), + outputs: { + pipeline: { + type: 'object', + description: 'Pipeline details', + properties: BITBUCKET_PIPELINE_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_pipeline_step_log.ts b/apps/sim/tools/bitbucket/get_pipeline_step_log.ts new file mode 100644 index 00000000000..2b5522c28e5 --- /dev/null +++ b/apps/sim/tools/bitbucket/get_pipeline_step_log.ts @@ -0,0 +1,82 @@ +import type { + BitbucketGetPipelineStepLogParams, + BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_DEFAULT_LOG_CHARACTERS, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketRawTail, + bitbucketRepositoryPath, + bitbucketTailRange, + encodeBitbucketSegment, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +interface BitbucketPipelineLogOutput { + log: string + truncated: boolean + totalBytes: number | null +} + +export const bitbucketGetPipelineStepLogTool: ToolConfig< + BitbucketGetPipelineStepLogParams, + BitbucketToolResponse +> = { + id: 'bitbucket_get_pipeline_step_log', + name: 'Bitbucket Get Pipeline Step Log', + description: 'Read a bounded UTF-8 tail of a pipeline step log', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pipeline'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + pipelineUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Pipeline UUID', + }, + stepUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Pipeline step UUID', + }, + maxCharacters: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum trailing log characters to return (1-200000)', + default: BITBUCKET_DEFAULT_LOG_CHARACTERS, + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/pipelines/${encodeBitbucketSegment(params.pipelineUuid, 'pipelineUuid')}/steps/${encodeBitbucketSegment(params.stepUuid, 'stepUuid')}/log`, + method: 'GET', + headers: (params) => + bitbucketHeaders(params.accessToken, { + json: false, + range: bitbucketTailRange(params.maxCharacters), + }), + retry: BITBUCKET_READ_RETRY, + stripAuthOnRedirect: true, + }, + transformResponse: async (response, params) => ({ + success: true, + output: await bitbucketRawTail(response, params?.maxCharacters), + }), + outputs: { + log: { type: 'string', description: 'Bounded trailing UTF-8 log text' }, + truncated: { type: 'boolean', description: 'Whether earlier log output was omitted' }, + totalBytes: { + type: 'number', + description: 'Full log byte size when reported', + nullable: true, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_pull_request.ts b/apps/sim/tools/bitbucket/get_pull_request.ts new file mode 100644 index 00000000000..98fb5cb485a --- /dev/null +++ b/apps/sim/tools/bitbucket/get_pull_request.ts @@ -0,0 +1,48 @@ +import { + BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + type BitbucketPullRequest, + type BitbucketPullRequestParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PULL_REQUEST_PARAMS, + BITBUCKET_READ_RETRY, + bitbucketHeaders, + bitbucketJson, + bitbucketPullRequestPath, + normalizeBitbucketPullRequest, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketGetPullRequestTool: ToolConfig< + BitbucketPullRequestParams, + BitbucketToolResponse<{ pullRequest: BitbucketPullRequest }> +> = { + id: 'bitbucket_get_pull_request', + name: 'Bitbucket Get Pull Request', + description: 'Get a pull request by repository-scoped ID', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest'] }, + params: { ...BITBUCKET_PULL_REQUEST_PARAMS }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}`, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: { pullRequest: normalizeBitbucketPullRequest(await bitbucketJson(response)) }, + }), + outputs: { + pullRequest: { + type: 'object', + description: 'Pull request details', + properties: BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_pull_request_diff.ts b/apps/sim/tools/bitbucket/get_pull_request_diff.ts new file mode 100644 index 00000000000..5aee9943a51 --- /dev/null +++ b/apps/sim/tools/bitbucket/get_pull_request_diff.ts @@ -0,0 +1,130 @@ +import type { + BitbucketGetPullRequestDiffParams, + BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + assertBitbucketResponseOk, + BITBUCKET_API_BASE, + BITBUCKET_DEFAULT_MAX_CHARACTERS, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PULL_REQUEST_PARAMS, + BITBUCKET_RAW_TRANSFER_MAX_BYTES, + BITBUCKET_READ_RETRY, + bitbucketHeaders, + bitbucketHeadRange, + bitbucketPullRequestPath, + bitbucketRawHead, + bitbucketRepositoryPathQuery, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +interface BitbucketDiffOutput { + diff: string + decodingLossy: boolean + truncated: boolean + returnedBytes: number + fullBytes: number | null +} + +function pullRequestDiffUrl(params: BitbucketGetPullRequestDiffParams): string { + bitbucketRepositoryPathQuery(params.path) + return `${BITBUCKET_API_BASE}${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/diff` +} + +async function transformDiff( + response: Response, + maxCharacters: number | undefined +): Promise> { + const raw = await bitbucketRawHead(response, maxCharacters, false, { allowLossyUtf8: true }) + if (raw.binary || raw.content === null) throw new Error('Bitbucket returned a binary diff') + if (raw.truncated === null) throw new Error('Bitbucket returned an indeterminate diff length') + return { + success: true, + output: { + diff: raw.content, + decodingLossy: raw.decodingLossy ?? false, + truncated: raw.truncated, + returnedBytes: raw.returnedBytes, + fullBytes: raw.fullBytes, + }, + } +} + +export const bitbucketGetPullRequestDiffTool: ToolConfig< + BitbucketGetPullRequestDiffParams, + BitbucketToolResponse +> = { + id: 'bitbucket_get_pull_request_diff', + name: 'Bitbucket Get Pull Request Diff', + description: 'Read a bounded UTF-8 unified diff for one pull request file', + version: '1.0.0', + oauth: { + required: true, + provider: 'bitbucket', + requiredScopes: ['pullrequest', 'repository'], + }, + params: { + ...BITBUCKET_PULL_REQUEST_PARAMS, + path: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Repository-relative file path to include in the diff', + }, + maxCharacters: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum diff characters to return (1-500000)', + default: BITBUCKET_DEFAULT_MAX_CHARACTERS, + }, + }, + directExecution: async (params, signal) => { + const { secureBitbucketPullRequestRedirect } = await import('@/tools/bitbucket/utils.server') + const headers = bitbucketHeaders(params.accessToken, { + json: false, + range: bitbucketHeadRange(params.maxCharacters), + }) + const response = await secureBitbucketPullRequestRedirect( + pullRequestDiffUrl(params), + params.workspaceSlug, + params.repoSlug, + 'diff', + headers, + BITBUCKET_RAW_TRANSFER_MAX_BYTES, + { + signal, + targetQuery: { path: bitbucketRepositoryPathQuery(params.path), binary: 'false' }, + } + ) + await assertBitbucketResponseOk(response) + return transformDiff(response, params.maxCharacters) + }, + request: { + url: pullRequestDiffUrl, + method: 'GET', + headers: (params) => + bitbucketHeaders(params.accessToken, { + json: false, + range: bitbucketHeadRange(params.maxCharacters), + }), + retry: BITBUCKET_READ_RETRY, + stripAuthOnRedirect: true, + }, + transformResponse: async (response, params) => transformDiff(response, params?.maxCharacters), + outputs: { + diff: { type: 'string', description: 'Bounded unified diff text decoded as UTF-8' }, + decodingLossy: { + type: 'boolean', + description: 'Whether invalid UTF-8 source bytes were replaced while decoding', + }, + truncated: { type: 'boolean', description: 'Whether later diff text was omitted' }, + returnedBytes: { type: 'number', description: 'Provider bytes read for the returned diff' }, + fullBytes: { + type: 'number', + description: 'Full diff byte size when reported', + nullable: true, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_pull_request_diffstat.ts b/apps/sim/tools/bitbucket/get_pull_request_diffstat.ts new file mode 100644 index 00000000000..7550fef5dbe --- /dev/null +++ b/apps/sim/tools/bitbucket/get_pull_request_diffstat.ts @@ -0,0 +1,133 @@ +import { + BITBUCKET_DIFFSTAT_OUTPUT_PROPERTIES, + BITBUCKET_PAGE_OUTPUT, + type BitbucketDiffstat, + type BitbucketListOutput, + type BitbucketPaginatedPullRequestParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + assertBitbucketResponseOk, + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_PULL_REQUEST_PARAMS, + BITBUCKET_READ_RETRY, + bitbucketHeaders, + bitbucketJson, + bitbucketPageLength, + bitbucketPullRequestPath, + normalizeBitbucketDiffstat, + normalizeBitbucketPage, + validateBitbucketPullRequestRedirect, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +function pullRequestDiffstatUrl(params: BitbucketPaginatedPullRequestParams): string { + const url = new URL( + `${BITBUCKET_API_BASE}${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/diffstat` + ) + return url.toString() +} + +function decodedPathname(url: string): string { + return new URL(url).pathname + .split('/') + .map((segment) => decodeURIComponent(segment)) + .join('/') +} + +export const bitbucketGetPullRequestDiffstatTool: ToolConfig< + BitbucketPaginatedPullRequestParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_get_pull_request_diffstat', + name: 'Bitbucket Get Pull Request Diffstat', + description: 'List per-file change statistics for a pull request', + version: '1.0.0', + oauth: { + required: true, + provider: 'bitbucket', + requiredScopes: ['pullrequest', 'repository'], + }, + params: { ...BITBUCKET_PULL_REQUEST_PARAMS, ...BITBUCKET_PAGINATION_PARAMS }, + directExecution: async (params, signal) => { + const { + resolveBitbucketPullRequestRedirect, + secureBitbucketPullRequestRedirect, + secureBitbucketRead, + } = await import('@/tools/bitbucket/utils.server') + const initialUrl = pullRequestDiffstatUrl(params) + const headers = bitbucketHeaders(params.accessToken) + let response: Response + if (params.nextUrl) { + const continuation = validateBitbucketPullRequestRedirect( + params.nextUrl, + params.workspaceSlug, + params.repoSlug, + 'diffstat' + ) + const resolvedTarget = await resolveBitbucketPullRequestRedirect( + initialUrl, + params.workspaceSlug, + params.repoSlug, + 'diffstat', + headers, + { signal } + ) + if (decodedPathname(continuation) !== decodedPathname(resolvedTarget)) { + throw new Error('nextUrl does not belong to this Bitbucket pull request diffstat') + } + response = await secureBitbucketRead(continuation, headers, 2 * 1024 * 1024, { + maxRedirects: 0, + signal, + }) + } else { + response = await secureBitbucketPullRequestRedirect( + initialUrl, + params.workspaceSlug, + params.repoSlug, + 'diffstat', + headers, + 2 * 1024 * 1024, + { + signal, + targetQuery: { pagelen: String(bitbucketPageLength(params.pageLen)) }, + } + ) + } + await assertBitbucketResponseOk(response) + return { + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketDiffstat), + } + }, + request: { + url: (params) => + params.nextUrl + ? validateBitbucketPullRequestRedirect( + params.nextUrl, + params.workspaceSlug, + params.repoSlug, + 'diffstat' + ) + : pullRequestDiffstatUrl(params), + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + stripAuthOnRedirect: true, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketDiffstat), + }), + outputs: { + items: { + type: 'array', + description: 'Per-file diff statistics', + items: { type: 'object', properties: BITBUCKET_DIFFSTAT_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/get_repository.ts b/apps/sim/tools/bitbucket/get_repository.ts new file mode 100644 index 00000000000..9bed2094d91 --- /dev/null +++ b/apps/sim/tools/bitbucket/get_repository.ts @@ -0,0 +1,48 @@ +import { + BITBUCKET_REPOSITORY_OUTPUT_PROPERTIES, + type BitbucketRepository, + type BitbucketRepositoryParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + normalizeBitbucketRepository, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketGetRepositoryTool: ToolConfig< + BitbucketRepositoryParams, + BitbucketToolResponse<{ repository: BitbucketRepository }> +> = { + id: 'bitbucket_get_repository', + name: 'Bitbucket Get Repository', + description: 'Get a Bitbucket Cloud repository', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository'] }, + params: { ...BITBUCKET_REPOSITORY_PARAMS }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}`, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: { repository: normalizeBitbucketRepository(await bitbucketJson(response)) }, + }), + outputs: { + repository: { + type: 'object', + description: 'Repository details', + properties: BITBUCKET_REPOSITORY_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/index.ts b/apps/sim/tools/bitbucket/index.ts new file mode 100644 index 00000000000..53c2aaf58f3 --- /dev/null +++ b/apps/sim/tools/bitbucket/index.ts @@ -0,0 +1,96 @@ +import { bitbucketApprovePullRequestTool } from '@/tools/bitbucket/approve_pull_request' +import { bitbucketCreateBranchTool } from '@/tools/bitbucket/create_branch' +import { bitbucketCreatePullRequestTool } from '@/tools/bitbucket/create_pull_request' +import { bitbucketCreatePullRequestCommentTool } from '@/tools/bitbucket/create_pull_request_comment' +import { bitbucketDeclinePullRequestTool } from '@/tools/bitbucket/decline_pull_request' +import { bitbucketDeleteBranchTool } from '@/tools/bitbucket/delete_branch' +import { bitbucketGetCommitTool } from '@/tools/bitbucket/get_commit' +import { bitbucketGetFileTool } from '@/tools/bitbucket/get_file' +import { bitbucketGetFileMetadataTool } from '@/tools/bitbucket/get_file_metadata' +import { bitbucketGetMergeTaskStatusTool } from '@/tools/bitbucket/get_merge_task_status' +import { bitbucketGetPipelineTool } from '@/tools/bitbucket/get_pipeline' +import { bitbucketGetPipelineStepLogTool } from '@/tools/bitbucket/get_pipeline_step_log' +import { bitbucketGetPullRequestTool } from '@/tools/bitbucket/get_pull_request' +import { bitbucketGetPullRequestDiffTool } from '@/tools/bitbucket/get_pull_request_diff' +import { bitbucketGetPullRequestDiffstatTool } from '@/tools/bitbucket/get_pull_request_diffstat' +import { bitbucketGetRepositoryTool } from '@/tools/bitbucket/get_repository' +import { bitbucketListBranchesTool } from '@/tools/bitbucket/list_branches' +import { bitbucketListCommitsTool } from '@/tools/bitbucket/list_commits' +import { bitbucketListDirectoryTool } from '@/tools/bitbucket/list_directory' +import { bitbucketListPipelineStepsTool } from '@/tools/bitbucket/list_pipeline_steps' +import { bitbucketListPipelinesTool } from '@/tools/bitbucket/list_pipelines' +import { bitbucketListPullRequestCommentsTool } from '@/tools/bitbucket/list_pull_request_comments' +import { bitbucketListPullRequestCommitStatusesTool } from '@/tools/bitbucket/list_pull_request_commit_statuses' +import { bitbucketListPullRequestsTool } from '@/tools/bitbucket/list_pull_requests' +import { bitbucketListRepositoriesTool } from '@/tools/bitbucket/list_repositories' +import { bitbucketListWorkspacesTool } from '@/tools/bitbucket/list_workspaces' +import { bitbucketMergePullRequestTool } from '@/tools/bitbucket/merge_pull_request' +import { bitbucketRequestPullRequestChangesTool } from '@/tools/bitbucket/request_pull_request_changes' +import { bitbucketStopPipelineTool } from '@/tools/bitbucket/stop_pipeline' +import { bitbucketTriggerPipelineTool } from '@/tools/bitbucket/trigger_pipeline' + +export { + bitbucketListWorkspacesTool, + bitbucketListRepositoriesTool, + bitbucketGetRepositoryTool, + bitbucketListBranchesTool, + bitbucketCreateBranchTool, + bitbucketDeleteBranchTool, + bitbucketListCommitsTool, + bitbucketGetCommitTool, + bitbucketListDirectoryTool, + bitbucketGetFileTool, + bitbucketGetFileMetadataTool, + bitbucketListPullRequestsTool, + bitbucketGetPullRequestTool, + bitbucketCreatePullRequestTool, + bitbucketMergePullRequestTool, + bitbucketGetMergeTaskStatusTool, + bitbucketDeclinePullRequestTool, + bitbucketApprovePullRequestTool, + bitbucketRequestPullRequestChangesTool, + bitbucketGetPullRequestDiffTool, + bitbucketGetPullRequestDiffstatTool, + bitbucketListPullRequestCommentsTool, + bitbucketCreatePullRequestCommentTool, + bitbucketListPullRequestCommitStatusesTool, + bitbucketListPipelinesTool, + bitbucketGetPipelineTool, + bitbucketTriggerPipelineTool, + bitbucketStopPipelineTool, + bitbucketListPipelineStepsTool, + bitbucketGetPipelineStepLogTool, +} + +export const bitbucketTools = [ + bitbucketListWorkspacesTool, + bitbucketListRepositoriesTool, + bitbucketGetRepositoryTool, + bitbucketListBranchesTool, + bitbucketCreateBranchTool, + bitbucketDeleteBranchTool, + bitbucketListCommitsTool, + bitbucketGetCommitTool, + bitbucketListDirectoryTool, + bitbucketGetFileTool, + bitbucketGetFileMetadataTool, + bitbucketListPullRequestsTool, + bitbucketGetPullRequestTool, + bitbucketCreatePullRequestTool, + bitbucketMergePullRequestTool, + bitbucketGetMergeTaskStatusTool, + bitbucketDeclinePullRequestTool, + bitbucketApprovePullRequestTool, + bitbucketRequestPullRequestChangesTool, + bitbucketGetPullRequestDiffTool, + bitbucketGetPullRequestDiffstatTool, + bitbucketListPullRequestCommentsTool, + bitbucketCreatePullRequestCommentTool, + bitbucketListPullRequestCommitStatusesTool, + bitbucketListPipelinesTool, + bitbucketGetPipelineTool, + bitbucketTriggerPipelineTool, + bitbucketStopPipelineTool, + bitbucketListPipelineStepsTool, + bitbucketGetPipelineStepLogTool, +] diff --git a/apps/sim/tools/bitbucket/list_branches.ts b/apps/sim/tools/bitbucket/list_branches.ts new file mode 100644 index 00000000000..d6b8a1687a6 --- /dev/null +++ b/apps/sim/tools/bitbucket/list_branches.ts @@ -0,0 +1,75 @@ +import { + BITBUCKET_BRANCH_OUTPUT_PROPERTIES, + BITBUCKET_PAGE_OUTPUT, + type BitbucketBranch, + type BitbucketListBranchesParams, + type BitbucketListOutput, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + normalizeBitbucketBranch, + normalizeBitbucketPage, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketListBranchesTool: ToolConfig< + BitbucketListBranchesParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_branches', + name: 'Bitbucket List Branches', + description: 'List branches in a Bitbucket Cloud repository', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + q: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket branch filtering expression', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket branch sort expression', + }, + ...BITBUCKET_PAGINATION_PARAMS, + }, + request: { + url: (params) => + bitbucketApiUrl( + `${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/refs/branches`, + { + nextUrl: params.nextUrl, + pageLen: params.pageLen, + query: { q: params.q, sort: params.sort }, + } + ), + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketBranch), + }), + outputs: { + items: { + type: 'array', + description: 'Branches', + items: { type: 'object', properties: BITBUCKET_BRANCH_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/list_commits.ts b/apps/sim/tools/bitbucket/list_commits.ts new file mode 100644 index 00000000000..a1536e2c2fb --- /dev/null +++ b/apps/sim/tools/bitbucket/list_commits.ts @@ -0,0 +1,56 @@ +import { + BITBUCKET_COMMIT_OUTPUT_PROPERTIES, + BITBUCKET_PAGE_OUTPUT, + type BitbucketCommit, + type BitbucketListCommitsParams, + type BitbucketListOutput, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + normalizeBitbucketCommit, + normalizeBitbucketPage, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketListCommitsTool: ToolConfig< + BitbucketListCommitsParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_commits', + name: 'Bitbucket List Commits', + description: 'List repository commits in reverse chronological order', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository'] }, + params: { ...BITBUCKET_REPOSITORY_PARAMS, ...BITBUCKET_PAGINATION_PARAMS }, + request: { + url: (params) => + bitbucketApiUrl(`${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/commits`, { + nextUrl: params.nextUrl, + pageLen: params.pageLen, + }), + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketCommit), + }), + outputs: { + items: { + type: 'array', + description: 'Commits', + items: { type: 'object', properties: BITBUCKET_COMMIT_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/list_directory.ts b/apps/sim/tools/bitbucket/list_directory.ts new file mode 100644 index 00000000000..d4f4203c2ba --- /dev/null +++ b/apps/sim/tools/bitbucket/list_directory.ts @@ -0,0 +1,96 @@ +import { + BITBUCKET_DIRECTORY_ENTRY_OUTPUT_PROPERTIES, + BITBUCKET_PAGE_OUTPUT, + type BitbucketDirectoryEntry, + type BitbucketListDirectoryParams, + type BitbucketListOutput, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + encodeBitbucketRepositoryPath, + encodeBitbucketSegment, + normalizeBitbucketDirectoryEntry, + normalizeBitbucketPage, +} from '@/tools/bitbucket/utils' +import { requireBitbucketSha1 } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketListDirectoryTool: ToolConfig< + BitbucketListDirectoryParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_directory', + name: 'Bitbucket List Directory', + description: 'List one shallow repository directory at a full commit SHA-1', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + commit: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Full 40-character commit SHA-1', + }, + path: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Repository-relative directory path; omit for the root', + }, + q: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket tree-entry filtering expression', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket tree-entry sort expression', + }, + ...BITBUCKET_PAGINATION_PARAMS, + }, + request: { + url: (params) => { + const directoryPath = encodeBitbucketRepositoryPath(params.path ?? '', true) + const commit = requireBitbucketSha1(params.commit, 'commit') + return bitbucketApiUrl( + `${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/src/${encodeBitbucketSegment(commit, 'commit')}/${directoryPath}`, + { + nextUrl: params.nextUrl, + pageLen: params.pageLen, + query: { q: params.q, sort: params.sort }, + nextPathPrefix: `${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/src`, + nextPathSuffix: directoryPath, + nextRevision: commit, + } + ) + }, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketDirectoryEntry), + }), + outputs: { + items: { + type: 'array', + description: 'Directory entries', + items: { type: 'object', properties: BITBUCKET_DIRECTORY_ENTRY_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/list_pipeline_steps.ts b/apps/sim/tools/bitbucket/list_pipeline_steps.ts new file mode 100644 index 00000000000..5c1735df788 --- /dev/null +++ b/apps/sim/tools/bitbucket/list_pipeline_steps.ts @@ -0,0 +1,66 @@ +import { + BITBUCKET_PAGE_OUTPUT, + BITBUCKET_PIPELINE_STEP_OUTPUT_PROPERTIES, + type BitbucketListOutput, + type BitbucketListPipelineStepsParams, + type BitbucketPipelineStep, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + encodeBitbucketSegment, + normalizeBitbucketPage, + normalizeBitbucketPipelineStep, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketListPipelineStepsTool: ToolConfig< + BitbucketListPipelineStepsParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_pipeline_steps', + name: 'Bitbucket List Pipeline Steps', + description: 'List the steps in a pipeline', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pipeline'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + pipelineUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Pipeline UUID', + }, + ...BITBUCKET_PAGINATION_PARAMS, + }, + request: { + url: (params) => + bitbucketApiUrl( + `${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/pipelines/${encodeBitbucketSegment(params.pipelineUuid, 'pipelineUuid')}/steps`, + { nextUrl: params.nextUrl, pageLen: params.pageLen } + ), + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketPipelineStep), + }), + outputs: { + items: { + type: 'array', + description: 'Pipeline steps', + items: { type: 'object', properties: BITBUCKET_PIPELINE_STEP_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/list_pipelines.ts b/apps/sim/tools/bitbucket/list_pipelines.ts new file mode 100644 index 00000000000..5f51a37376b --- /dev/null +++ b/apps/sim/tools/bitbucket/list_pipelines.ts @@ -0,0 +1,160 @@ +import { + BITBUCKET_PAGE_OUTPUT, + BITBUCKET_PIPELINE_OUTPUT_PROPERTIES, + type BitbucketListOutput, + type BitbucketListPipelinesParams, + type BitbucketPipeline, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + normalizeBitbucketPage, + normalizeBitbucketPipeline, +} from '@/tools/bitbucket/utils' +import { optionalBitbucketEnum, optionalBitbucketSha1 } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +const BITBUCKET_PIPELINE_LIST_REF_TYPES = ['BRANCH', 'TAG', 'ANNOTATED_TAG'] as const +const BITBUCKET_PIPELINE_LIST_SELECTOR_TYPES = [ + 'BRANCH', + 'TAG', + 'CUSTOM', + 'PULLREQUESTS', + 'DEFAULT', +] as const +const BITBUCKET_PIPELINE_TRIGGER_TYPES = ['PUSH', 'MANUAL', 'SCHEDULED', 'PARENT_STEP'] as const +const BITBUCKET_PIPELINE_STATUSES = [ + 'PARSING', + 'PENDING', + 'PAUSED', + 'HALTED', + 'BUILDING', + 'ERROR', + 'PASSED', + 'FAILED', + 'STOPPED', + 'UNKNOWN', +] as const +export const bitbucketListPipelinesTool: ToolConfig< + BitbucketListPipelinesParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_pipelines', + name: 'Bitbucket List Pipelines', + description: 'List pipelines for a Bitbucket Cloud repository', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pipeline'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + refType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reference type filter: BRANCH, TAG, or ANNOTATED_TAG', + }, + refName: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Reference name filter', + }, + commitHash: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Full 40-character target commit SHA-1 filter', + }, + selectorType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Selector type filter: BRANCH, TAG, CUSTOM, PULLREQUESTS, or DEFAULT', + }, + selectorPattern: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pipeline selector pattern filter', + }, + triggerType: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Trigger filter: PUSH, MANUAL, SCHEDULED, or PARENT_STEP', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pipeline status filter', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket pipeline sort expression', + }, + ...BITBUCKET_PAGINATION_PARAMS, + }, + request: { + url: (params) => { + const refType = optionalBitbucketEnum( + params.refType, + 'refType', + BITBUCKET_PIPELINE_LIST_REF_TYPES + ) + const selectorType = optionalBitbucketEnum( + params.selectorType, + 'selectorType', + BITBUCKET_PIPELINE_LIST_SELECTOR_TYPES + ) + const triggerType = optionalBitbucketEnum( + params.triggerType, + 'triggerType', + BITBUCKET_PIPELINE_TRIGGER_TYPES + ) + const status = optionalBitbucketEnum(params.status, 'status', BITBUCKET_PIPELINE_STATUSES) + const commitHash = optionalBitbucketSha1(params.commitHash, 'commitHash') + return bitbucketApiUrl( + `${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/pipelines`, + { + nextUrl: params.nextUrl, + pageLen: params.pageLen, + query: { + 'target.ref_type': refType, + 'target.ref_name': params.refName, + 'target.commit.hash': commitHash, + 'target.selector.type': selectorType, + 'target.selector.pattern': params.selectorPattern, + trigger_type: triggerType, + status, + sort: params.sort, + }, + } + ) + }, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketPipeline), + }), + outputs: { + items: { + type: 'array', + description: 'Pipelines', + items: { type: 'object', properties: BITBUCKET_PIPELINE_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/list_pull_request_comments.ts b/apps/sim/tools/bitbucket/list_pull_request_comments.ts new file mode 100644 index 00000000000..5a83722f96a --- /dev/null +++ b/apps/sim/tools/bitbucket/list_pull_request_comments.ts @@ -0,0 +1,75 @@ +import { + BITBUCKET_COMMENT_OUTPUT_PROPERTIES, + BITBUCKET_PAGE_OUTPUT, + type BitbucketComment, + type BitbucketListOutput, + type BitbucketListPullRequestCommentsParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_PULL_REQUEST_PARAMS, + BITBUCKET_READ_RETRY, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + bitbucketPullRequestPath, + normalizeBitbucketComment, + normalizeBitbucketPage, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketListPullRequestCommentsTool: ToolConfig< + BitbucketListPullRequestCommentsParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_pull_request_comments', + name: 'Bitbucket List Pull Request Comments', + description: 'List global, inline, and reply comments on a pull request', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest'] }, + params: { + ...BITBUCKET_PULL_REQUEST_PARAMS, + q: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket comment filtering expression', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket comment sort expression', + }, + ...BITBUCKET_PAGINATION_PARAMS, + }, + request: { + url: (params) => + bitbucketApiUrl( + `${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/comments`, + { + nextUrl: params.nextUrl, + pageLen: params.pageLen, + query: { q: params.q, sort: params.sort }, + } + ), + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketComment), + }), + outputs: { + items: { + type: 'array', + description: 'Pull request comments', + items: { type: 'object', properties: BITBUCKET_COMMENT_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/list_pull_request_commit_statuses.ts b/apps/sim/tools/bitbucket/list_pull_request_commit_statuses.ts new file mode 100644 index 00000000000..1215860c867 --- /dev/null +++ b/apps/sim/tools/bitbucket/list_pull_request_commit_statuses.ts @@ -0,0 +1,75 @@ +import { + BITBUCKET_COMMIT_STATUS_OUTPUT_PROPERTIES, + BITBUCKET_PAGE_OUTPUT, + type BitbucketCommitStatus, + type BitbucketListOutput, + type BitbucketListPullRequestCommitStatusesParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_PULL_REQUEST_PARAMS, + BITBUCKET_READ_RETRY, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + bitbucketPullRequestPath, + normalizeBitbucketCommitStatus, + normalizeBitbucketPage, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketListPullRequestCommitStatusesTool: ToolConfig< + BitbucketListPullRequestCommitStatusesParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_pull_request_commit_statuses', + name: 'Bitbucket List Pull Request Commit Statuses', + description: 'List commit statuses associated with a pull request', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest'] }, + params: { + ...BITBUCKET_PULL_REQUEST_PARAMS, + q: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket commit status filtering expression', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket commit status sort expression', + }, + ...BITBUCKET_PAGINATION_PARAMS, + }, + request: { + url: (params) => + bitbucketApiUrl( + `${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/statuses`, + { + nextUrl: params.nextUrl, + pageLen: params.pageLen, + query: { q: params.q, sort: params.sort }, + } + ), + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketCommitStatus), + }), + outputs: { + items: { + type: 'array', + description: 'Pull request commit statuses', + items: { type: 'object', properties: BITBUCKET_COMMIT_STATUS_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/list_pull_requests.ts b/apps/sim/tools/bitbucket/list_pull_requests.ts new file mode 100644 index 00000000000..a31b478b687 --- /dev/null +++ b/apps/sim/tools/bitbucket/list_pull_requests.ts @@ -0,0 +1,86 @@ +import { + BITBUCKET_PAGE_OUTPUT, + BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + type BitbucketListOutput, + type BitbucketListPullRequestsParams, + type BitbucketPullRequest, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_READ_RETRY, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + normalizeBitbucketPage, + normalizeBitbucketPullRequest, +} from '@/tools/bitbucket/utils' +import { optionalBitbucketEnum } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +const BITBUCKET_PULL_REQUEST_STATES = ['OPEN', 'MERGED', 'DECLINED', 'SUPERSEDED'] as const + +export const bitbucketListPullRequestsTool: ToolConfig< + BitbucketListPullRequestsParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_pull_requests', + name: 'Bitbucket List Pull Requests', + description: 'List pull requests in a Bitbucket Cloud repository', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + state: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'State filter: OPEN, MERGED, DECLINED, or SUPERSEDED', + }, + q: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket pull request filtering expression', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket pull request sort expression', + }, + ...BITBUCKET_PAGINATION_PARAMS, + }, + request: { + url: (params) => { + const state = optionalBitbucketEnum(params.state, 'state', BITBUCKET_PULL_REQUEST_STATES) + return bitbucketApiUrl( + `${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/pullrequests`, + { + nextUrl: params.nextUrl, + pageLen: params.pageLen, + query: { state, q: params.q, sort: params.sort }, + } + ) + }, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketPullRequest), + }), + outputs: { + items: { + type: 'array', + description: 'Pull requests', + items: { type: 'object', properties: BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/list_repositories.ts b/apps/sim/tools/bitbucket/list_repositories.ts new file mode 100644 index 00000000000..f181277a567 --- /dev/null +++ b/apps/sim/tools/bitbucket/list_repositories.ts @@ -0,0 +1,92 @@ +import { + BITBUCKET_PAGE_OUTPUT, + BITBUCKET_REPOSITORY_OUTPUT_PROPERTIES, + type BitbucketListOutput, + type BitbucketListRepositoriesParams, + type BitbucketRepository, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ACCESS_TOKEN_PARAM, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_READ_RETRY, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + encodeBitbucketSegment, + normalizeBitbucketPage, + normalizeBitbucketRepository, +} from '@/tools/bitbucket/utils' +import { optionalBitbucketEnum } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +const BITBUCKET_REPOSITORY_ROLES = ['admin', 'contributor', 'member', 'owner'] as const + +export const bitbucketListRepositoriesTool: ToolConfig< + BitbucketListRepositoriesParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_repositories', + name: 'Bitbucket List Repositories', + description: 'List repositories in a Bitbucket Cloud workspace', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['repository'] }, + params: { + workspaceSlug: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Bitbucket workspace slug or UUID', + }, + role: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Caller role filter: admin, contributor, member, or owner', + }, + q: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket filtering expression', + }, + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Bitbucket sort expression', + }, + ...BITBUCKET_PAGINATION_PARAMS, + accessToken: BITBUCKET_ACCESS_TOKEN_PARAM, + }, + request: { + url: (params) => { + const role = optionalBitbucketEnum(params.role, 'role', BITBUCKET_REPOSITORY_ROLES) + return bitbucketApiUrl( + `/repositories/${encodeBitbucketSegment(params.workspaceSlug, 'workspaceSlug')}`, + { + nextUrl: params.nextUrl, + pageLen: params.pageLen, + query: { role, q: params.q, sort: params.sort }, + } + ) + }, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage(await bitbucketJson(response), normalizeBitbucketRepository), + }), + outputs: { + items: { + type: 'array', + description: 'Repositories', + items: { type: 'object', properties: BITBUCKET_REPOSITORY_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/list_workspaces.ts b/apps/sim/tools/bitbucket/list_workspaces.ts new file mode 100644 index 00000000000..d285ab960e5 --- /dev/null +++ b/apps/sim/tools/bitbucket/list_workspaces.ts @@ -0,0 +1,79 @@ +import type { + BitbucketListOutput, + BitbucketListWorkspacesParams, + BitbucketToolResponse, + BitbucketWorkspaceAccess, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_PAGE_OUTPUT, + BITBUCKET_WORKSPACE_OUTPUT_PROPERTIES, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_ACCESS_TOKEN_PARAM, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PAGINATION_PARAMS, + BITBUCKET_READ_RETRY, + bitbucketApiUrl, + bitbucketHeaders, + bitbucketJson, + normalizeBitbucketPage, + normalizeBitbucketWorkspaceAccess, +} from '@/tools/bitbucket/utils' +import { optionalBitbucketBoolean } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketListWorkspacesTool: ToolConfig< + BitbucketListWorkspacesParams, + BitbucketToolResponse> +> = { + id: 'bitbucket_list_workspaces', + name: 'Bitbucket List Workspaces', + description: 'List Bitbucket Cloud workspaces available to the authenticated account', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['account'] }, + params: { + sort: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Workspace sort field; Bitbucket currently supports slug', + }, + administrator: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Filter by whether the caller is a workspace administrator', + }, + ...BITBUCKET_PAGINATION_PARAMS, + accessToken: BITBUCKET_ACCESS_TOKEN_PARAM, + }, + request: { + url: (params) => { + const administrator = optionalBitbucketBoolean(params.administrator, 'administrator') + return bitbucketApiUrl('/user/workspaces', { + nextUrl: params.nextUrl, + pageLen: params.pageLen, + query: { sort: params.sort, administrator }, + }) + }, + method: 'GET', + headers: (params) => bitbucketHeaders(params.accessToken), + retry: BITBUCKET_READ_RETRY, + }, + transformResponse: async (response) => ({ + success: true, + output: normalizeBitbucketPage( + await bitbucketJson(response), + normalizeBitbucketWorkspaceAccess + ), + }), + outputs: { + items: { + type: 'array', + description: 'Workspace access records', + items: { type: 'object', properties: BITBUCKET_WORKSPACE_OUTPUT_PROPERTIES }, + }, + page: BITBUCKET_PAGE_OUTPUT, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/merge_pull_request.ts b/apps/sim/tools/bitbucket/merge_pull_request.ts new file mode 100644 index 00000000000..ad1048743ed --- /dev/null +++ b/apps/sim/tools/bitbucket/merge_pull_request.ts @@ -0,0 +1,152 @@ +import { + BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + type BitbucketMergePullRequestParams, + type BitbucketPullRequest, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PULL_REQUEST_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketPullRequestPath, + normalizeBitbucketPullRequest, + validateBitbucketOpaqueUrl, +} from '@/tools/bitbucket/utils' +import { + optionalBitbucketBoolean, + optionalBitbucketEnum, + optionalBitbucketUtf8String, +} from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +const BITBUCKET_MERGE_MESSAGE_MAX_BYTES = 128 * 1024 +const BITBUCKET_MERGE_STRATEGIES = [ + 'merge_commit', + 'squash', + 'fast_forward', + 'squash_fast_forward', + 'rebase_fast_forward', + 'rebase_merge', +] as const + +interface BitbucketMergeOutput { + status: 'completed' | 'pending' + taskId: string | null + taskUrl: string | null + pullRequest: BitbucketPullRequest | null +} + +function mergeTaskLocation( + response: Response, + params: BitbucketMergePullRequestParams +): { + taskId: string + taskUrl: string +} { + const rawLocation = response.headers.get('location') + if (!rawLocation) throw new Error('Bitbucket async merge response omitted the Location header') + const taskUrl = validateBitbucketOpaqueUrl( + new URL(rawLocation, `${BITBUCKET_API_BASE}/`).toString() + ) + const parsed = new URL(taskUrl) + const expectedPrefix = `/2.0${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/merge/task-status/` + if (!parsed.pathname.startsWith(expectedPrefix)) { + throw new Error('Bitbucket merge task Location did not match the requested pull request') + } + const taskId = decodeURIComponent(parsed.pathname.slice(expectedPrefix.length)) + if (!taskId || taskId.includes('/')) throw new Error('Bitbucket merge task Location was invalid') + return { taskId, taskUrl } +} + +export const bitbucketMergePullRequestTool: ToolConfig< + BitbucketMergePullRequestParams, + BitbucketToolResponse +> = { + id: 'bitbucket_merge_pull_request', + name: 'Bitbucket Merge Pull Request', + description: 'Start an asynchronous pull request merge and return a task to poll when needed', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest:write'] }, + params: { + ...BITBUCKET_PULL_REQUEST_PARAMS, + mergeStrategy: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Merge strategy: merge_commit, squash, fast_forward, squash_fast_forward, rebase_fast_forward, or rebase_merge', + }, + message: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Merge commit message (maximum 128 KiB encoded as UTF-8)', + }, + closeSourceBranch: { + type: 'boolean', + required: false, + visibility: 'user-or-llm', + description: 'Delete the source branch after merging', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/merge?async=true`, + method: 'POST', + headers: (params) => bitbucketHeaders(params.accessToken, { json: true }), + body: (params) => { + const mergeStrategy = optionalBitbucketEnum( + params.mergeStrategy, + 'mergeStrategy', + BITBUCKET_MERGE_STRATEGIES + ) + const message = optionalBitbucketUtf8String( + params.message, + 'message', + BITBUCKET_MERGE_MESSAGE_MAX_BYTES + ) + const closeSourceBranch = optionalBitbucketBoolean( + params.closeSourceBranch, + 'closeSourceBranch' + ) + return { + ...(mergeStrategy !== undefined ? { merge_strategy: mergeStrategy } : {}), + ...(message !== undefined ? { message } : {}), + ...(closeSourceBranch !== undefined ? { close_source_branch: closeSourceBranch } : {}), + } + }, + }, + transformResponse: async (response, params) => { + if (response.status === 202) { + if (!params) throw new Error('Missing merge parameters while reading async merge response') + const task = mergeTaskLocation(response, params) + return { + success: true, + output: { status: 'pending', ...task, pullRequest: null }, + } + } + return { + success: true, + output: { + status: 'completed', + taskId: null, + taskUrl: null, + pullRequest: normalizeBitbucketPullRequest(await bitbucketJson(response)), + }, + } + }, + outputs: { + status: { type: 'string', description: 'Whether the merge completed or remains pending' }, + taskId: { type: 'string', description: 'Async merge task ID', nullable: true }, + taskUrl: { type: 'string', description: 'Validated task polling URL', nullable: true }, + pullRequest: { + type: 'object', + description: 'Merged pull request when completed synchronously', + nullable: true, + properties: BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/pipelines.test.ts b/apps/sim/tools/bitbucket/pipelines.test.ts new file mode 100644 index 00000000000..c566346606c --- /dev/null +++ b/apps/sim/tools/bitbucket/pipelines.test.ts @@ -0,0 +1,445 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { bitbucketGetPipelineTool } from '@/tools/bitbucket/get_pipeline' +import { bitbucketGetPipelineStepLogTool } from '@/tools/bitbucket/get_pipeline_step_log' +import { bitbucketListPipelineStepsTool } from '@/tools/bitbucket/list_pipeline_steps' +import { bitbucketListPipelinesTool } from '@/tools/bitbucket/list_pipelines' +import { bitbucketStopPipelineTool } from '@/tools/bitbucket/stop_pipeline' +import { bitbucketTriggerPipelineTool } from '@/tools/bitbucket/trigger_pipeline' +import type { + BitbucketGetPipelineStepLogParams, + BitbucketListPipelineStepsParams, + BitbucketListPipelinesParams, + BitbucketPipelineParams, + BitbucketTriggerPipelineParams, +} from '@/tools/bitbucket/types' +import type { ToolConfig } from '@/tools/types' + +const REPOSITORY_PARAMS = { + accessToken: 'oauth-token', + workspaceSlug: 'acme team', + repoSlug: 'sdk/core', +} as const + +const COMMIT_SHA = 'abcdef0123456789abcdef0123456789abcdef01' + +const RAW_USER = { + uuid: '{user-1}', + account_id: 'account-1', + type: 'user', + display_name: 'Ada Lovelace', + links: { self: { href: 'https://api.bitbucket.org/2.0/users/ada' } }, +} + +const RAW_PIPELINE = { + type: 'pipeline', + uuid: '{pipeline-1}', + build_number: 42, + creator: RAW_USER, + repository: { full_name: 'acme/demo' }, + target: { + type: 'pipeline_ref_target', + ref_type: 'branch', + ref_name: 'main', + commit: { hash: 'abc123' }, + selector: { type: 'custom', pattern: 'deploy' }, + }, + trigger: { type: 'pipeline_manual_trigger' }, + state: { + name: 'COMPLETED', + stage: { name: 'COMPLETED' }, + result: { + name: 'FAILED', + error: { key: 'configuration-error', message: 'Invalid pipeline configuration' }, + }, + }, + created_on: '2026-01-01T00:00:00Z', + completed_on: '2026-01-01T00:02:00Z', + build_seconds_used: 120, + links: { + self: { href: 'https://api.bitbucket.org/2.0/repositories/acme/demo/pipelines/pipeline-1' }, + steps: { + href: 'https://api.bitbucket.org/2.0/repositories/acme/demo/pipelines/pipeline-1/steps', + }, + }, +} + +const RAW_PIPELINE_STEP = { + type: 'pipeline_step', + uuid: '{step-1}', + started_on: '2026-01-01T00:00:00Z', + completed_on: '2026-01-01T00:02:00Z', + state: { + name: 'COMPLETED', + result: { + name: 'FAILED', + error: { key: 'script-error', message: 'Tests failed' }, + }, + }, + image: { name: 'node:22' }, + setup_commands: [{ name: 'setup', command: 'npm install' }], + script_commands: [ + { name: 'test', command: 'bun test' }, + { name: 'build', command: 'bun run build' }, + ], +} + +function requestUrl(tool: ToolConfig, params: P): string { + return typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url +} + +function requestBody(tool: ToolConfig, params: P): unknown { + return tool.request.body?.(params) +} + +describe('Bitbucket pipeline request builders', () => { + it('builds every documented list filter and bounded pagination parameter', () => { + const url = new URL( + requestUrl(bitbucketListPipelinesTool, { + ...REPOSITORY_PARAMS, + refType: 'BRANCH', + refName: 'main', + commitHash: COMMIT_SHA.toUpperCase(), + selectorType: 'CUSTOM', + selectorPattern: 'deploy', + triggerType: 'MANUAL', + status: 'FAILED', + sort: '-created_on,creator.uuid', + pageLen: 25, + } satisfies BitbucketListPipelinesParams) + ) + + expect(url.pathname).toBe('/2.0/repositories/acme%20team/sdk%2Fcore/pipelines') + expect(Object.fromEntries(url.searchParams)).toEqual({ + 'target.ref_type': 'BRANCH', + 'target.ref_name': 'main', + 'target.commit.hash': COMMIT_SHA, + 'target.selector.type': 'CUSTOM', + 'target.selector.pattern': 'deploy', + trigger_type: 'MANUAL', + status: 'FAILED', + sort: '-created_on,creator.uuid', + pagelen: '25', + }) + + for (const [field, value] of [ + ['refType', 'COMMIT'], + ['selectorType', 'DEPLOYMENT'], + ['triggerType', 'WEBHOOK'], + ['status', 'CANCELLED'], + ] as const) { + expect(() => + requestUrl(bitbucketListPipelinesTool, { + ...REPOSITORY_PARAMS, + [field]: value, + } as unknown as BitbucketListPipelinesParams) + ).toThrow(new RegExp(`${field} must be one of`)) + } + expect(() => + requestUrl(bitbucketListPipelinesTool, { + ...REPOSITORY_PARAMS, + sort: { malformed: true }, + } as unknown as BitbucketListPipelinesParams) + ).toThrow(/query parameter sort must be a string, number, or boolean/) + expect(() => + requestUrl(bitbucketListPipelinesTool, { + ...REPOSITORY_PARAMS, + commitHash: 'abc123', + } satisfies BitbucketListPipelinesParams) + ).toThrow(/commitHash must be a full 40-character SHA-1/) + }) + + it('binds a pipeline cursor to the selected repository list endpoint', () => { + const next = + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pipelines?page=2' + expect( + requestUrl(bitbucketListPipelinesTool, { + ...REPOSITORY_PARAMS, + nextUrl: next, + } satisfies BitbucketListPipelinesParams) + ).toBe(next) + expect(() => + requestUrl(bitbucketListPipelinesTool, { + ...REPOSITORY_PARAMS, + nextUrl: + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pullrequests?page=2', + } satisfies BitbucketListPipelinesParams) + ).toThrow(/does not belong/) + }) + + it('encodes pipeline and step UUID path segments', () => { + const pipelineParams = { + ...REPOSITORY_PARAMS, + pipelineUuid: '{pipeline/one ?#}', + } satisfies BitbucketPipelineParams + expect(requestUrl(bitbucketGetPipelineTool, pipelineParams)).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pipelines/%7Bpipeline%2Fone%20%3F%23%7D' + ) + expect(requestUrl(bitbucketStopPipelineTool, pipelineParams)).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pipelines/%7Bpipeline%2Fone%20%3F%23%7D/stopPipeline' + ) + expect( + requestUrl(bitbucketGetPipelineStepLogTool, { + ...pipelineParams, + stepUuid: '{step/one ?#}', + } satisfies BitbucketGetPipelineStepLogParams) + ).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pipelines/%7Bpipeline%2Fone%20%3F%23%7D/steps/%7Bstep%2Fone%20%3F%23%7D/log' + ) + }) + + it('builds the restricted pipeline ref target and excludes administration fields', () => { + const params = { + ...REPOSITORY_PARAMS, + refType: 'branch', + refName: ' main ', + commitHash: ` ${COMMIT_SHA.toUpperCase()} `, + } satisfies BitbucketTriggerPipelineParams + expect(requestBody(bitbucketTriggerPipelineTool, params)).toEqual({ + target: { + type: 'pipeline_ref_target', + ref_type: 'branch', + ref_name: 'main', + commit: { type: 'commit', hash: COMMIT_SHA }, + }, + }) + expect(bitbucketTriggerPipelineTool.params).not.toHaveProperty('variables') + expect(bitbucketTriggerPipelineTool.params).not.toHaveProperty('selector') + expect(bitbucketTriggerPipelineTool.params).not.toHaveProperty('runner') + expect(bitbucketTriggerPipelineTool.oauth?.requiredScopes).toEqual(['pipeline']) + expect(() => requestBody(bitbucketTriggerPipelineTool, { ...params, refName: ' ' })).toThrow( + /refName must be a non-empty string/ + ) + expect(() => + requestBody(bitbucketTriggerPipelineTool, { + ...params, + refType: 'commit', + } as unknown as BitbucketTriggerPipelineParams) + ).toThrow(/refType must be one of/) + expect(() => + requestBody(bitbucketTriggerPipelineTool, { + ...params, + commitHash: 'main', + }) + ).toThrow(/commitHash must be a full 40-character SHA-1/) + }) + + it('uses pipeline:write only for stopping and never retries either mutation', () => { + expect(bitbucketStopPipelineTool.oauth?.requiredScopes).toEqual(['pipeline:write']) + expect(bitbucketTriggerPipelineTool.request.retry).toBeUndefined() + expect(bitbucketStopPipelineTool.request.retry).toBeUndefined() + }) +}) + +describe('Bitbucket pipeline response normalization', () => { + it('normalizes pipeline lists, details, and trigger responses consistently', async () => { + const listed = await bitbucketListPipelinesTool.transformResponse!( + Response.json({ values: [RAW_PIPELINE], size: 1, page: 1, pagelen: 20 }) + ) + const fetched = await bitbucketGetPipelineTool.transformResponse!(Response.json(RAW_PIPELINE)) + const triggered = await bitbucketTriggerPipelineTool.transformResponse!( + Response.json(RAW_PIPELINE) + ) + + expect(listed.output.items[0]).toEqual({ + type: 'pipeline', + uuid: '{pipeline-1}', + buildNumber: 42, + creator: { + uuid: '{user-1}', + accountId: 'account-1', + type: 'user', + displayName: 'Ada Lovelace', + createdOn: null, + selfUrl: 'https://api.bitbucket.org/2.0/users/ada', + htmlUrl: null, + avatarUrl: null, + }, + repositoryFullName: 'acme/demo', + target: { + type: 'pipeline_ref_target', + refType: 'branch', + refName: 'main', + commitHash: 'abc123', + selectorType: 'custom', + selectorPattern: 'deploy', + }, + triggerType: 'pipeline_manual_trigger', + state: { + name: 'COMPLETED', + stage: 'COMPLETED', + result: 'FAILED', + errorKey: 'configuration-error', + errorMessage: 'Invalid pipeline configuration', + }, + createdOn: '2026-01-01T00:00:00Z', + completedOn: '2026-01-01T00:02:00Z', + buildSecondsUsed: 120, + selfUrl: 'https://api.bitbucket.org/2.0/repositories/acme/demo/pipelines/pipeline-1', + stepsUrl: 'https://api.bitbucket.org/2.0/repositories/acme/demo/pipelines/pipeline-1/steps', + }) + expect(fetched.output.pipeline).toEqual(listed.output.items[0]) + expect(triggered.output.pipeline).toEqual(listed.output.items[0]) + }) + + it('normalizes pipeline steps and their documented commands and errors', async () => { + const result = await bitbucketListPipelineStepsTool.transformResponse!( + Response.json({ values: [RAW_PIPELINE_STEP], size: 1 }) + ) + expect(result.output.items[0]).toEqual({ + type: 'pipeline_step', + uuid: '{step-1}', + startedOn: '2026-01-01T00:00:00Z', + completedOn: '2026-01-01T00:02:00Z', + state: { + name: 'COMPLETED', + result: 'FAILED', + errorKey: 'script-error', + errorMessage: 'Tests failed', + }, + imageName: 'node:22', + setupCommands: [{ name: 'setup', command: 'npm install' }], + scriptCommands: [ + { name: 'test', command: 'bun test' }, + { name: 'build', command: 'bun run build' }, + ], + }) + }) + + it('requires top-level resource types while preserving future type values', async () => { + await expect( + bitbucketGetPipelineTool.transformResponse!( + Response.json({ ...RAW_PIPELINE, type: undefined }) + ) + ).rejects.toThrow(/pipeline\.type must be a non-empty string/) + await expect( + bitbucketListPipelineStepsTool.transformResponse!( + Response.json({ values: [{ ...RAW_PIPELINE_STEP, type: '' }] }) + ) + ).rejects.toThrow(/pipeline step\.type must be a non-empty string/) + + const futurePipeline = await bitbucketGetPipelineTool.transformResponse!( + Response.json({ ...RAW_PIPELINE, type: 'future_pipeline_variant' }) + ) + const futureStep = await bitbucketListPipelineStepsTool.transformResponse!( + Response.json({ values: [{ ...RAW_PIPELINE_STEP, type: 'future_step_variant' }] }) + ) + expect(futurePipeline.output.pipeline.type).toBe('future_pipeline_variant') + expect(futureStep.output.items[0].type).toBe('future_step_variant') + }) + + it('distinguishes absent, empty, and malformed optional command collections', async () => { + const absent = await bitbucketListPipelineStepsTool.transformResponse!( + Response.json({ + values: [{ ...RAW_PIPELINE_STEP, setup_commands: undefined, script_commands: undefined }], + }) + ) + const empty = await bitbucketListPipelineStepsTool.transformResponse!( + Response.json({ values: [{ ...RAW_PIPELINE_STEP, setup_commands: [], script_commands: [] }] }) + ) + expect(absent.output.items[0]).toMatchObject({ setupCommands: null, scriptCommands: null }) + expect(empty.output.items[0]).toMatchObject({ setupCommands: [], scriptCommands: [] }) + + await expect( + bitbucketListPipelineStepsTool.transformResponse!( + Response.json({ values: [{ ...RAW_PIPELINE_STEP, setup_commands: null }] }) + ) + ).rejects.toThrow(/setup_commands must be an array when present/) + await expect( + bitbucketListPipelineStepsTool.transformResponse!( + Response.json({ values: [{ ...RAW_PIPELINE_STEP, script_commands: [null] }] }) + ) + ).rejects.toThrow(/script_commands\[0\] must be an object/) + }) + + it('treats a successful stopPipeline 204 as completion', async () => { + const result = await bitbucketStopPipelineTool.transformResponse!( + new Response(null, { status: 204 }) + ) + expect(result).toEqual({ success: true, output: { stopped: true } }) + }) + + it('builds and transforms paginated pipeline steps', async () => { + const params = { + ...REPOSITORY_PARAMS, + pipelineUuid: '{pipeline-1}', + pageLen: 30, + } satisfies BitbucketListPipelineStepsParams + expect(requestUrl(bitbucketListPipelineStepsTool, params)).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pipelines/%7Bpipeline-1%7D/steps?pagelen=30' + ) + expect(bitbucketListPipelineStepsTool.request.retry).toMatchObject({ + enabled: true, + retryIdempotentOnly: true, + }) + }) +}) + +describe('Bitbucket pipeline step logs', () => { + it('requests a bounded byte tail and drops authorization on redirects', () => { + const params = { + ...REPOSITORY_PARAMS, + pipelineUuid: '{pipeline-1}', + stepUuid: '{step-1}', + maxCharacters: 4_096, + } satisfies BitbucketGetPipelineStepLogParams + expect(bitbucketGetPipelineStepLogTool.request.headers(params)).toMatchObject({ + Accept: '*/*', + Authorization: 'Bearer oauth-token', + Range: 'bytes=-16384', + }) + expect(bitbucketGetPipelineStepLogTool.request.stripAuthOnRedirect).toBe(true) + expect(bitbucketGetPipelineStepLogTool.request.retry).toMatchObject({ + enabled: true, + maxRetries: 2, + retryIdempotentOnly: true, + }) + }) + + it('trims the partial leading line of a ranged log and reports total bytes', async () => { + const body = 'ise\nFAILED: expected 1 to be 2\n' + const result = await bitbucketGetPipelineStepLogTool.transformResponse!( + new Response(body, { + status: 206, + headers: { 'Content-Range': 'bytes 969-999/1000' }, + }), + { ...REPOSITORY_PARAMS, pipelineUuid: '{pipeline-1}', stepUuid: '{step-1}' } + ) + expect(result.output).toEqual({ + log: 'FAILED: expected 1 to be 2\n', + truncated: true, + totalBytes: 1000, + }) + }) + + it('locally retains only the useful tail when Range is ignored', async () => { + const body = `${'noise line\n'.repeat(20)}FAILED\n` + const result = await bitbucketGetPipelineStepLogTool.transformResponse!( + new Response(body, { headers: { 'Content-Length': String(Buffer.byteLength(body)) } }), + { + ...REPOSITORY_PARAMS, + pipelineUuid: '{pipeline-1}', + stepUuid: '{step-1}', + maxCharacters: 10, + } + ) + expect(result.output).toEqual({ + log: 'ne\nFAILED\n', + truncated: true, + totalBytes: Buffer.byteLength(body), + }) + }) + + it('rejects log caps outside the supported range', async () => { + await expect( + bitbucketGetPipelineStepLogTool.transformResponse!(new Response('log'), { + ...REPOSITORY_PARAMS, + pipelineUuid: '{pipeline-1}', + stepUuid: '{step-1}', + maxCharacters: 0, + }) + ).rejects.toThrow(/maxCharacters must be an integer between 1 and 200000/) + }) +}) diff --git a/apps/sim/tools/bitbucket/pull-requests.test.ts b/apps/sim/tools/bitbucket/pull-requests.test.ts new file mode 100644 index 00000000000..5d0b0ab6875 --- /dev/null +++ b/apps/sim/tools/bitbucket/pull-requests.test.ts @@ -0,0 +1,773 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { bitbucketApprovePullRequestTool } from '@/tools/bitbucket/approve_pull_request' +import { bitbucketCreatePullRequestTool } from '@/tools/bitbucket/create_pull_request' +import { bitbucketCreatePullRequestCommentTool } from '@/tools/bitbucket/create_pull_request_comment' +import { bitbucketDeclinePullRequestTool } from '@/tools/bitbucket/decline_pull_request' +import { bitbucketGetMergeTaskStatusTool } from '@/tools/bitbucket/get_merge_task_status' +import { bitbucketGetPullRequestTool } from '@/tools/bitbucket/get_pull_request' +import { bitbucketGetPullRequestDiffTool } from '@/tools/bitbucket/get_pull_request_diff' +import { bitbucketGetPullRequestDiffstatTool } from '@/tools/bitbucket/get_pull_request_diffstat' +import { bitbucketListPullRequestCommentsTool } from '@/tools/bitbucket/list_pull_request_comments' +import { bitbucketListPullRequestCommitStatusesTool } from '@/tools/bitbucket/list_pull_request_commit_statuses' +import { bitbucketListPullRequestsTool } from '@/tools/bitbucket/list_pull_requests' +import { bitbucketMergePullRequestTool } from '@/tools/bitbucket/merge_pull_request' +import { bitbucketRequestPullRequestChangesTool } from '@/tools/bitbucket/request_pull_request_changes' +import type { + BitbucketCreatePullRequestCommentParams, + BitbucketCreatePullRequestParams, + BitbucketGetMergeTaskStatusParams, + BitbucketGetPullRequestDiffParams, + BitbucketListPullRequestCommentsParams, + BitbucketListPullRequestCommitStatusesParams, + BitbucketListPullRequestsParams, + BitbucketMergePullRequestParams, + BitbucketPaginatedPullRequestParams, +} from '@/tools/bitbucket/types' +import { assertBitbucketResponseOk } from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +const serverMocks = vi.hoisted(() => ({ + resolveBitbucketPullRequestRedirect: vi.fn(), + secureBitbucketRead: vi.fn(), + secureBitbucketPullRequestRedirect: vi.fn(), +})) + +vi.mock('@/tools/bitbucket/utils.server', () => serverMocks) + +const PULL_REQUEST_PARAMS = { + accessToken: 'oauth-token', + workspaceSlug: 'acme team', + repoSlug: 'sdk/core', + prId: 7, +} as const + +const RAW_USER = { + uuid: '{user-1}', + account_id: 'account-1', + type: 'user', + display_name: 'Ada Lovelace', + created_on: '2026-01-01T00:00:00Z', + links: { + self: { href: 'https://api.bitbucket.org/2.0/users/ada' }, + html: { href: 'https://bitbucket.org/ada' }, + avatar: { href: 'https://avatar.test/ada' }, + }, +} + +const RAW_PARTICIPANT = { + type: 'participant', + user: RAW_USER, + role: 'REVIEWER', + approved: true, + state: 'approved', + participated_on: '2026-01-02T00:00:00Z', +} + +const RAW_PULL_REQUEST = { + type: 'pullrequest', + id: 7, + title: 'Ship the SDK', + description: 'Top-level description', + rendered: { description: { raw: 'Rendered fallback' } }, + summary: { raw: 'Summary fallback' }, + state: 'OPEN', + draft: false, + queued: false, + author: RAW_USER, + closed_by: null, + source: { + branch: { name: 'feature/sdk' }, + commit: { hash: 'source123' }, + repository: { uuid: '{repo-1}', full_name: 'acme/demo' }, + }, + destination: { + branch: { name: 'main' }, + commit: { hash: 'main123' }, + repository: { uuid: '{repo-1}', full_name: 'acme/demo' }, + }, + merge_commit: null, + comment_count: 3, + task_count: 1, + close_source_branch: true, + reason: null, + created_on: '2026-01-01T00:00:00Z', + updated_on: '2026-01-02T00:00:00Z', + reviewers: [RAW_USER], + participants: [RAW_PARTICIPANT], + links: { + self: { href: 'https://api.bitbucket.org/2.0/repositories/acme/demo/pullrequests/7' }, + html: { href: 'https://bitbucket.org/acme/demo/pull-requests/7' }, + }, +} + +const RAW_COMMENT = { + type: 'pullrequest_comment', + id: 10, + created_on: '2026-01-02T00:00:00Z', + updated_on: '2026-01-03T00:00:00Z', + content: { raw: 'Looks good' }, + user: RAW_USER, + deleted: false, + parent: { id: 9 }, + inline: { path: 'src/index.ts', from: 4, to: 5, start_from: 2, start_to: 3 }, + pending: false, + resolution: { user: RAW_USER, created_on: '2026-01-04T00:00:00Z' }, + links: { + self: { href: 'https://api.bitbucket.org/comment/10' }, + html: { href: 'https://bitbucket.org/comment/10' }, + }, +} + +const RAW_COMMIT_STATUS = { + type: 'build', + key: 'ci/test', + refname: 'feature/sdk', + url: 'https://ci.example.test/build/1', + state: 'SUCCESSFUL', + name: 'CI', + description: 'All checks passed', + created_on: '2026-01-02T00:00:00Z', + updated_on: '2026-01-03T00:00:00Z', + links: { + self: { href: 'https://api.bitbucket.org/status/1' }, + commit: { href: 'https://api.bitbucket.org/commit/source123' }, + }, +} + +const RAW_DIFFSTAT = { + type: 'diffstat', + status: 'modified', + lines_added: 12, + lines_removed: 4, + old: { path: 'src/old.ts', commit: { hash: 'main123' } }, + new: { path: 'src/new.ts', commit: { hash: 'source123' } }, +} + +function requestUrl(tool: ToolConfig, params: P): string { + return typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url +} + +function requestBody(tool: ToolConfig, params: P): unknown { + return tool.request.body?.(params) +} + +afterEach(() => { + vi.resetAllMocks() +}) + +describe('Bitbucket pull request request builders', () => { + it('builds list and detail URLs with encoded coordinates and filters', () => { + const list = new URL( + requestUrl(bitbucketListPullRequestsTool, { + ...PULL_REQUEST_PARAMS, + state: 'OPEN', + q: 'draft = false', + sort: '-updated_on', + pageLen: 25, + } satisfies BitbucketListPullRequestsParams) + ) + expect(list.pathname).toBe('/2.0/repositories/acme%20team/sdk%2Fcore/pullrequests') + expect(Object.fromEntries(list.searchParams)).toEqual({ + state: 'OPEN', + q: 'draft = false', + sort: '-updated_on', + pagelen: '25', + }) + expect(requestUrl(bitbucketGetPullRequestTool, PULL_REQUEST_PARAMS)).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pullrequests/7' + ) + expect(() => + requestUrl(bitbucketListPullRequestsTool, { + ...PULL_REQUEST_PARAMS, + state: 'CLOSED', + } as unknown as BitbucketListPullRequestsParams) + ).toThrow(/state must be one of/) + }) + + it('builds the complete documented create-pull-request body', () => { + const params = { + ...PULL_REQUEST_PARAMS, + title: ' Ship the SDK ', + sourceBranch: ' feature/sdk ', + destinationBranch: ' main ', + description: 'Ready for review', + closeSourceBranch: false, + draft: true, + reviewerUuids: [' {reviewer-1} ', '{reviewer-2}'], + } satisfies BitbucketCreatePullRequestParams + + expect(requestBody(bitbucketCreatePullRequestTool, params)).toEqual({ + title: 'Ship the SDK', + source: { branch: { name: 'feature/sdk' } }, + destination: { branch: { name: 'main' } }, + description: 'Ready for review', + close_source_branch: false, + draft: true, + reviewers: [{ uuid: '{reviewer-1}' }, { uuid: '{reviewer-2}' }], + }) + expect(() => + requestBody(bitbucketCreatePullRequestTool, { + ...params, + reviewerUuids: [' '], + }) + ).toThrow(/reviewer UUID must be a non-empty string/) + expect(() => + requestBody(bitbucketCreatePullRequestTool, { + ...params, + closeSourceBranch: 'false', + } as unknown as BitbucketCreatePullRequestParams) + ).toThrow(/closeSourceBranch must be a boolean/) + expect(() => + requestBody(bitbucketCreatePullRequestTool, { + ...params, + draft: 0, + } as unknown as BitbucketCreatePullRequestParams) + ).toThrow(/draft must be a boolean/) + expect(() => + requestBody(bitbucketCreatePullRequestTool, { + ...params, + reviewerUuids: ['{reviewer-1}', true], + } as unknown as BitbucketCreatePullRequestParams) + ).toThrow(/reviewer UUID must be a non-empty string/) + expect(() => + requestBody(bitbucketCreatePullRequestTool, { + ...params, + reviewerUuids: '{reviewer-1}', + } as unknown as BitbucketCreatePullRequestParams) + ).toThrow(/reviewerUuids must be an array of strings/) + }) + + it('builds every pull request action endpoint', () => { + expect(requestUrl(bitbucketDeclinePullRequestTool, PULL_REQUEST_PARAMS)).toMatch( + /\/pullrequests\/7\/decline$/ + ) + expect(requestUrl(bitbucketApprovePullRequestTool, PULL_REQUEST_PARAMS)).toMatch( + /\/pullrequests\/7\/approve$/ + ) + expect(requestUrl(bitbucketRequestPullRequestChangesTool, PULL_REQUEST_PARAMS)).toMatch( + /\/pullrequests\/7\/request-changes$/ + ) + expect( + requestUrl(bitbucketGetMergeTaskStatusTool, { + ...PULL_REQUEST_PARAMS, + taskId: 'task/with ?#', + } satisfies BitbucketGetMergeTaskStatusParams) + ).toMatch(/\/merge\/task-status\/task%2Fwith%20%3F%23$/) + }) + + it('builds comment and status pagination plus the reply body', () => { + const comments = new URL( + requestUrl(bitbucketListPullRequestCommentsTool, { + ...PULL_REQUEST_PARAMS, + q: 'deleted = false', + sort: '-created_on', + pageLen: 50, + } satisfies BitbucketListPullRequestCommentsParams) + ) + expect(comments.pathname).toMatch(/\/pullrequests\/7\/comments$/) + expect(Object.fromEntries(comments.searchParams)).toEqual({ + q: 'deleted = false', + sort: '-created_on', + pagelen: '50', + }) + + const statuses = new URL( + requestUrl(bitbucketListPullRequestCommitStatusesTool, { + ...PULL_REQUEST_PARAMS, + q: 'state = "FAILED"', + sort: '-created_on', + pageLen: 10, + } satisfies BitbucketListPullRequestCommitStatusesParams) + ) + expect(statuses.pathname).toMatch(/\/pullrequests\/7\/statuses$/) + expect(Object.fromEntries(statuses.searchParams)).toEqual({ + q: 'state = "FAILED"', + sort: '-created_on', + pagelen: '10', + }) + + const commentParams = { + ...PULL_REQUEST_PARAMS, + content: 'Please add a regression test.', + parentId: 9, + } satisfies BitbucketCreatePullRequestCommentParams + expect(requestBody(bitbucketCreatePullRequestCommentTool, commentParams)).toEqual({ + content: { raw: 'Please add a regression test.' }, + parent: { id: 9 }, + }) + expect(() => + requestBody(bitbucketCreatePullRequestCommentTool, { ...commentParams, content: ' ' }) + ).toThrow(/content must be a non-empty string/) + expect(() => + requestBody(bitbucketCreatePullRequestCommentTool, { ...commentParams, parentId: 0 }) + ).toThrow(/parentId must be a positive integer/) + expect(() => + requestBody(bitbucketCreatePullRequestCommentTool, { + ...commentParams, + parentId: true, + } as unknown as BitbucketCreatePullRequestCommentParams) + ).toThrow(/parentId must be a positive integer/) + }) +}) + +describe('Bitbucket pull request response normalization', () => { + it('normalizes list, get, create, decline, and synchronous merge responses consistently', async () => { + const listed = await bitbucketListPullRequestsTool.transformResponse!( + Response.json({ values: [RAW_PULL_REQUEST], size: 1, page: 1, pagelen: 20 }) + ) + const fetched = await bitbucketGetPullRequestTool.transformResponse!( + Response.json(RAW_PULL_REQUEST) + ) + const created = await bitbucketCreatePullRequestTool.transformResponse!( + Response.json(RAW_PULL_REQUEST) + ) + const declined = await bitbucketDeclinePullRequestTool.transformResponse!( + Response.json(RAW_PULL_REQUEST) + ) + const merged = await bitbucketMergePullRequestTool.transformResponse!( + Response.json({ ...RAW_PULL_REQUEST, state: 'MERGED' }), + PULL_REQUEST_PARAMS + ) + + expect(listed.output.items[0]).toMatchObject({ + id: 7, + description: 'Top-level description', + source: { + branchName: 'feature/sdk', + commitHash: 'source123', + repositoryUuid: '{repo-1}', + repositoryFullName: 'acme/demo', + }, + reviewers: [{ accountId: 'account-1' }], + participants: [{ approved: true, user: { accountId: 'account-1' } }], + }) + expect(fetched.output.pullRequest).toEqual(listed.output.items[0]) + expect(created.output.pullRequest).toEqual(listed.output.items[0]) + expect(declined.output.pullRequest).toEqual(listed.output.items[0]) + expect(merged.output).toMatchObject({ + status: 'completed', + taskId: null, + taskUrl: null, + pullRequest: { state: 'MERGED' }, + }) + }) + + it('preserves null reviewers and participants when Bitbucket omits those expansions', async () => { + const result = await bitbucketGetPullRequestTool.transformResponse!( + Response.json({ ...RAW_PULL_REQUEST, reviewers: undefined, participants: undefined }) + ) + expect(result.output.pullRequest).toMatchObject({ reviewers: null, participants: null }) + }) + + it('normalizes approval and change-request participants', async () => { + const approved = await bitbucketApprovePullRequestTool.transformResponse!( + Response.json(RAW_PARTICIPANT) + ) + const changes = await bitbucketRequestPullRequestChangesTool.transformResponse!( + Response.json({ ...RAW_PARTICIPANT, approved: false, state: 'changes_requested' }) + ) + expect(approved.output.participant).toMatchObject({ + role: 'REVIEWER', + approved: true, + user: { uuid: '{user-1}', accountId: 'account-1' }, + }) + expect(changes.output.participant).toMatchObject({ + approved: false, + state: 'changes_requested', + }) + }) + + it('normalizes comments, replies, inline coordinates, resolutions, and statuses', async () => { + const comments = await bitbucketListPullRequestCommentsTool.transformResponse!( + Response.json({ values: [RAW_COMMENT] }) + ) + const created = await bitbucketCreatePullRequestCommentTool.transformResponse!( + Response.json(RAW_COMMENT) + ) + expect(comments.output.items[0]).toMatchObject({ + id: 10, + content: 'Looks good', + parentId: 9, + inline: { path: 'src/index.ts', from: 4, to: 5, startFrom: 2, startTo: 3 }, + resolution: { resolver: { accountId: 'account-1' }, resolvedOn: '2026-01-04T00:00:00Z' }, + }) + expect(created.output.comment).toEqual(comments.output.items[0]) + + const statuses = await bitbucketListPullRequestCommitStatusesTool.transformResponse!( + Response.json({ values: [RAW_COMMIT_STATUS] }) + ) + expect(statuses.output.items[0]).toEqual({ + type: 'build', + key: 'ci/test', + refName: 'feature/sdk', + url: 'https://ci.example.test/build/1', + state: 'SUCCESSFUL', + name: 'CI', + description: 'All checks passed', + createdOn: '2026-01-02T00:00:00Z', + updatedOn: '2026-01-03T00:00:00Z', + selfUrl: 'https://api.bitbucket.org/status/1', + commitUrl: 'https://api.bitbucket.org/commit/source123', + }) + }) + + it('rejects resources missing required type and commit-status invariants', async () => { + await expect( + bitbucketGetPullRequestTool.transformResponse!( + Response.json({ ...RAW_PULL_REQUEST, type: undefined }) + ) + ).rejects.toThrow(/pull request\.type must be a non-empty string/) + + for (const field of ['type', 'key', 'state'] as const) { + await expect( + bitbucketListPullRequestCommitStatusesTool.transformResponse!( + Response.json({ values: [{ ...RAW_COMMIT_STATUS, [field]: undefined }] }) + ) + ).rejects.toThrow(new RegExp(`commit status\\.${field} must be a non-empty string`)) + } + }) +}) + +describe('Bitbucket merge lifecycle', () => { + it('sends optional merge fields and forces asynchronous task creation support', () => { + const params = { + ...PULL_REQUEST_PARAMS, + mergeStrategy: 'squash_fast_forward', + message: 'Ship it', + closeSourceBranch: false, + } satisfies BitbucketMergePullRequestParams + expect(requestUrl(bitbucketMergePullRequestTool, params)).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pullrequests/7/merge?async=true' + ) + expect(requestBody(bitbucketMergePullRequestTool, params)).toEqual({ + merge_strategy: 'squash_fast_forward', + message: 'Ship it', + close_source_branch: false, + }) + + expect(() => + requestBody(bitbucketMergePullRequestTool, { + ...params, + mergeStrategy: 'octopus', + } as unknown as BitbucketMergePullRequestParams) + ).toThrow(/mergeStrategy must be one of/) + expect(() => + requestBody(bitbucketMergePullRequestTool, { + ...params, + closeSourceBranch: 'false', + } as unknown as BitbucketMergePullRequestParams) + ).toThrow(/closeSourceBranch must be a boolean/) + expect(() => + requestBody(bitbucketMergePullRequestTool, { + ...params, + message: 'é'.repeat(65_537), + }) + ).toThrow(/message must not exceed 131072 UTF-8 bytes/) + }) + + it('returns a validated task for a 202 merge response', async () => { + const response = new Response(null, { + status: 202, + headers: { + Location: + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pullrequests/7/merge/task-status/task%20one', + }, + }) + const result = await bitbucketMergePullRequestTool.transformResponse!( + response, + PULL_REQUEST_PARAMS + ) + expect(result).toEqual({ + success: true, + output: { + status: 'pending', + taskId: 'task one', + taskUrl: + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pullrequests/7/merge/task-status/task%20one', + pullRequest: null, + }, + }) + }) + + it('rejects missing, cross-origin, and wrong-pull-request task Locations', async () => { + await expect( + bitbucketMergePullRequestTool.transformResponse!( + new Response(null, { status: 202 }), + PULL_REQUEST_PARAMS + ) + ).rejects.toThrow(/omitted the Location/) + await expect( + bitbucketMergePullRequestTool.transformResponse!( + new Response(null, { + status: 202, + headers: { Location: 'https://evil.test/task/1' }, + }), + PULL_REQUEST_PARAMS + ) + ).rejects.toThrow(/Bitbucket Cloud API 2.0 URL/) + await expect( + bitbucketMergePullRequestTool.transformResponse!( + new Response(null, { + status: 202, + headers: { + Location: + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pullrequests/8/merge/task-status/task-1', + }, + }), + PULL_REQUEST_PARAMS + ) + ).rejects.toThrow(/did not match the requested pull request/) + }) + + it('normalizes pending and successful task-status responses', async () => { + const pending = await bitbucketGetMergeTaskStatusTool.transformResponse!( + Response.json({ + task_status: 'PENDING', + links: { self: { href: 'https://api.bitbucket.org/task/task-1' } }, + }) + ) + expect(pending.output).toEqual({ + taskStatus: 'PENDING', + selfUrl: 'https://api.bitbucket.org/task/task-1', + mergeResult: null, + }) + + const success = await bitbucketGetMergeTaskStatusTool.transformResponse!( + Response.json({ + task_status: 'SUCCESS', + merge_result: { ...RAW_PULL_REQUEST, state: 'MERGED' }, + }) + ) + expect(success.output).toMatchObject({ + taskStatus: 'SUCCESS', + mergeResult: { id: 7, state: 'MERGED' }, + }) + }) + + it('rejects merge-task errors, unknown statuses, and missing success results', async () => { + await expect( + bitbucketGetMergeTaskStatusTool.transformResponse!( + Response.json({ + type: 'error', + error: { message: 'The destination changed while the merge was queued' }, + }) + ) + ).rejects.toThrow('The destination changed while the merge was queued') + await expect( + bitbucketGetMergeTaskStatusTool.transformResponse!(Response.json({ task_status: 'FAILED' })) + ).rejects.toThrow(/must be PENDING or SUCCESS/) + await expect( + bitbucketGetMergeTaskStatusTool.transformResponse!(Response.json({ task_status: 'SUCCESS' })) + ).rejects.toThrow(/omitted merge_result/) + await expect( + bitbucketGetMergeTaskStatusTool.transformResponse!( + Response.json({ task_status: 'PENDING', merge_result: RAW_PULL_REQUEST }) + ) + ).rejects.toThrow(/unexpected merge_result/) + }) + + it('surfaces merge conflicts and failed merge checks from structured Bitbucket errors', async () => { + await expect( + assertBitbucketResponseOk( + Response.json( + { error: { message: 'Merge conflict: destination changed' } }, + { status: 409 } + ) + ) + ).rejects.toThrow('Merge conflict: destination changed') + await expect( + assertBitbucketResponseOk( + Response.json( + { error: { message: 'Required merge checks have not passed' } }, + { status: 409 } + ) + ) + ).rejects.toThrow('Required merge checks have not passed') + }) +}) + +describe('Bitbucket pull request diff safety', () => { + it('requires a file path and applies it only to the validated repository diff target', async () => { + serverMocks.secureBitbucketPullRequestRedirect.mockResolvedValueOnce( + new Response('@@ -1 +1 @@\n-old\n+new\n', { + headers: { 'Content-Length': '24', 'Content-Type': 'text/plain' }, + }) + ) + const params = { + ...PULL_REQUEST_PARAMS, + path: '/src/my file.ts/', + maxCharacters: 100, + } satisfies BitbucketGetPullRequestDiffParams + + const result = await bitbucketGetPullRequestDiffTool.directExecution!(params) + + expect(result).toMatchObject({ + success: true, + output: { + diff: '@@ -1 +1 @@\n-old\n+new\n', + decodingLossy: false, + truncated: false, + }, + }) + expect(serverMocks.secureBitbucketPullRequestRedirect).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pullrequests/7/diff', + 'acme team', + 'sdk/core', + 'diff', + expect.objectContaining({ + Accept: '*/*', + Authorization: 'Bearer oauth-token', + Range: 'bytes=0-399', + }), + 10 * 1024 * 1024, + { signal: undefined, targetQuery: { path: 'src/my file.ts', binary: 'false' } } + ) + expect(requestUrl(bitbucketGetPullRequestDiffTool, params)).not.toContain('path=') + }) + + it('rejects hostile repository-relative paths before making a redirect request', async () => { + await expect( + bitbucketGetPullRequestDiffTool.directExecution!({ + ...PULL_REQUEST_PARAMS, + path: '../secret', + }) + ).rejects.toThrow(/dot segment/) + expect(serverMocks.secureBitbucketPullRequestRedirect).not.toHaveBeenCalled() + }) + + it('locally caps raw diff text when a Range response is ignored', async () => { + const result = await bitbucketGetPullRequestDiffTool.transformResponse!( + new Response('0123456789', { headers: { 'Content-Length': '10' } }), + { ...PULL_REQUEST_PARAMS, path: 'src/index.ts', maxCharacters: 4 } + ) + expect(result.output).toEqual({ + diff: '0123', + decodingLossy: false, + truncated: true, + returnedBytes: 10, + fullBytes: 10, + }) + }) + + it('lossily decodes invalid UTF-8 only for pull request diffs', async () => { + const result = await bitbucketGetPullRequestDiffTool.transformResponse!( + new Response(new Uint8Array([0x41, 0x80]), { + headers: { 'Content-Length': '2', 'Content-Type': 'text/plain' }, + }), + { ...PULL_REQUEST_PARAMS, path: 'src/index.ts', maxCharacters: 100 } + ) + + expect(result.output).toEqual({ + diff: 'A�', + decodingLossy: true, + truncated: false, + returnedBytes: 2, + fullBytes: 2, + }) + }) + + it('uses the PR redirect only for the first diffstat page and puts pagelen on the target', async () => { + serverMocks.secureBitbucketPullRequestRedirect.mockResolvedValueOnce( + Response.json({ values: [RAW_DIFFSTAT] }) + ) + const params = { + ...PULL_REQUEST_PARAMS, + pageLen: 25, + } satisfies BitbucketPaginatedPullRequestParams + + const result = await bitbucketGetPullRequestDiffstatTool.directExecution!(params) + + expect(result.output.items[0]).toEqual({ + type: 'diffstat', + status: 'modified', + linesAdded: 12, + linesRemoved: 4, + oldPath: 'src/old.ts', + newPath: 'src/new.ts', + oldCommitHash: 'main123', + newCommitHash: 'source123', + }) + expect(serverMocks.secureBitbucketPullRequestRedirect).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pullrequests/7/diffstat', + 'acme team', + 'sdk/core', + 'diffstat', + expect.objectContaining({ Authorization: 'Bearer oauth-token' }), + 2 * 1024 * 1024, + { signal: undefined, targetQuery: { pagelen: '25' } } + ) + expect(serverMocks.secureBitbucketRead).not.toHaveBeenCalled() + }) + + it('fetches an already-validated repository diffstat cursor directly', async () => { + const nextUrl = + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/diffstat/main..feature?page=2' + serverMocks.resolveBitbucketPullRequestRedirect.mockResolvedValueOnce( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/diffstat/main..feature' + ) + serverMocks.secureBitbucketRead.mockResolvedValueOnce( + Response.json({ values: [RAW_DIFFSTAT], page: 2 }) + ) + + const result = await bitbucketGetPullRequestDiffstatTool.directExecution!({ + ...PULL_REQUEST_PARAMS, + nextUrl, + pageLen: 99, + }) + + expect(result.output.page.page).toBe(2) + expect(serverMocks.secureBitbucketRead).toHaveBeenCalledWith( + nextUrl, + expect.objectContaining({ Authorization: 'Bearer oauth-token' }), + 2 * 1024 * 1024, + { maxRedirects: 0, signal: undefined } + ) + expect(serverMocks.secureBitbucketPullRequestRedirect).not.toHaveBeenCalled() + }) + + it('rejects a diffstat cursor for a different pull request revspec', async () => { + serverMocks.resolveBitbucketPullRequestRedirect.mockResolvedValueOnce( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/diffstat/main..feature' + ) + + await expect( + bitbucketGetPullRequestDiffstatTool.directExecution!({ + ...PULL_REQUEST_PARAMS, + nextUrl: + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/diffstat/main..unrelated?page=2', + }) + ).rejects.toThrow(/does not belong to this Bitbucket pull request diffstat/) + expect(serverMocks.secureBitbucketRead).not.toHaveBeenCalled() + }) + + it('rejects hostile or cross-repository diffstat cursors before fetching', async () => { + const invalid = [ + 'https://evil.test/2.0/repositories/acme%20team/sdk%2Fcore/diffstat/a..b?page=2', + 'https://api.bitbucket.org/2.0/repositories/acme%20team/other/diffstat/a..b?page=2', + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/diff/a..b?page=2', + ] + for (const nextUrl of invalid) { + await expect( + bitbucketGetPullRequestDiffstatTool.directExecution!({ + ...PULL_REQUEST_PARAMS, + nextUrl, + }) + ).rejects.toThrow() + } + expect(serverMocks.secureBitbucketRead).not.toHaveBeenCalled() + expect(serverMocks.secureBitbucketPullRequestRedirect).not.toHaveBeenCalled() + }) + + it('normalizes executor-provided diffstat JSON through the same transform', async () => { + const result = await bitbucketGetPullRequestDiffstatTool.transformResponse!( + Response.json({ values: [RAW_DIFFSTAT], page: 3, pagelen: 20 }) + ) + expect(result.output).toMatchObject({ + items: [{ newPath: 'src/new.ts', linesAdded: 12 }], + page: { page: 3, pageLen: 20 }, + }) + }) +}) diff --git a/apps/sim/tools/bitbucket/repository-source.test.ts b/apps/sim/tools/bitbucket/repository-source.test.ts new file mode 100644 index 00000000000..4197f76f50c --- /dev/null +++ b/apps/sim/tools/bitbucket/repository-source.test.ts @@ -0,0 +1,861 @@ +/** + * @vitest-environment node + */ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { bitbucketCreateBranchTool } from '@/tools/bitbucket/create_branch' +import { bitbucketDeleteBranchTool } from '@/tools/bitbucket/delete_branch' +import { bitbucketGetCommitTool } from '@/tools/bitbucket/get_commit' +import { bitbucketGetFileTool } from '@/tools/bitbucket/get_file' +import { bitbucketGetFileMetadataTool } from '@/tools/bitbucket/get_file_metadata' +import { bitbucketGetRepositoryTool } from '@/tools/bitbucket/get_repository' +import { bitbucketTools } from '@/tools/bitbucket/index' +import { bitbucketListBranchesTool } from '@/tools/bitbucket/list_branches' +import { bitbucketListCommitsTool } from '@/tools/bitbucket/list_commits' +import { bitbucketListDirectoryTool } from '@/tools/bitbucket/list_directory' +import { bitbucketListRepositoriesTool } from '@/tools/bitbucket/list_repositories' +import { bitbucketListWorkspacesTool } from '@/tools/bitbucket/list_workspaces' +import type { + BitbucketCreateBranchParams, + BitbucketFileParams, + BitbucketGetCommitParams, + BitbucketGetFileParams, + BitbucketListBranchesParams, + BitbucketListCommitsParams, + BitbucketListDirectoryParams, + BitbucketListRepositoriesParams, + BitbucketListWorkspacesParams, + BitbucketRepositoryParams, +} from '@/tools/bitbucket/types' +import type { ToolConfig } from '@/tools/types' + +const serverMocks = vi.hoisted(() => ({ + secureBitbucketRead: vi.fn(), + secureBitbucketPullRequestRedirect: vi.fn(), +})) + +vi.mock('@/tools/bitbucket/utils.server', () => serverMocks) + +const REPOSITORY_PARAMS = { + accessToken: 'oauth-token', + workspaceSlug: 'acme team', + repoSlug: 'sdk/core', +} as const + +const COMMIT_SHA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' +const FEATURE_SHA = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' + +const RAW_USER = { + uuid: '{user-1}', + account_id: 'account-1', + type: 'user', + display_name: 'Ada Lovelace', + created_on: '2026-01-01T00:00:00Z', + links: { + self: { href: 'https://api.bitbucket.org/2.0/users/ada' }, + html: { href: 'https://bitbucket.org/ada' }, + avatar: { href: 'https://avatar.test/ada' }, + }, +} + +const RAW_REPOSITORY = { + type: 'repository', + uuid: '{repo-1}', + slug: 'demo', + name: 'Demo repository', + full_name: 'acme/demo', + description: 'SDK repository', + is_private: true, + scm: 'git', + language: 'typescript', + size: 1234, + created_on: '2026-01-01T00:00:00Z', + updated_on: '2026-01-02T00:00:00Z', + mainbranch: { name: 'main' }, + owner: RAW_USER, + project: { uuid: '{project-1}', key: 'SDK', name: 'SDK' }, + links: { + self: { href: 'https://api.bitbucket.org/2.0/repositories/acme/demo' }, + html: { href: 'https://bitbucket.org/acme/demo' }, + }, +} + +const RAW_COMMIT = { + type: 'commit', + hash: 'abc123', + date: '2026-01-02T00:00:00Z', + message: 'Ship it', + summary: { raw: 'Ship it' }, + author: { raw: 'Ada ', user: RAW_USER }, + committer: { raw: 'Ada ', user: RAW_USER }, + parents: [{ type: 'commit', hash: 'parent123' }], + links: { + self: { href: 'https://api.bitbucket.org/2.0/repositories/acme/demo/commit/abc123' }, + html: { href: 'https://bitbucket.org/acme/demo/commits/abc123' }, + }, +} + +const RAW_BRANCH = { + name: 'feature/demo', + type: 'branch', + target: RAW_COMMIT, + merge_strategies: ['merge_commit', 'squash'], + default_merge_strategy: 'merge_commit', + links: { + self: { href: 'https://api.bitbucket.org/2.0/repositories/acme/demo/refs/branches/feature' }, + html: { href: 'https://bitbucket.org/acme/demo/branch/feature' }, + }, +} + +function requestUrl(tool: ToolConfig, params: P): string { + return typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url +} + +function requestBody(tool: ToolConfig, params: P): unknown { + return tool.request.body?.(params) +} + +afterEach(() => { + vi.clearAllMocks() +}) + +describe('Bitbucket action tool contracts', () => { + it('exports the complete 30-tool action-only surface', () => { + expect(bitbucketTools.map((tool) => tool.id).sort()).toEqual( + [ + 'bitbucket_approve_pull_request', + 'bitbucket_create_branch', + 'bitbucket_create_pull_request', + 'bitbucket_create_pull_request_comment', + 'bitbucket_decline_pull_request', + 'bitbucket_delete_branch', + 'bitbucket_get_commit', + 'bitbucket_get_file', + 'bitbucket_get_file_metadata', + 'bitbucket_get_pipeline', + 'bitbucket_get_pipeline_step_log', + 'bitbucket_get_pull_request', + 'bitbucket_get_pull_request_diff', + 'bitbucket_get_pull_request_diffstat', + 'bitbucket_get_pull_request_merge_task_status', + 'bitbucket_get_repository', + 'bitbucket_list_branches', + 'bitbucket_list_commits', + 'bitbucket_list_directory', + 'bitbucket_list_pipeline_steps', + 'bitbucket_list_pipelines', + 'bitbucket_list_pull_request_comments', + 'bitbucket_list_pull_request_commit_statuses', + 'bitbucket_list_pull_requests', + 'bitbucket_list_repositories', + 'bitbucket_list_workspaces', + 'bitbucket_merge_pull_request', + 'bitbucket_request_pull_request_changes', + 'bitbucket_stop_pipeline', + 'bitbucket_trigger_pipeline', + ].sort() + ) + }) + + it('uses hidden OAuth and the fixed Bitbucket provider on every action', () => { + for (const tool of bitbucketTools) { + expect(tool.oauth, tool.id).toMatchObject({ required: true, provider: 'bitbucket' }) + expect(tool.oauth?.requiredScopes?.length, tool.id).toBeGreaterThan(0) + expect(tool.params.accessToken, tool.id).toMatchObject({ + type: 'string', + required: true, + visibility: 'hidden', + }) + } + }) + + it('enables bounded retry only on safe reads, never on mutations', () => { + for (const tool of bitbucketTools) { + const method = typeof tool.request.method === 'function' ? null : tool.request.method + if (method !== 'GET') { + expect(tool.request.retry, tool.id).toBeUndefined() + } else { + expect(tool.request.retry, tool.id).toMatchObject({ + enabled: true, + maxRetries: 2, + retryIdempotentOnly: true, + }) + } + } + }) +}) + +describe('Bitbucket workspace and repository tools', () => { + it('builds workspace and repository list filters with bounded pagination', () => { + const workspaces = new URL( + requestUrl(bitbucketListWorkspacesTool, { + accessToken: 'oauth-token', + sort: 'slug', + administrator: false, + pageLen: 10, + } satisfies BitbucketListWorkspacesParams) + ) + expect(workspaces.pathname).toBe('/2.0/user/workspaces') + expect(Object.fromEntries(workspaces.searchParams)).toEqual({ + sort: 'slug', + administrator: 'false', + pagelen: '10', + }) + + const repositories = new URL( + requestUrl(bitbucketListRepositoriesTool, { + accessToken: 'oauth-token', + workspaceSlug: 'team / blue', + role: 'owner', + q: 'name ~ "sdk"', + sort: '-updated_on', + pageLen: 40, + } satisfies BitbucketListRepositoriesParams) + ) + expect(repositories.pathname).toBe('/2.0/repositories/team%20%2F%20blue') + expect(Object.fromEntries(repositories.searchParams)).toEqual({ + role: 'owner', + q: 'name ~ "sdk"', + sort: '-updated_on', + pagelen: '40', + }) + expect(() => + requestUrl(bitbucketListWorkspacesTool, { + accessToken: 'oauth-token', + administrator: 'false', + } as unknown as BitbucketListWorkspacesParams) + ).toThrow(/administrator must be a boolean/) + expect(() => + requestUrl(bitbucketListRepositoriesTool, { + accessToken: 'oauth-token', + workspaceSlug: 'team', + role: 'reader', + } as unknown as BitbucketListRepositoriesParams) + ).toThrow(/role must be one of/) + }) + + it('rejects a repository cursor from another workspace', () => { + expect(() => + requestUrl(bitbucketListRepositoriesTool, { + accessToken: 'oauth-token', + workspaceSlug: 'acme', + nextUrl: 'https://api.bitbucket.org/2.0/repositories/other?page=2', + } satisfies BitbucketListRepositoriesParams) + ).toThrow(/does not belong/) + }) + + it('encodes repository coordinates in detail URLs', () => { + expect(requestUrl(bitbucketGetRepositoryTool, REPOSITORY_PARAMS)).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore' + ) + }) + + it('normalizes workspace and repository responses', async () => { + const workspaceResult = await bitbucketListWorkspacesTool.transformResponse!( + Response.json({ + values: [ + { + type: 'workspace_access', + administrator: true, + workspace: { + slug: 'acme', + uuid: '{workspace-1}', + links: { + self: { href: 'https://api.bitbucket.org/2.0/workspaces/acme' }, + avatar: { href: 'https://avatar.test/acme' }, + }, + }, + }, + ], + size: 1, + page: 1, + pagelen: 20, + }) + ) + expect(workspaceResult.output).toEqual({ + items: [ + { + type: 'workspace_access', + slug: 'acme', + uuid: '{workspace-1}', + administrator: true, + selfUrl: 'https://api.bitbucket.org/2.0/workspaces/acme', + avatarUrl: 'https://avatar.test/acme', + }, + ], + page: { size: 1, page: 1, pageLen: 20, nextUrl: null, previousUrl: null }, + }) + + const listResult = await bitbucketListRepositoriesTool.transformResponse!( + Response.json({ values: [RAW_REPOSITORY], size: 1, page: 1, pagelen: 20 }) + ) + const getResult = await bitbucketGetRepositoryTool.transformResponse!( + Response.json(RAW_REPOSITORY) + ) + expect(listResult.output.items[0]).toMatchObject({ + type: 'repository', + uuid: '{repo-1}', + slug: 'demo', + fullName: 'acme/demo', + mainBranch: 'main', + owner: { accountId: 'account-1', displayName: 'Ada Lovelace' }, + project: { key: 'SDK' }, + }) + expect(getResult.output.repository).toEqual(listResult.output.items[0]) + }) + + it('requires a non-empty resource type while preserving future type values', async () => { + await expect( + bitbucketGetRepositoryTool.transformResponse!( + Response.json({ ...RAW_REPOSITORY, type: undefined }) + ) + ).rejects.toThrow(/repository\.type must be a non-empty string/) + await expect( + bitbucketGetRepositoryTool.transformResponse!(Response.json({ ...RAW_REPOSITORY, type: ' ' })) + ).rejects.toThrow(/repository\.type must be a non-empty string/) + + const future = await bitbucketGetRepositoryTool.transformResponse!( + Response.json({ ...RAW_REPOSITORY, type: 'future_repository_variant' }) + ) + expect(future.output.repository.type).toBe('future_repository_variant') + }) +}) + +describe('Bitbucket source tools', () => { + it('builds encoded branch, commit, directory, and file URLs', () => { + expect( + requestUrl(bitbucketListBranchesTool, { + ...REPOSITORY_PARAMS, + q: 'name ~ "feature"', + sort: '-name', + pageLen: 5, + } satisfies BitbucketListBranchesParams) + ).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/refs/branches?q=name+%7E+%22feature%22&sort=-name&pagelen=5' + ) + expect( + requestUrl(bitbucketDeleteBranchTool, { + ...REPOSITORY_PARAMS, + name: 'feature/space ?#', + }) + ).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/refs/branches/feature%2Fspace%20%3F%23' + ) + expect( + requestUrl(bitbucketGetCommitTool, { + ...REPOSITORY_PARAMS, + commit: COMMIT_SHA.toUpperCase(), + } satisfies BitbucketGetCommitParams) + ).toBe(`https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/commit/${COMMIT_SHA}`) + expect( + requestUrl(bitbucketGetFileMetadataTool, { + ...REPOSITORY_PARAMS, + commit: FEATURE_SHA, + path: '/src/my file?#.ts/', + } satisfies BitbucketFileParams) + ).toBe( + `https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/src/${FEATURE_SHA}/src/my%20file%3F%23.ts?format=meta` + ) + expect(() => + requestUrl(bitbucketGetCommitTool, { + ...REPOSITORY_PARAMS, + commit: 'main', + } satisfies BitbucketGetCommitParams) + ).toThrow(/commit must be a full 40-character SHA-1/) + expect(() => + requestUrl(bitbucketListDirectoryTool, { + ...REPOSITORY_PARAMS, + commit: 'abc123', + } satisfies BitbucketListDirectoryParams) + ).toThrow(/commit must be a full 40-character SHA-1/) + expect(() => + requestUrl(bitbucketGetFileMetadataTool, { + ...REPOSITORY_PARAMS, + commit: 'feature/demo', + path: 'README.md', + } satisfies BitbucketFileParams) + ).toThrow(/commit must be a full 40-character SHA-1/) + expect(() => + requestUrl(bitbucketGetFileTool, { + ...REPOSITORY_PARAMS, + commit: true, + path: 'README.md', + } as unknown as BitbucketGetFileParams) + ).toThrow(/commit must be a full 40-character SHA-1/) + }) + + it('keeps directory listing shallow and binds its cursor to the selected path', () => { + const first = new URL( + requestUrl(bitbucketListDirectoryTool, { + ...REPOSITORY_PARAMS, + commit: FEATURE_SHA, + path: 'src/my dir', + q: 'type = "commit_file"', + sort: 'path', + pageLen: 15, + } satisfies BitbucketListDirectoryParams) + ) + expect(first.pathname).toBe( + `/2.0/repositories/acme%20team/sdk%2Fcore/src/${FEATURE_SHA}/src/my%20dir` + ) + expect(Object.fromEntries(first.searchParams)).toEqual({ + q: 'type = "commit_file"', + sort: 'path', + pagelen: '15', + }) + expect(bitbucketListDirectoryTool.params).not.toHaveProperty('maxDepth') + + const next = `https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/src/${FEATURE_SHA}/src/my%20dir?page=2` + expect( + requestUrl(bitbucketListDirectoryTool, { + ...REPOSITORY_PARAMS, + commit: FEATURE_SHA, + path: 'src/my dir', + nextUrl: next, + } satisfies BitbucketListDirectoryParams) + ).toBe(next) + expect(() => + requestUrl(bitbucketListDirectoryTool, { + ...REPOSITORY_PARAMS, + commit: FEATURE_SHA, + path: 'src/my dir', + nextUrl: `https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/src/${FEATURE_SHA}/src/other?page=2`, + } satisfies BitbucketListDirectoryParams) + ).toThrow(/does not preserve/) + }) + + it('builds the documented create-branch body and never retries the mutation', () => { + const params = { + ...REPOSITORY_PARAMS, + name: ' feature/demo ', + target: ' abc123 ', + } satisfies BitbucketCreateBranchParams + expect(requestUrl(bitbucketCreateBranchTool, params)).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/refs/branches' + ) + expect(requestBody(bitbucketCreateBranchTool, params)).toEqual({ + name: 'feature/demo', + target: { hash: 'abc123' }, + }) + expect(bitbucketCreateBranchTool.request.retry).toBeUndefined() + }) + + it('normalizes branch, commit, directory, and metadata responses', async () => { + const branches = await bitbucketListBranchesTool.transformResponse!( + Response.json({ values: [RAW_BRANCH] }) + ) + const created = await bitbucketCreateBranchTool.transformResponse!(Response.json(RAW_BRANCH)) + expect(branches.output.items[0]).toMatchObject({ + type: 'branch', + name: 'feature/demo', + target: { type: 'commit', hash: 'abc123', author: { accountId: 'account-1' } }, + mergeStrategies: ['merge_commit', 'squash'], + }) + expect(created.output.branch).toEqual(branches.output.items[0]) + + const commits = await bitbucketListCommitsTool.transformResponse!( + Response.json({ values: [RAW_COMMIT] }) + ) + const commit = await bitbucketGetCommitTool.transformResponse!(Response.json(RAW_COMMIT)) + expect(commits.output.items[0]).toMatchObject({ + type: 'commit', + hash: 'abc123', + summary: 'Ship it', + authorRaw: 'Ada ', + parents: [{ hash: 'parent123' }], + }) + expect(commit.output.commit).toEqual(commits.output.items[0]) + + const directory = await bitbucketListDirectoryTool.transformResponse!( + Response.json({ + values: [ + { + type: 'commit_file', + path: 'src/index.ts', + commit: { hash: 'abc123' }, + size: 44, + attributes: ['binary'], + links: { + self: { href: 'https://api.bitbucket.org/file' }, + meta: { href: 'https://api.bitbucket.org/file?format=meta' }, + }, + }, + ], + }) + ) + expect(directory.output.items[0]).toEqual({ + type: 'commit_file', + path: 'src/index.ts', + commitHash: 'abc123', + size: 44, + attributes: ['binary'], + isBinary: true, + selfUrl: 'https://api.bitbucket.org/file', + metadataUrl: 'https://api.bitbucket.org/file?format=meta', + }) + + const metadata = await bitbucketGetFileMetadataTool.transformResponse!( + Response.json({ + type: 'commit_file', + path: 'src/index.ts', + commit: { hash: 'abc123' }, + escaped_path: 'src/index.ts', + size: 44, + }) + ) + expect(metadata.output.file).toMatchObject({ attributes: null, isBinary: null, size: 44 }) + }) + + it('distinguishes absent, empty, and malformed optional source collections', async () => { + const commitWithoutParents = await bitbucketGetCommitTool.transformResponse!( + Response.json({ ...RAW_COMMIT, parents: undefined }) + ) + const commitWithNoParents = await bitbucketGetCommitTool.transformResponse!( + Response.json({ ...RAW_COMMIT, parents: [] }) + ) + expect(commitWithoutParents.output.commit.parents).toBeNull() + expect(commitWithNoParents.output.commit.parents).toEqual([]) + await expect( + bitbucketGetCommitTool.transformResponse!(Response.json({ ...RAW_COMMIT, parents: null })) + ).rejects.toThrow(/commit\.parents must be an array when present/) + await expect( + bitbucketGetCommitTool.transformResponse!( + Response.json({ ...RAW_COMMIT, parents: [{ hash: 'missing-type' }] }) + ) + ).rejects.toThrow(/commit\.parents\[0\]\.type must be a non-empty string/) + + const branchWithoutStrategies = await bitbucketCreateBranchTool.transformResponse!( + Response.json({ ...RAW_BRANCH, merge_strategies: undefined }) + ) + const branchWithNoStrategies = await bitbucketCreateBranchTool.transformResponse!( + Response.json({ ...RAW_BRANCH, merge_strategies: [] }) + ) + expect(branchWithoutStrategies.output.branch.mergeStrategies).toBeNull() + expect(branchWithNoStrategies.output.branch.mergeStrategies).toEqual([]) + await expect( + bitbucketCreateBranchTool.transformResponse!( + Response.json({ ...RAW_BRANCH, merge_strategies: ['future_strategy', 7] }) + ) + ).rejects.toThrow(/merge_strategies\[1\] must be a string/) + + const futureStrategy = await bitbucketCreateBranchTool.transformResponse!( + Response.json({ ...RAW_BRANCH, merge_strategies: ['future_strategy'] }) + ) + expect(futureStrategy.output.branch.mergeStrategies).toEqual(['future_strategy']) + }) + + it('normalizes the exact commit_file metadata shape and rejects other source objects', async () => { + const futureAttribute = await bitbucketGetFileMetadataTool.transformResponse!( + Response.json({ + type: 'commit_file', + path: 'src/index.ts', + commit: { hash: 'abc123' }, + escaped_path: 'src/index.ts', + size: null, + attributes: ['future_attribute'], + }) + ) + expect(futureAttribute.output.file).toEqual({ + type: 'commit_file', + path: 'src/index.ts', + commitHash: 'abc123', + escapedPath: 'src/index.ts', + size: null, + attributes: ['future_attribute'], + isBinary: false, + }) + + await expect( + bitbucketGetFileMetadataTool.transformResponse!( + Response.json({ type: 'commit_directory', path: 'src' }) + ) + ).rejects.toThrow(/directory; use list_directory/) + await expect( + bitbucketGetFileMetadataTool.transformResponse!( + Response.json({ type: 'commit_file', path: 'src/index.ts', attributes: 'binary' }) + ) + ).rejects.toThrow(/metadata\.attributes must be an array when present/) + await expect( + bitbucketGetFileMetadataTool.transformResponse!( + Response.json({ type: 'commit_file', path: 'src/index.ts', attributes: ['binary', 7] }) + ) + ).rejects.toThrow(/metadata\.attributes\[1\] must be a string/) + await expect( + bitbucketGetFileMetadataTool.transformResponse!( + Response.json({ type: 'commit_file', path: 'src/index.ts', size: '44' }) + ) + ).rejects.toThrow(/metadata\.size must be a finite number or null/) + }) + + it('preserves future directory-entry types and attributes without enum gating', async () => { + const result = await bitbucketListDirectoryTool.transformResponse!( + Response.json({ + values: [ + { + type: 'future_source_entry', + path: 'src/new-kind', + attributes: ['future_attribute'], + }, + { type: 'commit_file', path: 'empty.txt', attributes: [] }, + { type: 'commit_directory', path: 'src' }, + ], + }) + ) + expect(result.output.items[0]).toMatchObject({ + type: 'future_source_entry', + attributes: ['future_attribute'], + isBinary: false, + }) + expect(result.output.items[1]).toMatchObject({ attributes: [], isBinary: false }) + expect(result.output.items[2]).toMatchObject({ attributes: null, isBinary: null }) + await expect( + bitbucketListDirectoryTool.transformResponse!( + Response.json({ + values: [{ type: 'commit_file', path: 'bad.txt', attributes: ['executable', 7] }], + }) + ) + ).rejects.toThrow(/directory entry\.attributes\[1\] must be a string/) + }) + + it('treats successful destructive 204 responses as completion', async () => { + const result = await bitbucketDeleteBranchTool.transformResponse!( + new Response(null, { status: 204 }) + ) + expect(result).toEqual({ success: true, output: { deleted: true } }) + }) + + it('preflights file metadata and returns no raw bytes for a documented binary file', async () => { + serverMocks.secureBitbucketRead.mockResolvedValueOnce( + Response.json({ + type: 'commit_file', + path: 'assets/logo.png', + commit: { hash: 'abc123' }, + size: 1_024, + attributes: ['binary'], + }) + ) + const params = { + ...REPOSITORY_PARAMS, + commit: FEATURE_SHA, + path: 'assets/logo.png', + } satisfies BitbucketGetFileParams + + const result = await bitbucketGetFileTool.directExecution!(params) + + expect(result).toEqual({ + success: true, + output: { + content: null, + binary: true, + truncated: true, + returnedBytes: 0, + fullBytes: 1_024, + contentType: null, + }, + }) + expect(serverMocks.secureBitbucketRead).toHaveBeenCalledTimes(1) + expect(serverMocks.secureBitbucketRead).toHaveBeenCalledWith( + `https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/src/${FEATURE_SHA}/assets/logo.png?format=meta`, + expect.objectContaining({ Authorization: 'Bearer oauth-token' }), + 256 * 1024, + { signal: undefined } + ) + }) + + it.each([ + { size: 0, truncated: false, fullBytes: 0 }, + { size: undefined, truncated: null, fullBytes: null }, + ])( + 'reports binary size $size as truncated=$truncated', + async ({ size, truncated, fullBytes }) => { + serverMocks.secureBitbucketRead.mockResolvedValueOnce( + Response.json({ + type: 'commit_file', + path: 'assets/logo.png', + commit: { hash: 'abc123' }, + ...(size !== undefined ? { size } : {}), + attributes: ['binary'], + }) + ) + + const result = await bitbucketGetFileTool.directExecution!({ + ...REPOSITORY_PARAMS, + commit: COMMIT_SHA, + path: 'assets/logo.png', + }) + + expect(result.output).toEqual({ + content: null, + binary: true, + truncated, + returnedBytes: 0, + fullBytes, + contentType: null, + }) + expect(serverMocks.secureBitbucketRead).toHaveBeenCalledTimes(1) + } + ) + + it('rejects directory metadata without making a raw-content request', async () => { + serverMocks.secureBitbucketRead.mockResolvedValueOnce( + Response.json({ type: 'commit_directory', path: 'src' }) + ) + + await expect( + bitbucketGetFileTool.directExecution!({ + ...REPOSITORY_PARAMS, + commit: COMMIT_SHA, + path: 'src', + }) + ).rejects.toThrow(/directory; use list_directory/) + expect(serverMocks.secureBitbucketRead).toHaveBeenCalledTimes(1) + }) + + it('validates maxCharacters before the metadata preflight', async () => { + await expect( + bitbucketGetFileTool.directExecution!({ + ...REPOSITORY_PARAMS, + commit: COMMIT_SHA, + path: 'README.md', + maxCharacters: 0, + }) + ).rejects.toThrow(/maxCharacters must be an integer between 1 and 500000/) + expect(serverMocks.secureBitbucketRead).not.toHaveBeenCalled() + await expect( + bitbucketGetFileTool.directExecution!({ + ...REPOSITORY_PARAMS, + commit: 'main', + path: 'README.md', + }) + ).rejects.toThrow(/commit must be a full 40-character SHA-1/) + expect(serverMocks.secureBitbucketRead).not.toHaveBeenCalled() + expect(bitbucketGetFileTool.outputs?.truncated).toMatchObject({ + type: 'boolean', + nullable: true, + }) + }) + + it('reads a bounded raw file when metadata says text or remains unknown', async () => { + serverMocks.secureBitbucketRead + .mockResolvedValueOnce( + Response.json({ + type: 'commit_file', + path: 'src/my file.ts', + commit: { hash: 'abc123' }, + size: 5, + }) + ) + .mockResolvedValueOnce( + new Response('hello', { + headers: { 'Content-Length': '5', 'Content-Type': 'text/plain' }, + }) + ) + const params = { + ...REPOSITORY_PARAMS, + commit: COMMIT_SHA, + path: 'src/my file.ts', + maxCharacters: 4, + } satisfies BitbucketGetFileParams + + const result = await bitbucketGetFileTool.directExecution!(params) + + expect(result).toEqual({ + success: true, + output: { + content: 'hell', + binary: null, + truncated: true, + returnedBytes: 5, + fullBytes: 5, + contentType: 'text/plain', + }, + }) + expect(serverMocks.secureBitbucketRead).toHaveBeenNthCalledWith( + 2, + `https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/src/${COMMIT_SHA}/src/my%20file.ts`, + expect.objectContaining({ + Accept: '*/*', + Authorization: 'Bearer oauth-token', + Range: 'bytes=0-15', + }), + 10 * 1024 * 1024, + { stripAuthOnRedirect: true, signal: undefined } + ) + }) + + it('uses metadata size when raw bytes reveal binary content with an unknown range total', async () => { + serverMocks.secureBitbucketRead + .mockResolvedValueOnce( + Response.json({ + type: 'commit_file', + path: 'unknown.bin', + commit: { hash: COMMIT_SHA }, + size: 3, + }) + ) + .mockResolvedValueOnce( + new Response(new Uint8Array([65, 0, 66]), { + status: 206, + headers: { 'Content-Range': 'bytes 0-2/*' }, + }) + ) + + const result = await bitbucketGetFileTool.directExecution!({ + ...REPOSITORY_PARAMS, + commit: COMMIT_SHA, + path: 'unknown.bin', + }) + + expect(result.output).toMatchObject({ + content: null, + binary: true, + truncated: true, + returnedBytes: 3, + fullBytes: 3, + }) + }) + + it('uses the normal HTTP path only as a guarded executor fallback', async () => { + expect(bitbucketGetFileTool.request.stripAuthOnRedirect).toBe(true) + await expect( + bitbucketGetFileTool.transformResponse!(new Response('content'), { + ...REPOSITORY_PARAMS, + commit: COMMIT_SHA, + path: 'README.md', + }) + ).rejects.toThrow(/metadata preflight direct execution path/) + }) + + it('builds the list-commits endpoint with its opaque cursor bound', () => { + const first = requestUrl(bitbucketListCommitsTool, { + ...REPOSITORY_PARAMS, + pageLen: 30, + } satisfies BitbucketListCommitsParams) + expect(first).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/commits?pagelen=30' + ) + expect(() => + requestUrl(bitbucketListCommitsTool, { + ...REPOSITORY_PARAMS, + nextUrl: + 'https://api.bitbucket.org/2.0/repositories/acme%20team/sdk%2Fcore/pipelines?page=2', + } satisfies BitbucketListCommitsParams) + ).toThrow(/does not belong/) + }) + + it('sets JSON content headers only on JSON mutations', () => { + const createHeaders = bitbucketCreateBranchTool.request.headers({ + ...REPOSITORY_PARAMS, + name: 'feature', + target: 'main', + }) + const getHeaders = bitbucketGetRepositoryTool.request.headers( + REPOSITORY_PARAMS satisfies BitbucketRepositoryParams + ) + expect(createHeaders).toMatchObject({ + Accept: 'application/json', + Authorization: 'Bearer oauth-token', + 'Content-Type': 'application/json', + }) + expect(getHeaders).not.toHaveProperty('Content-Type') + }) +}) diff --git a/apps/sim/tools/bitbucket/request_pull_request_changes.ts b/apps/sim/tools/bitbucket/request_pull_request_changes.ts new file mode 100644 index 00000000000..08e30025bc2 --- /dev/null +++ b/apps/sim/tools/bitbucket/request_pull_request_changes.ts @@ -0,0 +1,46 @@ +import { + BITBUCKET_PARTICIPANT_OUTPUT_PROPERTIES, + type BitbucketParticipant, + type BitbucketPullRequestParams, + type BitbucketToolResponse, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_PULL_REQUEST_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketPullRequestPath, + normalizeBitbucketParticipant, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketRequestPullRequestChangesTool: ToolConfig< + BitbucketPullRequestParams, + BitbucketToolResponse<{ participant: BitbucketParticipant }> +> = { + id: 'bitbucket_request_pull_request_changes', + name: 'Bitbucket Request Pull Request Changes', + description: 'Request changes on a pull request as the authenticated account', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pullrequest:write'] }, + params: { ...BITBUCKET_PULL_REQUEST_PARAMS }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketPullRequestPath(params.workspaceSlug, params.repoSlug, params.prId)}/request-changes`, + method: 'POST', + headers: (params) => bitbucketHeaders(params.accessToken), + }, + transformResponse: async (response) => ({ + success: true, + output: { participant: normalizeBitbucketParticipant(await bitbucketJson(response)) }, + }), + outputs: { + participant: { + type: 'object', + description: 'Change-request participant record', + properties: BITBUCKET_PARTICIPANT_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/stop_pipeline.ts b/apps/sim/tools/bitbucket/stop_pipeline.ts new file mode 100644 index 00000000000..8486df500a8 --- /dev/null +++ b/apps/sim/tools/bitbucket/stop_pipeline.ts @@ -0,0 +1,39 @@ +import type { BitbucketPipelineParams, BitbucketToolResponse } from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketRepositoryPath, + encodeBitbucketSegment, +} from '@/tools/bitbucket/utils' +import type { ToolConfig } from '@/tools/types' + +export const bitbucketStopPipelineTool: ToolConfig< + BitbucketPipelineParams, + BitbucketToolResponse<{ stopped: boolean }> +> = { + id: 'bitbucket_stop_pipeline', + name: 'Bitbucket Stop Pipeline', + description: 'Stop a running pipeline', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pipeline:write'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + pipelineUuid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Pipeline UUID', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/pipelines/${encodeBitbucketSegment(params.pipelineUuid, 'pipelineUuid')}/stopPipeline`, + method: 'POST', + headers: (params) => bitbucketHeaders(params.accessToken), + }, + transformResponse: async () => ({ success: true, output: { stopped: true } }), + outputs: { stopped: { type: 'boolean', description: 'Whether the stop request succeeded' } }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/trigger_pipeline.ts b/apps/sim/tools/bitbucket/trigger_pipeline.ts new file mode 100644 index 00000000000..c88be2423fc --- /dev/null +++ b/apps/sim/tools/bitbucket/trigger_pipeline.ts @@ -0,0 +1,81 @@ +import { + BITBUCKET_PIPELINE_OUTPUT_PROPERTIES, + type BitbucketPipeline, + type BitbucketToolResponse, + type BitbucketTriggerPipelineParams, +} from '@/tools/bitbucket/types' +import { + BITBUCKET_API_BASE, + BITBUCKET_ERROR_EXTRACTOR, + BITBUCKET_REPOSITORY_PARAMS, + bitbucketHeaders, + bitbucketJson, + bitbucketRepositoryPath, + normalizeBitbucketPipeline, + requireBitbucketString, +} from '@/tools/bitbucket/utils' +import { optionalBitbucketSha1, requireBitbucketEnum } from '@/tools/bitbucket/validation' +import type { ToolConfig } from '@/tools/types' + +const BITBUCKET_PIPELINE_REF_TYPES = ['branch', 'tag', 'named_branch', 'bookmark'] as const + +export const bitbucketTriggerPipelineTool: ToolConfig< + BitbucketTriggerPipelineParams, + BitbucketToolResponse<{ pipeline: BitbucketPipeline }> +> = { + id: 'bitbucket_trigger_pipeline', + name: 'Bitbucket Trigger Pipeline', + description: 'Run the repository pipeline selected by a branch or ref target', + version: '1.0.0', + oauth: { required: true, provider: 'bitbucket', requiredScopes: ['pipeline'] }, + params: { + ...BITBUCKET_REPOSITORY_PARAMS, + refType: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Reference type: branch, tag, named_branch, or bookmark', + }, + refName: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Reference name', + }, + commitHash: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Full 40-character commit SHA-1 to run in the reference context', + }, + }, + request: { + url: (params) => + `${BITBUCKET_API_BASE}${bitbucketRepositoryPath(params.workspaceSlug, params.repoSlug)}/pipelines`, + method: 'POST', + headers: (params) => bitbucketHeaders(params.accessToken, { json: true }), + body: (params) => { + const commitHash = optionalBitbucketSha1(params.commitHash, 'commitHash') + return { + target: { + type: 'pipeline_ref_target', + ref_type: requireBitbucketEnum(params.refType, 'refType', BITBUCKET_PIPELINE_REF_TYPES), + ref_name: requireBitbucketString(params.refName, 'refName'), + ...(commitHash !== undefined ? { commit: { type: 'commit', hash: commitHash } } : {}), + }, + } + }, + }, + transformResponse: async (response) => ({ + success: true, + output: { pipeline: normalizeBitbucketPipeline(await bitbucketJson(response)) }, + }), + outputs: { + pipeline: { + type: 'object', + description: 'Triggered pipeline', + properties: BITBUCKET_PIPELINE_OUTPUT_PROPERTIES, + }, + }, + errorExtractor: BITBUCKET_ERROR_EXTRACTOR, +} diff --git a/apps/sim/tools/bitbucket/types.ts b/apps/sim/tools/bitbucket/types.ts new file mode 100644 index 00000000000..df0d4b81ebd --- /dev/null +++ b/apps/sim/tools/bitbucket/types.ts @@ -0,0 +1,864 @@ +import type { ToolOutputProperty } from '@/tools/types' + +export interface BitbucketAuthParams { + accessToken: string +} + +export interface BitbucketPaginationParams { + nextUrl?: string + pageLen?: number +} + +export interface BitbucketRepositoryParams extends BitbucketAuthParams { + workspaceSlug: string + repoSlug: string +} + +export interface BitbucketPullRequestParams extends BitbucketRepositoryParams { + prId: number +} + +export interface BitbucketListWorkspacesParams + extends BitbucketAuthParams, + BitbucketPaginationParams { + sort?: string + administrator?: boolean +} + +export interface BitbucketListRepositoriesParams + extends BitbucketAuthParams, + BitbucketPaginationParams { + workspaceSlug: string + role?: 'admin' | 'contributor' | 'member' | 'owner' + q?: string + sort?: string +} + +export interface BitbucketListBranchesParams + extends BitbucketRepositoryParams, + BitbucketPaginationParams { + q?: string + sort?: string +} + +export interface BitbucketCreateBranchParams extends BitbucketRepositoryParams { + name: string + target: string +} + +export interface BitbucketDeleteBranchParams extends BitbucketRepositoryParams { + name: string +} + +export interface BitbucketListCommitsParams + extends BitbucketRepositoryParams, + BitbucketPaginationParams {} + +export interface BitbucketGetCommitParams extends BitbucketRepositoryParams { + commit: string +} + +export interface BitbucketListDirectoryParams + extends BitbucketRepositoryParams, + BitbucketPaginationParams { + commit: string + path?: string + q?: string + sort?: string +} + +export interface BitbucketFileParams extends BitbucketRepositoryParams { + commit: string + path: string +} + +export interface BitbucketGetFileParams extends BitbucketFileParams { + maxCharacters?: number +} + +export interface BitbucketListPullRequestsParams + extends BitbucketRepositoryParams, + BitbucketPaginationParams { + state?: 'OPEN' | 'MERGED' | 'DECLINED' | 'SUPERSEDED' + q?: string + sort?: string +} + +export interface BitbucketCreatePullRequestParams extends BitbucketRepositoryParams { + title: string + sourceBranch: string + destinationBranch: string + description?: string + closeSourceBranch?: boolean + draft?: boolean + reviewerUuids?: string[] +} + +export type BitbucketMergeStrategy = + | 'merge_commit' + | 'squash' + | 'fast_forward' + | 'squash_fast_forward' + | 'rebase_fast_forward' + | 'rebase_merge' + +export interface BitbucketMergePullRequestParams extends BitbucketPullRequestParams { + mergeStrategy?: BitbucketMergeStrategy + message?: string + closeSourceBranch?: boolean +} + +export interface BitbucketGetMergeTaskStatusParams extends BitbucketPullRequestParams { + taskId: string +} + +export interface BitbucketGetPullRequestDiffParams extends BitbucketPullRequestParams { + path: string + maxCharacters?: number +} + +export interface BitbucketPaginatedPullRequestParams + extends BitbucketPullRequestParams, + BitbucketPaginationParams {} + +export interface BitbucketListPullRequestCommentsParams + extends BitbucketPaginatedPullRequestParams { + q?: string + sort?: string +} + +export interface BitbucketCreatePullRequestCommentParams extends BitbucketPullRequestParams { + content: string + parentId?: number +} + +export interface BitbucketListPullRequestCommitStatusesParams + extends BitbucketPaginatedPullRequestParams { + q?: string + sort?: string +} + +export type BitbucketPipelineListRefType = 'BRANCH' | 'TAG' | 'ANNOTATED_TAG' +export type BitbucketPipelineListSelectorType = + | 'BRANCH' + | 'TAG' + | 'CUSTOM' + | 'PULLREQUESTS' + | 'DEFAULT' +export type BitbucketPipelineTriggerType = 'PUSH' | 'MANUAL' | 'SCHEDULED' | 'PARENT_STEP' +export type BitbucketPipelineStatus = + | 'PARSING' + | 'PENDING' + | 'PAUSED' + | 'HALTED' + | 'BUILDING' + | 'ERROR' + | 'PASSED' + | 'FAILED' + | 'STOPPED' + | 'UNKNOWN' + +export interface BitbucketListPipelinesParams + extends BitbucketRepositoryParams, + BitbucketPaginationParams { + refType?: BitbucketPipelineListRefType + refName?: string + commitHash?: string + selectorType?: BitbucketPipelineListSelectorType + selectorPattern?: string + triggerType?: BitbucketPipelineTriggerType + status?: BitbucketPipelineStatus + sort?: 'creator.uuid' | 'created_on' | 'run_creation_date' +} + +export interface BitbucketPipelineParams extends BitbucketRepositoryParams { + pipelineUuid: string +} + +export interface BitbucketTriggerPipelineParams extends BitbucketRepositoryParams { + refType: 'branch' | 'tag' | 'named_branch' | 'bookmark' + refName: string + commitHash?: string +} + +export interface BitbucketListPipelineStepsParams + extends BitbucketPipelineParams, + BitbucketPaginationParams {} + +export interface BitbucketGetPipelineStepLogParams extends BitbucketPipelineParams { + stepUuid: string + maxCharacters?: number +} + +export interface BitbucketPage { + size: number | null + page: number | null + pageLen: number | null + nextUrl: string | null + previousUrl: string | null +} + +export interface BitbucketUser { + type: string + uuid: string | null + accountId: string | null + displayName: string | null + createdOn: string | null + selfUrl: string | null + htmlUrl: string | null + avatarUrl: string | null +} + +export interface BitbucketWorkspaceAccess { + type: string + slug: string | null + uuid: string | null + administrator: boolean | null + selfUrl: string | null + avatarUrl: string | null +} + +export interface BitbucketRepository { + type: string + uuid: string | null + slug: string | null + name: string | null + fullName: string | null + description: string | null + isPrivate: boolean | null + scm: string | null + language: string | null + size: number | null + createdOn: string | null + updatedOn: string | null + mainBranch: string | null + owner: BitbucketUser | null + project: { + uuid: string | null + key: string | null + name: string | null + } | null + selfUrl: string | null + htmlUrl: string | null +} + +export interface BitbucketCommit { + type: string + hash: string | null + date: string | null + message: string | null + summary: string | null + authorRaw: string | null + author: BitbucketUser | null + committerRaw: string | null + committer: BitbucketUser | null + parents: Array<{ hash: string | null }> | null + selfUrl: string | null + htmlUrl: string | null +} + +export interface BitbucketBranch { + type: string + name: string | null + target: BitbucketCommit | null + mergeStrategies: string[] | null + defaultMergeStrategy: string | null + selfUrl: string | null + htmlUrl: string | null +} + +export interface BitbucketDirectoryEntry { + type: string + path: string | null + commitHash: string | null + size: number | null + attributes: string[] | null + isBinary: boolean | null + selfUrl: string | null + metadataUrl: string | null +} + +export interface BitbucketFileMetadata { + type: 'commit_file' + path: string | null + commitHash: string | null + escapedPath: string | null + size: number | null + attributes: string[] | null + isBinary: boolean | null +} + +export interface BitbucketPullRequestEndpoint { + branchName: string | null + commitHash: string | null + repositoryUuid: string | null + repositoryFullName: string | null +} + +export interface BitbucketParticipant { + type: string + user: BitbucketUser | null + role: string | null + approved: boolean | null + state: string | null + participatedOn: string | null +} + +export interface BitbucketPullRequest { + type: string + id: number | null + title: string | null + description: string | null + state: string | null + draft: boolean | null + queued: boolean | null + author: BitbucketUser | null + closedBy: BitbucketUser | null + source: BitbucketPullRequestEndpoint | null + destination: BitbucketPullRequestEndpoint | null + mergeCommitHash: string | null + commentCount: number | null + taskCount: number | null + closeSourceBranch: boolean | null + reason: string | null + createdOn: string | null + updatedOn: string | null + reviewers: BitbucketUser[] | null + participants: BitbucketParticipant[] | null + selfUrl: string | null + htmlUrl: string | null +} + +export interface BitbucketComment { + type: string + id: number | null + createdOn: string | null + updatedOn: string | null + content: string | null + user: BitbucketUser | null + deleted: boolean | null + parentId: number | null + inline: { + path: string | null + from: number | null + to: number | null + startFrom: number | null + startTo: number | null + } | null + pending: boolean | null + resolution: { + resolver: BitbucketUser | null + resolvedOn: string | null + } | null + selfUrl: string | null + htmlUrl: string | null +} + +export interface BitbucketCommitStatus { + type: string + key: string + refName: string | null + url: string | null + state: string + name: string | null + description: string | null + createdOn: string | null + updatedOn: string | null + selfUrl: string | null + commitUrl: string | null +} + +export interface BitbucketDiffstat { + type: string + status: string | null + linesAdded: number | null + linesRemoved: number | null + oldPath: string | null + newPath: string | null + oldCommitHash: string | null + newCommitHash: string | null +} + +export interface BitbucketPipeline { + type: string + uuid: string | null + buildNumber: number | null + creator: BitbucketUser | null + repositoryFullName: string | null + target: { + type: string | null + refType: string | null + refName: string | null + commitHash: string | null + selectorType: string | null + selectorPattern: string | null + } | null + triggerType: string | null + state: { + name: string | null + stage: string | null + result: string | null + errorKey: string | null + errorMessage: string | null + } | null + createdOn: string | null + completedOn: string | null + buildSecondsUsed: number | null + selfUrl: string | null + stepsUrl: string | null +} + +export interface BitbucketPipelineStep { + type: string + uuid: string | null + startedOn: string | null + completedOn: string | null + state: { + name: string | null + result: string | null + errorKey: string | null + errorMessage: string | null + } | null + imageName: string | null + setupCommands: Array<{ name: string | null; command: string | null }> | null + scriptCommands: Array<{ name: string | null; command: string | null }> | null +} + +export interface BitbucketListOutput { + items: T[] + page: BitbucketPage +} + +export interface BitbucketToolResponse { + success: true + output: T +} + +export const BITBUCKET_USER_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket account object type' }, + uuid: { type: 'string', description: 'Bitbucket account UUID', nullable: true }, + accountId: { type: 'string', description: 'Atlassian account ID', nullable: true }, + displayName: { type: 'string', description: 'Account display name', nullable: true }, + createdOn: { type: 'string', description: 'Account creation timestamp', nullable: true }, + selfUrl: { type: 'string', description: 'Account API URL', nullable: true }, + htmlUrl: { type: 'string', description: 'Account web URL', nullable: true }, + avatarUrl: { type: 'string', description: 'Account avatar URL', nullable: true }, +} + +export const BITBUCKET_WORKSPACE_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket workspace-access object type' }, + slug: { type: 'string', description: 'Workspace slug', nullable: true }, + uuid: { type: 'string', description: 'Workspace UUID', nullable: true }, + administrator: { + type: 'boolean', + description: 'Whether the caller administers the workspace', + nullable: true, + }, + selfUrl: { type: 'string', description: 'Workspace API URL', nullable: true }, + avatarUrl: { type: 'string', description: 'Workspace avatar URL', nullable: true }, +} + +export const BITBUCKET_REPOSITORY_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket repository object type' }, + uuid: { type: 'string', description: 'Repository UUID', nullable: true }, + slug: { type: 'string', description: 'Repository slug', nullable: true }, + name: { type: 'string', description: 'Repository name', nullable: true }, + fullName: { type: 'string', description: 'Workspace and repository full name', nullable: true }, + description: { type: 'string', description: 'Repository description', nullable: true }, + isPrivate: { type: 'boolean', description: 'Whether the repository is private', nullable: true }, + scm: { type: 'string', description: 'Source control system', nullable: true }, + language: { type: 'string', description: 'Primary repository language', nullable: true }, + size: { type: 'number', description: 'Repository size in bytes', nullable: true }, + createdOn: { type: 'string', description: 'Repository creation timestamp', nullable: true }, + updatedOn: { type: 'string', description: 'Repository update timestamp', nullable: true }, + mainBranch: { type: 'string', description: 'Main branch name', nullable: true }, + owner: { + type: 'object', + description: 'Repository owner', + nullable: true, + properties: BITBUCKET_USER_OUTPUT_PROPERTIES, + }, + project: { + type: 'object', + description: 'Containing Bitbucket project', + nullable: true, + properties: { + uuid: { type: 'string', description: 'Project UUID', nullable: true }, + key: { type: 'string', description: 'Project key', nullable: true }, + name: { type: 'string', description: 'Project name', nullable: true }, + }, + }, + selfUrl: { type: 'string', description: 'Repository API URL', nullable: true }, + htmlUrl: { type: 'string', description: 'Repository web URL', nullable: true }, +} + +export const BITBUCKET_COMMIT_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket commit object type' }, + hash: { type: 'string', description: 'Commit hash', nullable: true }, + date: { type: 'string', description: 'Commit timestamp', nullable: true }, + message: { type: 'string', description: 'Full commit message', nullable: true }, + summary: { type: 'string', description: 'Raw commit summary', nullable: true }, + authorRaw: { type: 'string', description: 'Raw author value stored by Git', nullable: true }, + author: { + type: 'object', + description: 'Matched Bitbucket account, when available', + nullable: true, + properties: BITBUCKET_USER_OUTPUT_PROPERTIES, + }, + committerRaw: { + type: 'string', + description: 'Raw committer value stored by Git', + nullable: true, + }, + committer: { + type: 'object', + description: 'Matched Bitbucket committer account, when available', + nullable: true, + properties: BITBUCKET_USER_OUTPUT_PROPERTIES, + }, + parents: { + type: 'array', + description: 'Parent commits', + nullable: true, + items: { + type: 'object', + properties: { + hash: { type: 'string', description: 'Parent commit hash', nullable: true }, + }, + }, + }, + selfUrl: { type: 'string', description: 'Commit API URL', nullable: true }, + htmlUrl: { type: 'string', description: 'Commit web URL', nullable: true }, +} + +export const BITBUCKET_BRANCH_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket branch object type' }, + name: { type: 'string', description: 'Branch name', nullable: true }, + target: { + type: 'object', + description: 'Commit targeted by the branch', + nullable: true, + properties: BITBUCKET_COMMIT_OUTPUT_PROPERTIES, + }, + mergeStrategies: { + type: 'array', + description: 'Merge strategies available for the branch', + nullable: true, + items: { type: 'string' }, + }, + defaultMergeStrategy: { type: 'string', description: 'Default merge strategy', nullable: true }, + selfUrl: { type: 'string', description: 'Branch API URL', nullable: true }, + htmlUrl: { type: 'string', description: 'Branch web URL', nullable: true }, +} + +export const BITBUCKET_DIRECTORY_ENTRY_OUTPUT_PROPERTIES: Record = { + type: { + type: 'string', + description: 'Entry type, such as commit_file or commit_directory', + }, + path: { type: 'string', description: 'Repository-relative path', nullable: true }, + commitHash: { type: 'string', description: 'Resolved commit hash', nullable: true }, + size: { + type: 'number', + description: 'File size in bytes when the entry is a file', + nullable: true, + }, + attributes: { + type: 'array', + description: 'File attributes when the entry is a file', + nullable: true, + items: { type: 'string' }, + }, + isBinary: { + type: 'boolean', + description: 'Whether file attributes include the binary marker', + nullable: true, + }, + selfUrl: { type: 'string', description: 'Source API URL', nullable: true }, + metadataUrl: { type: 'string', description: 'Source metadata API URL', nullable: true }, +} + +export const BITBUCKET_FILE_METADATA_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Entry type (commit_file)' }, + path: { type: 'string', description: 'Repository-relative path', nullable: true }, + commitHash: { type: 'string', description: 'Resolved commit hash', nullable: true }, + escapedPath: { type: 'string', description: 'Escaped display path', nullable: true }, + size: { type: 'number', description: 'File size in bytes', nullable: true }, + attributes: { + type: 'array', + description: 'File attributes reported by Bitbucket', + nullable: true, + items: { type: 'string' }, + }, + isBinary: { + type: 'boolean', + description: 'Whether the documented attributes include the binary marker', + nullable: true, + }, +} + +export const BITBUCKET_PARTICIPANT_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket participant object type' }, + user: { + type: 'object', + description: 'Participating account', + nullable: true, + properties: BITBUCKET_USER_OUTPUT_PROPERTIES, + }, + role: { type: 'string', description: 'Participant role', nullable: true }, + approved: { type: 'boolean', description: 'Whether the participant approved', nullable: true }, + state: { type: 'string', description: 'Review state', nullable: true }, + participatedOn: { + type: 'string', + description: 'Timestamp of the participant action', + nullable: true, + }, +} + +export const BITBUCKET_PR_ENDPOINT_OUTPUT_PROPERTIES: Record = { + branchName: { type: 'string', description: 'Branch name', nullable: true }, + commitHash: { type: 'string', description: 'Commit hash', nullable: true }, + repositoryUuid: { type: 'string', description: 'Repository UUID', nullable: true }, + repositoryFullName: { type: 'string', description: 'Repository full name', nullable: true }, +} + +export const BITBUCKET_PULL_REQUEST_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket pull request object type' }, + id: { type: 'number', description: 'Repository-scoped pull request ID', nullable: true }, + title: { type: 'string', description: 'Pull request title', nullable: true }, + description: { type: 'string', description: 'Pull request description', nullable: true }, + state: { type: 'string', description: 'Pull request state', nullable: true }, + draft: { type: 'boolean', description: 'Whether the pull request is a draft', nullable: true }, + queued: { type: 'boolean', description: 'Whether the pull request is queued', nullable: true }, + author: { + type: 'object', + description: 'Pull request author', + nullable: true, + properties: BITBUCKET_USER_OUTPUT_PROPERTIES, + }, + closedBy: { + type: 'object', + description: 'Account that closed the pull request', + nullable: true, + properties: BITBUCKET_USER_OUTPUT_PROPERTIES, + }, + source: { + type: 'object', + description: 'Source endpoint', + nullable: true, + properties: BITBUCKET_PR_ENDPOINT_OUTPUT_PROPERTIES, + }, + destination: { + type: 'object', + description: 'Destination endpoint', + nullable: true, + properties: BITBUCKET_PR_ENDPOINT_OUTPUT_PROPERTIES, + }, + mergeCommitHash: { type: 'string', description: 'Merge commit hash', nullable: true }, + commentCount: { type: 'number', description: 'Comment count', nullable: true }, + taskCount: { type: 'number', description: 'Open task count', nullable: true }, + closeSourceBranch: { + type: 'boolean', + description: 'Whether merging closes the source branch', + nullable: true, + }, + reason: { type: 'string', description: 'Reason the pull request was declined', nullable: true }, + createdOn: { type: 'string', description: 'Creation timestamp', nullable: true }, + updatedOn: { type: 'string', description: 'Update timestamp', nullable: true }, + reviewers: { + type: 'array', + description: 'Explicit reviewers', + nullable: true, + items: { type: 'object', properties: BITBUCKET_USER_OUTPUT_PROPERTIES }, + }, + participants: { + type: 'array', + description: 'Pull request participants', + nullable: true, + items: { type: 'object', properties: BITBUCKET_PARTICIPANT_OUTPUT_PROPERTIES }, + }, + selfUrl: { type: 'string', description: 'Pull request API URL', nullable: true }, + htmlUrl: { type: 'string', description: 'Pull request web URL', nullable: true }, +} + +export const BITBUCKET_COMMENT_RESOLUTION_OUTPUT_PROPERTIES: Record = { + resolver: { + type: 'object', + description: 'Account that resolved the comment', + nullable: true, + properties: BITBUCKET_USER_OUTPUT_PROPERTIES, + }, + resolvedOn: { type: 'string', description: 'Resolution timestamp', nullable: true }, +} + +export const BITBUCKET_COMMENT_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket comment object type' }, + id: { type: 'number', description: 'Comment ID', nullable: true }, + createdOn: { type: 'string', description: 'Creation timestamp', nullable: true }, + updatedOn: { type: 'string', description: 'Update timestamp', nullable: true }, + content: { type: 'string', description: 'Raw comment content', nullable: true }, + user: { + type: 'object', + description: 'Comment author', + nullable: true, + properties: BITBUCKET_USER_OUTPUT_PROPERTIES, + }, + deleted: { type: 'boolean', description: 'Whether the comment was deleted', nullable: true }, + parentId: { type: 'number', description: 'Parent comment ID', nullable: true }, + inline: { + type: 'object', + description: 'Inline comment anchor', + nullable: true, + properties: { + path: { type: 'string', description: 'Anchored file path', nullable: true }, + from: { type: 'number', description: 'Ending line in the old file', nullable: true }, + to: { type: 'number', description: 'Ending line in the new file', nullable: true }, + startFrom: { type: 'number', description: 'Starting line in the old file', nullable: true }, + startTo: { type: 'number', description: 'Starting line in the new file', nullable: true }, + }, + }, + pending: { type: 'boolean', description: 'Whether the comment is pending', nullable: true }, + resolution: { + type: 'object', + description: 'Comment resolution details', + nullable: true, + properties: BITBUCKET_COMMENT_RESOLUTION_OUTPUT_PROPERTIES, + }, + selfUrl: { type: 'string', description: 'Comment API URL', nullable: true }, + htmlUrl: { type: 'string', description: 'Comment web URL', nullable: true }, +} + +export const BITBUCKET_COMMIT_STATUS_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket commit-status object type' }, + key: { type: 'string', description: 'Vendor-unique status key' }, + refName: { + type: 'string', + description: 'Reference name at status creation time', + nullable: true, + }, + url: { type: 'string', description: 'External build URL', nullable: true }, + state: { type: 'string', description: 'Commit status state' }, + name: { type: 'string', description: 'Build name', nullable: true }, + description: { type: 'string', description: 'Build description', nullable: true }, + createdOn: { type: 'string', description: 'Creation timestamp', nullable: true }, + updatedOn: { type: 'string', description: 'Update timestamp', nullable: true }, + selfUrl: { type: 'string', description: 'Status API URL', nullable: true }, + commitUrl: { type: 'string', description: 'Commit API URL', nullable: true }, +} + +export const BITBUCKET_DIFFSTAT_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Diffstat object type' }, + status: { type: 'string', description: 'File change status', nullable: true }, + linesAdded: { type: 'number', description: 'Lines added', nullable: true }, + linesRemoved: { type: 'number', description: 'Lines removed', nullable: true }, + oldPath: { type: 'string', description: 'Old file path', nullable: true }, + newPath: { type: 'string', description: 'New file path', nullable: true }, + oldCommitHash: { type: 'string', description: 'Old file commit hash', nullable: true }, + newCommitHash: { type: 'string', description: 'New file commit hash', nullable: true }, +} + +export const BITBUCKET_PIPELINE_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket pipeline object type' }, + uuid: { type: 'string', description: 'Pipeline UUID', nullable: true }, + buildNumber: { type: 'number', description: 'Pipeline build number', nullable: true }, + creator: { + type: 'object', + description: 'Pipeline creator', + nullable: true, + properties: BITBUCKET_USER_OUTPUT_PROPERTIES, + }, + repositoryFullName: { type: 'string', description: 'Repository full name', nullable: true }, + target: { + type: 'object', + description: 'Pipeline target', + nullable: true, + properties: { + type: { type: 'string', description: 'Target object type', nullable: true }, + refType: { type: 'string', description: 'Reference type', nullable: true }, + refName: { type: 'string', description: 'Reference name', nullable: true }, + commitHash: { type: 'string', description: 'Target commit hash', nullable: true }, + selectorType: { type: 'string', description: 'Pipeline selector type', nullable: true }, + selectorPattern: { type: 'string', description: 'Pipeline selector pattern', nullable: true }, + }, + }, + triggerType: { type: 'string', description: 'Pipeline trigger object type', nullable: true }, + state: { + type: 'object', + description: 'Pipeline state', + nullable: true, + properties: { + name: { type: 'string', description: 'State name', nullable: true }, + stage: { type: 'string', description: 'In-progress stage name', nullable: true }, + result: { type: 'string', description: 'Completed result name', nullable: true }, + errorKey: { type: 'string', description: 'Completed-error key', nullable: true }, + errorMessage: { type: 'string', description: 'Completed-error message', nullable: true }, + }, + }, + createdOn: { type: 'string', description: 'Creation timestamp', nullable: true }, + completedOn: { type: 'string', description: 'Completion timestamp', nullable: true }, + buildSecondsUsed: { type: 'number', description: 'Build seconds used', nullable: true }, + selfUrl: { type: 'string', description: 'Pipeline API URL', nullable: true }, + stepsUrl: { type: 'string', description: 'Pipeline steps API URL', nullable: true }, +} + +export const BITBUCKET_PIPELINE_COMMAND_OUTPUT_PROPERTIES: Record = { + name: { type: 'string', description: 'Command name', nullable: true }, + command: { type: 'string', description: 'Executable command', nullable: true }, +} + +export const BITBUCKET_PIPELINE_STEP_OUTPUT_PROPERTIES: Record = { + type: { type: 'string', description: 'Bitbucket pipeline-step object type' }, + uuid: { type: 'string', description: 'Pipeline step UUID', nullable: true }, + startedOn: { type: 'string', description: 'Step start timestamp', nullable: true }, + completedOn: { type: 'string', description: 'Step completion timestamp', nullable: true }, + state: { + type: 'object', + description: 'Pipeline step state', + nullable: true, + properties: { + name: { type: 'string', description: 'State name', nullable: true }, + result: { type: 'string', description: 'Completed result name', nullable: true }, + errorKey: { type: 'string', description: 'Completed-error key', nullable: true }, + errorMessage: { type: 'string', description: 'Completed-error message', nullable: true }, + }, + }, + imageName: { type: 'string', description: 'Build container image name', nullable: true }, + setupCommands: { + type: 'array', + description: 'Setup commands', + nullable: true, + items: { type: 'object', properties: BITBUCKET_PIPELINE_COMMAND_OUTPUT_PROPERTIES }, + }, + scriptCommands: { + type: 'array', + description: 'Build script commands', + nullable: true, + items: { type: 'object', properties: BITBUCKET_PIPELINE_COMMAND_OUTPUT_PROPERTIES }, + }, +} + +export const BITBUCKET_PAGE_OUTPUT_PROPERTIES: Record = { + size: { + type: 'number', + description: 'Total result count reported by Bitbucket', + nullable: true, + }, + page: { type: 'number', description: 'Current page number', nullable: true }, + pageLen: { + type: 'number', + description: 'Number of results requested per page', + nullable: true, + }, + nextUrl: { type: 'string', description: 'Validated URL for the next page', nullable: true }, + previousUrl: { + type: 'string', + description: 'Validated URL for the previous page', + nullable: true, + }, +} + +export const BITBUCKET_PAGE_OUTPUT: ToolOutputProperty = { + type: 'object', + description: 'Pagination information', + properties: BITBUCKET_PAGE_OUTPUT_PROPERTIES, +} diff --git a/apps/sim/tools/bitbucket/utils.server.test.ts b/apps/sim/tools/bitbucket/utils.server.test.ts new file mode 100644 index 00000000000..b67ee62dd93 --- /dev/null +++ b/apps/sim/tools/bitbucket/utils.server.test.ts @@ -0,0 +1,188 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' + +const { + mockBackoffWithJitter, + mockCreatePinnedFetchWithDispatcher, + mockParseRetryAfter, + mockSecureFetchWithPinnedIP, + mockValidateUrlWithDNS, +} = vi.hoisted(() => ({ + mockBackoffWithJitter: vi.fn(() => 0), + mockCreatePinnedFetchWithDispatcher: vi.fn(), + mockParseRetryAfter: vi.fn((header: string | null) => + header === null ? null : Number(header) * 1000 + ), + mockSecureFetchWithPinnedIP: vi.fn(), + mockValidateUrlWithDNS: vi.fn(), +})) + +vi.mock('@sim/utils/retry', () => ({ + backoffWithJitter: mockBackoffWithJitter, + parseRetryAfter: mockParseRetryAfter, +})) + +vi.mock('@/lib/core/security/input-validation.server', () => ({ + createPinnedFetchWithDispatcher: mockCreatePinnedFetchWithDispatcher, + secureFetchWithPinnedIP: mockSecureFetchWithPinnedIP, + validateUrlWithDNS: mockValidateUrlWithDNS, +})) + +import { + resolveBitbucketPullRequestRedirect, + secureBitbucketRead, +} from '@/tools/bitbucket/utils.server' + +function secureResponse( + status: number, + options: { retryAfter?: string; cancel?: ReturnType } = {} +) { + const headers = { + get: (name: string) => + name.toLowerCase() === 'retry-after' ? (options.retryAfter ?? null) : null, + toRecord: () => (options.retryAfter ? { 'retry-after': options.retryAfter } : {}), + } + return { + ok: status >= 200 && status < 300, + status, + statusText: '', + headers, + body: options.cancel ? { cancel: options.cancel } : null, + } +} + +beforeEach(() => { + vi.clearAllMocks() + mockBackoffWithJitter.mockReturnValue(0) + mockParseRetryAfter.mockImplementation((header: string | null) => + header === null ? null : Number(header) * 1000 + ) + mockValidateUrlWithDNS.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' }) +}) + +describe('secureBitbucketRead', () => { + it('honors bounded Retry-After pacing for a retryable response', async () => { + const cancel = vi.fn().mockResolvedValue(undefined) + mockSecureFetchWithPinnedIP + .mockResolvedValueOnce(secureResponse(429, { retryAfter: '2', cancel })) + .mockResolvedValueOnce(secureResponse(200)) + + await expect( + secureBitbucketRead('https://api.bitbucket.org/2.0/repositories/acme/demo', {}, 1024) + ).resolves.toMatchObject({ status: 200 }) + + expect(mockParseRetryAfter).toHaveBeenCalledWith('2', Number.POSITIVE_INFINITY) + expect(mockBackoffWithJitter).toHaveBeenCalledWith(1, 2000, { + baseMs: 500, + maxMs: 30_000, + }) + expect(cancel).toHaveBeenCalledOnce() + expect(mockSecureFetchWithPinnedIP).toHaveBeenCalledTimes(2) + }) + + it('retries a narrowly classified transport failure', async () => { + const timeout = Object.assign(new Error('socket timeout'), { code: 'ETIMEDOUT' }) + mockSecureFetchWithPinnedIP + .mockRejectedValueOnce(timeout) + .mockResolvedValueOnce(secureResponse(200)) + + await expect( + secureBitbucketRead('https://api.bitbucket.org/2.0/repositories/acme/demo', {}, 1024) + ).resolves.toMatchObject({ status: 200 }) + expect(mockSecureFetchWithPinnedIP).toHaveBeenCalledTimes(2) + }) + + it.each([ + ['ordinary client response', secureResponse(403)], + ['aborted request', new DOMException('Aborted', 'AbortError')], + [ + 'bounded response failure', + new PayloadSizeLimitError({ label: 'response body', maxBytes: 1024, observedBytes: 2048 }), + ], + ])('does not retry a %s', async (_name, result) => { + if (result instanceof Error) mockSecureFetchWithPinnedIP.mockRejectedValueOnce(result) + else mockSecureFetchWithPinnedIP.mockResolvedValueOnce(result) + + const execution = secureBitbucketRead( + 'https://api.bitbucket.org/2.0/repositories/acme/demo', + {}, + 1024 + ) + if (result instanceof Error) await expect(execution).rejects.toBe(result) + else await expect(execution).resolves.toMatchObject({ status: 403 }) + expect(mockSecureFetchWithPinnedIP).toHaveBeenCalledOnce() + }) + + it('stops after the three-attempt safe-read budget', async () => { + mockSecureFetchWithPinnedIP.mockResolvedValue(secureResponse(503)) + + await expect( + secureBitbucketRead('https://api.bitbucket.org/2.0/repositories/acme/demo', {}, 1024) + ).resolves.toMatchObject({ status: 503 }) + expect(mockSecureFetchWithPinnedIP).toHaveBeenCalledTimes(3) + }) + + it('forwards redirect and response-cap safety options on every attempt', async () => { + mockSecureFetchWithPinnedIP.mockResolvedValueOnce(secureResponse(200)) + + await secureBitbucketRead( + 'https://api.bitbucket.org/2.0/repositories/acme/demo/src/hash/file', + { Authorization: 'Bearer placeholder' }, + 10 * 1024 * 1024, + { stripAuthOnRedirect: true, maxRedirects: 0 } + ) + + expect(mockSecureFetchWithPinnedIP).toHaveBeenCalledWith( + 'https://api.bitbucket.org/2.0/repositories/acme/demo/src/hash/file', + '203.0.113.10', + expect.objectContaining({ + maxResponseBytes: 10 * 1024 * 1024, + maxRedirects: 0, + stripAuthOnRedirect: true, + }) + ) + }) +}) + +describe('resolveBitbucketPullRequestRedirect', () => { + it('cancels the manual redirect body before closing its pinned dispatcher', async () => { + const order: string[] = [] + const cancel = vi.fn(async () => { + order.push('cancel') + }) + const close = vi.fn(async () => { + order.push('close') + }) + const pinnedFetch = vi.fn().mockResolvedValue({ + status: 302, + headers: { + get: (name: string) => + name.toLowerCase() === 'location' + ? 'https://api.bitbucket.org/2.0/repositories/acme/demo/diff/main..feature' + : null, + }, + body: { cancel }, + }) + mockCreatePinnedFetchWithDispatcher.mockReturnValue({ + fetch: pinnedFetch, + dispatcher: { close }, + }) + + await expect( + resolveBitbucketPullRequestRedirect( + 'https://api.bitbucket.org/2.0/repositories/acme/demo/pullrequests/7/diff', + 'acme', + 'demo', + 'diff', + { Authorization: 'Bearer placeholder' }, + { targetQuery: { path: 'src/index.ts', binary: 'false' } } + ) + ).resolves.toBe( + 'https://api.bitbucket.org/2.0/repositories/acme/demo/diff/main..feature?path=src%2Findex.ts&binary=false' + ) + expect(order).toEqual(['cancel', 'close']) + }) +}) diff --git a/apps/sim/tools/bitbucket/utils.server.ts b/apps/sim/tools/bitbucket/utils.server.ts new file mode 100644 index 00000000000..84ab8570b66 --- /dev/null +++ b/apps/sim/tools/bitbucket/utils.server.ts @@ -0,0 +1,238 @@ +import { sleep } from '@sim/utils/helpers' +import { backoffWithJitter, parseRetryAfter } from '@sim/utils/retry' +import { + createPinnedFetchWithDispatcher, + secureFetchWithPinnedIP, + validateUrlWithDNS, +} from '@/lib/core/security/input-validation.server' +import { isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits' +import { + type BitbucketPullRequestRedirectKind, + validateBitbucketPullRequestRedirect, +} from '@/tools/bitbucket/utils' + +function retryableStatus(status: number): boolean { + return status === 429 || (status >= 500 && status <= 599) +} + +const BITBUCKET_READ_MAX_ATTEMPTS = 3 +const BITBUCKET_RETRY_BASE_MS = 500 +const BITBUCKET_RETRY_MAX_MS = 30_000 + +function abortError(signal: AbortSignal): unknown { + return signal.reason ?? new DOMException('Aborted', 'AbortError') +} + +function retryableTransportError(error: unknown): boolean { + if (isPayloadSizeLimitError(error)) return false + if (!(error instanceof Error) || error.name === 'AbortError') return false + const code = (error as NodeJS.ErrnoException).code + if (code === 'ETIMEDOUT' || code === 'ECONNRESET' || code === 'ECONNABORTED') return true + const message = error.message.toLowerCase() + return message.includes('timeout') || message.includes('timed out') +} + +async function waitForRetry(delayMs: number, signal?: AbortSignal): Promise { + if (!signal) { + await sleep(delayMs) + return + } + if (signal.aborted) throw abortError(signal) + + let onAbort: (() => void) | undefined + const aborted = new Promise((_, reject) => { + onAbort = () => reject(abortError(signal)) + signal.addEventListener('abort', onAbort, { once: true }) + }) + try { + await Promise.race([sleep(delayMs), aborted]) + } finally { + if (onAbort) signal.removeEventListener('abort', onAbort) + } +} + +function retryDelayMs(attempt: number, retryAfter: string | null): number | null { + const retryAfterMs = parseRetryAfter(retryAfter, Number.POSITIVE_INFINITY) + if (retryAfterMs !== null && retryAfterMs > BITBUCKET_RETRY_MAX_MS) return null + return backoffWithJitter(attempt, retryAfterMs, { + baseMs: BITBUCKET_RETRY_BASE_MS, + maxMs: BITBUCKET_RETRY_MAX_MS, + }) +} + +/** + * Executes a bounded, DNS-pinned Bitbucket read. Redirect targets are validated + * and pinned by the shared transport; callers can drop authorization for media + * redirects without trusting a destination hostname. + */ +export async function secureBitbucketRead( + url: string, + headers: Record, + maxResponseBytes: number, + options: { + stripAuthOnRedirect?: boolean + maxRedirects?: number + signal?: AbortSignal + } = {} +): Promise { + const validation = await validateUrlWithDNS(url, 'bitbucketUrl') + if (!validation.isValid || !validation.resolvedIP) { + throw new Error(`Invalid Bitbucket URL: ${validation.error ?? 'DNS resolution failed'}`) + } + + for (let attempt = 1; attempt <= BITBUCKET_READ_MAX_ATTEMPTS; attempt++) { + try { + const response = await secureFetchWithPinnedIP(url, validation.resolvedIP, { + method: 'GET', + headers, + maxResponseBytes, + stripAuthOnRedirect: options.stripAuthOnRedirect, + maxRedirects: options.maxRedirects, + signal: options.signal, + }) + if (retryableStatus(response.status) && attempt < BITBUCKET_READ_MAX_ATTEMPTS) { + const delayMs = retryDelayMs(attempt, response.headers.get('retry-after')) + if (delayMs === null) { + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: response.headers.toRecord(), + }) + } + await response.body?.cancel() + await waitForRetry(delayMs, options.signal) + continue + } + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: response.headers.toRecord(), + }) + } catch (error) { + if ( + options.signal?.aborted || + attempt === BITBUCKET_READ_MAX_ATTEMPTS || + !retryableTransportError(error) + ) { + throw error + } + await waitForRetry(retryDelayMs(attempt, null)!, options.signal) + } + } + throw new Error('Bitbucket read failed') +} + +/** + * Resolves the documented PR-to-repository redirect without allowing the + * bearer-authenticated request to follow an unvalidated Location. + */ +export async function resolveBitbucketPullRequestRedirect( + initialUrl: string, + workspaceSlug: string, + repoSlug: string, + kind: BitbucketPullRequestRedirectKind, + headers: Record, + options: { + signal?: AbortSignal + targetQuery?: Record + } = {} +): Promise { + const initialValidation = await validateUrlWithDNS(initialUrl, 'bitbucketPullRequestUrl') + if (!initialValidation.isValid || !initialValidation.resolvedIP) { + throw new Error( + `Invalid Bitbucket pull request URL: ${initialValidation.error ?? 'DNS resolution failed'}` + ) + } + + const { fetch: pinnedFetch, dispatcher } = createPinnedFetchWithDispatcher( + initialValidation.resolvedIP, + { maxResponseSize: 64 * 1024 } + ) + let initial: Response | null = null + let initialStatus: number | null = null + let location: string | null = null + try { + for (let attempt = 1; attempt <= BITBUCKET_READ_MAX_ATTEMPTS; attempt++) { + try { + initial = await pinnedFetch(initialUrl, { + method: 'GET', + headers, + redirect: 'manual', + signal: options.signal, + }) + } catch (error) { + if ( + options.signal?.aborted || + attempt === BITBUCKET_READ_MAX_ATTEMPTS || + !retryableTransportError(error) + ) { + throw error + } + await waitForRetry(retryDelayMs(attempt, null)!, options.signal) + continue + } + if (!retryableStatus(initial.status) || attempt === BITBUCKET_READ_MAX_ATTEMPTS) break + const delayMs = retryDelayMs(attempt, initial.headers.get('retry-after')) + if (delayMs === null) break + await initial.body?.cancel() + await waitForRetry(delayMs, options.signal) + } + if (initial) { + initialStatus = initial.status + location = initial.headers.get('location') + } + } finally { + await initial?.body?.cancel().catch(() => undefined) + await dispatcher.close() + } + + if (initialStatus === null) throw new Error(`Bitbucket ${kind} redirect request failed`) + + if (![301, 302, 303, 307, 308].includes(initialStatus)) { + throw new Error(`Bitbucket ${kind} endpoint did not return its documented redirect`) + } + if (!location) throw new Error(`Bitbucket ${kind} redirect omitted the Location header`) + + const target = new URL( + validateBitbucketPullRequestRedirect( + new URL(location, initialUrl).toString(), + workspaceSlug, + repoSlug, + kind + ) + ) + for (const [key, value] of Object.entries(options.targetQuery ?? {})) { + target.searchParams.set(key, value) + } + return target.toString() +} + +/** + * Performs the documented PR-to-repository redirect without allowing the + * bearer-authenticated request to follow an unvalidated Location. + */ +export async function secureBitbucketPullRequestRedirect( + initialUrl: string, + workspaceSlug: string, + repoSlug: string, + kind: BitbucketPullRequestRedirectKind, + headers: Record, + maxResponseBytes: number, + options: { + signal?: AbortSignal + targetQuery?: Record + } = {} +): Promise { + const target = await resolveBitbucketPullRequestRedirect( + initialUrl, + workspaceSlug, + repoSlug, + kind, + headers, + options + ) + return secureBitbucketRead(target, headers, maxResponseBytes, { + maxRedirects: 0, + signal: options.signal, + }) +} diff --git a/apps/sim/tools/bitbucket/utils.test.ts b/apps/sim/tools/bitbucket/utils.test.ts new file mode 100644 index 00000000000..92bfd72b798 --- /dev/null +++ b/apps/sim/tools/bitbucket/utils.test.ts @@ -0,0 +1,418 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + assertBitbucketResponseOk, + bitbucketApiUrl, + bitbucketJson, + bitbucketRawHead, + bitbucketRawTail, + bitbucketRepositoryPath, + encodeBitbucketRepositoryPath, + encodeBitbucketSegment, + normalizeBitbucketFileMetadata, + normalizeBitbucketPage, + validateBitbucketOpaqueUrl, + validateBitbucketPullRequestRedirect, +} from '@/tools/bitbucket/utils' + +describe('Bitbucket path and pagination safety', () => { + it('encodes every identifier as one path segment', () => { + expect(encodeBitbucketSegment(' team/blue?admin=true#x ', 'workspaceSlug')).toBe( + 'team%2Fblue%3Fadmin%3Dtrue%23x' + ) + expect(bitbucketRepositoryPath('team / blue', 'repo/../secret?x=1')).toBe( + '/repositories/team%20%2F%20blue/repo%2F..%2Fsecret%3Fx%3D1' + ) + }) + + it('encodes repository paths segment-by-segment, including spaces and hostile characters', () => { + expect(encodeBitbucketRepositoryPath('/src/my file?#.ts/')).toBe('src/my%20file%3F%23.ts') + expect(encodeBitbucketRepositoryPath('', true)).toBe('') + expect(() => encodeBitbucketRepositoryPath('src/../secret')).toThrow(/dot segment/) + expect(() => encodeBitbucketRepositoryPath('src/./file')).toThrow(/dot segment/) + expect(() => encodeBitbucketSegment('..', 'repoSlug')).toThrow(/dot path segment/) + }) + + it('builds bounded list queries and does not add pagination to non-list calls', () => { + const list = new URL( + bitbucketApiUrl('/repositories/acme', { + pageLen: 25, + query: { role: 'owner', q: 'name ~ "sdk"', ignored: undefined }, + }) + ) + expect(list.origin).toBe('https://api.bitbucket.org') + expect(list.pathname).toBe('/2.0/repositories/acme') + expect(Object.fromEntries(list.searchParams)).toEqual({ + role: 'owner', + q: 'name ~ "sdk"', + pagelen: '25', + }) + + expect(bitbucketApiUrl('/repositories/acme/demo')).toBe( + 'https://api.bitbucket.org/2.0/repositories/acme/demo' + ) + expect(() => bitbucketApiUrl('/repositories/acme', { pageLen: 101 })).toThrow( + /between 1 and 100/ + ) + }) + + it('accepts only exact HTTPS Bitbucket Cloud API 2.0 cursor authorities', () => { + const valid = 'https://api.bitbucket.org/2.0/repositories/acme?page=2' + expect(validateBitbucketOpaqueUrl(valid)).toBe(valid) + + const hostile = [ + 'http://api.bitbucket.org/2.0/repositories/acme?page=2', + 'https://api.bitbucket.org.evil.test/2.0/repositories/acme?page=2', + 'https://api.bitbucket.org:444/2.0/repositories/acme?page=2', + 'https://user:pass@api.bitbucket.org/2.0/repositories/acme?page=2', + 'https://api.bitbucket.org/1.0/repositories/acme?page=2', + 'https://api.bitbucket.org/2.0/repositories/acme?page=2#fragment', + 'not a url', + ] + for (const candidate of hostile) { + expect(() => validateBitbucketOpaqueUrl(candidate), candidate).toThrow() + } + }) + + it('binds opaque cursors to the exact list endpoint', () => { + const expected = 'https://api.bitbucket.org/2.0/repositories/acme/demo/commits?page=2' + expect( + bitbucketApiUrl('/repositories/acme/demo/commits', { + nextUrl: expected, + pageLen: 100, + query: { q: 'ignored for opaque cursors' }, + }) + ).toBe(expected) + + expect(() => + bitbucketApiUrl('/repositories/acme/demo/commits', { + nextUrl: 'https://api.bitbucket.org/2.0/repositories/acme/demo/pipelines?page=2', + }) + ).toThrow(/does not belong/) + }) + + it('binds directory cursors to the selected repository path', () => { + const revision = '0123456789abcdef0123456789abcdef01234567' + const next = `https://api.bitbucket.org/2.0/repositories/acme/demo/src/${revision}/src/my%20dir?page=2` + expect( + bitbucketApiUrl(`/repositories/acme/demo/src/${revision}/src/my%20dir`, { + nextUrl: next, + nextPathPrefix: '/repositories/acme/demo/src', + nextPathSuffix: 'src/my%20dir', + nextRevision: revision, + }) + ).toBe(next) + + const equivalentEncoding = `https://api.bitbucket.org/2.0/repositories/acme/demo/src/${revision}/src/my%20%64ir?page=2` + expect( + bitbucketApiUrl(`/repositories/acme/demo/src/${revision}/src/my%20dir`, { + nextUrl: equivalentEncoding, + nextPathPrefix: '/repositories/acme/demo/src', + nextPathSuffix: 'src/my%20dir', + nextRevision: revision, + }) + ).toBe(equivalentEncoding) + + expect(() => + bitbucketApiUrl(`/repositories/acme/demo/src/${revision}/src/my%20dir`, { + nextUrl: `https://api.bitbucket.org/2.0/repositories/acme/demo/src/${revision}/src/other?page=2`, + nextPathPrefix: '/repositories/acme/demo/src', + nextPathSuffix: 'src/my%20dir', + nextRevision: revision, + }) + ).toThrow(/does not preserve/) + + expect(() => + bitbucketApiUrl(`/repositories/acme/demo/src/${revision}/src/my%20dir`, { + nextUrl: `https://api.bitbucket.org/2.0/repositories/acme/demo/src/${revision}/src//my%20dir?page=2`, + nextPathPrefix: '/repositories/acme/demo/src', + nextPathSuffix: 'src/my%20dir', + nextRevision: revision, + }) + ).toThrow(/empty path segments/) + + const otherRevision = 'fedcba9876543210fedcba9876543210fedcba98' + expect(() => + bitbucketApiUrl(`/repositories/acme/demo/src/${revision}/src/my%20dir`, { + nextUrl: `https://api.bitbucket.org/2.0/repositories/acme/demo/src/${otherRevision}/src/my%20dir?page=2`, + nextPathPrefix: '/repositories/acme/demo/src', + nextPathSuffix: 'src/my%20dir', + nextRevision: revision, + }) + ).toThrow(/requested Bitbucket revision/) + }) + + it('rejects non-primitive runtime query values', () => { + expect(() => + bitbucketApiUrl('/repositories/acme', { + query: { role: { unexpected: true } } as never, + }) + ).toThrow(/query parameter role must be a string, number, or boolean/) + }) + + it('accepts PR redirects only for the requested repository and endpoint kind', () => { + const diff = 'https://api.bitbucket.org/2.0/repositories/acme/demo/diff/main..feature' + expect(validateBitbucketPullRequestRedirect(diff, 'acme', 'demo', 'diff')).toBe(diff) + expect(() => + validateBitbucketPullRequestRedirect( + 'https://api.bitbucket.org/2.0/repositories/acme/other/diff/main..feature', + 'acme', + 'demo', + 'diff' + ) + ).toThrow(/did not target/) + expect(() => + validateBitbucketPullRequestRedirect( + 'https://api.bitbucket.org/2.0/repositories/acme/demo/diffstat/main..feature', + 'acme', + 'demo', + 'diff' + ) + ).toThrow(/did not target/) + }) +}) + +describe('Bitbucket envelopes and errors', () => { + it('normalizes lists to items plus stable page metadata', () => { + const output = normalizeBitbucketPage( + { + values: [{ id: 1 }, { id: 2 }], + size: 9, + page: 2, + pagelen: 2, + next: 'https://api.bitbucket.org/2.0/repositories/acme?page=3', + previous: 'https://api.bitbucket.org/2.0/repositories/acme?page=1', + }, + (value) => value + ) + + expect(output).toEqual({ + items: [{ id: 1 }, { id: 2 }], + page: { + size: 9, + page: 2, + pageLen: 2, + nextUrl: 'https://api.bitbucket.org/2.0/repositories/acme?page=3', + previousUrl: 'https://api.bitbucket.org/2.0/repositories/acme?page=1', + }, + }) + }) + + it('rejects malformed pagination envelopes and pagination links', () => { + expect(() => normalizeBitbucketPage({}, (value) => value)).toThrow(/values array/) + expect(() => + normalizeBitbucketPage({ values: [], next: { href: 'not-supported' } }, (value) => value) + ).toThrow(/pagination next must be a URL/) + expect(() => + normalizeBitbucketPage( + { values: [], next: 'https://evil.test/2.0/repositories/acme?page=2' }, + (value) => value + ) + ).toThrow(/Bitbucket Cloud API 2.0 URL/) + }) + + it('rejects non-object JSON responses', async () => { + await expect(bitbucketJson(Response.json([]))).rejects.toThrow(/non-object JSON/) + await expect(bitbucketJson(Response.json(null))).rejects.toThrow(/non-object JSON/) + }) + + it('extracts Bitbucket structured errors and preserves plain-text errors', async () => { + await expect( + assertBitbucketResponseOk( + Response.json({ error: { message: 'Merge checks failed' } }, { status: 409 }) + ) + ).rejects.toThrow('Merge checks failed') + await expect( + assertBitbucketResponseOk(new Response('Service unavailable', { status: 503 })) + ).rejects.toThrow('Service unavailable') + await expect(assertBitbucketResponseOk(new Response(null, { status: 429 }))).rejects.toThrow( + /Bitbucket API error: 429/ + ) + }) + + it('distinguishes documented binary metadata from unknown metadata', () => { + expect( + normalizeBitbucketFileMetadata({ + type: 'commit_file', + path: 'assets/logo.png', + commit: { hash: 'abc' }, + escaped_path: 'assets/logo.png', + size: 42, + attributes: ['binary', 'lfs'], + }) + ).toMatchObject({ attributes: ['binary', 'lfs'], isBinary: true }) + expect( + normalizeBitbucketFileMetadata({ type: 'commit_file', path: 'README.md' }) + ).toMatchObject({ + attributes: null, + isBinary: null, + }) + expect( + normalizeBitbucketFileMetadata({ + type: 'commit_file', + path: 'README.md', + attributes: [], + }) + ).toMatchObject({ attributes: [], isBinary: false }) + expect(() => + normalizeBitbucketFileMetadata({ + type: 'commit_file', + path: 'README.md', + attributes: ['future_attribute', 1], + }) + ).toThrow(/metadata\.attributes\[1\] must be a string/) + }) +}) + +describe('Bitbucket bounded raw content', () => { + it('caps UTF-8 file content without emitting an incomplete trailing character', async () => { + const bytes = new TextEncoder().encode('ab🙂cd') + const partial = bytes.slice(0, 5) + const result = await bitbucketRawHead( + new Response(partial, { + status: 206, + headers: { + 'Content-Range': `bytes 0-${partial.byteLength - 1}/${bytes.byteLength}`, + 'Content-Type': 'text/plain; charset=utf-8', + }, + }), + 5, + false + ) + + expect(result).toEqual({ + content: 'ab', + binary: false, + truncated: true, + returnedBytes: partial.byteLength, + fullBytes: bytes.byteLength, + contentType: 'text/plain; charset=utf-8', + }) + }) + + it('locally caps a full response when Range is ignored', async () => { + const text = 'abcdefghijklmnopqrstuvwxyz' + const result = await bitbucketRawHead( + new Response(text, { headers: { 'Content-Length': String(text.length) } }), + 3, + false + ) + + expect(result).toEqual({ + content: 'abc', + binary: false, + truncated: true, + returnedBytes: 12, + fullBytes: text.length, + contentType: 'text/plain;charset=UTF-8', + }) + }) + + it.each([ + ['missing range', {}, 'abcde'], + ['impossible total', { 'Content-Range': 'bytes 0-4/4' }, 'abcde'], + [ + 'inconsistent length header', + { 'Content-Range': 'bytes 0-4/10', 'Content-Length': '4' }, + 'abcde', + ], + ['inconsistent body length', { 'Content-Range': 'bytes 0-4/10' }, 'abcd'], + ])('rejects a 206 prefix with %s', async (_name, headers, body) => { + await expect( + bitbucketRawHead(new Response(body, { status: 206, headers }), 100, false) + ).rejects.toThrow(/Content-(?:Range|Length)|body does not match/) + }) + + it('detects NUL bytes when metadata cannot determine whether a file is binary', async () => { + const result = await bitbucketRawHead( + new Response(new Uint8Array([65, 0, 66]), { + headers: { 'Content-Type': 'application/octet-stream' }, + }), + 100, + null + ) + + expect(result).toMatchObject({ + content: null, + binary: true, + truncated: true, + returnedBytes: 3, + fullBytes: 3, + }) + }) + + it('reports nullable truncation when raw binary size remains unknown', async () => { + const result = await bitbucketRawHead( + new Response(new Uint8Array([65, 0, 66]), { + status: 206, + headers: { 'Content-Range': 'bytes 0-2/*' }, + }), + 100, + null + ) + + expect(result).toMatchObject({ + content: null, + binary: true, + truncated: null, + returnedBytes: 3, + fullBytes: null, + }) + }) + + it('trims a partial leading line from a ranged log tail', async () => { + const body = 'ise\nFAILED: expected 1 to be 2\n' + const result = await bitbucketRawTail( + new Response(body, { + status: 206, + headers: { 'Content-Range': `bytes 969-999/1000` }, + }), + 100 + ) + + expect(result).toEqual({ + log: 'FAILED: expected 1 to be 2\n', + truncated: true, + totalBytes: 1000, + }) + }) + + it('keeps the first line when a 206 response contains the entire log', async () => { + const body = 'first\nsecond\n' + const result = await bitbucketRawTail( + new Response(body, { + status: 206, + headers: { 'Content-Range': `bytes 0-${body.length - 1}/${body.length}` }, + }), + 100 + ) + + expect(result).toEqual({ log: body, truncated: false, totalBytes: body.length }) + }) + + it.each([ + ['missing Content-Range', {}, 'abc'], + ['unknown total', { 'Content-Range': 'bytes 7-9/*' }, 'abc'], + ['non-suffix range', { 'Content-Range': 'bytes 0-2/10' }, 'abc'], + ['body mismatch', { 'Content-Range': 'bytes 7-9/10' }, 'ab'], + ])('rejects a 206 log tail with %s', async (_name, headers, body) => { + await expect( + bitbucketRawTail(new Response(body, { status: 206, headers }), 100) + ).rejects.toThrow(/Content-Range|suffix|body does not match/) + }) + + it('keeps a bounded tail when the server ignores Range', async () => { + const body = `${'noise line\n'.repeat(20)}FAILED\n` + const result = await bitbucketRawTail( + new Response(body, { headers: { 'Content-Length': String(Buffer.byteLength(body)) } }), + 10 + ) + + expect(result.log).toHaveLength(10) + expect(result.log).toBe('ne\nFAILED\n') + expect(result.log.endsWith('FAILED\n')).toBe(true) + expect(result).toMatchObject({ truncated: true, totalBytes: Buffer.byteLength(body) }) + }) +}) diff --git a/apps/sim/tools/bitbucket/utils.ts b/apps/sim/tools/bitbucket/utils.ts new file mode 100644 index 00000000000..d55bce798e0 --- /dev/null +++ b/apps/sim/tools/bitbucket/utils.ts @@ -0,0 +1,1038 @@ +import type { + BitbucketBranch, + BitbucketComment, + BitbucketCommit, + BitbucketCommitStatus, + BitbucketDiffstat, + BitbucketDirectoryEntry, + BitbucketFileMetadata, + BitbucketListOutput, + BitbucketPage, + BitbucketParticipant, + BitbucketPipeline, + BitbucketPipelineStep, + BitbucketPullRequest, + BitbucketPullRequestEndpoint, + BitbucketRepository, + BitbucketUser, + BitbucketWorkspaceAccess, +} from '@/tools/bitbucket/types' +import type { ToolRetryConfig } from '@/tools/types' + +export const BITBUCKET_API_BASE = 'https://api.bitbucket.org/2.0' +export const BITBUCKET_ERROR_EXTRACTOR = 'nested-error-object' +export const BITBUCKET_DEFAULT_MAX_CHARACTERS = 100_000 +export const BITBUCKET_MAX_CHARACTERS = 500_000 +export const BITBUCKET_DEFAULT_LOG_CHARACTERS = 20_000 +export const BITBUCKET_MAX_LOG_CHARACTERS = 200_000 +export const BITBUCKET_RAW_TRANSFER_MAX_BYTES = 10 * 1024 * 1024 + +export const BITBUCKET_READ_RETRY: ToolRetryConfig = { + enabled: true, + maxRetries: 2, + retryIdempotentOnly: true, +} + +export const BITBUCKET_ACCESS_TOKEN_PARAM = { + type: 'string', + required: true, + visibility: 'hidden' as const, + description: 'Bitbucket OAuth access token', +} + +export const BITBUCKET_REPOSITORY_PARAMS = { + workspaceSlug: { + type: 'string', + required: true, + visibility: 'user-or-llm' as const, + description: 'Bitbucket workspace slug or UUID', + }, + repoSlug: { + type: 'string', + required: true, + visibility: 'user-or-llm' as const, + description: 'Bitbucket repository slug or UUID', + }, + accessToken: { + type: 'string', + required: true, + visibility: 'hidden' as const, + description: 'Bitbucket OAuth access token', + }, +} + +export const BITBUCKET_PULL_REQUEST_PARAMS = { + ...BITBUCKET_REPOSITORY_PARAMS, + prId: { + type: 'number', + required: true, + visibility: 'user-or-llm' as const, + description: 'Repository-scoped pull request ID', + }, +} + +export const BITBUCKET_PAGINATION_PARAMS = { + nextUrl: { + type: 'string', + required: false, + visibility: 'user-or-llm' as const, + description: 'Opaque next-page URL returned by a previous Bitbucket call', + }, + pageLen: { + type: 'number', + required: false, + visibility: 'user-or-llm' as const, + description: 'Results per page (1-100)', + default: 20, + }, +} + +type JsonRecord = Record + +function asRecord(value: unknown): JsonRecord | null { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? (value as JsonRecord) + : null +} + +function readRecord(record: JsonRecord | null, key: string): JsonRecord | null { + return asRecord(record?.[key]) +} + +function readString(record: JsonRecord | null, key: string): string | null { + const value = record?.[key] + return typeof value === 'string' ? value : null +} + +function readNumber(record: JsonRecord | null, key: string): number | null { + const value = record?.[key] + return typeof value === 'number' && Number.isFinite(value) ? value : null +} + +function readBoolean(record: JsonRecord | null, key: string): boolean | null { + const value = record?.[key] + return typeof value === 'boolean' ? value : null +} + +function readRequiredString(record: JsonRecord, key: string, context: string): string { + const value = record[key] + if (typeof value !== 'string' || value.trim().length === 0) { + throw new Error(`Bitbucket ${context}.${key} must be a non-empty string`) + } + return value +} + +function requireResourceRecord(value: unknown, context: string): JsonRecord { + const record = asRecord(value) + if (!record) throw new Error(`Bitbucket ${context} must be an object`) + readRequiredString(record, 'type', context) + return record +} + +function readOptionalArray(record: JsonRecord, key: string, context: string): unknown[] | null { + const value = record[key] + if (value === undefined) return null + if (!Array.isArray(value)) { + throw new Error(`Bitbucket ${context}.${key} must be an array when present`) + } + return value +} + +function readOptionalStringArray( + record: JsonRecord, + key: string, + context: string +): string[] | null { + const values = readOptionalArray(record, key, context) + return ( + values?.map((value, index) => { + if (typeof value !== 'string') { + throw new Error(`Bitbucket ${context}.${key}[${index}] must be a string`) + } + return value + }) ?? null + ) +} + +function readNullableNumber(record: JsonRecord, key: string, context: string): number | null { + const value = record[key] + if (value === undefined || value === null) return null + if (typeof value !== 'number' || !Number.isFinite(value)) { + throw new Error(`Bitbucket ${context}.${key} must be a finite number or null`) + } + return value +} + +function linkHref(record: JsonRecord | null, name: string): string | null { + return readString(readRecord(readRecord(record, 'links'), name), 'href') +} + +export function requireBitbucketString(value: string, name: string): string { + if (typeof value !== 'string' || value.trim().length === 0) { + throw new Error(`${name} must be a non-empty string`) + } + return value.trim() +} + +export function encodeBitbucketSegment(value: string, name: string): string { + const normalized = requireBitbucketString(value, name) + if (normalized === '.' || normalized === '..') { + throw new Error(`${name} cannot be a dot path segment`) + } + return encodeURIComponent(normalized) +} + +export function encodeBitbucketRepositoryPath(path: string, allowEmpty = false): string { + if (typeof path !== 'string') throw new Error('path must be a string') + const normalized = path.replace(/^\/+|\/+$/g, '') + if (!normalized) { + if (allowEmpty) return '' + throw new Error('path must be a non-empty repository-relative path') + } + return normalized + .split('/') + .map((segment) => { + if (segment.length === 0) throw new Error('path cannot contain an empty segment') + if (segment === '.' || segment === '..') { + throw new Error('path cannot contain a dot segment') + } + return encodeURIComponent(segment) + }) + .join('/') +} + +export function bitbucketRepositoryPathQuery(path: string): string { + if (typeof path !== 'string') throw new Error('path must be a string') + const normalized = path.replace(/^\/+|\/+$/g, '') + if (normalized.length === 0) { + throw new Error('path must be a non-empty repository-relative path') + } + encodeBitbucketRepositoryPath(normalized) + return normalized +} + +export function positiveBitbucketInteger(value: number, name: string): number { + if (!Number.isSafeInteger(value) || value < 1) { + throw new Error(`${name} must be a positive integer`) + } + return value +} + +export function bitbucketPageLength(value: number | undefined): number { + const pageLen = value ?? 20 + if (!Number.isSafeInteger(pageLen) || pageLen < 1 || pageLen > 100) { + throw new Error('pageLen must be an integer between 1 and 100') + } + return pageLen +} + +export function bitbucketMaxCharacters(value: number | undefined, log = false): number { + const defaultValue = log ? BITBUCKET_DEFAULT_LOG_CHARACTERS : BITBUCKET_DEFAULT_MAX_CHARACTERS + const limit = log ? BITBUCKET_MAX_LOG_CHARACTERS : BITBUCKET_MAX_CHARACTERS + const resolved = value ?? defaultValue + if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > limit) { + throw new Error(`maxCharacters must be an integer between 1 and ${limit}`) + } + return resolved +} + +export function bitbucketHeadRange(maxCharacters: number | undefined): string { + const byteLimit = bitbucketMaxCharacters(maxCharacters) * 4 + return `bytes=0-${byteLimit - 1}` +} + +export function bitbucketTailRange(maxCharacters: number | undefined): string { + const byteLimit = bitbucketMaxCharacters(maxCharacters, true) * 4 + return `bytes=-${byteLimit}` +} + +export function bitbucketHeaders( + accessToken: string, + options: { json?: boolean; range?: string } = {} +): Record { + if (typeof accessToken !== 'string' || accessToken.length === 0) { + throw new Error('Missing Bitbucket OAuth access token') + } + return { + Accept: options.json === false ? '*/*' : 'application/json', + Authorization: `Bearer ${accessToken}`, + ...(options.json ? { 'Content-Type': 'application/json' } : {}), + ...(options.range ? { Range: options.range } : {}), + } +} + +export function bitbucketRepositoryPath(workspaceSlug: string, repoSlug: string): string { + return `/repositories/${encodeBitbucketSegment(workspaceSlug, 'workspaceSlug')}/${encodeBitbucketSegment(repoSlug, 'repoSlug')}` +} + +export function bitbucketPullRequestPath( + workspaceSlug: string, + repoSlug: string, + prId: number +): string { + return `${bitbucketRepositoryPath(workspaceSlug, repoSlug)}/pullrequests/${positiveBitbucketInteger(prId, 'prId')}` +} + +export function validateBitbucketOpaqueUrl(value: string): string { + const candidate = requireBitbucketString(value, 'nextUrl') + let parsed: URL + try { + parsed = new URL(candidate) + } catch { + throw new Error('nextUrl must be a valid absolute URL') + } + if ( + parsed.protocol !== 'https:' || + parsed.hostname !== 'api.bitbucket.org' || + parsed.port !== '' || + parsed.username !== '' || + parsed.password !== '' || + parsed.hash !== '' || + !parsed.pathname.startsWith('/2.0/') + ) { + throw new Error('nextUrl must be an HTTPS Bitbucket Cloud API 2.0 URL') + } + return parsed.toString() +} + +export type BitbucketPullRequestRedirectKind = 'diff' | 'diffstat' + +export function validateBitbucketPullRequestRedirect( + value: string, + workspaceSlug: string, + repoSlug: string, + kind: BitbucketPullRequestRedirectKind +): string { + const validated = validateBitbucketOpaqueUrl(value) + const parsed = new URL(validated) + const expectedPrefix = `/2.0${bitbucketRepositoryPath(workspaceSlug, repoSlug)}/${kind}/` + if (!parsed.pathname.startsWith(expectedPrefix)) { + throw new Error(`Bitbucket ${kind} redirect did not target this repository's ${kind} endpoint`) + } + return parsed.toString() +} + +export async function assertBitbucketResponseOk(response: Response): Promise { + if (response.ok) return + const errorBytes = await readBoundedBytes(response, 4_000).catch(() => ({ + bytes: new Uint8Array(), + clipped: false, + })) + const errorBody = new TextDecoder().decode(errorBytes.bytes) + let message = errorBody + try { + const parsed: unknown = JSON.parse(errorBody) + const record = asRecord(parsed) + message = readString(readRecord(record, 'error'), 'message') ?? errorBody + } catch { + message = errorBody + } + throw new Error(message || `Bitbucket API error: ${response.status} ${response.statusText}`) +} + +export function bitbucketApiUrl( + path: string, + options: { + nextUrl?: string + pageLen?: number + query?: Record + nextPathPrefix?: string + nextPathSuffix?: string + nextRevision?: string + } = {} +): string { + if (options.nextUrl) { + const validated = validateBitbucketOpaqueUrl(options.nextUrl) + const decodePath = (pathname: string): string[] => { + const withoutLeadingSlash = pathname.startsWith('/') ? pathname.slice(1) : pathname + const normalized = withoutLeadingSlash.endsWith('/') + ? withoutLeadingSlash.slice(0, -1) + : withoutLeadingSlash + if (normalized.length === 0) return [] + const encodedSegments = normalized.split('/') + if (encodedSegments.some((segment) => segment.length === 0)) { + throw new Error('nextUrl cannot contain empty path segments') + } + try { + return encodedSegments.map((segment) => decodeURIComponent(segment)) + } catch { + throw new Error('nextUrl contains invalid path encoding') + } + } + const candidatePath = new URL(validated).pathname + const exactPath = `/2.0${path}`.replace(/\/$/, '') + const prefix = options.nextPathPrefix + ? `/2.0${options.nextPathPrefix}`.replace(/\/$/, '') + : null + if (prefix) { + const candidateSegments = decodePath(candidatePath) + const prefixSegments = decodePath(prefix) + if ( + candidateSegments.length <= prefixSegments.length || + !prefixSegments.every((segment, index) => candidateSegments[index] === segment) + ) { + throw new Error('nextUrl does not belong to this Bitbucket list endpoint') + } + const revisionAndPath = candidateSegments.slice(prefixSegments.length) + if (options.nextRevision === undefined || revisionAndPath[0] !== options.nextRevision) { + throw new Error('nextUrl does not preserve the requested Bitbucket revision') + } + const expectedSuffix = decodePath(options.nextPathSuffix ?? '') + const actualSuffix = revisionAndPath.slice(1) + if ( + actualSuffix.length !== expectedSuffix.length || + !expectedSuffix.every((segment, index) => actualSuffix[index] === segment) + ) { + throw new Error('nextUrl does not preserve the requested Bitbucket directory path') + } + } else if (candidatePath.replace(/\/$/, '') !== exactPath) { + throw new Error('nextUrl does not belong to this Bitbucket list endpoint') + } + return validated + } + const url = new URL(`${BITBUCKET_API_BASE}${path}`) + for (const [key, value] of Object.entries(options.query ?? {}) as Array<[string, unknown]>) { + if (value === undefined) continue + if ( + (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') || + (typeof value === 'number' && !Number.isFinite(value)) + ) { + throw new Error(`Bitbucket query parameter ${key} must be a string, number, or boolean`) + } + url.searchParams.set(key, String(value)) + } + if (Object.hasOwn(options, 'pageLen')) { + url.searchParams.set('pagelen', String(bitbucketPageLength(options.pageLen))) + } + return url.toString() +} + +export async function bitbucketJson(response: Response): Promise { + const data: unknown = await response.json() + const record = asRecord(data) + if (!record) throw new Error('Bitbucket returned a non-object JSON response') + return record +} + +function normalizePage(data: JsonRecord): BitbucketPage { + const pageLink = (key: 'next' | 'previous'): string | null => { + const value = data[key] + if (value === undefined || value === null) return null + if (typeof value !== 'string') throw new Error(`Bitbucket pagination ${key} must be a URL`) + return validateBitbucketOpaqueUrl(value) + } + return { + size: readNumber(data, 'size'), + page: readNumber(data, 'page'), + pageLen: readNumber(data, 'pagelen'), + nextUrl: pageLink('next'), + previousUrl: pageLink('previous'), + } +} + +export function normalizeBitbucketPage( + data: JsonRecord, + normalize: (value: unknown) => T +): BitbucketListOutput { + if (!Array.isArray(data.values)) { + throw new Error('Bitbucket pagination response must include a values array') + } + return { + items: data.values.map(normalize), + page: normalizePage(data), + } +} + +export function normalizeBitbucketUser(value: unknown): BitbucketUser | null { + if (value === undefined || value === null) return null + const data = requireResourceRecord(value, 'account') + return { + type: readRequiredString(data, 'type', 'account'), + uuid: readString(data, 'uuid'), + accountId: readString(data, 'account_id'), + displayName: readString(data, 'display_name'), + createdOn: readString(data, 'created_on'), + selfUrl: linkHref(data, 'self'), + htmlUrl: linkHref(data, 'html'), + avatarUrl: linkHref(data, 'avatar'), + } +} + +export function normalizeBitbucketWorkspaceAccess(value: unknown): BitbucketWorkspaceAccess { + const data = requireResourceRecord(value, 'workspace access') + const workspace = readRecord(data, 'workspace') + return { + type: readRequiredString(data, 'type', 'workspace access'), + slug: readString(workspace, 'slug'), + uuid: readString(workspace, 'uuid'), + administrator: readBoolean(data, 'administrator'), + selfUrl: linkHref(workspace, 'self'), + avatarUrl: linkHref(workspace, 'avatar'), + } +} + +export function normalizeBitbucketRepository(value: unknown): BitbucketRepository { + const data = requireResourceRecord(value, 'repository') + const project = readRecord(data, 'project') + return { + type: readRequiredString(data, 'type', 'repository'), + uuid: readString(data, 'uuid'), + slug: readString(data, 'slug'), + name: readString(data, 'name'), + fullName: readString(data, 'full_name'), + description: readString(data, 'description'), + isPrivate: readBoolean(data, 'is_private'), + scm: readString(data, 'scm'), + language: readString(data, 'language'), + size: readNumber(data, 'size'), + createdOn: readString(data, 'created_on'), + updatedOn: readString(data, 'updated_on'), + mainBranch: readString(readRecord(data, 'mainbranch'), 'name'), + owner: normalizeBitbucketUser(data?.owner), + project: project + ? { + uuid: readString(project, 'uuid'), + key: readString(project, 'key'), + name: readString(project, 'name'), + } + : null, + selfUrl: linkHref(data, 'self'), + htmlUrl: linkHref(data, 'html'), + } +} + +export function normalizeBitbucketCommit(value: unknown): BitbucketCommit { + const data = requireResourceRecord(value, 'commit') + const author = readRecord(data, 'author') + const committer = readRecord(data, 'committer') + const parents = readOptionalArray(data, 'parents', 'commit') + return { + type: readRequiredString(data, 'type', 'commit'), + hash: readString(data, 'hash'), + date: readString(data, 'date'), + message: readString(data, 'message'), + summary: readString(readRecord(data, 'summary'), 'raw'), + authorRaw: readString(author, 'raw'), + author: normalizeBitbucketUser(author?.user), + committerRaw: readString(committer, 'raw'), + committer: normalizeBitbucketUser(committer?.user), + parents: + parents?.map((parent, index) => { + const parentData = requireResourceRecord(parent, `commit.parents[${index}]`) + return { hash: readString(parentData, 'hash') } + }) ?? null, + selfUrl: linkHref(data, 'self'), + htmlUrl: linkHref(data, 'html'), + } +} + +export function normalizeBitbucketBranch(value: unknown): BitbucketBranch { + const data = requireResourceRecord(value, 'branch') + const target = readRecord(data, 'target') + const mergeStrategies = readOptionalArray(data, 'merge_strategies', 'branch') + return { + type: readRequiredString(data, 'type', 'branch'), + name: readString(data, 'name'), + target: target ? normalizeBitbucketCommit(target) : null, + mergeStrategies: + mergeStrategies?.map((strategy, index) => { + if (typeof strategy !== 'string') { + throw new Error(`Bitbucket branch.merge_strategies[${index}] must be a string`) + } + return strategy + }) ?? null, + defaultMergeStrategy: readString(data, 'default_merge_strategy'), + selfUrl: linkHref(data, 'self'), + htmlUrl: linkHref(data, 'html'), + } +} + +export function normalizeBitbucketDirectoryEntry(value: unknown): BitbucketDirectoryEntry { + const data = requireResourceRecord(value, 'directory entry') + const attributes = readOptionalStringArray(data, 'attributes', 'directory entry') + return { + type: readRequiredString(data, 'type', 'directory entry'), + path: readString(data, 'path'), + commitHash: readString(readRecord(data, 'commit'), 'hash'), + size: readNumber(data, 'size'), + attributes, + isBinary: attributes?.includes('binary') ?? null, + selfUrl: linkHref(data, 'self'), + metadataUrl: linkHref(data, 'meta'), + } +} + +export function normalizeBitbucketFileMetadata(value: unknown): BitbucketFileMetadata { + const data = requireResourceRecord(value, 'file metadata') + const type = readRequiredString(data, 'type', 'file metadata') + if (type === 'commit_directory') { + throw new Error('Bitbucket source path is a directory; use list_directory instead') + } + if (type !== 'commit_file') { + throw new Error('Bitbucket file metadata.type must be commit_file') + } + const attributes = readOptionalStringArray(data, 'attributes', 'file metadata') + return { + type, + path: readString(data, 'path'), + commitHash: readString(readRecord(data, 'commit'), 'hash'), + escapedPath: readString(data, 'escaped_path'), + size: readNullableNumber(data, 'size', 'file metadata'), + attributes, + isBinary: attributes?.includes('binary') ?? null, + } +} + +function normalizePullRequestEndpoint(value: unknown): BitbucketPullRequestEndpoint | null { + const data = asRecord(value) + if (!data) return null + return { + branchName: readString(readRecord(data, 'branch'), 'name'), + commitHash: readString(readRecord(data, 'commit'), 'hash'), + repositoryUuid: readString(readRecord(data, 'repository'), 'uuid'), + repositoryFullName: readString(readRecord(data, 'repository'), 'full_name'), + } +} + +export function normalizeBitbucketParticipant(value: unknown): BitbucketParticipant { + const data = requireResourceRecord(value, 'participant') + return { + type: readRequiredString(data, 'type', 'participant'), + user: normalizeBitbucketUser(data?.user), + role: readString(data, 'role'), + approved: readBoolean(data, 'approved'), + state: readString(data, 'state'), + participatedOn: readString(data, 'participated_on'), + } +} + +export function normalizeBitbucketPullRequest(value: unknown): BitbucketPullRequest { + const data = requireResourceRecord(value, 'pull request') + const renderedDescription = readRecord(readRecord(data, 'rendered'), 'description') + const reviewers = readOptionalArray(data, 'reviewers', 'pull request') + const participants = readOptionalArray(data, 'participants', 'pull request') + return { + type: readRequiredString(data, 'type', 'pull request'), + id: readNumber(data, 'id'), + title: readString(data, 'title'), + description: + readString(data, 'description') ?? + readString(renderedDescription, 'raw') ?? + readString(readRecord(data, 'summary'), 'raw'), + state: readString(data, 'state'), + draft: readBoolean(data, 'draft'), + queued: readBoolean(data, 'queued'), + author: normalizeBitbucketUser(data?.author), + closedBy: normalizeBitbucketUser(data?.closed_by), + source: normalizePullRequestEndpoint(data?.source), + destination: normalizePullRequestEndpoint(data?.destination), + mergeCommitHash: readString(readRecord(data, 'merge_commit'), 'hash'), + commentCount: readNumber(data, 'comment_count'), + taskCount: readNumber(data, 'task_count'), + closeSourceBranch: readBoolean(data, 'close_source_branch'), + reason: readString(data, 'reason'), + createdOn: readString(data, 'created_on'), + updatedOn: readString(data, 'updated_on'), + reviewers: + reviewers?.map((reviewer, index) => { + const normalized = normalizeBitbucketUser(reviewer) + if (!normalized) { + throw new Error(`Bitbucket pull request.reviewers[${index}] must be an account object`) + } + return normalized + }) ?? null, + participants: participants?.map(normalizeBitbucketParticipant) ?? null, + selfUrl: linkHref(data, 'self'), + htmlUrl: linkHref(data, 'html'), + } +} + +export function normalizeBitbucketComment(value: unknown): BitbucketComment { + const data = requireResourceRecord(value, 'comment') + const inline = readRecord(data, 'inline') + const resolution = readRecord(data, 'resolution') + return { + type: readRequiredString(data, 'type', 'comment'), + id: readNumber(data, 'id'), + createdOn: readString(data, 'created_on'), + updatedOn: readString(data, 'updated_on'), + content: readString(readRecord(data, 'content'), 'raw'), + user: normalizeBitbucketUser(data?.user), + deleted: readBoolean(data, 'deleted'), + parentId: readNumber(readRecord(data, 'parent'), 'id'), + inline: inline + ? { + path: readString(inline, 'path'), + from: readNumber(inline, 'from'), + to: readNumber(inline, 'to'), + startFrom: readNumber(inline, 'start_from'), + startTo: readNumber(inline, 'start_to'), + } + : null, + pending: readBoolean(data, 'pending'), + resolution: resolution + ? { + resolver: normalizeBitbucketUser(resolution.user), + resolvedOn: readString(resolution, 'created_on'), + } + : null, + selfUrl: linkHref(data, 'self'), + htmlUrl: linkHref(data, 'html'), + } +} + +export function normalizeBitbucketCommitStatus(value: unknown): BitbucketCommitStatus { + const data = requireResourceRecord(value, 'commit status') + return { + type: readRequiredString(data, 'type', 'commit status'), + key: readRequiredString(data, 'key', 'commit status'), + refName: readString(data, 'refname'), + url: readString(data, 'url'), + state: readRequiredString(data, 'state', 'commit status'), + name: readString(data, 'name'), + description: readString(data, 'description'), + createdOn: readString(data, 'created_on'), + updatedOn: readString(data, 'updated_on'), + selfUrl: linkHref(data, 'self'), + commitUrl: linkHref(data, 'commit'), + } +} + +export function normalizeBitbucketDiffstat(value: unknown): BitbucketDiffstat { + const data = requireResourceRecord(value, 'diffstat') + const oldFile = readRecord(data, 'old') + const newFile = readRecord(data, 'new') + return { + type: readRequiredString(data, 'type', 'diffstat'), + status: readString(data, 'status'), + linesAdded: readNumber(data, 'lines_added'), + linesRemoved: readNumber(data, 'lines_removed'), + oldPath: readString(oldFile, 'path'), + newPath: readString(newFile, 'path'), + oldCommitHash: readString(readRecord(oldFile, 'commit'), 'hash'), + newCommitHash: readString(readRecord(newFile, 'commit'), 'hash'), + } +} + +export function normalizeBitbucketPipeline(value: unknown): BitbucketPipeline { + const data = requireResourceRecord(value, 'pipeline') + const target = readRecord(data, 'target') + const selector = readRecord(target, 'selector') + const state = readRecord(data, 'state') + const stateError = readRecord(readRecord(state, 'result'), 'error') + return { + type: readRequiredString(data, 'type', 'pipeline'), + uuid: readString(data, 'uuid'), + buildNumber: readNumber(data, 'build_number'), + creator: normalizeBitbucketUser(data?.creator), + repositoryFullName: readString(readRecord(data, 'repository'), 'full_name'), + target: target + ? { + type: readString(target, 'type'), + refType: readString(target, 'ref_type'), + refName: readString(target, 'ref_name'), + commitHash: readString(readRecord(target, 'commit'), 'hash'), + selectorType: readString(selector, 'type'), + selectorPattern: readString(selector, 'pattern'), + } + : null, + triggerType: readString(readRecord(data, 'trigger'), 'type'), + state: state + ? { + name: readString(state, 'name'), + stage: readString(readRecord(state, 'stage'), 'name'), + result: readString(readRecord(state, 'result'), 'name'), + errorKey: readString(stateError, 'key'), + errorMessage: readString(stateError, 'message'), + } + : null, + createdOn: readString(data, 'created_on'), + completedOn: readString(data, 'completed_on'), + buildSecondsUsed: readNumber(data, 'build_seconds_used'), + selfUrl: linkHref(data, 'self'), + stepsUrl: linkHref(data, 'steps'), + } +} + +function normalizePipelineCommands( + data: JsonRecord, + key: 'setup_commands' | 'script_commands' +): Array<{ + name: string | null + command: string | null +}> | null { + const commands = readOptionalArray(data, key, 'pipeline step') + return ( + commands?.map((command, index) => { + const commandData = asRecord(command) + if (!commandData) { + throw new Error(`Bitbucket pipeline step.${key}[${index}] must be an object`) + } + return { + name: readString(commandData, 'name'), + command: readString(commandData, 'command'), + } + }) ?? null + ) +} + +export function normalizeBitbucketPipelineStep(value: unknown): BitbucketPipelineStep { + const data = requireResourceRecord(value, 'pipeline step') + const state = readRecord(data, 'state') + const stateError = readRecord(readRecord(state, 'result'), 'error') + return { + type: readRequiredString(data, 'type', 'pipeline step'), + uuid: readString(data, 'uuid'), + startedOn: readString(data, 'started_on'), + completedOn: readString(data, 'completed_on'), + state: state + ? { + name: readString(state, 'name'), + result: readString(readRecord(state, 'result'), 'name'), + errorKey: readString(stateError, 'key'), + errorMessage: readString(stateError, 'message'), + } + : null, + imageName: readString(readRecord(data, 'image'), 'name'), + setupCommands: normalizePipelineCommands(data, 'setup_commands'), + scriptCommands: normalizePipelineCommands(data, 'script_commands'), + } +} + +interface ParsedContentRange { + start: number + end: number + total: number | null +} + +function parseContentRange(header: string | null): ParsedContentRange | null { + const match = header?.match(/^bytes\s+(\d+)-(\d+)\/(\d+|\*)$/) + if (!match) return null + const start = Number(match[1]) + const end = Number(match[2]) + const total = match[3] === '*' ? null : Number(match[3]) + if ( + !Number.isSafeInteger(start) || + !Number.isSafeInteger(end) || + start < 0 || + end < start || + (total !== null && (!Number.isSafeInteger(total) || total < 1 || end >= total)) + ) { + return null + } + return { start, end, total } +} + +function requireContentRange(response: Response): ParsedContentRange { + const range = parseContentRange(response.headers.get('content-range')) + if (!range) { + throw new Error('Bitbucket 206 response must include a valid Content-Range header') + } + const expectedBytes = range.end - range.start + 1 + const contentLength = parseByteCount(response.headers.get('content-length')) + if (contentLength !== null && contentLength !== expectedBytes) { + throw new Error('Bitbucket 206 Content-Length does not match Content-Range') + } + return range +} + +function assertContentRangeBody( + range: ParsedContentRange, + byteLength: number, + clipped: boolean +): void { + if (clipped || byteLength !== range.end - range.start + 1) { + throw new Error('Bitbucket 206 response body does not match Content-Range') + } +} + +function parseByteCount(value: string | null): number | null { + if (!value || !/^\d+$/.test(value)) return null + const parsed = Number(value) + return Number.isSafeInteger(parsed) ? parsed : null +} + +async function readBoundedBytes( + response: Response, + maxBytes: number +): Promise<{ bytes: Uint8Array; clipped: boolean }> { + if (!response.body) return { bytes: new Uint8Array(), clipped: false } + const reader = response.body.getReader() + const chunks: Uint8Array[] = [] + let kept = 0 + let clipped = false + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + const remaining = maxBytes - kept + if (remaining <= 0) { + clipped = true + await reader.cancel() + break + } + if (value.byteLength > remaining) { + chunks.push(value.slice(0, remaining)) + kept += remaining + clipped = true + await reader.cancel() + break + } + chunks.push(value) + kept += value.byteLength + } + } finally { + reader.releaseLock() + } + const bytes = new Uint8Array(kept) + let offset = 0 + for (const chunk of chunks) { + bytes.set(chunk, offset) + offset += chunk.byteLength + } + return { bytes, clipped } +} + +async function readRollingTailBytes( + response: Response, + maxBytes: number +): Promise<{ bytes: Uint8Array; totalBytes: number }> { + if (!response.body) return { bytes: new Uint8Array(), totalBytes: 0 } + const reader = response.body.getReader() + let tail = new Uint8Array() + let totalBytes = 0 + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + totalBytes += value.byteLength + if (value.byteLength >= maxBytes) { + tail = value.slice(value.byteLength - maxBytes) + continue + } + const keepFromTail = Math.min(tail.byteLength, maxBytes - value.byteLength) + const combined = new Uint8Array(keepFromTail + value.byteLength) + combined.set(tail.slice(tail.byteLength - keepFromTail), 0) + combined.set(value, keepFromTail) + tail = combined + } + } finally { + reader.releaseLock() + } + return { bytes: tail, totalBytes } +} + +function decodeUtf8Tail(bytes: Uint8Array, mayStartMidCharacter: boolean): string { + const maxOffset = mayStartMidCharacter ? Math.min(3, bytes.byteLength) : 0 + for (let offset = 0; offset <= maxOffset; offset++) { + try { + return new TextDecoder('utf-8', { fatal: true }).decode(bytes.slice(offset)) + } catch {} + } + throw new Error('Bitbucket pipeline log is not valid UTF-8 text') +} + +function decodeUtf8Prefix( + bytes: Uint8Array, + allowTrailingIncomplete: boolean, + allowLossy: boolean +): { text: string; lossy: boolean } { + try { + return { + text: new TextDecoder('utf-8', { fatal: true }).decode(bytes, { + stream: allowTrailingIncomplete, + }), + lossy: false, + } + } catch { + if (!allowLossy) { + throw new Error('Bitbucket file text is not valid UTF-8; inspect file metadata instead') + } + return { + text: new TextDecoder('utf-8').decode(bytes, { stream: allowTrailingIncomplete }), + lossy: true, + } + } +} + +export async function bitbucketRawHead( + response: Response, + maxCharacters: number | undefined, + binary: boolean | null, + options: { allowLossyUtf8?: boolean } = {} +): Promise<{ + content: string | null + binary: boolean | null + truncated: boolean | null + returnedBytes: number + fullBytes: number | null + contentType: string | null + decodingLossy?: boolean +}> { + const limit = bitbucketMaxCharacters(maxCharacters) + const range = response.status === 206 ? requireContentRange(response) : null + if (range && range.start !== 0) { + throw new Error('Bitbucket 206 prefix response must start at byte 0') + } + const { bytes, clipped } = await readBoundedBytes(response, limit * 4) + if (range) assertContentRangeBody(range, bytes.byteLength, clipped) + const partial = range !== null && (range.total === null || range.end + 1 < range.total) + let text: string | null = null + let decodingLossy = false + const effectiveBinary = binary === null && bytes.includes(0) ? true : binary + if (effectiveBinary !== true) { + const decoded = decodeUtf8Prefix(bytes, clipped || partial, options.allowLossyUtf8 === true) + text = decoded.text + decodingLossy = decoded.lossy + } + const contentLength = parseByteCount(response.headers.get('content-length')) + const fullBytes = + response.status === 206 + ? (range?.total ?? null) + : (contentLength ?? (clipped ? null : bytes.byteLength)) + return { + content: text?.slice(0, limit) ?? null, + binary: effectiveBinary, + truncated: + effectiveBinary === true + ? fullBytes === null + ? null + : fullBytes > 0 + : clipped || partial || (text !== null && text.length > limit), + returnedBytes: bytes.byteLength, + fullBytes, + contentType: response.headers.get('content-type'), + ...(options.allowLossyUtf8 ? { decodingLossy } : {}), + } +} + +export async function bitbucketRawTail( + response: Response, + maxCharacters: number | undefined +): Promise<{ log: string; truncated: boolean; totalBytes: number | null }> { + const limit = bitbucketMaxCharacters(maxCharacters, true) + const range = response.status === 206 ? requireContentRange(response) : null + if (range && (range.total === null || range.end !== range.total - 1)) { + throw new Error('Bitbucket 206 log response must describe a suffix of the complete log') + } + const byteLimit = limit * 4 + const read = + response.status === 206 + ? await readBoundedBytes(response, byteLimit) + : await readRollingTailBytes(response, byteLimit) + const clipped = 'clipped' in read ? read.clipped : read.totalBytes > byteLimit + if (range) assertContentRangeBody(range, read.bytes.byteLength, clipped) + const partialStart = (range !== null && range.start > 0) || clipped + const text = decodeUtf8Tail(read.bytes, partialStart) + const firstBreak = partialStart ? text.indexOf('\n') : -1 + const completeLines = firstBreak === -1 ? text : text.slice(firstBreak + 1) + return { + log: completeLines.slice(-limit), + truncated: partialStart || completeLines.length > limit, + totalBytes: + response.status === 206 + ? (range?.total ?? null) + : (parseByteCount(response.headers.get('content-length')) ?? + ('totalBytes' in read ? read.totalBytes : read.bytes.byteLength)), + } +} diff --git a/apps/sim/tools/bitbucket/validation.ts b/apps/sim/tools/bitbucket/validation.ts new file mode 100644 index 00000000000..a27c58b4bc2 --- /dev/null +++ b/apps/sim/tools/bitbucket/validation.ts @@ -0,0 +1,78 @@ +const BITBUCKET_SHA1_PATTERN = /^[0-9a-f]{40}$/i + +export function optionalBitbucketBoolean(value: unknown, name: string): boolean | undefined { + if (value === undefined) return undefined + if (typeof value !== 'boolean') throw new Error(`${name} must be a boolean`) + return value +} + +export function optionalBitbucketEnum( + value: unknown, + name: string, + allowed: readonly T[] +): T | undefined { + if (value === undefined) return undefined + if (typeof value !== 'string' || !allowed.includes(value as T)) { + throw new Error(`${name} must be one of: ${allowed.join(', ')}`) + } + return value as T +} + +export function requireBitbucketEnum( + value: unknown, + name: string, + allowed: readonly T[] +): T { + const validated = optionalBitbucketEnum(value, name, allowed) + if (validated === undefined) throw new Error(`${name} is required`) + return validated +} + +export function optionalBitbucketStringArray( + value: unknown, + name: string, + itemName: string +): string[] | undefined { + if (value === undefined) return undefined + if (!Array.isArray(value)) throw new Error(`${name} must be an array of strings`) + return value.map((item) => { + if (typeof item !== 'string' || item.trim().length === 0) { + throw new Error(`${itemName} must be a non-empty string`) + } + return item.trim() + }) +} + +export function requireBitbucketPositiveInteger(value: unknown, name: string): number { + if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1) { + throw new Error(`${name} must be a positive integer`) + } + return value +} + +export function requireBitbucketSha1(value: unknown, name: string): string { + if (typeof value !== 'string') throw new Error(`${name} must be a full 40-character SHA-1`) + const normalized = value.trim() + if (!BITBUCKET_SHA1_PATTERN.test(normalized)) { + throw new Error(`${name} must be a full 40-character SHA-1`) + } + return normalized.toLowerCase() +} + +export function optionalBitbucketSha1(value: unknown, name: string): string | undefined { + if (value === undefined) return undefined + return requireBitbucketSha1(value, name) +} + +export function optionalBitbucketUtf8String( + value: unknown, + name: string, + maxBytes: number +): string | undefined { + if (value === undefined) return undefined + if (typeof value !== 'string') throw new Error(`${name} must be a string`) + if (new TextEncoder().encode(value).byteLength > maxBytes) { + throw new Error(`${name} must not exceed ${maxBytes} UTF-8 bytes`) + } + return value +} diff --git a/apps/sim/tools/generated/tool-ids.ts b/apps/sim/tools/generated/tool-ids.ts index d8322a70ea7..79f34867780 100644 --- a/apps/sim/tools/generated/tool-ids.ts +++ b/apps/sim/tools/generated/tool-ids.ts @@ -3,7 +3,7 @@ /** Every registered tool id, including versioned variants. */ const toolIds: string[] = JSON.parse( - '["a2a_cancel_task","a2a_get_agent_card","a2a_get_task","a2a_send_message","agentmail_create_draft","agentmail_create_inbox","agentmail_delete_draft","agentmail_delete_inbox","agentmail_delete_thread","agentmail_forward_message","agentmail_get_draft","agentmail_get_inbox","agentmail_get_message","agentmail_get_thread","agentmail_list_drafts","agentmail_list_inboxes","agentmail_list_messages","agentmail_list_threads","agentmail_reply_message","agentmail_send_draft","agentmail_send_message","agentmail_update_draft","agentmail_update_inbox","agentmail_update_message","agentmail_update_thread","agentphone_create_call","agentphone_create_contact","agentphone_create_number","agentphone_delete_contact","agentphone_get_call","agentphone_get_call_transcript","agentphone_get_contact","agentphone_get_conversation","agentphone_get_conversation_messages","agentphone_get_number_messages","agentphone_get_usage","agentphone_get_usage_daily","agentphone_get_usage_monthly","agentphone_list_calls","agentphone_list_contacts","agentphone_list_conversations","agentphone_list_numbers","agentphone_react_to_message","agentphone_release_number","agentphone_send_message","agentphone_update_contact","agentphone_update_conversation","agiloft_async_status","agiloft_attach_file","agiloft_attachment_info","agiloft_create_record","agiloft_delete_record","agiloft_get_choice_line_id","agiloft_list_tables","agiloft_lock_record","agiloft_nlp_search","agiloft_read_record","agiloft_remove_attachment","agiloft_retrieve_attachment","agiloft_run_action_button","agiloft_saved_search","agiloft_search_records","agiloft_select_records","agiloft_update_record","agiloft_upsert_record","ahrefs_anchors","ahrefs_backlinks","ahrefs_backlinks_stats","ahrefs_batch_analysis","ahrefs_broken_backlinks","ahrefs_domain_rating","ahrefs_domain_rating_history","ahrefs_keyword_overview","ahrefs_keywords_history","ahrefs_metrics","ahrefs_metrics_history","ahrefs_organic_competitors","ahrefs_organic_keywords","ahrefs_paid_pages","ahrefs_rank_tracker_competitors_overview","ahrefs_rank_tracker_competitors_stats","ahrefs_rank_tracker_overview","ahrefs_rank_tracker_serp_overview","ahrefs_refdomains_history","ahrefs_referring_domains","ahrefs_related_terms","ahrefs_site_audit_page_explorer","ahrefs_top_pages","airtable_create_records","airtable_delete_records","airtable_get_base_schema","airtable_get_record","airtable_list_bases","airtable_list_records","airtable_list_tables","airtable_update_multiple_records","airtable_update_record","airtable_upsert_records","airweave_search","algolia_add_record","algolia_batch_operations","algolia_browse_records","algolia_clear_records","algolia_copy_move_index","algolia_delete_by_filter","algolia_delete_index","algolia_delete_record","algolia_get_record","algolia_get_records","algolia_get_settings","algolia_get_task_status","algolia_list_indices","algolia_partial_update_record","algolia_search","algolia_update_settings","amplitude_event_segmentation","amplitude_funnels","amplitude_get_active_users","amplitude_get_revenue","amplitude_group_identify","amplitude_identify_user","amplitude_list_events","amplitude_realtime_active_users","amplitude_retention","amplitude_send_event","amplitude_user_activity","amplitude_user_profile","amplitude_user_search","apify_get_dataset_items","apify_get_run","apify_run_actor_async","apify_run_actor_sync","apify_run_task","apollo_account_bulk_create","apollo_account_bulk_update","apollo_account_create","apollo_account_search","apollo_account_update","apollo_contact_bulk_create","apollo_contact_bulk_update","apollo_contact_create","apollo_contact_search","apollo_contact_update","apollo_email_accounts","apollo_opportunity_create","apollo_opportunity_get","apollo_opportunity_search","apollo_opportunity_update","apollo_organization_bulk_enrich","apollo_organization_enrich","apollo_organization_search","apollo_people_bulk_enrich","apollo_people_enrich","apollo_people_search","apollo_sequence_add_contacts","apollo_sequence_search","apollo_task_create","apollo_task_search","appconfig_create_application","appconfig_create_configuration_profile","appconfig_create_environment","appconfig_create_hosted_configuration_version","appconfig_delete_application","appconfig_delete_configuration_profile","appconfig_delete_environment","appconfig_delete_hosted_configuration_version","appconfig_get_application","appconfig_get_configuration","appconfig_get_configuration_profile","appconfig_get_deployment","appconfig_get_environment","appconfig_get_hosted_configuration_version","appconfig_list_applications","appconfig_list_configuration_profiles","appconfig_list_deployment_strategies","appconfig_list_deployments","appconfig_list_environments","appconfig_list_hosted_configuration_versions","appconfig_start_deployment","appconfig_stop_deployment","appconfig_update_application","appconfig_update_configuration_profile","appconfig_update_environment","arxiv_get_author_papers","arxiv_get_paper","arxiv_search","asana_add_comment","asana_add_followers","asana_create_project","asana_create_section","asana_create_subtask","asana_create_task","asana_delete_task","asana_get_project","asana_get_projects","asana_get_task","asana_list_sections","asana_list_workspaces","asana_search_tasks","asana_update_task","ashby_add_candidate_tag","ashby_anonymize_candidate","ashby_change_application_source","ashby_change_application_stage","ashby_create_application","ashby_create_candidate","ashby_create_note","ashby_delete_application","ashby_get_application","ashby_get_candidate","ashby_get_job","ashby_get_job_posting","ashby_get_offer","ashby_list_applications","ashby_list_archive_reasons","ashby_list_candidate_tags","ashby_list_candidates","ashby_list_custom_fields","ashby_list_departments","ashby_list_interviews","ashby_list_job_postings","ashby_list_jobs","ashby_list_locations","ashby_list_notes","ashby_list_offers","ashby_list_openings","ashby_list_sources","ashby_list_users","ashby_remove_candidate_tag","ashby_search_candidates","ashby_set_custom_field_value","ashby_set_custom_field_values","ashby_update_candidate","athena_batch_get_query_execution","athena_create_named_query","athena_delete_named_query","athena_get_named_query","athena_get_query_execution","athena_get_query_results","athena_list_databases","athena_list_named_queries","athena_list_query_executions","athena_list_table_metadata","athena_start_query","athena_stop_query","attio_assert_record","attio_create_attribute","attio_create_comment","attio_create_list","attio_create_list_entry","attio_create_note","attio_create_object","attio_create_record","attio_create_task","attio_create_webhook","attio_delete_comment","attio_delete_list_entry","attio_delete_note","attio_delete_record","attio_delete_task","attio_delete_webhook","attio_get_attribute","attio_get_comment","attio_get_list","attio_get_list_entry","attio_get_member","attio_get_note","attio_get_object","attio_get_record","attio_get_task","attio_get_thread","attio_get_webhook","attio_list_attributes","attio_list_lists","attio_list_members","attio_list_notes","attio_list_objects","attio_list_records","attio_list_tasks","attio_list_threads","attio_list_webhooks","attio_query_list_entries","attio_search_records","attio_update_attribute","attio_update_list","attio_update_list_entry","attio_update_object","attio_update_record","attio_update_task","attio_update_webhook","azure_data_explorer_create_table","azure_data_explorer_drop_table","azure_data_explorer_ingest_from_query","azure_data_explorer_ingest_inline","azure_data_explorer_list_databases","azure_data_explorer_list_functions","azure_data_explorer_list_tables","azure_data_explorer_management","azure_data_explorer_query","azure_data_explorer_show_database_schema","azure_data_explorer_show_ingestion_failures","azure_data_explorer_show_operations","azure_data_explorer_show_table_details","azure_data_explorer_show_table_schema","azure_devops_add_comment","azure_devops_create_work_item","azure_devops_get_build_log","azure_devops_get_build_timeline","azure_devops_get_comments","azure_devops_get_pipeline","azure_devops_get_pipeline_run","azure_devops_get_work_item","azure_devops_get_work_items_batch","azure_devops_get_work_items_between_builds","azure_devops_list_build_logs","azure_devops_list_builds","azure_devops_list_pipeline_runs","azure_devops_list_pipelines","azure_devops_query_work_items","azure_devops_update_work_item","box_copy_file","box_create_folder","box_delete_file","box_delete_folder","box_download_file","box_get_file_info","box_list_folder_items","box_search","box_sign_cancel_request","box_sign_create_request","box_sign_get_request","box_sign_list_requests","box_sign_resend_request","box_update_file","box_upload_file","brandfetch_get_brand","brandfetch_search","brex_archive_budget","brex_create_budget","brex_create_spend_limit","brex_create_transfer","brex_create_vendor","brex_get_budget","brex_get_cash_account","brex_get_company","brex_get_current_user","brex_get_expense","brex_get_spend_limit","brex_get_transfer","brex_get_user","brex_get_vendor","brex_list_budgets","brex_list_card_accounts","brex_list_card_statements","brex_list_card_transactions","brex_list_cards","brex_list_cash_accounts","brex_list_cash_statements","brex_list_cash_transactions","brex_list_departments","brex_list_expenses","brex_list_locations","brex_list_spend_limits","brex_list_titles","brex_list_transfers","brex_list_users","brex_list_vendors","brex_match_receipt","brex_update_expense","brex_update_vendor","brex_upload_receipt","brightdata_cancel_snapshot","brightdata_discover","brightdata_download_snapshot","brightdata_scrape_dataset","brightdata_scrape_url","brightdata_serp_search","brightdata_snapshot_status","brightdata_sync_scrape","browser_use_run_task","buffer_create_idea","buffer_create_post","buffer_delete_post","buffer_edit_post","buffer_get_account","buffer_get_channels","buffer_get_idea_groups","buffer_get_ideas","buffer_get_post","buffer_get_posts","calcom_cancel_booking","calcom_confirm_booking","calcom_create_booking","calcom_create_event_type","calcom_create_schedule","calcom_decline_booking","calcom_delete_event_type","calcom_delete_schedule","calcom_get_booking","calcom_get_default_schedule","calcom_get_event_type","calcom_get_schedule","calcom_get_slots","calcom_list_bookings","calcom_list_event_types","calcom_list_schedules","calcom_reschedule_booking","calcom_update_event_type","calcom_update_schedule","calendly_cancel_event","calendly_create_event_invitee","calendly_create_invitee_no_show","calendly_create_scheduling_link","calendly_create_webhook","calendly_delete_invitee_no_show","calendly_delete_webhook","calendly_get_current_user","calendly_get_event_invitee","calendly_get_event_type","calendly_get_scheduled_event","calendly_get_user","calendly_list_event_invitees","calendly_list_event_type_available_times","calendly_list_event_types","calendly_list_organization_memberships","calendly_list_routing_form_submissions","calendly_list_routing_forms","calendly_list_scheduled_events","calendly_list_user_availability_schedules","calendly_list_user_busy_times","calendly_list_webhooks","cbinsights_chat","cbinsights_get_commercial_maturity_history","cbinsights_get_exit_probability_history","cbinsights_get_mosaic_history","cbinsights_get_org_business_relationships","cbinsights_get_org_funding_window","cbinsights_get_org_fundings","cbinsights_get_org_investments","cbinsights_get_org_management_and_board","cbinsights_get_org_outlook","cbinsights_get_org_portfolio_exits","cbinsights_get_org_revenue","cbinsights_get_scouting_report","cbinsights_get_strategy_map","cbinsights_list_business_relationships","cbinsights_list_funding_window","cbinsights_list_fundings","cbinsights_list_investments","cbinsights_list_management_and_board","cbinsights_list_outlook","cbinsights_list_portfolio_exits","cbinsights_list_revenue","cbinsights_lookup_organizations","cbinsights_rag","cbinsights_search_firmographics","clay_populate","clerk_add_organization_member","clerk_ban_user","clerk_create_actor_token","clerk_create_allowlist_identifier","clerk_create_blocklist_identifier","clerk_create_organization","clerk_create_organization_invitation","clerk_create_user","clerk_delete_allowlist_identifier","clerk_delete_blocklist_identifier","clerk_delete_organization","clerk_delete_user","clerk_get_jwt_template","clerk_get_organization","clerk_get_session","clerk_get_user","clerk_get_user_oauth_token","clerk_list_allowlist_identifiers","clerk_list_blocklist_identifiers","clerk_list_jwt_templates","clerk_list_organization_invitations","clerk_list_organization_memberships","clerk_list_organizations","clerk_list_sessions","clerk_list_users","clerk_lock_user","clerk_remove_organization_member","clerk_revoke_actor_token","clerk_revoke_session","clerk_unban_user","clerk_unlock_user","clerk_update_organization","clerk_update_organization_membership","clerk_update_user","clickhouse_count_rows","clickhouse_create_database","clickhouse_create_table","clickhouse_delete","clickhouse_describe_table","clickhouse_drop_database","clickhouse_drop_partition","clickhouse_drop_table","clickhouse_execute","clickhouse_insert","clickhouse_insert_rows","clickhouse_introspect","clickhouse_kill_query","clickhouse_list_clusters","clickhouse_list_databases","clickhouse_list_mutations","clickhouse_list_partitions","clickhouse_list_running_queries","clickhouse_list_tables","clickhouse_optimize_table","clickhouse_query","clickhouse_rename_table","clickhouse_show_create_table","clickhouse_table_stats","clickhouse_truncate_table","clickhouse_update","clickup_add_tag_to_task","clickup_create_checklist","clickup_create_checklist_item","clickup_create_comment","clickup_create_folder","clickup_create_list","clickup_create_task","clickup_create_time_entry","clickup_delete_checklist","clickup_delete_checklist_item","clickup_delete_comment","clickup_delete_task","clickup_delete_time_entry","clickup_get_comments","clickup_get_custom_fields","clickup_get_folders","clickup_get_list_members","clickup_get_lists","clickup_get_running_timer","clickup_get_space_tags","clickup_get_spaces","clickup_get_task","clickup_get_task_members","clickup_get_tasks","clickup_get_time_entries","clickup_get_workspaces","clickup_remove_custom_field_value","clickup_remove_tag_from_task","clickup_search_tasks","clickup_set_custom_field_value","clickup_start_timer","clickup_stop_timer","clickup_update_checklist","clickup_update_checklist_item","clickup_update_comment","clickup_update_task","clickup_update_time_entry","clickup_upload_attachment","cloudflare_create_access_application","cloudflare_create_access_policy","cloudflare_create_access_service_token","cloudflare_create_dns_record","cloudflare_create_r2_bucket","cloudflare_create_rate_limit_rule","cloudflare_create_ruleset","cloudflare_create_ruleset_rule","cloudflare_create_zone","cloudflare_delete_access_application","cloudflare_delete_access_policy","cloudflare_delete_dns_record","cloudflare_delete_r2_bucket","cloudflare_delete_ruleset_rule","cloudflare_delete_zone","cloudflare_dns_analytics","cloudflare_get_access_application","cloudflare_get_r2_bucket","cloudflare_get_ruleset","cloudflare_get_ruleset_entrypoint","cloudflare_get_tunnel","cloudflare_get_tunnel_configuration","cloudflare_get_worker_script_settings","cloudflare_get_zone","cloudflare_get_zone_settings","cloudflare_list_access_applications","cloudflare_list_access_groups","cloudflare_list_access_identity_providers","cloudflare_list_access_policies","cloudflare_list_access_service_tokens","cloudflare_list_certificates","cloudflare_list_dns_records","cloudflare_list_managed_ruleset_overrides","cloudflare_list_r2_buckets","cloudflare_list_rate_limit_rules","cloudflare_list_rulesets","cloudflare_list_tunnels","cloudflare_list_worker_routes","cloudflare_list_worker_scripts","cloudflare_list_zones","cloudflare_purge_cache","cloudflare_revoke_access_service_token","cloudflare_update_access_application","cloudflare_update_access_policy","cloudflare_update_dns_record","cloudflare_update_rate_limit_rule","cloudflare_update_ruleset_rule","cloudflare_update_zone_setting","cloudformation_cancel_update_stack","cloudformation_create_change_set","cloudformation_create_stack","cloudformation_delete_stack","cloudformation_describe_change_set","cloudformation_describe_stack_drift_detection_status","cloudformation_describe_stack_events","cloudformation_describe_stacks","cloudformation_detect_stack_drift","cloudformation_execute_change_set","cloudformation_get_template","cloudformation_get_template_summary","cloudformation_list_stack_resources","cloudformation_update_stack","cloudformation_validate_template","cloudwatch_describe_alarm_history","cloudwatch_describe_alarms","cloudwatch_describe_log_groups","cloudwatch_describe_log_streams","cloudwatch_filter_log_events","cloudwatch_get_log_events","cloudwatch_get_metric_statistics","cloudwatch_list_metrics","cloudwatch_mute_alarm","cloudwatch_put_log_group_retention","cloudwatch_put_metric_data","cloudwatch_query_logs","cloudwatch_unmute_alarm","codepipeline_disable_stage_transition","codepipeline_enable_stage_transition","codepipeline_get_pipeline","codepipeline_get_pipeline_execution","codepipeline_get_pipeline_state","codepipeline_list_action_executions","codepipeline_list_pipeline_executions","codepipeline_list_pipelines","codepipeline_put_approval_result","codepipeline_retry_stage_execution","codepipeline_start_execution","codepipeline_stop_execution","confluence_add_label","confluence_create_blogpost","confluence_create_comment","confluence_create_page","confluence_create_page_property","confluence_create_space","confluence_create_space_property","confluence_delete_attachment","confluence_delete_blogpost","confluence_delete_comment","confluence_delete_label","confluence_delete_page","confluence_delete_page_property","confluence_delete_space","confluence_delete_space_property","confluence_get_blogpost","confluence_get_page_ancestors","confluence_get_page_children","confluence_get_page_descendants","confluence_get_page_version","confluence_get_pages_by_label","confluence_get_space","confluence_get_task","confluence_get_user","confluence_list_attachments","confluence_list_blogposts","confluence_list_blogposts_in_space","confluence_list_comments","confluence_list_labels","confluence_list_page_properties","confluence_list_page_versions","confluence_list_pages_in_space","confluence_list_space_labels","confluence_list_space_permissions","confluence_list_space_properties","confluence_list_spaces","confluence_list_tasks","confluence_retrieve","confluence_search","confluence_search_in_space","confluence_update","confluence_update_blogpost","confluence_update_comment","confluence_update_space","confluence_update_task","confluence_upload_attachment","context_dev_classify_naics","context_dev_classify_sic","context_dev_crawl","context_dev_extract","context_dev_extract_product","context_dev_extract_products","context_dev_get_brand","context_dev_get_brand_by_email","context_dev_get_brand_by_name","context_dev_get_brand_by_ticker","context_dev_identify_transaction","context_dev_map","context_dev_scrape_fonts","context_dev_scrape_html","context_dev_scrape_images","context_dev_scrape_markdown","context_dev_scrape_styleguide","context_dev_screenshot","context_dev_search","convex_action","convex_document_deltas","convex_list_documents","convex_list_tables","convex_mutation","convex_query","convex_run_function","crowdstrike_create_indicators","crowdstrike_delete_indicators","crowdstrike_delete_rtr_session","crowdstrike_execute_rtr_command","crowdstrike_get_alert_details","crowdstrike_get_case_details","crowdstrike_get_host_group_details","crowdstrike_get_indicator_details","crowdstrike_get_rtr_command_status","crowdstrike_get_sensor_aggregates","crowdstrike_get_sensor_details","crowdstrike_get_vulnerability_details","crowdstrike_init_rtr_session","crowdstrike_perform_host_action","crowdstrike_perform_host_group_action","crowdstrike_query_alerts","crowdstrike_query_cases","crowdstrike_query_host_groups","crowdstrike_query_indicators","crowdstrike_query_sensors","crowdstrike_query_vulnerabilities","crowdstrike_update_alerts","crowdstrike_update_indicators","crunchbase_autocomplete","crunchbase_get_acquisition","crunchbase_get_entity","crunchbase_get_entity_card","crunchbase_get_fields_metadata","crunchbase_get_funding_round","crunchbase_get_organization","crunchbase_get_person","crunchbase_list_deleted_entities","crunchbase_search_acquisitions","crunchbase_search_entities","crunchbase_search_funding_rounds","crunchbase_search_organizations","crunchbase_search_people","cursor_add_followup","cursor_add_followup_v2","cursor_delete_agent","cursor_delete_agent_v2","cursor_download_artifact","cursor_download_artifact_v2","cursor_get_agent","cursor_get_agent_v2","cursor_get_api_key_info","cursor_get_api_key_info_v2","cursor_get_conversation","cursor_get_conversation_v2","cursor_launch_agent","cursor_launch_agent_v2","cursor_list_agents","cursor_list_agents_v2","cursor_list_artifacts","cursor_list_artifacts_v2","cursor_list_models","cursor_list_models_v2","cursor_list_repositories","cursor_list_repositories_v2","cursor_stop_agent","cursor_stop_agent_v2","dagster_delete_run","dagster_get_asset","dagster_get_run","dagster_get_run_logs","dagster_launch_run","dagster_list_assets","dagster_list_jobs","dagster_list_runs","dagster_list_schedules","dagster_list_sensors","dagster_materialize_assets","dagster_reexecute_run","dagster_report_asset_materialization","dagster_start_schedule","dagster_start_sensor","dagster_stop_schedule","dagster_stop_sensor","dagster_terminate_run","dagster_wipe_asset","databricks_cancel_run","databricks_execute_sql","databricks_get_cluster","databricks_get_job","databricks_get_run","databricks_get_run_output","databricks_get_statement","databricks_list_clusters","databricks_list_jobs","databricks_list_runs","databricks_list_warehouses","databricks_run_job","datadog_add_incident_todo","datadog_cancel_downtime","datadog_create_dashboard","datadog_create_downtime","datadog_create_event","datadog_create_incident","datadog_create_monitor","datadog_create_slo","datadog_delete_dashboard","datadog_delete_slo","datadog_get_browser_synthetics_results","datadog_get_dashboard","datadog_get_incident","datadog_get_monitor","datadog_get_security_signal","datadog_get_slo","datadog_get_slo_history","datadog_get_synthetics_results","datadog_get_synthetics_test","datadog_list_dashboards","datadog_list_downtimes","datadog_list_incidents","datadog_list_monitors","datadog_list_security_rules","datadog_list_security_signals","datadog_list_services","datadog_list_slos","datadog_list_synthetics_tests","datadog_mute_monitor","datadog_query_logs","datadog_query_timeseries","datadog_search_spans","datadog_send_logs","datadog_submit_metrics","datadog_trigger_synthetics_tests","datadog_unmute_monitor","datadog_update_incident","datadog_update_security_signal_assignee","datadog_update_security_signal_state","datadog_update_slo","datadog_update_synthetics_status","datagma_enrich_company","datagma_enrich_person","datagma_find_email","datagma_find_phone","datagma_get_credits","daytona_create_sandbox","daytona_delete_sandbox","daytona_download_file","daytona_execute_command","daytona_get_sandbox","daytona_git_clone","daytona_list_files","daytona_list_sandboxes","daytona_run_code","daytona_start_sandbox","daytona_stop_sandbox","daytona_upload_file","deployed_block_executor","deployments_deploy","deployments_get_version","deployments_list_versions","deployments_promote","deployments_undeploy","devin_append_session_tags","devin_archive_session","devin_create_session","devin_get_session","devin_get_session_tags","devin_list_session_attachments","devin_list_session_messages","devin_list_sessions","devin_replace_session_tags","devin_send_message","devin_terminate_session","discord_add_reaction","discord_archive_thread","discord_assign_role","discord_ban_member","discord_bulk_delete_messages","discord_create_channel","discord_create_invite","discord_create_role","discord_create_thread","discord_create_webhook","discord_delete_channel","discord_delete_invite","discord_delete_message","discord_delete_role","discord_delete_webhook","discord_edit_message","discord_execute_webhook","discord_get_channel","discord_get_invite","discord_get_member","discord_get_messages","discord_get_pinned_messages","discord_get_server","discord_get_user","discord_get_webhook","discord_join_thread","discord_kick_member","discord_leave_thread","discord_list_channels","discord_list_roles","discord_pin_message","discord_remove_reaction","discord_remove_role","discord_send_message","discord_unban_member","discord_unpin_message","discord_update_channel","discord_update_member","discord_update_role","docusign_create_from_template","docusign_download_document","docusign_get_envelope","docusign_list_envelopes","docusign_list_recipients","docusign_list_templates","docusign_send_envelope","docusign_void_envelope","downdetector_get_company","downdetector_get_company_attribution","downdetector_get_company_baseline","downdetector_get_company_events","downdetector_get_company_incidents","downdetector_get_company_indicators","downdetector_get_company_last_15","downdetector_get_company_status","downdetector_get_provider","downdetector_get_reports","downdetector_get_site_companies","downdetector_list_categories","downdetector_list_incidents","downdetector_list_sites","downdetector_search_companies","dropbox_copy","dropbox_create_folder","dropbox_create_shared_link","dropbox_delete","dropbox_download","dropbox_get_metadata","dropbox_list_folder","dropbox_list_revisions","dropbox_list_shared_links","dropbox_move","dropbox_restore","dropbox_search","dropbox_upload","dropcontact_enrich_contact","dspy_chain_of_thought","dspy_predict","dspy_react","dub_bulk_create_links","dub_bulk_delete_links","dub_bulk_update_links","dub_create_link","dub_create_tag","dub_delete_link","dub_get_analytics","dub_get_events","dub_get_link","dub_get_links_count","dub_get_qr_code","dub_list_domains","dub_list_folders","dub_list_links","dub_list_tags","dub_update_link","dub_upsert_link","duckduckgo_search","dynamodb_delete","dynamodb_get","dynamodb_introspect","dynamodb_put","dynamodb_query","dynamodb_scan","dynamodb_update","dynatrace_add_problem_comment","dynatrace_add_tags","dynatrace_close_problem","dynatrace_create_settings_object","dynatrace_create_slo","dynatrace_delete_problem_comment","dynatrace_delete_settings_object","dynatrace_delete_slo","dynatrace_delete_tag","dynatrace_execute_synthetic_monitors","dynatrace_get_attack","dynatrace_get_audit_logs","dynatrace_get_entity","dynatrace_get_event","dynatrace_get_metric","dynatrace_get_problem","dynatrace_get_problem_comment","dynatrace_get_security_problem","dynatrace_get_settings_object","dynatrace_get_slo","dynatrace_get_synthetic_batch","dynatrace_ingest_event","dynatrace_ingest_logs","dynatrace_ingest_metrics","dynatrace_list_attacks","dynatrace_list_entities","dynatrace_list_entity_types","dynatrace_list_events","dynatrace_list_metrics","dynatrace_list_problem_comments","dynatrace_list_problems","dynatrace_list_remediation_items","dynatrace_list_security_problems","dynatrace_list_settings_objects","dynatrace_list_settings_schemas","dynatrace_list_slos","dynatrace_list_synthetic_monitors","dynatrace_list_tags","dynatrace_mute_security_problem","dynatrace_mute_security_problems","dynatrace_query_metrics","dynatrace_search_logs","dynatrace_unmute_security_problem","dynatrace_unmute_security_problems","dynatrace_update_problem_comment","dynatrace_update_settings_object","dynatrace_update_slo","elasticsearch_bulk","elasticsearch_cluster_health","elasticsearch_cluster_stats","elasticsearch_count","elasticsearch_create_index","elasticsearch_delete_document","elasticsearch_delete_index","elasticsearch_get_document","elasticsearch_get_index","elasticsearch_index_document","elasticsearch_list_indices","elasticsearch_search","elasticsearch_update_document","elevenlabs_audio_isolation","elevenlabs_edit_voice_settings","elevenlabs_get_user","elevenlabs_get_voice","elevenlabs_get_voice_settings","elevenlabs_list_models","elevenlabs_list_voices","elevenlabs_sound_effects","elevenlabs_speech_to_speech","elevenlabs_tts","emailbison_attach_leads_to_campaign","emailbison_attach_tags_to_leads","emailbison_create_campaign","emailbison_create_lead","emailbison_create_tag","emailbison_get_lead","emailbison_list_campaigns","emailbison_list_leads","emailbison_list_replies","emailbison_list_tags","emailbison_update_campaign","emailbison_update_campaign_status","emailbison_update_lead","embeddings_cohere","embeddings_gemini","embeddings_mistral","embeddings_openai","embeddings_openrouter","enrich_check_credits","enrich_company_funding","enrich_company_lookup","enrich_company_revenue","enrich_disposable_email_check","enrich_email_to_ip","enrich_email_to_person_lite","enrich_email_to_phone","enrich_email_to_profile","enrich_find_email","enrich_get_post_details","enrich_ip_to_company","enrich_linkedin_profile","enrich_linkedin_to_personal_email","enrich_linkedin_to_work_email","enrich_phone_finder","enrich_reverse_hash_lookup","enrich_sales_pointer_people","enrich_search_company","enrich_search_company_activities","enrich_search_company_employees","enrich_search_jobs","enrich_search_logo","enrich_search_people","enrich_search_people_activities","enrich_search_post_comments","enrich_search_post_comments_by_url","enrich_search_post_reactions","enrich_search_post_reactions_by_url","enrich_search_posts","enrich_search_similar_companies","enrich_verify_email","enrichment_run","enrow_find_email","enrow_verify_email","exa_agent","exa_answer","exa_find_similar_links","exa_get_contents","exa_search","extend_parser","extend_parser_v2","fathom_get_summary","fathom_get_transcript","fathom_list_meeting_types","fathom_list_meetings","fathom_list_team_members","fathom_list_teams","file_append","file_compress","file_decompress","file_fetch","file_get","file_get_content","file_manage_sharing","file_parser","file_parser_v2","file_parser_v3","file_read","file_write","findymail_find_email_from_linkedin","findymail_find_email_from_name","findymail_find_emails_by_domain","findymail_find_employees","findymail_find_phone","findymail_get_company","findymail_get_credits","findymail_lookup_technologies","findymail_reverse_email_lookup","findymail_search_technologies","findymail_verify_email","firecrawl_agent","firecrawl_batch_scrape","firecrawl_batch_scrape_status","firecrawl_cancel_crawl","firecrawl_crawl","firecrawl_crawl_status","firecrawl_credit_usage","firecrawl_extract","firecrawl_extract_status","firecrawl_map","firecrawl_parse","firecrawl_scrape","firecrawl_search","fireflies_add_to_live_meeting","fireflies_create_bite","fireflies_delete_transcript","fireflies_get_transcript","fireflies_get_user","fireflies_list_bites","fireflies_list_contacts","fireflies_list_transcripts","fireflies_list_users","fireflies_upload_audio","flint_create_task","flint_generate_pages","flint_get_task","function_execute","gamma_check_status","gamma_generate","gamma_generate_from_template","gamma_list_folders","gamma_list_themes","github_add_assignees","github_add_assignees_v2","github_add_labels","github_add_labels_v2","github_cancel_workflow_run","github_cancel_workflow_run_v2","github_check_star","github_check_star_v2","github_close_issue","github_close_issue_v2","github_close_pr","github_close_pr_v2","github_comment","github_comment_v2","github_compare_commits","github_compare_commits_v2","github_create_branch","github_create_branch_v2","github_create_comment_reaction","github_create_comment_reaction_v2","github_create_file","github_create_file_v2","github_create_gist","github_create_gist_v2","github_create_issue","github_create_issue_reaction","github_create_issue_reaction_v2","github_create_issue_v2","github_create_milestone","github_create_milestone_v2","github_create_pr","github_create_pr_review","github_create_pr_review_v2","github_create_pr_v2","github_create_project","github_create_project_v2","github_create_release","github_create_release_v2","github_delete_branch","github_delete_branch_v2","github_delete_comment","github_delete_comment_reaction","github_delete_comment_reaction_v2","github_delete_comment_v2","github_delete_file","github_delete_file_v2","github_delete_gist","github_delete_gist_v2","github_delete_issue_reaction","github_delete_issue_reaction_v2","github_delete_milestone","github_delete_milestone_v2","github_delete_project","github_delete_project_v2","github_delete_release","github_delete_release_v2","github_fork_gist","github_fork_gist_v2","github_fork_repo","github_fork_repo_v2","github_get_branch","github_get_branch_protection","github_get_branch_protection_v2","github_get_branch_v2","github_get_commit","github_get_commit_v2","github_get_file_content","github_get_file_content_v2","github_get_gist","github_get_gist_v2","github_get_issue","github_get_issue_v2","github_get_latest_release","github_get_latest_release_v2","github_get_milestone","github_get_milestone_v2","github_get_pr_files","github_get_pr_files_v2","github_get_project","github_get_project_v2","github_get_readme","github_get_readme_v2","github_get_release","github_get_release_v2","github_get_tree","github_get_tree_v2","github_get_workflow","github_get_workflow_run","github_get_workflow_run_v2","github_get_workflow_v2","github_issue_comment","github_issue_comment_v2","github_job_logs","github_latest_commit","github_latest_commit_v2","github_list_branches","github_list_branches_v2","github_list_commits","github_list_commits_v2","github_list_forks","github_list_forks_v2","github_list_gists","github_list_gists_v2","github_list_issue_comments","github_list_issue_comments_v2","github_list_issues","github_list_issues_v2","github_list_milestones","github_list_milestones_v2","github_list_pr_comments","github_list_pr_comments_v2","github_list_projects","github_list_projects_v2","github_list_prs","github_list_prs_v2","github_list_releases","github_list_releases_v2","github_list_review_threads","github_list_stargazers","github_list_stargazers_v2","github_list_tags","github_list_tags_v2","github_list_workflow_runs","github_list_workflow_runs_v2","github_list_workflows","github_list_workflows_v2","github_merge_pr","github_merge_pr_v2","github_pr","github_pr_v2","github_remove_label","github_remove_label_v2","github_reply_review_thread","github_repo_info","github_repo_info_v2","github_request_reviewers","github_request_reviewers_v2","github_rerun_workflow","github_rerun_workflow_v2","github_resolve_review_thread","github_search_code","github_search_code_v2","github_search_commits","github_search_commits_v2","github_search_issues","github_search_issues_v2","github_search_repos","github_search_repos_v2","github_search_users","github_search_users_v2","github_star_gist","github_star_gist_v2","github_star_repo","github_star_repo_v2","github_status_check_rollup","github_trigger_workflow","github_trigger_workflow_v2","github_unstar_gist","github_unstar_gist_v2","github_unstar_repo","github_unstar_repo_v2","github_update_branch_protection","github_update_branch_protection_v2","github_update_comment","github_update_comment_v2","github_update_file","github_update_file_v2","github_update_gist","github_update_gist_v2","github_update_issue","github_update_issue_v2","github_update_milestone","github_update_milestone_v2","github_update_pr","github_update_pr_v2","github_update_project","github_update_project_v2","github_update_release","github_update_release_v2","gitlab_activate_user","gitlab_add_member","gitlab_add_saml_group_link","gitlab_approve_access_request","gitlab_approve_merge_request","gitlab_approve_user","gitlab_ban_user","gitlab_block_user","gitlab_cancel_pipeline","gitlab_compare_branches","gitlab_create_branch","gitlab_create_file","gitlab_create_issue","gitlab_create_issue_note","gitlab_create_merge_request","gitlab_create_merge_request_note","gitlab_create_pipeline","gitlab_create_release","gitlab_create_user","gitlab_deactivate_user","gitlab_delete_branch","gitlab_delete_issue","gitlab_delete_saml_group_link","gitlab_delete_user","gitlab_delete_user_identity","gitlab_deny_access_request","gitlab_get_file","gitlab_get_group","gitlab_get_issue","gitlab_get_job_log","gitlab_get_merge_request","gitlab_get_merge_request_changes","gitlab_get_pipeline","gitlab_get_project","gitlab_invite_member","gitlab_list_access_requests","gitlab_list_branches","gitlab_list_commits","gitlab_list_groups","gitlab_list_invitations","gitlab_list_issues","gitlab_list_members","gitlab_list_merge_requests","gitlab_list_pipeline_jobs","gitlab_list_pipelines","gitlab_list_projects","gitlab_list_releases","gitlab_list_repository_tree","gitlab_list_saml_group_links","gitlab_list_user_memberships","gitlab_merge_merge_request","gitlab_play_job","gitlab_reject_user","gitlab_remove_member","gitlab_retry_pipeline","gitlab_revoke_invitation","gitlab_search_users","gitlab_unban_user","gitlab_unblock_user","gitlab_update_file","gitlab_update_invitation","gitlab_update_issue","gitlab_update_member","gitlab_update_merge_request","gitlab_update_user","gmail_add_label","gmail_add_label_v2","gmail_archive","gmail_archive_v2","gmail_create_label_v2","gmail_delete","gmail_delete_draft_v2","gmail_delete_label_v2","gmail_delete_v2","gmail_draft","gmail_draft_v2","gmail_edit_draft_v2","gmail_get_draft_v2","gmail_get_thread_v2","gmail_list_drafts_v2","gmail_list_labels_v2","gmail_list_threads_v2","gmail_mark_read","gmail_mark_read_v2","gmail_mark_unread","gmail_mark_unread_v2","gmail_move","gmail_move_v2","gmail_read","gmail_read_v2","gmail_remove_label","gmail_remove_label_v2","gmail_search","gmail_search_v2","gmail_send","gmail_send_v2","gmail_trash_thread_v2","gmail_unarchive","gmail_unarchive_v2","gmail_untrash_thread_v2","gmail_update_label_v2","gong_aggregate_activity","gong_aggregate_by_period","gong_answered_scorecards","gong_ask_anything","gong_assign_flow_prospects","gong_create_call","gong_day_by_day_activity","gong_get_brief","gong_get_call","gong_get_call_transcript","gong_get_coaching","gong_get_extensive_calls","gong_get_folder_content","gong_get_logs","gong_get_prospect_flows","gong_get_user","gong_interaction_stats","gong_list_calls","gong_list_flows","gong_list_library_folders","gong_list_scorecards","gong_list_trackers","gong_list_users","gong_list_workspaces","gong_lookup_email","gong_lookup_phone","gong_purge_email_address","gong_purge_phone_number","gong_unassign_flow_prospects","google_ads_ad_performance","google_ads_campaign_performance","google_ads_list_ad_groups","google_ads_list_campaigns","google_ads_list_customers","google_ads_search","google_appsheet_add_rows","google_appsheet_delete_rows","google_appsheet_edit_rows","google_appsheet_find_rows","google_bigquery_create_dataset","google_bigquery_create_table","google_bigquery_delete_dataset","google_bigquery_delete_table","google_bigquery_get_query_results","google_bigquery_get_table","google_bigquery_insert_rows","google_bigquery_list_datasets","google_bigquery_list_table_data","google_bigquery_list_tables","google_bigquery_query","google_books_volume_details","google_books_volume_search","google_calendar_create","google_calendar_create_calendar","google_calendar_create_calendar_v2","google_calendar_create_v2","google_calendar_delete","google_calendar_delete_calendar","google_calendar_delete_calendar_v2","google_calendar_delete_v2","google_calendar_freebusy","google_calendar_freebusy_v2","google_calendar_get","google_calendar_get_v2","google_calendar_instances","google_calendar_instances_v2","google_calendar_invite","google_calendar_invite_v2","google_calendar_list","google_calendar_list_acl","google_calendar_list_acl_v2","google_calendar_list_calendars","google_calendar_list_calendars_v2","google_calendar_list_v2","google_calendar_move","google_calendar_move_v2","google_calendar_quick_add","google_calendar_quick_add_v2","google_calendar_share_calendar","google_calendar_share_calendar_v2","google_calendar_unshare_calendar","google_calendar_unshare_calendar_v2","google_calendar_update","google_calendar_update_acl","google_calendar_update_acl_v2","google_calendar_update_calendar","google_calendar_update_calendar_v2","google_calendar_update_v2","google_contacts_create","google_contacts_delete","google_contacts_get","google_contacts_list","google_contacts_search","google_contacts_update","google_docs_create","google_docs_create_named_range","google_docs_create_paragraph_bullets","google_docs_delete_content_range","google_docs_delete_named_range","google_docs_delete_paragraph_bullets","google_docs_insert_image","google_docs_insert_page_break","google_docs_insert_table","google_docs_insert_text","google_docs_read","google_docs_replace_text","google_docs_update_paragraph_style","google_docs_update_text_style","google_docs_write","google_drive_copy","google_drive_create_comment","google_drive_create_folder","google_drive_delete","google_drive_delete_comment","google_drive_download","google_drive_export","google_drive_get_about","google_drive_get_content","google_drive_get_file","google_drive_get_revision","google_drive_list","google_drive_list_comments","google_drive_list_permissions","google_drive_list_revisions","google_drive_move","google_drive_search","google_drive_share","google_drive_trash","google_drive_unshare","google_drive_untrash","google_drive_update","google_drive_upload","google_forms_batch_update","google_forms_create_form","google_forms_create_watch","google_forms_delete_watch","google_forms_get_form","google_forms_get_responses","google_forms_list_watches","google_forms_renew_watch","google_forms_set_publish_settings","google_groups_add_alias","google_groups_add_member","google_groups_create_group","google_groups_delete_group","google_groups_get_group","google_groups_get_member","google_groups_get_settings","google_groups_has_member","google_groups_list_aliases","google_groups_list_groups","google_groups_list_members","google_groups_remove_alias","google_groups_remove_member","google_groups_update_group","google_groups_update_member","google_groups_update_settings","google_maps_air_quality","google_maps_directions","google_maps_distance_matrix","google_maps_elevation","google_maps_geocode","google_maps_geolocate","google_maps_place_details","google_maps_places_nearby","google_maps_places_search","google_maps_pollen","google_maps_reverse_geocode","google_maps_snap_to_roads","google_maps_solar","google_maps_speed_limits","google_maps_timezone","google_maps_validate_address","google_meet_create_space","google_meet_end_conference","google_meet_get_conference_record","google_meet_get_space","google_meet_list_conference_records","google_meet_list_participants","google_pagespeed_analyze","google_search","google_sheets_append","google_sheets_append_v2","google_sheets_batch_clear_v2","google_sheets_batch_get_v2","google_sheets_batch_update_v2","google_sheets_clear_v2","google_sheets_copy_sheet_v2","google_sheets_create_spreadsheet_v2","google_sheets_delete_rows_v2","google_sheets_delete_sheet_v2","google_sheets_delete_spreadsheet_v2","google_sheets_get_spreadsheet_v2","google_sheets_read","google_sheets_read_v2","google_sheets_update","google_sheets_update_v2","google_sheets_write","google_sheets_write_v2","google_slides_add_image","google_slides_add_slide","google_slides_batch_update","google_slides_copy_presentation","google_slides_create","google_slides_create_line","google_slides_create_paragraph_bullets","google_slides_create_shape","google_slides_create_sheets_chart","google_slides_create_table","google_slides_create_video","google_slides_delete_object","google_slides_delete_paragraph_bullets","google_slides_delete_table_column","google_slides_delete_table_row","google_slides_delete_text","google_slides_duplicate_object","google_slides_export_presentation","google_slides_get_page","google_slides_get_thumbnail","google_slides_group_objects","google_slides_insert_table_columns","google_slides_insert_table_rows","google_slides_insert_text","google_slides_merge_table_cells","google_slides_read","google_slides_refresh_sheets_chart","google_slides_replace_all_shapes_with_image","google_slides_replace_all_shapes_with_sheets_chart","google_slides_replace_all_text","google_slides_replace_image","google_slides_reroute_line","google_slides_ungroup_objects","google_slides_unmerge_table_cells","google_slides_update_image_properties","google_slides_update_line_category","google_slides_update_line_properties","google_slides_update_page_element_alt_text","google_slides_update_page_element_transform","google_slides_update_page_elements_z_order","google_slides_update_page_properties","google_slides_update_paragraph_style","google_slides_update_shape_properties","google_slides_update_slide_properties","google_slides_update_slides_position","google_slides_update_table_border_properties","google_slides_update_table_cell_properties","google_slides_update_table_column_properties","google_slides_update_table_row_properties","google_slides_update_text_style","google_slides_update_video_properties","google_slides_write","google_tasks_create","google_tasks_delete","google_tasks_get","google_tasks_list","google_tasks_list_task_lists","google_tasks_update","google_translate_detect","google_translate_text","google_vault_add_held_accounts","google_vault_add_matters_permissions","google_vault_close_matters","google_vault_create_matters","google_vault_create_matters_export","google_vault_create_matters_holds","google_vault_create_saved_query","google_vault_delete_matters","google_vault_delete_matters_export","google_vault_delete_matters_holds","google_vault_delete_saved_query","google_vault_download_export_file","google_vault_list_matters","google_vault_list_matters_export","google_vault_list_matters_holds","google_vault_list_saved_queries","google_vault_remove_held_accounts","google_vault_remove_matters_permissions","google_vault_reopen_matters","google_vault_undelete_matters","google_vault_update_matters","google_vault_update_matters_holds","grafana_check_data_source_health","grafana_create_alert_rule","grafana_create_annotation","grafana_create_contact_point","grafana_create_dashboard","grafana_create_folder","grafana_delete_alert_rule","grafana_delete_annotation","grafana_delete_contact_point","grafana_delete_dashboard","grafana_delete_folder","grafana_get_alert_rule","grafana_get_alert_rule_group","grafana_get_dashboard","grafana_get_data_source","grafana_get_folder","grafana_get_health","grafana_list_alert_rules","grafana_list_annotations","grafana_list_contact_points","grafana_list_dashboards","grafana_list_data_sources","grafana_list_folders","grafana_move_folder","grafana_query_data_source","grafana_update_alert_rule","grafana_update_annotation","grafana_update_contact_point","grafana_update_dashboard","grafana_update_folder","grain_create_hook","grain_create_hook_v2","grain_delete_hook","grain_delete_hook_v2","grain_get_recording","grain_get_transcript","grain_list_hooks","grain_list_hooks_v2","grain_list_meeting_types","grain_list_recordings","grain_list_teams","grain_list_views","granola_create_webhook_endpoint","granola_delete_webhook_endpoint","granola_get_note","granola_get_transcript","granola_list_audit_events","granola_list_folders","granola_list_notes","granola_list_webhook_endpoints","granola_update_webhook_endpoint","greenhouse_get_application","greenhouse_get_candidate","greenhouse_get_job","greenhouse_get_user","greenhouse_list_applications","greenhouse_list_candidates","greenhouse_list_departments","greenhouse_list_job_stages","greenhouse_list_jobs","greenhouse_list_offices","greenhouse_list_users","greptile_index_repo","greptile_query","greptile_search","greptile_status","guardrails_validate","hex_cancel_run","hex_create_collection","hex_create_group","hex_deactivate_user","hex_delete_group","hex_get_collection","hex_get_data_connection","hex_get_group","hex_get_project","hex_get_project_runs","hex_get_queried_tables","hex_get_run_status","hex_list_collections","hex_list_data_connections","hex_list_groups","hex_list_projects","hex_list_users","hex_run_project","hex_update_collection","hex_update_group","hex_update_project","http_request","hubspot_add_list_memberships","hubspot_create_appointment","hubspot_create_association","hubspot_create_company","hubspot_create_contact","hubspot_create_deal","hubspot_create_email","hubspot_create_line_item","hubspot_create_list","hubspot_create_note","hubspot_create_ticket","hubspot_delete_association","hubspot_delete_company","hubspot_delete_contact","hubspot_delete_deal","hubspot_delete_line_item","hubspot_delete_ticket","hubspot_get_appointment","hubspot_get_association_labels","hubspot_get_cart","hubspot_get_company","hubspot_get_contact","hubspot_get_deal","hubspot_get_email","hubspot_get_line_item","hubspot_get_list","hubspot_get_list_memberships","hubspot_get_marketing_event","hubspot_get_note","hubspot_get_properties","hubspot_get_quote","hubspot_get_ticket","hubspot_get_users","hubspot_list_appointments","hubspot_list_associations","hubspot_list_carts","hubspot_list_companies","hubspot_list_contacts","hubspot_list_deals","hubspot_list_emails","hubspot_list_line_items","hubspot_list_lists","hubspot_list_marketing_events","hubspot_list_notes","hubspot_list_owners","hubspot_list_quotes","hubspot_list_tickets","hubspot_remove_list_memberships","hubspot_search_companies","hubspot_search_contacts","hubspot_search_deals","hubspot_search_emails","hubspot_search_line_items","hubspot_search_notes","hubspot_search_quotes","hubspot_search_tickets","hubspot_update_appointment","hubspot_update_company","hubspot_update_contact","hubspot_update_deal","hubspot_update_line_item","hubspot_update_ticket","huggingface_chat","hunter_companies_find","hunter_discover","hunter_domain_search","hunter_email_count","hunter_email_finder","hunter_email_verifier","iam_add_user_to_group","iam_attach_role_policy","iam_attach_user_policy","iam_create_access_key","iam_create_role","iam_create_user","iam_delete_access_key","iam_delete_role","iam_delete_user","iam_detach_role_policy","iam_detach_user_policy","iam_get_role","iam_get_user","iam_list_attached_role_policies","iam_list_attached_user_policies","iam_list_groups","iam_list_policies","iam_list_roles","iam_list_users","iam_remove_user_from_group","iam_simulate_principal_policy","icypeas_find_email","icypeas_verify_email","identity_center_check_assignment_deletion_status","identity_center_check_assignment_status","identity_center_create_account_assignment","identity_center_delete_account_assignment","identity_center_describe_account","identity_center_get_group","identity_center_get_user","identity_center_list_account_assignments","identity_center_list_accounts","identity_center_list_groups","identity_center_list_instances","identity_center_list_permission_sets","image_generate","incidentio_actions_create","incidentio_actions_list","incidentio_actions_show","incidentio_actions_update","incidentio_alert_events_create","incidentio_alerts_list","incidentio_alerts_resolve","incidentio_alerts_show","incidentio_catalog_entries_list","incidentio_catalog_types_list","incidentio_custom_fields_create","incidentio_custom_fields_delete","incidentio_custom_fields_list","incidentio_custom_fields_show","incidentio_custom_fields_update","incidentio_escalation_paths_create","incidentio_escalation_paths_delete","incidentio_escalation_paths_list","incidentio_escalation_paths_show","incidentio_escalation_paths_update","incidentio_escalations_cancel","incidentio_escalations_create","incidentio_escalations_list","incidentio_escalations_show","incidentio_follow_ups_create","incidentio_follow_ups_list","incidentio_follow_ups_show","incidentio_follow_ups_update","incidentio_incident_alerts_list","incidentio_incident_memberships_create","incidentio_incident_memberships_revoke","incidentio_incident_participants_list","incidentio_incident_roles_create","incidentio_incident_roles_delete","incidentio_incident_roles_list","incidentio_incident_roles_show","incidentio_incident_roles_update","incidentio_incident_statuses_list","incidentio_incident_timestamps_list","incidentio_incident_timestamps_show","incidentio_incident_types_list","incidentio_incident_updates_list","incidentio_incidents_create","incidentio_incidents_list","incidentio_incidents_show","incidentio_incidents_update","incidentio_on_call_now","incidentio_schedule_entries_list","incidentio_schedule_overrides_create","incidentio_schedule_overrides_list","incidentio_schedules_create","incidentio_schedules_delete","incidentio_schedules_list","incidentio_schedules_show","incidentio_schedules_update","incidentio_severities_list","incidentio_teams_list","incidentio_teams_show","incidentio_users_list","incidentio_users_show","incidentio_workflows_create","incidentio_workflows_delete","incidentio_workflows_list","incidentio_workflows_show","incidentio_workflows_update","infisical_create_secret","infisical_delete_secret","infisical_get_secret","infisical_list_secrets","infisical_update_secret","instagram_delete_comment","instagram_download_media","instagram_get_account_insights","instagram_get_container_status","instagram_get_conversation_messages","instagram_get_media","instagram_get_media_insights","instagram_get_message","instagram_get_profile","instagram_get_publishing_limit","instagram_hide_comment","instagram_list_comments","instagram_list_conversations","instagram_list_media","instagram_list_stories","instagram_private_reply","instagram_publish_carousel","instagram_publish_image","instagram_publish_reel","instagram_publish_story","instagram_publish_video","instagram_reply_to_comment","instagram_send_text_message","instagram_set_comments_enabled","instantly_activate_campaign","instantly_create_campaign","instantly_create_lead","instantly_create_lead_list","instantly_delete_campaign","instantly_delete_leads","instantly_get_lead","instantly_list_campaigns","instantly_list_emails","instantly_list_lead_lists","instantly_list_leads","instantly_patch_campaign","instantly_patch_lead","instantly_pause_campaign","instantly_reply_to_email","instantly_update_lead_interest_status","intercom_assign_conversation_v2","intercom_attach_contact_to_company_v2","intercom_close_conversation_v2","intercom_create_company","intercom_create_company_v2","intercom_create_contact","intercom_create_contact_v2","intercom_create_event_v2","intercom_create_message","intercom_create_message_v2","intercom_create_note_v2","intercom_create_tag_v2","intercom_create_ticket","intercom_create_ticket_v2","intercom_delete_contact","intercom_delete_contact_v2","intercom_detach_contact_from_company_v2","intercom_get_company","intercom_get_company_v2","intercom_get_contact","intercom_get_contact_v2","intercom_get_conversation","intercom_get_conversation_v2","intercom_get_ticket","intercom_get_ticket_v2","intercom_list_admins_v2","intercom_list_companies","intercom_list_companies_v2","intercom_list_contacts","intercom_list_contacts_v2","intercom_list_conversations","intercom_list_conversations_v2","intercom_list_tags_v2","intercom_open_conversation_v2","intercom_reply_conversation","intercom_reply_conversation_v2","intercom_search_contacts","intercom_search_contacts_v2","intercom_search_conversations","intercom_search_conversations_v2","intercom_snooze_conversation_v2","intercom_tag_contact_v2","intercom_tag_conversation_v2","intercom_untag_contact_v2","intercom_update_contact","intercom_update_contact_v2","intercom_update_ticket_v2","jina_read_url","jina_search","jira_add_attachment","jira_add_comment","jira_add_watcher","jira_add_worklog","jira_assign_issue","jira_bulk_read","jira_create_issue_link","jira_delete_attachment","jira_delete_comment","jira_delete_issue","jira_delete_issue_link","jira_delete_worklog","jira_get_attachments","jira_get_comments","jira_get_fields","jira_get_project","jira_get_transitions","jira_get_users","jira_get_worklogs","jira_list_issue_types","jira_list_projects","jira_remove_watcher","jira_retrieve","jira_search_issues","jira_search_users","jira_transition_issue","jira_update","jira_update_comment","jira_update_worklog","jira_write","jotform_add_label_resources","jotform_clone_form","jotform_create_form","jotform_create_label","jotform_create_question","jotform_create_questions","jotform_create_report","jotform_create_submission","jotform_create_submissions","jotform_create_webhook","jotform_delete_form","jotform_delete_label","jotform_delete_question","jotform_delete_report","jotform_delete_submission","jotform_delete_webhook","jotform_get_form","jotform_get_form_properties","jotform_get_history","jotform_get_label","jotform_get_question","jotform_get_report","jotform_get_settings","jotform_get_submission","jotform_get_usage","jotform_get_user","jotform_list_form_files","jotform_list_form_reports","jotform_list_form_submissions","jotform_list_forms","jotform_list_label_resources","jotform_list_labels","jotform_list_questions","jotform_list_reports","jotform_list_submissions","jotform_list_subusers","jotform_list_webhooks","jotform_remove_label_resources","jotform_update_form_properties","jotform_update_label","jotform_update_question","jotform_update_settings","jotform_update_submission","jsm_add_comment","jsm_add_customer","jsm_add_organization","jsm_add_participants","jsm_answer_approval","jsm_attach_form","jsm_copy_forms","jsm_create_object","jsm_create_organization","jsm_create_request","jsm_delete_form","jsm_delete_object","jsm_externalise_form","jsm_get_approvals","jsm_get_comments","jsm_get_customers","jsm_get_form","jsm_get_form_answers","jsm_get_form_structure","jsm_get_form_templates","jsm_get_issue_forms","jsm_get_object","jsm_get_object_schema","jsm_get_object_type_attributes","jsm_get_organizations","jsm_get_participants","jsm_get_queues","jsm_get_request","jsm_get_request_type_fields","jsm_get_request_types","jsm_get_requests","jsm_get_service_desks","jsm_get_sla","jsm_get_transitions","jsm_internalise_form","jsm_list_object_schemas","jsm_list_object_types","jsm_reopen_form","jsm_save_form_answers","jsm_search_objects_aql","jsm_submit_form","jsm_transition_request","jsm_update_object","jupyter_copy_content","jupyter_create_file","jupyter_create_session","jupyter_delete_content","jupyter_delete_session","jupyter_get_content","jupyter_interrupt_kernel","jupyter_list_contents","jupyter_list_kernels","jupyter_list_kernelspecs","jupyter_list_sessions","jupyter_rename_content","jupyter_restart_kernel","jupyter_start_kernel","jupyter_stop_kernel","jupyter_upload_file","kalshi_amend_order","kalshi_amend_order_v2","kalshi_cancel_order","kalshi_cancel_order_v2","kalshi_create_order","kalshi_create_order_v2","kalshi_get_balance","kalshi_get_balance_v2","kalshi_get_candlesticks","kalshi_get_candlesticks_v2","kalshi_get_event","kalshi_get_event_candlesticks","kalshi_get_event_candlesticks_v2","kalshi_get_event_v2","kalshi_get_events","kalshi_get_events_v2","kalshi_get_exchange_announcements","kalshi_get_exchange_announcements_v2","kalshi_get_exchange_schedule","kalshi_get_exchange_schedule_v2","kalshi_get_exchange_status","kalshi_get_exchange_status_v2","kalshi_get_fills","kalshi_get_fills_v2","kalshi_get_market","kalshi_get_market_v2","kalshi_get_markets","kalshi_get_markets_v2","kalshi_get_order","kalshi_get_order_v2","kalshi_get_orderbook","kalshi_get_orderbook_v2","kalshi_get_orders","kalshi_get_orders_v2","kalshi_get_positions","kalshi_get_positions_v2","kalshi_get_series_by_ticker","kalshi_get_series_by_ticker_v2","kalshi_get_series_list","kalshi_get_series_list_v2","kalshi_get_settlements","kalshi_get_settlements_v2","kalshi_get_trades","kalshi_get_trades_v2","ketch_get_consent","ketch_get_subscriptions","ketch_invoke_right","ketch_set_consent","ketch_set_subscriptions","knowledge_create_document","knowledge_delete_chunk","knowledge_delete_document","knowledge_get_connector","knowledge_get_document","knowledge_list_chunks","knowledge_list_connectors","knowledge_list_documents","knowledge_list_tags","knowledge_search","knowledge_trigger_sync","knowledge_update_chunk","knowledge_upload_chunk","knowledge_upsert_document","langsmith_create_feedback","langsmith_create_run","langsmith_create_runs_batch","langsmith_get_run","langsmith_update_run","latex_compile","latex_get_package","latex_list_fonts","latex_search_packages","launchdarkly_create_flag","launchdarkly_delete_flag","launchdarkly_get_audit_log","launchdarkly_get_flag","launchdarkly_get_flag_status","launchdarkly_list_environments","launchdarkly_list_flags","launchdarkly_list_members","launchdarkly_list_projects","launchdarkly_list_segments","launchdarkly_toggle_flag","launchdarkly_update_flag","leadmagic_company_search","leadmagic_email_to_profile","leadmagic_find_email","leadmagic_find_mobile","leadmagic_get_credits","leadmagic_profile_search","leadmagic_profile_to_email","leadmagic_role_finder","leadmagic_validate_email","lemlist_get_activities","lemlist_get_lead","lemlist_send_email","linear_add_label_to_issue","linear_add_label_to_project","linear_archive_issue","linear_archive_label","linear_archive_project","linear_create_attachment","linear_create_comment","linear_create_customer","linear_create_customer_request","linear_create_customer_status","linear_create_customer_tier","linear_create_cycle","linear_create_favorite","linear_create_issue","linear_create_issue_relation","linear_create_label","linear_create_project","linear_create_project_label","linear_create_project_milestone","linear_create_project_status","linear_create_project_update","linear_create_workflow_state","linear_delete_attachment","linear_delete_comment","linear_delete_customer","linear_delete_customer_status","linear_delete_customer_tier","linear_delete_issue","linear_delete_issue_relation","linear_delete_project","linear_delete_project_label","linear_delete_project_milestone","linear_delete_project_status","linear_get_active_cycle","linear_get_customer","linear_get_cycle","linear_get_issue","linear_get_project","linear_get_viewer","linear_list_attachments","linear_list_comments","linear_list_customer_requests","linear_list_customer_statuses","linear_list_customer_tiers","linear_list_customers","linear_list_cycles","linear_list_favorites","linear_list_issue_relations","linear_list_labels","linear_list_notifications","linear_list_project_labels","linear_list_project_milestones","linear_list_project_statuses","linear_list_project_updates","linear_list_projects","linear_list_teams","linear_list_users","linear_list_workflow_states","linear_merge_customers","linear_read_issues","linear_remove_label_from_issue","linear_remove_label_from_project","linear_search_issues","linear_unarchive_issue","linear_update_attachment","linear_update_comment","linear_update_customer","linear_update_customer_request","linear_update_customer_status","linear_update_customer_tier","linear_update_issue","linear_update_label","linear_update_notification","linear_update_project","linear_update_project_label","linear_update_project_milestone","linear_update_project_status","linear_update_workflow_state","linkedin_get_profile","linkedin_share_post","linkup_search","linq_add_participant","linq_check_imessage","linq_check_rcs","linq_create_attachment","linq_create_chat","linq_create_contact_card","linq_create_webhook_subscription","linq_delete_attachment","linq_delete_message","linq_delete_webhook_subscription","linq_edit_message","linq_get_attachment","linq_get_chat","linq_get_contact_card","linq_get_message","linq_get_webhook_subscription","linq_leave_chat","linq_list_chats","linq_list_messages","linq_list_phone_numbers","linq_list_thread","linq_list_webhook_events","linq_list_webhook_subscriptions","linq_mark_chat_read","linq_react_to_message","linq_remove_participant","linq_send_message","linq_send_voice_memo","linq_share_contact_card","linq_start_typing","linq_stop_typing","linq_update_chat","linq_update_contact_card","linq_update_webhook_subscription","llm_chat","logfire_get_token_info","logfire_get_trace","logfire_query","logfire_search_records","logrocket_create_release","logrocket_get_audit_logs","logrocket_get_highlights","logrocket_identify_user","logrocket_list_exported_sessions","logrocket_request_highlights","logs_get","logs_get_execution","logs_get_run_details","logs_query","logs_query_runs","loops_check_contact_suppression","loops_create_contact","loops_create_contact_property","loops_delete_contact","loops_find_contact","loops_get_transactional_email","loops_list_contact_properties","loops_list_mailing_lists","loops_list_transactional_emails","loops_remove_contact_suppression","loops_send_event","loops_send_transactional_email","loops_update_contact","luma_add_guests","luma_cancel_event","luma_create_event","luma_get_event","luma_get_guest","luma_get_guests","luma_list_events","luma_lookup_event","luma_send_invites","luma_update_event","luma_update_guest_status","mailchimp_add_member","mailchimp_add_member_tags","mailchimp_add_or_update_member","mailchimp_add_segment_member","mailchimp_add_subscriber_to_automation","mailchimp_archive_member","mailchimp_create_audience","mailchimp_create_batch_operation","mailchimp_create_campaign","mailchimp_create_interest","mailchimp_create_interest_category","mailchimp_create_landing_page","mailchimp_create_merge_field","mailchimp_create_segment","mailchimp_create_template","mailchimp_delete_audience","mailchimp_delete_batch_operation","mailchimp_delete_campaign","mailchimp_delete_interest","mailchimp_delete_interest_category","mailchimp_delete_landing_page","mailchimp_delete_member","mailchimp_delete_merge_field","mailchimp_delete_segment","mailchimp_delete_template","mailchimp_get_audience","mailchimp_get_audiences","mailchimp_get_automation","mailchimp_get_automations","mailchimp_get_batch_operation","mailchimp_get_batch_operations","mailchimp_get_campaign","mailchimp_get_campaign_content","mailchimp_get_campaign_report","mailchimp_get_campaign_reports","mailchimp_get_campaigns","mailchimp_get_interest","mailchimp_get_interest_categories","mailchimp_get_interest_category","mailchimp_get_interests","mailchimp_get_landing_page","mailchimp_get_landing_pages","mailchimp_get_member","mailchimp_get_member_tags","mailchimp_get_members","mailchimp_get_merge_field","mailchimp_get_merge_fields","mailchimp_get_segment","mailchimp_get_segment_members","mailchimp_get_segments","mailchimp_get_template","mailchimp_get_templates","mailchimp_pause_automation","mailchimp_publish_landing_page","mailchimp_remove_member_tags","mailchimp_remove_segment_member","mailchimp_replicate_campaign","mailchimp_schedule_campaign","mailchimp_send_campaign","mailchimp_set_campaign_content","mailchimp_start_automation","mailchimp_unarchive_member","mailchimp_unpublish_landing_page","mailchimp_unschedule_campaign","mailchimp_update_audience","mailchimp_update_campaign","mailchimp_update_interest","mailchimp_update_interest_category","mailchimp_update_landing_page","mailchimp_update_member","mailchimp_update_merge_field","mailchimp_update_segment","mailchimp_update_template","mailgun_add_list_member","mailgun_create_mailing_list","mailgun_get_domain","mailgun_get_mailing_list","mailgun_get_message","mailgun_list_domains","mailgun_list_messages","mailgun_send_message","managed_agent_archive_session","managed_agent_create_session","managed_agent_delete_session","managed_agent_get_session","managed_agent_interrupt_session","managed_agent_list_events","managed_agent_respond_custom_tool","managed_agent_respond_tool_confirmation","managed_agent_run_session","managed_agent_send_message","managed_agent_update_session","mem0_add_memories","mem0_get_memories","mem0_search_memories","memory_add","memory_delete","memory_get","memory_get_all","microsoft_ad_add_directory_role_member","microsoft_ad_add_group_member","microsoft_ad_add_user_app_role_assignment","microsoft_ad_assign_license","microsoft_ad_create_group","microsoft_ad_create_user","microsoft_ad_delete_group","microsoft_ad_delete_user","microsoft_ad_get_conditional_access_policy","microsoft_ad_get_device","microsoft_ad_get_group","microsoft_ad_get_user","microsoft_ad_list_authentication_methods","microsoft_ad_list_conditional_access_policies","microsoft_ad_list_devices","microsoft_ad_list_directory_audits","microsoft_ad_list_directory_role_members","microsoft_ad_list_directory_roles","microsoft_ad_list_group_members","microsoft_ad_list_groups","microsoft_ad_list_service_principal_app_role_assignments","microsoft_ad_list_service_principals","microsoft_ad_list_sign_ins","microsoft_ad_list_subscribed_skus","microsoft_ad_list_user_app_role_assignments","microsoft_ad_list_user_devices","microsoft_ad_list_user_licenses","microsoft_ad_list_users","microsoft_ad_remove_directory_role_member","microsoft_ad_remove_group_member","microsoft_ad_remove_user_app_role_assignment","microsoft_ad_reset_password","microsoft_ad_revoke_sign_in_sessions","microsoft_ad_set_password","microsoft_ad_update_group","microsoft_ad_update_user","microsoft_dataverse_associate","microsoft_dataverse_create_multiple","microsoft_dataverse_create_record","microsoft_dataverse_delete_record","microsoft_dataverse_disassociate","microsoft_dataverse_download_file","microsoft_dataverse_execute_action","microsoft_dataverse_execute_function","microsoft_dataverse_fetchxml_query","microsoft_dataverse_get_entity_metadata","microsoft_dataverse_get_record","microsoft_dataverse_list_records","microsoft_dataverse_search","microsoft_dataverse_update_multiple","microsoft_dataverse_update_record","microsoft_dataverse_upload_file","microsoft_dataverse_upsert_record","microsoft_dataverse_whoami","microsoft_excel_clear_range","microsoft_excel_create_table","microsoft_excel_delete_worksheet","microsoft_excel_format_range","microsoft_excel_read","microsoft_excel_read_v2","microsoft_excel_sort_range","microsoft_excel_table_add","microsoft_excel_worksheet_add","microsoft_excel_write","microsoft_excel_write_v2","microsoft_planner_create_bucket","microsoft_planner_create_plan","microsoft_planner_create_task","microsoft_planner_delete_bucket","microsoft_planner_delete_plan","microsoft_planner_delete_task","microsoft_planner_get_plan_details","microsoft_planner_get_task_details","microsoft_planner_list_buckets","microsoft_planner_list_plans","microsoft_planner_read_bucket","microsoft_planner_read_plan","microsoft_planner_read_task","microsoft_planner_update_bucket","microsoft_planner_update_plan","microsoft_planner_update_plan_details","microsoft_planner_update_task","microsoft_planner_update_task_details","microsoft_teams_delete_channel_message","microsoft_teams_delete_chat_message","microsoft_teams_get_message","microsoft_teams_list_channel_members","microsoft_teams_list_channels","microsoft_teams_list_chat_members","microsoft_teams_list_chats","microsoft_teams_list_team_members","microsoft_teams_list_teams","microsoft_teams_read_channel","microsoft_teams_read_chat","microsoft_teams_reply_to_message","microsoft_teams_set_reaction","microsoft_teams_unset_reaction","microsoft_teams_update_channel_message","microsoft_teams_update_chat_message","microsoft_teams_write_channel","microsoft_teams_write_chat","millionverifier_get_credits","millionverifier_verify_email","mintlify_create_agent_job","mintlify_create_assistant_message","mintlify_detect_ai_prose","mintlify_get_agent_job","mintlify_get_assistant_caller_stats","mintlify_get_assistant_conversations","mintlify_get_feedback","mintlify_get_feedback_by_page","mintlify_get_page_content","mintlify_get_searches","mintlify_get_update_status","mintlify_get_views","mintlify_get_visitors","mintlify_search","mintlify_send_agent_message","mintlify_trigger_automation","mintlify_trigger_preview","mintlify_trigger_update","mistral_parser","mistral_parser_v2","mistral_parser_v3","monday_archive_item","monday_change_column_value","monday_create_board","monday_create_column","monday_create_group","monday_create_item","monday_create_subitem","monday_create_update","monday_delete_item","monday_duplicate_item","monday_get_board","monday_get_groups","monday_get_item","monday_get_items","monday_list_boards","monday_move_item_to_group","monday_search_items","monday_update_item","mongodb_delete","mongodb_execute","mongodb_insert","mongodb_introspect","mongodb_query","mongodb_update","mssql_delete","mssql_execute","mssql_insert","mssql_introspect","mssql_query","mssql_update","mysql_delete","mysql_execute","mysql_insert","mysql_introspect","mysql_query","mysql_update","neo4j_create","neo4j_delete","neo4j_execute","neo4j_introspect","neo4j_merge","neo4j_query","neo4j_update","netsuite_attach_record","netsuite_batch_create_records","netsuite_batch_delete_records","netsuite_batch_get_records","netsuite_batch_update_records","netsuite_batch_upsert_records","netsuite_create_record","netsuite_delete_record","netsuite_detach_record","netsuite_execute_action","netsuite_execute_dataset","netsuite_execute_suiteql","netsuite_get_async_result","netsuite_get_async_status","netsuite_get_governance_limits","netsuite_get_record","netsuite_get_record_form","netsuite_get_record_metadata","netsuite_get_select_options","netsuite_get_server_time","netsuite_get_subresource","netsuite_list_datasets","netsuite_list_record_types","netsuite_list_records","netsuite_transform_record","netsuite_update_record","netsuite_upsert_record","neverbounce_get_credits","neverbounce_verify_email","new_relic_create_deployment_event","new_relic_get_entity","new_relic_nrql_query","new_relic_search_entities","notion_add_database_row","notion_add_database_row_v2","notion_append_blocks","notion_append_blocks_v2","notion_create_comment","notion_create_comment_v2","notion_create_database","notion_create_database_v2","notion_create_page","notion_create_page_v2","notion_delete_block","notion_delete_block_v2","notion_list_comments","notion_list_comments_v2","notion_list_users","notion_list_users_v2","notion_query_database","notion_query_database_v2","notion_read","notion_read_database","notion_read_database_v2","notion_read_v2","notion_retrieve_block","notion_retrieve_block_children","notion_retrieve_block_children_v2","notion_retrieve_block_v2","notion_retrieve_user","notion_retrieve_user_v2","notion_search","notion_search_v2","notion_update_block","notion_update_block_v2","notion_update_page","notion_update_page_v2","notion_write","notion_write_v2","obsidian_append_active","obsidian_append_note","obsidian_append_periodic_note","obsidian_create_note","obsidian_delete_note","obsidian_execute_command","obsidian_get_active","obsidian_get_note","obsidian_get_periodic_note","obsidian_list_commands","obsidian_list_files","obsidian_open_file","obsidian_patch_active","obsidian_patch_note","obsidian_search","okta_activate_group_rule","okta_activate_user","okta_add_user_to_group","okta_assign_group_to_app","okta_assign_user_role","okta_assign_user_to_app","okta_clear_user_sessions","okta_create_group","okta_create_group_rule","okta_create_user","okta_deactivate_group_rule","okta_deactivate_user","okta_delete_group","okta_delete_group_rule","okta_delete_user","okta_enroll_factor","okta_get_app","okta_get_factor","okta_get_group","okta_get_group_rule","okta_get_logs","okta_get_session","okta_get_user","okta_list_app_groups","okta_list_app_users","okta_list_apps","okta_list_factors","okta_list_group_members","okta_list_group_rules","okta_list_groups","okta_list_user_roles","okta_list_users","okta_remove_group_from_app","okta_remove_user_from_app","okta_remove_user_from_group","okta_remove_user_role","okta_reset_all_factors","okta_reset_factor","okta_reset_password","okta_revoke_session","okta_suspend_user","okta_unsuspend_user","okta_update_group","okta_update_user","onedrive_copy","onedrive_create_folder","onedrive_create_share_link","onedrive_delete","onedrive_download","onedrive_get_drive_info","onedrive_get_item","onedrive_list","onedrive_move","onedrive_search","onedrive_upload","onepassword_create_item","onepassword_delete_item","onepassword_get_item","onepassword_get_item_file","onepassword_get_vault","onepassword_list_items","onepassword_list_vaults","onepassword_replace_item","onepassword_resolve_secret","onepassword_update_item","openai_embeddings","openai_image","outlook_calendar_create_event","outlook_calendar_delete_event","outlook_calendar_get_event","outlook_calendar_list_events","outlook_calendar_respond","outlook_calendar_update_event","outlook_copy","outlook_create_folder","outlook_delete","outlook_draft","outlook_forward","outlook_get_attachment","outlook_list_attachments","outlook_list_folders","outlook_mark_read","outlook_mark_unread","outlook_move","outlook_read","outlook_reply","outlook_reply_all","outlook_search","outlook_send","outlook_update_message","pagerduty_add_note","pagerduty_create_incident","pagerduty_get_incident","pagerduty_get_service","pagerduty_list_escalation_policies","pagerduty_list_incident_alerts","pagerduty_list_incidents","pagerduty_list_oncalls","pagerduty_list_schedules","pagerduty_list_services","pagerduty_list_users","pagerduty_merge_incidents","pagerduty_send_event","pagerduty_snooze_incident","pagerduty_update_incident","parallel_deep_research","parallel_extract","parallel_search","pdl_autocomplete","pdl_bulk_company_enrich","pdl_bulk_person_enrich","pdl_clean_company","pdl_clean_location","pdl_clean_school","pdl_company_enrich","pdl_company_search","pdl_person_enrich","pdl_person_identify","pdl_person_search","perplexity_chat","perplexity_search","persona_approve_inquiry","persona_create_account","persona_create_inquiry","persona_create_report","persona_decline_inquiry","persona_expire_inquiry","persona_generate_inquiry_link","persona_get_account","persona_get_case","persona_get_document","persona_get_inquiry","persona_get_report","persona_get_verification","persona_import_accounts","persona_list_accounts","persona_list_cases","persona_list_inquiries","persona_list_inquiry_templates","persona_list_reports","persona_mark_inquiry_for_review","persona_print_inquiry_pdf","persona_redact_account","persona_redact_inquiry","persona_resume_inquiry","persona_update_account","persona_update_inquiry","pinecone_delete_vectors","pinecone_describe_index","pinecone_describe_index_stats","pinecone_fetch","pinecone_generate_embeddings","pinecone_list_indexes","pinecone_list_vector_ids","pinecone_search_text","pinecone_search_vector","pinecone_update_vector","pinecone_upsert_text","pipedrive_create_activity","pipedrive_create_deal","pipedrive_create_lead","pipedrive_create_project","pipedrive_delete_lead","pipedrive_get_activities","pipedrive_get_all_deals","pipedrive_get_deal","pipedrive_get_files","pipedrive_get_leads","pipedrive_get_mail_messages","pipedrive_get_mail_thread","pipedrive_get_pipeline_deals","pipedrive_get_pipelines","pipedrive_get_projects","pipedrive_update_activity","pipedrive_update_deal","pipedrive_update_lead","pitchbook_company_active_investors","pitchbook_company_bio","pitchbook_company_deal_service_providers","pitchbook_company_deals","pitchbook_company_financials","pitchbook_company_general_service_providers","pitchbook_company_industries","pitchbook_company_investors","pitchbook_company_most_recent_debt_financing","pitchbook_company_most_recent_financials","pitchbook_company_most_recent_financing","pitchbook_company_search","pitchbook_company_similar_companies","pitchbook_company_social_analytics","pitchbook_company_updates","pitchbook_company_vc_exit_predictions","pitchbook_contracts_history","pitchbook_cost_of_calls","pitchbook_credit_history","pitchbook_credit_news","pitchbook_credit_news_bulk","pitchbook_credit_news_most_recent","pitchbook_credit_news_search","pitchbook_deal_bio","pitchbook_deal_cap_table_history","pitchbook_deal_debt_lenders","pitchbook_deal_detailed","pitchbook_deal_investors","pitchbook_deal_multiples","pitchbook_deal_search","pitchbook_deal_service_providers","pitchbook_deal_stock_info","pitchbook_deal_tranche_info","pitchbook_deal_updates","pitchbook_deal_valuation","pitchbook_entity_affiliates","pitchbook_entity_locations","pitchbook_entity_news","pitchbook_entity_people","pitchbook_entity_updates","pitchbook_fund_active_investments","pitchbook_fund_benchmark","pitchbook_fund_bio","pitchbook_fund_cash_flows","pitchbook_fund_commitments","pitchbook_fund_investment_preferences","pitchbook_fund_investments","pitchbook_fund_performance","pitchbook_fund_search","pitchbook_fund_team","pitchbook_fund_updates","pitchbook_investor_active_investments","pitchbook_investor_bio","pitchbook_investor_board_seats","pitchbook_investor_deal_service_providers","pitchbook_investor_funds","pitchbook_investor_general_service_providers","pitchbook_investor_investments","pitchbook_investor_last_closed_fund","pitchbook_investor_preferences","pitchbook_investor_search","pitchbook_investor_updates","pitchbook_limited_partner_actual_allocations","pitchbook_limited_partner_bio","pitchbook_limited_partner_commitment_aggregates","pitchbook_limited_partner_commitment_preferences","pitchbook_limited_partner_commitments_detailed","pitchbook_limited_partner_search","pitchbook_limited_partner_service_providers","pitchbook_limited_partner_target_allocations","pitchbook_limited_partner_updates","pitchbook_lookup_table_structure","pitchbook_lookup_tables","pitchbook_patent_detailed","pitchbook_patent_search","pitchbook_people_search","pitchbook_person_bio","pitchbook_person_contact","pitchbook_person_education_work","pitchbook_sandbox_entities","pitchbook_search","pitchbook_service_provider_bio","pitchbook_service_provider_search","pitchbook_service_provider_updates","pitchbook_serviced_companies","pitchbook_serviced_deals","pitchbook_serviced_funds","pitchbook_serviced_investors","pitchbook_serviced_limited_partners","pitchbook_shared_search","pitchbook_usage_report","polymarket_get_activity","polymarket_get_event","polymarket_get_events","polymarket_get_holders","polymarket_get_last_trade_price","polymarket_get_leaderboard","polymarket_get_market","polymarket_get_markets","polymarket_get_midpoint","polymarket_get_orderbook","polymarket_get_positions","polymarket_get_price","polymarket_get_price_history","polymarket_get_series","polymarket_get_series_by_id","polymarket_get_spread","polymarket_get_tags","polymarket_get_tick_size","polymarket_get_trades","polymarket_search","postgresql_delete","postgresql_execute","postgresql_insert","postgresql_introspect","postgresql_query","postgresql_update","posthog_batch_events","posthog_capture_event","posthog_create_annotation","posthog_create_cohort","posthog_create_dashboard","posthog_create_experiment","posthog_create_feature_flag","posthog_create_insight","posthog_create_survey","posthog_delete_feature_flag","posthog_delete_person","posthog_delete_survey","posthog_evaluate_flags","posthog_get_cohort","posthog_get_dashboard","posthog_get_event_definition","posthog_get_experiment","posthog_get_feature_flag","posthog_get_insight","posthog_get_organization","posthog_get_person","posthog_get_project","posthog_get_property_definition","posthog_get_session_recording","posthog_get_survey","posthog_list_actions","posthog_list_annotations","posthog_list_cohorts","posthog_list_dashboards","posthog_list_event_definitions","posthog_list_experiments","posthog_list_feature_flags","posthog_list_insights","posthog_list_organizations","posthog_list_persons","posthog_list_projects","posthog_list_property_definitions","posthog_list_recording_playlists","posthog_list_session_recordings","posthog_list_surveys","posthog_query","posthog_update_cohort","posthog_update_event_definition","posthog_update_experiment","posthog_update_feature_flag","posthog_update_insight","posthog_update_property_definition","posthog_update_survey","profound_bot_logs","profound_bots_report","profound_category_assets","profound_category_personas","profound_category_prompts","profound_category_tags","profound_category_topics","profound_citation_prompts","profound_citations_report","profound_list_assets","profound_list_categories","profound_list_domains","profound_list_models","profound_list_optimizations","profound_list_personas","profound_list_regions","profound_optimization_analysis","profound_prompt_answers","profound_prompt_volume","profound_query_fanouts","profound_raw_logs","profound_referrals_report","profound_sentiment_report","profound_visibility_report","prospeo_account_information","prospeo_bulk_enrich_company","prospeo_bulk_enrich_person","prospeo_enrich_company","prospeo_enrich_person","prospeo_search_company","prospeo_search_person","prospeo_search_suggestions","pulse_parser","pulse_parser_v2","qdrant_fetch_points","qdrant_search_vector","qdrant_upsert_points","quartr_get_audio","quartr_get_company","quartr_get_event","quartr_get_event_summary","quartr_get_report","quartr_get_slide_deck","quartr_get_transcript","quartr_list_audio","quartr_list_companies","quartr_list_document_types","quartr_list_documents","quartr_list_event_types","quartr_list_events","quartr_list_live_events","quartr_list_reports","quartr_list_slide_decks","quartr_list_transcripts","quiver_image_to_svg","quiver_list_models","quiver_text_to_svg","rabbitmq_create_binding","rabbitmq_create_exchange","rabbitmq_create_policy","rabbitmq_create_queue","rabbitmq_delete_binding","rabbitmq_delete_exchange","rabbitmq_delete_policy","rabbitmq_delete_queue","rabbitmq_get_exchange","rabbitmq_get_messages","rabbitmq_get_overview","rabbitmq_get_queue","rabbitmq_health_check","rabbitmq_list_bindings","rabbitmq_list_channels","rabbitmq_list_connections","rabbitmq_list_consumers","rabbitmq_list_exchange_bindings","rabbitmq_list_exchanges","rabbitmq_list_nodes","rabbitmq_list_policies","rabbitmq_list_queues","rabbitmq_list_vhosts","rabbitmq_publish_message","rabbitmq_purge_queue","railway_create_environment","railway_create_project","railway_create_service","railway_delete_environment","railway_delete_project","railway_delete_service","railway_delete_variable","railway_deploy_service","railway_get_deployment","railway_get_deployment_logs","railway_get_project","railway_list_deployments","railway_list_project_members","railway_list_projects","railway_list_variables","railway_restart_deployment","railway_rollback_deployment","railway_transfer_project","railway_update_project","railway_upsert_variable","rb2b_credit_check","rb2b_email_to_activity","rb2b_hem_to_best_linkedin","rb2b_hem_to_business_profile","rb2b_hem_to_linkedin","rb2b_hem_to_maid","rb2b_ip_to_company","rb2b_ip_to_hem","rb2b_ip_to_maid","rb2b_linkedin_slug_search","rb2b_linkedin_to_best_personal_email","rb2b_linkedin_to_business_profile","rb2b_linkedin_to_hashed_emails","rb2b_linkedin_to_mobile_phone","rb2b_linkedin_to_personal_email","rds_delete","rds_execute","rds_insert","rds_introspect","rds_query","rds_update","reddit_delete","reddit_edit","reddit_get_comments","reddit_get_controversial","reddit_get_info","reddit_get_me","reddit_get_messages","reddit_get_posts","reddit_get_saved","reddit_get_subreddit_info","reddit_get_subreddit_rules","reddit_get_user","reddit_get_user_comments","reddit_get_user_posts","reddit_hide","reddit_hot_posts","reddit_list_my_subreddits","reddit_lock","reddit_mark_all_read","reddit_mark_read","reddit_marknsfw","reddit_mod_approve","reddit_mod_distinguish","reddit_mod_remove","reddit_mod_sticky","reddit_reply","reddit_report","reddit_save","reddit_search","reddit_search_subreddits","reddit_send_message","reddit_submit_post","reddit_subscribe","reddit_unhide","reddit_unlock","reddit_unmarknsfw","reddit_unsave","reddit_vote","redis_command","redis_delete","redis_exists","redis_expire","redis_get","redis_hdel","redis_hget","redis_hgetall","redis_hset","redis_incr","redis_incrby","redis_keys","redis_llen","redis_lpop","redis_lpush","redis_lrange","redis_persist","redis_rpop","redis_rpush","redis_set","redis_setnx","redis_ttl","reducto_parser","reducto_parser_v2","resend_cancel_email","resend_create_audience","resend_create_broadcast","resend_create_contact","resend_delete_audience","resend_delete_contact","resend_get_audience","resend_get_broadcast","resend_get_contact","resend_get_email","resend_list_audiences","resend_list_contacts","resend_list_domains","resend_send","resend_send_broadcast","resend_update_contact","revenuecat_create_purchase","revenuecat_defer_google_subscription","revenuecat_delete_customer","revenuecat_get_customer","revenuecat_grant_entitlement","revenuecat_list_offerings","revenuecat_refund_google_subscription","revenuecat_revoke_entitlement","revenuecat_revoke_google_subscription","revenuecat_update_subscriber_attributes","rippling_bulk_create_custom_object_records","rippling_bulk_delete_custom_object_records","rippling_bulk_update_custom_object_records","rippling_create_business_partner","rippling_create_business_partner_group","rippling_create_custom_app","rippling_create_custom_object","rippling_create_custom_object_field","rippling_create_custom_object_record","rippling_create_custom_page","rippling_create_custom_setting","rippling_create_department","rippling_create_draft_hires","rippling_create_object_category","rippling_create_title","rippling_create_work_location","rippling_delete_business_partner","rippling_delete_business_partner_group","rippling_delete_custom_app","rippling_delete_custom_object","rippling_delete_custom_object_field","rippling_delete_custom_object_record","rippling_delete_custom_page","rippling_delete_custom_setting","rippling_delete_object_category","rippling_delete_title","rippling_delete_work_location","rippling_get_business_partner","rippling_get_business_partner_group","rippling_get_current_user","rippling_get_custom_app","rippling_get_custom_object","rippling_get_custom_object_field","rippling_get_custom_object_record","rippling_get_custom_object_record_by_external_id","rippling_get_custom_page","rippling_get_custom_setting","rippling_get_department","rippling_get_employment_type","rippling_get_job_function","rippling_get_object_category","rippling_get_report_run","rippling_get_supergroup","rippling_get_team","rippling_get_title","rippling_get_user","rippling_get_work_location","rippling_get_worker","rippling_list_business_partner_groups","rippling_list_business_partners","rippling_list_companies","rippling_list_custom_apps","rippling_list_custom_fields","rippling_list_custom_object_fields","rippling_list_custom_object_records","rippling_list_custom_objects","rippling_list_custom_pages","rippling_list_custom_settings","rippling_list_departments","rippling_list_employment_types","rippling_list_entitlements","rippling_list_job_functions","rippling_list_object_categories","rippling_list_supergroup_exclusion_members","rippling_list_supergroup_inclusion_members","rippling_list_supergroup_members","rippling_list_supergroups","rippling_list_teams","rippling_list_titles","rippling_list_users","rippling_list_work_locations","rippling_list_workers","rippling_query_custom_object_records","rippling_trigger_report_run","rippling_update_custom_app","rippling_update_custom_object","rippling_update_custom_object_field","rippling_update_custom_object_record","rippling_update_custom_page","rippling_update_custom_setting","rippling_update_department","rippling_update_object_category","rippling_update_supergroup_exclusion_members","rippling_update_supergroup_inclusion_members","rippling_update_title","rippling_update_work_location","rocketlane_add_field_option","rocketlane_add_project_members","rocketlane_add_task_assignees","rocketlane_add_task_dependencies","rocketlane_add_task_followers","rocketlane_archive_project","rocketlane_assign_placeholders","rocketlane_create_field","rocketlane_create_phase","rocketlane_create_project","rocketlane_create_space","rocketlane_create_space_document","rocketlane_create_task","rocketlane_create_time_entry","rocketlane_create_time_off","rocketlane_delete_field","rocketlane_delete_phase","rocketlane_delete_project","rocketlane_delete_space","rocketlane_delete_space_document","rocketlane_delete_task","rocketlane_delete_time_entry","rocketlane_delete_time_off","rocketlane_get_field","rocketlane_get_invoice","rocketlane_get_invoice_line_items","rocketlane_get_invoice_payments","rocketlane_get_phase","rocketlane_get_project","rocketlane_get_space","rocketlane_get_space_document","rocketlane_get_task","rocketlane_get_time_entry","rocketlane_get_time_off","rocketlane_get_user","rocketlane_import_template","rocketlane_list_fields","rocketlane_list_invoices","rocketlane_list_phases","rocketlane_list_placeholders","rocketlane_list_projects","rocketlane_list_resource_allocations","rocketlane_list_space_documents","rocketlane_list_spaces","rocketlane_list_tasks","rocketlane_list_time_entries","rocketlane_list_time_entry_categories","rocketlane_list_time_offs","rocketlane_list_users","rocketlane_move_task_to_phase","rocketlane_remove_project_members","rocketlane_remove_task_assignees","rocketlane_remove_task_dependencies","rocketlane_remove_task_followers","rocketlane_search_time_entries","rocketlane_unassign_placeholders","rocketlane_update_field","rocketlane_update_field_option","rocketlane_update_phase","rocketlane_update_project","rocketlane_update_space","rocketlane_update_space_document","rocketlane_update_task","rocketlane_update_time_entry","rootly_acknowledge_alert","rootly_add_incident_event","rootly_add_subscribers","rootly_assign_incident_role","rootly_create_action_item","rootly_create_alert","rootly_create_incident","rootly_create_status_page_event","rootly_delete_action_item","rootly_delete_incident","rootly_escalate_alert","rootly_get_alert","rootly_get_incident","rootly_list_action_items","rootly_list_alerts","rootly_list_causes","rootly_list_environments","rootly_list_escalation_policies","rootly_list_functionalities","rootly_list_incident_events","rootly_list_incident_roles","rootly_list_incident_types","rootly_list_incidents","rootly_list_on_calls","rootly_list_playbooks","rootly_list_retrospectives","rootly_list_schedules","rootly_list_services","rootly_list_severities","rootly_list_teams","rootly_list_users","rootly_mitigate_incident","rootly_remove_subscribers","rootly_resolve_alert","rootly_resolve_incident","rootly_run_workflow","rootly_snooze_alert","rootly_unassign_incident_role","rootly_update_action_item","rootly_update_alert","rootly_update_incident","s3_copy_object","s3_create_bucket","s3_delete_bucket","s3_delete_object","s3_delete_objects","s3_get_object","s3_head_object","s3_list_buckets","s3_list_objects","s3_presigned_url","s3_put_object","salesforce_create_account","salesforce_create_case","salesforce_create_contact","salesforce_create_custom_field","salesforce_create_custom_object","salesforce_create_lead","salesforce_create_opportunity","salesforce_create_task","salesforce_delete_account","salesforce_delete_case","salesforce_delete_contact","salesforce_delete_custom_field","salesforce_delete_lead","salesforce_delete_opportunity","salesforce_delete_task","salesforce_describe_object","salesforce_get_accounts","salesforce_get_cases","salesforce_get_contacts","salesforce_get_dashboard","salesforce_get_leads","salesforce_get_opportunities","salesforce_get_report","salesforce_get_tasks","salesforce_list_dashboards","salesforce_list_objects","salesforce_list_report_types","salesforce_list_reports","salesforce_query","salesforce_query_more","salesforce_refresh_dashboard","salesforce_run_report","salesforce_tooling_query","salesforce_update_account","salesforce_update_case","salesforce_update_contact","salesforce_update_custom_field","salesforce_update_lead","salesforce_update_opportunity","salesforce_update_task","sap_concur_approve_expense_report","sap_concur_associate_attendees","sap_concur_create_cash_advance","sap_concur_create_expected_expense","sap_concur_create_expense_report","sap_concur_create_list_item","sap_concur_create_purchase_request","sap_concur_create_quick_expense","sap_concur_create_quick_expense_with_image","sap_concur_create_report_comment","sap_concur_create_travel_request","sap_concur_create_user","sap_concur_delete_expected_expense","sap_concur_delete_expense","sap_concur_delete_expense_report","sap_concur_delete_list_item","sap_concur_delete_travel_request","sap_concur_delete_user","sap_concur_get_allocation","sap_concur_get_budget","sap_concur_get_cash_advance","sap_concur_get_expected_expense","sap_concur_get_expense","sap_concur_get_expense_report","sap_concur_get_itemizations","sap_concur_get_itinerary","sap_concur_get_list","sap_concur_get_list_item","sap_concur_get_purchase_request","sap_concur_get_receipt","sap_concur_get_receipt_status","sap_concur_get_request_cash_advance","sap_concur_get_travel_profile","sap_concur_get_travel_request","sap_concur_get_user","sap_concur_issue_cash_advance","sap_concur_list_allocations","sap_concur_list_attendee_associations","sap_concur_list_budget_categories","sap_concur_list_budgets","sap_concur_list_exceptions","sap_concur_list_expected_expenses","sap_concur_list_expense_reports","sap_concur_list_expenses","sap_concur_list_itineraries","sap_concur_list_list_items","sap_concur_list_lists","sap_concur_list_receipts","sap_concur_list_report_comments","sap_concur_list_reports_to_approve","sap_concur_list_travel_profiles_summary","sap_concur_list_travel_request_comments","sap_concur_list_travel_requests","sap_concur_list_users","sap_concur_move_travel_request","sap_concur_recall_expense_report","sap_concur_remove_all_attendees","sap_concur_search_locations","sap_concur_search_users","sap_concur_send_back_expense_report","sap_concur_submit_expense_report","sap_concur_update_allocation","sap_concur_update_expected_expense","sap_concur_update_expense","sap_concur_update_expense_report","sap_concur_update_list_item","sap_concur_update_travel_request","sap_concur_update_user","sap_concur_upload_exchange_rates","sap_concur_upload_receipt_image","sap_s4hana_create_business_partner","sap_s4hana_create_purchase_order","sap_s4hana_create_purchase_requisition","sap_s4hana_create_sales_order","sap_s4hana_delete_sales_order","sap_s4hana_get_billing_document","sap_s4hana_get_business_partner","sap_s4hana_get_customer","sap_s4hana_get_inbound_delivery","sap_s4hana_get_material_document","sap_s4hana_get_outbound_delivery","sap_s4hana_get_product","sap_s4hana_get_purchase_order","sap_s4hana_get_purchase_requisition","sap_s4hana_get_sales_order","sap_s4hana_get_supplier","sap_s4hana_get_supplier_invoice","sap_s4hana_list_billing_documents","sap_s4hana_list_business_partners","sap_s4hana_list_customers","sap_s4hana_list_inbound_deliveries","sap_s4hana_list_material_documents","sap_s4hana_list_material_stock","sap_s4hana_list_outbound_deliveries","sap_s4hana_list_products","sap_s4hana_list_purchase_orders","sap_s4hana_list_purchase_requisitions","sap_s4hana_list_sales_orders","sap_s4hana_list_supplier_invoices","sap_s4hana_list_suppliers","sap_s4hana_odata_query","sap_s4hana_update_business_partner","sap_s4hana_update_customer","sap_s4hana_update_product","sap_s4hana_update_purchase_order","sap_s4hana_update_purchase_requisition","sap_s4hana_update_sales_order","sap_s4hana_update_supplier","search_tool","secrets_manager_create_secret","secrets_manager_delete_secret","secrets_manager_describe_secret","secrets_manager_get_secret","secrets_manager_list_secrets","secrets_manager_restore_secret","secrets_manager_rotate_secret","secrets_manager_tag_resource","secrets_manager_untag_resource","secrets_manager_update_secret","sendblue_evaluate_service","sendblue_get_message","sendblue_send_group_message","sendblue_send_message","sendblue_send_typing_indicator","sendgrid_add_contact","sendgrid_add_contacts_to_list","sendgrid_create_list","sendgrid_create_template","sendgrid_create_template_version","sendgrid_delete_contacts","sendgrid_delete_list","sendgrid_delete_template","sendgrid_get_contact","sendgrid_get_list","sendgrid_get_template","sendgrid_list_all_lists","sendgrid_list_templates","sendgrid_remove_contacts_from_list","sendgrid_search_contacts","sendgrid_send_mail","sentry_events_get","sentry_events_list","sentry_issues_get","sentry_issues_list","sentry_issues_update","sentry_projects_create","sentry_projects_get","sentry_projects_list","sentry_projects_update","sentry_releases_create","sentry_releases_deploy","sentry_releases_list","sentry_teams_list","serper_search","servicenow_add_incident_comment","servicenow_aggregate","servicenow_close_incident","servicenow_create_change_request","servicenow_create_incident","servicenow_create_record","servicenow_delete_record","servicenow_download_attachment","servicenow_find_user","servicenow_get_change_next_states","servicenow_get_change_request","servicenow_get_ci","servicenow_get_incident","servicenow_get_knowledge_article","servicenow_get_requested_item","servicenow_list_approvals","servicenow_list_attachments","servicenow_list_catalog_items","servicenow_list_change_requests","servicenow_list_change_tasks","servicenow_list_ci_relationships","servicenow_list_group_members","servicenow_list_incidents","servicenow_list_requested_items","servicenow_order_catalog_item","servicenow_read_record","servicenow_resolve_incident","servicenow_search_cis","servicenow_search_knowledge","servicenow_update_approval","servicenow_update_change_request","servicenow_update_change_state","servicenow_update_incident","servicenow_update_record","servicenow_upload_attachment","ses_create_configuration_set","ses_create_email_identity","ses_create_template","ses_delete_email_identity","ses_delete_suppressed_destination","ses_delete_template","ses_get_account","ses_get_email_identity","ses_get_suppressed_destination","ses_get_template","ses_list_identities","ses_list_suppressed_destinations","ses_list_templates","ses_put_suppressed_destination","ses_send_bulk_email","ses_send_custom_verification_email","ses_send_email","ses_send_templated_email","ses_update_template","sftp_delete","sftp_download","sftp_list","sftp_mkdir","sftp_upload","sharepoint_add_list_items","sharepoint_create_list","sharepoint_create_page","sharepoint_delete_file","sharepoint_delete_list_item","sharepoint_delete_page","sharepoint_download_file","sharepoint_get_drive_item","sharepoint_get_list","sharepoint_get_list_item","sharepoint_list_sites","sharepoint_publish_page","sharepoint_read_page","sharepoint_update_list","sharepoint_update_page","sharepoint_upload_file","shopify_adjust_inventory","shopify_cancel_order","shopify_create_customer","shopify_create_fulfillment","shopify_create_product","shopify_delete_customer","shopify_delete_product","shopify_get_collection","shopify_get_customer","shopify_get_inventory_level","shopify_get_order","shopify_get_product","shopify_list_collections","shopify_list_customers","shopify_list_inventory_items","shopify_list_locations","shopify_list_orders","shopify_list_products","shopify_update_customer","shopify_update_order","shopify_update_product","similarweb_bounce_rate","similarweb_page_views","similarweb_pages_per_visit","similarweb_traffic_visits","similarweb_visit_duration","similarweb_website_overview","sixtyfour_enrich_company","sixtyfour_enrich_lead","sixtyfour_find_email","sixtyfour_find_phone","slack_add_reaction","slack_archive_conversation","slack_canvas","slack_create_channel_canvas","slack_create_conversation","slack_delete_canvas","slack_delete_message","slack_delete_scheduled_message","slack_download","slack_edit_canvas","slack_ephemeral_message","slack_get_canvas","slack_get_channel_history","slack_get_channel_info","slack_get_message","slack_get_permalink","slack_get_thread","slack_get_thread_replies","slack_get_user","slack_get_user_presence","slack_invite_to_conversation","slack_list_canvases","slack_list_channels","slack_list_members","slack_list_scheduled_messages","slack_list_users","slack_lookup_canvas_sections","slack_message","slack_message_reader","slack_open_view","slack_publish_view","slack_push_view","slack_remove_reaction","slack_rename_conversation","slack_schedule_message","slack_set_conversation_purpose","slack_set_conversation_topic","slack_set_status","slack_set_suggested_prompts","slack_set_title","slack_update_message","slack_update_view","smartlead_add_email_accounts_to_campaign","smartlead_add_leads_to_campaign","smartlead_create_campaign","smartlead_create_lead_list","smartlead_delete_campaign","smartlead_delete_campaign_webhook","smartlead_delete_lead_from_campaign","smartlead_delete_lead_list","smartlead_duplicate_campaign","smartlead_export_campaign_leads","smartlead_get_campaign","smartlead_get_campaign_analytics","smartlead_get_campaign_analytics_by_date","smartlead_get_campaign_lead_statistics","smartlead_get_campaign_mailbox_statistics","smartlead_get_campaign_sequences","smartlead_get_campaign_statistics","smartlead_get_campaign_top_level_analytics_by_date","smartlead_get_campaign_webhook_summary","smartlead_get_lead_by_email","smartlead_get_lead_by_id","smartlead_get_lead_list","smartlead_get_lead_message_history","smartlead_list_campaign_email_accounts","smartlead_list_campaign_leads","smartlead_list_campaign_webhooks","smartlead_list_campaigns","smartlead_list_clients","smartlead_list_email_accounts","smartlead_list_inbox_replies","smartlead_list_lead_activities","smartlead_list_lead_categories","smartlead_list_lead_lists","smartlead_mark_lead_complete","smartlead_pause_lead","smartlead_remove_email_accounts_from_campaign","smartlead_resume_lead","smartlead_save_campaign_sequences","smartlead_unsubscribe_lead_from_campaign","smartlead_unsubscribe_lead_globally","smartlead_update_campaign_schedule","smartlead_update_campaign_settings","smartlead_update_campaign_status","smartlead_update_lead","smartlead_update_lead_category","smartlead_update_lead_list","smartlead_upsert_campaign_webhook","sms_send","smtp_send_mail","snowflake_alter_warehouse","snowflake_call_procedure","snowflake_cancel_statement","snowflake_cancel_task_run","snowflake_delete_rows","snowflake_execute_sql","snowflake_get_statement","snowflake_get_task","snowflake_get_task_run","snowflake_get_task_run_output","snowflake_get_warehouse","snowflake_insert_rows","snowflake_introspect_schema","snowflake_list_copy_history","snowflake_list_databases","snowflake_list_query_history","snowflake_list_schemas","snowflake_list_tables","snowflake_list_task_runs","snowflake_list_tasks","snowflake_list_warehouses","snowflake_load_data","snowflake_resume_task","snowflake_resume_warehouse","snowflake_run_task","snowflake_suspend_task","snowflake_suspend_warehouse","snowflake_unload_data","snowflake_update_rows","snowflake_upsert_rows","splunk_cancel_search_job","splunk_create_search_job","splunk_dispatch_saved_search","splunk_get_fired_alerts","splunk_get_saved_search","splunk_get_search_job","splunk_get_search_results","splunk_list_apps","splunk_list_fired_alerts","splunk_list_indexes","splunk_list_saved_searches","splunk_run_search","sportmonks_core_get_cities","sportmonks_core_get_city","sportmonks_core_get_continent","sportmonks_core_get_continents","sportmonks_core_get_countries","sportmonks_core_get_country","sportmonks_core_get_entity_filters","sportmonks_core_get_my_usage","sportmonks_core_get_region","sportmonks_core_get_regions","sportmonks_core_get_timezones","sportmonks_core_get_type","sportmonks_core_get_type_by_entity","sportmonks_core_get_types","sportmonks_core_search_cities","sportmonks_core_search_countries","sportmonks_core_search_regions","sportmonks_football_expected_by_player","sportmonks_football_expected_by_team","sportmonks_football_get_all_commentaries","sportmonks_football_get_all_fixtures","sportmonks_football_get_all_players","sportmonks_football_get_all_rivals","sportmonks_football_get_all_teams","sportmonks_football_get_all_transfer_rumours","sportmonks_football_get_all_transfers","sportmonks_football_get_brackets_by_season","sportmonks_football_get_coach","sportmonks_football_get_coaches","sportmonks_football_get_coaches_by_country","sportmonks_football_get_commentaries_by_fixture","sportmonks_football_get_current_leagues_by_team","sportmonks_football_get_expected_lineups_by_player","sportmonks_football_get_expected_lineups_by_team","sportmonks_football_get_extended_team_squad","sportmonks_football_get_fixture","sportmonks_football_get_fixtures_by_date","sportmonks_football_get_fixtures_by_date_range","sportmonks_football_get_fixtures_by_date_range_for_team","sportmonks_football_get_fixtures_by_ids","sportmonks_football_get_grouped_standings_by_round","sportmonks_football_get_head_to_head","sportmonks_football_get_inplay_livescores","sportmonks_football_get_latest_coaches","sportmonks_football_get_latest_fixtures","sportmonks_football_get_latest_livescores","sportmonks_football_get_latest_players","sportmonks_football_get_latest_totw","sportmonks_football_get_latest_transfers","sportmonks_football_get_league","sportmonks_football_get_leagues","sportmonks_football_get_leagues_by_country","sportmonks_football_get_leagues_by_date","sportmonks_football_get_leagues_by_team","sportmonks_football_get_live_leagues","sportmonks_football_get_live_probabilities","sportmonks_football_get_live_probabilities_by_fixture","sportmonks_football_get_live_standings_by_league","sportmonks_football_get_livescores","sportmonks_football_get_match_facts","sportmonks_football_get_match_facts_by_date_range","sportmonks_football_get_match_facts_by_fixture","sportmonks_football_get_match_facts_by_league","sportmonks_football_get_past_fixtures_by_tv_station","sportmonks_football_get_player","sportmonks_football_get_players_by_country","sportmonks_football_get_postmatch_news","sportmonks_football_get_postmatch_news_by_season","sportmonks_football_get_predictability_by_league","sportmonks_football_get_prematch_news","sportmonks_football_get_prematch_news_by_season","sportmonks_football_get_prematch_news_upcoming","sportmonks_football_get_probabilities","sportmonks_football_get_probabilities_by_fixture","sportmonks_football_get_referee","sportmonks_football_get_referees","sportmonks_football_get_referees_by_country","sportmonks_football_get_referees_by_season","sportmonks_football_get_rivals_by_team","sportmonks_football_get_round","sportmonks_football_get_round_statistics","sportmonks_football_get_rounds","sportmonks_football_get_rounds_by_season","sportmonks_football_get_schedules_by_season","sportmonks_football_get_schedules_by_season_and_team","sportmonks_football_get_schedules_by_team","sportmonks_football_get_season","sportmonks_football_get_seasons","sportmonks_football_get_seasons_by_team","sportmonks_football_get_stage","sportmonks_football_get_stage_statistics","sportmonks_football_get_stages","sportmonks_football_get_stages_by_season","sportmonks_football_get_standing_corrections_by_season","sportmonks_football_get_standings","sportmonks_football_get_standings_by_round","sportmonks_football_get_standings_by_season","sportmonks_football_get_state","sportmonks_football_get_states","sportmonks_football_get_team","sportmonks_football_get_team_rankings","sportmonks_football_get_team_rankings_by_date","sportmonks_football_get_team_rankings_by_team","sportmonks_football_get_team_squad","sportmonks_football_get_team_squad_by_season","sportmonks_football_get_teams_by_country","sportmonks_football_get_teams_by_season","sportmonks_football_get_topscorers_by_season","sportmonks_football_get_topscorers_by_stage","sportmonks_football_get_totw","sportmonks_football_get_totw_by_round","sportmonks_football_get_transfer","sportmonks_football_get_transfer_rumour","sportmonks_football_get_transfer_rumours_between_dates","sportmonks_football_get_transfer_rumours_by_player","sportmonks_football_get_transfer_rumours_by_team","sportmonks_football_get_transfers_between_dates","sportmonks_football_get_transfers_by_player","sportmonks_football_get_transfers_by_team","sportmonks_football_get_tv_station","sportmonks_football_get_tv_stations","sportmonks_football_get_tv_stations_by_fixture","sportmonks_football_get_upcoming_fixtures_by_market","sportmonks_football_get_upcoming_fixtures_by_tv_station","sportmonks_football_get_value_bets","sportmonks_football_get_value_bets_by_fixture","sportmonks_football_get_venue","sportmonks_football_get_venues","sportmonks_football_get_venues_by_season","sportmonks_football_search_coaches","sportmonks_football_search_fixtures","sportmonks_football_search_leagues","sportmonks_football_search_players","sportmonks_football_search_referees","sportmonks_football_search_rounds","sportmonks_football_search_seasons","sportmonks_football_search_stages","sportmonks_football_search_teams","sportmonks_football_search_venues","sportmonks_motorsport_get_all_fixtures","sportmonks_motorsport_get_current_leagues_by_team","sportmonks_motorsport_get_driver","sportmonks_motorsport_get_driver_standings","sportmonks_motorsport_get_driver_standings_by_season","sportmonks_motorsport_get_drivers","sportmonks_motorsport_get_drivers_by_country","sportmonks_motorsport_get_drivers_by_season","sportmonks_motorsport_get_fixture","sportmonks_motorsport_get_fixtures_by_date","sportmonks_motorsport_get_fixtures_by_date_range","sportmonks_motorsport_get_fixtures_by_ids","sportmonks_motorsport_get_laps_by_fixture","sportmonks_motorsport_get_laps_by_fixture_and_driver","sportmonks_motorsport_get_laps_by_fixture_and_lap","sportmonks_motorsport_get_latest_laps_by_fixture","sportmonks_motorsport_get_latest_pitstops_by_fixture","sportmonks_motorsport_get_latest_stints_by_fixture","sportmonks_motorsport_get_latest_updated_drivers","sportmonks_motorsport_get_latest_updated_fixtures","sportmonks_motorsport_get_league","sportmonks_motorsport_get_leagues","sportmonks_motorsport_get_leagues_by_country","sportmonks_motorsport_get_leagues_by_date","sportmonks_motorsport_get_leagues_by_live","sportmonks_motorsport_get_leagues_by_team","sportmonks_motorsport_get_livescores","sportmonks_motorsport_get_pitstops_by_fixture","sportmonks_motorsport_get_pitstops_by_fixture_and_driver","sportmonks_motorsport_get_pitstops_by_fixture_and_lap","sportmonks_motorsport_get_race_results_by_season_and_driver","sportmonks_motorsport_get_race_results_by_season_and_team","sportmonks_motorsport_get_schedules_by_season","sportmonks_motorsport_get_season","sportmonks_motorsport_get_seasons","sportmonks_motorsport_get_stage","sportmonks_motorsport_get_stages","sportmonks_motorsport_get_stages_by_season","sportmonks_motorsport_get_state","sportmonks_motorsport_get_states","sportmonks_motorsport_get_stints_by_fixture","sportmonks_motorsport_get_stints_by_fixture_and_driver","sportmonks_motorsport_get_stints_by_fixture_and_stint","sportmonks_motorsport_get_team","sportmonks_motorsport_get_team_standings","sportmonks_motorsport_get_team_standings_by_season","sportmonks_motorsport_get_teams","sportmonks_motorsport_get_teams_by_country","sportmonks_motorsport_get_teams_by_season","sportmonks_motorsport_get_venue","sportmonks_motorsport_get_venues","sportmonks_motorsport_get_venues_by_season","sportmonks_motorsport_search_drivers","sportmonks_motorsport_search_leagues","sportmonks_motorsport_search_stages","sportmonks_motorsport_search_teams","sportmonks_motorsport_search_venues","sportmonks_odds_get_all_historical_odds","sportmonks_odds_get_all_inplay_odds","sportmonks_odds_get_all_pre_match_odds","sportmonks_odds_get_all_premium_odds","sportmonks_odds_get_bookmaker","sportmonks_odds_get_bookmaker_event_ids_by_fixture","sportmonks_odds_get_bookmakers","sportmonks_odds_get_bookmakers_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker","sportmonks_odds_get_inplay_odds_by_fixture_and_market","sportmonks_odds_get_last_updated_inplay_odds","sportmonks_odds_get_last_updated_pre_match_odds","sportmonks_odds_get_market","sportmonks_odds_get_markets","sportmonks_odds_get_pre_match_odds_by_fixture","sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker","sportmonks_odds_get_pre_match_odds_by_fixture_and_market","sportmonks_odds_get_premium_odds_by_fixture","sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker","sportmonks_odds_get_premium_odds_by_fixture_and_market","sportmonks_odds_get_updated_historical_odds_between","sportmonks_odds_get_updated_premium_odds_between","sportmonks_odds_search_bookmakers","sportmonks_odds_search_markets","spotify_add_playlist_cover","spotify_add_to_queue","spotify_add_tracks_to_playlist","spotify_check_following","spotify_check_playlist_followers","spotify_check_saved_albums","spotify_check_saved_audiobooks","spotify_check_saved_episodes","spotify_check_saved_shows","spotify_check_saved_tracks","spotify_create_playlist","spotify_follow_artists","spotify_follow_playlist","spotify_get_album","spotify_get_album_tracks","spotify_get_albums","spotify_get_artist","spotify_get_artist_albums","spotify_get_artist_top_tracks","spotify_get_artists","spotify_get_audiobook","spotify_get_audiobook_chapters","spotify_get_audiobooks","spotify_get_categories","spotify_get_current_user","spotify_get_currently_playing","spotify_get_devices","spotify_get_episode","spotify_get_episodes","spotify_get_followed_artists","spotify_get_markets","spotify_get_new_releases","spotify_get_playback_state","spotify_get_playlist","spotify_get_playlist_cover","spotify_get_playlist_tracks","spotify_get_queue","spotify_get_recently_played","spotify_get_saved_albums","spotify_get_saved_audiobooks","spotify_get_saved_episodes","spotify_get_saved_shows","spotify_get_saved_tracks","spotify_get_show","spotify_get_show_episodes","spotify_get_shows","spotify_get_top_artists","spotify_get_top_tracks","spotify_get_track","spotify_get_tracks","spotify_get_user_playlists","spotify_get_user_profile","spotify_pause","spotify_play","spotify_remove_saved_albums","spotify_remove_saved_audiobooks","spotify_remove_saved_episodes","spotify_remove_saved_shows","spotify_remove_saved_tracks","spotify_remove_tracks_from_playlist","spotify_reorder_playlist_items","spotify_replace_playlist_items","spotify_save_albums","spotify_save_audiobooks","spotify_save_episodes","spotify_save_shows","spotify_save_tracks","spotify_search","spotify_seek","spotify_set_repeat","spotify_set_shuffle","spotify_set_volume","spotify_skip_next","spotify_skip_previous","spotify_transfer_playback","spotify_unfollow_artists","spotify_unfollow_playlist","spotify_update_playlist","sqs_send","square_batch_retrieve_inventory_counts","square_cancel_invoice","square_cancel_payment","square_complete_payment","square_create_catalog_image","square_create_customer","square_create_invoice","square_create_order","square_create_payment","square_delete_catalog_object","square_delete_customer","square_delete_invoice","square_get_catalog_object","square_get_customer","square_get_invoice","square_get_location","square_get_order","square_get_payment","square_get_refund","square_list_catalog","square_list_customers","square_list_invoices","square_list_locations","square_list_payments","square_list_refunds","square_pay_order","square_publish_invoice","square_refund_payment","square_search_catalog_objects","square_search_customers","square_search_invoices","square_search_orders","square_update_customer","square_upsert_catalog_object","ssh_check_command_exists","ssh_check_file_exists","ssh_create_directory","ssh_delete_file","ssh_download_file","ssh_execute_command","ssh_execute_script","ssh_get_system_info","ssh_list_directory","ssh_move_rename","ssh_read_file_content","ssh_upload_file","ssh_write_file_content","stagehand_agent","stagehand_extract","stripe_cancel_payment_intent","stripe_cancel_subscription","stripe_capture_charge","stripe_capture_payment_intent","stripe_confirm_payment_intent","stripe_create_charge","stripe_create_customer","stripe_create_invoice","stripe_create_payment_intent","stripe_create_price","stripe_create_product","stripe_create_subscription","stripe_delete_customer","stripe_delete_invoice","stripe_delete_product","stripe_finalize_invoice","stripe_list_charges","stripe_list_customers","stripe_list_events","stripe_list_invoices","stripe_list_payment_intents","stripe_list_prices","stripe_list_products","stripe_list_subscriptions","stripe_pay_invoice","stripe_resume_subscription","stripe_retrieve_charge","stripe_retrieve_customer","stripe_retrieve_event","stripe_retrieve_invoice","stripe_retrieve_payment_intent","stripe_retrieve_price","stripe_retrieve_product","stripe_retrieve_subscription","stripe_search_charges","stripe_search_customers","stripe_search_invoices","stripe_search_payment_intents","stripe_search_prices","stripe_search_products","stripe_search_subscriptions","stripe_send_invoice","stripe_update_charge","stripe_update_customer","stripe_update_invoice","stripe_update_payment_intent","stripe_update_price","stripe_update_product","stripe_update_subscription","stripe_void_invoice","sts_assume_role","sts_assume_role_with_saml","sts_assume_role_with_web_identity","sts_get_access_key_info","sts_get_caller_identity","sts_get_session_token","stt_assemblyai","stt_assemblyai_v2","stt_deepgram","stt_deepgram_v2","stt_elevenlabs","stt_elevenlabs_v2","stt_gemini","stt_gemini_v2","stt_whisper","stt_whisper_v2","supabase_count","supabase_delete","supabase_get_row","supabase_insert","supabase_introspect","supabase_invoke_function","supabase_query","supabase_rpc","supabase_storage_copy","supabase_storage_create_bucket","supabase_storage_create_signed_upload_url","supabase_storage_create_signed_url","supabase_storage_delete","supabase_storage_delete_bucket","supabase_storage_download","supabase_storage_empty_bucket","supabase_storage_get_public_url","supabase_storage_list","supabase_storage_list_buckets","supabase_storage_move","supabase_storage_update_bucket","supabase_storage_upload","supabase_text_search","supabase_update","supabase_upsert","supabase_vector_search","table_batch_insert_rows","table_create","table_delete_row","table_delete_rows_by_filter","table_get_row","table_get_schema","table_insert_row","table_list","table_query_rows","table_query_rows_v2","table_update_row","table_update_rows_by_filter","table_upsert_row","tailscale_authorize_device","tailscale_create_auth_key","tailscale_delete_auth_key","tailscale_delete_device","tailscale_delete_user","tailscale_expire_device_key","tailscale_get_acl","tailscale_get_auth_key","tailscale_get_device","tailscale_get_device_routes","tailscale_get_dns_preferences","tailscale_get_dns_searchpaths","tailscale_list_auth_keys","tailscale_list_devices","tailscale_list_dns_nameservers","tailscale_list_users","tailscale_set_acl","tailscale_set_device_routes","tailscale_set_device_tags","tailscale_set_dns_nameservers","tailscale_set_dns_preferences","tailscale_set_dns_searchpaths","tailscale_suspend_user","tailscale_update_device_key","tavily_crawl","tavily_extract","tavily_map","tavily_search","telegram_copy_message","telegram_delete_message","telegram_edit_message_text","telegram_forward_message","telegram_get_chat","telegram_get_chat_member","telegram_message","telegram_pin_message","telegram_send_animation","telegram_send_audio","telegram_send_chat_action","telegram_send_contact","telegram_send_document","telegram_send_location","telegram_send_photo","telegram_send_poll","telegram_send_video","telegram_set_message_reaction","telegram_unpin_message","temporal_cancel_workflow","temporal_count_workflows","temporal_create_schedule","temporal_delete_schedule","temporal_describe_schedule","temporal_describe_task_queue","temporal_describe_workflow","temporal_get_workflow_history","temporal_list_schedules","temporal_list_workflows","temporal_pause_schedule","temporal_query_workflow","temporal_reset_workflow","temporal_signal_with_start","temporal_signal_workflow","temporal_start_workflow","temporal_terminate_workflow","temporal_trigger_schedule","temporal_unpause_schedule","temporal_update_workflow","textract_analyze_expense","textract_analyze_id","textract_parser","textract_parser_v2","thinking_tool","thrive_add_audience_managers","thrive_add_audience_members","thrive_add_user_tags","thrive_create_assignment","thrive_create_audience","thrive_create_completion","thrive_create_user","thrive_delete_assignment","thrive_delete_audience","thrive_delete_user","thrive_get_activity","thrive_get_assignment","thrive_get_audience","thrive_get_completion","thrive_get_content","thrive_get_cpd_category","thrive_get_cpd_entry","thrive_get_cpd_requirement","thrive_get_enrolment","thrive_get_skill_levels","thrive_get_tag","thrive_get_user_by_id","thrive_get_user_by_ref","thrive_list_assignments","thrive_list_audience_managers","thrive_list_audience_members","thrive_list_audiences","thrive_list_completions","thrive_list_enrolments","thrive_list_tags","thrive_query_activities","thrive_query_content","thrive_query_cpd_categories","thrive_query_cpd_entries","thrive_query_cpd_requirements","thrive_query_cpd_user_summaries","thrive_remove_audience_manager","thrive_remove_audience_member","thrive_remove_user_tags","thrive_replace_audience_managers","thrive_replace_audience_members","thrive_search_users","thrive_suspend_user","thrive_update_assignment","thrive_update_audience","thrive_update_user","thrive_update_user_skills","tiktok_get_post_status","tiktok_get_user","tiktok_list_videos","tiktok_query_videos","tiktok_upload_video_draft","tinybird_append_datasource","tinybird_delete_datasource_rows","tinybird_events","tinybird_get_job","tinybird_query","tinybird_query_pipe","tinybird_truncate_datasource","trello_add_checklist","trello_add_checklist_item","trello_add_comment","trello_add_label","trello_add_member","trello_create_board","trello_create_card","trello_create_list","trello_delete_card","trello_get_actions","trello_get_board","trello_get_card","trello_list_cards","trello_list_lists","trello_list_members","trello_remove_label","trello_remove_member","trello_search","trello_update_card","trello_update_checklist_item","trello_update_list","trigger_dev_activate_schedule","trigger_dev_add_run_tags","trigger_dev_batch_trigger_task","trigger_dev_cancel_run","trigger_dev_complete_waitpoint_token","trigger_dev_create_env_var","trigger_dev_create_schedule","trigger_dev_create_waitpoint_token","trigger_dev_deactivate_schedule","trigger_dev_delete_env_var","trigger_dev_delete_schedule","trigger_dev_execute_query","trigger_dev_get_batch","trigger_dev_get_batch_results","trigger_dev_get_deployment","trigger_dev_get_env_var","trigger_dev_get_latest_deployment","trigger_dev_get_query_schema","trigger_dev_get_queue","trigger_dev_get_run","trigger_dev_get_run_events","trigger_dev_get_run_result","trigger_dev_get_run_trace","trigger_dev_get_schedule","trigger_dev_get_waitpoint_token","trigger_dev_import_env_vars","trigger_dev_list_deployments","trigger_dev_list_env_vars","trigger_dev_list_queues","trigger_dev_list_runs","trigger_dev_list_schedules","trigger_dev_list_timezones","trigger_dev_list_waitpoint_tokens","trigger_dev_override_queue_concurrency","trigger_dev_pause_queue","trigger_dev_promote_deployment","trigger_dev_replay_run","trigger_dev_reschedule_run","trigger_dev_reset_queue_concurrency","trigger_dev_resume_queue","trigger_dev_trigger_task","trigger_dev_update_env_var","trigger_dev_update_run_metadata","trigger_dev_update_schedule","tts_azure","tts_cartesia","tts_deepgram","tts_elevenlabs","tts_google","tts_openai","tts_playht","twilio_send_sms","twilio_voice_get_recording","twilio_voice_list_calls","twilio_voice_make_call","typeform_create_form","typeform_delete_form","typeform_files","typeform_get_form","typeform_insights","typeform_list_forms","typeform_responses","typeform_update_form","upstash_redis_command","upstash_redis_delete","upstash_redis_exists","upstash_redis_expire","upstash_redis_get","upstash_redis_hget","upstash_redis_hgetall","upstash_redis_hset","upstash_redis_incr","upstash_redis_incrby","upstash_redis_keys","upstash_redis_lpush","upstash_redis_lrange","upstash_redis_set","upstash_redis_setnx","upstash_redis_ttl","uptimerobot_create_alert_contact","uptimerobot_create_maintenance_window","uptimerobot_create_monitor","uptimerobot_create_psp","uptimerobot_delete_alert_contact","uptimerobot_delete_maintenance_window","uptimerobot_delete_monitor","uptimerobot_delete_psp","uptimerobot_get_account","uptimerobot_get_alert_contact","uptimerobot_get_incident","uptimerobot_get_maintenance_window","uptimerobot_get_monitor","uptimerobot_get_psp","uptimerobot_list_alert_contacts","uptimerobot_list_incidents","uptimerobot_list_maintenance_windows","uptimerobot_list_monitors","uptimerobot_list_psps","uptimerobot_pause_monitor","uptimerobot_start_monitor","uptimerobot_update_maintenance_window","uptimerobot_update_monitor","uptimerobot_update_psp","vanta_download_document_file","vanta_get_control","vanta_get_document","vanta_get_framework","vanta_get_person","vanta_get_policy","vanta_get_risk_scenario","vanta_get_test","vanta_get_vendor","vanta_get_vulnerable_asset","vanta_list_control_documents","vanta_list_control_tests","vanta_list_controls","vanta_list_document_uploads","vanta_list_documents","vanta_list_framework_controls","vanta_list_frameworks","vanta_list_monitored_computers","vanta_list_people","vanta_list_policies","vanta_list_risk_scenarios","vanta_list_test_entities","vanta_list_tests","vanta_list_vendors","vanta_list_vulnerabilities","vanta_list_vulnerability_remediations","vanta_list_vulnerable_assets","vanta_submit_document","vanta_upload_document_file","vercel_add_domain","vercel_add_project_domain","vercel_cancel_deployment","vercel_create_alias","vercel_create_check","vercel_create_deployment","vercel_create_dns_record","vercel_create_edge_config","vercel_create_env_var","vercel_create_project","vercel_create_webhook","vercel_delete_alias","vercel_delete_deployment","vercel_delete_dns_record","vercel_delete_domain","vercel_delete_edge_config","vercel_delete_env_var","vercel_delete_project","vercel_delete_webhook","vercel_get_alias","vercel_get_check","vercel_get_deployment","vercel_get_deployment_events","vercel_get_domain","vercel_get_domain_config","vercel_get_edge_config","vercel_get_edge_config_items","vercel_get_env_vars","vercel_get_project","vercel_get_team","vercel_get_user","vercel_get_webhook","vercel_list_aliases","vercel_list_checks","vercel_list_deployment_files","vercel_list_deployments","vercel_list_dns_records","vercel_list_domains","vercel_list_edge_configs","vercel_list_project_domains","vercel_list_projects","vercel_list_team_members","vercel_list_teams","vercel_list_webhooks","vercel_pause_project","vercel_promote_deployment","vercel_remove_project_domain","vercel_rerequest_check","vercel_unpause_project","vercel_update_check","vercel_update_dns_record","vercel_update_edge_config_items","vercel_update_env_var","vercel_update_project","vercel_update_project_domain","vercel_verify_project_domain","video_falai","video_luma","video_minimax","video_runway","video_veo","vision_tool","vision_tool_v2","wealthbox_read_contact","wealthbox_read_note","wealthbox_read_task","wealthbox_write_contact","wealthbox_write_note","wealthbox_write_task","webflow_create_item","webflow_delete_item","webflow_get_item","webflow_list_items","webflow_update_item","webhook_request","whatsapp_get_media","whatsapp_mark_read","whatsapp_send_interactive","whatsapp_send_media","whatsapp_send_message","whatsapp_send_reaction","whatsapp_send_template","whatsapp_upload_media","wikipedia_content","wikipedia_random","wikipedia_search","wikipedia_summary","windchill_check_in_document","windchill_check_in_documents","windchill_check_out_document","windchill_check_out_documents","windchill_create_document","windchill_create_documents","windchill_delete_document","windchill_delete_documents","windchill_download_attachment","windchill_download_primary_content","windchill_get_document","windchill_get_document_structure","windchill_get_primary_content","windchill_get_valid_state_transitions","windchill_list_attachments","windchill_list_documents","windchill_revise_document","windchill_revise_documents","windchill_set_lifecycle_state","windchill_undo_check_out_document","windchill_undo_check_out_documents","windchill_update_common_properties","windchill_update_document","windchill_update_document_security_labels","windchill_update_documents","windchill_upload_attachments","windchill_upload_primary_content","wiza_company_enrichment","wiza_get_credits","wiza_individual_reveal","wiza_prospect_search","wordpress_create_category","wordpress_create_comment","wordpress_create_page","wordpress_create_post","wordpress_create_tag","wordpress_delete_category","wordpress_delete_comment","wordpress_delete_media","wordpress_delete_page","wordpress_delete_post","wordpress_delete_tag","wordpress_get_category","wordpress_get_current_user","wordpress_get_media","wordpress_get_page","wordpress_get_post","wordpress_get_tag","wordpress_get_user","wordpress_list_categories","wordpress_list_comments","wordpress_list_media","wordpress_list_pages","wordpress_list_posts","wordpress_list_tags","wordpress_list_users","wordpress_search_content","wordpress_update_category","wordpress_update_comment","wordpress_update_page","wordpress_update_post","wordpress_update_tag","wordpress_upload_media","workday_assign_onboarding","workday_change_job","workday_create_prehire","workday_get_compensation","workday_get_organizations","workday_get_worker","workday_hire_employee","workday_list_workers","workday_terminate_worker","workday_update_worker","workflow_executor","x_create_bookmark","x_create_tweet","x_delete_bookmark","x_delete_tweet","x_get_blocking","x_get_bookmarks","x_get_followers","x_get_following","x_get_liked_tweets","x_get_liking_users","x_get_me","x_get_personalized_trends","x_get_quote_tweets","x_get_retweeted_by","x_get_trends_by_woeid","x_get_tweets_by_ids","x_get_usage","x_get_user_mentions","x_get_user_timeline","x_get_user_tweets","x_hide_reply","x_manage_block","x_manage_follow","x_manage_like","x_manage_mute","x_manage_retweet","x_read","x_search","x_search_tweets","x_search_users","x_user","x_write","youtube_channel_info","youtube_channel_playlists","youtube_channel_videos","youtube_comments","youtube_playlist_items","youtube_search","youtube_trending","youtube_video_categories","youtube_video_details","zendesk_autocomplete_organizations","zendesk_create_organization","zendesk_create_organizations_bulk","zendesk_create_ticket","zendesk_create_tickets_bulk","zendesk_create_user","zendesk_create_users_bulk","zendesk_delete_organization","zendesk_delete_ticket","zendesk_delete_user","zendesk_get_current_user","zendesk_get_organization","zendesk_get_organizations","zendesk_get_ticket","zendesk_get_tickets","zendesk_get_user","zendesk_get_users","zendesk_merge_tickets","zendesk_search","zendesk_search_count","zendesk_search_users","zendesk_update_organization","zendesk_update_ticket","zendesk_update_tickets_bulk","zendesk_update_user","zendesk_update_users_bulk","zep_add_messages","zep_add_user","zep_create_thread","zep_delete_thread","zep_get_context","zep_get_messages","zep_get_threads","zep_get_user","zep_get_user_threads","zerobounce_get_credits","zerobounce_verify_email","zoho_desk_add_comment","zoho_desk_get_attachment","zoho_desk_get_contact","zoho_desk_get_thread","zoho_desk_get_ticket","zoho_desk_list_comments","zoho_desk_list_organizations","zoho_desk_list_threads","zoho_desk_list_tickets","zoho_desk_update_ticket","zoom_create_meeting","zoom_delete_meeting","zoom_delete_recording","zoom_get_meeting","zoom_get_meeting_invitation","zoom_get_meeting_recordings","zoom_list_meetings","zoom_list_past_participants","zoom_list_recordings","zoom_update_meeting","zoominfo_enrich_companies","zoominfo_enrich_contacts","zoominfo_search_companies","zoominfo_search_contacts","zoominfo_search_intent","zoominfo_search_news"]' + '["a2a_cancel_task","a2a_get_agent_card","a2a_get_task","a2a_send_message","agentmail_create_draft","agentmail_create_inbox","agentmail_delete_draft","agentmail_delete_inbox","agentmail_delete_thread","agentmail_forward_message","agentmail_get_draft","agentmail_get_inbox","agentmail_get_message","agentmail_get_thread","agentmail_list_drafts","agentmail_list_inboxes","agentmail_list_messages","agentmail_list_threads","agentmail_reply_message","agentmail_send_draft","agentmail_send_message","agentmail_update_draft","agentmail_update_inbox","agentmail_update_message","agentmail_update_thread","agentphone_create_call","agentphone_create_contact","agentphone_create_number","agentphone_delete_contact","agentphone_get_call","agentphone_get_call_transcript","agentphone_get_contact","agentphone_get_conversation","agentphone_get_conversation_messages","agentphone_get_number_messages","agentphone_get_usage","agentphone_get_usage_daily","agentphone_get_usage_monthly","agentphone_list_calls","agentphone_list_contacts","agentphone_list_conversations","agentphone_list_numbers","agentphone_react_to_message","agentphone_release_number","agentphone_send_message","agentphone_update_contact","agentphone_update_conversation","agiloft_async_status","agiloft_attach_file","agiloft_attachment_info","agiloft_create_record","agiloft_delete_record","agiloft_get_choice_line_id","agiloft_list_tables","agiloft_lock_record","agiloft_nlp_search","agiloft_read_record","agiloft_remove_attachment","agiloft_retrieve_attachment","agiloft_run_action_button","agiloft_saved_search","agiloft_search_records","agiloft_select_records","agiloft_update_record","agiloft_upsert_record","ahrefs_anchors","ahrefs_backlinks","ahrefs_backlinks_stats","ahrefs_batch_analysis","ahrefs_broken_backlinks","ahrefs_domain_rating","ahrefs_domain_rating_history","ahrefs_keyword_overview","ahrefs_keywords_history","ahrefs_metrics","ahrefs_metrics_history","ahrefs_organic_competitors","ahrefs_organic_keywords","ahrefs_paid_pages","ahrefs_rank_tracker_competitors_overview","ahrefs_rank_tracker_competitors_stats","ahrefs_rank_tracker_overview","ahrefs_rank_tracker_serp_overview","ahrefs_refdomains_history","ahrefs_referring_domains","ahrefs_related_terms","ahrefs_site_audit_page_explorer","ahrefs_top_pages","airtable_create_records","airtable_delete_records","airtable_get_base_schema","airtable_get_record","airtable_list_bases","airtable_list_records","airtable_list_tables","airtable_update_multiple_records","airtable_update_record","airtable_upsert_records","airweave_search","algolia_add_record","algolia_batch_operations","algolia_browse_records","algolia_clear_records","algolia_copy_move_index","algolia_delete_by_filter","algolia_delete_index","algolia_delete_record","algolia_get_record","algolia_get_records","algolia_get_settings","algolia_get_task_status","algolia_list_indices","algolia_partial_update_record","algolia_search","algolia_update_settings","amplitude_event_segmentation","amplitude_funnels","amplitude_get_active_users","amplitude_get_revenue","amplitude_group_identify","amplitude_identify_user","amplitude_list_events","amplitude_realtime_active_users","amplitude_retention","amplitude_send_event","amplitude_user_activity","amplitude_user_profile","amplitude_user_search","apify_get_dataset_items","apify_get_run","apify_run_actor_async","apify_run_actor_sync","apify_run_task","apollo_account_bulk_create","apollo_account_bulk_update","apollo_account_create","apollo_account_search","apollo_account_update","apollo_contact_bulk_create","apollo_contact_bulk_update","apollo_contact_create","apollo_contact_search","apollo_contact_update","apollo_email_accounts","apollo_opportunity_create","apollo_opportunity_get","apollo_opportunity_search","apollo_opportunity_update","apollo_organization_bulk_enrich","apollo_organization_enrich","apollo_organization_search","apollo_people_bulk_enrich","apollo_people_enrich","apollo_people_search","apollo_sequence_add_contacts","apollo_sequence_search","apollo_task_create","apollo_task_search","appconfig_create_application","appconfig_create_configuration_profile","appconfig_create_environment","appconfig_create_hosted_configuration_version","appconfig_delete_application","appconfig_delete_configuration_profile","appconfig_delete_environment","appconfig_delete_hosted_configuration_version","appconfig_get_application","appconfig_get_configuration","appconfig_get_configuration_profile","appconfig_get_deployment","appconfig_get_environment","appconfig_get_hosted_configuration_version","appconfig_list_applications","appconfig_list_configuration_profiles","appconfig_list_deployment_strategies","appconfig_list_deployments","appconfig_list_environments","appconfig_list_hosted_configuration_versions","appconfig_start_deployment","appconfig_stop_deployment","appconfig_update_application","appconfig_update_configuration_profile","appconfig_update_environment","arxiv_get_author_papers","arxiv_get_paper","arxiv_search","asana_add_comment","asana_add_followers","asana_create_project","asana_create_section","asana_create_subtask","asana_create_task","asana_delete_task","asana_get_project","asana_get_projects","asana_get_task","asana_list_sections","asana_list_workspaces","asana_search_tasks","asana_update_task","ashby_add_candidate_tag","ashby_anonymize_candidate","ashby_change_application_source","ashby_change_application_stage","ashby_create_application","ashby_create_candidate","ashby_create_note","ashby_delete_application","ashby_get_application","ashby_get_candidate","ashby_get_job","ashby_get_job_posting","ashby_get_offer","ashby_list_applications","ashby_list_archive_reasons","ashby_list_candidate_tags","ashby_list_candidates","ashby_list_custom_fields","ashby_list_departments","ashby_list_interviews","ashby_list_job_postings","ashby_list_jobs","ashby_list_locations","ashby_list_notes","ashby_list_offers","ashby_list_openings","ashby_list_sources","ashby_list_users","ashby_remove_candidate_tag","ashby_search_candidates","ashby_set_custom_field_value","ashby_set_custom_field_values","ashby_update_candidate","athena_batch_get_query_execution","athena_create_named_query","athena_delete_named_query","athena_get_named_query","athena_get_query_execution","athena_get_query_results","athena_list_databases","athena_list_named_queries","athena_list_query_executions","athena_list_table_metadata","athena_start_query","athena_stop_query","attio_assert_record","attio_create_attribute","attio_create_comment","attio_create_list","attio_create_list_entry","attio_create_note","attio_create_object","attio_create_record","attio_create_task","attio_create_webhook","attio_delete_comment","attio_delete_list_entry","attio_delete_note","attio_delete_record","attio_delete_task","attio_delete_webhook","attio_get_attribute","attio_get_comment","attio_get_list","attio_get_list_entry","attio_get_member","attio_get_note","attio_get_object","attio_get_record","attio_get_task","attio_get_thread","attio_get_webhook","attio_list_attributes","attio_list_lists","attio_list_members","attio_list_notes","attio_list_objects","attio_list_records","attio_list_tasks","attio_list_threads","attio_list_webhooks","attio_query_list_entries","attio_search_records","attio_update_attribute","attio_update_list","attio_update_list_entry","attio_update_object","attio_update_record","attio_update_task","attio_update_webhook","azure_data_explorer_create_table","azure_data_explorer_drop_table","azure_data_explorer_ingest_from_query","azure_data_explorer_ingest_inline","azure_data_explorer_list_databases","azure_data_explorer_list_functions","azure_data_explorer_list_tables","azure_data_explorer_management","azure_data_explorer_query","azure_data_explorer_show_database_schema","azure_data_explorer_show_ingestion_failures","azure_data_explorer_show_operations","azure_data_explorer_show_table_details","azure_data_explorer_show_table_schema","azure_devops_add_comment","azure_devops_create_work_item","azure_devops_get_build_log","azure_devops_get_build_timeline","azure_devops_get_comments","azure_devops_get_pipeline","azure_devops_get_pipeline_run","azure_devops_get_work_item","azure_devops_get_work_items_batch","azure_devops_get_work_items_between_builds","azure_devops_list_build_logs","azure_devops_list_builds","azure_devops_list_pipeline_runs","azure_devops_list_pipelines","azure_devops_query_work_items","azure_devops_update_work_item","bitbucket_approve_pull_request","bitbucket_create_branch","bitbucket_create_pull_request","bitbucket_create_pull_request_comment","bitbucket_decline_pull_request","bitbucket_delete_branch","bitbucket_get_commit","bitbucket_get_file","bitbucket_get_file_metadata","bitbucket_get_pipeline","bitbucket_get_pipeline_step_log","bitbucket_get_pull_request","bitbucket_get_pull_request_diff","bitbucket_get_pull_request_diffstat","bitbucket_get_pull_request_merge_task_status","bitbucket_get_repository","bitbucket_list_branches","bitbucket_list_commits","bitbucket_list_directory","bitbucket_list_pipeline_steps","bitbucket_list_pipelines","bitbucket_list_pull_request_comments","bitbucket_list_pull_request_commit_statuses","bitbucket_list_pull_requests","bitbucket_list_repositories","bitbucket_list_workspaces","bitbucket_merge_pull_request","bitbucket_request_pull_request_changes","bitbucket_stop_pipeline","bitbucket_trigger_pipeline","box_copy_file","box_create_folder","box_delete_file","box_delete_folder","box_download_file","box_get_file_info","box_list_folder_items","box_search","box_sign_cancel_request","box_sign_create_request","box_sign_get_request","box_sign_list_requests","box_sign_resend_request","box_update_file","box_upload_file","brandfetch_get_brand","brandfetch_search","brex_archive_budget","brex_create_budget","brex_create_spend_limit","brex_create_transfer","brex_create_vendor","brex_get_budget","brex_get_cash_account","brex_get_company","brex_get_current_user","brex_get_expense","brex_get_spend_limit","brex_get_transfer","brex_get_user","brex_get_vendor","brex_list_budgets","brex_list_card_accounts","brex_list_card_statements","brex_list_card_transactions","brex_list_cards","brex_list_cash_accounts","brex_list_cash_statements","brex_list_cash_transactions","brex_list_departments","brex_list_expenses","brex_list_locations","brex_list_spend_limits","brex_list_titles","brex_list_transfers","brex_list_users","brex_list_vendors","brex_match_receipt","brex_update_expense","brex_update_vendor","brex_upload_receipt","brightdata_cancel_snapshot","brightdata_discover","brightdata_download_snapshot","brightdata_scrape_dataset","brightdata_scrape_url","brightdata_serp_search","brightdata_snapshot_status","brightdata_sync_scrape","browser_use_run_task","buffer_create_idea","buffer_create_post","buffer_delete_post","buffer_edit_post","buffer_get_account","buffer_get_channels","buffer_get_idea_groups","buffer_get_ideas","buffer_get_post","buffer_get_posts","calcom_cancel_booking","calcom_confirm_booking","calcom_create_booking","calcom_create_event_type","calcom_create_schedule","calcom_decline_booking","calcom_delete_event_type","calcom_delete_schedule","calcom_get_booking","calcom_get_default_schedule","calcom_get_event_type","calcom_get_schedule","calcom_get_slots","calcom_list_bookings","calcom_list_event_types","calcom_list_schedules","calcom_reschedule_booking","calcom_update_event_type","calcom_update_schedule","calendly_cancel_event","calendly_create_event_invitee","calendly_create_invitee_no_show","calendly_create_scheduling_link","calendly_create_webhook","calendly_delete_invitee_no_show","calendly_delete_webhook","calendly_get_current_user","calendly_get_event_invitee","calendly_get_event_type","calendly_get_scheduled_event","calendly_get_user","calendly_list_event_invitees","calendly_list_event_type_available_times","calendly_list_event_types","calendly_list_organization_memberships","calendly_list_routing_form_submissions","calendly_list_routing_forms","calendly_list_scheduled_events","calendly_list_user_availability_schedules","calendly_list_user_busy_times","calendly_list_webhooks","cbinsights_chat","cbinsights_get_commercial_maturity_history","cbinsights_get_exit_probability_history","cbinsights_get_mosaic_history","cbinsights_get_org_business_relationships","cbinsights_get_org_funding_window","cbinsights_get_org_fundings","cbinsights_get_org_investments","cbinsights_get_org_management_and_board","cbinsights_get_org_outlook","cbinsights_get_org_portfolio_exits","cbinsights_get_org_revenue","cbinsights_get_scouting_report","cbinsights_get_strategy_map","cbinsights_list_business_relationships","cbinsights_list_funding_window","cbinsights_list_fundings","cbinsights_list_investments","cbinsights_list_management_and_board","cbinsights_list_outlook","cbinsights_list_portfolio_exits","cbinsights_list_revenue","cbinsights_lookup_organizations","cbinsights_rag","cbinsights_search_firmographics","clay_populate","clerk_add_organization_member","clerk_ban_user","clerk_create_actor_token","clerk_create_allowlist_identifier","clerk_create_blocklist_identifier","clerk_create_organization","clerk_create_organization_invitation","clerk_create_user","clerk_delete_allowlist_identifier","clerk_delete_blocklist_identifier","clerk_delete_organization","clerk_delete_user","clerk_get_jwt_template","clerk_get_organization","clerk_get_session","clerk_get_user","clerk_get_user_oauth_token","clerk_list_allowlist_identifiers","clerk_list_blocklist_identifiers","clerk_list_jwt_templates","clerk_list_organization_invitations","clerk_list_organization_memberships","clerk_list_organizations","clerk_list_sessions","clerk_list_users","clerk_lock_user","clerk_remove_organization_member","clerk_revoke_actor_token","clerk_revoke_session","clerk_unban_user","clerk_unlock_user","clerk_update_organization","clerk_update_organization_membership","clerk_update_user","clickhouse_count_rows","clickhouse_create_database","clickhouse_create_table","clickhouse_delete","clickhouse_describe_table","clickhouse_drop_database","clickhouse_drop_partition","clickhouse_drop_table","clickhouse_execute","clickhouse_insert","clickhouse_insert_rows","clickhouse_introspect","clickhouse_kill_query","clickhouse_list_clusters","clickhouse_list_databases","clickhouse_list_mutations","clickhouse_list_partitions","clickhouse_list_running_queries","clickhouse_list_tables","clickhouse_optimize_table","clickhouse_query","clickhouse_rename_table","clickhouse_show_create_table","clickhouse_table_stats","clickhouse_truncate_table","clickhouse_update","clickup_add_tag_to_task","clickup_create_checklist","clickup_create_checklist_item","clickup_create_comment","clickup_create_folder","clickup_create_list","clickup_create_task","clickup_create_time_entry","clickup_delete_checklist","clickup_delete_checklist_item","clickup_delete_comment","clickup_delete_task","clickup_delete_time_entry","clickup_get_comments","clickup_get_custom_fields","clickup_get_folders","clickup_get_list_members","clickup_get_lists","clickup_get_running_timer","clickup_get_space_tags","clickup_get_spaces","clickup_get_task","clickup_get_task_members","clickup_get_tasks","clickup_get_time_entries","clickup_get_workspaces","clickup_remove_custom_field_value","clickup_remove_tag_from_task","clickup_search_tasks","clickup_set_custom_field_value","clickup_start_timer","clickup_stop_timer","clickup_update_checklist","clickup_update_checklist_item","clickup_update_comment","clickup_update_task","clickup_update_time_entry","clickup_upload_attachment","cloudflare_create_access_application","cloudflare_create_access_policy","cloudflare_create_access_service_token","cloudflare_create_dns_record","cloudflare_create_r2_bucket","cloudflare_create_rate_limit_rule","cloudflare_create_ruleset","cloudflare_create_ruleset_rule","cloudflare_create_zone","cloudflare_delete_access_application","cloudflare_delete_access_policy","cloudflare_delete_dns_record","cloudflare_delete_r2_bucket","cloudflare_delete_ruleset_rule","cloudflare_delete_zone","cloudflare_dns_analytics","cloudflare_get_access_application","cloudflare_get_r2_bucket","cloudflare_get_ruleset","cloudflare_get_ruleset_entrypoint","cloudflare_get_tunnel","cloudflare_get_tunnel_configuration","cloudflare_get_worker_script_settings","cloudflare_get_zone","cloudflare_get_zone_settings","cloudflare_list_access_applications","cloudflare_list_access_groups","cloudflare_list_access_identity_providers","cloudflare_list_access_policies","cloudflare_list_access_service_tokens","cloudflare_list_certificates","cloudflare_list_dns_records","cloudflare_list_managed_ruleset_overrides","cloudflare_list_r2_buckets","cloudflare_list_rate_limit_rules","cloudflare_list_rulesets","cloudflare_list_tunnels","cloudflare_list_worker_routes","cloudflare_list_worker_scripts","cloudflare_list_zones","cloudflare_purge_cache","cloudflare_revoke_access_service_token","cloudflare_update_access_application","cloudflare_update_access_policy","cloudflare_update_dns_record","cloudflare_update_rate_limit_rule","cloudflare_update_ruleset_rule","cloudflare_update_zone_setting","cloudformation_cancel_update_stack","cloudformation_create_change_set","cloudformation_create_stack","cloudformation_delete_stack","cloudformation_describe_change_set","cloudformation_describe_stack_drift_detection_status","cloudformation_describe_stack_events","cloudformation_describe_stacks","cloudformation_detect_stack_drift","cloudformation_execute_change_set","cloudformation_get_template","cloudformation_get_template_summary","cloudformation_list_stack_resources","cloudformation_update_stack","cloudformation_validate_template","cloudwatch_describe_alarm_history","cloudwatch_describe_alarms","cloudwatch_describe_log_groups","cloudwatch_describe_log_streams","cloudwatch_filter_log_events","cloudwatch_get_log_events","cloudwatch_get_metric_statistics","cloudwatch_list_metrics","cloudwatch_mute_alarm","cloudwatch_put_log_group_retention","cloudwatch_put_metric_data","cloudwatch_query_logs","cloudwatch_unmute_alarm","codepipeline_disable_stage_transition","codepipeline_enable_stage_transition","codepipeline_get_pipeline","codepipeline_get_pipeline_execution","codepipeline_get_pipeline_state","codepipeline_list_action_executions","codepipeline_list_pipeline_executions","codepipeline_list_pipelines","codepipeline_put_approval_result","codepipeline_retry_stage_execution","codepipeline_start_execution","codepipeline_stop_execution","confluence_add_label","confluence_create_blogpost","confluence_create_comment","confluence_create_page","confluence_create_page_property","confluence_create_space","confluence_create_space_property","confluence_delete_attachment","confluence_delete_blogpost","confluence_delete_comment","confluence_delete_label","confluence_delete_page","confluence_delete_page_property","confluence_delete_space","confluence_delete_space_property","confluence_get_blogpost","confluence_get_page_ancestors","confluence_get_page_children","confluence_get_page_descendants","confluence_get_page_version","confluence_get_pages_by_label","confluence_get_space","confluence_get_task","confluence_get_user","confluence_list_attachments","confluence_list_blogposts","confluence_list_blogposts_in_space","confluence_list_comments","confluence_list_labels","confluence_list_page_properties","confluence_list_page_versions","confluence_list_pages_in_space","confluence_list_space_labels","confluence_list_space_permissions","confluence_list_space_properties","confluence_list_spaces","confluence_list_tasks","confluence_retrieve","confluence_search","confluence_search_in_space","confluence_update","confluence_update_blogpost","confluence_update_comment","confluence_update_space","confluence_update_task","confluence_upload_attachment","context_dev_classify_naics","context_dev_classify_sic","context_dev_crawl","context_dev_extract","context_dev_extract_product","context_dev_extract_products","context_dev_get_brand","context_dev_get_brand_by_email","context_dev_get_brand_by_name","context_dev_get_brand_by_ticker","context_dev_identify_transaction","context_dev_map","context_dev_scrape_fonts","context_dev_scrape_html","context_dev_scrape_images","context_dev_scrape_markdown","context_dev_scrape_styleguide","context_dev_screenshot","context_dev_search","convex_action","convex_document_deltas","convex_list_documents","convex_list_tables","convex_mutation","convex_query","convex_run_function","crowdstrike_create_indicators","crowdstrike_delete_indicators","crowdstrike_delete_rtr_session","crowdstrike_execute_rtr_command","crowdstrike_get_alert_details","crowdstrike_get_case_details","crowdstrike_get_host_group_details","crowdstrike_get_indicator_details","crowdstrike_get_rtr_command_status","crowdstrike_get_sensor_aggregates","crowdstrike_get_sensor_details","crowdstrike_get_vulnerability_details","crowdstrike_init_rtr_session","crowdstrike_perform_host_action","crowdstrike_perform_host_group_action","crowdstrike_query_alerts","crowdstrike_query_cases","crowdstrike_query_host_groups","crowdstrike_query_indicators","crowdstrike_query_sensors","crowdstrike_query_vulnerabilities","crowdstrike_update_alerts","crowdstrike_update_indicators","crunchbase_autocomplete","crunchbase_get_acquisition","crunchbase_get_entity","crunchbase_get_entity_card","crunchbase_get_fields_metadata","crunchbase_get_funding_round","crunchbase_get_organization","crunchbase_get_person","crunchbase_list_deleted_entities","crunchbase_search_acquisitions","crunchbase_search_entities","crunchbase_search_funding_rounds","crunchbase_search_organizations","crunchbase_search_people","cursor_add_followup","cursor_add_followup_v2","cursor_delete_agent","cursor_delete_agent_v2","cursor_download_artifact","cursor_download_artifact_v2","cursor_get_agent","cursor_get_agent_v2","cursor_get_api_key_info","cursor_get_api_key_info_v2","cursor_get_conversation","cursor_get_conversation_v2","cursor_launch_agent","cursor_launch_agent_v2","cursor_list_agents","cursor_list_agents_v2","cursor_list_artifacts","cursor_list_artifacts_v2","cursor_list_models","cursor_list_models_v2","cursor_list_repositories","cursor_list_repositories_v2","cursor_stop_agent","cursor_stop_agent_v2","dagster_delete_run","dagster_get_asset","dagster_get_run","dagster_get_run_logs","dagster_launch_run","dagster_list_assets","dagster_list_jobs","dagster_list_runs","dagster_list_schedules","dagster_list_sensors","dagster_materialize_assets","dagster_reexecute_run","dagster_report_asset_materialization","dagster_start_schedule","dagster_start_sensor","dagster_stop_schedule","dagster_stop_sensor","dagster_terminate_run","dagster_wipe_asset","databricks_cancel_run","databricks_execute_sql","databricks_get_cluster","databricks_get_job","databricks_get_run","databricks_get_run_output","databricks_get_statement","databricks_list_clusters","databricks_list_jobs","databricks_list_runs","databricks_list_warehouses","databricks_run_job","datadog_add_incident_todo","datadog_cancel_downtime","datadog_create_dashboard","datadog_create_downtime","datadog_create_event","datadog_create_incident","datadog_create_monitor","datadog_create_slo","datadog_delete_dashboard","datadog_delete_slo","datadog_get_browser_synthetics_results","datadog_get_dashboard","datadog_get_incident","datadog_get_monitor","datadog_get_security_signal","datadog_get_slo","datadog_get_slo_history","datadog_get_synthetics_results","datadog_get_synthetics_test","datadog_list_dashboards","datadog_list_downtimes","datadog_list_incidents","datadog_list_monitors","datadog_list_security_rules","datadog_list_security_signals","datadog_list_services","datadog_list_slos","datadog_list_synthetics_tests","datadog_mute_monitor","datadog_query_logs","datadog_query_timeseries","datadog_search_spans","datadog_send_logs","datadog_submit_metrics","datadog_trigger_synthetics_tests","datadog_unmute_monitor","datadog_update_incident","datadog_update_security_signal_assignee","datadog_update_security_signal_state","datadog_update_slo","datadog_update_synthetics_status","datagma_enrich_company","datagma_enrich_person","datagma_find_email","datagma_find_phone","datagma_get_credits","daytona_create_sandbox","daytona_delete_sandbox","daytona_download_file","daytona_execute_command","daytona_get_sandbox","daytona_git_clone","daytona_list_files","daytona_list_sandboxes","daytona_run_code","daytona_start_sandbox","daytona_stop_sandbox","daytona_upload_file","deployed_block_executor","deployments_deploy","deployments_get_version","deployments_list_versions","deployments_promote","deployments_undeploy","devin_append_session_tags","devin_archive_session","devin_create_session","devin_get_session","devin_get_session_tags","devin_list_session_attachments","devin_list_session_messages","devin_list_sessions","devin_replace_session_tags","devin_send_message","devin_terminate_session","discord_add_reaction","discord_archive_thread","discord_assign_role","discord_ban_member","discord_bulk_delete_messages","discord_create_channel","discord_create_invite","discord_create_role","discord_create_thread","discord_create_webhook","discord_delete_channel","discord_delete_invite","discord_delete_message","discord_delete_role","discord_delete_webhook","discord_edit_message","discord_execute_webhook","discord_get_channel","discord_get_invite","discord_get_member","discord_get_messages","discord_get_pinned_messages","discord_get_server","discord_get_user","discord_get_webhook","discord_join_thread","discord_kick_member","discord_leave_thread","discord_list_channels","discord_list_roles","discord_pin_message","discord_remove_reaction","discord_remove_role","discord_send_message","discord_unban_member","discord_unpin_message","discord_update_channel","discord_update_member","discord_update_role","docusign_create_from_template","docusign_download_document","docusign_get_envelope","docusign_list_envelopes","docusign_list_recipients","docusign_list_templates","docusign_send_envelope","docusign_void_envelope","downdetector_get_company","downdetector_get_company_attribution","downdetector_get_company_baseline","downdetector_get_company_events","downdetector_get_company_incidents","downdetector_get_company_indicators","downdetector_get_company_last_15","downdetector_get_company_status","downdetector_get_provider","downdetector_get_reports","downdetector_get_site_companies","downdetector_list_categories","downdetector_list_incidents","downdetector_list_sites","downdetector_search_companies","dropbox_copy","dropbox_create_folder","dropbox_create_shared_link","dropbox_delete","dropbox_download","dropbox_get_metadata","dropbox_list_folder","dropbox_list_revisions","dropbox_list_shared_links","dropbox_move","dropbox_restore","dropbox_search","dropbox_upload","dropcontact_enrich_contact","dspy_chain_of_thought","dspy_predict","dspy_react","dub_bulk_create_links","dub_bulk_delete_links","dub_bulk_update_links","dub_create_link","dub_create_tag","dub_delete_link","dub_get_analytics","dub_get_events","dub_get_link","dub_get_links_count","dub_get_qr_code","dub_list_domains","dub_list_folders","dub_list_links","dub_list_tags","dub_update_link","dub_upsert_link","duckduckgo_search","dynamodb_delete","dynamodb_get","dynamodb_introspect","dynamodb_put","dynamodb_query","dynamodb_scan","dynamodb_update","dynatrace_add_problem_comment","dynatrace_add_tags","dynatrace_close_problem","dynatrace_create_settings_object","dynatrace_create_slo","dynatrace_delete_problem_comment","dynatrace_delete_settings_object","dynatrace_delete_slo","dynatrace_delete_tag","dynatrace_execute_synthetic_monitors","dynatrace_get_attack","dynatrace_get_audit_logs","dynatrace_get_entity","dynatrace_get_event","dynatrace_get_metric","dynatrace_get_problem","dynatrace_get_problem_comment","dynatrace_get_security_problem","dynatrace_get_settings_object","dynatrace_get_slo","dynatrace_get_synthetic_batch","dynatrace_ingest_event","dynatrace_ingest_logs","dynatrace_ingest_metrics","dynatrace_list_attacks","dynatrace_list_entities","dynatrace_list_entity_types","dynatrace_list_events","dynatrace_list_metrics","dynatrace_list_problem_comments","dynatrace_list_problems","dynatrace_list_remediation_items","dynatrace_list_security_problems","dynatrace_list_settings_objects","dynatrace_list_settings_schemas","dynatrace_list_slos","dynatrace_list_synthetic_monitors","dynatrace_list_tags","dynatrace_mute_security_problem","dynatrace_mute_security_problems","dynatrace_query_metrics","dynatrace_search_logs","dynatrace_unmute_security_problem","dynatrace_unmute_security_problems","dynatrace_update_problem_comment","dynatrace_update_settings_object","dynatrace_update_slo","elasticsearch_bulk","elasticsearch_cluster_health","elasticsearch_cluster_stats","elasticsearch_count","elasticsearch_create_index","elasticsearch_delete_document","elasticsearch_delete_index","elasticsearch_get_document","elasticsearch_get_index","elasticsearch_index_document","elasticsearch_list_indices","elasticsearch_search","elasticsearch_update_document","elevenlabs_audio_isolation","elevenlabs_edit_voice_settings","elevenlabs_get_user","elevenlabs_get_voice","elevenlabs_get_voice_settings","elevenlabs_list_models","elevenlabs_list_voices","elevenlabs_sound_effects","elevenlabs_speech_to_speech","elevenlabs_tts","emailbison_attach_leads_to_campaign","emailbison_attach_tags_to_leads","emailbison_create_campaign","emailbison_create_lead","emailbison_create_tag","emailbison_get_lead","emailbison_list_campaigns","emailbison_list_leads","emailbison_list_replies","emailbison_list_tags","emailbison_update_campaign","emailbison_update_campaign_status","emailbison_update_lead","embeddings_cohere","embeddings_gemini","embeddings_mistral","embeddings_openai","embeddings_openrouter","enrich_check_credits","enrich_company_funding","enrich_company_lookup","enrich_company_revenue","enrich_disposable_email_check","enrich_email_to_ip","enrich_email_to_person_lite","enrich_email_to_phone","enrich_email_to_profile","enrich_find_email","enrich_get_post_details","enrich_ip_to_company","enrich_linkedin_profile","enrich_linkedin_to_personal_email","enrich_linkedin_to_work_email","enrich_phone_finder","enrich_reverse_hash_lookup","enrich_sales_pointer_people","enrich_search_company","enrich_search_company_activities","enrich_search_company_employees","enrich_search_jobs","enrich_search_logo","enrich_search_people","enrich_search_people_activities","enrich_search_post_comments","enrich_search_post_comments_by_url","enrich_search_post_reactions","enrich_search_post_reactions_by_url","enrich_search_posts","enrich_search_similar_companies","enrich_verify_email","enrichment_run","enrow_find_email","enrow_verify_email","exa_agent","exa_answer","exa_find_similar_links","exa_get_contents","exa_search","extend_parser","extend_parser_v2","fathom_get_summary","fathom_get_transcript","fathom_list_meeting_types","fathom_list_meetings","fathom_list_team_members","fathom_list_teams","file_append","file_compress","file_decompress","file_fetch","file_get","file_get_content","file_manage_sharing","file_parser","file_parser_v2","file_parser_v3","file_read","file_write","findymail_find_email_from_linkedin","findymail_find_email_from_name","findymail_find_emails_by_domain","findymail_find_employees","findymail_find_phone","findymail_get_company","findymail_get_credits","findymail_lookup_technologies","findymail_reverse_email_lookup","findymail_search_technologies","findymail_verify_email","firecrawl_agent","firecrawl_batch_scrape","firecrawl_batch_scrape_status","firecrawl_cancel_crawl","firecrawl_crawl","firecrawl_crawl_status","firecrawl_credit_usage","firecrawl_extract","firecrawl_extract_status","firecrawl_map","firecrawl_parse","firecrawl_scrape","firecrawl_search","fireflies_add_to_live_meeting","fireflies_create_bite","fireflies_delete_transcript","fireflies_get_transcript","fireflies_get_user","fireflies_list_bites","fireflies_list_contacts","fireflies_list_transcripts","fireflies_list_users","fireflies_upload_audio","flint_create_task","flint_generate_pages","flint_get_task","function_execute","gamma_check_status","gamma_generate","gamma_generate_from_template","gamma_list_folders","gamma_list_themes","github_add_assignees","github_add_assignees_v2","github_add_labels","github_add_labels_v2","github_cancel_workflow_run","github_cancel_workflow_run_v2","github_check_star","github_check_star_v2","github_close_issue","github_close_issue_v2","github_close_pr","github_close_pr_v2","github_comment","github_comment_v2","github_compare_commits","github_compare_commits_v2","github_create_branch","github_create_branch_v2","github_create_comment_reaction","github_create_comment_reaction_v2","github_create_file","github_create_file_v2","github_create_gist","github_create_gist_v2","github_create_issue","github_create_issue_reaction","github_create_issue_reaction_v2","github_create_issue_v2","github_create_milestone","github_create_milestone_v2","github_create_pr","github_create_pr_review","github_create_pr_review_v2","github_create_pr_v2","github_create_project","github_create_project_v2","github_create_release","github_create_release_v2","github_delete_branch","github_delete_branch_v2","github_delete_comment","github_delete_comment_reaction","github_delete_comment_reaction_v2","github_delete_comment_v2","github_delete_file","github_delete_file_v2","github_delete_gist","github_delete_gist_v2","github_delete_issue_reaction","github_delete_issue_reaction_v2","github_delete_milestone","github_delete_milestone_v2","github_delete_project","github_delete_project_v2","github_delete_release","github_delete_release_v2","github_fork_gist","github_fork_gist_v2","github_fork_repo","github_fork_repo_v2","github_get_branch","github_get_branch_protection","github_get_branch_protection_v2","github_get_branch_v2","github_get_commit","github_get_commit_v2","github_get_file_content","github_get_file_content_v2","github_get_gist","github_get_gist_v2","github_get_issue","github_get_issue_v2","github_get_latest_release","github_get_latest_release_v2","github_get_milestone","github_get_milestone_v2","github_get_pr_files","github_get_pr_files_v2","github_get_project","github_get_project_v2","github_get_readme","github_get_readme_v2","github_get_release","github_get_release_v2","github_get_tree","github_get_tree_v2","github_get_workflow","github_get_workflow_run","github_get_workflow_run_v2","github_get_workflow_v2","github_issue_comment","github_issue_comment_v2","github_job_logs","github_latest_commit","github_latest_commit_v2","github_list_branches","github_list_branches_v2","github_list_commits","github_list_commits_v2","github_list_forks","github_list_forks_v2","github_list_gists","github_list_gists_v2","github_list_issue_comments","github_list_issue_comments_v2","github_list_issues","github_list_issues_v2","github_list_milestones","github_list_milestones_v2","github_list_pr_comments","github_list_pr_comments_v2","github_list_projects","github_list_projects_v2","github_list_prs","github_list_prs_v2","github_list_releases","github_list_releases_v2","github_list_review_threads","github_list_stargazers","github_list_stargazers_v2","github_list_tags","github_list_tags_v2","github_list_workflow_runs","github_list_workflow_runs_v2","github_list_workflows","github_list_workflows_v2","github_merge_pr","github_merge_pr_v2","github_pr","github_pr_v2","github_remove_label","github_remove_label_v2","github_reply_review_thread","github_repo_info","github_repo_info_v2","github_request_reviewers","github_request_reviewers_v2","github_rerun_workflow","github_rerun_workflow_v2","github_resolve_review_thread","github_search_code","github_search_code_v2","github_search_commits","github_search_commits_v2","github_search_issues","github_search_issues_v2","github_search_repos","github_search_repos_v2","github_search_users","github_search_users_v2","github_star_gist","github_star_gist_v2","github_star_repo","github_star_repo_v2","github_status_check_rollup","github_trigger_workflow","github_trigger_workflow_v2","github_unstar_gist","github_unstar_gist_v2","github_unstar_repo","github_unstar_repo_v2","github_update_branch_protection","github_update_branch_protection_v2","github_update_comment","github_update_comment_v2","github_update_file","github_update_file_v2","github_update_gist","github_update_gist_v2","github_update_issue","github_update_issue_v2","github_update_milestone","github_update_milestone_v2","github_update_pr","github_update_pr_v2","github_update_project","github_update_project_v2","github_update_release","github_update_release_v2","gitlab_activate_user","gitlab_add_member","gitlab_add_saml_group_link","gitlab_approve_access_request","gitlab_approve_merge_request","gitlab_approve_user","gitlab_ban_user","gitlab_block_user","gitlab_cancel_pipeline","gitlab_compare_branches","gitlab_create_branch","gitlab_create_file","gitlab_create_issue","gitlab_create_issue_note","gitlab_create_merge_request","gitlab_create_merge_request_note","gitlab_create_pipeline","gitlab_create_release","gitlab_create_user","gitlab_deactivate_user","gitlab_delete_branch","gitlab_delete_issue","gitlab_delete_saml_group_link","gitlab_delete_user","gitlab_delete_user_identity","gitlab_deny_access_request","gitlab_get_file","gitlab_get_group","gitlab_get_issue","gitlab_get_job_log","gitlab_get_merge_request","gitlab_get_merge_request_changes","gitlab_get_pipeline","gitlab_get_project","gitlab_invite_member","gitlab_list_access_requests","gitlab_list_branches","gitlab_list_commits","gitlab_list_groups","gitlab_list_invitations","gitlab_list_issues","gitlab_list_members","gitlab_list_merge_requests","gitlab_list_pipeline_jobs","gitlab_list_pipelines","gitlab_list_projects","gitlab_list_releases","gitlab_list_repository_tree","gitlab_list_saml_group_links","gitlab_list_user_memberships","gitlab_merge_merge_request","gitlab_play_job","gitlab_reject_user","gitlab_remove_member","gitlab_retry_pipeline","gitlab_revoke_invitation","gitlab_search_users","gitlab_unban_user","gitlab_unblock_user","gitlab_update_file","gitlab_update_invitation","gitlab_update_issue","gitlab_update_member","gitlab_update_merge_request","gitlab_update_user","gmail_add_label","gmail_add_label_v2","gmail_archive","gmail_archive_v2","gmail_create_label_v2","gmail_delete","gmail_delete_draft_v2","gmail_delete_label_v2","gmail_delete_v2","gmail_draft","gmail_draft_v2","gmail_edit_draft_v2","gmail_get_draft_v2","gmail_get_thread_v2","gmail_list_drafts_v2","gmail_list_labels_v2","gmail_list_threads_v2","gmail_mark_read","gmail_mark_read_v2","gmail_mark_unread","gmail_mark_unread_v2","gmail_move","gmail_move_v2","gmail_read","gmail_read_v2","gmail_remove_label","gmail_remove_label_v2","gmail_search","gmail_search_v2","gmail_send","gmail_send_v2","gmail_trash_thread_v2","gmail_unarchive","gmail_unarchive_v2","gmail_untrash_thread_v2","gmail_update_label_v2","gong_aggregate_activity","gong_aggregate_by_period","gong_answered_scorecards","gong_ask_anything","gong_assign_flow_prospects","gong_create_call","gong_day_by_day_activity","gong_get_brief","gong_get_call","gong_get_call_transcript","gong_get_coaching","gong_get_extensive_calls","gong_get_folder_content","gong_get_logs","gong_get_prospect_flows","gong_get_user","gong_interaction_stats","gong_list_calls","gong_list_flows","gong_list_library_folders","gong_list_scorecards","gong_list_trackers","gong_list_users","gong_list_workspaces","gong_lookup_email","gong_lookup_phone","gong_purge_email_address","gong_purge_phone_number","gong_unassign_flow_prospects","google_ads_ad_performance","google_ads_campaign_performance","google_ads_list_ad_groups","google_ads_list_campaigns","google_ads_list_customers","google_ads_search","google_appsheet_add_rows","google_appsheet_delete_rows","google_appsheet_edit_rows","google_appsheet_find_rows","google_bigquery_create_dataset","google_bigquery_create_table","google_bigquery_delete_dataset","google_bigquery_delete_table","google_bigquery_get_query_results","google_bigquery_get_table","google_bigquery_insert_rows","google_bigquery_list_datasets","google_bigquery_list_table_data","google_bigquery_list_tables","google_bigquery_query","google_books_volume_details","google_books_volume_search","google_calendar_create","google_calendar_create_calendar","google_calendar_create_calendar_v2","google_calendar_create_v2","google_calendar_delete","google_calendar_delete_calendar","google_calendar_delete_calendar_v2","google_calendar_delete_v2","google_calendar_freebusy","google_calendar_freebusy_v2","google_calendar_get","google_calendar_get_v2","google_calendar_instances","google_calendar_instances_v2","google_calendar_invite","google_calendar_invite_v2","google_calendar_list","google_calendar_list_acl","google_calendar_list_acl_v2","google_calendar_list_calendars","google_calendar_list_calendars_v2","google_calendar_list_v2","google_calendar_move","google_calendar_move_v2","google_calendar_quick_add","google_calendar_quick_add_v2","google_calendar_share_calendar","google_calendar_share_calendar_v2","google_calendar_unshare_calendar","google_calendar_unshare_calendar_v2","google_calendar_update","google_calendar_update_acl","google_calendar_update_acl_v2","google_calendar_update_calendar","google_calendar_update_calendar_v2","google_calendar_update_v2","google_contacts_create","google_contacts_delete","google_contacts_get","google_contacts_list","google_contacts_search","google_contacts_update","google_docs_create","google_docs_create_named_range","google_docs_create_paragraph_bullets","google_docs_delete_content_range","google_docs_delete_named_range","google_docs_delete_paragraph_bullets","google_docs_insert_image","google_docs_insert_page_break","google_docs_insert_table","google_docs_insert_text","google_docs_read","google_docs_replace_text","google_docs_update_paragraph_style","google_docs_update_text_style","google_docs_write","google_drive_copy","google_drive_create_comment","google_drive_create_folder","google_drive_delete","google_drive_delete_comment","google_drive_download","google_drive_export","google_drive_get_about","google_drive_get_content","google_drive_get_file","google_drive_get_revision","google_drive_list","google_drive_list_comments","google_drive_list_permissions","google_drive_list_revisions","google_drive_move","google_drive_search","google_drive_share","google_drive_trash","google_drive_unshare","google_drive_untrash","google_drive_update","google_drive_upload","google_forms_batch_update","google_forms_create_form","google_forms_create_watch","google_forms_delete_watch","google_forms_get_form","google_forms_get_responses","google_forms_list_watches","google_forms_renew_watch","google_forms_set_publish_settings","google_groups_add_alias","google_groups_add_member","google_groups_create_group","google_groups_delete_group","google_groups_get_group","google_groups_get_member","google_groups_get_settings","google_groups_has_member","google_groups_list_aliases","google_groups_list_groups","google_groups_list_members","google_groups_remove_alias","google_groups_remove_member","google_groups_update_group","google_groups_update_member","google_groups_update_settings","google_maps_air_quality","google_maps_directions","google_maps_distance_matrix","google_maps_elevation","google_maps_geocode","google_maps_geolocate","google_maps_place_details","google_maps_places_nearby","google_maps_places_search","google_maps_pollen","google_maps_reverse_geocode","google_maps_snap_to_roads","google_maps_solar","google_maps_speed_limits","google_maps_timezone","google_maps_validate_address","google_meet_create_space","google_meet_end_conference","google_meet_get_conference_record","google_meet_get_space","google_meet_list_conference_records","google_meet_list_participants","google_pagespeed_analyze","google_search","google_sheets_append","google_sheets_append_v2","google_sheets_batch_clear_v2","google_sheets_batch_get_v2","google_sheets_batch_update_v2","google_sheets_clear_v2","google_sheets_copy_sheet_v2","google_sheets_create_spreadsheet_v2","google_sheets_delete_rows_v2","google_sheets_delete_sheet_v2","google_sheets_delete_spreadsheet_v2","google_sheets_get_spreadsheet_v2","google_sheets_read","google_sheets_read_v2","google_sheets_update","google_sheets_update_v2","google_sheets_write","google_sheets_write_v2","google_slides_add_image","google_slides_add_slide","google_slides_batch_update","google_slides_copy_presentation","google_slides_create","google_slides_create_line","google_slides_create_paragraph_bullets","google_slides_create_shape","google_slides_create_sheets_chart","google_slides_create_table","google_slides_create_video","google_slides_delete_object","google_slides_delete_paragraph_bullets","google_slides_delete_table_column","google_slides_delete_table_row","google_slides_delete_text","google_slides_duplicate_object","google_slides_export_presentation","google_slides_get_page","google_slides_get_thumbnail","google_slides_group_objects","google_slides_insert_table_columns","google_slides_insert_table_rows","google_slides_insert_text","google_slides_merge_table_cells","google_slides_read","google_slides_refresh_sheets_chart","google_slides_replace_all_shapes_with_image","google_slides_replace_all_shapes_with_sheets_chart","google_slides_replace_all_text","google_slides_replace_image","google_slides_reroute_line","google_slides_ungroup_objects","google_slides_unmerge_table_cells","google_slides_update_image_properties","google_slides_update_line_category","google_slides_update_line_properties","google_slides_update_page_element_alt_text","google_slides_update_page_element_transform","google_slides_update_page_elements_z_order","google_slides_update_page_properties","google_slides_update_paragraph_style","google_slides_update_shape_properties","google_slides_update_slide_properties","google_slides_update_slides_position","google_slides_update_table_border_properties","google_slides_update_table_cell_properties","google_slides_update_table_column_properties","google_slides_update_table_row_properties","google_slides_update_text_style","google_slides_update_video_properties","google_slides_write","google_tasks_create","google_tasks_delete","google_tasks_get","google_tasks_list","google_tasks_list_task_lists","google_tasks_update","google_translate_detect","google_translate_text","google_vault_add_held_accounts","google_vault_add_matters_permissions","google_vault_close_matters","google_vault_create_matters","google_vault_create_matters_export","google_vault_create_matters_holds","google_vault_create_saved_query","google_vault_delete_matters","google_vault_delete_matters_export","google_vault_delete_matters_holds","google_vault_delete_saved_query","google_vault_download_export_file","google_vault_list_matters","google_vault_list_matters_export","google_vault_list_matters_holds","google_vault_list_saved_queries","google_vault_remove_held_accounts","google_vault_remove_matters_permissions","google_vault_reopen_matters","google_vault_undelete_matters","google_vault_update_matters","google_vault_update_matters_holds","grafana_check_data_source_health","grafana_create_alert_rule","grafana_create_annotation","grafana_create_contact_point","grafana_create_dashboard","grafana_create_folder","grafana_delete_alert_rule","grafana_delete_annotation","grafana_delete_contact_point","grafana_delete_dashboard","grafana_delete_folder","grafana_get_alert_rule","grafana_get_alert_rule_group","grafana_get_dashboard","grafana_get_data_source","grafana_get_folder","grafana_get_health","grafana_list_alert_rules","grafana_list_annotations","grafana_list_contact_points","grafana_list_dashboards","grafana_list_data_sources","grafana_list_folders","grafana_move_folder","grafana_query_data_source","grafana_update_alert_rule","grafana_update_annotation","grafana_update_contact_point","grafana_update_dashboard","grafana_update_folder","grain_create_hook","grain_create_hook_v2","grain_delete_hook","grain_delete_hook_v2","grain_get_recording","grain_get_transcript","grain_list_hooks","grain_list_hooks_v2","grain_list_meeting_types","grain_list_recordings","grain_list_teams","grain_list_views","granola_create_webhook_endpoint","granola_delete_webhook_endpoint","granola_get_note","granola_get_transcript","granola_list_audit_events","granola_list_folders","granola_list_notes","granola_list_webhook_endpoints","granola_update_webhook_endpoint","greenhouse_get_application","greenhouse_get_candidate","greenhouse_get_job","greenhouse_get_user","greenhouse_list_applications","greenhouse_list_candidates","greenhouse_list_departments","greenhouse_list_job_stages","greenhouse_list_jobs","greenhouse_list_offices","greenhouse_list_users","greptile_index_repo","greptile_query","greptile_search","greptile_status","guardrails_validate","hex_cancel_run","hex_create_collection","hex_create_group","hex_deactivate_user","hex_delete_group","hex_get_collection","hex_get_data_connection","hex_get_group","hex_get_project","hex_get_project_runs","hex_get_queried_tables","hex_get_run_status","hex_list_collections","hex_list_data_connections","hex_list_groups","hex_list_projects","hex_list_users","hex_run_project","hex_update_collection","hex_update_group","hex_update_project","http_request","hubspot_add_list_memberships","hubspot_create_appointment","hubspot_create_association","hubspot_create_company","hubspot_create_contact","hubspot_create_deal","hubspot_create_email","hubspot_create_line_item","hubspot_create_list","hubspot_create_note","hubspot_create_ticket","hubspot_delete_association","hubspot_delete_company","hubspot_delete_contact","hubspot_delete_deal","hubspot_delete_line_item","hubspot_delete_ticket","hubspot_get_appointment","hubspot_get_association_labels","hubspot_get_cart","hubspot_get_company","hubspot_get_contact","hubspot_get_deal","hubspot_get_email","hubspot_get_line_item","hubspot_get_list","hubspot_get_list_memberships","hubspot_get_marketing_event","hubspot_get_note","hubspot_get_properties","hubspot_get_quote","hubspot_get_ticket","hubspot_get_users","hubspot_list_appointments","hubspot_list_associations","hubspot_list_carts","hubspot_list_companies","hubspot_list_contacts","hubspot_list_deals","hubspot_list_emails","hubspot_list_line_items","hubspot_list_lists","hubspot_list_marketing_events","hubspot_list_notes","hubspot_list_owners","hubspot_list_quotes","hubspot_list_tickets","hubspot_remove_list_memberships","hubspot_search_companies","hubspot_search_contacts","hubspot_search_deals","hubspot_search_emails","hubspot_search_line_items","hubspot_search_notes","hubspot_search_quotes","hubspot_search_tickets","hubspot_update_appointment","hubspot_update_company","hubspot_update_contact","hubspot_update_deal","hubspot_update_line_item","hubspot_update_ticket","huggingface_chat","hunter_companies_find","hunter_discover","hunter_domain_search","hunter_email_count","hunter_email_finder","hunter_email_verifier","iam_add_user_to_group","iam_attach_role_policy","iam_attach_user_policy","iam_create_access_key","iam_create_role","iam_create_user","iam_delete_access_key","iam_delete_role","iam_delete_user","iam_detach_role_policy","iam_detach_user_policy","iam_get_role","iam_get_user","iam_list_attached_role_policies","iam_list_attached_user_policies","iam_list_groups","iam_list_policies","iam_list_roles","iam_list_users","iam_remove_user_from_group","iam_simulate_principal_policy","icypeas_find_email","icypeas_verify_email","identity_center_check_assignment_deletion_status","identity_center_check_assignment_status","identity_center_create_account_assignment","identity_center_delete_account_assignment","identity_center_describe_account","identity_center_get_group","identity_center_get_user","identity_center_list_account_assignments","identity_center_list_accounts","identity_center_list_groups","identity_center_list_instances","identity_center_list_permission_sets","image_generate","incidentio_actions_create","incidentio_actions_list","incidentio_actions_show","incidentio_actions_update","incidentio_alert_events_create","incidentio_alerts_list","incidentio_alerts_resolve","incidentio_alerts_show","incidentio_catalog_entries_list","incidentio_catalog_types_list","incidentio_custom_fields_create","incidentio_custom_fields_delete","incidentio_custom_fields_list","incidentio_custom_fields_show","incidentio_custom_fields_update","incidentio_escalation_paths_create","incidentio_escalation_paths_delete","incidentio_escalation_paths_list","incidentio_escalation_paths_show","incidentio_escalation_paths_update","incidentio_escalations_cancel","incidentio_escalations_create","incidentio_escalations_list","incidentio_escalations_show","incidentio_follow_ups_create","incidentio_follow_ups_list","incidentio_follow_ups_show","incidentio_follow_ups_update","incidentio_incident_alerts_list","incidentio_incident_memberships_create","incidentio_incident_memberships_revoke","incidentio_incident_participants_list","incidentio_incident_roles_create","incidentio_incident_roles_delete","incidentio_incident_roles_list","incidentio_incident_roles_show","incidentio_incident_roles_update","incidentio_incident_statuses_list","incidentio_incident_timestamps_list","incidentio_incident_timestamps_show","incidentio_incident_types_list","incidentio_incident_updates_list","incidentio_incidents_create","incidentio_incidents_list","incidentio_incidents_show","incidentio_incidents_update","incidentio_on_call_now","incidentio_schedule_entries_list","incidentio_schedule_overrides_create","incidentio_schedule_overrides_list","incidentio_schedules_create","incidentio_schedules_delete","incidentio_schedules_list","incidentio_schedules_show","incidentio_schedules_update","incidentio_severities_list","incidentio_teams_list","incidentio_teams_show","incidentio_users_list","incidentio_users_show","incidentio_workflows_create","incidentio_workflows_delete","incidentio_workflows_list","incidentio_workflows_show","incidentio_workflows_update","infisical_create_secret","infisical_delete_secret","infisical_get_secret","infisical_list_secrets","infisical_update_secret","instagram_delete_comment","instagram_download_media","instagram_get_account_insights","instagram_get_container_status","instagram_get_conversation_messages","instagram_get_media","instagram_get_media_insights","instagram_get_message","instagram_get_profile","instagram_get_publishing_limit","instagram_hide_comment","instagram_list_comments","instagram_list_conversations","instagram_list_media","instagram_list_stories","instagram_private_reply","instagram_publish_carousel","instagram_publish_image","instagram_publish_reel","instagram_publish_story","instagram_publish_video","instagram_reply_to_comment","instagram_send_text_message","instagram_set_comments_enabled","instantly_activate_campaign","instantly_create_campaign","instantly_create_lead","instantly_create_lead_list","instantly_delete_campaign","instantly_delete_leads","instantly_get_lead","instantly_list_campaigns","instantly_list_emails","instantly_list_lead_lists","instantly_list_leads","instantly_patch_campaign","instantly_patch_lead","instantly_pause_campaign","instantly_reply_to_email","instantly_update_lead_interest_status","intercom_assign_conversation_v2","intercom_attach_contact_to_company_v2","intercom_close_conversation_v2","intercom_create_company","intercom_create_company_v2","intercom_create_contact","intercom_create_contact_v2","intercom_create_event_v2","intercom_create_message","intercom_create_message_v2","intercom_create_note_v2","intercom_create_tag_v2","intercom_create_ticket","intercom_create_ticket_v2","intercom_delete_contact","intercom_delete_contact_v2","intercom_detach_contact_from_company_v2","intercom_get_company","intercom_get_company_v2","intercom_get_contact","intercom_get_contact_v2","intercom_get_conversation","intercom_get_conversation_v2","intercom_get_ticket","intercom_get_ticket_v2","intercom_list_admins_v2","intercom_list_companies","intercom_list_companies_v2","intercom_list_contacts","intercom_list_contacts_v2","intercom_list_conversations","intercom_list_conversations_v2","intercom_list_tags_v2","intercom_open_conversation_v2","intercom_reply_conversation","intercom_reply_conversation_v2","intercom_search_contacts","intercom_search_contacts_v2","intercom_search_conversations","intercom_search_conversations_v2","intercom_snooze_conversation_v2","intercom_tag_contact_v2","intercom_tag_conversation_v2","intercom_untag_contact_v2","intercom_update_contact","intercom_update_contact_v2","intercom_update_ticket_v2","jina_read_url","jina_search","jira_add_attachment","jira_add_comment","jira_add_watcher","jira_add_worklog","jira_assign_issue","jira_bulk_read","jira_create_issue_link","jira_delete_attachment","jira_delete_comment","jira_delete_issue","jira_delete_issue_link","jira_delete_worklog","jira_get_attachments","jira_get_comments","jira_get_fields","jira_get_project","jira_get_transitions","jira_get_users","jira_get_worklogs","jira_list_issue_types","jira_list_projects","jira_remove_watcher","jira_retrieve","jira_search_issues","jira_search_users","jira_transition_issue","jira_update","jira_update_comment","jira_update_worklog","jira_write","jotform_add_label_resources","jotform_clone_form","jotform_create_form","jotform_create_label","jotform_create_question","jotform_create_questions","jotform_create_report","jotform_create_submission","jotform_create_submissions","jotform_create_webhook","jotform_delete_form","jotform_delete_label","jotform_delete_question","jotform_delete_report","jotform_delete_submission","jotform_delete_webhook","jotform_get_form","jotform_get_form_properties","jotform_get_history","jotform_get_label","jotform_get_question","jotform_get_report","jotform_get_settings","jotform_get_submission","jotform_get_usage","jotform_get_user","jotform_list_form_files","jotform_list_form_reports","jotform_list_form_submissions","jotform_list_forms","jotform_list_label_resources","jotform_list_labels","jotform_list_questions","jotform_list_reports","jotform_list_submissions","jotform_list_subusers","jotform_list_webhooks","jotform_remove_label_resources","jotform_update_form_properties","jotform_update_label","jotform_update_question","jotform_update_settings","jotform_update_submission","jsm_add_comment","jsm_add_customer","jsm_add_organization","jsm_add_participants","jsm_answer_approval","jsm_attach_form","jsm_copy_forms","jsm_create_object","jsm_create_organization","jsm_create_request","jsm_delete_form","jsm_delete_object","jsm_externalise_form","jsm_get_approvals","jsm_get_comments","jsm_get_customers","jsm_get_form","jsm_get_form_answers","jsm_get_form_structure","jsm_get_form_templates","jsm_get_issue_forms","jsm_get_object","jsm_get_object_schema","jsm_get_object_type_attributes","jsm_get_organizations","jsm_get_participants","jsm_get_queues","jsm_get_request","jsm_get_request_type_fields","jsm_get_request_types","jsm_get_requests","jsm_get_service_desks","jsm_get_sla","jsm_get_transitions","jsm_internalise_form","jsm_list_object_schemas","jsm_list_object_types","jsm_reopen_form","jsm_save_form_answers","jsm_search_objects_aql","jsm_submit_form","jsm_transition_request","jsm_update_object","jupyter_copy_content","jupyter_create_file","jupyter_create_session","jupyter_delete_content","jupyter_delete_session","jupyter_get_content","jupyter_interrupt_kernel","jupyter_list_contents","jupyter_list_kernels","jupyter_list_kernelspecs","jupyter_list_sessions","jupyter_rename_content","jupyter_restart_kernel","jupyter_start_kernel","jupyter_stop_kernel","jupyter_upload_file","kalshi_amend_order","kalshi_amend_order_v2","kalshi_cancel_order","kalshi_cancel_order_v2","kalshi_create_order","kalshi_create_order_v2","kalshi_get_balance","kalshi_get_balance_v2","kalshi_get_candlesticks","kalshi_get_candlesticks_v2","kalshi_get_event","kalshi_get_event_candlesticks","kalshi_get_event_candlesticks_v2","kalshi_get_event_v2","kalshi_get_events","kalshi_get_events_v2","kalshi_get_exchange_announcements","kalshi_get_exchange_announcements_v2","kalshi_get_exchange_schedule","kalshi_get_exchange_schedule_v2","kalshi_get_exchange_status","kalshi_get_exchange_status_v2","kalshi_get_fills","kalshi_get_fills_v2","kalshi_get_market","kalshi_get_market_v2","kalshi_get_markets","kalshi_get_markets_v2","kalshi_get_order","kalshi_get_order_v2","kalshi_get_orderbook","kalshi_get_orderbook_v2","kalshi_get_orders","kalshi_get_orders_v2","kalshi_get_positions","kalshi_get_positions_v2","kalshi_get_series_by_ticker","kalshi_get_series_by_ticker_v2","kalshi_get_series_list","kalshi_get_series_list_v2","kalshi_get_settlements","kalshi_get_settlements_v2","kalshi_get_trades","kalshi_get_trades_v2","ketch_get_consent","ketch_get_subscriptions","ketch_invoke_right","ketch_set_consent","ketch_set_subscriptions","knowledge_create_document","knowledge_delete_chunk","knowledge_delete_document","knowledge_get_connector","knowledge_get_document","knowledge_list_chunks","knowledge_list_connectors","knowledge_list_documents","knowledge_list_tags","knowledge_search","knowledge_trigger_sync","knowledge_update_chunk","knowledge_upload_chunk","knowledge_upsert_document","langsmith_create_feedback","langsmith_create_run","langsmith_create_runs_batch","langsmith_get_run","langsmith_update_run","latex_compile","latex_get_package","latex_list_fonts","latex_search_packages","launchdarkly_create_flag","launchdarkly_delete_flag","launchdarkly_get_audit_log","launchdarkly_get_flag","launchdarkly_get_flag_status","launchdarkly_list_environments","launchdarkly_list_flags","launchdarkly_list_members","launchdarkly_list_projects","launchdarkly_list_segments","launchdarkly_toggle_flag","launchdarkly_update_flag","leadmagic_company_search","leadmagic_email_to_profile","leadmagic_find_email","leadmagic_find_mobile","leadmagic_get_credits","leadmagic_profile_search","leadmagic_profile_to_email","leadmagic_role_finder","leadmagic_validate_email","lemlist_get_activities","lemlist_get_lead","lemlist_send_email","linear_add_label_to_issue","linear_add_label_to_project","linear_archive_issue","linear_archive_label","linear_archive_project","linear_create_attachment","linear_create_comment","linear_create_customer","linear_create_customer_request","linear_create_customer_status","linear_create_customer_tier","linear_create_cycle","linear_create_favorite","linear_create_issue","linear_create_issue_relation","linear_create_label","linear_create_project","linear_create_project_label","linear_create_project_milestone","linear_create_project_status","linear_create_project_update","linear_create_workflow_state","linear_delete_attachment","linear_delete_comment","linear_delete_customer","linear_delete_customer_status","linear_delete_customer_tier","linear_delete_issue","linear_delete_issue_relation","linear_delete_project","linear_delete_project_label","linear_delete_project_milestone","linear_delete_project_status","linear_get_active_cycle","linear_get_customer","linear_get_cycle","linear_get_issue","linear_get_project","linear_get_viewer","linear_list_attachments","linear_list_comments","linear_list_customer_requests","linear_list_customer_statuses","linear_list_customer_tiers","linear_list_customers","linear_list_cycles","linear_list_favorites","linear_list_issue_relations","linear_list_labels","linear_list_notifications","linear_list_project_labels","linear_list_project_milestones","linear_list_project_statuses","linear_list_project_updates","linear_list_projects","linear_list_teams","linear_list_users","linear_list_workflow_states","linear_merge_customers","linear_read_issues","linear_remove_label_from_issue","linear_remove_label_from_project","linear_search_issues","linear_unarchive_issue","linear_update_attachment","linear_update_comment","linear_update_customer","linear_update_customer_request","linear_update_customer_status","linear_update_customer_tier","linear_update_issue","linear_update_label","linear_update_notification","linear_update_project","linear_update_project_label","linear_update_project_milestone","linear_update_project_status","linear_update_workflow_state","linkedin_get_profile","linkedin_share_post","linkup_search","linq_add_participant","linq_check_imessage","linq_check_rcs","linq_create_attachment","linq_create_chat","linq_create_contact_card","linq_create_webhook_subscription","linq_delete_attachment","linq_delete_message","linq_delete_webhook_subscription","linq_edit_message","linq_get_attachment","linq_get_chat","linq_get_contact_card","linq_get_message","linq_get_webhook_subscription","linq_leave_chat","linq_list_chats","linq_list_messages","linq_list_phone_numbers","linq_list_thread","linq_list_webhook_events","linq_list_webhook_subscriptions","linq_mark_chat_read","linq_react_to_message","linq_remove_participant","linq_send_message","linq_send_voice_memo","linq_share_contact_card","linq_start_typing","linq_stop_typing","linq_update_chat","linq_update_contact_card","linq_update_webhook_subscription","llm_chat","logfire_get_token_info","logfire_get_trace","logfire_query","logfire_search_records","logrocket_create_release","logrocket_get_audit_logs","logrocket_get_highlights","logrocket_identify_user","logrocket_list_exported_sessions","logrocket_request_highlights","logs_get","logs_get_execution","logs_get_run_details","logs_query","logs_query_runs","loops_check_contact_suppression","loops_create_contact","loops_create_contact_property","loops_delete_contact","loops_find_contact","loops_get_transactional_email","loops_list_contact_properties","loops_list_mailing_lists","loops_list_transactional_emails","loops_remove_contact_suppression","loops_send_event","loops_send_transactional_email","loops_update_contact","luma_add_guests","luma_cancel_event","luma_create_event","luma_get_event","luma_get_guest","luma_get_guests","luma_list_events","luma_lookup_event","luma_send_invites","luma_update_event","luma_update_guest_status","mailchimp_add_member","mailchimp_add_member_tags","mailchimp_add_or_update_member","mailchimp_add_segment_member","mailchimp_add_subscriber_to_automation","mailchimp_archive_member","mailchimp_create_audience","mailchimp_create_batch_operation","mailchimp_create_campaign","mailchimp_create_interest","mailchimp_create_interest_category","mailchimp_create_landing_page","mailchimp_create_merge_field","mailchimp_create_segment","mailchimp_create_template","mailchimp_delete_audience","mailchimp_delete_batch_operation","mailchimp_delete_campaign","mailchimp_delete_interest","mailchimp_delete_interest_category","mailchimp_delete_landing_page","mailchimp_delete_member","mailchimp_delete_merge_field","mailchimp_delete_segment","mailchimp_delete_template","mailchimp_get_audience","mailchimp_get_audiences","mailchimp_get_automation","mailchimp_get_automations","mailchimp_get_batch_operation","mailchimp_get_batch_operations","mailchimp_get_campaign","mailchimp_get_campaign_content","mailchimp_get_campaign_report","mailchimp_get_campaign_reports","mailchimp_get_campaigns","mailchimp_get_interest","mailchimp_get_interest_categories","mailchimp_get_interest_category","mailchimp_get_interests","mailchimp_get_landing_page","mailchimp_get_landing_pages","mailchimp_get_member","mailchimp_get_member_tags","mailchimp_get_members","mailchimp_get_merge_field","mailchimp_get_merge_fields","mailchimp_get_segment","mailchimp_get_segment_members","mailchimp_get_segments","mailchimp_get_template","mailchimp_get_templates","mailchimp_pause_automation","mailchimp_publish_landing_page","mailchimp_remove_member_tags","mailchimp_remove_segment_member","mailchimp_replicate_campaign","mailchimp_schedule_campaign","mailchimp_send_campaign","mailchimp_set_campaign_content","mailchimp_start_automation","mailchimp_unarchive_member","mailchimp_unpublish_landing_page","mailchimp_unschedule_campaign","mailchimp_update_audience","mailchimp_update_campaign","mailchimp_update_interest","mailchimp_update_interest_category","mailchimp_update_landing_page","mailchimp_update_member","mailchimp_update_merge_field","mailchimp_update_segment","mailchimp_update_template","mailgun_add_list_member","mailgun_create_mailing_list","mailgun_get_domain","mailgun_get_mailing_list","mailgun_get_message","mailgun_list_domains","mailgun_list_messages","mailgun_send_message","managed_agent_archive_session","managed_agent_create_session","managed_agent_delete_session","managed_agent_get_session","managed_agent_interrupt_session","managed_agent_list_events","managed_agent_respond_custom_tool","managed_agent_respond_tool_confirmation","managed_agent_run_session","managed_agent_send_message","managed_agent_update_session","mem0_add_memories","mem0_get_memories","mem0_search_memories","memory_add","memory_delete","memory_get","memory_get_all","microsoft_ad_add_directory_role_member","microsoft_ad_add_group_member","microsoft_ad_add_user_app_role_assignment","microsoft_ad_assign_license","microsoft_ad_create_group","microsoft_ad_create_user","microsoft_ad_delete_group","microsoft_ad_delete_user","microsoft_ad_get_conditional_access_policy","microsoft_ad_get_device","microsoft_ad_get_group","microsoft_ad_get_user","microsoft_ad_list_authentication_methods","microsoft_ad_list_conditional_access_policies","microsoft_ad_list_devices","microsoft_ad_list_directory_audits","microsoft_ad_list_directory_role_members","microsoft_ad_list_directory_roles","microsoft_ad_list_group_members","microsoft_ad_list_groups","microsoft_ad_list_service_principal_app_role_assignments","microsoft_ad_list_service_principals","microsoft_ad_list_sign_ins","microsoft_ad_list_subscribed_skus","microsoft_ad_list_user_app_role_assignments","microsoft_ad_list_user_devices","microsoft_ad_list_user_licenses","microsoft_ad_list_users","microsoft_ad_remove_directory_role_member","microsoft_ad_remove_group_member","microsoft_ad_remove_user_app_role_assignment","microsoft_ad_reset_password","microsoft_ad_revoke_sign_in_sessions","microsoft_ad_set_password","microsoft_ad_update_group","microsoft_ad_update_user","microsoft_dataverse_associate","microsoft_dataverse_create_multiple","microsoft_dataverse_create_record","microsoft_dataverse_delete_record","microsoft_dataverse_disassociate","microsoft_dataverse_download_file","microsoft_dataverse_execute_action","microsoft_dataverse_execute_function","microsoft_dataverse_fetchxml_query","microsoft_dataverse_get_entity_metadata","microsoft_dataverse_get_record","microsoft_dataverse_list_records","microsoft_dataverse_search","microsoft_dataverse_update_multiple","microsoft_dataverse_update_record","microsoft_dataverse_upload_file","microsoft_dataverse_upsert_record","microsoft_dataverse_whoami","microsoft_excel_clear_range","microsoft_excel_create_table","microsoft_excel_delete_worksheet","microsoft_excel_format_range","microsoft_excel_read","microsoft_excel_read_v2","microsoft_excel_sort_range","microsoft_excel_table_add","microsoft_excel_worksheet_add","microsoft_excel_write","microsoft_excel_write_v2","microsoft_planner_create_bucket","microsoft_planner_create_plan","microsoft_planner_create_task","microsoft_planner_delete_bucket","microsoft_planner_delete_plan","microsoft_planner_delete_task","microsoft_planner_get_plan_details","microsoft_planner_get_task_details","microsoft_planner_list_buckets","microsoft_planner_list_plans","microsoft_planner_read_bucket","microsoft_planner_read_plan","microsoft_planner_read_task","microsoft_planner_update_bucket","microsoft_planner_update_plan","microsoft_planner_update_plan_details","microsoft_planner_update_task","microsoft_planner_update_task_details","microsoft_teams_delete_channel_message","microsoft_teams_delete_chat_message","microsoft_teams_get_message","microsoft_teams_list_channel_members","microsoft_teams_list_channels","microsoft_teams_list_chat_members","microsoft_teams_list_chats","microsoft_teams_list_team_members","microsoft_teams_list_teams","microsoft_teams_read_channel","microsoft_teams_read_chat","microsoft_teams_reply_to_message","microsoft_teams_set_reaction","microsoft_teams_unset_reaction","microsoft_teams_update_channel_message","microsoft_teams_update_chat_message","microsoft_teams_write_channel","microsoft_teams_write_chat","millionverifier_get_credits","millionverifier_verify_email","mintlify_create_agent_job","mintlify_create_assistant_message","mintlify_detect_ai_prose","mintlify_get_agent_job","mintlify_get_assistant_caller_stats","mintlify_get_assistant_conversations","mintlify_get_feedback","mintlify_get_feedback_by_page","mintlify_get_page_content","mintlify_get_searches","mintlify_get_update_status","mintlify_get_views","mintlify_get_visitors","mintlify_search","mintlify_send_agent_message","mintlify_trigger_automation","mintlify_trigger_preview","mintlify_trigger_update","mistral_parser","mistral_parser_v2","mistral_parser_v3","monday_archive_item","monday_change_column_value","monday_create_board","monday_create_column","monday_create_group","monday_create_item","monday_create_subitem","monday_create_update","monday_delete_item","monday_duplicate_item","monday_get_board","monday_get_groups","monday_get_item","monday_get_items","monday_list_boards","monday_move_item_to_group","monday_search_items","monday_update_item","mongodb_delete","mongodb_execute","mongodb_insert","mongodb_introspect","mongodb_query","mongodb_update","mssql_delete","mssql_execute","mssql_insert","mssql_introspect","mssql_query","mssql_update","mysql_delete","mysql_execute","mysql_insert","mysql_introspect","mysql_query","mysql_update","neo4j_create","neo4j_delete","neo4j_execute","neo4j_introspect","neo4j_merge","neo4j_query","neo4j_update","netsuite_attach_record","netsuite_batch_create_records","netsuite_batch_delete_records","netsuite_batch_get_records","netsuite_batch_update_records","netsuite_batch_upsert_records","netsuite_create_record","netsuite_delete_record","netsuite_detach_record","netsuite_execute_action","netsuite_execute_dataset","netsuite_execute_suiteql","netsuite_get_async_result","netsuite_get_async_status","netsuite_get_governance_limits","netsuite_get_record","netsuite_get_record_form","netsuite_get_record_metadata","netsuite_get_select_options","netsuite_get_server_time","netsuite_get_subresource","netsuite_list_datasets","netsuite_list_record_types","netsuite_list_records","netsuite_transform_record","netsuite_update_record","netsuite_upsert_record","neverbounce_get_credits","neverbounce_verify_email","new_relic_create_deployment_event","new_relic_get_entity","new_relic_nrql_query","new_relic_search_entities","notion_add_database_row","notion_add_database_row_v2","notion_append_blocks","notion_append_blocks_v2","notion_create_comment","notion_create_comment_v2","notion_create_database","notion_create_database_v2","notion_create_page","notion_create_page_v2","notion_delete_block","notion_delete_block_v2","notion_list_comments","notion_list_comments_v2","notion_list_users","notion_list_users_v2","notion_query_database","notion_query_database_v2","notion_read","notion_read_database","notion_read_database_v2","notion_read_v2","notion_retrieve_block","notion_retrieve_block_children","notion_retrieve_block_children_v2","notion_retrieve_block_v2","notion_retrieve_user","notion_retrieve_user_v2","notion_search","notion_search_v2","notion_update_block","notion_update_block_v2","notion_update_page","notion_update_page_v2","notion_write","notion_write_v2","obsidian_append_active","obsidian_append_note","obsidian_append_periodic_note","obsidian_create_note","obsidian_delete_note","obsidian_execute_command","obsidian_get_active","obsidian_get_note","obsidian_get_periodic_note","obsidian_list_commands","obsidian_list_files","obsidian_open_file","obsidian_patch_active","obsidian_patch_note","obsidian_search","okta_activate_group_rule","okta_activate_user","okta_add_user_to_group","okta_assign_group_to_app","okta_assign_user_role","okta_assign_user_to_app","okta_clear_user_sessions","okta_create_group","okta_create_group_rule","okta_create_user","okta_deactivate_group_rule","okta_deactivate_user","okta_delete_group","okta_delete_group_rule","okta_delete_user","okta_enroll_factor","okta_get_app","okta_get_factor","okta_get_group","okta_get_group_rule","okta_get_logs","okta_get_session","okta_get_user","okta_list_app_groups","okta_list_app_users","okta_list_apps","okta_list_factors","okta_list_group_members","okta_list_group_rules","okta_list_groups","okta_list_user_roles","okta_list_users","okta_remove_group_from_app","okta_remove_user_from_app","okta_remove_user_from_group","okta_remove_user_role","okta_reset_all_factors","okta_reset_factor","okta_reset_password","okta_revoke_session","okta_suspend_user","okta_unsuspend_user","okta_update_group","okta_update_user","onedrive_copy","onedrive_create_folder","onedrive_create_share_link","onedrive_delete","onedrive_download","onedrive_get_drive_info","onedrive_get_item","onedrive_list","onedrive_move","onedrive_search","onedrive_upload","onepassword_create_item","onepassword_delete_item","onepassword_get_item","onepassword_get_item_file","onepassword_get_vault","onepassword_list_items","onepassword_list_vaults","onepassword_replace_item","onepassword_resolve_secret","onepassword_update_item","openai_embeddings","openai_image","outlook_calendar_create_event","outlook_calendar_delete_event","outlook_calendar_get_event","outlook_calendar_list_events","outlook_calendar_respond","outlook_calendar_update_event","outlook_copy","outlook_create_folder","outlook_delete","outlook_draft","outlook_forward","outlook_get_attachment","outlook_list_attachments","outlook_list_folders","outlook_mark_read","outlook_mark_unread","outlook_move","outlook_read","outlook_reply","outlook_reply_all","outlook_search","outlook_send","outlook_update_message","pagerduty_add_note","pagerduty_create_incident","pagerduty_get_incident","pagerduty_get_service","pagerduty_list_escalation_policies","pagerduty_list_incident_alerts","pagerduty_list_incidents","pagerduty_list_oncalls","pagerduty_list_schedules","pagerduty_list_services","pagerduty_list_users","pagerduty_merge_incidents","pagerduty_send_event","pagerduty_snooze_incident","pagerduty_update_incident","parallel_deep_research","parallel_extract","parallel_search","pdl_autocomplete","pdl_bulk_company_enrich","pdl_bulk_person_enrich","pdl_clean_company","pdl_clean_location","pdl_clean_school","pdl_company_enrich","pdl_company_search","pdl_person_enrich","pdl_person_identify","pdl_person_search","perplexity_chat","perplexity_search","persona_approve_inquiry","persona_create_account","persona_create_inquiry","persona_create_report","persona_decline_inquiry","persona_expire_inquiry","persona_generate_inquiry_link","persona_get_account","persona_get_case","persona_get_document","persona_get_inquiry","persona_get_report","persona_get_verification","persona_import_accounts","persona_list_accounts","persona_list_cases","persona_list_inquiries","persona_list_inquiry_templates","persona_list_reports","persona_mark_inquiry_for_review","persona_print_inquiry_pdf","persona_redact_account","persona_redact_inquiry","persona_resume_inquiry","persona_update_account","persona_update_inquiry","pinecone_delete_vectors","pinecone_describe_index","pinecone_describe_index_stats","pinecone_fetch","pinecone_generate_embeddings","pinecone_list_indexes","pinecone_list_vector_ids","pinecone_search_text","pinecone_search_vector","pinecone_update_vector","pinecone_upsert_text","pipedrive_create_activity","pipedrive_create_deal","pipedrive_create_lead","pipedrive_create_project","pipedrive_delete_lead","pipedrive_get_activities","pipedrive_get_all_deals","pipedrive_get_deal","pipedrive_get_files","pipedrive_get_leads","pipedrive_get_mail_messages","pipedrive_get_mail_thread","pipedrive_get_pipeline_deals","pipedrive_get_pipelines","pipedrive_get_projects","pipedrive_update_activity","pipedrive_update_deal","pipedrive_update_lead","pitchbook_company_active_investors","pitchbook_company_bio","pitchbook_company_deal_service_providers","pitchbook_company_deals","pitchbook_company_financials","pitchbook_company_general_service_providers","pitchbook_company_industries","pitchbook_company_investors","pitchbook_company_most_recent_debt_financing","pitchbook_company_most_recent_financials","pitchbook_company_most_recent_financing","pitchbook_company_search","pitchbook_company_similar_companies","pitchbook_company_social_analytics","pitchbook_company_updates","pitchbook_company_vc_exit_predictions","pitchbook_contracts_history","pitchbook_cost_of_calls","pitchbook_credit_history","pitchbook_credit_news","pitchbook_credit_news_bulk","pitchbook_credit_news_most_recent","pitchbook_credit_news_search","pitchbook_deal_bio","pitchbook_deal_cap_table_history","pitchbook_deal_debt_lenders","pitchbook_deal_detailed","pitchbook_deal_investors","pitchbook_deal_multiples","pitchbook_deal_search","pitchbook_deal_service_providers","pitchbook_deal_stock_info","pitchbook_deal_tranche_info","pitchbook_deal_updates","pitchbook_deal_valuation","pitchbook_entity_affiliates","pitchbook_entity_locations","pitchbook_entity_news","pitchbook_entity_people","pitchbook_entity_updates","pitchbook_fund_active_investments","pitchbook_fund_benchmark","pitchbook_fund_bio","pitchbook_fund_cash_flows","pitchbook_fund_commitments","pitchbook_fund_investment_preferences","pitchbook_fund_investments","pitchbook_fund_performance","pitchbook_fund_search","pitchbook_fund_team","pitchbook_fund_updates","pitchbook_investor_active_investments","pitchbook_investor_bio","pitchbook_investor_board_seats","pitchbook_investor_deal_service_providers","pitchbook_investor_funds","pitchbook_investor_general_service_providers","pitchbook_investor_investments","pitchbook_investor_last_closed_fund","pitchbook_investor_preferences","pitchbook_investor_search","pitchbook_investor_updates","pitchbook_limited_partner_actual_allocations","pitchbook_limited_partner_bio","pitchbook_limited_partner_commitment_aggregates","pitchbook_limited_partner_commitment_preferences","pitchbook_limited_partner_commitments_detailed","pitchbook_limited_partner_search","pitchbook_limited_partner_service_providers","pitchbook_limited_partner_target_allocations","pitchbook_limited_partner_updates","pitchbook_lookup_table_structure","pitchbook_lookup_tables","pitchbook_patent_detailed","pitchbook_patent_search","pitchbook_people_search","pitchbook_person_bio","pitchbook_person_contact","pitchbook_person_education_work","pitchbook_sandbox_entities","pitchbook_search","pitchbook_service_provider_bio","pitchbook_service_provider_search","pitchbook_service_provider_updates","pitchbook_serviced_companies","pitchbook_serviced_deals","pitchbook_serviced_funds","pitchbook_serviced_investors","pitchbook_serviced_limited_partners","pitchbook_shared_search","pitchbook_usage_report","polymarket_get_activity","polymarket_get_event","polymarket_get_events","polymarket_get_holders","polymarket_get_last_trade_price","polymarket_get_leaderboard","polymarket_get_market","polymarket_get_markets","polymarket_get_midpoint","polymarket_get_orderbook","polymarket_get_positions","polymarket_get_price","polymarket_get_price_history","polymarket_get_series","polymarket_get_series_by_id","polymarket_get_spread","polymarket_get_tags","polymarket_get_tick_size","polymarket_get_trades","polymarket_search","postgresql_delete","postgresql_execute","postgresql_insert","postgresql_introspect","postgresql_query","postgresql_update","posthog_batch_events","posthog_capture_event","posthog_create_annotation","posthog_create_cohort","posthog_create_dashboard","posthog_create_experiment","posthog_create_feature_flag","posthog_create_insight","posthog_create_survey","posthog_delete_feature_flag","posthog_delete_person","posthog_delete_survey","posthog_evaluate_flags","posthog_get_cohort","posthog_get_dashboard","posthog_get_event_definition","posthog_get_experiment","posthog_get_feature_flag","posthog_get_insight","posthog_get_organization","posthog_get_person","posthog_get_project","posthog_get_property_definition","posthog_get_session_recording","posthog_get_survey","posthog_list_actions","posthog_list_annotations","posthog_list_cohorts","posthog_list_dashboards","posthog_list_event_definitions","posthog_list_experiments","posthog_list_feature_flags","posthog_list_insights","posthog_list_organizations","posthog_list_persons","posthog_list_projects","posthog_list_property_definitions","posthog_list_recording_playlists","posthog_list_session_recordings","posthog_list_surveys","posthog_query","posthog_update_cohort","posthog_update_event_definition","posthog_update_experiment","posthog_update_feature_flag","posthog_update_insight","posthog_update_property_definition","posthog_update_survey","profound_bot_logs","profound_bots_report","profound_category_assets","profound_category_personas","profound_category_prompts","profound_category_tags","profound_category_topics","profound_citation_prompts","profound_citations_report","profound_list_assets","profound_list_categories","profound_list_domains","profound_list_models","profound_list_optimizations","profound_list_personas","profound_list_regions","profound_optimization_analysis","profound_prompt_answers","profound_prompt_volume","profound_query_fanouts","profound_raw_logs","profound_referrals_report","profound_sentiment_report","profound_visibility_report","prospeo_account_information","prospeo_bulk_enrich_company","prospeo_bulk_enrich_person","prospeo_enrich_company","prospeo_enrich_person","prospeo_search_company","prospeo_search_person","prospeo_search_suggestions","pulse_parser","pulse_parser_v2","qdrant_fetch_points","qdrant_search_vector","qdrant_upsert_points","quartr_get_audio","quartr_get_company","quartr_get_event","quartr_get_event_summary","quartr_get_report","quartr_get_slide_deck","quartr_get_transcript","quartr_list_audio","quartr_list_companies","quartr_list_document_types","quartr_list_documents","quartr_list_event_types","quartr_list_events","quartr_list_live_events","quartr_list_reports","quartr_list_slide_decks","quartr_list_transcripts","quiver_image_to_svg","quiver_list_models","quiver_text_to_svg","rabbitmq_create_binding","rabbitmq_create_exchange","rabbitmq_create_policy","rabbitmq_create_queue","rabbitmq_delete_binding","rabbitmq_delete_exchange","rabbitmq_delete_policy","rabbitmq_delete_queue","rabbitmq_get_exchange","rabbitmq_get_messages","rabbitmq_get_overview","rabbitmq_get_queue","rabbitmq_health_check","rabbitmq_list_bindings","rabbitmq_list_channels","rabbitmq_list_connections","rabbitmq_list_consumers","rabbitmq_list_exchange_bindings","rabbitmq_list_exchanges","rabbitmq_list_nodes","rabbitmq_list_policies","rabbitmq_list_queues","rabbitmq_list_vhosts","rabbitmq_publish_message","rabbitmq_purge_queue","railway_create_environment","railway_create_project","railway_create_service","railway_delete_environment","railway_delete_project","railway_delete_service","railway_delete_variable","railway_deploy_service","railway_get_deployment","railway_get_deployment_logs","railway_get_project","railway_list_deployments","railway_list_project_members","railway_list_projects","railway_list_variables","railway_restart_deployment","railway_rollback_deployment","railway_transfer_project","railway_update_project","railway_upsert_variable","rb2b_credit_check","rb2b_email_to_activity","rb2b_hem_to_best_linkedin","rb2b_hem_to_business_profile","rb2b_hem_to_linkedin","rb2b_hem_to_maid","rb2b_ip_to_company","rb2b_ip_to_hem","rb2b_ip_to_maid","rb2b_linkedin_slug_search","rb2b_linkedin_to_best_personal_email","rb2b_linkedin_to_business_profile","rb2b_linkedin_to_hashed_emails","rb2b_linkedin_to_mobile_phone","rb2b_linkedin_to_personal_email","rds_delete","rds_execute","rds_insert","rds_introspect","rds_query","rds_update","reddit_delete","reddit_edit","reddit_get_comments","reddit_get_controversial","reddit_get_info","reddit_get_me","reddit_get_messages","reddit_get_posts","reddit_get_saved","reddit_get_subreddit_info","reddit_get_subreddit_rules","reddit_get_user","reddit_get_user_comments","reddit_get_user_posts","reddit_hide","reddit_hot_posts","reddit_list_my_subreddits","reddit_lock","reddit_mark_all_read","reddit_mark_read","reddit_marknsfw","reddit_mod_approve","reddit_mod_distinguish","reddit_mod_remove","reddit_mod_sticky","reddit_reply","reddit_report","reddit_save","reddit_search","reddit_search_subreddits","reddit_send_message","reddit_submit_post","reddit_subscribe","reddit_unhide","reddit_unlock","reddit_unmarknsfw","reddit_unsave","reddit_vote","redis_command","redis_delete","redis_exists","redis_expire","redis_get","redis_hdel","redis_hget","redis_hgetall","redis_hset","redis_incr","redis_incrby","redis_keys","redis_llen","redis_lpop","redis_lpush","redis_lrange","redis_persist","redis_rpop","redis_rpush","redis_set","redis_setnx","redis_ttl","reducto_parser","reducto_parser_v2","resend_cancel_email","resend_create_audience","resend_create_broadcast","resend_create_contact","resend_delete_audience","resend_delete_contact","resend_get_audience","resend_get_broadcast","resend_get_contact","resend_get_email","resend_list_audiences","resend_list_contacts","resend_list_domains","resend_send","resend_send_broadcast","resend_update_contact","revenuecat_create_purchase","revenuecat_defer_google_subscription","revenuecat_delete_customer","revenuecat_get_customer","revenuecat_grant_entitlement","revenuecat_list_offerings","revenuecat_refund_google_subscription","revenuecat_revoke_entitlement","revenuecat_revoke_google_subscription","revenuecat_update_subscriber_attributes","rippling_bulk_create_custom_object_records","rippling_bulk_delete_custom_object_records","rippling_bulk_update_custom_object_records","rippling_create_business_partner","rippling_create_business_partner_group","rippling_create_custom_app","rippling_create_custom_object","rippling_create_custom_object_field","rippling_create_custom_object_record","rippling_create_custom_page","rippling_create_custom_setting","rippling_create_department","rippling_create_draft_hires","rippling_create_object_category","rippling_create_title","rippling_create_work_location","rippling_delete_business_partner","rippling_delete_business_partner_group","rippling_delete_custom_app","rippling_delete_custom_object","rippling_delete_custom_object_field","rippling_delete_custom_object_record","rippling_delete_custom_page","rippling_delete_custom_setting","rippling_delete_object_category","rippling_delete_title","rippling_delete_work_location","rippling_get_business_partner","rippling_get_business_partner_group","rippling_get_current_user","rippling_get_custom_app","rippling_get_custom_object","rippling_get_custom_object_field","rippling_get_custom_object_record","rippling_get_custom_object_record_by_external_id","rippling_get_custom_page","rippling_get_custom_setting","rippling_get_department","rippling_get_employment_type","rippling_get_job_function","rippling_get_object_category","rippling_get_report_run","rippling_get_supergroup","rippling_get_team","rippling_get_title","rippling_get_user","rippling_get_work_location","rippling_get_worker","rippling_list_business_partner_groups","rippling_list_business_partners","rippling_list_companies","rippling_list_custom_apps","rippling_list_custom_fields","rippling_list_custom_object_fields","rippling_list_custom_object_records","rippling_list_custom_objects","rippling_list_custom_pages","rippling_list_custom_settings","rippling_list_departments","rippling_list_employment_types","rippling_list_entitlements","rippling_list_job_functions","rippling_list_object_categories","rippling_list_supergroup_exclusion_members","rippling_list_supergroup_inclusion_members","rippling_list_supergroup_members","rippling_list_supergroups","rippling_list_teams","rippling_list_titles","rippling_list_users","rippling_list_work_locations","rippling_list_workers","rippling_query_custom_object_records","rippling_trigger_report_run","rippling_update_custom_app","rippling_update_custom_object","rippling_update_custom_object_field","rippling_update_custom_object_record","rippling_update_custom_page","rippling_update_custom_setting","rippling_update_department","rippling_update_object_category","rippling_update_supergroup_exclusion_members","rippling_update_supergroup_inclusion_members","rippling_update_title","rippling_update_work_location","rocketlane_add_field_option","rocketlane_add_project_members","rocketlane_add_task_assignees","rocketlane_add_task_dependencies","rocketlane_add_task_followers","rocketlane_archive_project","rocketlane_assign_placeholders","rocketlane_create_field","rocketlane_create_phase","rocketlane_create_project","rocketlane_create_space","rocketlane_create_space_document","rocketlane_create_task","rocketlane_create_time_entry","rocketlane_create_time_off","rocketlane_delete_field","rocketlane_delete_phase","rocketlane_delete_project","rocketlane_delete_space","rocketlane_delete_space_document","rocketlane_delete_task","rocketlane_delete_time_entry","rocketlane_delete_time_off","rocketlane_get_field","rocketlane_get_invoice","rocketlane_get_invoice_line_items","rocketlane_get_invoice_payments","rocketlane_get_phase","rocketlane_get_project","rocketlane_get_space","rocketlane_get_space_document","rocketlane_get_task","rocketlane_get_time_entry","rocketlane_get_time_off","rocketlane_get_user","rocketlane_import_template","rocketlane_list_fields","rocketlane_list_invoices","rocketlane_list_phases","rocketlane_list_placeholders","rocketlane_list_projects","rocketlane_list_resource_allocations","rocketlane_list_space_documents","rocketlane_list_spaces","rocketlane_list_tasks","rocketlane_list_time_entries","rocketlane_list_time_entry_categories","rocketlane_list_time_offs","rocketlane_list_users","rocketlane_move_task_to_phase","rocketlane_remove_project_members","rocketlane_remove_task_assignees","rocketlane_remove_task_dependencies","rocketlane_remove_task_followers","rocketlane_search_time_entries","rocketlane_unassign_placeholders","rocketlane_update_field","rocketlane_update_field_option","rocketlane_update_phase","rocketlane_update_project","rocketlane_update_space","rocketlane_update_space_document","rocketlane_update_task","rocketlane_update_time_entry","rootly_acknowledge_alert","rootly_add_incident_event","rootly_add_subscribers","rootly_assign_incident_role","rootly_create_action_item","rootly_create_alert","rootly_create_incident","rootly_create_status_page_event","rootly_delete_action_item","rootly_delete_incident","rootly_escalate_alert","rootly_get_alert","rootly_get_incident","rootly_list_action_items","rootly_list_alerts","rootly_list_causes","rootly_list_environments","rootly_list_escalation_policies","rootly_list_functionalities","rootly_list_incident_events","rootly_list_incident_roles","rootly_list_incident_types","rootly_list_incidents","rootly_list_on_calls","rootly_list_playbooks","rootly_list_retrospectives","rootly_list_schedules","rootly_list_services","rootly_list_severities","rootly_list_teams","rootly_list_users","rootly_mitigate_incident","rootly_remove_subscribers","rootly_resolve_alert","rootly_resolve_incident","rootly_run_workflow","rootly_snooze_alert","rootly_unassign_incident_role","rootly_update_action_item","rootly_update_alert","rootly_update_incident","s3_copy_object","s3_create_bucket","s3_delete_bucket","s3_delete_object","s3_delete_objects","s3_get_object","s3_head_object","s3_list_buckets","s3_list_objects","s3_presigned_url","s3_put_object","salesforce_create_account","salesforce_create_case","salesforce_create_contact","salesforce_create_custom_field","salesforce_create_custom_object","salesforce_create_lead","salesforce_create_opportunity","salesforce_create_task","salesforce_delete_account","salesforce_delete_case","salesforce_delete_contact","salesforce_delete_custom_field","salesforce_delete_lead","salesforce_delete_opportunity","salesforce_delete_task","salesforce_describe_object","salesforce_get_accounts","salesforce_get_cases","salesforce_get_contacts","salesforce_get_dashboard","salesforce_get_leads","salesforce_get_opportunities","salesforce_get_report","salesforce_get_tasks","salesforce_list_dashboards","salesforce_list_objects","salesforce_list_report_types","salesforce_list_reports","salesforce_query","salesforce_query_more","salesforce_refresh_dashboard","salesforce_run_report","salesforce_tooling_query","salesforce_update_account","salesforce_update_case","salesforce_update_contact","salesforce_update_custom_field","salesforce_update_lead","salesforce_update_opportunity","salesforce_update_task","sap_concur_approve_expense_report","sap_concur_associate_attendees","sap_concur_create_cash_advance","sap_concur_create_expected_expense","sap_concur_create_expense_report","sap_concur_create_list_item","sap_concur_create_purchase_request","sap_concur_create_quick_expense","sap_concur_create_quick_expense_with_image","sap_concur_create_report_comment","sap_concur_create_travel_request","sap_concur_create_user","sap_concur_delete_expected_expense","sap_concur_delete_expense","sap_concur_delete_expense_report","sap_concur_delete_list_item","sap_concur_delete_travel_request","sap_concur_delete_user","sap_concur_get_allocation","sap_concur_get_budget","sap_concur_get_cash_advance","sap_concur_get_expected_expense","sap_concur_get_expense","sap_concur_get_expense_report","sap_concur_get_itemizations","sap_concur_get_itinerary","sap_concur_get_list","sap_concur_get_list_item","sap_concur_get_purchase_request","sap_concur_get_receipt","sap_concur_get_receipt_status","sap_concur_get_request_cash_advance","sap_concur_get_travel_profile","sap_concur_get_travel_request","sap_concur_get_user","sap_concur_issue_cash_advance","sap_concur_list_allocations","sap_concur_list_attendee_associations","sap_concur_list_budget_categories","sap_concur_list_budgets","sap_concur_list_exceptions","sap_concur_list_expected_expenses","sap_concur_list_expense_reports","sap_concur_list_expenses","sap_concur_list_itineraries","sap_concur_list_list_items","sap_concur_list_lists","sap_concur_list_receipts","sap_concur_list_report_comments","sap_concur_list_reports_to_approve","sap_concur_list_travel_profiles_summary","sap_concur_list_travel_request_comments","sap_concur_list_travel_requests","sap_concur_list_users","sap_concur_move_travel_request","sap_concur_recall_expense_report","sap_concur_remove_all_attendees","sap_concur_search_locations","sap_concur_search_users","sap_concur_send_back_expense_report","sap_concur_submit_expense_report","sap_concur_update_allocation","sap_concur_update_expected_expense","sap_concur_update_expense","sap_concur_update_expense_report","sap_concur_update_list_item","sap_concur_update_travel_request","sap_concur_update_user","sap_concur_upload_exchange_rates","sap_concur_upload_receipt_image","sap_s4hana_create_business_partner","sap_s4hana_create_purchase_order","sap_s4hana_create_purchase_requisition","sap_s4hana_create_sales_order","sap_s4hana_delete_sales_order","sap_s4hana_get_billing_document","sap_s4hana_get_business_partner","sap_s4hana_get_customer","sap_s4hana_get_inbound_delivery","sap_s4hana_get_material_document","sap_s4hana_get_outbound_delivery","sap_s4hana_get_product","sap_s4hana_get_purchase_order","sap_s4hana_get_purchase_requisition","sap_s4hana_get_sales_order","sap_s4hana_get_supplier","sap_s4hana_get_supplier_invoice","sap_s4hana_list_billing_documents","sap_s4hana_list_business_partners","sap_s4hana_list_customers","sap_s4hana_list_inbound_deliveries","sap_s4hana_list_material_documents","sap_s4hana_list_material_stock","sap_s4hana_list_outbound_deliveries","sap_s4hana_list_products","sap_s4hana_list_purchase_orders","sap_s4hana_list_purchase_requisitions","sap_s4hana_list_sales_orders","sap_s4hana_list_supplier_invoices","sap_s4hana_list_suppliers","sap_s4hana_odata_query","sap_s4hana_update_business_partner","sap_s4hana_update_customer","sap_s4hana_update_product","sap_s4hana_update_purchase_order","sap_s4hana_update_purchase_requisition","sap_s4hana_update_sales_order","sap_s4hana_update_supplier","search_tool","secrets_manager_create_secret","secrets_manager_delete_secret","secrets_manager_describe_secret","secrets_manager_get_secret","secrets_manager_list_secrets","secrets_manager_restore_secret","secrets_manager_rotate_secret","secrets_manager_tag_resource","secrets_manager_untag_resource","secrets_manager_update_secret","sendblue_evaluate_service","sendblue_get_message","sendblue_send_group_message","sendblue_send_message","sendblue_send_typing_indicator","sendgrid_add_contact","sendgrid_add_contacts_to_list","sendgrid_create_list","sendgrid_create_template","sendgrid_create_template_version","sendgrid_delete_contacts","sendgrid_delete_list","sendgrid_delete_template","sendgrid_get_contact","sendgrid_get_list","sendgrid_get_template","sendgrid_list_all_lists","sendgrid_list_templates","sendgrid_remove_contacts_from_list","sendgrid_search_contacts","sendgrid_send_mail","sentry_events_get","sentry_events_list","sentry_issues_get","sentry_issues_list","sentry_issues_update","sentry_projects_create","sentry_projects_get","sentry_projects_list","sentry_projects_update","sentry_releases_create","sentry_releases_deploy","sentry_releases_list","sentry_teams_list","serper_search","servicenow_add_incident_comment","servicenow_aggregate","servicenow_close_incident","servicenow_create_change_request","servicenow_create_incident","servicenow_create_record","servicenow_delete_record","servicenow_download_attachment","servicenow_find_user","servicenow_get_change_next_states","servicenow_get_change_request","servicenow_get_ci","servicenow_get_incident","servicenow_get_knowledge_article","servicenow_get_requested_item","servicenow_list_approvals","servicenow_list_attachments","servicenow_list_catalog_items","servicenow_list_change_requests","servicenow_list_change_tasks","servicenow_list_ci_relationships","servicenow_list_group_members","servicenow_list_incidents","servicenow_list_requested_items","servicenow_order_catalog_item","servicenow_read_record","servicenow_resolve_incident","servicenow_search_cis","servicenow_search_knowledge","servicenow_update_approval","servicenow_update_change_request","servicenow_update_change_state","servicenow_update_incident","servicenow_update_record","servicenow_upload_attachment","ses_create_configuration_set","ses_create_email_identity","ses_create_template","ses_delete_email_identity","ses_delete_suppressed_destination","ses_delete_template","ses_get_account","ses_get_email_identity","ses_get_suppressed_destination","ses_get_template","ses_list_identities","ses_list_suppressed_destinations","ses_list_templates","ses_put_suppressed_destination","ses_send_bulk_email","ses_send_custom_verification_email","ses_send_email","ses_send_templated_email","ses_update_template","sftp_delete","sftp_download","sftp_list","sftp_mkdir","sftp_upload","sharepoint_add_list_items","sharepoint_create_list","sharepoint_create_page","sharepoint_delete_file","sharepoint_delete_list_item","sharepoint_delete_page","sharepoint_download_file","sharepoint_get_drive_item","sharepoint_get_list","sharepoint_get_list_item","sharepoint_list_sites","sharepoint_publish_page","sharepoint_read_page","sharepoint_update_list","sharepoint_update_page","sharepoint_upload_file","shopify_adjust_inventory","shopify_cancel_order","shopify_create_customer","shopify_create_fulfillment","shopify_create_product","shopify_delete_customer","shopify_delete_product","shopify_get_collection","shopify_get_customer","shopify_get_inventory_level","shopify_get_order","shopify_get_product","shopify_list_collections","shopify_list_customers","shopify_list_inventory_items","shopify_list_locations","shopify_list_orders","shopify_list_products","shopify_update_customer","shopify_update_order","shopify_update_product","similarweb_bounce_rate","similarweb_page_views","similarweb_pages_per_visit","similarweb_traffic_visits","similarweb_visit_duration","similarweb_website_overview","sixtyfour_enrich_company","sixtyfour_enrich_lead","sixtyfour_find_email","sixtyfour_find_phone","slack_add_reaction","slack_archive_conversation","slack_canvas","slack_create_channel_canvas","slack_create_conversation","slack_delete_canvas","slack_delete_message","slack_delete_scheduled_message","slack_download","slack_edit_canvas","slack_ephemeral_message","slack_get_canvas","slack_get_channel_history","slack_get_channel_info","slack_get_message","slack_get_permalink","slack_get_thread","slack_get_thread_replies","slack_get_user","slack_get_user_presence","slack_invite_to_conversation","slack_list_canvases","slack_list_channels","slack_list_members","slack_list_scheduled_messages","slack_list_users","slack_lookup_canvas_sections","slack_message","slack_message_reader","slack_open_view","slack_publish_view","slack_push_view","slack_remove_reaction","slack_rename_conversation","slack_schedule_message","slack_set_conversation_purpose","slack_set_conversation_topic","slack_set_status","slack_set_suggested_prompts","slack_set_title","slack_update_message","slack_update_view","smartlead_add_email_accounts_to_campaign","smartlead_add_leads_to_campaign","smartlead_create_campaign","smartlead_create_lead_list","smartlead_delete_campaign","smartlead_delete_campaign_webhook","smartlead_delete_lead_from_campaign","smartlead_delete_lead_list","smartlead_duplicate_campaign","smartlead_export_campaign_leads","smartlead_get_campaign","smartlead_get_campaign_analytics","smartlead_get_campaign_analytics_by_date","smartlead_get_campaign_lead_statistics","smartlead_get_campaign_mailbox_statistics","smartlead_get_campaign_sequences","smartlead_get_campaign_statistics","smartlead_get_campaign_top_level_analytics_by_date","smartlead_get_campaign_webhook_summary","smartlead_get_lead_by_email","smartlead_get_lead_by_id","smartlead_get_lead_list","smartlead_get_lead_message_history","smartlead_list_campaign_email_accounts","smartlead_list_campaign_leads","smartlead_list_campaign_webhooks","smartlead_list_campaigns","smartlead_list_clients","smartlead_list_email_accounts","smartlead_list_inbox_replies","smartlead_list_lead_activities","smartlead_list_lead_categories","smartlead_list_lead_lists","smartlead_mark_lead_complete","smartlead_pause_lead","smartlead_remove_email_accounts_from_campaign","smartlead_resume_lead","smartlead_save_campaign_sequences","smartlead_unsubscribe_lead_from_campaign","smartlead_unsubscribe_lead_globally","smartlead_update_campaign_schedule","smartlead_update_campaign_settings","smartlead_update_campaign_status","smartlead_update_lead","smartlead_update_lead_category","smartlead_update_lead_list","smartlead_upsert_campaign_webhook","sms_send","smtp_send_mail","snowflake_alter_warehouse","snowflake_call_procedure","snowflake_cancel_statement","snowflake_cancel_task_run","snowflake_delete_rows","snowflake_execute_sql","snowflake_get_statement","snowflake_get_task","snowflake_get_task_run","snowflake_get_task_run_output","snowflake_get_warehouse","snowflake_insert_rows","snowflake_introspect_schema","snowflake_list_copy_history","snowflake_list_databases","snowflake_list_query_history","snowflake_list_schemas","snowflake_list_tables","snowflake_list_task_runs","snowflake_list_tasks","snowflake_list_warehouses","snowflake_load_data","snowflake_resume_task","snowflake_resume_warehouse","snowflake_run_task","snowflake_suspend_task","snowflake_suspend_warehouse","snowflake_unload_data","snowflake_update_rows","snowflake_upsert_rows","splunk_cancel_search_job","splunk_create_search_job","splunk_dispatch_saved_search","splunk_get_fired_alerts","splunk_get_saved_search","splunk_get_search_job","splunk_get_search_results","splunk_list_apps","splunk_list_fired_alerts","splunk_list_indexes","splunk_list_saved_searches","splunk_run_search","sportmonks_core_get_cities","sportmonks_core_get_city","sportmonks_core_get_continent","sportmonks_core_get_continents","sportmonks_core_get_countries","sportmonks_core_get_country","sportmonks_core_get_entity_filters","sportmonks_core_get_my_usage","sportmonks_core_get_region","sportmonks_core_get_regions","sportmonks_core_get_timezones","sportmonks_core_get_type","sportmonks_core_get_type_by_entity","sportmonks_core_get_types","sportmonks_core_search_cities","sportmonks_core_search_countries","sportmonks_core_search_regions","sportmonks_football_expected_by_player","sportmonks_football_expected_by_team","sportmonks_football_get_all_commentaries","sportmonks_football_get_all_fixtures","sportmonks_football_get_all_players","sportmonks_football_get_all_rivals","sportmonks_football_get_all_teams","sportmonks_football_get_all_transfer_rumours","sportmonks_football_get_all_transfers","sportmonks_football_get_brackets_by_season","sportmonks_football_get_coach","sportmonks_football_get_coaches","sportmonks_football_get_coaches_by_country","sportmonks_football_get_commentaries_by_fixture","sportmonks_football_get_current_leagues_by_team","sportmonks_football_get_expected_lineups_by_player","sportmonks_football_get_expected_lineups_by_team","sportmonks_football_get_extended_team_squad","sportmonks_football_get_fixture","sportmonks_football_get_fixtures_by_date","sportmonks_football_get_fixtures_by_date_range","sportmonks_football_get_fixtures_by_date_range_for_team","sportmonks_football_get_fixtures_by_ids","sportmonks_football_get_grouped_standings_by_round","sportmonks_football_get_head_to_head","sportmonks_football_get_inplay_livescores","sportmonks_football_get_latest_coaches","sportmonks_football_get_latest_fixtures","sportmonks_football_get_latest_livescores","sportmonks_football_get_latest_players","sportmonks_football_get_latest_totw","sportmonks_football_get_latest_transfers","sportmonks_football_get_league","sportmonks_football_get_leagues","sportmonks_football_get_leagues_by_country","sportmonks_football_get_leagues_by_date","sportmonks_football_get_leagues_by_team","sportmonks_football_get_live_leagues","sportmonks_football_get_live_probabilities","sportmonks_football_get_live_probabilities_by_fixture","sportmonks_football_get_live_standings_by_league","sportmonks_football_get_livescores","sportmonks_football_get_match_facts","sportmonks_football_get_match_facts_by_date_range","sportmonks_football_get_match_facts_by_fixture","sportmonks_football_get_match_facts_by_league","sportmonks_football_get_past_fixtures_by_tv_station","sportmonks_football_get_player","sportmonks_football_get_players_by_country","sportmonks_football_get_postmatch_news","sportmonks_football_get_postmatch_news_by_season","sportmonks_football_get_predictability_by_league","sportmonks_football_get_prematch_news","sportmonks_football_get_prematch_news_by_season","sportmonks_football_get_prematch_news_upcoming","sportmonks_football_get_probabilities","sportmonks_football_get_probabilities_by_fixture","sportmonks_football_get_referee","sportmonks_football_get_referees","sportmonks_football_get_referees_by_country","sportmonks_football_get_referees_by_season","sportmonks_football_get_rivals_by_team","sportmonks_football_get_round","sportmonks_football_get_round_statistics","sportmonks_football_get_rounds","sportmonks_football_get_rounds_by_season","sportmonks_football_get_schedules_by_season","sportmonks_football_get_schedules_by_season_and_team","sportmonks_football_get_schedules_by_team","sportmonks_football_get_season","sportmonks_football_get_seasons","sportmonks_football_get_seasons_by_team","sportmonks_football_get_stage","sportmonks_football_get_stage_statistics","sportmonks_football_get_stages","sportmonks_football_get_stages_by_season","sportmonks_football_get_standing_corrections_by_season","sportmonks_football_get_standings","sportmonks_football_get_standings_by_round","sportmonks_football_get_standings_by_season","sportmonks_football_get_state","sportmonks_football_get_states","sportmonks_football_get_team","sportmonks_football_get_team_rankings","sportmonks_football_get_team_rankings_by_date","sportmonks_football_get_team_rankings_by_team","sportmonks_football_get_team_squad","sportmonks_football_get_team_squad_by_season","sportmonks_football_get_teams_by_country","sportmonks_football_get_teams_by_season","sportmonks_football_get_topscorers_by_season","sportmonks_football_get_topscorers_by_stage","sportmonks_football_get_totw","sportmonks_football_get_totw_by_round","sportmonks_football_get_transfer","sportmonks_football_get_transfer_rumour","sportmonks_football_get_transfer_rumours_between_dates","sportmonks_football_get_transfer_rumours_by_player","sportmonks_football_get_transfer_rumours_by_team","sportmonks_football_get_transfers_between_dates","sportmonks_football_get_transfers_by_player","sportmonks_football_get_transfers_by_team","sportmonks_football_get_tv_station","sportmonks_football_get_tv_stations","sportmonks_football_get_tv_stations_by_fixture","sportmonks_football_get_upcoming_fixtures_by_market","sportmonks_football_get_upcoming_fixtures_by_tv_station","sportmonks_football_get_value_bets","sportmonks_football_get_value_bets_by_fixture","sportmonks_football_get_venue","sportmonks_football_get_venues","sportmonks_football_get_venues_by_season","sportmonks_football_search_coaches","sportmonks_football_search_fixtures","sportmonks_football_search_leagues","sportmonks_football_search_players","sportmonks_football_search_referees","sportmonks_football_search_rounds","sportmonks_football_search_seasons","sportmonks_football_search_stages","sportmonks_football_search_teams","sportmonks_football_search_venues","sportmonks_motorsport_get_all_fixtures","sportmonks_motorsport_get_current_leagues_by_team","sportmonks_motorsport_get_driver","sportmonks_motorsport_get_driver_standings","sportmonks_motorsport_get_driver_standings_by_season","sportmonks_motorsport_get_drivers","sportmonks_motorsport_get_drivers_by_country","sportmonks_motorsport_get_drivers_by_season","sportmonks_motorsport_get_fixture","sportmonks_motorsport_get_fixtures_by_date","sportmonks_motorsport_get_fixtures_by_date_range","sportmonks_motorsport_get_fixtures_by_ids","sportmonks_motorsport_get_laps_by_fixture","sportmonks_motorsport_get_laps_by_fixture_and_driver","sportmonks_motorsport_get_laps_by_fixture_and_lap","sportmonks_motorsport_get_latest_laps_by_fixture","sportmonks_motorsport_get_latest_pitstops_by_fixture","sportmonks_motorsport_get_latest_stints_by_fixture","sportmonks_motorsport_get_latest_updated_drivers","sportmonks_motorsport_get_latest_updated_fixtures","sportmonks_motorsport_get_league","sportmonks_motorsport_get_leagues","sportmonks_motorsport_get_leagues_by_country","sportmonks_motorsport_get_leagues_by_date","sportmonks_motorsport_get_leagues_by_live","sportmonks_motorsport_get_leagues_by_team","sportmonks_motorsport_get_livescores","sportmonks_motorsport_get_pitstops_by_fixture","sportmonks_motorsport_get_pitstops_by_fixture_and_driver","sportmonks_motorsport_get_pitstops_by_fixture_and_lap","sportmonks_motorsport_get_race_results_by_season_and_driver","sportmonks_motorsport_get_race_results_by_season_and_team","sportmonks_motorsport_get_schedules_by_season","sportmonks_motorsport_get_season","sportmonks_motorsport_get_seasons","sportmonks_motorsport_get_stage","sportmonks_motorsport_get_stages","sportmonks_motorsport_get_stages_by_season","sportmonks_motorsport_get_state","sportmonks_motorsport_get_states","sportmonks_motorsport_get_stints_by_fixture","sportmonks_motorsport_get_stints_by_fixture_and_driver","sportmonks_motorsport_get_stints_by_fixture_and_stint","sportmonks_motorsport_get_team","sportmonks_motorsport_get_team_standings","sportmonks_motorsport_get_team_standings_by_season","sportmonks_motorsport_get_teams","sportmonks_motorsport_get_teams_by_country","sportmonks_motorsport_get_teams_by_season","sportmonks_motorsport_get_venue","sportmonks_motorsport_get_venues","sportmonks_motorsport_get_venues_by_season","sportmonks_motorsport_search_drivers","sportmonks_motorsport_search_leagues","sportmonks_motorsport_search_stages","sportmonks_motorsport_search_teams","sportmonks_motorsport_search_venues","sportmonks_odds_get_all_historical_odds","sportmonks_odds_get_all_inplay_odds","sportmonks_odds_get_all_pre_match_odds","sportmonks_odds_get_all_premium_odds","sportmonks_odds_get_bookmaker","sportmonks_odds_get_bookmaker_event_ids_by_fixture","sportmonks_odds_get_bookmakers","sportmonks_odds_get_bookmakers_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture","sportmonks_odds_get_inplay_odds_by_fixture_and_bookmaker","sportmonks_odds_get_inplay_odds_by_fixture_and_market","sportmonks_odds_get_last_updated_inplay_odds","sportmonks_odds_get_last_updated_pre_match_odds","sportmonks_odds_get_market","sportmonks_odds_get_markets","sportmonks_odds_get_pre_match_odds_by_fixture","sportmonks_odds_get_pre_match_odds_by_fixture_and_bookmaker","sportmonks_odds_get_pre_match_odds_by_fixture_and_market","sportmonks_odds_get_premium_odds_by_fixture","sportmonks_odds_get_premium_odds_by_fixture_and_bookmaker","sportmonks_odds_get_premium_odds_by_fixture_and_market","sportmonks_odds_get_updated_historical_odds_between","sportmonks_odds_get_updated_premium_odds_between","sportmonks_odds_search_bookmakers","sportmonks_odds_search_markets","spotify_add_playlist_cover","spotify_add_to_queue","spotify_add_tracks_to_playlist","spotify_check_following","spotify_check_playlist_followers","spotify_check_saved_albums","spotify_check_saved_audiobooks","spotify_check_saved_episodes","spotify_check_saved_shows","spotify_check_saved_tracks","spotify_create_playlist","spotify_follow_artists","spotify_follow_playlist","spotify_get_album","spotify_get_album_tracks","spotify_get_albums","spotify_get_artist","spotify_get_artist_albums","spotify_get_artist_top_tracks","spotify_get_artists","spotify_get_audiobook","spotify_get_audiobook_chapters","spotify_get_audiobooks","spotify_get_categories","spotify_get_current_user","spotify_get_currently_playing","spotify_get_devices","spotify_get_episode","spotify_get_episodes","spotify_get_followed_artists","spotify_get_markets","spotify_get_new_releases","spotify_get_playback_state","spotify_get_playlist","spotify_get_playlist_cover","spotify_get_playlist_tracks","spotify_get_queue","spotify_get_recently_played","spotify_get_saved_albums","spotify_get_saved_audiobooks","spotify_get_saved_episodes","spotify_get_saved_shows","spotify_get_saved_tracks","spotify_get_show","spotify_get_show_episodes","spotify_get_shows","spotify_get_top_artists","spotify_get_top_tracks","spotify_get_track","spotify_get_tracks","spotify_get_user_playlists","spotify_get_user_profile","spotify_pause","spotify_play","spotify_remove_saved_albums","spotify_remove_saved_audiobooks","spotify_remove_saved_episodes","spotify_remove_saved_shows","spotify_remove_saved_tracks","spotify_remove_tracks_from_playlist","spotify_reorder_playlist_items","spotify_replace_playlist_items","spotify_save_albums","spotify_save_audiobooks","spotify_save_episodes","spotify_save_shows","spotify_save_tracks","spotify_search","spotify_seek","spotify_set_repeat","spotify_set_shuffle","spotify_set_volume","spotify_skip_next","spotify_skip_previous","spotify_transfer_playback","spotify_unfollow_artists","spotify_unfollow_playlist","spotify_update_playlist","sqs_send","square_batch_retrieve_inventory_counts","square_cancel_invoice","square_cancel_payment","square_complete_payment","square_create_catalog_image","square_create_customer","square_create_invoice","square_create_order","square_create_payment","square_delete_catalog_object","square_delete_customer","square_delete_invoice","square_get_catalog_object","square_get_customer","square_get_invoice","square_get_location","square_get_order","square_get_payment","square_get_refund","square_list_catalog","square_list_customers","square_list_invoices","square_list_locations","square_list_payments","square_list_refunds","square_pay_order","square_publish_invoice","square_refund_payment","square_search_catalog_objects","square_search_customers","square_search_invoices","square_search_orders","square_update_customer","square_upsert_catalog_object","ssh_check_command_exists","ssh_check_file_exists","ssh_create_directory","ssh_delete_file","ssh_download_file","ssh_execute_command","ssh_execute_script","ssh_get_system_info","ssh_list_directory","ssh_move_rename","ssh_read_file_content","ssh_upload_file","ssh_write_file_content","stagehand_agent","stagehand_extract","stripe_cancel_payment_intent","stripe_cancel_subscription","stripe_capture_charge","stripe_capture_payment_intent","stripe_confirm_payment_intent","stripe_create_charge","stripe_create_customer","stripe_create_invoice","stripe_create_payment_intent","stripe_create_price","stripe_create_product","stripe_create_subscription","stripe_delete_customer","stripe_delete_invoice","stripe_delete_product","stripe_finalize_invoice","stripe_list_charges","stripe_list_customers","stripe_list_events","stripe_list_invoices","stripe_list_payment_intents","stripe_list_prices","stripe_list_products","stripe_list_subscriptions","stripe_pay_invoice","stripe_resume_subscription","stripe_retrieve_charge","stripe_retrieve_customer","stripe_retrieve_event","stripe_retrieve_invoice","stripe_retrieve_payment_intent","stripe_retrieve_price","stripe_retrieve_product","stripe_retrieve_subscription","stripe_search_charges","stripe_search_customers","stripe_search_invoices","stripe_search_payment_intents","stripe_search_prices","stripe_search_products","stripe_search_subscriptions","stripe_send_invoice","stripe_update_charge","stripe_update_customer","stripe_update_invoice","stripe_update_payment_intent","stripe_update_price","stripe_update_product","stripe_update_subscription","stripe_void_invoice","sts_assume_role","sts_assume_role_with_saml","sts_assume_role_with_web_identity","sts_get_access_key_info","sts_get_caller_identity","sts_get_session_token","stt_assemblyai","stt_assemblyai_v2","stt_deepgram","stt_deepgram_v2","stt_elevenlabs","stt_elevenlabs_v2","stt_gemini","stt_gemini_v2","stt_whisper","stt_whisper_v2","supabase_count","supabase_delete","supabase_get_row","supabase_insert","supabase_introspect","supabase_invoke_function","supabase_query","supabase_rpc","supabase_storage_copy","supabase_storage_create_bucket","supabase_storage_create_signed_upload_url","supabase_storage_create_signed_url","supabase_storage_delete","supabase_storage_delete_bucket","supabase_storage_download","supabase_storage_empty_bucket","supabase_storage_get_public_url","supabase_storage_list","supabase_storage_list_buckets","supabase_storage_move","supabase_storage_update_bucket","supabase_storage_upload","supabase_text_search","supabase_update","supabase_upsert","supabase_vector_search","table_batch_insert_rows","table_create","table_delete_row","table_delete_rows_by_filter","table_get_row","table_get_schema","table_insert_row","table_list","table_query_rows","table_query_rows_v2","table_update_row","table_update_rows_by_filter","table_upsert_row","tailscale_authorize_device","tailscale_create_auth_key","tailscale_delete_auth_key","tailscale_delete_device","tailscale_delete_user","tailscale_expire_device_key","tailscale_get_acl","tailscale_get_auth_key","tailscale_get_device","tailscale_get_device_routes","tailscale_get_dns_preferences","tailscale_get_dns_searchpaths","tailscale_list_auth_keys","tailscale_list_devices","tailscale_list_dns_nameservers","tailscale_list_users","tailscale_set_acl","tailscale_set_device_routes","tailscale_set_device_tags","tailscale_set_dns_nameservers","tailscale_set_dns_preferences","tailscale_set_dns_searchpaths","tailscale_suspend_user","tailscale_update_device_key","tavily_crawl","tavily_extract","tavily_map","tavily_search","telegram_copy_message","telegram_delete_message","telegram_edit_message_text","telegram_forward_message","telegram_get_chat","telegram_get_chat_member","telegram_message","telegram_pin_message","telegram_send_animation","telegram_send_audio","telegram_send_chat_action","telegram_send_contact","telegram_send_document","telegram_send_location","telegram_send_photo","telegram_send_poll","telegram_send_video","telegram_set_message_reaction","telegram_unpin_message","temporal_cancel_workflow","temporal_count_workflows","temporal_create_schedule","temporal_delete_schedule","temporal_describe_schedule","temporal_describe_task_queue","temporal_describe_workflow","temporal_get_workflow_history","temporal_list_schedules","temporal_list_workflows","temporal_pause_schedule","temporal_query_workflow","temporal_reset_workflow","temporal_signal_with_start","temporal_signal_workflow","temporal_start_workflow","temporal_terminate_workflow","temporal_trigger_schedule","temporal_unpause_schedule","temporal_update_workflow","textract_analyze_expense","textract_analyze_id","textract_parser","textract_parser_v2","thinking_tool","thrive_add_audience_managers","thrive_add_audience_members","thrive_add_user_tags","thrive_create_assignment","thrive_create_audience","thrive_create_completion","thrive_create_user","thrive_delete_assignment","thrive_delete_audience","thrive_delete_user","thrive_get_activity","thrive_get_assignment","thrive_get_audience","thrive_get_completion","thrive_get_content","thrive_get_cpd_category","thrive_get_cpd_entry","thrive_get_cpd_requirement","thrive_get_enrolment","thrive_get_skill_levels","thrive_get_tag","thrive_get_user_by_id","thrive_get_user_by_ref","thrive_list_assignments","thrive_list_audience_managers","thrive_list_audience_members","thrive_list_audiences","thrive_list_completions","thrive_list_enrolments","thrive_list_tags","thrive_query_activities","thrive_query_content","thrive_query_cpd_categories","thrive_query_cpd_entries","thrive_query_cpd_requirements","thrive_query_cpd_user_summaries","thrive_remove_audience_manager","thrive_remove_audience_member","thrive_remove_user_tags","thrive_replace_audience_managers","thrive_replace_audience_members","thrive_search_users","thrive_suspend_user","thrive_update_assignment","thrive_update_audience","thrive_update_user","thrive_update_user_skills","tiktok_get_post_status","tiktok_get_user","tiktok_list_videos","tiktok_query_videos","tiktok_upload_video_draft","tinybird_append_datasource","tinybird_delete_datasource_rows","tinybird_events","tinybird_get_job","tinybird_query","tinybird_query_pipe","tinybird_truncate_datasource","trello_add_checklist","trello_add_checklist_item","trello_add_comment","trello_add_label","trello_add_member","trello_create_board","trello_create_card","trello_create_list","trello_delete_card","trello_get_actions","trello_get_board","trello_get_card","trello_list_cards","trello_list_lists","trello_list_members","trello_remove_label","trello_remove_member","trello_search","trello_update_card","trello_update_checklist_item","trello_update_list","trigger_dev_activate_schedule","trigger_dev_add_run_tags","trigger_dev_batch_trigger_task","trigger_dev_cancel_run","trigger_dev_complete_waitpoint_token","trigger_dev_create_env_var","trigger_dev_create_schedule","trigger_dev_create_waitpoint_token","trigger_dev_deactivate_schedule","trigger_dev_delete_env_var","trigger_dev_delete_schedule","trigger_dev_execute_query","trigger_dev_get_batch","trigger_dev_get_batch_results","trigger_dev_get_deployment","trigger_dev_get_env_var","trigger_dev_get_latest_deployment","trigger_dev_get_query_schema","trigger_dev_get_queue","trigger_dev_get_run","trigger_dev_get_run_events","trigger_dev_get_run_result","trigger_dev_get_run_trace","trigger_dev_get_schedule","trigger_dev_get_waitpoint_token","trigger_dev_import_env_vars","trigger_dev_list_deployments","trigger_dev_list_env_vars","trigger_dev_list_queues","trigger_dev_list_runs","trigger_dev_list_schedules","trigger_dev_list_timezones","trigger_dev_list_waitpoint_tokens","trigger_dev_override_queue_concurrency","trigger_dev_pause_queue","trigger_dev_promote_deployment","trigger_dev_replay_run","trigger_dev_reschedule_run","trigger_dev_reset_queue_concurrency","trigger_dev_resume_queue","trigger_dev_trigger_task","trigger_dev_update_env_var","trigger_dev_update_run_metadata","trigger_dev_update_schedule","tts_azure","tts_cartesia","tts_deepgram","tts_elevenlabs","tts_google","tts_openai","tts_playht","twilio_send_sms","twilio_voice_get_recording","twilio_voice_list_calls","twilio_voice_make_call","typeform_create_form","typeform_delete_form","typeform_files","typeform_get_form","typeform_insights","typeform_list_forms","typeform_responses","typeform_update_form","upstash_redis_command","upstash_redis_delete","upstash_redis_exists","upstash_redis_expire","upstash_redis_get","upstash_redis_hget","upstash_redis_hgetall","upstash_redis_hset","upstash_redis_incr","upstash_redis_incrby","upstash_redis_keys","upstash_redis_lpush","upstash_redis_lrange","upstash_redis_set","upstash_redis_setnx","upstash_redis_ttl","uptimerobot_create_alert_contact","uptimerobot_create_maintenance_window","uptimerobot_create_monitor","uptimerobot_create_psp","uptimerobot_delete_alert_contact","uptimerobot_delete_maintenance_window","uptimerobot_delete_monitor","uptimerobot_delete_psp","uptimerobot_get_account","uptimerobot_get_alert_contact","uptimerobot_get_incident","uptimerobot_get_maintenance_window","uptimerobot_get_monitor","uptimerobot_get_psp","uptimerobot_list_alert_contacts","uptimerobot_list_incidents","uptimerobot_list_maintenance_windows","uptimerobot_list_monitors","uptimerobot_list_psps","uptimerobot_pause_monitor","uptimerobot_start_monitor","uptimerobot_update_maintenance_window","uptimerobot_update_monitor","uptimerobot_update_psp","vanta_download_document_file","vanta_get_control","vanta_get_document","vanta_get_framework","vanta_get_person","vanta_get_policy","vanta_get_risk_scenario","vanta_get_test","vanta_get_vendor","vanta_get_vulnerable_asset","vanta_list_control_documents","vanta_list_control_tests","vanta_list_controls","vanta_list_document_uploads","vanta_list_documents","vanta_list_framework_controls","vanta_list_frameworks","vanta_list_monitored_computers","vanta_list_people","vanta_list_policies","vanta_list_risk_scenarios","vanta_list_test_entities","vanta_list_tests","vanta_list_vendors","vanta_list_vulnerabilities","vanta_list_vulnerability_remediations","vanta_list_vulnerable_assets","vanta_submit_document","vanta_upload_document_file","vercel_add_domain","vercel_add_project_domain","vercel_cancel_deployment","vercel_create_alias","vercel_create_check","vercel_create_deployment","vercel_create_dns_record","vercel_create_edge_config","vercel_create_env_var","vercel_create_project","vercel_create_webhook","vercel_delete_alias","vercel_delete_deployment","vercel_delete_dns_record","vercel_delete_domain","vercel_delete_edge_config","vercel_delete_env_var","vercel_delete_project","vercel_delete_webhook","vercel_get_alias","vercel_get_check","vercel_get_deployment","vercel_get_deployment_events","vercel_get_domain","vercel_get_domain_config","vercel_get_edge_config","vercel_get_edge_config_items","vercel_get_env_vars","vercel_get_project","vercel_get_team","vercel_get_user","vercel_get_webhook","vercel_list_aliases","vercel_list_checks","vercel_list_deployment_files","vercel_list_deployments","vercel_list_dns_records","vercel_list_domains","vercel_list_edge_configs","vercel_list_project_domains","vercel_list_projects","vercel_list_team_members","vercel_list_teams","vercel_list_webhooks","vercel_pause_project","vercel_promote_deployment","vercel_remove_project_domain","vercel_rerequest_check","vercel_unpause_project","vercel_update_check","vercel_update_dns_record","vercel_update_edge_config_items","vercel_update_env_var","vercel_update_project","vercel_update_project_domain","vercel_verify_project_domain","video_falai","video_luma","video_minimax","video_runway","video_veo","vision_tool","vision_tool_v2","wealthbox_read_contact","wealthbox_read_note","wealthbox_read_task","wealthbox_write_contact","wealthbox_write_note","wealthbox_write_task","webflow_create_item","webflow_delete_item","webflow_get_item","webflow_list_items","webflow_update_item","webhook_request","whatsapp_get_media","whatsapp_mark_read","whatsapp_send_interactive","whatsapp_send_media","whatsapp_send_message","whatsapp_send_reaction","whatsapp_send_template","whatsapp_upload_media","wikipedia_content","wikipedia_random","wikipedia_search","wikipedia_summary","windchill_check_in_document","windchill_check_in_documents","windchill_check_out_document","windchill_check_out_documents","windchill_create_document","windchill_create_documents","windchill_delete_document","windchill_delete_documents","windchill_download_attachment","windchill_download_primary_content","windchill_get_document","windchill_get_document_structure","windchill_get_primary_content","windchill_get_valid_state_transitions","windchill_list_attachments","windchill_list_documents","windchill_revise_document","windchill_revise_documents","windchill_set_lifecycle_state","windchill_undo_check_out_document","windchill_undo_check_out_documents","windchill_update_common_properties","windchill_update_document","windchill_update_document_security_labels","windchill_update_documents","windchill_upload_attachments","windchill_upload_primary_content","wiza_company_enrichment","wiza_get_credits","wiza_individual_reveal","wiza_prospect_search","wordpress_create_category","wordpress_create_comment","wordpress_create_page","wordpress_create_post","wordpress_create_tag","wordpress_delete_category","wordpress_delete_comment","wordpress_delete_media","wordpress_delete_page","wordpress_delete_post","wordpress_delete_tag","wordpress_get_category","wordpress_get_current_user","wordpress_get_media","wordpress_get_page","wordpress_get_post","wordpress_get_tag","wordpress_get_user","wordpress_list_categories","wordpress_list_comments","wordpress_list_media","wordpress_list_pages","wordpress_list_posts","wordpress_list_tags","wordpress_list_users","wordpress_search_content","wordpress_update_category","wordpress_update_comment","wordpress_update_page","wordpress_update_post","wordpress_update_tag","wordpress_upload_media","workday_assign_onboarding","workday_change_job","workday_create_prehire","workday_get_compensation","workday_get_organizations","workday_get_worker","workday_hire_employee","workday_list_workers","workday_terminate_worker","workday_update_worker","workflow_executor","x_create_bookmark","x_create_tweet","x_delete_bookmark","x_delete_tweet","x_get_blocking","x_get_bookmarks","x_get_followers","x_get_following","x_get_liked_tweets","x_get_liking_users","x_get_me","x_get_personalized_trends","x_get_quote_tweets","x_get_retweeted_by","x_get_trends_by_woeid","x_get_tweets_by_ids","x_get_usage","x_get_user_mentions","x_get_user_timeline","x_get_user_tweets","x_hide_reply","x_manage_block","x_manage_follow","x_manage_like","x_manage_mute","x_manage_retweet","x_read","x_search","x_search_tweets","x_search_users","x_user","x_write","youtube_channel_info","youtube_channel_playlists","youtube_channel_videos","youtube_comments","youtube_playlist_items","youtube_search","youtube_trending","youtube_video_categories","youtube_video_details","zendesk_autocomplete_organizations","zendesk_create_organization","zendesk_create_organizations_bulk","zendesk_create_ticket","zendesk_create_tickets_bulk","zendesk_create_user","zendesk_create_users_bulk","zendesk_delete_organization","zendesk_delete_ticket","zendesk_delete_user","zendesk_get_current_user","zendesk_get_organization","zendesk_get_organizations","zendesk_get_ticket","zendesk_get_tickets","zendesk_get_user","zendesk_get_users","zendesk_merge_tickets","zendesk_search","zendesk_search_count","zendesk_search_users","zendesk_update_organization","zendesk_update_ticket","zendesk_update_tickets_bulk","zendesk_update_user","zendesk_update_users_bulk","zep_add_messages","zep_add_user","zep_create_thread","zep_delete_thread","zep_get_context","zep_get_messages","zep_get_threads","zep_get_user","zep_get_user_threads","zerobounce_get_credits","zerobounce_verify_email","zoho_desk_add_comment","zoho_desk_get_attachment","zoho_desk_get_contact","zoho_desk_get_thread","zoho_desk_get_ticket","zoho_desk_list_comments","zoho_desk_list_organizations","zoho_desk_list_threads","zoho_desk_list_tickets","zoho_desk_update_ticket","zoom_create_meeting","zoom_delete_meeting","zoom_delete_recording","zoom_get_meeting","zoom_get_meeting_invitation","zoom_get_meeting_recordings","zoom_list_meetings","zoom_list_past_participants","zoom_list_recordings","zoom_update_meeting","zoominfo_enrich_companies","zoominfo_enrich_contacts","zoominfo_search_companies","zoominfo_search_contacts","zoominfo_search_intent","zoominfo_search_news"]' ) export default toolIds diff --git a/apps/sim/tools/generated/tool-metadata.ts b/apps/sim/tools/generated/tool-metadata.ts index 323c3e9130e..f2e960199f7 100644 --- a/apps/sim/tools/generated/tool-metadata.ts +++ b/apps/sim/tools/generated/tool-metadata.ts @@ -3,7 +3,7 @@ /** Serializable metadata for every built-in tool, keyed by tool id. */ const toolMetadata: Record = JSON.parse( - '{"a2a_cancel_task":{"id":"a2a_cancel_task","name":"A2A Cancel Task","description":"Request cancellation of an in-progress A2A task.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The task ID to cancel"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}}},"a2a_get_agent_card":{"id":"a2a_get_agent_card","name":"A2A Get Agent Card","description":"Fetch the Agent Card (discovery document) for an external A2A agent.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}}},"a2a_get_task":{"id":"a2a_get_task","name":"A2A Get Task","description":"Retrieve the current state and result of an A2A task.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The task ID to retrieve"},"historyLength":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of history messages to include"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}}},"a2a_send_message":{"id":"a2a_send_message","name":"A2A Send Message","description":"Send a message to an external A2A agent and return its response.","version":"1.0.0","params":{"agentUrl":{"type":"string","required":true,"visibility":"user-only","description":"The A2A agent endpoint URL"},"message":{"type":"string","required":true,"visibility":"user-or-llm","description":"The message text to send"},"data":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional structured JSON data to attach"},"files":{"type":"json","required":false,"visibility":"user-or-llm","description":"Optional files to attach"},"taskId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Existing task ID to continue"},"contextId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Conversation context ID to continue"},"apiKey":{"type":"string","required":false,"visibility":"user-only","description":"API key for authentication (if required)"}}},"agentmail_create_draft":{"id":"agentmail_create_draft","name":"Create Draft","description":"Create a new email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to create the draft in"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Draft subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text draft body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML draft body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"},"inReplyTo":{"type":"string","required":false,"visibility":"user-or-llm","description":"ID of message being replied to"},"sendAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to schedule sending"}}},"agentmail_create_inbox":{"id":"agentmail_create_inbox","name":"Create Inbox","description":"Create a new email inbox with AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"username":{"type":"string","required":false,"visibility":"user-or-llm","description":"Username for the inbox email address"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Domain for the inbox email address"},"displayName":{"type":"string","required":false,"visibility":"user-or-llm","description":"Display name for the inbox"}}},"agentmail_delete_draft":{"id":"agentmail_delete_draft","name":"Delete Draft","description":"Delete an email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to delete"}}},"agentmail_delete_inbox":{"id":"agentmail_delete_inbox","name":"Delete Inbox","description":"Delete an email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to delete"}}},"agentmail_delete_thread":{"id":"agentmail_delete_thread","name":"Delete Thread","description":"Delete an email thread in AgentMail (moves to trash, or permanently deletes if already in trash)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to delete"},"permanent":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Force permanent deletion instead of moving to trash"}}},"agentmail_forward_message":{"id":"agentmail_forward_message","name":"Forward Message","description":"Forward an email message to new recipients in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to forward"},"to":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Override subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Additional plain text to prepend"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"Additional HTML to prepend"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"}}},"agentmail_get_draft":{"id":"agentmail_get_draft","name":"Get Draft","description":"Get details of a specific email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox the draft belongs to"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to retrieve"}}},"agentmail_get_inbox":{"id":"agentmail_get_inbox","name":"Get Inbox","description":"Get details of a specific email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to retrieve"}}},"agentmail_get_message":{"id":"agentmail_get_message","name":"Get Message","description":"Get details of a specific email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to retrieve"}}},"agentmail_get_thread":{"id":"agentmail_get_thread","name":"Get Thread","description":"Get details of a specific email thread including messages in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to retrieve"}}},"agentmail_list_drafts":{"id":"agentmail_list_drafts","name":"List Drafts","description":"List email drafts in an inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list drafts from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of drafts to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}}},"agentmail_list_inboxes":{"id":"agentmail_list_inboxes","name":"List Inboxes","description":"List all email inboxes in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of inboxes to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}}},"agentmail_list_messages":{"id":"agentmail_list_messages","name":"List Messages","description":"List messages in an inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list messages from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of messages to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"}}},"agentmail_list_threads":{"id":"agentmail_list_threads","name":"List Threads","description":"List email threads in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to list threads from"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of threads to return"},"pageToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token for next page of results"},"labels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to filter threads by"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter threads before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter threads after this ISO 8601 timestamp"}}},"agentmail_reply_message":{"id":"agentmail_reply_message","name":"Reply to Message","description":"Reply to an existing email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to reply from"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to reply to"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text reply body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML reply body"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Override recipient email addresses (comma-separated)"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC email addresses (comma-separated)"},"replyAll":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Reply to all recipients of the original message"}}},"agentmail_send_draft":{"id":"agentmail_send_draft","name":"Send Draft","description":"Send an existing email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to send"}}},"agentmail_send_message":{"id":"agentmail_send_message","name":"Send Message","description":"Send an email message from an AgentMail inbox","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to send from"},"to":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient email address (comma-separated for multiple)"},"subject":{"type":"string","required":true,"visibility":"user-or-llm","description":"Email subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text email body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML email body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"}}},"agentmail_update_draft":{"id":"agentmail_update_draft","name":"Update Draft","description":"Update an existing email draft in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the draft"},"draftId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the draft to update"},"to":{"type":"string","required":false,"visibility":"user-or-llm","description":"Recipient email addresses (comma-separated)"},"subject":{"type":"string","required":false,"visibility":"user-or-llm","description":"Draft subject line"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Plain text draft body"},"html":{"type":"string","required":false,"visibility":"user-or-llm","description":"HTML draft body"},"cc":{"type":"string","required":false,"visibility":"user-or-llm","description":"CC recipient email addresses (comma-separated)"},"bcc":{"type":"string","required":false,"visibility":"user-or-llm","description":"BCC recipient email addresses (comma-separated)"},"sendAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to schedule sending"}}},"agentmail_update_inbox":{"id":"agentmail_update_inbox","name":"Update Inbox","description":"Update the display name of an email inbox in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox to update"},"displayName":{"type":"string","required":true,"visibility":"user-or-llm","description":"New display name for the inbox"}}},"agentmail_update_message":{"id":"agentmail_update_message","name":"Update Message","description":"Add or remove labels on an email message in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the message"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to update"},"addLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to add to the message"},"removeLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to remove from the message"}}},"agentmail_update_thread":{"id":"agentmail_update_thread","name":"Update Thread Labels","description":"Add or remove labels on an email thread in AgentMail","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentMail API key"},"inboxId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the inbox containing the thread"},"threadId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the thread to update"},"addLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to add to the thread"},"removeLabels":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated labels to remove from the thread"}}},"agentphone_create_call":{"id":"agentphone_create_call","name":"Create Outbound Call","description":"Initiate an outbound voice call from an AgentPhone agent","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"agentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Agent that will handle the call"},"toNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Phone number to call in E.164 format (e.g. +14155551234)"},"fromNumberId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Phone number ID to use as caller ID. Must belong to the agent. If omitted, the agent\'s first assigned number is used."},"initialGreeting":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optional greeting spoken when the recipient answers"},"voice":{"type":"string","required":false,"visibility":"user-or-llm","description":"Voice ID override for this call (defaults to the agent\'s configured voice)"},"systemPrompt":{"type":"string","required":false,"visibility":"user-or-llm","description":"When provided, uses a built-in LLM for the conversation instead of forwarding to your webhook"}}},"agentphone_create_contact":{"id":"agentphone_create_contact","name":"Create Contact","description":"Create a new contact in AgentPhone","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"phoneNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Phone number in E.164 format (e.g. +14155551234)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact\'s full name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Contact\'s email address"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Freeform notes stored on the contact"}}},"agentphone_create_number":{"id":"agentphone_create_number","name":"Create Phone Number","description":"Provision a new SMS- and voice-enabled phone number","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Two-letter country code (e.g. US, CA). Defaults to US."},"areaCode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Preferred area code (US/CA only, e.g. \\"415\\"). Best-effort — may be ignored if unavailable."},"agentId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optionally attach the number to an agent immediately"}}},"agentphone_delete_contact":{"id":"agentphone_delete_contact","name":"Delete Contact","description":"Delete a contact by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"}}},"agentphone_get_call":{"id":"agentphone_get_call","name":"Get Call","description":"Fetch a call and its full transcript","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"callId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the call to retrieve"}}},"agentphone_get_call_transcript":{"id":"agentphone_get_call_transcript","name":"Get Call Transcript","description":"Get the full ordered transcript for a call","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"callId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the call to retrieve the transcript for"}}},"agentphone_get_contact":{"id":"agentphone_get_contact","name":"Get Contact","description":"Fetch a single contact by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"}}},"agentphone_get_conversation":{"id":"agentphone_get_conversation","name":"Get Conversation","description":"Get a conversation along with its recent messages","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"messageLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of recent messages to include (default 50, max 100)"}}},"agentphone_get_conversation_messages":{"id":"agentphone_get_conversation_messages","name":"Get Conversation Messages","description":"Get paginated messages for a conversation","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of messages to return (default 50, max 200)"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received after this ISO 8601 timestamp"}}},"agentphone_get_number_messages":{"id":"agentphone_get_number_messages","name":"Get Phone Number Messages","description":"Fetch messages received on a specific phone number","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"numberId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the phone number"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of messages to return (default 50, max 200)"},"before":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received before this ISO 8601 timestamp"},"after":{"type":"string","required":false,"visibility":"user-or-llm","description":"Return messages received after this ISO 8601 timestamp"}}},"agentphone_get_usage":{"id":"agentphone_get_usage","name":"Get Usage","description":"Retrieve current usage statistics for the AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"}}},"agentphone_get_usage_daily":{"id":"agentphone_get_usage_daily","name":"Get Daily Usage","description":"Get a daily breakdown of usage (messages, calls, webhooks) for the last N days","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"days":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of days to return (1-365, default 30)"}}},"agentphone_get_usage_monthly":{"id":"agentphone_get_usage_monthly","name":"Get Monthly Usage","description":"Get monthly usage aggregation (messages, calls, webhooks) for the last N months","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"months":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of months to return (1-24, default 6)"}}},"agentphone_list_calls":{"id":"agentphone_list_calls","name":"List Calls","description":"List voice calls for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"},"status":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by status (completed, in-progress, failed)"},"direction":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by direction (inbound, outbound)"},"type":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by call type (pstn, web)"},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search by phone number (matches fromNumber or toNumber)"}}},"agentphone_list_contacts":{"id":"agentphone_list_contacts","name":"List Contacts","description":"List contacts for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter by name or phone number (case-insensitive contains)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 50, max 200)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}}},"agentphone_list_conversations":{"id":"agentphone_list_conversations","name":"List Conversations","description":"List conversations (message threads) for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}}},"agentphone_list_numbers":{"id":"agentphone_list_numbers","name":"List Phone Numbers","description":"List all phone numbers provisioned for this AgentPhone account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to return (default 20, max 100)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip (min 0)"}}},"agentphone_react_to_message":{"id":"agentphone_react_to_message","name":"React to Message","description":"Send an iMessage tapback reaction to a message (iMessage only)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"messageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the message to react to"},"reaction":{"type":"string","required":true,"visibility":"user-or-llm","description":"Reaction type: love, like, dislike, laugh, emphasize, or question"}}},"agentphone_release_number":{"id":"agentphone_release_number","name":"Release Phone Number","description":"Release (delete) a phone number. This action is irreversible.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"numberId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the phone number to release"}}},"agentphone_send_message":{"id":"agentphone_send_message","name":"Send Message","description":"Send an outbound SMS or iMessage from an AgentPhone agent","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"agentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Agent sending the message"},"toNumber":{"type":"string","required":true,"visibility":"user-or-llm","description":"Recipient phone number in E.164 format (e.g. +14155551234)"},"body":{"type":"string","required":true,"visibility":"user-or-llm","description":"Message text to send"},"mediaUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"Optional URL of an image, video, or file to attach"},"numberId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Phone number ID to send from. If omitted, the agent\'s first assigned number is used."}}},"agentphone_update_contact":{"id":"agentphone_update_contact","name":"Update Contact","description":"Update a contact\'s fields","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"contactId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Contact ID"},"phoneNumber":{"type":"string","required":false,"visibility":"user-or-llm","description":"New phone number in E.164 format"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New contact name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"New email address"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"New freeform notes"}}},"agentphone_update_conversation":{"id":"agentphone_update_conversation","name":"Update Conversation","description":"Update conversation metadata (stored state). Pass null to clear existing metadata.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"AgentPhone API key"},"conversationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Conversation ID"},"metadata":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom key-value metadata to store on the conversation. Pass null to clear existing metadata."}}},"agiloft_async_status":{"id":"agiloft_async_status","name":"Agiloft Async Status","description":"Check whether an asynchronous Agiloft call, such as a run action button, has completed.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table the asynchronous call was made against"},"callbackId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Callback ID returned by the asynchronous call, e.g. from Run Action Button"}}},"agiloft_attach_file":{"id":"agiloft_attach_file","name":"Agiloft Attach File","description":"Attach a file to a field in an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to attach the file to"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"file":{"type":"file","required":true,"visibility":"user-or-llm","description":"File to attach"},"fileName":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name to assign to the file (defaults to original file name)"},"overwrite":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Replace the contents of the field instead of adding another file to it"}}},"agiloft_attachment_info":{"id":"agiloft_attachment_info","name":"Agiloft Attachment Info","description":"Get information about file attachments on a record field.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to check attachments on"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field to inspect"}}},"agiloft_create_record":{"id":"agiloft_create_record","name":"Agiloft Create Record","description":"Create a new record in an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record field values as a JSON object (e.g., {\\"first_name\\": \\"John\\", \\"status\\": \\"Active\\"})"}}},"agiloft_delete_record":{"id":"agiloft_delete_record","name":"Agiloft Delete Record","description":"Delete a record from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to delete"},"substituteIds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated IDs of records that adopt the dependants of the deleted record. Read only when the delete rule is REPLACE_WITH_ANOTHER."},"deleteRule":{"type":"string","required":false,"visibility":"user-or-llm","description":"How to treat records that depend on this one: ERROR_IF_DEPENDANTS (default — fails rather than cascading), APPLY_DELETE_WHERE_POSSIBLE, DELETE_WHERE_POSSIBLE_OTHERWISE_UNLINK, APPLY_UNLINK, UNLINK_WHERE_POSSIBLE_OTHERWISE_DELETE, or REPLACE_WITH_ANOTHER"}}},"agiloft_get_choice_line_id":{"id":"agiloft_get_choice_line_id","name":"Agiloft Get Choice Line ID","description":"Resolve the internal numeric ID of a choice-list value, for use in EWSelect WHERE clauses against choice fields.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"case\\", \\"contracts\\")"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Choice field name (e.g., \\"priority\\", \\"status\\")"},"value":{"type":"string","required":true,"visibility":"user-or-llm","description":"Choice display value to resolve (e.g., \\"High\\", \\"Active\\")"}}},"agiloft_list_tables":{"id":"agiloft_list_tables","name":"Agiloft List Tables","description":"List the tables and fields in an Agiloft knowledge base, to discover the logical names other operations need.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":false,"visibility":"user-or-llm","description":"Logical name of a single table to describe (e.g., \\"contacts\\"). Leave empty to list every table in the knowledge base."},"includeLinkedInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Include the source table and column behind each linked field"},"skipColumnsInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Return table names only, omitting field details, for a much smaller response"}}},"agiloft_lock_record":{"id":"agiloft_lock_record","name":"Agiloft Lock Record","description":"Lock, unlock, or check the lock status of an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to lock, unlock, or check"},"lockAction":{"type":"string","required":true,"visibility":"user-or-llm","description":"Action to perform: \\"lock\\", \\"unlock\\", or \\"check\\""},"force":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Unlock only: release a lock held by another user."}}},"agiloft_nlp_search":{"id":"agiloft_nlp_search","name":"Agiloft Natural Language Search","description":"Search Agiloft records by describing what you want in plain language, such as \\"active NDAs submitted last month\\".","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"nlpQuery":{"type":"string","required":true,"visibility":"user-or-llm","description":"The request in plain language, e.g. \\"Show me open, high-priority contracts\\". Structured field filters are not accepted — use Search Records for those."},"fields":{"type":"string","required":true,"visibility":"user-or-llm","description":"Comma-separated field names to return, e.g. \\"id, contract_title1, company_name\\""},"page":{"type":"string","required":false,"visibility":"user-or-llm","description":"Page number, starting from 0"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Records per page"}}},"agiloft_read_record":{"id":"agiloft_read_record","name":"Agiloft Read Record","description":"Read a record by ID from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to read"},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of field names to include in the response"}}},"agiloft_remove_attachment":{"id":"agiloft_remove_attachment","name":"Agiloft Remove Attachment","description":"Remove an attached file from a field in an Agiloft record.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record containing the attachment"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"position":{"type":"string","required":true,"visibility":"user-or-llm","description":"Position index of the file to remove (starting from 0)"}}},"agiloft_retrieve_attachment":{"id":"agiloft_retrieve_attachment","name":"Agiloft Retrieve Attachment","description":"Download an attached file from an Agiloft record field.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record containing the attachment"},"fieldName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the attachment field"},"position":{"type":"string","required":true,"visibility":"user-or-llm","description":"Position index of the file in the field (starting from 0)"}}},"agiloft_run_action_button":{"id":"agiloft_run_action_button","name":"Agiloft Run Action Button","description":"Run an action button on an Agiloft record, such as an approval or send-for-signature step.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"case\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to run the action button on"},"actionButtonField":{"type":"string","required":true,"visibility":"user-or-llm","description":"Logical name of the field holding the action button (e.g., \\"ab_field\\")"}}},"agiloft_saved_search":{"id":"agiloft_saved_search","name":"Agiloft Saved Search","description":"List the saved searches defined for an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Logical table name to list saved searches for (e.g., \\"contract\\")"}}},"agiloft_search_records":{"id":"agiloft_search_records","name":"Agiloft Search Records","description":"Search for records in an Agiloft table using a query.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name to search in (e.g., \\"contracts\\", \\"contacts.employees\\")"},"query":{"type":"string","required":false,"visibility":"user-or-llm","description":"Ad hoc EWSearch query. Combine conditions with && (and) or || (or) and quote every value — e.g. \\"summary~=\'test\'&&priority=\'High\'\\". Required unless a saved search is given."},"search":{"type":"string","required":false,"visibility":"user-or-llm","description":"Label of a saved search defined on the table (e.g., \\"C: Status is Closed\\"). Can be combined with a query to narrow it further."},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of field names to include in the results"},"page":{"type":"string","required":false,"visibility":"user-or-llm","description":"Page number for paginated results (starting from 0)"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of records to return per page. Agiloft treats 0 as \\"all records\\", so leave it unset or use a positive value to keep result sizes bounded."}}},"agiloft_select_records":{"id":"agiloft_select_records","name":"Agiloft Select Records","description":"Select record IDs matching a SQL WHERE clause from an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"where":{"type":"string","required":true,"visibility":"user-or-llm","description":"SQL WHERE clause using database column names (e.g., \\"summary like \'%new%\'\\" or \\"assigned_person=\'John Doe\'\\"). EWSelect has no page size and returns every matching ID, so append a database limit such as \\"limit 0,200\\" to bound the result."}}},"agiloft_update_record":{"id":"agiloft_update_record","name":"Agiloft Update Record","description":"Update an existing record in an Agiloft table.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the record to update"},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Updated field values as a JSON object (e.g., {\\"status\\": \\"Active\\", \\"priority\\": \\"High\\"})"}}},"agiloft_upsert_record":{"id":"agiloft_upsert_record","name":"Agiloft Upsert Record","description":"Create an Agiloft record, or update it when a record already matches the given fields.","version":"1.0.0","params":{"instanceUrl":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft instance URL (e.g., https://mycompany.agiloft.com)"},"knowledgeBase":{"type":"string","required":true,"visibility":"user-only","description":"Knowledge base name"},"login":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft username"},"password":{"type":"string","required":true,"visibility":"user-only","description":"Agiloft password"},"table":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table name (e.g., \\"contracts\\", \\"contacts.employees\\")"},"match":{"type":"string","required":true,"visibility":"user-or-llm","description":"Field used to find an existing record (e.g., \\"ext_id\\"). Pick something that identifies a record uniquely — if more than one record matches, Agiloft writes nothing and returns a conflict."},"async":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Queue the write instead of waiting for it. Returns a callback ID instead of a record ID; pass that to Async Status to poll the result."},"data":{"type":"string","required":true,"visibility":"user-or-llm","description":"Field values as a JSON object. On create these populate the new record; on update only the supplied fields change."}}},"ahrefs_anchors":{"id":"ahrefs_anchors","name":"Ahrefs Anchors","description":"Get the anchor text distribution for a target domain or URL\'s backlinks, showing how many links and referring domains use each anchor text.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live), \\"all_time\\" (default, includes lost backlinks), or \\"since:YYYY-MM-DD\\" (backlinks found since a date)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_backlinks":{"id":"ahrefs_backlinks","name":"Ahrefs Backlinks","description":"Get a list of backlinks pointing to a target domain or URL. Returns details about each backlink including source URL, anchor text, and domain rating.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live backlinks), \\"all_time\\" (default, includes lost backlinks), or \\"since:YYYY-MM-DD\\" (backlinks found since a date)."},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_backlinks_stats":{"id":"ahrefs_backlinks_stats","name":"Ahrefs Backlinks Stats","description":"Get backlink and referring domain totals for a target domain or URL, both currently live and across all time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_batch_analysis":{"id":"ahrefs_batch_analysis","name":"Ahrefs Batch Analysis","description":"Get bulk SEO metrics (Domain Rating, backlinks, referring domains, organic traffic, and more) for multiple domains or URLs in a single request. Useful for comparing many competitors at once.","version":"1.0.0","params":{"targets":{"type":"string","required":true,"visibility":"user-or-llm","description":"Comma-separated list of domains or URLs to analyze. Example: \\"example.com,competitor.com\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode applied to every target: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"protocol":{"type":"string","required":false,"visibility":"user-or-llm","description":"Protocol applied to every target: \\"both\\" (default), \\"http\\", or \\"https\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_broken_backlinks":{"id":"ahrefs_broken_backlinks","name":"Ahrefs Broken Backlinks","description":"Get a list of broken backlinks pointing to a target domain or URL. Useful for identifying link reclamation opportunities.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_domain_rating":{"id":"ahrefs_domain_rating","name":"Ahrefs Domain Rating","description":"Get the Domain Rating (DR) and Ahrefs Rank for a target domain. Domain Rating shows the strength of a website\'s backlink profile on a scale from 0 to 100.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain to analyze (e.g., example.com)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date for historical data in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_domain_rating_history":{"id":"ahrefs_domain_rating_history","name":"Ahrefs Domain Rating History","description":"Get the historical Domain Rating (DR) trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_keyword_overview":{"id":"ahrefs_keyword_overview","name":"Ahrefs Keyword Overview","description":"Get detailed metrics for a keyword including search volume, keyword difficulty, CPC, clicks, and traffic potential.","version":"1.0.0","params":{"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The keyword to analyze"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for keyword data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_keywords_history":{"id":"ahrefs_keywords_history","name":"Ahrefs Keywords History","description":"Get the historical organic keyword ranking distribution for a target domain or URL over a date range: how many keywords rank in each position bucket at each point in time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_metrics":{"id":"ahrefs_metrics","name":"Ahrefs Metrics","description":"Get a one-call organic and paid search overview for a target domain or URL: organic traffic, organic keywords, paid traffic, paid keywords, and estimated traffic cost.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_metrics_history":{"id":"ahrefs_metrics_history","name":"Ahrefs Metrics History","description":"Get the historical organic and paid traffic trend for a target domain or URL over a date range: organic traffic/cost and paid traffic/cost at each point in time.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_organic_competitors":{"id":"ahrefs_organic_competitors","name":"Ahrefs Organic Competitors","description":"Get domains that compete with a target domain or URL for the same organic keywords, ranked by keyword overlap.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_organic_keywords":{"id":"ahrefs_organic_keywords","name":"Ahrefs Organic Keywords","description":"Get organic keywords that a target domain or URL ranks for in Google search results. Returns keyword details including search volume, ranking position, and estimated traffic.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for search results. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_paid_pages":{"id":"ahrefs_paid_pages","name":"Ahrefs Paid Pages","description":"Get a target domain\'s pages that receive paid search traffic, sorted by estimated paid traffic. Returns page URLs with their paid traffic, keyword counts, and estimated spend.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_rank_tracker_competitors_overview":{"id":"ahrefs_rank_tracker_competitors_overview","name":"Ahrefs Rank Tracker Competitors Overview","description":"Get competitor rankings for the keywords tracked in an Ahrefs Rank Tracker project: each tracked keyword\'s volume and difficulty alongside every competitor\'s position, traffic, and traffic value. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report rankings for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"dateCompared":{"type":"string","required":false,"visibility":"user-only","description":"Comparison date in YYYY-MM-DD format, to compute position/traffic deltas"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_rank_tracker_competitors_stats":{"id":"ahrefs_rank_tracker_competitors_stats","name":"Ahrefs Rank Tracker Competitors Stats","description":"Get aggregate competitor stats for an Ahrefs Rank Tracker project: each competitor\'s traffic, traffic value, average position, and share of voice across all tracked keywords. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report metrics for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_rank_tracker_overview":{"id":"ahrefs_rank_tracker_overview","name":"Ahrefs Rank Tracker Overview","description":"Get ranking overview metrics for the keywords tracked in an Ahrefs Rank Tracker project: position, search volume, keyword difficulty, and estimated traffic. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":true,"visibility":"user-only","description":"Date to report rankings for, in YYYY-MM-DD format"},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"dateCompared":{"type":"string","required":false,"visibility":"user-only","description":"Comparison date in YYYY-MM-DD format, to compute position/traffic deltas"},"volumeMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search volume calculation: \\"monthly\\" or \\"average\\" (default: \\"monthly\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_rank_tracker_serp_overview":{"id":"ahrefs_rank_tracker_serp_overview","name":"Ahrefs Rank Tracker SERP Overview","description":"Get the full SERP (search engine results page) for a keyword tracked in an Ahrefs Rank Tracker project, including every ranking URL with its position, title, and authority metrics. This endpoint is free and does not consume API units.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Rank Tracker project ID (found in the project URL in Ahrefs)"},"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The tracked keyword to retrieve SERP data for"},"country":{"type":"string","required":true,"visibility":"user-or-llm","description":"Country code for the tracked keyword. Example: \\"us\\", \\"gb\\", \\"de\\""},"device":{"type":"string","required":true,"visibility":"user-or-llm","description":"Rankings device type: \\"desktop\\" or \\"mobile\\""},"topPositions":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of top organic positions to return (defaults to all available)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Timestamp to return the last available SERP Overview at, in YYYY-MM-DDThh:mm:ss format"},"locationId":{"type":"number","required":false,"visibility":"user-or-llm","description":"Location ID of the tracked keyword, if tracked at a specific location"},"languageCode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Language code of the tracked keyword"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_refdomains_history":{"id":"ahrefs_refdomains_history","name":"Ahrefs Referring Domains History","description":"Get the historical referring domains trend for a target domain or URL over a date range, grouped daily, weekly, or monthly.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\""},"dateFrom":{"type":"string","required":true,"visibility":"user-only","description":"Start date of the historical period, in YYYY-MM-DD format"},"dateTo":{"type":"string","required":false,"visibility":"user-only","description":"End date of the historical period, in YYYY-MM-DD format (defaults to today)"},"historyGrouping":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval for grouping data points: \\"daily\\", \\"weekly\\", or \\"monthly\\" (default: \\"monthly\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_referring_domains":{"id":"ahrefs_referring_domains","name":"Ahrefs Referring Domains","description":"Get a list of domains that link to a target domain or URL. Returns unique referring domains with their domain rating, backlink counts, and discovery dates.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain or URL to analyze. Example: \\"example.com\\" or \\"https://example.com/page\\""},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"history":{"type":"string","required":false,"visibility":"user-or-llm","description":"Historical scope: \\"live\\" (currently live), \\"all_time\\" (default, includes lost domains), or \\"since:YYYY-MM-DD\\" (domains found since a date)."},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_related_terms":{"id":"ahrefs_related_terms","name":"Ahrefs Related Terms","description":"Get keyword ideas related to a seed keyword: terms the same top-ranking pages also rank for (\\"also rank for\\") or also discuss (\\"also talk about\\"), with volume, difficulty, and CPC.","version":"1.0.0","params":{"keyword":{"type":"string","required":true,"visibility":"user-or-llm","description":"The seed keyword to find related terms for"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for keyword data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"terms":{"type":"string","required":false,"visibility":"user-or-llm","description":"Type of related keywords to return: \\"also_rank_for\\", \\"also_talk_about\\", or \\"all\\" (default: \\"all\\")"},"viewFor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Whether to derive related terms from the top 10 or top 100 ranking pages (default: \\"top_10\\")"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_site_audit_page_explorer":{"id":"ahrefs_site_audit_page_explorer","name":"Ahrefs Site Audit Page Explorer","description":"Get crawled pages from an Ahrefs Site Audit project with health and SEO metrics: HTTP status, title, link counts, backlinks, indexability, and traffic. Optionally filter to pages affected by a specific issue.","version":"1.0.0","params":{"projectId":{"type":"number","required":true,"visibility":"user-or-llm","description":"The Site Audit project ID (found in the project URL in Ahrefs)"},"date":{"type":"string","required":false,"visibility":"user-only","description":"Crawl date in YYYY-MM-DDThh:mm:ss format (defaults to the most recent crawl)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of results to skip, for pagination"},"issueId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Only return pages affected by this issue ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"ahrefs_top_pages":{"id":"ahrefs_top_pages","name":"Ahrefs Top Pages","description":"Get the top pages of a target domain sorted by organic traffic. Returns page URLs with their traffic, keyword counts, and estimated traffic value.","version":"1.0.0","params":{"target":{"type":"string","required":true,"visibility":"user-or-llm","description":"The target domain to analyze. Example: \\"example.com\\""},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Country code for traffic data. Example: \\"us\\", \\"gb\\", \\"de\\" (default: \\"us\\")"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Analysis mode: domain (entire domain), prefix (URL prefix), subdomains (include all subdomains, default), exact (exact URL match). Example: \\"domain\\""},"date":{"type":"string","required":false,"visibility":"user-only","description":"Date to report metrics on, in YYYY-MM-DD format (defaults to today)"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of results to return. Example: 50 (default: 1000)"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ahrefs API Key"}}},"airtable_create_records":{"id":"airtable_create_records","name":"Airtable Create Records","description":"Write new records to an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to create, each with a `fields` object"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_delete_records":{"id":"airtable_delete_records","name":"Airtable Delete Records","description":"Delete one or more records from an Airtable table by ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordIds":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of record IDs to delete (each starts with \\"rec\\", e.g., [\\"recXXXXXXXXXXXXXX\\"]). Pass a single-element array to delete one record."}},"oauth":{"required":true,"provider":"airtable"}},"airtable_get_base_schema":{"id":"airtable_get_base_schema","name":"Airtable Get Base Schema","description":"Get the schema of all tables, fields, and views in an Airtable base","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_get_record":{"id":"airtable_get_record","name":"Airtable Get Record","description":"Retrieve a single record from an Airtable table by its ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record ID to retrieve (starts with \\"rec\\", e.g., \\"recXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_list_bases":{"id":"airtable_list_bases","name":"Airtable List Bases","description":"List all bases the authenticated user has access to","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"offset":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination offset for retrieving additional bases"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_list_records":{"id":"airtable_list_records","name":"Airtable List Records","description":"Read records from an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"maxRecords":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of records to return (default: all records)"},"filterFormula":{"type":"string","required":false,"visibility":"user-or-llm","description":"Formula to filter records (e.g., \\"({Field Name} = \'Value\')\\")"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_list_tables":{"id":"airtable_list_tables","name":"Airtable List Tables","description":"List all tables and their schema in an Airtable base","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_update_multiple_records":{"id":"airtable_update_multiple_records","name":"Airtable Update Multiple Records","description":"Update multiple existing records in an Airtable table","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to update, each with an `id` and a `fields` object"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_update_record":{"id":"airtable_update_record","name":"Airtable Update Record","description":"Update an existing record in an Airtable table by ID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"recordId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Record ID to update (starts with \\"rec\\", e.g., \\"recXXXXXXXXXXXXXX\\")"},"fields":{"type":"json","required":true,"visibility":"user-or-llm","description":"An object containing the field names and their new values"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"}},"airtable_upsert_records":{"id":"airtable_upsert_records","name":"Airtable Upsert Records","description":"Update existing records or create new ones in an Airtable table, matching on the specified merge fields","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token"},"baseId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Airtable base ID (starts with \\"app\\", e.g., \\"appXXXXXXXXXXXXXX\\")"},"tableId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Table ID (starts with \\"tbl\\") or table name"},"records":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of records to upsert, each with a `fields` object"},"fieldsToMergeOn":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of field names used to match existing records (max 3). A record is updated when all merge fields match, otherwise it is created. Example: [\\"Name\\"]"},"typecast":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, Airtable automatically converts string values to the field type"}},"oauth":{"required":true,"provider":"airtable"}},"airweave_search":{"id":"airweave_search","name":"Airweave Search","description":"Search your synced data collections using Airweave. Supports semantic search with hybrid, neural, or keyword retrieval strategies. Optionally generate AI-powered answers from search results.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Airweave API Key for authentication"},"collectionId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The readable ID of the collection to search"},"query":{"type":"string","required":true,"visibility":"user-or-llm","description":"The search query text"},"limit":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 100)"},"retrievalStrategy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Retrieval strategy: hybrid (default), neural, or keyword"},"expandQuery":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Generate query variations to improve recall"},"rerank":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Reorder results for improved relevance using LLM"},"generateAnswer":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Generate a natural-language answer to the query"}}},"algolia_add_record":{"id":"algolia_add_record","name":"Algolia Add Record","description":"Add or replace a record in an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":false,"visibility":"user-or-llm","description":"Object ID for the record (auto-generated if not provided)"},"record":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object representing the record to add"}}},"algolia_batch_operations":{"id":"algolia_batch_operations","name":"Algolia Batch Operations","description":"Perform batch add, update, partial update, or delete operations on records in an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"requests":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of batch operations. Each item has \\"action\\" (addObject, updateObject, partialUpdateObject, partialUpdateObjectNoCreate, deleteObject, delete, clear) and \\"body\\" (the record data; must include objectID for update/delete; use an empty object {} for the index-level delete/clear actions)"}}},"algolia_browse_records":{"id":"algolia_browse_records","name":"Algolia Browse Records","description":"Browse and iterate over all records in an Algolia index using cursor pagination","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key (must have browse ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to browse"},"query":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search query to filter browsed records"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter string to narrow down results"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of hits per page (default: 1000, max: 1000)"},"cursor":{"type":"string","required":false,"visibility":"user-or-llm","description":"Cursor from a previous browse response for pagination"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search"}}},"algolia_clear_records":{"id":"algolia_clear_records","name":"Algolia Clear Records","description":"Clear all records from an Algolia index while keeping settings, synonyms, and rules","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to clear"}}},"algolia_copy_move_index":{"id":"algolia_copy_move_index","name":"Algolia Copy/Move Index","description":"Copy or move an Algolia index to a new destination","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the source index"},"operation":{"type":"string","required":true,"visibility":"user-or-llm","description":"Operation to perform: \\"copy\\" or \\"move\\""},"destination":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the destination index"},"scope":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of scopes to copy (only for \\"copy\\" operation): [\\"settings\\", \\"synonyms\\", \\"rules\\"]. Omit to copy everything including records."}}},"algolia_delete_by_filter":{"id":"algolia_delete_by_filter","name":"Algolia Delete By Filter","description":"Delete all records matching a filter from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter expression to match records for deletion (e.g., \\"category:outdated\\")"},"facetFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of facet filters (e.g., [\\"brand:Acme\\"])"},"numericFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of numeric filters (e.g., [\\"price > 100\\"])"},"tagFilters":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of tag filters using the _tags attribute (e.g., [\\"published\\"])"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search filter (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search filter"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search filter"}}},"algolia_delete_index":{"id":"algolia_delete_index","name":"Algolia Delete Index","description":"Delete an entire Algolia index and all its records","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have deleteIndex ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to delete"}}},"algolia_delete_record":{"id":"algolia_delete_record","name":"Algolia Delete Record","description":"Delete a record by objectID from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to delete"}}},"algolia_get_record":{"id":"algolia_get_record","name":"Algolia Get Record","description":"Get a record by objectID from an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to retrieve"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"}}},"algolia_get_records":{"id":"algolia_get_records","name":"Algolia Get Records","description":"Retrieve multiple records by objectID from one or more Algolia indices","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Default index name for all requests"},"requests":{"type":"json","required":true,"visibility":"user-or-llm","description":"Array of objects specifying records to retrieve. Each must have \\"objectID\\" and optionally \\"indexName\\" and \\"attributesToRetrieve\\"."}}},"algolia_get_settings":{"id":"algolia_get_settings","name":"Algolia Get Settings","description":"Retrieve the settings of an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"}}},"algolia_get_task_status":{"id":"algolia_get_task_status","name":"Algolia Get Task Status","description":"Check whether an Algolia indexing task has finished publishing","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index the task ran against"},"taskID":{"type":"number","required":true,"visibility":"user-or-llm","description":"The taskID returned by a previous write operation"}}},"algolia_list_indices":{"id":"algolia_list_indices","name":"Algolia List Indices","description":"List all indices in an Algolia application","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for paginating indices (default: not paginated)"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of indices per page (default: 100)"}}},"algolia_partial_update_record":{"id":"algolia_partial_update_record","name":"Algolia Partial Update Record","description":"Partially update a record in an Algolia index without replacing it entirely","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"objectID":{"type":"string","required":true,"visibility":"user-or-llm","description":"The objectID of the record to update"},"attributes":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object with attributes to update. Supports built-in operations like {\\"stock\\": {\\"_operation\\": \\"Decrement\\", \\"value\\": 1}}"},"createIfNotExists":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to create the record if it does not exist (default: true)"}}},"algolia_search":{"id":"algolia_search","name":"Algolia Search","description":"Search an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia API Key"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index to search"},"query":{"type":"string","required":true,"visibility":"user-or-llm","description":"Search query text"},"hitsPerPage":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of hits per page (default: 20)"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number to retrieve (default: 0)"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter string (e.g., \\"category:electronics AND price < 100\\")"},"attributesToRetrieve":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of attributes to retrieve"},"facets":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of facet attribute names to retrieve counts for (use \\"*\\" for all)"},"getRankingInfo":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to include detailed ranking information in each hit"},"aroundLatLng":{"type":"string","required":false,"visibility":"user-or-llm","description":"Coordinates for geo-search (e.g., \\"40.71,-74.01\\")"},"aroundRadius":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum radius in meters for geo-search, or \\"all\\" for unlimited"},"insideBoundingBox":{"type":"json","required":false,"visibility":"user-or-llm","description":"Bounding box coordinates as [[lat1, lng1, lat2, lng2]] for geo-search"},"insidePolygon":{"type":"json","required":false,"visibility":"user-or-llm","description":"Polygon coordinates as [[lat1, lng1, lat2, lng2, lat3, lng3, ...]] for geo-search"}}},"algolia_update_settings":{"id":"algolia_update_settings","name":"Algolia Update Settings","description":"Update the settings of an Algolia index","version":"1.0","params":{"applicationId":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Application ID"},"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Algolia Admin API Key (must have editSettings ACL)"},"indexName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the Algolia index"},"settings":{"type":"json","required":true,"visibility":"user-or-llm","description":"JSON object with settings to update (e.g., {\\"searchableAttributes\\": [\\"name\\", \\"description\\"], \\"customRanking\\": [\\"desc(popularity)\\"]})"},"forwardToReplicas":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to apply changes to replica indices (default: false)"}}},"amplitude_event_segmentation":{"id":"amplitude_event_segmentation","name":"Amplitude Event Segmentation","description":"Query event analytics data with segmentation. Get event counts, uniques, averages, and more.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"eventType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Event type name to analyze"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric type: uniques, totals, pct_dau, average, histogram, sums, value_avg, or formula (default: uniques)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by (prefix custom user properties with \\"gp:\\")"},"groupBy2":{"type":"string","required":false,"visibility":"user-or-llm","description":"Second property name to group by (prefix custom user properties with \\"gp:\\")"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of group-by values (max 1000)"},"filters":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON array of filter objects applied to the event, e.g. [{\\"subprop_type\\":\\"event\\",\\"subprop_key\\":\\"city\\",\\"subprop_op\\":\\"is\\",\\"subprop_value\\":[\\"San Francisco\\"]}]"},"formula":{"type":"string","required":false,"visibility":"user-or-llm","description":"Required when metric is \\"formula\\", e.g. \\"UNIQUES(A)/UNIQUES(B)\\""},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_funnels":{"id":"amplitude_funnels","name":"Amplitude Funnels","description":"Analyze conversion rates and drop-off between a sequence of events.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"events":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON array of event objects, one per funnel step in order, e.g. [{\\"event_type\\":\\"signup\\"},{\\"event_type\\":\\"purchase\\"}]"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"mode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Funnel ordering: \\"ordered\\", \\"unordered\\", or \\"sequential\\" (default: ordered)"},"userType":{"type":"string","required":false,"visibility":"user-or-llm","description":"User type: \\"new\\" or \\"active\\" (default: active)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: -300000 (real-time), -3600000 (hourly), 1 (daily), 7 (weekly), or 30 (monthly)"},"conversionWindowSeconds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Conversion window in seconds (default: 2592000, i.e. 30 days)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property to group by (limit: one; prefix custom properties with \\"gp:\\")"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of group-by values (default: 100, max: 1000)"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_get_active_users":{"id":"amplitude_get_active_users","name":"Amplitude Get Active Users","description":"Get active or new user counts over a date range from the Dashboard REST API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric type: \\"active\\" or \\"new\\" (default: active)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_get_revenue":{"id":"amplitude_get_revenue","name":"Amplitude Get Revenue","description":"Get revenue LTV data including ARPU, ARPPU, total revenue, and paying user counts.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"metric":{"type":"string","required":false,"visibility":"user-or-llm","description":"Metric: 0 (ARPU), 1 (ARPPU), 2 (Total Revenue), 3 (Paying Users)"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property name to group by (limit: one)"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_group_identify":{"id":"amplitude_group_identify","name":"Amplitude Group Identify","description":"Set group-level properties in Amplitude. Supports $set, $setOnce, $add, $append, $unset operations.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"groupType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Group classification (e.g., \\"company\\", \\"org_id\\")"},"groupValue":{"type":"string","required":true,"visibility":"user-or-llm","description":"Specific group identifier (e.g., \\"Acme Corp\\")"},"groupProperties":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON object of group properties. Use operations like $set, $setOnce, $add, $append, $unset."},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_identify_user":{"id":"amplitude_identify_user","name":"Amplitude Identify User","description":"Set user properties in Amplitude using the Identify API. Supports $set, $setOnce, $add, $append, $unset operations.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"User ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"userProperties":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON object of user properties. Use operations like $set, $setOnce, $add, $append, $unset."},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_list_events":{"id":"amplitude_list_events","name":"Amplitude List Events","description":"List all event types in the Amplitude project with their weekly totals and unique counts.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_realtime_active_users":{"id":"amplitude_realtime_active_users","name":"Amplitude Real-time Active Users","description":"Get real-time active user counts at 5-minute granularity for the last 2 days.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_retention":{"id":"amplitude_retention","name":"Amplitude Retention","description":"Measure how many users return to perform an action after a starting action.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"startEvent":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON starting event object, e.g. {\\"event_type\\":\\"_new\\"} or {\\"event_type\\":\\"_active\\"}"},"returnEvent":{"type":"string","required":true,"visibility":"user-or-llm","description":"JSON returning event object, e.g. {\\"event_type\\":\\"_all\\"} or {\\"event_type\\":\\"_active\\"}"},"start":{"type":"string","required":true,"visibility":"user-or-llm","description":"Start date in YYYYMMDD format"},"end":{"type":"string","required":true,"visibility":"user-or-llm","description":"End date in YYYYMMDD format"},"retentionMode":{"type":"string","required":false,"visibility":"user-or-llm","description":"Retention type: \\"bracket\\", \\"rolling\\", or \\"n-day\\" (default: n-day)"},"retentionBrackets":{"type":"string","required":false,"visibility":"user-or-llm","description":"Required when Retention Mode is \\"bracket\\". Day ranges, e.g. [[0,4]]"},"interval":{"type":"string","required":false,"visibility":"user-or-llm","description":"Time interval: 1 (daily), 7 (weekly), or 30 (monthly)"},"groupBy":{"type":"string","required":false,"visibility":"user-or-llm","description":"Property to group by (limit: one; prefix custom properties with \\"gp:\\")"},"segment":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON segment definition(s) applied to the query"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_send_event":{"id":"amplitude_send_event","name":"Amplitude Send Event","description":"Track an event in Amplitude using the HTTP V2 API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"User ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"eventType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the event (e.g., \\"page_view\\", \\"purchase\\")"},"eventProperties":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON object of custom event properties"},"userProperties":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON object of user properties to set (supports $set, $setOnce, $add, $append, $unset)"},"time":{"type":"string","required":false,"visibility":"user-or-llm","description":"Event timestamp in milliseconds since epoch"},"sessionId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Session start time in milliseconds since epoch"},"insertId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Unique ID for deduplication (within 7-day window)"},"appVersion":{"type":"string","required":false,"visibility":"user-or-llm","description":"Application version string"},"platform":{"type":"string","required":false,"visibility":"user-or-llm","description":"Platform (e.g., \\"Web\\", \\"iOS\\", \\"Android\\")"},"country":{"type":"string","required":false,"visibility":"user-or-llm","description":"Two-letter country code"},"language":{"type":"string","required":false,"visibility":"user-or-llm","description":"Language code (e.g., \\"en\\")"},"ip":{"type":"string","required":false,"visibility":"user-or-llm","description":"IP address for geo-location"},"price":{"type":"string","required":false,"visibility":"user-or-llm","description":"Price of the item purchased"},"quantity":{"type":"string","required":false,"visibility":"user-or-llm","description":"Quantity of items purchased"},"revenue":{"type":"string","required":false,"visibility":"user-or-llm","description":"Revenue amount"},"productId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Product identifier"},"revenueType":{"type":"string","required":false,"visibility":"user-or-llm","description":"Revenue type (e.g., \\"purchase\\", \\"refund\\")"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_user_activity":{"id":"amplitude_user_activity","name":"Amplitude User Activity","description":"Get the event stream for a specific user by their Amplitude ID.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"amplitudeId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Amplitude internal user ID"},"offset":{"type":"string","required":false,"visibility":"user-or-llm","description":"Offset for pagination (default 0)"},"limit":{"type":"string","required":false,"visibility":"user-or-llm","description":"Maximum number of events to return (default 1000, max 1000)"},"direction":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort direction: \\"latest\\" or \\"earliest\\" (default: latest)"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"amplitude_user_profile":{"id":"amplitude_user_profile","name":"Amplitude User Profile","description":"Get a user profile including properties, cohort memberships, and computed properties. Not available for EU data-residency projects.","version":"1.0.0","params":{"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"userId":{"type":"string","required":false,"visibility":"user-or-llm","description":"External user ID (required if no device_id)"},"deviceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Device ID (required if no user_id)"},"getAmpProps":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include Amplitude user properties (true/false, default: false)"},"getCohortIds":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include cohort IDs the user belongs to (true/false, default: false)"},"getComputations":{"type":"string","required":false,"visibility":"user-or-llm","description":"Include computed user properties (true/false, default: false)"}}},"amplitude_user_search":{"id":"amplitude_user_search","name":"Amplitude User Search","description":"Search for a user by User ID, Device ID, or Amplitude ID using the Dashboard REST API.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude API Key"},"secretKey":{"type":"string","required":true,"visibility":"user-only","description":"Amplitude Secret Key"},"user":{"type":"string","required":true,"visibility":"user-or-llm","description":"User ID, Device ID, or Amplitude ID to search for"},"dataResidency":{"type":"string","required":false,"visibility":"user-or-llm","description":"Data residency region: \\"us\\" (default) or \\"eu\\""}}},"apify_get_dataset_items":{"id":"apify_get_dataset_items","name":"APIFY Get Dataset Items","description":"Retrieve items stored in an APIFY dataset","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"datasetId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Dataset ID to read items from. Example: \\"9RnD3Pql2vGZkc5H5\\""},"itemLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Max items to return (1-250000). Default: all items. Example: 500"},"offset":{"type":"number","required":false,"visibility":"user-or-llm","description":"Number of items to skip at the start. Default: 0"},"fields":{"type":"string","required":false,"visibility":"user-or-llm","description":"Comma-separated list of fields to include. Example: \\"title,url,price\\""}}},"apify_get_run":{"id":"apify_get_run","name":"APIFY Get Run","description":"Get the status and details of an APIFY actor run","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"runId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor run ID to fetch. Example: \\"HG7ML7M8z78YcAPEB\\""}}},"apify_run_actor_async":{"id":"apify_run_actor_async","name":"APIFY Run Actor (Async)","description":"Run an APIFY actor asynchronously with polling for long-running tasks","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"actorId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor ID or username/actor-name. Examples: \\"apify/web-scraper\\", \\"janedoe/my-actor\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor input as JSON string. Example: {\\"startUrls\\": [{\\"url\\": \\"https://example.com\\"}], \\"maxPages\\": 10}"},"waitForFinish":{"type":"number","required":false,"visibility":"user-or-llm","description":"Initial wait time in seconds (0-60) before polling starts. Example: 30"},"itemLimit":{"type":"number","required":false,"default":100,"visibility":"user-or-llm","description":"Max dataset items to fetch (1-250000). Default: 100. Example: 500"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the actor run (128-32768). Example: 1024 for 1GB, 2048 for 2GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\", \\"build-tag-name\\""}}},"apify_run_actor_sync":{"id":"apify_run_actor_sync","name":"APIFY Run Actor (Sync)","description":"Run an APIFY actor synchronously and get results (max 5 minutes)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"actorId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Actor ID or username/actor-name. Examples: \\"apify/web-scraper\\", \\"janedoe/my-actor\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor input as JSON string. Example: {\\"startUrls\\": [{\\"url\\": \\"https://example.com\\"}], \\"maxPages\\": 10}"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the actor run (128-32768). Example: 1024 for 1GB, 2048 for 2GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the actor run. Example: 300 for 5 minutes, 3600 for 1 hour"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\", \\"build-tag-name\\""}}},"apify_run_task":{"id":"apify_run_task","name":"APIFY Run Task","description":"Run a saved APIFY actor task synchronously and get dataset items (max 5 minutes)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"APIFY API token from console.apify.com/account#/integrations"},"taskId":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task ID or username/task-name. Examples: \\"janedoe/my-task\\", \\"moJRLRc85AitArpNN\\""},"input":{"type":"string","required":false,"visibility":"user-or-llm","description":"JSON string that overrides the task\'s saved input. Example: {\\"startUrls\\": [{\\"url\\": \\"https://example.com\\"}]}"},"itemLimit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Max dataset items to return (1-250000). Example: 500"},"memory":{"type":"number","required":false,"visibility":"user-or-llm","description":"Memory in megabytes allocated for the run (128-32768). Example: 1024 for 1GB"},"timeout":{"type":"number","required":false,"visibility":"user-or-llm","description":"Timeout in seconds for the run. Example: 300 for 5 minutes"},"build":{"type":"string","required":false,"visibility":"user-or-llm","description":"Actor build to run. Examples: \\"latest\\", \\"beta\\", \\"1.2.3\\""}}},"apollo_account_bulk_create":{"id":"apollo_account_bulk_create","name":"Apollo Bulk Create Accounts","description":"Create up to 100 accounts at once in your Apollo database. Set run_dedupe=true to deduplicate by domain, organization_id, and name. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"accounts":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of accounts to create (max 100). Each account should include a name, and may optionally include domain, phone, phone_status_cd, raw_address, owner_id, linkedin_url, facebook_url, twitter_url, salesforce_id, and hubspot_id."},"append_label_names":{"type":"array","required":false,"visibility":"user-only","description":"Array of label names to add to ALL accounts in this request"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"When true, performs aggressive deduplication by domain, organization_id, and name (defaults to false)"}}},"apollo_account_bulk_update":{"id":"apollo_account_bulk_update","name":"Apollo Bulk Update Accounts","description":"Update up to 1000 existing accounts at once in your Apollo database (higher limit than contacts!). Each account must include an id field. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"account_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of account IDs to update with the same values (max 1000). Use with name/owner_id for uniform updates. Use either this OR account_attributes."},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this name to all accounts"},"owner_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this owner to all accounts"},"account_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"When using account_ids, apply this account stage to all accounts"},"account_attributes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of account objects with individual updates (each must include id). Example: [{\\"id\\": \\"acc1\\", \\"name\\": \\"Acme\\", \\"owner_id\\": \\"u1\\", \\"account_stage_id\\": \\"s1\\", \\"typed_custom_fields\\": {\\"field_id\\": \\"value\\"}}]"},"async":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, processes the update asynchronously. Only supported when using account_ids; returns 422 if used with account_attributes."}}},"apollo_account_create":{"id":"apollo_account_create","name":"Apollo Create Account","description":"Create a new account (company) in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Company name (e.g., \\"Acme Corporation\\")"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain without www. prefix (e.g., \\"acme.com\\")"},"phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number for the account"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo user ID of the account owner"},"account_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo ID for the account stage to assign this account to"},"raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate location (e.g., \\"San Francisco, CA, USA\\")"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}}},"apollo_account_search":{"id":"apollo_account_search","name":"Apollo Search Accounts","description":"Search your team\'s accounts in Apollo. Display limit: 50,000 records (100 records per page, 500 pages max). Use filters to narrow results. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"q_organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter accounts by organization name (partial-match search)"},"account_stage_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by account stage IDs"},"account_label_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by account label IDs"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"account_last_activity_date\\", \\"account_created_at\\", or \\"account_updated_at\\""},"sort_ascending":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Sort ascending when true. Defaults to descending."},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_account_update":{"id":"apollo_account_update","name":"Apollo Update Account","description":"Update an existing account in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"account_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the account to update (e.g., \\"acc_abc123\\")"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company name (e.g., \\"Acme Corporation\\")"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain (e.g., \\"acme.com\\")"},"phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company phone number"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo user ID of the account owner"},"account_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"Apollo ID for the account stage to assign this account to"},"raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate location (e.g., \\"San Francisco, CA, USA\\")"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}}},"apollo_contact_bulk_create":{"id":"apollo_contact_bulk_create","name":"Apollo Bulk Create Contacts","description":"Create up to 100 contacts at once in your Apollo database. Supports deduplication to prevent creating duplicate contacts. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"contacts":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of contacts to create (max 100). Each contact may include first_name, last_name, email, title, organization_name, account_id, owner_id, contact_stage_id, linkedin_url, phone (single string) or phone_numbers (array of {raw_number, position}), contact_emails, typed_custom_fields, and CRM IDs (salesforce_contact_id, hubspot_id, team_id) for cross-system matching"},"append_label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Label names to add to all contacts in this request (e.g., [\\"Hot Lead\\"])"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-only","description":"Enable deduplication to prevent creating duplicate contacts. When true, existing contacts are returned without modification"}}},"apollo_contact_bulk_update":{"id":"apollo_contact_bulk_update","name":"Apollo Bulk Update Contacts","description":"Update up to 100 existing contacts at once in your Apollo database. Each contact must include an id field. Master key required.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"contact_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of contact IDs to update. Must be paired with an object-form contact_attributes specifying the fields to apply uniformly to all listed contacts."},"contact_attributes":{"type":"json","required":false,"visibility":"user-or-llm","description":"Required. Either an array of per-contact updates (each with id) — used standalone — or a single object of attributes to apply to all contact_ids. Supported fields: owner_id, email, organization_name, title, first_name, last_name, account_id, present_raw_address, linkedin_url, typed_custom_fields"},"async":{"type":"boolean","required":false,"visibility":"user-only","description":"Force asynchronous processing. Automatically enabled for >100 contacts"}}},"apollo_contact_create":{"id":"apollo_contact_create","name":"Apollo Create Contact","description":"Create a new contact in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"first_name":{"type":"string","required":true,"visibility":"user-or-llm","description":"First name of the contact"},"last_name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Last name of the contact"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address of the contact"},"title":{"type":"string","required":false,"visibility":"user-or-llm","description":"Job title (e.g., \\"VP of Sales\\", \\"Software Engineer\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo account ID to associate with (e.g., \\"acc_abc123\\")"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the contact owner (accepted by Apollo but not officially documented for POST /contacts)"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the contact\'s employer (e.g., \\"Apollo\\")"},"website_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate website URL (e.g., \\"https://www.apollo.io/\\")"},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Lists/labels to add the contact to (e.g., [\\"Prospects\\"])"},"contact_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the contact stage"},"present_raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal location for the contact (e.g., \\"Atlanta, United States\\")"},"direct_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number"},"corporate_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Work/office phone number"},"mobile_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Mobile phone number"},"home_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Home phone number"},"other_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Alternative phone number"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom field values keyed by custom field ID"},"run_dedupe":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, Apollo deduplicates against existing contacts"}}},"apollo_contact_search":{"id":"apollo_contact_search","name":"Apollo Search Contacts","description":"Search your team\'s contacts in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"q_keywords":{"type":"string","required":false,"visibility":"user-or-llm","description":"Keywords to search for"},"contact_stage_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by contact stage IDs"},"contact_label_ids":{"type":"array","required":false,"visibility":"user-only","description":"Filter by Apollo label IDs (lists)"},"sort_by_field":{"type":"string","required":false,"visibility":"user-only","description":"Sort field: contact_last_activity_date, contact_email_last_opened_at, contact_email_last_clicked_at, contact_created_at, or contact_updated_at"},"sort_ascending":{"type":"boolean","required":false,"visibility":"user-only","description":"When true, sort ascending. Must be used together with sort_by_field"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_contact_update":{"id":"apollo_contact_update","name":"Apollo Update Contact","description":"Update an existing contact in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"contact_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the contact to update (e.g., \\"con_abc123\\")"},"first_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"First name of the contact"},"last_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Last name of the contact"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address"},"title":{"type":"string","required":false,"visibility":"user-or-llm","description":"Job title (e.g., \\"VP of Sales\\", \\"Software Engineer\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo account ID (e.g., \\"acc_abc123\\")"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the contact owner (accepted by Apollo but not officially documented for PATCH /contacts/{id})"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the contact\'s employer (e.g., \\"Apollo\\")"},"website_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"Corporate website URL (e.g., \\"https://www.apollo.io/\\")"},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Lists/labels to add the contact to (e.g., [\\"Prospects\\"])"},"contact_stage_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the contact stage"},"present_raw_address":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal location for the contact (e.g., \\"Atlanta, United States\\")"},"direct_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number"},"corporate_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Work/office phone number"},"mobile_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Mobile phone number"},"home_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Home phone number"},"other_phone":{"type":"string","required":false,"visibility":"user-or-llm","description":"Alternative phone number"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-or-llm","description":"Custom field values keyed by custom field ID"}}},"apollo_email_accounts":{"id":"apollo_email_accounts","name":"Apollo Get Email Accounts","description":"Get list of team\'s linked email accounts in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"}}},"apollo_opportunity_create":{"id":"apollo_opportunity_create","name":"Apollo Create Opportunity","description":"Create a new deal for an account in your Apollo database (master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the opportunity/deal (e.g., \\"Enterprise License - Q1\\")"},"account_id":{"type":"string","required":false,"visibility":"user-or-llm","description":"ID of the account this opportunity belongs to (e.g., \\"acc_abc123\\")"},"amount":{"type":"string","required":false,"visibility":"user-or-llm","description":"Monetary value as a plain number string with no commas or currency symbols"},"opportunity_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the opportunity stage"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the opportunity owner"},"closed_date":{"type":"string","required":false,"visibility":"user-or-llm","description":"Expected close date in YYYY-MM-DD format"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}}},"apollo_opportunity_get":{"id":"apollo_opportunity_get","name":"Apollo Get Opportunity","description":"Retrieve complete details of a specific deal/opportunity by ID","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"opportunity_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the opportunity to retrieve (e.g., \\"opp_abc123\\")"}}},"apollo_opportunity_search":{"id":"apollo_opportunity_search","name":"Apollo Search Opportunities","description":"Search and list all deals/opportunities in your team\'s Apollo account","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"amount\\", \\"is_closed\\", or \\"is_won\\""},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_opportunity_update":{"id":"apollo_opportunity_update","name":"Apollo Update Opportunity","description":"Update an existing deal/opportunity in your Apollo database","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"opportunity_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the opportunity to update (e.g., \\"opp_abc123\\")"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Name of the opportunity/deal (e.g., \\"Enterprise License - Q1\\")"},"amount":{"type":"string","required":false,"visibility":"user-or-llm","description":"Monetary value as a plain number string with no commas or currency symbols"},"opportunity_stage_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the opportunity stage"},"owner_id":{"type":"string","required":false,"visibility":"user-only","description":"User ID of the opportunity owner"},"closed_date":{"type":"string","required":false,"visibility":"user-or-llm","description":"Expected close date in YYYY-MM-DD format"},"typed_custom_fields":{"type":"json","required":false,"visibility":"user-only","description":"Custom field values as { custom_field_id: value } map"}}},"apollo_organization_bulk_enrich":{"id":"apollo_organization_bulk_enrich","name":"Apollo Bulk Organization Enrichment","description":"Enrich data for up to 10 organizations at once using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"domains":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of company domains to enrich (max 10, no www. or @, e.g., [\\"apollo.io\\", \\"stripe.com\\"])"}}},"apollo_organization_enrich":{"id":"apollo_organization_enrich","name":"Apollo Organization Enrichment","description":"Enrich data for a single organization using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"domain":{"type":"string","required":true,"visibility":"user-or-llm","description":"Company domain (e.g., \\"apollo.io\\", \\"acme.com\\")"}}},"apollo_organization_search":{"id":"apollo_organization_search","name":"Apollo Organization Search","description":"Search Apollo\'s database for companies using filters","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"organization_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Company HQ locations (cities, US states, or countries)"},"organization_not_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Exclude companies whose HQ is in these locations"},"organization_num_employees_ranges":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employee count ranges as \\"min,max\\" strings (e.g., [\\"1,10\\", \\"250,500\\", \\"10000,20000\\"])"},"q_organization_keyword_tags":{"type":"array","required":false,"visibility":"user-or-llm","description":"Industry or keyword tags"},"q_organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Organization name to search for (e.g., \\"Acme\\", \\"TechCorp\\")"},"organization_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Apollo organization IDs to include (e.g., [\\"5e66b6381e05b4008c8331b8\\"])"},"q_organization_domains_list":{"type":"array","required":false,"visibility":"user-or-llm","description":"Domain names to filter by (no www. or @, up to 1,000)"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_people_bulk_enrich":{"id":"apollo_people_bulk_enrich","name":"Apollo Bulk People Enrichment","description":"Enrich data for up to 10 people at once using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"people":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of people to enrich (max 10)"},"reveal_personal_emails":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal personal email addresses (uses credits)"},"reveal_phone_number":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal phone numbers (uses credits, requires webhook_url)"},"webhook_url":{"type":"string","required":false,"visibility":"user-only","description":"Webhook URL for async phone number delivery (required when reveal_phone_number is true)"}}},"apollo_people_enrich":{"id":"apollo_people_enrich","name":"Apollo People Enrichment","description":"Enrich data for a single person using Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"first_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"First name of the person"},"last_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Last name of the person"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Full name of the person (alternative to first_name/last_name)"},"id":{"type":"string","required":false,"visibility":"user-or-llm","description":"Apollo ID for the person"},"hashed_email":{"type":"string","required":false,"visibility":"user-or-llm","description":"MD5 or SHA-256 hashed email"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Email address of the person"},"organization_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company name where the person works"},"domain":{"type":"string","required":false,"visibility":"user-or-llm","description":"Company domain (e.g., \\"apollo.io\\", \\"acme.com\\")"},"linkedin_url":{"type":"string","required":false,"visibility":"user-or-llm","description":"LinkedIn profile URL"},"reveal_personal_emails":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal personal email addresses (uses credits)"},"reveal_phone_number":{"type":"boolean","required":false,"visibility":"user-only","description":"Reveal phone numbers (uses credits, requires webhook_url)"},"webhook_url":{"type":"string","required":false,"visibility":"user-only","description":"Webhook URL for async phone number delivery (required when reveal_phone_number is true)"}}},"apollo_people_search":{"id":"apollo_people_search","name":"Apollo People Search","description":"Search Apollo\'s database for people using demographic filters","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key"},"person_titles":{"type":"array","required":false,"visibility":"user-or-llm","description":"Job titles to search for (e.g., [\\"CEO\\", \\"VP of Sales\\"])"},"include_similar_titles":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Whether to return people with job titles similar to person_titles"},"person_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Locations to search in (e.g., [\\"San Francisco, CA\\", \\"New York, NY\\"])"},"person_seniorities":{"type":"array","required":false,"visibility":"user-or-llm","description":"Seniority levels (one of: owner, founder, c_suite, partner, vp, head, director, manager, senior, entry, intern)"},"organization_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Apollo organization IDs to filter by (e.g., [\\"5e66b6381e05b4008c8331b8\\"])"},"organization_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Company names to search within (legacy filter)"},"organization_locations":{"type":"array","required":false,"visibility":"user-or-llm","description":"Headquarters locations of the people\'s current employer (e.g., [\'texas\', \'tokyo\', \'spain\'])"},"q_organization_domains_list":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employer domain names (e.g., [\\"apollo.io\\", \\"microsoft.com\\"]) — up to 1,000, no www. or @"},"organization_num_employees_ranges":{"type":"array","required":false,"visibility":"user-or-llm","description":"Employee count ranges for the person\'s current employer. Each entry is \\"min,max\\" (e.g., [\\"1,10\\", \\"250,500\\", \\"10000,20000\\"])"},"contact_email_status":{"type":"array","required":false,"visibility":"user-or-llm","description":"Email statuses to filter by: \\"verified\\", \\"unverified\\", \\"likely to engage\\", \\"unavailable\\""},"q_keywords":{"type":"string","required":false,"visibility":"user-or-llm","description":"Keywords to search for"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination, default 1 (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, default 25, max 100 (e.g., 25, 50, 100)"}}},"apollo_sequence_add_contacts":{"id":"apollo_sequence_add_contacts","name":"Apollo Add Contacts to Sequence","description":"Add contacts to an Apollo sequence","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"sequence_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the sequence to add contacts to (e.g., \\"seq_abc123\\")"},"contact_ids":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of contact IDs to add to the sequence (e.g., [\\"con_abc123\\", \\"con_def456\\"]). Either contact_ids or label_names must be provided."},"label_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of label names to identify contacts to add to the sequence. Either contact_ids or label_names must be provided."},"send_email_from_email_account_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the email account to send from. Use the Get Email Accounts operation to look this up."},"send_email_from_email_address":{"type":"string","required":false,"visibility":"user-only","description":"Specific email address to send from within the email account."},"sequence_no_email":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if they have no email address"},"sequence_unverified_email":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts with unverified email addresses"},"sequence_job_change":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts who recently changed jobs"},"sequence_active_in_other_campaigns":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts active in other campaigns"},"sequence_finished_in_other_campaigns":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts who finished other campaigns"},"sequence_same_company_in_same_campaign":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if others from the same company are in the sequence"},"contacts_without_ownership_permission":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts without ownership permission"},"add_if_in_queue":{"type":"boolean","required":false,"visibility":"user-only","description":"Add contacts even if they are in the queue"},"contact_verification_skipped":{"type":"boolean","required":false,"visibility":"user-only","description":"Skip contact verification when adding"},"user_id":{"type":"string","required":false,"visibility":"user-only","description":"ID of the user performing the action"},"status":{"type":"string","required":false,"visibility":"user-only","description":"Initial status for added contacts: \\"active\\" or \\"paused\\""},"auto_unpause_at":{"type":"string","required":false,"visibility":"user-only","description":"ISO 8601 datetime to automatically unpause contacts"}}},"apollo_sequence_search":{"id":"apollo_sequence_search","name":"Apollo Search Sequences","description":"Search for sequences/campaigns in your team\'s Apollo account (master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"q_name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Search sequences by name (e.g., \\"Outbound Q1\\", \\"Follow-up\\")"},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"apollo_task_create":{"id":"apollo_task_create","name":"Apollo Create Task","description":"Create one or more tasks in Apollo (one task per contact_id, master key required)","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"user_id":{"type":"string","required":true,"visibility":"user-or-llm","description":"ID of the Apollo user the task is assigned to"},"contact_ids":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of contact IDs. One task is created per contact."},"priority":{"type":"string","required":false,"visibility":"user-or-llm","description":"Task priority: \\"high\\", \\"medium\\", or \\"low\\" (defaults to \\"medium\\")"},"due_at":{"type":"string","required":true,"visibility":"user-or-llm","description":"Due date/time in ISO 8601 format (e.g., \\"2024-12-31T23:59:59Z\\")"},"type":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task type: \\"call\\", \\"outreach_manual_email\\", \\"linkedin_step_connect\\", \\"linkedin_step_message\\", \\"linkedin_step_view_profile\\", \\"linkedin_step_interact_post\\", or \\"action_item\\""},"status":{"type":"string","required":true,"visibility":"user-or-llm","description":"Task status: \\"scheduled\\", \\"completed\\", or \\"skipped\\""},"note":{"type":"string","required":false,"visibility":"user-or-llm","description":"Free-form note providing context for the task"}}},"apollo_task_search":{"id":"apollo_task_search","name":"Apollo Search Tasks","description":"Search for tasks in Apollo","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Apollo API key (master key required)"},"sort_by_field":{"type":"string","required":false,"visibility":"user-or-llm","description":"Sort field: \\"task_due_at\\" or \\"task_priority\\""},"open_factor_names":{"type":"array","required":false,"visibility":"user-or-llm","description":"Filter by status. Common values: [\\"task_types\\"] for open tasks, [\\"task_completed_at\\"] for completed tasks."},"page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Page number for pagination (e.g., 1, 2, 3)"},"per_page":{"type":"number","required":false,"visibility":"user-or-llm","description":"Results per page, max 100 (e.g., 25, 50, 100)"}}},"appconfig_create_application":{"id":"appconfig_create_application","name":"AppConfig Create Application","description":"Create an application in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the application to create"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the application"}}},"appconfig_create_configuration_profile":{"id":"appconfig_create_configuration_profile","name":"AppConfig Create Configuration Profile","description":"Create a configuration profile in an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to create the configuration profile in"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the configuration profile"},"locationUri":{"type":"string","required":true,"visibility":"user-or-llm","description":"Where the configuration is stored. Use \\"hosted\\" for AppConfig-hosted configurations, or an SSM/S3 URI"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the configuration profile"},"retrievalRoleArn":{"type":"string","required":false,"visibility":"user-or-llm","description":"ARN of an IAM role to retrieve the configuration (required for non-hosted URIs)"},"type":{"type":"string","required":false,"visibility":"user-or-llm","description":"Profile type: AWS.Freeform (default) or AWS.AppConfig.FeatureFlags"}}},"appconfig_create_environment":{"id":"appconfig_create_environment","name":"AppConfig Create Environment","description":"Create an environment for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to create the environment in"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the environment to create"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the environment"}}},"appconfig_create_hosted_configuration_version":{"id":"appconfig_create_hosted_configuration_version","name":"AppConfig Create Hosted Configuration Version","description":"Create a new hosted configuration version for an AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to add the version to"},"content":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration content (e.g., a JSON or YAML document)"},"contentType":{"type":"string","required":true,"visibility":"user-or-llm","description":"Content type of the configuration (e.g., application/json, text/plain)"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the configuration version"},"latestVersionNumber":{"type":"number","required":false,"visibility":"user-or-llm","description":"The version number of the latest version, used for optimistic concurrency"},"versionLabel":{"type":"string","required":false,"visibility":"user-or-llm","description":"A user-defined label for the configuration version"}}},"appconfig_delete_application":{"id":"appconfig_delete_application","name":"AppConfig Delete Application","description":"Delete an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to delete"}}},"appconfig_delete_configuration_profile":{"id":"appconfig_delete_configuration_profile","name":"AppConfig Delete Configuration Profile","description":"Delete an AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to delete"}}},"appconfig_delete_environment":{"id":"appconfig_delete_environment","name":"AppConfig Delete Environment","description":"Delete an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to delete"}}},"appconfig_delete_hosted_configuration_version":{"id":"appconfig_delete_hosted_configuration_version","name":"AppConfig Delete Hosted Configuration Version","description":"Delete a specific hosted configuration version from an AppConfig profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID that owns the version"},"versionNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The version number to delete"}}},"appconfig_get_application":{"id":"appconfig_get_application","name":"AppConfig Get Application","description":"Get details about a single AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to retrieve"}}},"appconfig_get_configuration":{"id":"appconfig_get_configuration","name":"AppConfig Get Configuration","description":"Retrieve the latest deployed configuration for an AppConfig application, environment, and profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID or name to retrieve configuration for"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID or name to retrieve configuration for"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID or name to retrieve"}}},"appconfig_get_configuration_profile":{"id":"appconfig_get_configuration_profile","name":"AppConfig Get Configuration Profile","description":"Get details about a single AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to retrieve"}}},"appconfig_get_deployment":{"id":"appconfig_get_deployment","name":"AppConfig Get Deployment","description":"Get details about a specific AWS AppConfig deployment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployment"},"deploymentNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The sequence number of the deployment"}}},"appconfig_get_environment":{"id":"appconfig_get_environment","name":"AppConfig Get Environment","description":"Get details about a single AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to retrieve"}}},"appconfig_get_hosted_configuration_version":{"id":"appconfig_get_hosted_configuration_version","name":"AppConfig Get Hosted Configuration Version","description":"Retrieve a specific hosted configuration version from an AppConfig profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to read the version from"},"versionNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The version number to retrieve"}}},"appconfig_list_applications":{"id":"appconfig_list_applications","name":"AppConfig List Applications","description":"List applications in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of applications to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_configuration_profiles":{"id":"appconfig_list_configuration_profiles","name":"AppConfig List Configuration Profiles","description":"List configuration profiles for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profiles"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of configuration profiles to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_deployment_strategies":{"id":"appconfig_list_deployment_strategies","name":"AppConfig List Deployment Strategies","description":"List deployment strategies available in AWS AppConfig","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of deployment strategies to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_deployments":{"id":"appconfig_list_deployments","name":"AppConfig List Deployments","description":"List deployments for an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployments"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployments"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of deployments to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_environments":{"id":"appconfig_list_environments","name":"AppConfig List Environments","description":"List environments for an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environments"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of environments to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_list_hosted_configuration_versions":{"id":"appconfig_list_hosted_configuration_versions","name":"AppConfig List Hosted Configuration Versions","description":"List hosted configuration versions for an AWS AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to list versions for"},"maxResults":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of versions to return (1-50)"},"nextToken":{"type":"string","required":false,"visibility":"user-or-llm","description":"Pagination token from a previous response"}}},"appconfig_start_deployment":{"id":"appconfig_start_deployment","name":"AppConfig Start Deployment","description":"Start deploying a configuration version to an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to deploy in"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to deploy to"},"deploymentStrategyId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The deployment strategy ID to use"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to deploy"},"configurationVersion":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration version to deploy"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"Description of the deployment"}}},"appconfig_stop_deployment":{"id":"appconfig_stop_deployment","name":"AppConfig Stop Deployment","description":"Stop an in-progress AWS AppConfig deployment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID of the deployment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID of the deployment"},"deploymentNumber":{"type":"number","required":true,"visibility":"user-or-llm","description":"The sequence number of the deployment to stop"}}},"appconfig_update_application":{"id":"appconfig_update_application","name":"AppConfig Update Application","description":"Update the name or description of an AWS AppConfig application","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the application"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the application"}}},"appconfig_update_configuration_profile":{"id":"appconfig_update_configuration_profile","name":"AppConfig Update Configuration Profile","description":"Update the name, description, or retrieval role of an AppConfig configuration profile","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the configuration profile"},"configurationProfileId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The configuration profile ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the configuration profile"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the configuration profile"},"retrievalRoleArn":{"type":"string","required":false,"visibility":"user-or-llm","description":"New ARN of the IAM role used to retrieve the configuration"}}},"appconfig_update_environment":{"id":"appconfig_update_environment","name":"AppConfig Update Environment","description":"Update the name or description of an AWS AppConfig environment","version":"1.0","params":{"region":{"type":"string","required":true,"visibility":"user-only","description":"AWS region (e.g., us-east-1)"},"accessKeyId":{"type":"string","required":true,"visibility":"user-only","description":"AWS access key ID"},"secretAccessKey":{"type":"string","required":true,"visibility":"user-only","description":"AWS secret access key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The application ID that owns the environment"},"environmentId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The environment ID to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"New name for the environment"},"description":{"type":"string","required":false,"visibility":"user-or-llm","description":"New description for the environment"}}},"arxiv_get_author_papers":{"id":"arxiv_get_author_papers","name":"ArXiv Get Author Papers","description":"Search for papers by a specific author on ArXiv.","version":"1.0.0","params":{"authorName":{"type":"string","required":true,"visibility":"user-or-llm","description":"Author name to search for"},"maxResults":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 10, max: 2000)"}}},"arxiv_get_paper":{"id":"arxiv_get_paper","name":"ArXiv Get Paper","description":"Get detailed information about a specific ArXiv paper by its ID.","version":"1.0.0","params":{"paperId":{"type":"string","required":true,"visibility":"user-or-llm","description":"ArXiv paper ID (e.g., \\"1706.03762\\")"}}},"arxiv_search":{"id":"arxiv_search","name":"ArXiv Search","description":"Search for academic papers on ArXiv by keywords, authors, titles, or other fields.","version":"1.0.0","params":{"searchQuery":{"type":"string","required":true,"visibility":"user-or-llm","description":"The search query to execute"},"searchField":{"type":"string","required":false,"visibility":"user-only","description":"Field to search in: all, ti (title), au (author), abs (abstract), co (comment), jr (journal), cat (category), rn (report number)"},"maxResults":{"type":"number","required":false,"visibility":"user-only","description":"Maximum number of results to return (default: 10, max: 2000)"},"sortBy":{"type":"string","required":false,"visibility":"user-only","description":"Sort by: relevance, lastUpdatedDate, submittedDate (default: relevance)"},"sortOrder":{"type":"string","required":false,"visibility":"user-only","description":"Sort order: ascending, descending (default: descending)"}}},"asana_add_comment":{"id":"asana_add_comment","name":"Asana Add Comment","description":"Add a comment (story) to an Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana task GID (numeric string)"},"text":{"type":"string","required":true,"visibility":"user-or-llm","description":"The text content of the comment"}},"oauth":{"required":true,"provider":"asana"}},"asana_add_followers":{"id":"asana_add_followers","name":"Asana Add Followers","description":"Add one or more followers to an Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana task (numeric string)"},"followers":{"type":"array","required":true,"visibility":"user-or-llm","description":"Array of user GIDs to add as followers to the task"}},"oauth":{"required":true,"provider":"asana"}},"asana_create_project":{"id":"asana_create_project","name":"Asana Create Project","description":"Create a new project in an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) where the project will be created"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the project"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the project"}},"oauth":{"required":true,"provider":"asana"}},"asana_create_section":{"id":"asana_create_section","name":"Asana Create Section","description":"Create a new section in an Asana project","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana project (numeric string) to add the section to"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the section"}},"oauth":{"required":true,"provider":"asana"}},"asana_create_subtask":{"id":"asana_create_subtask","name":"Asana Create Subtask","description":"Create a subtask under an existing Asana task","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the parent Asana task (numeric string)"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the subtask"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the subtask"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"User GID to assign the subtask to"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"}},"asana_create_task":{"id":"asana_create_task","name":"Asana Create Task","description":"Create a new task in Asana","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) where the task will be created"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"Name of the task"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Notes or description for the task"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"User GID to assign the task to"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"}},"asana_delete_task":{"id":"asana_delete_task","name":"Asana Delete Task","description":"Delete an Asana task by its GID (moves it to the trash)","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana task to delete (numeric string)"}},"oauth":{"required":true,"provider":"asana"}},"asana_get_project":{"id":"asana_get_project","name":"Asana Get Project","description":"Retrieve a single Asana project by its GID","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana project GID (numeric string) to retrieve"}},"oauth":{"required":true,"provider":"asana"}},"asana_get_projects":{"id":"asana_get_projects","name":"Asana Get Projects","description":"Retrieve all projects from an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to retrieve projects from"}},"oauth":{"required":true,"provider":"asana"}},"asana_get_task":{"id":"asana_get_task","name":"Asana Get Task","description":"Retrieve a single task by GID or get multiple tasks with filters","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":false,"visibility":"user-or-llm","description":"The globally unique identifier (GID) of the task. If not provided, will get multiple tasks."},"workspace":{"type":"string","required":false,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to filter tasks (required when not using taskGid)"},"project":{"type":"string","required":false,"visibility":"user-or-llm","description":"Asana project GID (numeric string) to filter tasks"},"limit":{"type":"number","required":false,"visibility":"user-or-llm","description":"Maximum number of tasks to return (default: 50)"}},"oauth":{"required":true,"provider":"asana"}},"asana_list_sections":{"id":"asana_list_sections","name":"Asana List Sections","description":"List all sections in an Asana project","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"projectGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"GID of the Asana project (numeric string) to list sections from"}},"oauth":{"required":true,"provider":"asana"}},"asana_list_workspaces":{"id":"asana_list_workspaces","name":"Asana List Workspaces","description":"List all Asana workspaces and organizations the authenticated user belongs to","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"}},"oauth":{"required":true,"provider":"asana"}},"asana_search_tasks":{"id":"asana_search_tasks","name":"Asana Search Tasks","description":"Search for tasks in an Asana workspace","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"workspace":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana workspace GID (numeric string) to search tasks in"},"text":{"type":"string","required":false,"visibility":"user-or-llm","description":"Text to search for in task names"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"Filter tasks by assignee user GID"},"projects":{"type":"array","required":false,"visibility":"user-or-llm","description":"Array of Asana project GIDs (numeric strings) to filter tasks by"},"completed":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Filter by completion status"}},"oauth":{"required":true,"provider":"asana"}},"asana_update_task":{"id":"asana_update_task","name":"Asana Update Task","description":"Update an existing task in Asana","version":"1.0.0","params":{"accessToken":{"type":"string","required":true,"visibility":"hidden","description":"OAuth access token for Asana"},"taskGid":{"type":"string","required":true,"visibility":"user-or-llm","description":"Asana task GID (numeric string) of the task to update"},"name":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated name for the task"},"notes":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated notes or description for the task"},"assignee":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated assignee user GID"},"completed":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Mark task as completed or not completed"},"due_on":{"type":"string","required":false,"visibility":"user-or-llm","description":"Updated due date in YYYY-MM-DD format"}},"oauth":{"required":true,"provider":"asana"}},"ashby_add_candidate_tag":{"id":"ashby_add_candidate_tag","name":"Ashby Add Candidate Tag","description":"Adds a tag to a candidate in Ashby and returns the updated candidate.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to add the tag to"},"tagId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the tag to add"}}},"ashby_anonymize_candidate":{"id":"ashby_anonymize_candidate","name":"Ashby Anonymize Candidate","description":"Strips personally identifiable information from a candidate in Ashby. This does not delete the candidate - the record and its applications remain, with the PII removed. Ashby exposes no candidate deletion endpoint; true deletion is UI-only, restricted by role, and limited to a 10-day window. Requires the candidatesWrite permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"UUID of the candidate to anonymize"}}},"ashby_change_application_source":{"id":"ashby_change_application_source","name":"Ashby Change Application Source","description":"Changes the source attributed to an existing application, so programmatically created applications report correctly on the recruiting side. Requires the candidatesWrite permission.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"UUID of the application whose source should change"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to attribute the application to, as returned by List Sources. Omit only when unsetSource is true."},"unsetSource":{"type":"boolean","required":false,"visibility":"user-or-llm","description":"Set true to deliberately clear the application source. Required to unset, so that a missing or empty sourceId cannot wipe attribution by accident."}}},"ashby_change_application_stage":{"id":"ashby_change_application_stage","name":"Ashby Change Application Stage","description":"Moves an application to a different interview stage. Requires an archive reason when moving to an Archived stage.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"applicationId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the application to update the stage of"},"interviewStageId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the interview stage to move the application to"},"archiveReasonId":{"type":"string","required":false,"visibility":"user-or-llm","description":"Archive reason UUID. Required when moving to an Archived stage, ignored otherwise"}}},"ashby_create_application":{"id":"ashby_create_application","name":"Ashby Create Application","description":"Creates a new application for a candidate on a job. Optionally specify interview plan, stage, source, and credited user.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to consider for the job"},"jobId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the job to consider the candidate for"},"interviewPlanId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the interview plan to use (defaults to the job default plan)"},"interviewStageId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the interview stage to place the application in (defaults to first Lead stage)"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to set on the application"},"creditedToUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the user the application is credited to"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"ISO 8601 timestamp to set as the application creation date (defaults to now)"}}},"ashby_create_candidate":{"id":"ashby_create_candidate","name":"Ashby Create Candidate","description":"Creates a new candidate record in Ashby.","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"name":{"type":"string","required":true,"visibility":"user-or-llm","description":"The candidate full name"},"email":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary email address for the candidate"},"phoneNumber":{"type":"string","required":false,"visibility":"user-or-llm","description":"Primary phone number for the candidate"},"linkedInUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"LinkedIn profile URL"},"githubUrl":{"type":"string","required":false,"visibility":"user-or-llm","description":"GitHub profile URL"},"website":{"type":"string","required":false,"visibility":"user-or-llm","description":"Personal website URL"},"sourceId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the source to attribute the candidate to"},"creditedToUserId":{"type":"string","required":false,"visibility":"user-or-llm","description":"UUID of the Ashby user to credit with sourcing this candidate"},"createdAt":{"type":"string","required":false,"visibility":"user-or-llm","description":"Backdated creation timestamp in ISO 8601 (e.g. 2024-01-01T00:00:00Z). Defaults to now."},"alternateEmailAddresses":{"type":"json","required":false,"visibility":"user-or-llm","description":"Array of additional email address strings to add to the candidate, e.g. [\\"a@x.com\\",\\"b@y.com\\"]"}}},"ashby_create_note":{"id":"ashby_create_note","name":"Ashby Create Note","description":"Creates a note on a candidate in Ashby. Supports plain text and HTML content (bold, italic, underline, links, lists, code).","version":"1.0.0","params":{"apiKey":{"type":"string","required":true,"visibility":"user-only","description":"Ashby API Key"},"candidateId":{"type":"string","required":true,"visibility":"user-or-llm","description":"The UUID of the candidate to add the note to"},"note":{"type":"string","required":true,"visibility":"user-or-llm","description":"The note content. If noteType is text/html, supports: , , , ,